From aa82f0b47726e7f29a6478241cc2dc3c22bb0e9a Mon Sep 17 00:00:00 2001 From: Elizabeth Schneider Date: Tue, 9 Nov 2021 21:14:35 -0700 Subject: [PATCH] feat: add support for system.text.json update to .NET 6.0 --- GitVersion.yml | 12 +- src/.editorconfig | 1 + src/Directory.Build.props | 25 +- src/Directory.Build.targets | 6 +- ...edygeek.ZendeskAPI.IntegrationTests.csproj | 2 +- .../Base/ResponseSaver.cs | 4 +- .../Configuration/CredentialsProviderTests.cs | 18 +- .../ServiceCollectionExtensionsTest.cs | 1 - .../CollaboratorConverterTest.cs | 20 +- .../Serialization/ListResponseBaseTest.cs | 31 +- .../Speedygeek.ZendeskAPI.UnitTests.csproj | 10 +- .../Support/TicketTests.cs | 40 +- .../Configuration/APITokenCredentials.cs | 4 +- .../Configuration/BasicCredentials.cs | 4 +- .../Configuration/ICredentialsProvider.cs | 3 +- .../OAuthAccessTokenCredentials.cs | 14 +- .../ServiceCollectionExtensions.cs | 9 +- .../Models/Base/Links.cs | 26 ++ .../Models/Base/ListResponseBase.cs | 13 +- src/Speedygeek.ZendeskAPI/Models/Base/Meta.cs | 32 ++ .../Models/Base/PaginationBase.cs | 25 ++ .../Models/Shared/ZenFile.cs | 2 +- .../{TicketPiority.cs => TicketPriority.cs} | 0 .../Models/Support/Enums/UserRoles.cs | 1 - .../Models/Support/Tickets/Comment.cs | 8 +- .../Models/Support/Tickets/Follower.cs | 2 +- .../Tickets/Responses/TicketListResponse.cs | 6 +- .../Tickets/Responses/TicketResponse.cs | 8 +- .../Models/Support/Tickets/Ticket.cs | 32 +- .../Users/Responses/UserListResponse.cs | 2 +- .../Support/Users/Responses/UserResponse.cs | 2 +- .../Models/Support/Users/User.cs | 8 +- .../Operations/BaseOperations.cs | 13 +- .../Operations/Support/ITicketOperations.cs | 6 +- .../Operations/Support/TicketOperations.cs | 9 +- .../Operations/Support/UserOperations.cs | 1 + .../Context/SnakeCaseNamePolicy.cs | 13 + .../Serialization/Context/ZenJsonContext.cs | 17 + .../Converters/CollaboratorConverter.cs | 97 +++-- .../Converters/CollaboratorConverter_Old.cs | 75 ++++ .../Converters/EnumToStringConverter.cs | 377 ++++++++++++++++++ .../Serialization/JsonDotNetSerializer.cs | 2 +- .../Serialization/TextJsonSerializer.cs | 64 +++ .../Serialization/ZendeskContractResolver.cs | 2 +- .../Speedygeek.ZendeskAPI.csproj | 16 +- .../Utilities/Helpers.cs | 20 +- .../Utilities/StringUtils.cs | 95 +++++ src/Speedygeek.ZendeskAPI/ZendeskClient.cs | 2 +- src/global.json | 8 +- 49 files changed, 966 insertions(+), 222 deletions(-) create mode 100644 src/Speedygeek.ZendeskAPI/Models/Base/Links.cs create mode 100644 src/Speedygeek.ZendeskAPI/Models/Base/Meta.cs create mode 100644 src/Speedygeek.ZendeskAPI/Models/Base/PaginationBase.cs rename src/Speedygeek.ZendeskAPI/Models/Support/Enums/{TicketPiority.cs => TicketPriority.cs} (100%) create mode 100644 src/Speedygeek.ZendeskAPI/Serialization/Context/SnakeCaseNamePolicy.cs create mode 100644 src/Speedygeek.ZendeskAPI/Serialization/Context/ZenJsonContext.cs create mode 100644 src/Speedygeek.ZendeskAPI/Serialization/Converters/CollaboratorConverter_Old.cs create mode 100644 src/Speedygeek.ZendeskAPI/Serialization/Converters/EnumToStringConverter.cs create mode 100644 src/Speedygeek.ZendeskAPI/Serialization/TextJsonSerializer.cs create mode 100644 src/Speedygeek.ZendeskAPI/Utilities/StringUtils.cs diff --git a/GitVersion.yml b/GitVersion.yml index e024869..3731dd5 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -8,4 +8,14 @@ commits-since-version-source-padding: 3 commit-message-incrementing: Enabled branches: master: - tag: alpha \ No newline at end of file + tag: alpha + pull-request: + mode: ContinuousDelivery + tag: PullRequest + increment: None + prevent-increment-of-merged-branch-version: false + tag-number-pattern: '[/-](?\d+)[-/]' + track-merge-target: false + regex: (pull|pull\-requests|pr|[0-9]+)[/-] + tracks-release-branches: false + is-release-branch: false diff --git a/src/.editorconfig b/src/.editorconfig index 686804b..cd30b20 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -228,3 +228,4 @@ dotnet_naming_rule.public_constant_fields_rule.severity = warning dotnet_naming_rule.public_static_readonly_fields_rule.symbols = public_static_readonly_fields dotnet_naming_rule.public_static_readonly_fields_rule.style = pascal_case dotnet_naming_rule.public_static_readonly_fields_rule.severity = warning +dotnet_diagnostic.CA2000.severity=silent diff --git a/src/Directory.Build.props b/src/Directory.Build.props index b7ab244..c064e26 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -21,24 +21,25 @@ true true - + stylecop.json - - - + + + - + + - - - - - - + + + + + + @@ -47,7 +48,7 @@ CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member' VSTHRD200:Use "Async" suffix in names of methods that return an awaitable type. --> 1701,1702,CS1591 - NU5105 + NU5105,CS0400,CS1591 diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 1d29b69..a49c699 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -1,7 +1,7 @@ - + - + --> <_LocalTopLevelSourceRoot Include="@(SourceRoot)" Condition="'%(SourceRoot.NestedRoot)' == ''"/> - + --> diff --git a/src/Speedygeek.ZendeskAPI.IntegrationTests/Speedygeek.ZendeskAPI.IntegrationTests.csproj b/src/Speedygeek.ZendeskAPI.IntegrationTests/Speedygeek.ZendeskAPI.IntegrationTests.csproj index 0d85bff..9b54185 100644 --- a/src/Speedygeek.ZendeskAPI.IntegrationTests/Speedygeek.ZendeskAPI.IntegrationTests.csproj +++ b/src/Speedygeek.ZendeskAPI.IntegrationTests/Speedygeek.ZendeskAPI.IntegrationTests.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + net6.0 false true diff --git a/src/Speedygeek.ZendeskAPI.UnitTests/Base/ResponseSaver.cs b/src/Speedygeek.ZendeskAPI.UnitTests/Base/ResponseSaver.cs index 38a15f7..1d4a069 100644 --- a/src/Speedygeek.ZendeskAPI.UnitTests/Base/ResponseSaver.cs +++ b/src/Speedygeek.ZendeskAPI.UnitTests/Base/ResponseSaver.cs @@ -23,9 +23,9 @@ protected override async Task SendAsync(HttpRequestMessage if (!File.Exists(filePath) | FileName == DEFAULTFILENAME) { - var content = await resp.Content.ReadAsStringAsync().ConfigureAwait(false); + var content = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); content = Regex.Replace(content, @"\s+", " "); - await File.WriteAllTextAsync(filePath, content); + await File.WriteAllTextAsync(filePath, content, cancellationToken); } return resp; } diff --git a/src/Speedygeek.ZendeskAPI.UnitTests/Configuration/CredentialsProviderTests.cs b/src/Speedygeek.ZendeskAPI.UnitTests/Configuration/CredentialsProviderTests.cs index ee18b70..9c94eef 100644 --- a/src/Speedygeek.ZendeskAPI.UnitTests/Configuration/CredentialsProviderTests.cs +++ b/src/Speedygeek.ZendeskAPI.UnitTests/Configuration/CredentialsProviderTests.cs @@ -40,21 +40,21 @@ public void ApiTokenAuthApiTokenNull() [Test] public void ApiTokenAuthNullClient() { - Assert.That(() => { new APITokenCredentials(Settings.AdminUserName, Settings.ApiToken).ConfigureHttpClientAsync(null).ConfigureAwait(false); }, + Assert.That(() => { new APITokenCredentials(Settings.AdminUserName, Settings.ApiToken).ConfigureHttpClient(null); }, Throws.ArgumentNullException); } [Test] public void OAuthTokenAuthNullClient() { - Assert.That(() => { new OAuthAccessTokenCredentials(Settings.AdminOAuthToken).ConfigureHttpClientAsync(null).ConfigureAwait(false); }, + Assert.That(() => { new OAuthAccessTokenCredentials(Settings.AdminOAuthToken).ConfigureHttpClient(null); }, Throws.ArgumentNullException); } [Test] public void BasicAuthNullClient() { - Assert.That(() => { new BasicCredentials(Settings.AdminUserName, Settings.AdminPassword).ConfigureHttpClientAsync(null).ConfigureAwait(false); }, + Assert.That(() => { new BasicCredentials(Settings.AdminUserName, Settings.AdminPassword).ConfigureHttpClient(null); }, Throws.ArgumentNullException); } @@ -80,12 +80,12 @@ public void OAuthTokenAuthNullEndUserId() } [Test] - public async Task ApiTokenAuthBuildHeader() + public void ApiTokenAuthBuildHeader() { using var client = new HttpClient(); var cred = new APITokenCredentials(Settings.AdminUserName, Settings.ApiToken); - await cred.ConfigureHttpClientAsync(client).ConfigureAwait(false); + cred.ConfigureHttpClient(client); var headerScheme = client.DefaultRequestHeaders.Authorization.Scheme; var headerParameter = client.DefaultRequestHeaders.Authorization.Parameter; @@ -95,12 +95,12 @@ public async Task ApiTokenAuthBuildHeader() } [Test] - public async Task BasicAuthBuildHeader() + public void BasicAuthBuildHeader() { using var client = new HttpClient(); var cred = new BasicCredentials(Settings.AdminUserName, Settings.AdminPassword); - await cred.ConfigureHttpClientAsync(client).ConfigureAwait(false); + cred.ConfigureHttpClient(client); var headerScheme = client.DefaultRequestHeaders.Authorization.Scheme; var headerParameter = client.DefaultRequestHeaders.Authorization.Parameter; @@ -110,13 +110,13 @@ public async Task BasicAuthBuildHeader() } [Test] - public async Task OAuthOnBehalfOfAuthBuildHeader() + public void OAuthOnBehalfOfAuthBuildHeader() { using var client = new HttpClient(); var endUserId = "enduser@test.com"; var cred = new OAuthAccessTokenCredentials(Settings.AdminOAuthToken, endUserId); - await cred.ConfigureHttpClientAsync(client).ConfigureAwait(false); + cred.ConfigureHttpClient(client); var headerValue = client.DefaultRequestHeaders.GetValues("X-On-Behalf-Of").FirstOrDefault(); diff --git a/src/Speedygeek.ZendeskAPI.UnitTests/Configuration/ServiceCollectionExtensionsTest.cs b/src/Speedygeek.ZendeskAPI.UnitTests/Configuration/ServiceCollectionExtensionsTest.cs index feef149..0e725de 100644 --- a/src/Speedygeek.ZendeskAPI.UnitTests/Configuration/ServiceCollectionExtensionsTest.cs +++ b/src/Speedygeek.ZendeskAPI.UnitTests/Configuration/ServiceCollectionExtensionsTest.cs @@ -6,7 +6,6 @@ namespace Speedygeek.ZendeskAPI.UnitTests.Configuration [TestFixture] public class ServiceCollectionExtensionsTest { - [Test] public void BasicAuthSubDomainNull() { diff --git a/src/Speedygeek.ZendeskAPI.UnitTests/Serialization/CollaboratorConverterTest.cs b/src/Speedygeek.ZendeskAPI.UnitTests/Serialization/CollaboratorConverterTest.cs index 23be4f9..316c99d 100644 --- a/src/Speedygeek.ZendeskAPI.UnitTests/Serialization/CollaboratorConverterTest.cs +++ b/src/Speedygeek.ZendeskAPI.UnitTests/Serialization/CollaboratorConverterTest.cs @@ -6,7 +6,7 @@ using Speedygeek.ZendeskAPI.Serialization; -namespace Speedygeek.ZendeskAPI.UnitTests +namespace Speedygeek.ZendeskAPI.UnitTests.Serialization { [TestFixture] public class CollaboratorConverterTest @@ -25,20 +25,20 @@ public void SetUp() [Test] public void ConvertMixedTypes() { - var json = @"{ ""Ticket"": { ""id"": 1002, ""collaborators"": [ 562562562, ""someone@example.com"", { ""name"": ""Someone Else"", ""email"": ""else@example.com"" } ]}}"; + var json = @"{ ""id"": 1002, ""collaborators"": [ 562562562, ""someone@example.com"", { ""name"": ""Someone Else"", ""email"": ""else@example.com"" } ]}"; - TicketResponse resp = null; - using (var stream = new MemoryStream(Encoding.ASCII.GetBytes(json))) + Ticket resp = null; + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(json))) { - resp = _serializer.Deserialize(stream); + resp = _serializer.Deserialize(stream); } - Assert.That(resp.Ticket, Is.Not.Null); - Assert.That(resp.Ticket.Collaborators[0].Id, Is.Not.Zero); - Assert.That(resp.Ticket.Collaborators[1].Email, Is.EqualTo("someone@example.com")); + Assert.That(resp, Is.Not.Null); + Assert.That(resp.Collaborators[0].Id, Is.Not.Zero); + Assert.That(resp.Collaborators[1].Email, Is.EqualTo("someone@example.com")); - Assert.That(resp.Ticket.Collaborators[2].Email, Is.EqualTo("else@example.com")); - Assert.That(resp.Ticket.Collaborators[2].Name, Is.EqualTo("Someone Else")); + Assert.That(resp.Collaborators[2].Email, Is.EqualTo("else@example.com")); + Assert.That(resp.Collaborators[2].Name, Is.EqualTo("Someone Else")); } } } diff --git a/src/Speedygeek.ZendeskAPI.UnitTests/Serialization/ListResponseBaseTest.cs b/src/Speedygeek.ZendeskAPI.UnitTests/Serialization/ListResponseBaseTest.cs index 79dce51..6ac5b5e 100644 --- a/src/Speedygeek.ZendeskAPI.UnitTests/Serialization/ListResponseBaseTest.cs +++ b/src/Speedygeek.ZendeskAPI.UnitTests/Serialization/ListResponseBaseTest.cs @@ -22,20 +22,37 @@ public void SetUp() } + //[Test] + //public void PageNumber() + //{ + // var json = @"{ ""next_page"":""https://csharpapi.zendesk.com/api/v2/tickets.json?page=3"",""previous_page"":""https://csharpapi.zendesk.com/api/v2/tickets.json?page=1"",""count"":1365}"; + + // TicketListResponse resp = null; + // using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(json))) + // { + // resp = _serializer.Deserialize(stream); + // } + + // Assert.That(resp.Page, Is.EqualTo(2)); + //} + + [Test] - public void PageNumber() + public void Meta() { - var json = @"{ ""next_page"":""https://csharpapi.zendesk.com/api/v2/tickets.json?page=3"",""previous_page"":""https://csharpapi.zendesk.com/api/v2/tickets.json?page=1"",""count"":1365}"; - + var json = @"{ ""meta"": {""has_more"": true, ""after_cursor"": ""100"", ""before_cursor"": ""200"" }, +""links"": { + ""next"": ""https://example.zendesk.com/api/v2/tickets.json?page[size]=100&page[after]=101"", + ""prev"": ""https://example.zendesk.com/api/v2/tickets.json?page[size]=100&page[before]=200"" +} +}"; TicketListResponse resp = null; - using (var stream = new MemoryStream(Encoding.ASCII.GetBytes(json))) + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(json))) { resp = _serializer.Deserialize(stream); } - Assert.That(resp.Page, Is.EqualTo(2)); + Assert.That(resp.Meta.HasMore, Is.True); } - - } } diff --git a/src/Speedygeek.ZendeskAPI.UnitTests/Speedygeek.ZendeskAPI.UnitTests.csproj b/src/Speedygeek.ZendeskAPI.UnitTests/Speedygeek.ZendeskAPI.UnitTests.csproj index 4479e0c..4fe31c0 100644 --- a/src/Speedygeek.ZendeskAPI.UnitTests/Speedygeek.ZendeskAPI.UnitTests.csproj +++ b/src/Speedygeek.ZendeskAPI.UnitTests/Speedygeek.ZendeskAPI.UnitTests.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + netcoreapp6.0 false true @@ -15,10 +15,10 @@ - - - - + + + + diff --git a/src/Speedygeek.ZendeskAPI.UnitTests/Support/TicketTests.cs b/src/Speedygeek.ZendeskAPI.UnitTests/Support/TicketTests.cs index bd2b953..6486a38 100644 --- a/src/Speedygeek.ZendeskAPI.UnitTests/Support/TicketTests.cs +++ b/src/Speedygeek.ZendeskAPI.UnitTests/Support/TicketTests.cs @@ -73,18 +73,18 @@ public async Task TicketCreateMany() Assert.That(resp.JobStatus.Status, Is.EqualTo(JobStatuses.Queued)); } - [Test] - public async Task TicketsByOrg() - { - BuildResponse("organizations/22560572/tickets.json?page=1&per_page=50", "organization_22560572_tickets.json"); + //[Test] + //public async Task TicketsByOrg() + //{ + // BuildResponse("organizations/22560572/tickets.json?page=1&per_page=50", "organization_22560572_tickets.json"); - var resp = await Client.Support.Tickets.GetByOrganizationAsync(22560572, new TicketPageParams { PerPage = 50 }, TicketSideloads.None).ConfigureAwait(false); + // var resp = await Client.Support.Tickets.GetByOrganizationAsync(22560572, new TicketPageParams { PerPage = 50 }, TicketSideloads.None).ConfigureAwait(false); - Assert.That(resp.Tickets.Count, Is.EqualTo(50)); - Assert.That(resp.NextPage, Is.Not.Null); - Assert.That(resp.PerPage, Is.EqualTo(50)); - Assert.That(resp.TotalPages, Is.EqualTo(27)); - } + // Assert.That(resp.Tickets.Count, Is.EqualTo(50)); + // Assert.That(resp.NextPage, Is.Not.Null); + // Assert.That(resp.PerPage, Is.EqualTo(50)); + // Assert.That(resp.TotalPages, Is.EqualTo(27)); + //} [Test] public async Task TicketsGetAll() @@ -333,22 +333,22 @@ public void TicketGetManyOver100() Throws.ArgumentException.With.Message.EqualTo($"API will not accept a list over 100 items long{ Environment.NewLine}Parameter name: ids")); } - [Test] - public async Task TicketNextPage() - { - BuildResponse("tickets.json", "ticketsGetAllV2.json"); - var respAll = await Client.Support.Tickets.GetAllAsync().ConfigureAwait(false); + //[Test] + //public async Task TicketNextPage() + //{ + // BuildResponse("tickets.json", "ticketsGetAllV2.json"); + // var respAll = await Client.Support.Tickets.GetAllAsync().ConfigureAwait(false); - BuildResponse($"tickets.json?page=2", "TicketsNextPage.json", HttpMethod.Get); - var resp = await Client.Support.Tickets.GetNextPageAsync(respAll.NextPage).ConfigureAwait(false); + // BuildResponse($"tickets.json?page=2", "TicketsNextPage.json", HttpMethod.Get); + // var resp = await Client.Support.Tickets.GetPageAsync(respAll.NextPage).ConfigureAwait(false); - Assert.That(resp.Page, Is.EqualTo(2)); - } + // Assert.That(resp.Page, Is.EqualTo(2)); + //} [Test] public void TicketNextPageNull() { - Assert.That(async () => { var resp = await Client.Support.Tickets.GetNextPageAsync(null).ConfigureAwait(false); }, + Assert.That(async () => { var resp = await Client.Support.Tickets.GetPageAsync(null).ConfigureAwait(false); }, Throws.ArgumentNullException); } diff --git a/src/Speedygeek.ZendeskAPI/Configuration/APITokenCredentials.cs b/src/Speedygeek.ZendeskAPI/Configuration/APITokenCredentials.cs index d6bdaf0..1a64403 100644 --- a/src/Speedygeek.ZendeskAPI/Configuration/APITokenCredentials.cs +++ b/src/Speedygeek.ZendeskAPI/Configuration/APITokenCredentials.cs @@ -43,8 +43,7 @@ public APITokenCredentials(string userName, string apiToken) /// Configure the authentication header /// /// to update - /// when completed - public Task ConfigureHttpClientAsync(HttpClient client) + public void ConfigureHttpClient(HttpClient client) { if (client is null) { @@ -53,7 +52,6 @@ public Task ConfigureHttpClientAsync(HttpClient client) var auth = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_userName}/token:{_apiToken}")); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", auth); - return Task.CompletedTask; } } } diff --git a/src/Speedygeek.ZendeskAPI/Configuration/BasicCredentials.cs b/src/Speedygeek.ZendeskAPI/Configuration/BasicCredentials.cs index 85200b5..8b370cb 100644 --- a/src/Speedygeek.ZendeskAPI/Configuration/BasicCredentials.cs +++ b/src/Speedygeek.ZendeskAPI/Configuration/BasicCredentials.cs @@ -43,8 +43,7 @@ public BasicCredentials(string userName, string password) /// Configure the authentication header /// /// to update - /// when completed - public Task ConfigureHttpClientAsync(HttpClient client) + public void ConfigureHttpClient(HttpClient client) { if (client is null) { @@ -53,7 +52,6 @@ public Task ConfigureHttpClientAsync(HttpClient client) var auth = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_userName}:{_password}")); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", auth); - return Task.CompletedTask; } } } diff --git a/src/Speedygeek.ZendeskAPI/Configuration/ICredentialsProvider.cs b/src/Speedygeek.ZendeskAPI/Configuration/ICredentialsProvider.cs index d424046..98a4518 100644 --- a/src/Speedygeek.ZendeskAPI/Configuration/ICredentialsProvider.cs +++ b/src/Speedygeek.ZendeskAPI/Configuration/ICredentialsProvider.cs @@ -15,7 +15,6 @@ public interface ICredentialsProvider /// Override to configure the HttpClient /// /// HttpClient to configure - /// as when complete - Task ConfigureHttpClientAsync(HttpClient client); + void ConfigureHttpClient(HttpClient client); } } diff --git a/src/Speedygeek.ZendeskAPI/Configuration/OAuthAccessTokenCredentials.cs b/src/Speedygeek.ZendeskAPI/Configuration/OAuthAccessTokenCredentials.cs index 85939d4..f314702 100644 --- a/src/Speedygeek.ZendeskAPI/Configuration/OAuthAccessTokenCredentials.cs +++ b/src/Speedygeek.ZendeskAPI/Configuration/OAuthAccessTokenCredentials.cs @@ -56,21 +56,19 @@ public OAuthAccessTokenCredentials(string accessToken) _accessToken = accessToken; } - /// - public Task ConfigureHttpClientAsync(HttpClient client) + /// + /// Configure the authentication header + /// + /// to update + public void ConfigureHttpClient(HttpClient client) { - if (client is null) - { - throw new ArgumentNullException(nameof(client)); - } + ArgumentNullException.ThrowIfNull(client); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(Scheme, _accessToken); if (!string.IsNullOrWhiteSpace(_endUserId)) { client.DefaultRequestHeaders.Add("X-On-Behalf-Of", _endUserId); } - - return Task.CompletedTask; } } } diff --git a/src/Speedygeek.ZendeskAPI/Configuration/ServiceCollectionExtensions.cs b/src/Speedygeek.ZendeskAPI/Configuration/ServiceCollectionExtensions.cs index eaa8dfb..6a3830c 100644 --- a/src/Speedygeek.ZendeskAPI/Configuration/ServiceCollectionExtensions.cs +++ b/src/Speedygeek.ZendeskAPI/Configuration/ServiceCollectionExtensions.cs @@ -116,21 +116,20 @@ public static IServiceCollection AddZendeskClient(this IServiceCollection servic services.Configure(configureOptions); services.AddScoped(); - services.AddScoped(); -#pragma warning disable VSTHRD101 // Avoid unsupported async delegates - _ = services.AddHttpClient(async (sp, client) => + // services.AddScoped(); + services.AddScoped(); + _ = services.AddHttpClient((sp, client) => { var options = sp.GetRequiredService>().Value; client.BaseAddress = new Uri($"https://{options.SubDomain}.zendesk.com/api/v2/"); - await options.Credentials.ConfigureHttpClientAsync(client).ConfigureAwait(false); + options.Credentials.ConfigureHttpClient(client); if (options.TimeOut != TimeSpan.Zero) { client.Timeout = options.TimeOut; } }); -#pragma warning restore VSTHRD101 // Avoid unsupported async delegates return services; } } diff --git a/src/Speedygeek.ZendeskAPI/Models/Base/Links.cs b/src/Speedygeek.ZendeskAPI/Models/Base/Links.cs new file mode 100644 index 0000000..4014ec6 --- /dev/null +++ b/src/Speedygeek.ZendeskAPI/Models/Base/Links.cs @@ -0,0 +1,26 @@ +// Copyright (c) Elizabeth Schneider. All Rights Reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Text.Json.Serialization; + +namespace Speedygeek.ZendeskAPI.Models.Base +{ + /// + /// Cursor Pagination Links + /// + public class Links + { + /// + /// Next page Uri + /// + [JsonInclude] + public Uri Next { get; private set; } + + /// + /// Previous page Uri + /// + [JsonInclude] + public Uri Prev { get; private set; } + } +} diff --git a/src/Speedygeek.ZendeskAPI/Models/Base/ListResponseBase.cs b/src/Speedygeek.ZendeskAPI/Models/Base/ListResponseBase.cs index 7b4f125..9bf4b2b 100644 --- a/src/Speedygeek.ZendeskAPI/Models/Base/ListResponseBase.cs +++ b/src/Speedygeek.ZendeskAPI/Models/Base/ListResponseBase.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; +using System.Text.Json.Serialization; using Microsoft.AspNetCore.WebUtilities; using Speedygeek.ZendeskAPI.Utilities; @@ -13,24 +14,26 @@ namespace Speedygeek.ZendeskAPI.Models.Base /// public class ListResponseBase { - private int _page = 0; + private int _page; private int _perPage = 100; - private bool _updatedValues = false; + private bool _updatedValues; /// /// URL of the next page /// - public Uri NextPage { get; } = null; + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Uri NextPage { get; private set; } /// /// URl of the Previous page /// - public Uri PreviousPage { get; } = null; + [JsonInclude] + public Uri PreviousPage { get; private set; } /// /// Count of Items /// - public int Count { get; } = 0; + public long Count { get; internal set; } /// /// Total number of pages diff --git a/src/Speedygeek.ZendeskAPI/Models/Base/Meta.cs b/src/Speedygeek.ZendeskAPI/Models/Base/Meta.cs new file mode 100644 index 0000000..90cca45 --- /dev/null +++ b/src/Speedygeek.ZendeskAPI/Models/Base/Meta.cs @@ -0,0 +1,32 @@ +// Copyright (c) Elizabeth Schneider. All Rights Reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Text.Json.Serialization; + +namespace Speedygeek.ZendeskAPI.Models.Base +{ + /// + /// Cursor Pagination meta data + /// See Cursor Pagination + /// + public class Meta + { + /// + /// Pagination are more Items to load. + /// + [JsonInclude] + public bool HasMore { get; private set; } + + /// + /// Request parameter of the subsequent request to retrieve the next page of results. + /// + [JsonInclude] + public long AfterCursor { get; private set; } + + /// + /// Request parameter of the subsequent request to retrieve the previous page of results. + /// + [JsonInclude] + public long BeforeCursor { get; private set; } + } +} diff --git a/src/Speedygeek.ZendeskAPI/Models/Base/PaginationBase.cs b/src/Speedygeek.ZendeskAPI/Models/Base/PaginationBase.cs new file mode 100644 index 0000000..ccda256 --- /dev/null +++ b/src/Speedygeek.ZendeskAPI/Models/Base/PaginationBase.cs @@ -0,0 +1,25 @@ +// Copyright (c) Elizabeth Schneider. All Rights Reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Text.Json.Serialization; + +namespace Speedygeek.ZendeskAPI.Models.Base +{ + /// + /// Base class for Cursor Pagination + /// + public class PaginationBase + { + /// + /// Pagination Meta data + /// + [JsonInclude] + public Meta Meta { get; private set; } + + /// + /// Pagination Links + /// + [JsonInclude] + public Links Links { get; private set; } + } +} diff --git a/src/Speedygeek.ZendeskAPI/Models/Shared/ZenFile.cs b/src/Speedygeek.ZendeskAPI/Models/Shared/ZenFile.cs index 3b8f1fe..cc1a665 100644 --- a/src/Speedygeek.ZendeskAPI/Models/Shared/ZenFile.cs +++ b/src/Speedygeek.ZendeskAPI/Models/Shared/ZenFile.cs @@ -11,7 +11,7 @@ namespace Speedygeek.ZendeskAPI.Models /// public class ZenFile : IDisposable { - private bool _disposedValue = false; // To detect redundant calls + private bool _disposedValue; // To detect redundant calls /// /// Finalizes an instance of the class. diff --git a/src/Speedygeek.ZendeskAPI/Models/Support/Enums/TicketPiority.cs b/src/Speedygeek.ZendeskAPI/Models/Support/Enums/TicketPriority.cs similarity index 100% rename from src/Speedygeek.ZendeskAPI/Models/Support/Enums/TicketPiority.cs rename to src/Speedygeek.ZendeskAPI/Models/Support/Enums/TicketPriority.cs diff --git a/src/Speedygeek.ZendeskAPI/Models/Support/Enums/UserRoles.cs b/src/Speedygeek.ZendeskAPI/Models/Support/Enums/UserRoles.cs index 0f98374..eca31be 100644 --- a/src/Speedygeek.ZendeskAPI/Models/Support/Enums/UserRoles.cs +++ b/src/Speedygeek.ZendeskAPI/Models/Support/Enums/UserRoles.cs @@ -20,7 +20,6 @@ public enum UserRoles /// /// End User /// - [EnumMember(Value = "end-user")] EndUser = 1, /// diff --git a/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Comment.cs b/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Comment.cs index fc26a3f..5161530 100644 --- a/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Comment.cs +++ b/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Comment.cs @@ -62,9 +62,9 @@ public class Comment : ZenEntity /// public Via Via { get; set; } - /// - /// System information (web client, IP address, etc.) and comment flags, if any. See - /// - public dynamic Metadata { get; set; } + ///// + ///// System information (web client, IP address, etc.) and comment flags, if any. See + ///// + // public dynamic Metadata { get; set; } } } diff --git a/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Follower.cs b/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Follower.cs index faba6d2..01f20c4 100644 --- a/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Follower.cs +++ b/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Follower.cs @@ -23,7 +23,7 @@ public class Follower /// /// Update action /// - [DefaultValue(FollowerAction.None)] + // [DefaultValue(FollowerAction.None)] public FollowerAction Action { get; set; } = FollowerAction.None; } } diff --git a/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Responses/TicketListResponse.cs b/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Responses/TicketListResponse.cs index 9b9dd4d..845af8c 100644 --- a/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Responses/TicketListResponse.cs +++ b/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Responses/TicketListResponse.cs @@ -9,16 +9,16 @@ namespace Speedygeek.ZendeskAPI.Models.Support /// /// Paged list of Tickets /// - public class TicketListResponse : ListResponseBase + public class TicketListResponse : PaginationBase { /// /// Requested Tickets /// - public IList Tickets { get; } + public List Tickets { get; } /// /// Users related to requested tickets /// - public IList Users { get; } + public List Users { get; } } } diff --git a/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Responses/TicketResponse.cs b/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Responses/TicketResponse.cs index d0e37d8..be71275 100644 --- a/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Responses/TicketResponse.cs +++ b/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Responses/TicketResponse.cs @@ -15,9 +15,9 @@ public class TicketResponse /// public Ticket Ticket { get; } - /// - /// Users related to requested tickets - /// - public IList Users { get; } + ///// + ///// Users related to requested tickets + ///// + // public IList Users { get; } } } diff --git a/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Ticket.cs b/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Ticket.cs index cc96f4b..ea2f33c 100644 --- a/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Ticket.cs +++ b/src/Speedygeek.ZendeskAPI/Models/Support/Tickets/Ticket.cs @@ -85,7 +85,7 @@ public class Ticket : ZenEntity /// /// The ids of users currently CC'ed on the ticket /// - public IList CollaboratorIds { get; set; } + public List CollaboratorIds { get; set; } /// /// Update operations only @@ -95,39 +95,39 @@ public class Ticket : ZenEntity /// so make sure to include existing collaborators in the array /// if you wish to retain these on the ticket. /// - [JsonConverter(typeof(CollaboratorConverter))] - public IList Collaborators { get; set; } + // [JsonConverter(typeof(CollaboratorConverter_Old))] + public List Collaborators { get; set; } /// /// An array of numeric IDs, emails, or objects containing name and email properties. /// An email notification is sent to them when the ticket is updated /// - [JsonConverter(typeof(CollaboratorConverter))] - public IList AdditionalCollaborators { get; set; } + // [JsonConverter(typeof(CollaboratorConverter_Old))] + public List AdditionalCollaborators { get; set; } /// /// The ids of agents or end users currently CC'ed on the ticket. /// See /// - public IList EmailCcIds { get; set; } + public List EmailCcIds { get; set; } /// /// The ids of agents currently following the ticket. /// See /// - public IList FollowerIds { get; set; } + public List FollowerIds { get; set; } /// /// The list of Agnets currently following the ticket. /// See /// - public IList Followers { get; set; } + public List Followers { get; set; } /// /// The list of Users currently CCed for the ticket. /// See /// - public IList EmailCcs { get; set; } + public List EmailCcs { get; set; } /// /// The topic this ticket originated from, if any @@ -153,19 +153,19 @@ public class Ticket : ZenEntity /// /// The tags applied to this ticket /// - public IList Tags { get; set; } + public List Tags { get; set; } /// /// Use to add tags in bulk updates /// /// - public IList AdditionalTags { get; set; } + public List AdditionalTags { get; set; } /// /// Use to Remove tags in bulk updates /// /// - public IList RemoveTags { get; set; } + public List RemoveTags { get; set; } /// /// This object explains how the ticket was created @@ -175,7 +175,7 @@ public class Ticket : ZenEntity /// /// Custom fields for the ticket. /// - public IList CustomFields { get; set; } + public List CustomFields { get; set; } /// /// The satisfaction rating of the ticket, if it exists @@ -185,13 +185,13 @@ public class Ticket : ZenEntity /// /// The ids of the sharing agreements used for this ticket. /// - public IList SharingAgreementIds { get; set; } + public List SharingAgreementIds { get; set; } /// /// The ids of the followup's created from this ticket. /// Ids are only visible once the ticket is closed /// - public IList FollowupIds { get; set; } + public List FollowupIds { get; set; } /// /// Update operations only @@ -204,7 +204,7 @@ public class Ticket : ZenEntity /// Update operations only /// List of macro IDs to be recorded in the ticket audit /// - public IList MacroIds { get; set; } + public List MacroIds { get; set; } /// /// Enterprise Accounts only. diff --git a/src/Speedygeek.ZendeskAPI/Models/Support/Users/Responses/UserListResponse.cs b/src/Speedygeek.ZendeskAPI/Models/Support/Users/Responses/UserListResponse.cs index 6000408..007529e 100644 --- a/src/Speedygeek.ZendeskAPI/Models/Support/Users/Responses/UserListResponse.cs +++ b/src/Speedygeek.ZendeskAPI/Models/Support/Users/Responses/UserListResponse.cs @@ -19,6 +19,6 @@ public class UserListResponse : ListResponseBase /// /// Total number of open tickets assigned to the user. /// - public dynamic OpenTicketCount { get; set; } + public long OpenTicketCount { get; set; } } } diff --git a/src/Speedygeek.ZendeskAPI/Models/Support/Users/Responses/UserResponse.cs b/src/Speedygeek.ZendeskAPI/Models/Support/Users/Responses/UserResponse.cs index e45a391..6340056 100644 --- a/src/Speedygeek.ZendeskAPI/Models/Support/Users/Responses/UserResponse.cs +++ b/src/Speedygeek.ZendeskAPI/Models/Support/Users/Responses/UserResponse.cs @@ -16,6 +16,6 @@ public class UserResponse /// /// Total number of open tickets assigned to the user. /// - public dynamic OpenTicketCount { get; set; } + public long OpenTicketCount { get; set; } } } diff --git a/src/Speedygeek.ZendeskAPI/Models/Support/Users/User.cs b/src/Speedygeek.ZendeskAPI/Models/Support/Users/User.cs index 8504ecd..9b49016 100644 --- a/src/Speedygeek.ZendeskAPI/Models/Support/Users/User.cs +++ b/src/Speedygeek.ZendeskAPI/Models/Support/Users/User.cs @@ -180,10 +180,10 @@ public class User : ZenEntity /// public bool TwoFactorAuthEnabled { get; } - /// - /// Values of custom fields in the user's profile. - /// - public dynamic UserFields { get; set; } + ///// + ///// Values of custom fields in the user's profile. + ///// + // public dynamic UserFields { get; set; } /// /// The user's primary identity is verified or not. diff --git a/src/Speedygeek.ZendeskAPI/Operations/BaseOperations.cs b/src/Speedygeek.ZendeskAPI/Operations/BaseOperations.cs index 932d4de..1d62941 100644 --- a/src/Speedygeek.ZendeskAPI/Operations/BaseOperations.cs +++ b/src/Speedygeek.ZendeskAPI/Operations/BaseOperations.cs @@ -21,7 +21,7 @@ namespace Speedygeek.ZendeskAPI public abstract class BaseOperations { private const string JSONTYPE = "application/json"; - private const HttpStatusCode TooManyRequests = (HttpStatusCode)429; + private const HttpStatusCode TooManyRequests = HttpStatusCode.TooManyRequests; private readonly IRESTClient _restClient; /// @@ -82,7 +82,7 @@ protected async Task SendAsync(HttpMethod httpMethod, stri if (response.IsSuccessStatusCode) { cancellationToken.ThrowIfCancellationRequested(); - using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(true); + using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(true); result = _restClient.Serializer.Deserialize(stream); } else if (response.StatusCode == TooManyRequests) @@ -93,8 +93,8 @@ protected async Task SendAsync(HttpMethod httpMethod, stri } else if (!response.IsSuccessStatusCode) { - var bodyString = await response.Content.ReadAsStringAsync().ConfigureAwait(true); - var message = $"Error {response.StatusCode} details: HEADERS: {response.Headers} BODY: {bodyString}"; + var bodyString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(true); + var message = $"Error {response.StatusCode}:{response.StatusCode:D} details: HEADERS: {response.Headers} BODY: {bodyString}"; throw new HttpRequestException(message); } @@ -104,7 +104,6 @@ protected async Task SendAsync(HttpMethod httpMethod, stri private static HttpContent BuildFormContent(Dictionary formData) { -#pragma warning disable CA2000 // Dispose objects before losing scope var fromContent = new MultipartFormDataContent(Constants.FormBoundary); foreach (var item in formData) @@ -127,7 +126,7 @@ private static HttpContent BuildFormContent(Dictionary formData) fromContent.Add(content, item.Key); } } -#pragma warning restore CA2000 // Dispose objects before losing scope + return fromContent; } @@ -158,7 +157,7 @@ protected async Task SendAyncAsync(HttpMethod httpMethod, stri } else if (!response.IsSuccessStatusCode) { - var bodyString = await response.Content.ReadAsStringAsync().ConfigureAwait(true); + var bodyString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(true); var message = $"Error {response.StatusCode} details: HEADERS: {response.Headers} BODY: {bodyString}"; throw new HttpRequestException(message); diff --git a/src/Speedygeek.ZendeskAPI/Operations/Support/ITicketOperations.cs b/src/Speedygeek.ZendeskAPI/Operations/Support/ITicketOperations.cs index 5016238..e4b2f41 100644 --- a/src/Speedygeek.ZendeskAPI/Operations/Support/ITicketOperations.cs +++ b/src/Speedygeek.ZendeskAPI/Operations/Support/ITicketOperations.cs @@ -269,11 +269,11 @@ public interface ITicketOperations Task GetIncidentsAsync(long id, CancellationToken cancellationToken = default); /// - /// will load the next page for a list of + /// will load the page for a list of /// - /// URL of the next page + /// URL of the page /// The cancellation token to cancel operation. /// Returns a - Task GetNextPageAsync(Uri nextPage, CancellationToken cancellationToken = default); + Task GetPageAsync(Uri page, CancellationToken cancellationToken = default); } } diff --git a/src/Speedygeek.ZendeskAPI/Operations/Support/TicketOperations.cs b/src/Speedygeek.ZendeskAPI/Operations/Support/TicketOperations.cs index cc97e18..21e3969 100644 --- a/src/Speedygeek.ZendeskAPI/Operations/Support/TicketOperations.cs +++ b/src/Speedygeek.ZendeskAPI/Operations/Support/TicketOperations.cs @@ -275,14 +275,15 @@ public Task GetIncidentsAsync(long id, CancellationToken can } /// - public Task GetNextPageAsync(Uri nextPage, CancellationToken cancellationToken = default) + public Task GetPageAsync(Uri page, CancellationToken cancellationToken = default) { - if (nextPage is null) + if (page is null) { - throw new ArgumentNullException(nameof(nextPage)); + throw new ArgumentNullException(nameof(page)); } - return SendAsync(HttpMethod.Get, nextPage.PathAndQuery, cancellationToken: cancellationToken); + // var pageUri = new Uri(page); + return SendAsync(HttpMethod.Get, page.PathAndQuery, cancellationToken: cancellationToken); } private static string GetSideLoadParam(string requestSuffix, TicketSideloads options, TicketPageParams pageParameters = default) diff --git a/src/Speedygeek.ZendeskAPI/Operations/Support/UserOperations.cs b/src/Speedygeek.ZendeskAPI/Operations/Support/UserOperations.cs index 9b0d286..1d4de32 100644 --- a/src/Speedygeek.ZendeskAPI/Operations/Support/UserOperations.cs +++ b/src/Speedygeek.ZendeskAPI/Operations/Support/UserOperations.cs @@ -257,6 +257,7 @@ public Task GetNextPageAsync(Uri nextPage, CancellationToken c throw new ArgumentNullException(nameof(nextPage)); } + // var pageUri = new Uri(nextPage); return SendAsync(HttpMethod.Get, nextPage.PathAndQuery, cancellationToken: cancellationToken); } diff --git a/src/Speedygeek.ZendeskAPI/Serialization/Context/SnakeCaseNamePolicy.cs b/src/Speedygeek.ZendeskAPI/Serialization/Context/SnakeCaseNamePolicy.cs new file mode 100644 index 0000000..92b43f1 --- /dev/null +++ b/src/Speedygeek.ZendeskAPI/Serialization/Context/SnakeCaseNamePolicy.cs @@ -0,0 +1,13 @@ +// Copyright (c) Elizabeth Schneider. All Rights Reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Text.Json; +using Speedygeek.ZendeskAPI.Utilities; + +namespace Speedygeek.ZendeskAPI.Serialization.Context +{ + internal class SnakeCaseNamePolicy : JsonNamingPolicy + { + public override string ConvertName(string name) => name.ToSnakeCase(); + } +} diff --git a/src/Speedygeek.ZendeskAPI/Serialization/Context/ZenJsonContext.cs b/src/Speedygeek.ZendeskAPI/Serialization/Context/ZenJsonContext.cs new file mode 100644 index 0000000..8039ed6 --- /dev/null +++ b/src/Speedygeek.ZendeskAPI/Serialization/Context/ZenJsonContext.cs @@ -0,0 +1,17 @@ +// Copyright (c) Elizabeth Schneider. All Rights Reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Text.Json.Serialization; +using Speedygeek.ZendeskAPI.Models.Support; + +namespace Speedygeek.ZendeskAPI.Serialization.Context +{ + // /// + // /// Context + // /// + // // [JsonSerializable(typeof(TicketResponse))] + // [JsonSerializable(typeof(Ticket))] + // public partial class ZenJsonContext : JsonSerializerContext + // { + // } +} diff --git a/src/Speedygeek.ZendeskAPI/Serialization/Converters/CollaboratorConverter.cs b/src/Speedygeek.ZendeskAPI/Serialization/Converters/CollaboratorConverter.cs index 888beda..70433eb 100644 --- a/src/Speedygeek.ZendeskAPI/Serialization/Converters/CollaboratorConverter.cs +++ b/src/Speedygeek.ZendeskAPI/Serialization/Converters/CollaboratorConverter.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; using Speedygeek.ZendeskAPI.Models.Support; namespace Speedygeek.ZendeskAPI.Serialization.Converters @@ -12,64 +12,57 @@ namespace Speedygeek.ZendeskAPI.Serialization.Converters /// /// converts an array of mixed type to a single type /// - internal class CollaboratorConverter : JsonConverter + public class CollaboratorConverter : JsonConverter> { - public override bool CanWrite { get => true; } - - public override bool CanConvert(Type objectType) - { - return typeof(List).Equals(objectType); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + /// + public override List Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - var token = JToken.ReadFrom(reader); - var collaborators = new List(); - foreach (var item in token.Children()) + var list = new List(); + switch (reader.TokenType) { - if (item.Type == JTokenType.Object) - { - var collaborator = item.ToObject(); - collaborators.Add(collaborator); - } - else if (item.Type == JTokenType.String) - { - var collaborator = new Collaborator { Email = item.ToObject() }; - collaborators.Add(collaborator); - } - else if (item.Type == JTokenType.Integer) - { - var collaborator = new Collaborator { Id = item.ToObject() }; - collaborators.Add(collaborator); - } + case JsonTokenType.Null: + return list; + case JsonTokenType.StartArray: + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + { + break; + } + else if (reader.TokenType == JsonTokenType.StartObject) + { + var item = JsonSerializer.Deserialize(ref reader, options); + if (item != null) + { + list.Add(item); + } + } + else if (reader.TokenType == JsonTokenType.String) + { + var collaborator = new Collaborator { Email = reader.GetString() }; + list.Add(collaborator); + } + else if (reader.TokenType == JsonTokenType.Number) + { + var collaborator = new Collaborator { Id = reader.GetInt64() }; + list.Add(collaborator); + } + else if (reader.TokenType == JsonTokenType.Null) + { + continue; + } + } + + break; } - return collaborators; + return list; } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + /// + public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options) { - if (value is List collaborators && collaborators?.Count > 0) - { - writer.WriteStartArray(); - foreach (var collaborator in collaborators) - { - if (collaborator.Id != 0) - { - serializer.Serialize(writer, collaborator.Id); - } - else if (!string.IsNullOrWhiteSpace(collaborator.Name) && !string.IsNullOrWhiteSpace(collaborator.Email)) - { - serializer.Serialize(writer, collaborator); - } - else if (!string.IsNullOrWhiteSpace(collaborator.Email)) - { - serializer.Serialize(writer, collaborator.Email); - } - } - - writer.WriteEndArray(); - } + JsonSerializer.Serialize(writer, value, options); } } } diff --git a/src/Speedygeek.ZendeskAPI/Serialization/Converters/CollaboratorConverter_Old.cs b/src/Speedygeek.ZendeskAPI/Serialization/Converters/CollaboratorConverter_Old.cs new file mode 100644 index 0000000..03006ab --- /dev/null +++ b/src/Speedygeek.ZendeskAPI/Serialization/Converters/CollaboratorConverter_Old.cs @@ -0,0 +1,75 @@ +// Copyright (c) Elizabeth Schneider. All Rights Reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Speedygeek.ZendeskAPI.Models.Support; + +namespace Speedygeek.ZendeskAPI.Serialization.Converters +{ + /// + /// converts an array of mixed type to a single type + /// + internal class CollaboratorConverter_Old : JsonConverter + { + public override bool CanWrite { get => true; } + + public override bool CanConvert(Type objectType) + { + return typeof(List).Equals(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var token = JToken.ReadFrom(reader); + var collaborators = new List(); + foreach (var item in token.Children()) + { + if (item.Type == JTokenType.Object) + { + var collaborator = item.ToObject(); + collaborators.Add(collaborator); + } + else if (item.Type == JTokenType.String) + { + var collaborator = new Collaborator { Email = item.ToObject() }; + collaborators.Add(collaborator); + } + else if (item.Type == JTokenType.Integer) + { + var collaborator = new Collaborator { Id = item.ToObject() }; + collaborators.Add(collaborator); + } + } + + return collaborators; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value is List collaborators && collaborators?.Count > 0) + { + writer.WriteStartArray(); + foreach (var collaborator in collaborators) + { + if (collaborator.Id != 0) + { + serializer.Serialize(writer, collaborator.Id); + } + else if (!string.IsNullOrWhiteSpace(collaborator.Name) && !string.IsNullOrWhiteSpace(collaborator.Email)) + { + serializer.Serialize(writer, collaborator); + } + else if (!string.IsNullOrWhiteSpace(collaborator.Email)) + { + serializer.Serialize(writer, collaborator.Email); + } + } + + writer.WriteEndArray(); + } + } + } +} diff --git a/src/Speedygeek.ZendeskAPI/Serialization/Converters/EnumToStringConverter.cs b/src/Speedygeek.ZendeskAPI/Serialization/Converters/EnumToStringConverter.cs new file mode 100644 index 0000000..bd2d2f0 --- /dev/null +++ b/src/Speedygeek.ZendeskAPI/Serialization/Converters/EnumToStringConverter.cs @@ -0,0 +1,377 @@ +// Copyright (c) Elizabeth Schneider. All Rights Reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.Serialization; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Speedygeek.ZendeskAPI.Serialization.Converters +{ + /// + /// Convert for Enum to String + /// + public class EnumToStringConverter : JsonConverterFactory + { + private readonly JsonNamingPolicy _namingPolicy; + private readonly bool _allowIntegerValues; + + /// + /// Initializes a new instance of the class. + /// + public EnumToStringConverter() + : this(namingPolicy: null, allowIntegerValues: true) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// name policy used in name casing + /// can numbers be used + public EnumToStringConverter(JsonNamingPolicy namingPolicy = null, bool allowIntegerValues = true) + { + _namingPolicy = namingPolicy; + _allowIntegerValues = allowIntegerValues; + } + + /// + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsEnum; + } + + /// + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return (JsonConverter)Activator.CreateInstance( + typeof(Converter<>).MakeGenericType(typeToConvert), + BindingFlags.Instance | BindingFlags.Public, + binder: null, + args: new object[] { _namingPolicy, _allowIntegerValues }, + culture: null); + } + + private class Converter : JsonConverter + where T : struct, Enum + { + private static readonly Type _enumType = typeof(T); + private static readonly TypeCode _enumTypeCode = Type.GetTypeCode(_enumType); + private readonly bool _allowIntegerValues; + private readonly bool _isFlags; + private readonly Dictionary _rawToTransformed; + private readonly Dictionary _transformedToRaw; + + public Converter(JsonNamingPolicy namingPolicy = null, bool allowIntegerValues = true) + { + _allowIntegerValues = allowIntegerValues; + + _isFlags = _enumType.IsDefined(typeof(FlagsAttribute), true); + + var builtInNames = _enumType.GetEnumNames(); + var builtInValues = _enumType.GetEnumValues(); + + _rawToTransformed = new Dictionary(); + _transformedToRaw = new Dictionary(); + + for (var i = 0; i < builtInNames.Length; i++) + { + var enumValue = (T)builtInValues.GetValue(i); + var rawValue = GetEnumValue(enumValue); + + var name = builtInNames[i]; + + string transformedName; + if (namingPolicy == null) + { + var field = _enumType.GetField(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static)!; + var enumMemberAttribute = field.GetCustomAttribute(true); + transformedName = enumMemberAttribute?.Value ?? name; + } + else + { + transformedName = namingPolicy.ConvertName(name) ?? name; + } + + _rawToTransformed[rawValue] = new EnumInfo + { + Name = transformedName, + EnumValue = enumValue, + RawValue = rawValue, + }; + _transformedToRaw[transformedName] = new EnumInfo + { + Name = name, + EnumValue = enumValue, + RawValue = rawValue, + }; + } + } + + private static ulong GetEnumValue(object value) + { + switch (_enumTypeCode) + { + case TypeCode.Int32: + return (ulong)(int)value; + case TypeCode.UInt32: + return (uint)value; + case TypeCode.UInt64: + return (ulong)value; + case TypeCode.Int64: + return (ulong)(long)value; + case TypeCode.SByte: + return (ulong)(sbyte)value; + case TypeCode.Byte: + return (byte)value; + case TypeCode.Int16: + return (ulong)(short)value; + case TypeCode.UInt16: + return (ushort)value; + } + + throw new NotSupportedException(); + } + + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var token = reader.TokenType; + + if (token == JsonTokenType.String) + { + var enumString = reader.GetString(); + + // Case sensitive search attempted first. + if (_transformedToRaw.TryGetValue(enumString, out var enumInfo)) + { + return (T)Enum.ToObject(_enumType, enumInfo.RawValue); + } + + if (_isFlags) + { + ulong calculatedValue = 0; + + var flagValues = enumString.Split(", "); + foreach (var flagValue in flagValues) + { + // Case sensitive search attempted first. + if (_transformedToRaw.TryGetValue(flagValue, out enumInfo)) + { + calculatedValue |= enumInfo.RawValue; + } + else + { + // Case insensitive search attempted second. + var matched = false; + foreach (var enumItem in _transformedToRaw) + { + if (string.Equals(enumItem.Key, flagValue, StringComparison.OrdinalIgnoreCase)) + { + calculatedValue |= enumItem.Value.RawValue; + matched = true; + break; + } + } + + if (!matched) + { + throw new NotSupportedException(); + } + } + } + + return (T)Enum.ToObject(_enumType, calculatedValue); + } + else + { + // Case insensitive search attempted second. + foreach (var enumItem in _transformedToRaw) + { + if (string.Equals(enumItem.Key, enumString, StringComparison.OrdinalIgnoreCase)) + { + return (T)Enum.ToObject(_enumType, enumItem.Value.RawValue); + } + } + } + + throw new NotSupportedException(); + } + + if (token != JsonTokenType.Number || !_allowIntegerValues) + { + throw new NotSupportedException(); + } + + switch (_enumTypeCode) + { + // Switch cases ordered by expected frequency + case TypeCode.Int32: + if (reader.TryGetInt32(out var int32)) + { + return (T)Enum.ToObject(_enumType, int32); + } + + break; + case TypeCode.UInt32: + if (reader.TryGetUInt32(out var uint32)) + { + return (T)Enum.ToObject(_enumType, uint32); + } + + break; + case TypeCode.UInt64: + if (reader.TryGetUInt64(out var uint64)) + { + return (T)Enum.ToObject(_enumType, uint64); + } + + break; + case TypeCode.Int64: + if (reader.TryGetInt64(out var int64)) + { + return (T)Enum.ToObject(_enumType, int64); + } + + break; + case TypeCode.SByte: + if (reader.TryGetSByte(out var byte8)) + { + return (T)Enum.ToObject(_enumType, byte8); + } + + break; + case TypeCode.Byte: + if (reader.TryGetByte(out var ubyte8)) + { + return (T)Enum.ToObject(_enumType, ubyte8); + } + + break; + case TypeCode.Int16: + if (reader.TryGetInt16(out var int16)) + { + return (T)Enum.ToObject(_enumType, int16); + } + + break; + case TypeCode.UInt16: + if (reader.TryGetUInt16(out var uint16)) + { + return (T)Enum.ToObject(_enumType, uint16); + } + + break; + } + + throw new NotSupportedException(); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + var rawValue = GetEnumValue(value); + + if (_rawToTransformed.TryGetValue(rawValue, out var enumInfo)) + { + writer.WriteStringValue(enumInfo.Name); + return; + } + + if (_isFlags) + { + ulong calculatedValue = 0; + + var builder = new StringBuilder(); + foreach (var enumItem in _rawToTransformed) + { + enumInfo = enumItem.Value; + + // Definitions with 'None' should hit the cache case. + if (!value.HasFlag(enumInfo.EnumValue) + || enumInfo.RawValue == 0) + { + continue; + } + + // Track the value to make sure all bits are represented. + calculatedValue |= enumInfo.RawValue; + + if (builder.Length > 0) + { + builder.Append(", "); + } + + builder.Append(enumInfo.Name); + } + + if (calculatedValue == rawValue) + { + writer.WriteStringValue(builder.ToString()); + return; + } + } + + if (!_allowIntegerValues) + { + throw new NotSupportedException(); + } + + switch (_enumTypeCode) + { + case TypeCode.Int32: + writer.WriteNumberValue((int)rawValue); + break; + case TypeCode.UInt32: + writer.WriteNumberValue((uint)rawValue); + break; + case TypeCode.UInt64: + writer.WriteNumberValue(rawValue); + break; + case TypeCode.Int64: + writer.WriteNumberValue((long)rawValue); + break; + case TypeCode.Int16: + writer.WriteNumberValue((short)rawValue); + break; + case TypeCode.UInt16: + writer.WriteNumberValue((ushort)rawValue); + break; + case TypeCode.Byte: + writer.WriteNumberValue((byte)rawValue); + break; + case TypeCode.SByte: + writer.WriteNumberValue((sbyte)rawValue); + break; + default: + throw new NotSupportedException(); + } + } + + private class EnumInfo + { + /// + /// Name + /// +#pragma warning disable SA1401 // Fields should be private + public string Name; +#pragma warning restore SA1401 // Fields should be private + + /// + /// Enum value + /// +#pragma warning disable SA1401 // Fields should be private + public T EnumValue; +#pragma warning restore SA1401 // Fields should be private + + /// + /// value + /// +#pragma warning disable SA1401 // Fields should be private + public ulong RawValue; +#pragma warning restore SA1401 // Fields should be private + } + } + } +} diff --git a/src/Speedygeek.ZendeskAPI/Serialization/JsonDotNetSerializer.cs b/src/Speedygeek.ZendeskAPI/Serialization/JsonDotNetSerializer.cs index 64afaf5..13a9595 100644 --- a/src/Speedygeek.ZendeskAPI/Serialization/JsonDotNetSerializer.cs +++ b/src/Speedygeek.ZendeskAPI/Serialization/JsonDotNetSerializer.cs @@ -33,7 +33,7 @@ public JsonDotNetSerializer(JsonSerializerSettings settings = null) }; _serializerSettings.Converters.Add(new StringEnumConverter(new SnakeCaseNamingStrategy())); - _serializerSettings.Converters.Add(new CollaboratorConverter()); + _serializerSettings.Converters.Add(new CollaboratorConverter_Old()); #if DEBUG _serializerSettings.Formatting = Formatting.Indented; diff --git a/src/Speedygeek.ZendeskAPI/Serialization/TextJsonSerializer.cs b/src/Speedygeek.ZendeskAPI/Serialization/TextJsonSerializer.cs new file mode 100644 index 0000000..55841f0 --- /dev/null +++ b/src/Speedygeek.ZendeskAPI/Serialization/TextJsonSerializer.cs @@ -0,0 +1,64 @@ +// Copyright (c) Elizabeth Schneider. All Rights Reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; +using Speedygeek.ZendeskAPI.Serialization.Context; +using Speedygeek.ZendeskAPI.Serialization.Converters; + +namespace Speedygeek.ZendeskAPI.Serialization +{ + /// + /// ISerializer implementation that uses System.Text.Json. + /// + public class TextJsonSerializer : ISerializer + { + private readonly JsonNamingPolicy _policy; + private readonly JsonSerializerOptions _options; + + // private readonly ZenJsonContext _context; + + /// + /// Initializes a new instance of the class. + /// + public TextJsonSerializer() + { + _policy = new SnakeCaseNamePolicy(); + _options = new JsonSerializerOptions + { + WriteIndented = false, + PropertyNamingPolicy = _policy, + DictionaryKeyPolicy = _policy, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + IgnoreReadOnlyProperties = false, + Converters = + { + new EnumToStringConverter(_policy), + new CollaboratorConverter(), + }, + }; + + // _context = new ZenJsonContext(_options); + } + + /// + public T Deserialize(Stream stream) + { + if (stream.Position != 0) + { + stream.Position = 0; + } + + return JsonSerializer.Deserialize(stream, _options); + } + + /// + public string Serialize(object data) + { + return JsonSerializer.Serialize(data, _options); + } + } +} diff --git a/src/Speedygeek.ZendeskAPI/Serialization/ZendeskContractResolver.cs b/src/Speedygeek.ZendeskAPI/Serialization/ZendeskContractResolver.cs index 8d95084..6c2b65e 100644 --- a/src/Speedygeek.ZendeskAPI/Serialization/ZendeskContractResolver.cs +++ b/src/Speedygeek.ZendeskAPI/Serialization/ZendeskContractResolver.cs @@ -16,7 +16,7 @@ public class ZendeskContractResolver : DefaultContractResolver /// /// Static Instance used to speed and memory /// - public static readonly ZendeskContractResolver Instance = new ZendeskContractResolver(); + public static readonly ZendeskContractResolver Instance = new(); /// /// Initializes a new instance of the class. diff --git a/src/Speedygeek.ZendeskAPI/Speedygeek.ZendeskAPI.csproj b/src/Speedygeek.ZendeskAPI/Speedygeek.ZendeskAPI.csproj index 5c66201..33eab4d 100644 --- a/src/Speedygeek.ZendeskAPI/Speedygeek.ZendeskAPI.csproj +++ b/src/Speedygeek.ZendeskAPI/Speedygeek.ZendeskAPI.csproj @@ -1,12 +1,12 @@  - netstandard2.0 + net6.0 true StrongNameKey.snk false true - Elizabeth Schneider and contributors + Elizabeth Schneider, contributors Speedy-Geek Zendesk SDK for .NET Copyright (c) Elizabeth Schneider $([System.DateTime]::Now.ToString(yyyy)) @@ -14,14 +14,18 @@ false MIT https://github.com/Speedygeek/ZendeskAPI + NU5105,CS0400,CS1591 + True + True - + - - - + + + + diff --git a/src/Speedygeek.ZendeskAPI/Utilities/Helpers.cs b/src/Speedygeek.ZendeskAPI/Utilities/Helpers.cs index 7f65de6..f19dbf0 100644 --- a/src/Speedygeek.ZendeskAPI/Utilities/Helpers.cs +++ b/src/Speedygeek.ZendeskAPI/Utilities/Helpers.cs @@ -56,17 +56,17 @@ public static string ToInvariantString(this int value) return value.ToString(CultureInfo.InvariantCulture); } - /// - /// converts string to lower Snake Case - /// - /// string to convert - /// string in snake case - public static string ToSnakeCase(this string value) - { - var strategy = new SnakeCaseNamingStrategy(); + ///// + ///// converts string to lower Snake Case + ///// + ///// string to convert + ///// string in snake case + // public static string ToSnakeCase(this string value) + // { + // var strategy = new SnakeCaseNamingStrategy(); - return strategy.GetPropertyName(value, false); - } + // return strategy.GetPropertyName(value, false); + // } /// /// Converts to a comma separated list diff --git a/src/Speedygeek.ZendeskAPI/Utilities/StringUtils.cs b/src/Speedygeek.ZendeskAPI/Utilities/StringUtils.cs new file mode 100644 index 0000000..a932891 --- /dev/null +++ b/src/Speedygeek.ZendeskAPI/Utilities/StringUtils.cs @@ -0,0 +1,95 @@ +// Copyright (c) Elizabeth Schneider. All Rights Reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Text; + +namespace Speedygeek.ZendeskAPI.Utilities +{ + internal enum SeparatedCaseState + { + Start, + Lower, + Upper, + NewWord, + } + + /// + /// String helpers + /// + public static class StringUtils + { + /// + /// converts string to lower Snake Case + /// + /// string to convert + /// string in snake case + public static string ToSnakeCase(this string value) => ToSeparatedCase(value, '_'); + + private static string ToSeparatedCase(string s, char separator) + { + if (string.IsNullOrWhiteSpace(s)) + { + return s; + } + + StringBuilder sb = new(); + var state = SeparatedCaseState.Start; + + for (var i = 0; i < s.Length; i++) + { + if (s[i] == ' ') + { + if (state != SeparatedCaseState.Start) + { + state = SeparatedCaseState.NewWord; + } + } + else if (char.IsUpper(s[i])) + { + switch (state) + { + case SeparatedCaseState.Upper: + var hasNext = i + 1 < s.Length; + if (i > 0 && hasNext) + { + var nextChar = s[i + 1]; + if (!char.IsUpper(nextChar) && nextChar != separator) + { + sb.Append(separator); + } + } + + break; + case SeparatedCaseState.Lower: + case SeparatedCaseState.NewWord: + sb.Append(separator); + break; + } + + char c; + c = char.ToLowerInvariant(s[i]); + sb.Append(c); + + state = SeparatedCaseState.Upper; + } + else if (s[i] == separator) + { + sb.Append(separator); + state = SeparatedCaseState.Start; + } + else + { + if (state == SeparatedCaseState.NewWord) + { + sb.Append(separator); + } + + sb.Append(s[i]); + state = SeparatedCaseState.Lower; + } + } + + return sb.ToString(); + } + } +} diff --git a/src/Speedygeek.ZendeskAPI/ZendeskClient.cs b/src/Speedygeek.ZendeskAPI/ZendeskClient.cs index 0cd968c..b74b8db 100644 --- a/src/Speedygeek.ZendeskAPI/ZendeskClient.cs +++ b/src/Speedygeek.ZendeskAPI/ZendeskClient.cs @@ -19,7 +19,7 @@ public class ZendeskClient : IZendeskClient /// client used to make HTTP request public ZendeskClient(IRESTClient restClient) => _restClient = restClient; - private Lazy SupportLazy => new Lazy(() => new SupportOperations(_restClient)); + private Lazy SupportLazy => new(() => new SupportOperations(_restClient)); /// public ISupportOperations Support => SupportLazy.Value; diff --git a/src/global.json b/src/global.json index b6297cd..bc1319b 100644 --- a/src/global.json +++ b/src/global.json @@ -1,5 +1,5 @@ { - "sdk": { - "version": "3.1.202" - } - } \ No newline at end of file + //"sdk": { + // "version": "5.0.100" + //} +} \ No newline at end of file