From 58cf5952284073b703754a2175ab256d72666718 Mon Sep 17 00:00:00 2001 From: Todd Date: Mon, 4 Dec 2023 16:21:54 -0600 Subject: [PATCH 1/6] introducing Flurl.Http.Newtonsoft --- .editorconfig | 4 ++ Flurl.sln | 7 +++ .../Flurl.Http.Newtonsoft.csproj | 33 +++++++++++++ .../NewtonsoftJsonSerializer.cs | 46 +++++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 src/Flurl.Http.Newtonsoft/Flurl.Http.Newtonsoft.csproj create mode 100644 src/Flurl.Http.Newtonsoft/NewtonsoftJsonSerializer.cs diff --git a/.editorconfig b/.editorconfig index cddb73ba..c0b7230f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,6 +8,10 @@ indent_style = tab indent_size = 4 trim_trailing_whitespace = true +[*.csproj] +indent_style = space +indent_size = 2 + # VS/.NET extensions # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference diff --git a/Flurl.sln b/Flurl.sln index 1001e2a2..ddb267eb 100644 --- a/Flurl.sln +++ b/Flurl.sln @@ -14,6 +14,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flurl.Test", "test\Flurl.Te EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flurl.CodeGen", "src\Flurl.CodeGen\Flurl.CodeGen.csproj", "{BE943E04-705F-42B1-BF95-A0642D9CA51D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flurl.Http.Newtonsoft", "src\Flurl.Http.Newtonsoft\Flurl.Http.Newtonsoft.csproj", "{BDF18B21-6B7B-4945-BB86-9CB8A8B1E93A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,6 +38,10 @@ Global {BE943E04-705F-42B1-BF95-A0642D9CA51D}.Debug|Any CPU.Build.0 = Debug|Any CPU {BE943E04-705F-42B1-BF95-A0642D9CA51D}.Release|Any CPU.ActiveCfg = Release|Any CPU {BE943E04-705F-42B1-BF95-A0642D9CA51D}.Release|Any CPU.Build.0 = Release|Any CPU + {BDF18B21-6B7B-4945-BB86-9CB8A8B1E93A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BDF18B21-6B7B-4945-BB86-9CB8A8B1E93A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BDF18B21-6B7B-4945-BB86-9CB8A8B1E93A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BDF18B21-6B7B-4945-BB86-9CB8A8B1E93A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -45,6 +51,7 @@ Global {D7AC6172-73DD-468D-955A-3562F2BE303B} = {B82E8094-AFA9-466E-9E60-473B7B89AFE2} {DF68EB0E-9566-4577-B709-291520383F8D} = {86A5ACB4-F3B3-4395-A5D5-924C9F35F628} {BE943E04-705F-42B1-BF95-A0642D9CA51D} = {B82E8094-AFA9-466E-9E60-473B7B89AFE2} + {BDF18B21-6B7B-4945-BB86-9CB8A8B1E93A} = {B82E8094-AFA9-466E-9E60-473B7B89AFE2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {61289482-AC5A-44E1-AEA1-76A3F3CCB6A4} diff --git a/src/Flurl.Http.Newtonsoft/Flurl.Http.Newtonsoft.csproj b/src/Flurl.Http.Newtonsoft/Flurl.Http.Newtonsoft.csproj new file mode 100644 index 00000000..9c87c813 --- /dev/null +++ b/src/Flurl.Http.Newtonsoft/Flurl.Http.Newtonsoft.csproj @@ -0,0 +1,33 @@ + + + net6.0;netstandard2.0;net461 + 9.0 + True + Flurl.Http.Newtonsoft + 0.9.0-pre1 + Todd Menier + A Newtonsoft-based JSON serializer for Flurl.Http 4.0 and above. + https://flurl.dev + icon.png + https://github.com/tmenier/Flurl.git + git + MIT + flurl http json + https://github.com/tmenier/Flurl/releases + true + + + + true + true + snupkg + true + + + + + + + + + diff --git a/src/Flurl.Http.Newtonsoft/NewtonsoftJsonSerializer.cs b/src/Flurl.Http.Newtonsoft/NewtonsoftJsonSerializer.cs new file mode 100644 index 00000000..96cf4c6b --- /dev/null +++ b/src/Flurl.Http.Newtonsoft/NewtonsoftJsonSerializer.cs @@ -0,0 +1,46 @@ +using System.IO; +using Flurl.Http.Configuration; +using Newtonsoft.Json; + +namespace Flurl.Http.Newtonsoft +{ + /// + /// ISerializer implementation based on Newtonsoft.Json. + /// Default serializer used in calls to GetJsonAsync, PostJsonAsync, etc. + /// + public class NewtonsoftJsonSerializer : ISerializer + { + private readonly JsonSerializerSettings _settings; + + /// + /// Initializes a new instance of the class. + /// + /// Settings to control (de)serialization behavior. + public NewtonsoftJsonSerializer(JsonSerializerSettings settings = null) { + _settings = settings; + } + + /// + /// Serializes the specified object to a JSON string. + /// + /// The object to serialize. + public string Serialize(object obj) => JsonConvert.SerializeObject(obj, _settings); + + /// + /// Deserializes the specified JSON string to an object of type T. + /// + /// The JSON string to deserialize. + public T Deserialize(string s) => JsonConvert.DeserializeObject(s, _settings); + + /// + /// Deserializes the specified stream to an object of type T. + /// + /// The stream to deserialize. + public T Deserialize(Stream stream) { + // https://www.newtonsoft.com/json/help/html/Performance.htm#MemoryUsage + using var sr = new StreamReader(stream); + using var jr = new JsonTextReader(sr); + return JsonSerializer.CreateDefault(_settings).Deserialize(jr); + } + } +} \ No newline at end of file From 2599242f8a74cf3dac975db579e0247eb196a774 Mon Sep 17 00:00:00 2001 From: Todd Date: Mon, 4 Dec 2023 16:22:20 -0600 Subject: [PATCH 2/6] normalizing project files --- src/Flurl.Http/Flurl.Http.csproj | 18 +++--------------- src/Flurl/Flurl.csproj | 16 ++-------------- 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/src/Flurl.Http/Flurl.Http.csproj b/src/Flurl.Http/Flurl.Http.csproj index 05f5904e..ee479a80 100644 --- a/src/Flurl.Http/Flurl.Http.csproj +++ b/src/Flurl.Http/Flurl.Http.csproj @@ -9,13 +9,12 @@ A fluent, portable, testable HTTP client library. https://flurl.dev icon.png - https://pbs.twimg.com/profile_images/534024476296376320/IuPGZ_bX_400x400.png https://github.com/tmenier/Flurl.git git MIT rest http httpclient json url uri tdd https://github.com/tmenier/Flurl/releases - false + true @@ -25,17 +24,10 @@ true - - bin\Release\Flurl.Http.xml - - - - bin\Debug\Flurl.Http.xml - - - + + @@ -47,8 +39,4 @@ - - - - \ No newline at end of file diff --git a/src/Flurl/Flurl.csproj b/src/Flurl/Flurl.csproj index efbe4fb0..c3092eff 100644 --- a/src/Flurl/Flurl.csproj +++ b/src/Flurl/Flurl.csproj @@ -8,13 +8,12 @@ A fluent, portable URL builder. To make HTTP calls off the fluent chain, check out Flurl.Http. https://flurl.dev icon.png - https://pbs.twimg.com/profile_images/534024476296376320/IuPGZ_bX_400x400.png https://github.com/tmenier/Flurl.git git MIT fluent url uri querystring builder https://github.com/tmenier/Flurl/releases - false + true @@ -24,23 +23,12 @@ true - - bin\Release\Flurl.xml - - - - bin\Debug\Flurl.xml - - + - - - - \ No newline at end of file From 3ef55a3406db3ab55db5b24630b0f416716b4049 Mon Sep 17 00:00:00 2001 From: Todd Date: Mon, 4 Dec 2023 19:57:57 -0600 Subject: [PATCH 3/6] bring back support for dynamics via Flurl.Http.Newtonsoft --- src/Flurl.CodeGen/HttpExtensionMethod.cs | 6 +- src/Flurl.CodeGen/Metadata.cs | 7 ++ src/Flurl.CodeGen/Program.cs | 99 ++++++++++++----- src/Flurl.Http.Newtonsoft/ExtensionMethods.cs | 51 +++++++++ .../Flurl.Http.Newtonsoft.csproj | 6 +- .../GeneratedExtensions.cs | 104 ++++++++++++++++++ 6 files changed, 242 insertions(+), 31 deletions(-) create mode 100644 src/Flurl.Http.Newtonsoft/ExtensionMethods.cs create mode 100644 src/Flurl.Http.Newtonsoft/GeneratedExtensions.cs diff --git a/src/Flurl.CodeGen/HttpExtensionMethod.cs b/src/Flurl.CodeGen/HttpExtensionMethod.cs index 2dbda5e7..e3dfd843 100644 --- a/src/Flurl.CodeGen/HttpExtensionMethod.cs +++ b/src/Flurl.CodeGen/HttpExtensionMethod.cs @@ -47,7 +47,8 @@ public HttpExtensionMethod(string verb, bool isGeneric, string reqBodyType, stri public string ResponseBodyType { get; } public string TaskArg => ResponseBodyType switch { - "Json" => "T", + "Json" => IsGeneric ? "T" : "dynamic", + "JsonList" => "IList", "String" => "string", "Stream" => "Stream", "Bytes" => "byte[]", @@ -55,7 +56,8 @@ public HttpExtensionMethod(string verb, bool isGeneric, string reqBodyType, stri }; public string ReturnTypeDescription => ResponseBodyType switch { - "Json" => "the JSON response body deserialized to an object of type T", + "Json" => "the JSON response body deserialized to " + (IsGeneric ? "an object of type T" : "a dynamic"), + "JsonList" => "the JSON response body deserialized to a list of dynamics", "String" => "the response body as a string", "Stream" => "the response body as a Stream", "Bytes" => "the response body as a byte array", diff --git a/src/Flurl.CodeGen/Metadata.cs b/src/Flurl.CodeGen/Metadata.cs index d2c0ecaa..6c80e567 100644 --- a/src/Flurl.CodeGen/Metadata.cs +++ b/src/Flurl.CodeGen/Metadata.cs @@ -158,6 +158,13 @@ where IsSupportedCombo(verb, reqType, respType, extendedArg.Type) let isGenenric = (respType == "Json") select new HttpExtensionMethod(verb, isGenenric, reqType, respType) { ExtendedTypeArg = extendedArg }; + /// + /// Additional HTTP-calling methods that return dynamic or IList<dynamic>, supported only with Flurl.Http.Newtonsoft. + /// + public static IEnumerable GetDynamicReturningExtensions(MethodArg extendedArg) => + from respType in new[] { "Json", "JsonList" } + select new HttpExtensionMethod("Get", false, null, respType) { ExtendedTypeArg = extendedArg }; + public static IEnumerable GetMiscAsyncExtensions(MethodArg extendedArg) { // a couple oddballs diff --git a/src/Flurl.CodeGen/Program.cs b/src/Flurl.CodeGen/Program.cs index 280d7957..0d3f1467 100644 --- a/src/Flurl.CodeGen/Program.cs +++ b/src/Flurl.CodeGen/Program.cs @@ -73,6 +73,37 @@ static int Main(string[] args) { .WriteLine("}"); } + path = codeRoot + @"\src\Flurl.Http.Newtonsoft\GeneratedExtensions.cs"; + if (!File.Exists(path)) { + ShowError("Code file not found: " + Path.GetFullPath(path)); + return 2; + } + + File.WriteAllText(path, ""); + using (var writer = new CodeWriter(path)) { + writer + .WriteLine("// This file was auto-generated by Flurl.CodeGen. Do not edit directly.") + .WriteLine("using System;") + .WriteLine("using System.Collections.Generic;") + .WriteLine("using System.Net.Http;") + .WriteLine("using System.Threading;") + .WriteLine("using System.Threading.Tasks;") + .WriteLine("") + .WriteLine("namespace Flurl.Http.Newtonsoft") + .WriteLine("{") + .WriteLine("/// ") + .WriteLine("/// Fluent extension methods on String, Url, Uri, and IFlurlRequest.") + .WriteLine("/// ") + .WriteLine("public static class GeneratedExtensions") + .WriteLine("{"); + + WriteDynamicReturningMethods(writer); + + writer + .WriteLine("}") + .WriteLine("}"); + } + Console.WriteLine("File writing succeeded."); return 0; } @@ -107,42 +138,54 @@ private static void WriteUrlBuilderExtensionMethods(CodeWriter writer) { private static void WriteHttpExtensionMethods(CodeWriter writer) { var reqArg = _extendedArgs[0]; - foreach (var xm in Metadata.GetHttpCallingExtensions(reqArg)) { - Console.WriteLine($"writing {xm.Name} for IFlurlRequest..."); - xm.Write(writer, () => { - var args = new List(); - var genericArg = xm.IsGeneric ? "" : ""; - - args.Add( - xm.HttpVerb == null ? "verb" : - xm.HttpVerb == "Patch" ? "new HttpMethod(\"PATCH\")" : // there's no HttpMethod.Patch - "HttpMethod." + xm.HttpVerb); - - args.Add(xm.HasRequestBody ? "content" : "null"); - args.Add("completionOption"); - args.Add("cancellationToken"); - - if (xm.RequestBodyType != null) { - writer.WriteLine("var content = new Captured@0Content(@1);", - xm.RequestBodyType, - xm.RequestBodyType == "String" ? "body" : $"request.Settings.{xm.RequestBodyType}Serializer.Serialize(body)"); - } - - var receive = (xm.ResponseBodyType != null) ? $".Receive{xm.ResponseBodyType}{genericArg}()" : ""; - writer.WriteLine($"return {reqArg.Name}.SendAsync({string.Join(", ", args)}){receive};"); - }); - } + foreach (var xm in Metadata.GetHttpCallingExtensions(reqArg)) + WriteHttpExtensionMethod(writer, xm); foreach (var xarg in _extendedArgs.Skip(1)) { // skip 1 because these don't apply to IFlurlRequest var all = Metadata.GetHttpCallingExtensions(xarg) .Concat(Metadata.GetMiscAsyncExtensions(xarg)) .Concat(Metadata.GetRequestReturningExtensions(xarg)); - foreach (var xm in all) { - Console.WriteLine($"writing {xm.Name} for {xarg.Type}..."); + foreach (var xm in all) xm.Write(writer, $"new FlurlRequest({xarg.Name})"); - } } } + + private static void WriteDynamicReturningMethods(CodeWriter writer) { + var reqArg = _extendedArgs[0]; + + foreach (var xm in Metadata.GetDynamicReturningExtensions(reqArg)) + WriteHttpExtensionMethod(writer, xm); + + foreach (var xarg in _extendedArgs.Skip(1)) { // skip 1 because these don't apply to IFlurlRequest + foreach (var xm in Metadata.GetDynamicReturningExtensions(xarg)) + xm.Write(writer, $"new FlurlRequest({xarg.Name})"); + } + } + + private static void WriteHttpExtensionMethod(CodeWriter writer, HttpExtensionMethod xm) { + xm.Write(writer, () => { + var args = new List(); + var genericArg = xm.IsGeneric ? "" : ""; + + args.Add( + xm.HttpVerb == null ? "verb" : + xm.HttpVerb == "Patch" ? "new HttpMethod(\"PATCH\")" : // there's no HttpMethod.Patch + "HttpMethod." + xm.HttpVerb); + + args.Add(xm.HasRequestBody ? "content" : "null"); + args.Add("completionOption"); + args.Add("cancellationToken"); + + if (xm.RequestBodyType != null) { + writer.WriteLine("var content = new Captured@0Content(@1);", + xm.RequestBodyType, + xm.RequestBodyType == "String" ? "body" : $"request.Settings.{xm.RequestBodyType}Serializer.Serialize(body)"); + } + + var receive = (xm.ResponseBodyType != null) ? $".Receive{xm.ResponseBodyType}{genericArg}()" : ""; + writer.WriteLine($"return request.SendAsync({string.Join(", ", args)}){receive};"); + }); + } } } \ No newline at end of file diff --git a/src/Flurl.Http.Newtonsoft/ExtensionMethods.cs b/src/Flurl.Http.Newtonsoft/ExtensionMethods.cs new file mode 100644 index 00000000..4e184ebd --- /dev/null +++ b/src/Flurl.Http.Newtonsoft/ExtensionMethods.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Threading.Tasks; + +namespace Flurl.Http.Newtonsoft +{ + /// + /// Extensions to Flurl objects that use Newtonsoft.Json. + /// + public static class ExtensionMethods + { + /// + /// Deserializes JSON-formatted HTTP response body to a dynamic object. + /// + /// A Task whose result is a dynamic object containing data in the response body. + public static async Task GetJsonAsync(this IFlurlResponse resp) { + dynamic d = await resp.GetJsonAsync().ConfigureAwait(false); + return d; + } + + /// + /// Deserializes JSON-formatted HTTP response body to a list of dynamic objects. + /// + /// A Task whose result is a list of dynamic objects containing data in the response body. + public static async Task> GetJsonListAsync(this IFlurlResponse resp) { + dynamic[] d = await resp.GetJsonAsync().ConfigureAwait(false); + return d; + } + + /// + /// Deserializes JSON-formatted HTTP response body to a dynamic object. Intended to chain off an async call. + /// + /// A Task whose result is a dynamic object containing data in the response body. + public static async Task ReceiveJson(this Task response) { + using var resp = await response.ConfigureAwait(false); + if (resp == null) return null; + return await resp.GetJsonAsync().ConfigureAwait(false); + } + + /// + /// Deserializes JSON-formatted HTTP response body to a list of dynamic objects. Intended to chain off an async call. + /// + /// A Task whose result is a list of dynamic objects containing data in the response body. + public static async Task> ReceiveJsonList(this Task response) { + using var resp = await response.ConfigureAwait(false); + if (resp == null) return null; + return await resp.GetJsonListAsync().ConfigureAwait(false); + } + } +} diff --git a/src/Flurl.Http.Newtonsoft/Flurl.Http.Newtonsoft.csproj b/src/Flurl.Http.Newtonsoft/Flurl.Http.Newtonsoft.csproj index 9c87c813..8621527c 100644 --- a/src/Flurl.Http.Newtonsoft/Flurl.Http.Newtonsoft.csproj +++ b/src/Flurl.Http.Newtonsoft/Flurl.Http.Newtonsoft.csproj @@ -1,4 +1,4 @@ - + net6.0;netstandard2.0;net461 9.0 @@ -30,4 +30,8 @@ + + + + diff --git a/src/Flurl.Http.Newtonsoft/GeneratedExtensions.cs b/src/Flurl.Http.Newtonsoft/GeneratedExtensions.cs new file mode 100644 index 00000000..547fba68 --- /dev/null +++ b/src/Flurl.Http.Newtonsoft/GeneratedExtensions.cs @@ -0,0 +1,104 @@ +// This file was auto-generated by Flurl.CodeGen. Do not edit directly. +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Flurl.Http.Newtonsoft +{ + /// + /// Fluent extension methods on String, Url, Uri, and IFlurlRequest. + /// + public static class GeneratedExtensions + { + /// + /// Sends an asynchronous GET request. + /// + /// This IFlurlRequest + /// The HttpCompletionOption used in the request. Optional. + /// The token to monitor for cancellation requests. + /// A Task whose result is the JSON response body deserialized to a dynamic. + public static Task GetJsonAsync(this IFlurlRequest request, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) { + return request.SendAsync(HttpMethod.Get, null, completionOption, cancellationToken).ReceiveJson(); + } + + /// + /// Sends an asynchronous GET request. + /// + /// This IFlurlRequest + /// The HttpCompletionOption used in the request. Optional. + /// The token to monitor for cancellation requests. + /// A Task whose result is the JSON response body deserialized to a list of dynamics. + public static Task> GetJsonListAsync(this IFlurlRequest request, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) { + return request.SendAsync(HttpMethod.Get, null, completionOption, cancellationToken).ReceiveJsonList(); + } + + /// + /// Creates a FlurlRequest and sends an asynchronous GET request. + /// + /// This Flurl.Url. + /// The HttpCompletionOption used in the request. Optional. + /// The token to monitor for cancellation requests. + /// A Task whose result is the JSON response body deserialized to a dynamic. + public static Task GetJsonAsync(this Url url, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) { + return new FlurlRequest(url).GetJsonAsync(completionOption, cancellationToken); + } + + /// + /// Creates a FlurlRequest and sends an asynchronous GET request. + /// + /// This Flurl.Url. + /// The HttpCompletionOption used in the request. Optional. + /// The token to monitor for cancellation requests. + /// A Task whose result is the JSON response body deserialized to a list of dynamics. + public static Task> GetJsonListAsync(this Url url, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) { + return new FlurlRequest(url).GetJsonListAsync(completionOption, cancellationToken); + } + + /// + /// Creates a FlurlRequest and sends an asynchronous GET request. + /// + /// This URL. + /// The HttpCompletionOption used in the request. Optional. + /// The token to monitor for cancellation requests. + /// A Task whose result is the JSON response body deserialized to a dynamic. + public static Task GetJsonAsync(this string url, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) { + return new FlurlRequest(url).GetJsonAsync(completionOption, cancellationToken); + } + + /// + /// Creates a FlurlRequest and sends an asynchronous GET request. + /// + /// This URL. + /// The HttpCompletionOption used in the request. Optional. + /// The token to monitor for cancellation requests. + /// A Task whose result is the JSON response body deserialized to a list of dynamics. + public static Task> GetJsonListAsync(this string url, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) { + return new FlurlRequest(url).GetJsonListAsync(completionOption, cancellationToken); + } + + /// + /// Creates a FlurlRequest and sends an asynchronous GET request. + /// + /// This System.Uri. + /// The HttpCompletionOption used in the request. Optional. + /// The token to monitor for cancellation requests. + /// A Task whose result is the JSON response body deserialized to a dynamic. + public static Task GetJsonAsync(this Uri uri, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) { + return new FlurlRequest(uri).GetJsonAsync(completionOption, cancellationToken); + } + + /// + /// Creates a FlurlRequest and sends an asynchronous GET request. + /// + /// This System.Uri. + /// The HttpCompletionOption used in the request. Optional. + /// The token to monitor for cancellation requests. + /// A Task whose result is the JSON response body deserialized to a list of dynamics. + public static Task> GetJsonListAsync(this Uri uri, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) { + return new FlurlRequest(uri).GetJsonListAsync(completionOption, cancellationToken); + } + + } +} From c4dec2a475f69171318043c1c9ac98be3e66bd84 Mon Sep 17 00:00:00 2001 From: Todd Date: Mon, 4 Dec 2023 21:04:45 -0600 Subject: [PATCH 4/6] Newtonsoft tests --- test/Flurl.Test/Flurl.Test.csproj | 2 +- test/Flurl.Test/Http/HttpTestFixtureBase.cs | 8 +- .../Http/NewtonsoftJsonSerializer.cs | 45 ----------- test/Flurl.Test/Http/NewtonsoftTests.cs | 76 +++++++++++++++++++ 4 files changed, 82 insertions(+), 49 deletions(-) delete mode 100644 test/Flurl.Test/Http/NewtonsoftJsonSerializer.cs create mode 100644 test/Flurl.Test/Http/NewtonsoftTests.cs diff --git a/test/Flurl.Test/Flurl.Test.csproj b/test/Flurl.Test/Flurl.Test.csproj index fbdc4a13..a11ac9f7 100644 --- a/test/Flurl.Test/Flurl.Test.csproj +++ b/test/Flurl.Test/Flurl.Test.csproj @@ -10,7 +10,6 @@ all - @@ -19,6 +18,7 @@ + diff --git a/test/Flurl.Test/Http/HttpTestFixtureBase.cs b/test/Flurl.Test/Http/HttpTestFixtureBase.cs index 5cadba8a..1050ea2d 100644 --- a/test/Flurl.Test/Http/HttpTestFixtureBase.cs +++ b/test/Flurl.Test/Http/HttpTestFixtureBase.cs @@ -11,13 +11,15 @@ public abstract class HttpTestFixtureBase protected HttpTest HttpTest { get; private set; } [SetUp] - public void CreateHttpTest() { - HttpTest = new HttpTest(); + public void SetUp() { + HttpTest = CreateHttpTest(); } [TearDown] - public void DisposeHttpTest() { + public void TearTown() { HttpTest.Dispose(); } + + protected virtual HttpTest CreateHttpTest() => new HttpTest(); } } diff --git a/test/Flurl.Test/Http/NewtonsoftJsonSerializer.cs b/test/Flurl.Test/Http/NewtonsoftJsonSerializer.cs deleted file mode 100644 index d998ae4a..00000000 --- a/test/Flurl.Test/Http/NewtonsoftJsonSerializer.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.IO; -using Newtonsoft.Json; - -namespace Flurl.Http.Configuration -{ - /// - /// ISerializer implementation based on Newtonsoft.Json. - /// Default serializer used in calls to GetJsonAsync, PostJsonAsync, etc. - /// - public class NewtonsoftJsonSerializer : ISerializer - { - private readonly JsonSerializerSettings _settings; - - /// - /// Initializes a new instance of the class. - /// - /// Settings to control (de)serialization behavior. - public NewtonsoftJsonSerializer(JsonSerializerSettings settings = null) { - _settings = settings; - } - - /// - /// Serializes the specified object to a JSON string. - /// - /// The object to serialize. - public string Serialize(object obj) => JsonConvert.SerializeObject(obj, _settings); - - /// - /// Deserializes the specified JSON string to an object of type T. - /// - /// The JSON string to deserialize. - public T Deserialize(string s) => JsonConvert.DeserializeObject(s, _settings); - - /// - /// Deserializes the specified stream to an object of type T. - /// - /// The stream to deserialize. - public T Deserialize(Stream stream) { - // https://www.newtonsoft.com/json/help/html/Performance.htm#MemoryUsage - using var sr = new StreamReader(stream); - using var jr = new JsonTextReader(sr); - return JsonSerializer.CreateDefault(_settings).Deserialize(jr); - } - } -} \ No newline at end of file diff --git a/test/Flurl.Test/Http/NewtonsoftTests.cs b/test/Flurl.Test/Http/NewtonsoftTests.cs new file mode 100644 index 00000000..6f01fa3f --- /dev/null +++ b/test/Flurl.Test/Http/NewtonsoftTests.cs @@ -0,0 +1,76 @@ +using NUnit.Framework; +using Flurl.Http; +using Flurl.Http.Newtonsoft; +using Flurl.Http.Testing; +using System.Threading.Tasks; + +namespace Flurl.Test.Http +{ + // These inherit from GetTests and PostTests and swap out the JSON serializer + // in play, which gets us a lot of free tests but also a lot of redundant ones. + // Maybe worth refactoring someday, but they're fast so it's tolerable for now. + + [TestFixture, Parallelizable] + public class NewtonsoftGetTests : GetTests + { + protected override HttpTest CreateHttpTest() => base.CreateHttpTest() + .WithSettings(settings => settings.JsonSerializer = new NewtonsoftJsonSerializer()); + + [Test] + public async Task can_get_dynamic() { + HttpTest.RespondWithJson(new { id = 1, name = "Frank" }); + + var data = await "http://some-api.com".GetJsonAsync(); + + Assert.AreEqual(1, data.id); + Assert.AreEqual("Frank", data.name); + } + + [Test] + public async Task can_get_dynamic_list() { + HttpTest.RespondWithJson(new[] { + new { id = 1, name = "Frank" }, + new { id = 2, name = "Claire" } + }); + + var data = await "http://some-api.com".GetJsonListAsync(); + + Assert.AreEqual(1, data[0].id); + Assert.AreEqual("Frank", data[0].name); + Assert.AreEqual(2, data[1].id); + Assert.AreEqual("Claire", data[1].name); + } + } + + [TestFixture, Parallelizable] + public class NewtonsofPostTests : PostTests + { + protected override HttpTest CreateHttpTest() => base.CreateHttpTest() + .WithSettings(settings => settings.JsonSerializer = new NewtonsoftJsonSerializer()); + + [Test] + public async Task can_receive_json_dynamic() { + HttpTest.RespondWithJson(new { id = 1, name = "Frank" }); + + var data = await "http://some-api.com".PostJsonAsync(new { a = 1, b = 2 }).ReceiveJson(); + + Assert.AreEqual(1, data.id); + Assert.AreEqual("Frank", data.name); + } + + [Test] + public async Task can_receive_json_dynamic_list() { + HttpTest.RespondWithJson(new[] { + new { id = 1, name = "Frank" }, + new { id = 2, name = "Claire" } + }); + + var data = await "http://some-api.com".PostJsonAsync(new { a = 1, b = 2 }).ReceiveJsonList(); + + Assert.AreEqual(1, data[0].id); + Assert.AreEqual("Frank", data[0].name); + Assert.AreEqual(2, data[1].id); + Assert.AreEqual("Claire", data[1].name); + } + } +} From cef9dfefd69487867f00a1cac174c924366f4788 Mon Sep 17 00:00:00 2001 From: Todd Date: Mon, 4 Dec 2023 22:01:51 -0600 Subject: [PATCH 5/6] Newtonsoft config methods --- src/Flurl.Http.Newtonsoft/ExtensionMethods.cs | 20 +++++++++++ test/Flurl.Test/Http/NewtonsoftTests.cs | 36 +++++++++++++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/Flurl.Http.Newtonsoft/ExtensionMethods.cs b/src/Flurl.Http.Newtonsoft/ExtensionMethods.cs index 4e184ebd..c43618d5 100644 --- a/src/Flurl.Http.Newtonsoft/ExtensionMethods.cs +++ b/src/Flurl.Http.Newtonsoft/ExtensionMethods.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Dynamic; using System.Threading.Tasks; +using Flurl.Http.Configuration; +using Newtonsoft.Json; namespace Flurl.Http.Newtonsoft { @@ -47,5 +49,23 @@ public static async Task> ReceiveJsonList(this Task + /// Shortcut to use NewtonsoftJsonSerializer with this IFlurlClientBuilder. + /// + /// This IFlurlClientBuilder. + /// Optional custom JsonSerializerSettings. + /// + public static IFlurlClientBuilder UseNewtonsoft(this IFlurlClientBuilder builder, JsonSerializerSettings settings = null) => + builder.WithSettings(fs => fs.JsonSerializer = new NewtonsoftJsonSerializer(settings)); + + /// + /// Shortcut to use NewtonsoftJsonSerializer with all FlurlClients registered in this cache. + /// + /// This IFlurlClientCache. + /// Optional custom JsonSerializerSettings. + /// + public static IFlurlClientCache UseNewtonsoft(this IFlurlClientCache cache, JsonSerializerSettings settings = null) => + cache.WithDefaults(builder => builder.UseNewtonsoft(settings)); } } diff --git a/test/Flurl.Test/Http/NewtonsoftTests.cs b/test/Flurl.Test/Http/NewtonsoftTests.cs index 6f01fa3f..557273ad 100644 --- a/test/Flurl.Test/Http/NewtonsoftTests.cs +++ b/test/Flurl.Test/Http/NewtonsoftTests.cs @@ -1,8 +1,11 @@ -using NUnit.Framework; +using System; +using NUnit.Framework; using Flurl.Http; using Flurl.Http.Newtonsoft; using Flurl.Http.Testing; using System.Threading.Tasks; +using Flurl.Http.Configuration; +using Newtonsoft.Json; namespace Flurl.Test.Http { @@ -43,7 +46,7 @@ public async Task can_get_dynamic_list() { } [TestFixture, Parallelizable] - public class NewtonsofPostTests : PostTests + public class NewtonsoftPostTests : PostTests { protected override HttpTest CreateHttpTest() => base.CreateHttpTest() .WithSettings(settings => settings.JsonSerializer = new NewtonsoftJsonSerializer()); @@ -73,4 +76,33 @@ public async Task can_receive_json_dynamic_list() { Assert.AreEqual("Claire", data[1].name); } } + + [TestFixture, Parallelizable] + public class NewtonsoftConfigTests + { + [Test] + public void can_register_with_builder() { + var cache = new FlurlClientCache(); + var cli = cache.GetOrAdd("foo", null, builder => builder.UseNewtonsoft(new JsonSerializerSettings { DateFormatString = "1234" })); + + Assert.IsInstanceOf(cli.Settings.JsonSerializer); + + var obj = new { Date = DateTime.Now }; + var json = cli.Settings.JsonSerializer.Serialize(obj); + Assert.AreEqual("{\"Date\":\"1234\"}", json); + } + + [Test] + public void can_register_with_cache() { + var cache = new FlurlClientCache().UseNewtonsoft(new JsonSerializerSettings { DateFormatString = "1234" }); + var cli = cache.GetOrAdd("foo"); + + Assert.IsInstanceOf(cli.Settings.JsonSerializer); + + var obj = new { Date = DateTime.Now }; + var json = cli.Settings.JsonSerializer.Serialize(obj); + Assert.AreEqual("{\"Date\":\"1234\"}", json); + } + + } } From 672c358f596e4d11182f6b853b77a2d395fa377d Mon Sep 17 00:00:00 2001 From: Todd Date: Mon, 4 Dec 2023 22:15:34 -0600 Subject: [PATCH 6/6] add newtonsoft to release workflow --- .github/workflows/draft-release.yml | 32 ++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml index 36afb2e5..cab28e06 100644 --- a/.github/workflows/draft-release.yml +++ b/.github/workflows/draft-release.yml @@ -24,7 +24,7 @@ jobs: run: dotnet build -c Release --no-restore - name: Test - run: dotnet test --no-build -c Release /p:CollectCoverage=true /p:Threshold=80 /p:Include=\"[Flurl]*,[Flurl.Http]*\" /p:Exclude="[*]*.GeneratedExtensions" + run: dotnet test --no-build -c Release /p:CollectCoverage=true /p:Threshold=80 /p:Include=\"[Flurl]*,[Flurl.Http]*,[Flurl.Http.Newtonsoft]*\" /p:Exclude="[*]*.GeneratedExtensions" # Compare version from csproj with latest release tag. # If different, create a draft release. @@ -57,12 +57,28 @@ jobs: releases-only: true regex: '^Flurl\.Http\.\d+' + - name: Get Flurl.Http.Newtonsoft csproj version + id: csproj_ver_flurl_newtonsoft + uses: KageKirin/get-csproj-version@v1.0.0 + with: + file: src/Flurl.Http.Newtonsoft/Flurl.Http.Newtonsoft.csproj + + - name: Get Flurl.Http.Newtonsoft latest release tag + id: release_ver_flurl_newtonsoft + uses: oprypin/find-latest-tag@v1.1.1 + with: + repository: tmenier/Flurl + releases-only: true + regex: '^Flurl\.Http\.Newtonsoft\.\d+' + - name: Output versions run: | echo "Flurl csproj version: ${{ steps.csproj_ver_flurl.outputs.version }}" echo "Flurl latest release tag: ${{ steps.release_ver_flurl.outputs.tag }}" echo "Flurl.Http csproj version: ${{ steps.csproj_ver_flurl_http.outputs.version }}" echo "Flurl.Http latest release tag: ${{ steps.release_ver_flurl_http.outputs.tag }}" + echo "Flurl.Http.Newtonsoft csproj version: ${{ steps.csproj_ver_flurl_newtonsoft.outputs.version }}" + echo "Flurl.Http.Newtonsoft latest release tag: ${{ steps.release_ver_flurl_newtonsoft.outputs.tag }}" - name: Draft Flurl release env: @@ -91,3 +107,17 @@ jobs: generateReleaseNotes: true artifacts: "**/${{ env.NEXT_TAG }}.nupkg,**/${{ env.NEXT_TAG }}.snupkg" draft: true + + - name: Draft Flurl.Http.Newtonsoft release + env: + CURRENT_TAG: ${{ steps.release_ver_flurl_newtonsoft.outputs.tag }} + NEXT_TAG: "Flurl.Http.${{ steps.csproj_ver_flurl_newtonsoft.outputs.version }}" + RELEASE_NAME: "Flurl.Http ${{ steps.csproj_ver_flurl_newtonsoft.outputs.version }}" + if: env.NEXT_TAG != env.CURRENT_TAG + uses: ncipollo/release-action@v1 + with: + name: ${{ env.RELEASE_NAME }} + tag: ${{ env.NEXT_TAG }} + generateReleaseNotes: true + artifacts: "**/${{ env.NEXT_TAG }}.nupkg,**/${{ env.NEXT_TAG }}.snupkg" + draft: true