Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion codegen/internal/generator/templates/model_enum.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace {{ .Namespace }};
using System.Runtime.Serialization;
using System.Text.Json.Serialization;

[JsonConverter(typeof(JsonStringEnumConverter))]
[JsonConverter(typeof(EnumMemberJsonConverterFactory))]
public enum {{ .Name }}
{
{{- range .EnumValues }}
Expand Down
24 changes: 24 additions & 0 deletions src/SumUp.Tests/RequestBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using SumUp.Http;
using Xunit;

Expand Down Expand Up @@ -47,4 +49,26 @@ public void Build_EmitsExplicitNullOptionalQuery()

Assert.Equal("https://api.sumup.com/v0.1/items?status=null", request.RequestUri!.AbsoluteUri);
}

[Fact]
public void CreateContent_SerializesEnumMemberValues()
{
using var httpClient = new HttpClient { BaseAddress = new Uri("https://api.sumup.com") };
var apiClient = new ApiClient(httpClient, new SumUpClientOptions());
var request = new CheckoutCreateRequest
{
CheckoutReference = "test-123",
Amount = 10.0f,
Currency = Currency.Eur,
MerchantCode = "merchant-code",
Description = "Test order",
};

using var content = apiClient.CreateContent(request, "application/json");
using var stream = content.ReadAsStream();
using var reader = new StreamReader(stream, Encoding.UTF8);
var body = reader.ReadToEnd();

Assert.Contains("\"currency\":\"EUR\"", body);
}
}
9 changes: 9 additions & 0 deletions src/SumUp.Tests/SumUpClientOptionsTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
Expand Down Expand Up @@ -51,4 +52,12 @@ public async Task AccessTokenProvider_WinsOverEnvironment()
Environment.SetEnvironmentVariable(variable, originalValue);
}
}

[Fact]
public void JsonSerializer_UsesEnumMemberValueForStandaloneEnums()
{
var json = JsonSerializer.Serialize(Currency.Eur);

Assert.Equal("\"EUR\"", json);
}
}
207 changes: 207 additions & 0 deletions src/SumUp/EnumMemberJsonConverterFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SumUp;

/// <summary>
/// Serializes enums using <see cref="EnumMemberAttribute.Value"/> when present.
/// Replace this with built-in enum member name support once the SDK can target a newer .NET runtime across all TFMs.
/// </summary>
public sealed class EnumMemberJsonConverterFactory : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
return enumType.IsEnum;
}

/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var underlyingType = Nullable.GetUnderlyingType(typeToConvert);
var enumType = underlyingType ?? typeToConvert;
var converterType = underlyingType is null
? typeof(EnumMemberJsonConverter<>).MakeGenericType(enumType)
: typeof(NullableEnumMemberJsonConverter<>).MakeGenericType(enumType);

return (JsonConverter)Activator.CreateInstance(converterType)!;
}

private sealed class NullableEnumMemberJsonConverter<TEnum> : JsonConverter<TEnum?>
where TEnum : struct, Enum
{
private static readonly EnumMemberJsonConverter<TEnum> InnerConverter = new();

public override TEnum? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}

return InnerConverter.Read(ref reader, typeof(TEnum), options);
}

public override void Write(Utf8JsonWriter writer, TEnum? value, JsonSerializerOptions options)
{
if (value is null)
{
writer.WriteNullValue();
return;
}

InnerConverter.Write(writer, value.Value, options);
}
}

private sealed class EnumMemberJsonConverter<TEnum> : JsonConverter<TEnum>
where TEnum : struct, Enum
{
private static readonly IReadOnlyDictionary<TEnum, string> WriteMappings = BuildWriteMappings();
private static readonly IReadOnlyDictionary<string, TEnum> ReadMappings = BuildReadMappings(WriteMappings);

public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
var value = reader.GetString();
if (value is not null)
{
if (ReadMappings.TryGetValue(value, out var mapped))
{
return mapped;
}

if (Enum.TryParse<TEnum>(value, ignoreCase: true, out var parsed))
{
return parsed;
}
}
}
else if (reader.TokenType == JsonTokenType.Number)
{
return ReadNumeric(ref reader);
}

throw new JsonException(
string.Format(
CultureInfo.InvariantCulture,
"Unable to convert value to enum type '{0}'.",
typeof(TEnum).FullName));
}

public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
{
if (WriteMappings.TryGetValue(value, out var mapped))
{
writer.WriteStringValue(mapped);
return;
}

var name = Enum.GetName(typeof(TEnum), value);
if (name is not null)
{
writer.WriteStringValue(name);
return;
}

WriteNumeric(writer, value);
}

private static Dictionary<TEnum, string> BuildWriteMappings()
{
var values = new Dictionary<TEnum, string>();
foreach (var field in typeof(TEnum).GetFields(BindingFlags.Public | BindingFlags.Static))
{
var enumValue = (TEnum)field.GetValue(null)!;
var enumMember = field.GetCustomAttribute<EnumMemberAttribute>();
values[enumValue] = enumMember?.Value ?? field.Name;
}

return values;
}

private static Dictionary<string, TEnum> BuildReadMappings(IReadOnlyDictionary<TEnum, string> writeMappings)
{
var values = new Dictionary<string, TEnum>(StringComparer.OrdinalIgnoreCase);
foreach (var pair in writeMappings)
{
values[pair.Value] = pair.Key;
values[pair.Key.ToString()] = pair.Key;
}

foreach (var value in Enum.GetValues(typeof(TEnum)).Cast<TEnum>())
{
values[value.ToString()] = value;
}

return values;
}

