diff --git a/src/Octokit.Webhooks/Converter/ChangesFieldValueChangeConverter.cs b/src/Octokit.Webhooks/Converter/ChangesFieldValueChangeConverter.cs new file mode 100644 index 00000000..e903804e --- /dev/null +++ b/src/Octokit.Webhooks/Converter/ChangesFieldValueChangeConverter.cs @@ -0,0 +1,46 @@ +namespace Octokit.Webhooks.Converter; + +using Octokit.Webhooks.Models.ProjectsV2ItemEvent; + +public class ChangesFieldValueChangeConverter : JsonConverter +{ + public override ChangesFieldValueChangeBase? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader) + { + case { TokenType: JsonTokenType.StartObject }: + var changeObject = JsonSerializer.Deserialize(ref reader, options); + return changeObject; + case { TokenType: JsonTokenType.String }: + return new ChangesFieldValueScalarChange { StringValue = reader.GetString() }; + case { TokenType: JsonTokenType.Null }: + return null; + case { TokenType: JsonTokenType.Number }: + return new ChangesFieldValueScalarChange { NumericValue = reader.GetDecimal() }; + default: + throw new JsonException($"Invalid JsonTokenType {reader.TokenType}"); + } + } + + public override void Write(Utf8JsonWriter writer, ChangesFieldValueChangeBase value, JsonSerializerOptions options) + { + switch (value) + { + case ChangesFieldValueScalarChange { StringValue: not null } scalarChange: + writer.WriteStringValue(scalarChange.StringValue); + break; + case ChangesFieldValueScalarChange { NumericValue: not null } scalarChange: + writer.WriteNumberValue(scalarChange.NumericValue.Value); + break; + case ChangesFieldValueScalarChange: + writer.WriteNullValue(); + break; + case ChangesFieldValueChange change: + JsonSerializer.Serialize(writer, change, options); + break; + default: + writer.WriteNullValue(); + break; + } + } +} diff --git a/src/Octokit.Webhooks/Models/ProjectsV2ItemEvent/ChangesFieldValue.cs b/src/Octokit.Webhooks/Models/ProjectsV2ItemEvent/ChangesFieldValue.cs index 3760ed0f..29db515d 100644 --- a/src/Octokit.Webhooks/Models/ProjectsV2ItemEvent/ChangesFieldValue.cs +++ b/src/Octokit.Webhooks/Models/ProjectsV2ItemEvent/ChangesFieldValue.cs @@ -17,8 +17,8 @@ public sealed record ChangesFieldValue public long ProjectNumber { get; init; } [JsonPropertyName("from")] - public ChangesFieldValueChange From { get; init; } = null!; + public ChangesFieldValueChangeBase From { get; init; } = null!; [JsonPropertyName("to")] - public ChangesFieldValueChange To { get; init; } = null!; + public ChangesFieldValueChangeBase To { get; init; } = null!; } diff --git a/src/Octokit.Webhooks/Models/ProjectsV2ItemEvent/ChangesFieldValueChange.cs b/src/Octokit.Webhooks/Models/ProjectsV2ItemEvent/ChangesFieldValueChange.cs index 3104510e..6b8abf50 100644 --- a/src/Octokit.Webhooks/Models/ProjectsV2ItemEvent/ChangesFieldValueChange.cs +++ b/src/Octokit.Webhooks/Models/ProjectsV2ItemEvent/ChangesFieldValueChange.cs @@ -1,7 +1,7 @@ namespace Octokit.Webhooks.Models.ProjectsV2ItemEvent; [PublicAPI] -public sealed record ChangesFieldValueChange +public sealed record ChangesFieldValueChange : ChangesFieldValueChangeBase { [JsonPropertyName("id")] public string Id { get; init; } = null!; diff --git a/src/Octokit.Webhooks/Models/ProjectsV2ItemEvent/ChangesFieldValueChangeBase.cs b/src/Octokit.Webhooks/Models/ProjectsV2ItemEvent/ChangesFieldValueChangeBase.cs new file mode 100644 index 00000000..f1d743b4 --- /dev/null +++ b/src/Octokit.Webhooks/Models/ProjectsV2ItemEvent/ChangesFieldValueChangeBase.cs @@ -0,0 +1,6 @@ +namespace Octokit.Webhooks.Models.ProjectsV2ItemEvent; + +[JsonConverter(typeof(ChangesFieldValueChangeConverter))] +public abstract record ChangesFieldValueChangeBase +{ +} diff --git a/src/Octokit.Webhooks/Models/ProjectsV2ItemEvent/ChangesFieldValueScalarChange.cs b/src/Octokit.Webhooks/Models/ProjectsV2ItemEvent/ChangesFieldValueScalarChange.cs new file mode 100644 index 00000000..5577f8e1 --- /dev/null +++ b/src/Octokit.Webhooks/Models/ProjectsV2ItemEvent/ChangesFieldValueScalarChange.cs @@ -0,0 +1,9 @@ +namespace Octokit.Webhooks.Models.ProjectsV2ItemEvent; + +[PublicAPI] +public sealed record ChangesFieldValueScalarChange : ChangesFieldValueChangeBase +{ + public string? StringValue { get; init; } + + public decimal? NumericValue { get; init; } +} diff --git a/test/Octokit.Webhooks.Test/Converter/ChangesFieldValueChangeConverterTests.cs b/test/Octokit.Webhooks.Test/Converter/ChangesFieldValueChangeConverterTests.cs new file mode 100644 index 00000000..7dd1b380 --- /dev/null +++ b/test/Octokit.Webhooks.Test/Converter/ChangesFieldValueChangeConverterTests.cs @@ -0,0 +1,72 @@ +namespace Octokit.Webhooks.Test.Converter; + +using System.Text.Json; +using System.Text.Json.Serialization; +using AwesomeAssertions; +using Octokit.Webhooks.Converter; +using Octokit.Webhooks.Models.ProjectsV2ItemEvent; +using Xunit; + +public class ChangesFieldValueChangeConverterTests(ITestOutputHelper output) +{ + private readonly JsonSerializerOptions options = new() + { + WriteIndented = true, + Converters = + { + new ChangesFieldValueChangeConverter(), + }, + }; + + [Fact] + public void Roundtrip() + { + var test = new TestObject + { + AsString = new ChangesFieldValueScalarChange { StringValue = "Hello world" }, + AsNumber = new ChangesFieldValueScalarChange { NumericValue = 3.1415926m }, + AsObject = new ChangesFieldValueChange + { + Color = "color", + Description = "description", + Id = "12345", + Name = "Name", + }, + }; + + var serialized = JsonSerializer.Serialize(test, this.options); + output.WriteLine(serialized); + + var deserialized = JsonSerializer.Deserialize(serialized, this.options); + + deserialized.Should().NotBeNull(); + deserialized.AsString.Should().NotBeNull().And.BeOfType(); + deserialized.AsNumber.Should().NotBeNull().And.BeOfType(); + + (deserialized.AsString as ChangesFieldValueScalarChange)?.StringValue.Should().Be("Hello world"); + (deserialized.AsString as ChangesFieldValueScalarChange)?.NumericValue.Should().BeNull(); + + (deserialized.AsNumber as ChangesFieldValueScalarChange)?.StringValue.Should().BeNull(); + (deserialized.AsNumber as ChangesFieldValueScalarChange)?.NumericValue.Should().Be(3.1415926m); + + deserialized.AsObject.Should().NotBeNull().And.BeOfType(); + var changeObject = deserialized.AsObject.As(); + + changeObject.Color.Should().Be("color"); + changeObject.Description.Should().Be("description"); + changeObject.Id.Should().Be("12345"); + changeObject.Name.Should().Be("Name"); + } + + internal sealed class TestObject + { + [JsonPropertyName("as_string")] + public ChangesFieldValueChangeBase AsString { get; init; } = null!; + + [JsonPropertyName("as_number")] + public ChangesFieldValueChangeBase AsNumber { get; init; } = null!; + + [JsonPropertyName("as_object")] + public ChangesFieldValueChangeBase AsObject { get; init; } = null!; + } +} diff --git a/test/Octokit.Webhooks.Test/Resources/projects_v2_item/edited.with-date.payload.json b/test/Octokit.Webhooks.Test/Resources/projects_v2_item/edited.with-date.payload.json new file mode 100644 index 00000000..1871fb43 --- /dev/null +++ b/test/Octokit.Webhooks.Test/Resources/projects_v2_item/edited.with-date.payload.json @@ -0,0 +1,77 @@ +{ + "action": "edited", + "projects_v2_item": { + "id": 5678510, + "node_id": "PVTI_lADOAWcTxs07G84AVqWu", + "project_node_id": "PVT_kwDOAWcTxs07Gw", + "content_node_id": "DI_lADOAWcTxs07G84AIjOy", + "content_type": "DraftIssue", + "creator": { + "login": "wolfy1339", + "id": 4595477, + "node_id": "MDQ6VXNlcjQ1OTU0Nzc=", + "avatar_url": "https://avatars.githubusercontent.com/u/4595477?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wolfy1339", + "html_url": "https://github.com/wolfy1339", + "followers_url": "https://api.github.com/users/wolfy1339/followers", + "following_url": "https://api.github.com/users/wolfy1339/following{/other_user}", + "gists_url": "https://api.github.com/users/wolfy1339/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wolfy1339/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wolfy1339/subscriptions", + "organizations_url": "https://api.github.com/users/wolfy1339/orgs", + "repos_url": "https://api.github.com/users/wolfy1339/repos", + "events_url": "https://api.github.com/users/wolfy1339/events{/privacy}", + "received_events_url": "https://api.github.com/users/wolfy1339/received_events", + "type": "User", + "site_admin": false + }, + "created_at": "2022-06-08T20:34:26Z", + "updated_at": "2022-06-08T20:34:26Z", + "archived_at": null + }, + "changes": { + "field_value": { + "field_node_id": "PVTF_lADOAEzhac4AjFojzgbzsKM", + "field_type": "date", + "field_name": "Start Date", + "project_number": 233, + "from": null, + "to": "2025-08-01T00:00:00+00:00" + } + }, + "organization": { + "login": "Octocoders", + "id": 38302899, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjM4MzAyODk5", + "url": "https://api.github.com/orgs/Octocoders", + "repos_url": "https://api.github.com/orgs/Octocoders/repos", + "events_url": "https://api.github.com/orgs/Octocoders/events", + "hooks_url": "https://api.github.com/orgs/Octocoders/hooks", + "issues_url": "https://api.github.com/orgs/Octocoders/issues", + "members_url": "https://api.github.com/orgs/Octocoders/members{/member}", + "public_members_url": "https://api.github.com/orgs/Octocoders/public_members{/member}", + "avatar_url": "https://avatars1.githubusercontent.com/u/38302899?v=4", + "description": "" + }, + "sender": { + "login": "wolfy1339", + "id": 4595477, + "node_id": "MDQ6VXNlcjQ1OTU0Nzc=", + "avatar_url": "https://avatars.githubusercontent.com/u/4595477?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wolfy1339", + "html_url": "https://github.com/wolfy1339", + "followers_url": "https://api.github.com/users/wolfy1339/followers", + "following_url": "https://api.github.com/users/wolfy1339/following{/other_user}", + "gists_url": "https://api.github.com/users/wolfy1339/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wolfy1339/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wolfy1339/subscriptions", + "organizations_url": "https://api.github.com/users/wolfy1339/orgs", + "repos_url": "https://api.github.com/users/wolfy1339/repos", + "events_url": "https://api.github.com/users/wolfy1339/events{/privacy}", + "received_events_url": "https://api.github.com/users/wolfy1339/received_events", + "type": "User", + "site_admin": false + } +} diff --git a/test/Octokit.Webhooks.Test/Resources/projects_v2_item/edited.with-number.payload.json b/test/Octokit.Webhooks.Test/Resources/projects_v2_item/edited.with-number.payload.json new file mode 100644 index 00000000..1104a70d --- /dev/null +++ b/test/Octokit.Webhooks.Test/Resources/projects_v2_item/edited.with-number.payload.json @@ -0,0 +1,77 @@ +{ + "action": "edited", + "projects_v2_item": { + "id": 5678510, + "node_id": "PVTI_lADOAWcTxs07G84AVqWu", + "project_node_id": "PVT_kwDOAWcTxs07Gw", + "content_node_id": "DI_lADOAWcTxs07G84AIjOy", + "content_type": "DraftIssue", + "creator": { + "login": "wolfy1339", + "id": 4595477, + "node_id": "MDQ6VXNlcjQ1OTU0Nzc=", + "avatar_url": "https://avatars.githubusercontent.com/u/4595477?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wolfy1339", + "html_url": "https://github.com/wolfy1339", + "followers_url": "https://api.github.com/users/wolfy1339/followers", + "following_url": "https://api.github.com/users/wolfy1339/following{/other_user}", + "gists_url": "https://api.github.com/users/wolfy1339/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wolfy1339/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wolfy1339/subscriptions", + "organizations_url": "https://api.github.com/users/wolfy1339/orgs", + "repos_url": "https://api.github.com/users/wolfy1339/repos", + "events_url": "https://api.github.com/users/wolfy1339/events{/privacy}", + "received_events_url": "https://api.github.com/users/wolfy1339/received_events", + "type": "User", + "site_admin": false + }, + "created_at": "2022-06-08T20:34:26Z", + "updated_at": "2022-06-08T20:34:26Z", + "archived_at": null + }, + "changes": { + "field_value": { + "field_node_id": "PVTF_lADOAEzhac4AjFojzgyEIsU", + "field_type": "number", + "field_name": "tempNumber", + "project_number": 233, + "from": null, + "to": 123.456 + } + }, + "organization": { + "login": "Octocoders", + "id": 38302899, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjM4MzAyODk5", + "url": "https://api.github.com/orgs/Octocoders", + "repos_url": "https://api.github.com/orgs/Octocoders/repos", + "events_url": "https://api.github.com/orgs/Octocoders/events", + "hooks_url": "https://api.github.com/orgs/Octocoders/hooks", + "issues_url": "https://api.github.com/orgs/Octocoders/issues", + "members_url": "https://api.github.com/orgs/Octocoders/members{/member}", + "public_members_url": "https://api.github.com/orgs/Octocoders/public_members{/member}", + "avatar_url": "https://avatars1.githubusercontent.com/u/38302899?v=4", + "description": "" + }, + "sender": { + "login": "wolfy1339", + "id": 4595477, + "node_id": "MDQ6VXNlcjQ1OTU0Nzc=", + "avatar_url": "https://avatars.githubusercontent.com/u/4595477?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wolfy1339", + "html_url": "https://github.com/wolfy1339", + "followers_url": "https://api.github.com/users/wolfy1339/followers", + "following_url": "https://api.github.com/users/wolfy1339/following{/other_user}", + "gists_url": "https://api.github.com/users/wolfy1339/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wolfy1339/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wolfy1339/subscriptions", + "organizations_url": "https://api.github.com/users/wolfy1339/orgs", + "repos_url": "https://api.github.com/users/wolfy1339/repos", + "events_url": "https://api.github.com/users/wolfy1339/events{/privacy}", + "received_events_url": "https://api.github.com/users/wolfy1339/received_events", + "type": "User", + "site_admin": false + } +} diff --git a/test/Octokit.Webhooks.Test/WebhookEventProcessorTests.cs b/test/Octokit.Webhooks.Test/WebhookEventProcessorTests.cs index 8d25d6f7..5f47f85a 100644 --- a/test/Octokit.Webhooks.Test/WebhookEventProcessorTests.cs +++ b/test/Octokit.Webhooks.Test/WebhookEventProcessorTests.cs @@ -10,8 +10,10 @@ public class WebhookEventProcessorTests [Theory] [ClassData(typeof(WebhookEventProcessorTestsData))] - public void CanDeserialize(string @event, string payload, Type expectedType) + public void CanDeserialize(string @event, string testName, string payload, Type expectedType) { + // Only used to make it easier to differentiate test cases for the same event without looking at whole payload + _ = testName; var headers = new WebhookHeaders { Event = @event, diff --git a/test/Octokit.Webhooks.Test/WebhookEventProcessorTestsData.cs b/test/Octokit.Webhooks.Test/WebhookEventProcessorTestsData.cs index 22275702..5ef60028 100644 --- a/test/Octokit.Webhooks.Test/WebhookEventProcessorTestsData.cs +++ b/test/Octokit.Webhooks.Test/WebhookEventProcessorTestsData.cs @@ -8,9 +8,9 @@ namespace Octokit.Webhooks.Test; using Octokit.Webhooks.TestUtils; using Xunit; -public class WebhookEventProcessorTestsData : IEnumerable> +public class WebhookEventProcessorTestsData : IEnumerable> { - public IEnumerator> GetEnumerator() + public IEnumerator> GetEnumerator() { var resourcesDirectory = ResourceUtils.GetResources(); var files = Directory.GetFiles(resourcesDirectory, "*.json", SearchOption.AllDirectories); @@ -20,7 +20,7 @@ public IEnumerator> GetEnumerator() var parts = relativeResource.Split(Path.DirectorySeparatorChar); var expectedType = ClassUtils.GetEventTypeByName(parts[0].ToPascalCase()); var content = ResourceUtils.ReadResource(relativeResource); - yield return new TheoryDataRow(parts[0], content, expectedType); + yield return new TheoryDataRow(parts[0], parts[1], content, expectedType); } }