Skip to content

Commit

Permalink
fix ProblemDetail Deserialize, add a QueryString IEnumerable overload
Browse files Browse the repository at this point in the history
  • Loading branch information
pwelter34 committed Aug 8, 2024
1 parent 15a527b commit 5ca67c4
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 116 deletions.
47 changes: 36 additions & 11 deletions src/FluentRest/ProblemDetails.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using System.Text.Json.Serialization;

#nullable enable

namespace FluentRest;

/// <summary>
/// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807.
/// </summary>
[JsonConverter(typeof(ProblemDetailsConverter))]
public class ProblemDetails
{
/// <summary>
Expand All @@ -14,43 +15,67 @@ public class ProblemDetails
public const string ContentType = "application/problem+json";

/// <summary>
/// A URI reference that identifies the problem type.
/// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when
/// dereferenced, it provide human-readable documentation for the problem type
/// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be
/// "about:blank".
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyOrder(-5)]
[JsonPropertyName("type")]
public string Type { get; set; }
public string? Type { get; set; }

/// <summary>
/// A short, human-readable summary of the problem type.
/// A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence
/// of the problem, except for purposes of localization(e.g., using proactive content negotiation;
/// see[RFC7231], Section 3.4).
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyOrder(-4)]
[JsonPropertyName("title")]
public string Title { get; set; }
public string? Title { get; set; }

/// <summary>
/// The HTTP status code generated by the origin server for this occurrence of the problem.
/// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyOrder(-3)]
[JsonPropertyName("status")]
public int? Status { get; set; }

/// <summary>
/// A human-readable explanation specific to this occurrence of the problem.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyOrder(-2)]
[JsonPropertyName("detail")]
public string Detail { get; set; }
public string? Detail { get; set; }

/// <summary>
/// A URI reference that identifies the specific occurrence of the problem.
/// A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyOrder(-1)]
[JsonPropertyName("instance")]
public string Instance { get; set; }
public string? Instance { get; set; }

/// <summary>
/// Gets the validation errors associated with this instance of problem details
/// </summary>
[JsonPropertyName("errors")]
public IDictionary<string, string[]> Errors { get; set; } = new Dictionary<string, string[]>(StringComparer.Ordinal);

/// <summary>
/// Problem type definitions MAY extend the problem details object with additional members.
/// Gets the <see cref="IDictionary{TKey, TValue}"/> for extension members.
/// <para>
/// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as
/// other members of a problem type.
/// </para>
/// </summary>
/// <remarks>
/// The round-tripping behavior for <see cref="Extensions"/> is determined by the implementation of the Input \ Output formatters.
/// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters.
/// </remarks>
[JsonExtensionData]
public IDictionary<string, object> Extensions { get; } = new Dictionary<string, object>(StringComparer.Ordinal);
public IDictionary<string, object?> Extensions { get; set; } = new Dictionary<string, object?>(StringComparer.Ordinal);
}
105 changes: 0 additions & 105 deletions src/FluentRest/ProblemDetailsConverter.cs

This file was deleted.

28 changes: 28 additions & 0 deletions src/FluentRest/QueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,34 @@ public TBuilder QueryString<TValue>(string name, TValue value)
return QueryString(name, v);
}

/// <summary>
/// Appends the specified <paramref name="name" /> and <paramref name="values" /> to the request Uri.
/// </summary>
/// <typeparam name="TValue">The type of the value.</typeparam>
/// <param name="name">The query parameter name.</param>
/// <param name="values">The query parameter values.</param>
/// <returns>
/// A fluent request builder.
/// </returns>
/// <exception cref="System.ArgumentNullException"><paramref name="name" /> is <see langword="null" />.</exception>
public TBuilder QueryString<TValue>(string name, IEnumerable<TValue> values)
{
if (name == null)
throw new ArgumentNullException(nameof(name));

if (values == null)
return this as TBuilder;

foreach (var value in values)
{
var v = value != null ? value.ToString() : string.Empty;
QueryString(name, v);
}

return this as TBuilder;
}


