Skip to content

Commit 79c603a

Browse files
committed
fix: correctness fixes and performance improvements to custom json converters
1 parent aea11ce commit 79c603a

File tree

5 files changed

+75
-40
lines changed

5 files changed

+75
-40
lines changed

src/Octokit.Webhooks/Converter/DateTimeOffsetConverter.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@ private static DateTimeOffset HandleString(Utf8JsonReader reader)
2222
{
2323
var stringValue = reader.GetString() ?? throw new InvalidOperationException("Cannot parse a String JsonToken with a null value");
2424

25-
return DateTimeOffset.Parse(stringValue, CultureInfo.InvariantCulture);
25+
var span = stringValue.AsSpan();
26+
if (!DateTimeOffset.TryParse(span, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result))
27+
{
28+
throw new JsonException($"Unable to parse '{stringValue}' as DateTimeOffset");
29+
}
30+
31+
return result;
2632
}
2733

2834
private static DateTimeOffset HandleNumber(Utf8JsonReader reader) => DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64());

src/Octokit.Webhooks/Converter/StringEnumConverter.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ public sealed class StringEnumConverter<TEnum> : JsonConverter<StringEnum<TEnum>
1111
public override StringEnum<TEnum> Read(
1212
ref Utf8JsonReader reader,
1313
Type typeToConvert,
14-
JsonSerializerOptions options) => new(reader.GetString()!);
14+
JsonSerializerOptions options)
15+
{
16+
var stringValue = reader.GetString() ?? throw new JsonException("Unexpected null value when deserializing StringEnum.");
17+
18+
return new(stringValue);
19+
}
1520

1621
public override void Write(Utf8JsonWriter writer, StringEnum<TEnum> value, JsonSerializerOptions options) =>
1722
JsonSerializer.Serialize(writer, value.StringValue, options);

src/Octokit.Webhooks/Converter/StringEnumEnumerableConverter.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,16 @@ private static List<StringEnum<TEnum>> ReadInternal(ref Utf8JsonReader reader)
2727
throw new JsonException("Unexpected null value.");
2828
}
2929

30+
if (reader.TokenType != JsonTokenType.StartArray)
31+
{
32+
throw new JsonException("Expected JSON array.");
33+
}
34+
3035
var returnValue = new List<StringEnum<TEnum>>();
3136

32-
while (reader.TokenType != JsonTokenType.EndArray)
37+
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
3338
{
34-
if (reader.TokenType != JsonTokenType.StartArray)
35-
{
36-
returnValue.Add(JsonSerializer.Deserialize<StringEnum<TEnum>>(ref reader, Options)!);
37-
}
38-
39-
_ = reader.Read();
39+
returnValue.Add(JsonSerializer.Deserialize<StringEnum<TEnum>>(ref reader, Options)!);
4040
}
4141

4242
return returnValue;

src/Octokit.Webhooks/Converter/WebhookConverter.cs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,64 @@
11
namespace Octokit.Webhooks.Converter;
22

3+
using System.Collections.Concurrent;
34
using System.Linq;
45
using System.Reflection;
56