private static TEnum ReadNumeric(ref Utf8JsonReader reader)
{
object value = Type.GetTypeCode(Enum.GetUnderlyingType(typeof(TEnum))) switch
{
TypeCode.SByte => reader.GetSByte(),
TypeCode.Byte => reader.GetByte(),
TypeCode.Int16 => reader.GetInt16(),
TypeCode.UInt16 => reader.GetUInt16(),
TypeCode.Int32 => reader.GetInt32(),
TypeCode.UInt32 => reader.GetUInt32(),
TypeCode.Int64 => reader.GetInt64(),
TypeCode.UInt64 => reader.GetUInt64(),
_ => throw new JsonException(
string.Format(
CultureInfo.InvariantCulture,
"Enum type '{0}' has an unsupported underlying type.",
typeof(TEnum).FullName)),
};

return (TEnum)Enum.ToObject(typeof(TEnum), value);
}

private static void WriteNumeric(Utf8JsonWriter writer, TEnum value)
{
switch (Type.GetTypeCode(Enum.GetUnderlyingType(typeof(TEnum))))
{
case TypeCode.SByte:
writer.WriteNumberValue(Convert.ToSByte(value, CultureInfo.InvariantCulture));
return;
case TypeCode.Byte:
writer.WriteNumberValue(Convert.ToByte(value, CultureInfo.InvariantCulture));
return;
case TypeCode.Int16:
writer.WriteNumberValue(Convert.ToInt16(value, CultureInfo.InvariantCulture));
return;
case TypeCode.UInt16:
writer.WriteNumberValue(Convert.ToUInt16(value, CultureInfo.InvariantCulture));
return;
case TypeCode.Int32:
writer.WriteNumberValue(Convert.ToInt32(value, CultureInfo.InvariantCulture));
return;
case TypeCode.UInt32:
writer.WriteNumberValue(Convert.ToUInt32(value, CultureInfo.InvariantCulture));
return;
case TypeCode.Int64:
writer.WriteNumberValue(Convert.ToInt64(value, CultureInfo.InvariantCulture));
return;
case TypeCode.UInt64:
writer.WriteNumberValue(Convert.ToUInt64(value, CultureInfo.InvariantCulture));
return;
default:
throw new JsonException(
string.Format(
CultureInfo.InvariantCulture,
"Enum type '{0}' has an unsupported underlying type.",
typeof(TEnum).FullName));
}
}
}
}
3 changes: 1 addition & 2 deletions src/SumUp/Http/ApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;

Expand All @@ -28,7 +27,7 @@ internal ApiClient(HttpClient httpClient, SumUpClientOptions options)
{
PropertyNameCaseInsensitive = true
};
_serializerOptions.Converters.Add(new JsonStringEnumConverter());
_serializerOptions.Converters.Add(new EnumMemberJsonConverterFactory());
}

internal HttpRequestMessage CreateRequest(HttpMethod method, string pathTemplate, Action<RequestBuilder>? configure = null)
Expand Down
2 changes: 1 addition & 1 deletion src/SumUp/Models/CardType.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace SumUp;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;

[JsonConverter(typeof(JsonStringEnumConverter))]
[JsonConverter(typeof(EnumMemberJsonConverterFactory))]
public enum CardType
{
[EnumMember(Value = "ALELO")]
Expand Down
2 changes: 1 addition & 1 deletion src/SumUp/Models/Currency.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace SumUp;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;

[JsonConverter(typeof(JsonStringEnumConverter))]
[JsonConverter(typeof(EnumMemberJsonConverterFactory))]
public enum Currency
{
[EnumMember(Value = "BGN")]
Expand Down
2 changes: 1 addition & 1 deletion src/SumUp/Models/EntryMode.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace SumUp;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;

[JsonConverter(typeof(JsonStringEnumConverter))]
[JsonConverter(typeof(EnumMemberJsonConverterFactory))]
public enum EntryMode
{
[EnumMember(Value = "none")]
Expand Down
2 changes: 1 addition & 1 deletion src/SumUp/Models/EntryModeFilter.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace SumUp;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;

[JsonConverter(typeof(JsonStringEnumConverter))]
[JsonConverter(typeof(EnumMemberJsonConverterFactory))]
public enum EntryModeFilter
{
[EnumMember(Value = "BOLETO")]
Expand Down
2 changes: 1 addition & 1 deletion src/SumUp/Models/EventStatus.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace SumUp;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;

[JsonConverter(typeof(JsonStringEnumConverter))]
[JsonConverter(typeof(EnumMemberJsonConverterFactory))]
public enum EventStatus
{
[EnumMember(Value = "PENDING")]
Expand Down
2 changes: 1 addition & 1 deletion src/SumUp/Models/EventType.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace SumUp;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;

[JsonConverter(typeof(JsonStringEnumConverter))]
[JsonConverter(typeof(EnumMemberJsonConverterFactory))]
public enum EventType
{
[EnumMember(Value = "PAYOUT")]
Expand Down
2 changes: 1 addition & 1 deletion src/SumUp/Models/MembershipStatus.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace SumUp;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;

[JsonConverter(typeof(JsonStringEnumConverter))]
[JsonConverter(typeof(EnumMemberJsonConverterFactory))]
public enum MembershipStatus
{
[EnumMember(Value = "accepted")]
Expand Down
2 changes: 1 addition & 1 deletion src/SumUp/Models/PaymentType.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace SumUp;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;

[JsonConverter(typeof(JsonStringEnumConverter))]
[JsonConverter(typeof(EnumMemberJsonConverterFactory))]
public enum PaymentType
{
[EnumMember(Value = "CASH")]
Expand Down
2 changes: 1 addition & 1 deletion src/SumUp/Models/ReaderStatus.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace SumUp;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;

[JsonConverter(typeof(JsonStringEnumConverter))]
[JsonConverter(typeof(EnumMemberJsonConverterFactory))]
public enum ReaderStatus
{
[EnumMember(Value = "unknown")]
Expand Down
Loading