/// <summary>
/// Appends the specified <paramref name="name"/> and <paramref name="value"/> to the request Uri if the specified <paramref name="condition"/> is true.
/// </summary>
Expand Down
25 changes: 25 additions & 0 deletions src/FluentRest/UrlBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,31 @@ public UrlBuilder AppendQuery<TValue>(string name, TValue value)
return AppendQuery(name, v);
}

/// <summary>
/// Appends the query string name and values to the current url.
/// </summary>
/// <typeparam name="TValue">The type of the value.</typeparam>
/// <param name="name">The query string name.</param>
/// <param name="values">The query string values.</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException">name is <c>null</c></exception>
public UrlBuilder AppendQuery<TValue>(string name, IEnumerable<TValue> values)
{
if (name == null)
throw new ArgumentNullException(nameof(name));

if (values == null)
return this;

foreach (var value in values)
{
var v = value != null ? value.ToString() : string.Empty;
AppendQuery(name, v);
}

return this;
}

/// <summary>
/// Conditionally appends the query string name and value to the current url if the specified <paramref name="condition" /> is <c>true</c>.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions test/FluentRest.Tests/FluentRest.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>Latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\FluentRest\FluentRest.csproj" />
Expand Down
80 changes: 80 additions & 0 deletions test/FluentRest.Tests/ProblemDetailsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.Text.Json;
using System.Text.Json.Serialization;

using FluentAssertions;

using Xunit;

namespace FluentRest.Tests;

public class ProblemDetailsTests
{
[Fact]
public void DeserializeBadRequestValidation()
{
var options = CreateOptions();

var json = @"{
""type"": ""https://tools.ietf.org/html/rfc9110#section-15.5.1"",
""title"": ""One or more validation errors occurred."",
""status"": 400,
""errors"": {
""caseManagerIds"": [
""The value 'System.Collections.Generic.List`1[System.Int32]' is not valid.""
]
},
""traceId"": ""00-894a84e8e9621fb8fcabcb2911e6d51c-5a4777d830f30cff-00""
}";
var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(json, options);
problemDetails.Should().NotBeNull();
problemDetails.Title.Should().Be("One or more validation errors occurred.");
problemDetails.Status.Should().Be(400);
}



[Fact]
public void DeserializeServerError()
{
var options = CreateOptions();

var json = @"{
""type"": ""https://tools.ietf.org/html/rfc9110#section-15.5.1"",
""title"": ""One or more errors occurred."",
""status"": 500,
""traceId"": ""00-894a84e8e9621fb8fcabcb2911e6d51c-5a4777d830f30cff-00""
}";
var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(json, options);
problemDetails.Should().NotBeNull();
problemDetails.Title.Should().Be("One or more errors occurred.");
problemDetails.Status.Should().Be(500);
}

[Fact]
public void DeserializeServerErrorExtra()
{
var options = CreateOptions();

var json = @"{
""type"": ""https://tools.ietf.org/html/rfc9110#section-15.5.1"",
""title"": ""One or more errors occurred."",
""status"": 500,
""exception"": ""this is an exception"",
""traceId"": ""00-894a84e8e9621fb8fcabcb2911e6d51c-5a4777d830f30cff-00""
}";
var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(json, options);
problemDetails.Should().NotBeNull();
problemDetails.Title.Should().Be("One or more errors occurred.");
problemDetails.Status.Should().Be(500);
}

private static JsonSerializerOptions CreateOptions()
{
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault;
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
options.TypeInfoResolverChain.Add(ProblemDetailsJsonContext.Default);
return options;
}
}
14 changes: 14 additions & 0 deletions test/FluentRest.Tests/QueryBuilderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ public void QueryStringMultipleValue()
Assert.Equal("http://test.com/?Test=Test1&Test=Test2", uri.ToString());
}

[Fact]
public void QueryStringMultipleList()
{
var request = new HttpRequestMessage();
var builder = new QueryBuilder(request);

builder.BaseUri("http://test.com/");
builder.QueryString("Test", ["Test1", "Test2"]);

var uri = request.GetUrlBuilder();

Assert.Equal("http://test.com/?Test=Test1&Test=Test2", uri.ToString());
}

[Fact]
public void HeaderSingleValue()
{
Expand Down

0 comments on commit 5ca67c4

Please sign in to comment.