67
[PublicAPI]
78
public class WebhookConverter<T> : JsonConverter<T>
89
where T : WebhookEvent
910
{
11+
private static readonly ConcurrentDictionary<Type, Dictionary<string, Type>> TypeCache = new();
1012
private readonly Dictionary<string, Type> types;
1113

1214
public WebhookConverter()
1315
{
1416
var type = typeof(T);
15-
this.types = this.GetType().Assembly.GetTypes()
16-
.Where(x => type.IsAssignableFrom(x) && x is { IsClass: true, IsAbstract: false } &&
17-
x.GetCustomAttribute<WebhookActionTypeAttribute>() is not null)
18-
.ToDictionary(
19-
y => y.GetCustomAttribute<WebhookActionTypeAttribute>()!.ActionType,
20-
y => y);
17+
this.types = TypeCache.GetOrAdd(type, static t =>
18+
t.Assembly.GetTypes()
19+
.Where(x => t.IsAssignableFrom(x) && x is { IsClass: true, IsAbstract: false } &&
20+
x.GetCustomAttribute<WebhookActionTypeAttribute>() is not null)
21+
.ToDictionary(
22+
y => y.GetCustomAttribute<WebhookActionTypeAttribute>()!.ActionType,
23+
y => y));
2124
}
2225

2326
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
2427
{
2528
if (reader.TokenType != JsonTokenType.StartObject)
2629
{
27-
throw new JsonException();
30+
throw new JsonException($"Expected JSON object but found {reader.TokenType}");
2831
}
2932

3033
using var jsonDocument = JsonDocument.ParseValue(ref reader);
3134
if (!jsonDocument.RootElement.TryGetProperty("action", out var action))
3235
{
33-
throw new JsonException();
36+
throw new JsonException("Webhook payload is missing required 'action' property");
3437
}
3538

3639
Type? type = null;
37-
3840
var actionValue = action.GetString();
3941

4042
if (actionValue is not null && this.types.TryGetValue(actionValue, out var payloadType))
4143
{
4244
type = payloadType;
4345
}
4446

45-
// repository_dispatch events can have custom user-defined actions values
47+
// Special case: repository_dispatch events can have custom user-defined action values
4648
if (type == null && typeToConvert == typeof(Events.RepositoryDispatchEvent))
4749
{
50+
if (string.IsNullOrEmpty(actionValue))
51+
{
52+
throw new JsonException("Repository dispatch event is missing action value");
53+
}
54+
4855
type = typeof(Events.RepositoryDispatch.RepositoryDispatchCustomEvent);
4956
}
5057

5158
if (type is null)
5259
{
53-
throw new JsonException();
60+
var availableActions = string.Join(", ", this.types.Keys.OrderBy(k => k));
61+
throw new JsonException($"Unknown action '{actionValue}' for webhook event type '{typeof(T).Name}'. Available actions: {availableActions}");
5462
}
5563

5664
var jsonObject = jsonDocument.RootElement.GetRawText();

src/Octokit.Webhooks/Extensions/StringEnum.cs

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace Octokit.Webhooks.Extensions;
22

33
using System;
4+
using System.Collections.Concurrent;
45
using System.Globalization;
56
using System.Linq;
67
using System.Runtime.Serialization;
@@ -10,6 +11,8 @@ namespace Octokit.Webhooks.Extensions;
1011
public sealed record StringEnum<TEnum>
1112
where TEnum : struct
1213
{
14+
private static readonly ConcurrentDictionary<TEnum, string> EnumToStringCache = new();
15+
private static readonly ConcurrentDictionary<string, TEnum> StringToEnumCache = new();
1316
private TEnum? parsedValue;
1417

1518
public StringEnum(string stringValue)
@@ -74,31 +77,44 @@ private TEnum ParseValue()
7477
throw GetArgumentException(this.StringValue);
7578
}
7679

77-
private static string ToEnumString(TEnum type)
78-
{
79-
var enumType = typeof(TEnum);
80-
var name = Enum.GetName(enumType, type);
81-
if (name is not null)
80+
private static string ToEnumString(TEnum type) =>
81+
EnumToStringCache.GetOrAdd(type, static enumValue =>
8282
{
83-
var enumMemberAttribute = ((EnumMemberAttribute[])enumType.GetField(name)!.GetCustomAttributes(typeof(EnumMemberAttribute), true)).Single();
84-
return enumMemberAttribute.Value!;
85-
}
83+
var enumType = typeof(TEnum);
84+
var name = Enum.GetName(enumType, enumValue);
85+
if (name is not null)
86+
{
87+
var field = enumType.GetField(name)!;
88+
var enumMemberAttribute = field.GetCustomAttributes(typeof(EnumMemberAttribute), true)
89+
.Cast<EnumMemberAttribute>()
90+
.FirstOrDefault();
91+
92+
if (enumMemberAttribute?.Value is not null)
93+
{
94+
return enumMemberAttribute.Value;
95+
}
96+
}
8697

87-
throw new ArgumentException(type.ToString());
88-
}
98+
throw new ArgumentException($"Enum value '{enumValue}' does not have a valid EnumMemberAttribute");
99+
});
89100

90-
private static TEnum ToEnum(string str)
91-
{
92-
var enumType = typeof(TEnum);
93-
foreach (var name in Enum.GetNames(enumType))
101+
private static TEnum ToEnum(string str) =>
102+
StringToEnumCache.GetOrAdd(str, static stringValue =>
94103
{
95-
var enumMemberAttribute = ((EnumMemberAttribute[])enumType.GetField(name)!.GetCustomAttributes(typeof(EnumMemberAttribute), true)).Single();
96-
if (enumMemberAttribute.Value == str)
104+
var enumType = typeof(TEnum);
105+
foreach (var name in Enum.GetNames(enumType))
97106
{
98-
return (TEnum)Enum.Parse(enumType, name);
107+
var field = enumType.GetField(name)!;
108+
var enumMemberAttribute = field.GetCustomAttributes(typeof(EnumMemberAttribute), true)
109+
.Cast<EnumMemberAttribute>()
110+
.FirstOrDefault();
111+
112+
if (enumMemberAttribute?.Value == stringValue)
113+
{
114+
return (TEnum)Enum.Parse(enumType, name);
115+
}
99116
}
100-
}
101117

102-
throw new ArgumentException(str);
103-
}
118+
throw new ArgumentException($"String value '{stringValue}' is not a valid '{enumType.Name}' enum value");
119+
});
104120
}

0 commit comments

Comments
 (0)