From cff7f196229298f3812e2cad383e35514b2c998f Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 1 Dec 2023 09:10:14 +0000 Subject: [PATCH 001/151] clear old projects out --- DragonFruit.Data.sln | 34 -- common/DragonFruit.Data.Common.csproj | 15 - .../ApiHtmlSerializer.cs | 45 --- .../DragonFruit.Data.Serializers.Html.csproj | 14 - .../HtmlDocumentExtensions.cs | 32 -- .../ApiJsonSerializer.cs | 67 ---- ...onFruit.Data.Serializers.Newtonsoft.csproj | 14 - .../JsonArrayPool.cs | 29 -- .../JsonExtensions.cs | 101 ------ .../JsonFileServices.cs | 112 ------- .../ApiSystemTextJsonSerializer.cs | 50 --- ...onFruit.Data.Serializers.SystemJson.csproj | 14 - .../JsonFileServices.cs | 103 ------ src/ApiClient.cs | 305 ------------------ src/ApiClient_Async.cs | 124 ------- src/ApiFileRequest.cs | 20 -- src/Basic/BasicApiFileRequest.cs | 48 --- src/Basic/BasicApiRequest.cs | 32 -- src/Basic/BasicApiRequestExtensions.cs | 27 -- src/Basic/IBasicApiRequest.cs | 16 - src/Exceptions/ClientValidationException.cs | 15 - src/Exceptions/NullRequestException.cs | 15 - src/Extensions/RequestExtensions.cs | 33 -- src/Extensions/ResponseExtensions.cs | 38 --- src/Headers/HeaderCollection.cs | 69 ---- src/Methods.cs | 87 ----- src/Parameters/FormParameter.cs | 40 --- src/Parameters/IProperty.cs | 18 -- src/Parameters/QueryParameter.cs | 40 --- src/Parameters/RequestBody.cs | 12 - .../IAsyncRequestExecutingCallback.cs | 20 -- src/Requests/IRequestExecutingCallback.cs | 16 - src/Serializers/ApiSerializer.cs | 95 ------ src/Serializers/ApiXmlSerializer.cs | 40 --- src/Serializers/IAsyncSerializer.cs | 16 - src/Serializers/InternalStreamSerializer.cs | 52 --- src/Serializers/SerializerResolver.cs | 184 ----------- src/TargetTypedApiClient.cs | 24 -- src/Utils/CultureUtils.cs | 32 -- src/Utils/HttpVersionUtils.cs | 32 -- src/Utils/ParameterUtils.cs | 154 --------- src/Utils/QueryUtils.cs | 21 -- tests/ApiTest.cs | 19 -- tests/DragonFruit.Data.Tests.csproj | 21 -- tests/Header/HeaderLevelTests.cs | 57 ---- tests/Header/HeaderTests.cs | 53 --- tests/RequestDataCompilationTests.cs | 92 ------ tests/Requests/EchoRequest.cs | 16 - tests/Requests/FileRequestTests.cs | 48 --- tests/Requests/RequestFilterTests.cs | 34 -- tests/Requests/RequestTests.cs | 80 ----- tests/Requests/StreamTests.cs | 25 -- tests/Serializers/SerializerResolverTests.cs | 87 ----- tests/Serializers/SerializerTests.cs | 49 --- 54 files changed, 2836 deletions(-) delete mode 100644 common/DragonFruit.Data.Common.csproj delete mode 100644 serializers/DragonFruit.Data.Serializers.Html/ApiHtmlSerializer.cs delete mode 100644 serializers/DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj delete mode 100644 serializers/DragonFruit.Data.Serializers.Html/HtmlDocumentExtensions.cs delete mode 100644 serializers/DragonFruit.Data.Serializers.Newtonsoft/ApiJsonSerializer.cs delete mode 100644 serializers/DragonFruit.Data.Serializers.Newtonsoft/DragonFruit.Data.Serializers.Newtonsoft.csproj delete mode 100644 serializers/DragonFruit.Data.Serializers.Newtonsoft/JsonArrayPool.cs delete mode 100644 serializers/DragonFruit.Data.Serializers.Newtonsoft/JsonExtensions.cs delete mode 100644 serializers/DragonFruit.Data.Serializers.Newtonsoft/JsonFileServices.cs delete mode 100644 serializers/DragonFruit.Data.Serializers.SystemJson/ApiSystemTextJsonSerializer.cs delete mode 100644 serializers/DragonFruit.Data.Serializers.SystemJson/DragonFruit.Data.Serializers.SystemJson.csproj delete mode 100644 serializers/DragonFruit.Data.Serializers.SystemJson/JsonFileServices.cs delete mode 100644 src/ApiClient.cs delete mode 100644 src/ApiClient_Async.cs delete mode 100644 src/ApiFileRequest.cs delete mode 100644 src/Basic/BasicApiFileRequest.cs delete mode 100644 src/Basic/BasicApiRequest.cs delete mode 100644 src/Basic/BasicApiRequestExtensions.cs delete mode 100644 src/Basic/IBasicApiRequest.cs delete mode 100644 src/Exceptions/ClientValidationException.cs delete mode 100644 src/Exceptions/NullRequestException.cs delete mode 100644 src/Extensions/RequestExtensions.cs delete mode 100644 src/Extensions/ResponseExtensions.cs delete mode 100644 src/Headers/HeaderCollection.cs delete mode 100644 src/Methods.cs delete mode 100644 src/Parameters/FormParameter.cs delete mode 100644 src/Parameters/IProperty.cs delete mode 100644 src/Parameters/QueryParameter.cs delete mode 100644 src/Parameters/RequestBody.cs delete mode 100644 src/Requests/IAsyncRequestExecutingCallback.cs delete mode 100644 src/Requests/IRequestExecutingCallback.cs delete mode 100644 src/Serializers/ApiSerializer.cs delete mode 100644 src/Serializers/ApiXmlSerializer.cs delete mode 100644 src/Serializers/IAsyncSerializer.cs delete mode 100644 src/Serializers/InternalStreamSerializer.cs delete mode 100644 src/Serializers/SerializerResolver.cs delete mode 100644 src/TargetTypedApiClient.cs delete mode 100644 src/Utils/CultureUtils.cs delete mode 100644 src/Utils/HttpVersionUtils.cs delete mode 100644 src/Utils/ParameterUtils.cs delete mode 100644 src/Utils/QueryUtils.cs delete mode 100644 tests/ApiTest.cs delete mode 100644 tests/DragonFruit.Data.Tests.csproj delete mode 100644 tests/Header/HeaderLevelTests.cs delete mode 100644 tests/Header/HeaderTests.cs delete mode 100644 tests/RequestDataCompilationTests.cs delete mode 100644 tests/Requests/EchoRequest.cs delete mode 100644 tests/Requests/FileRequestTests.cs delete mode 100644 tests/Requests/RequestFilterTests.cs delete mode 100644 tests/Requests/RequestTests.cs delete mode 100644 tests/Requests/StreamTests.cs delete mode 100644 tests/Serializers/SerializerResolverTests.cs delete mode 100644 tests/Serializers/SerializerTests.cs diff --git a/DragonFruit.Data.sln b/DragonFruit.Data.sln index 2c1f1c9..b6c3fc1 100644 --- a/DragonFruit.Data.sln +++ b/DragonFruit.Data.sln @@ -7,25 +7,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject license.md = license.md readme.md = readme.md - res\DragonFruit.Data.props = res\DragonFruit.Data.props res\DragonFruit.Data.Serializers.props = res\DragonFruit.Data.Serializers.props res\DragonFruit.Data.Nuget.props = res\DragonFruit.Data.Nuget.props EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data", "src\DragonFruit.Data.csproj", "{0A0921D2-637F-4245-B993-054983A97EAC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Tests", "tests\DragonFruit.Data.Tests.csproj", "{A4D3F242-8E21-4756-9EB0-000BE6CBAE7B}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Serializers", "Serializers", "{5A8982CD-EEF9-4B9F-AF74-C8D45241E137}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Serializers.Newtonsoft", "serializers\DragonFruit.Data.Serializers.Newtonsoft\DragonFruit.Data.Serializers.Newtonsoft.csproj", "{0D9C70B4-99AD-4D0C-BD6B-8E2B02E82C66}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Common", "common\DragonFruit.Data.Common.csproj", "{7C169022-60C3-488D-9A67-C0E5A36DE2C8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Serializers.Html", "serializers\DragonFruit.Data.Serializers.Html\DragonFruit.Data.Serializers.Html.csproj", "{BE0F43F0-871B-4A6D-8F43-5AAECDA129C3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Serializers.SystemJson", "serializers\DragonFruit.Data.Serializers.SystemJson\DragonFruit.Data.Serializers.SystemJson.csproj", "{72224F8B-EB2A-4CAF-B325-BC40704AD7DE}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,26 +25,6 @@ Global {0A0921D2-637F-4245-B993-054983A97EAC}.Debug|Any CPU.Build.0 = Debug|Any CPU {0A0921D2-637F-4245-B993-054983A97EAC}.Release|Any CPU.ActiveCfg = Release|Any CPU {0A0921D2-637F-4245-B993-054983A97EAC}.Release|Any CPU.Build.0 = Release|Any CPU - {A4D3F242-8E21-4756-9EB0-000BE6CBAE7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A4D3F242-8E21-4756-9EB0-000BE6CBAE7B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A4D3F242-8E21-4756-9EB0-000BE6CBAE7B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A4D3F242-8E21-4756-9EB0-000BE6CBAE7B}.Release|Any CPU.Build.0 = Release|Any CPU - {0D9C70B4-99AD-4D0C-BD6B-8E2B02E82C66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0D9C70B4-99AD-4D0C-BD6B-8E2B02E82C66}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0D9C70B4-99AD-4D0C-BD6B-8E2B02E82C66}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0D9C70B4-99AD-4D0C-BD6B-8E2B02E82C66}.Release|Any CPU.Build.0 = Release|Any CPU - {7C169022-60C3-488D-9A67-C0E5A36DE2C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7C169022-60C3-488D-9A67-C0E5A36DE2C8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7C169022-60C3-488D-9A67-C0E5A36DE2C8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7C169022-60C3-488D-9A67-C0E5A36DE2C8}.Release|Any CPU.Build.0 = Release|Any CPU - {BE0F43F0-871B-4A6D-8F43-5AAECDA129C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BE0F43F0-871B-4A6D-8F43-5AAECDA129C3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BE0F43F0-871B-4A6D-8F43-5AAECDA129C3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BE0F43F0-871B-4A6D-8F43-5AAECDA129C3}.Release|Any CPU.Build.0 = Release|Any CPU - {72224F8B-EB2A-4CAF-B325-BC40704AD7DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {72224F8B-EB2A-4CAF-B325-BC40704AD7DE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {72224F8B-EB2A-4CAF-B325-BC40704AD7DE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {72224F8B-EB2A-4CAF-B325-BC40704AD7DE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -64,8 +33,5 @@ Global SolutionGuid = {368819C7-3F6F-4B76-BBF1-F581D180EA8F} EndGlobalSection GlobalSection(NestedProjects) = preSolution - {0D9C70B4-99AD-4D0C-BD6B-8E2B02E82C66} = {5A8982CD-EEF9-4B9F-AF74-C8D45241E137} - {BE0F43F0-871B-4A6D-8F43-5AAECDA129C3} = {5A8982CD-EEF9-4B9F-AF74-C8D45241E137} - {72224F8B-EB2A-4CAF-B325-BC40704AD7DE} = {5A8982CD-EEF9-4B9F-AF74-C8D45241E137} EndGlobalSection EndGlobal diff --git a/common/DragonFruit.Data.Common.csproj b/common/DragonFruit.Data.Common.csproj deleted file mode 100644 index a5002ba..0000000 --- a/common/DragonFruit.Data.Common.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - DragonFruit.Common.Data - A metapackage for DragonFruit.Data including the main library and the Newtonsoft serializer - - - - - - - - - - diff --git a/serializers/DragonFruit.Data.Serializers.Html/ApiHtmlSerializer.cs b/serializers/DragonFruit.Data.Serializers.Html/ApiHtmlSerializer.cs deleted file mode 100644 index a5f1f30..0000000 --- a/serializers/DragonFruit.Data.Serializers.Html/ApiHtmlSerializer.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.IO; -using System.Net.Http; -using HtmlAgilityPack; - -namespace DragonFruit.Data.Serializers.Html -{ - public class ApiHtmlSerializer : ApiSerializer - { - public override string ContentType => "text/html"; - public override bool IsGeneric => false; - - public override HttpContent Serialize(object input) - { - if (!(input is HtmlDocument document)) - { - throw new ArgumentException($"Cannot process {input.GetType().Name}", nameof(input)); - } - - // html is usually larger than 80kb - var stream = GetStream(true); - document.Save(stream, Encoding); - - return GetHttpContent(stream); - } - - public override T Deserialize(Stream input) - { - if (typeof(T) != typeof(HtmlDocument)) - { - throw new ArgumentException($"Cannot process {typeof(T).Name}", nameof(T)); - } - - var document = new HtmlDocument(); - document.Load(input, Encoding, AutoDetectEncoding); - - return document as T; // where T is validated as a HtmlDocument - } - - /// - /// Registers the to resolve objects - /// - public static void RegisterDefaults() => SerializerResolver.Register(); - } -} diff --git a/serializers/DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj b/serializers/DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj deleted file mode 100644 index bb4c58b..0000000 --- a/serializers/DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - DragonFruit.Data.Serializers.Html - HTML parsing (through HTMLAgilityPack) for DragonFruit.Data - - - - - - - - - diff --git a/serializers/DragonFruit.Data.Serializers.Html/HtmlDocumentExtensions.cs b/serializers/DragonFruit.Data.Serializers.Html/HtmlDocumentExtensions.cs deleted file mode 100644 index 472d16c..0000000 --- a/serializers/DragonFruit.Data.Serializers.Html/HtmlDocumentExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using HtmlAgilityPack; - -namespace DragonFruit.Data.Serializers.Html -{ - public static class HtmlDocumentExtensions - { - /// - /// Extracts a value from the based on its XPath and attribute name - /// - public static string GetValue(this HtmlNode node, string xpath = default, string attribute = default, bool throwOnNotFound = false) - { - var subNode = string.IsNullOrEmpty(xpath) ? node : node.SelectSingleNode(xpath); - var useInnerText = string.IsNullOrEmpty(attribute); - - switch (useInnerText) - { - case false when throwOnNotFound: - return subNode.GetAttributeValue(attribute, null) ?? throw new NullReferenceException(); - - case false: - return subNode.GetAttributeValue(attribute, null); - - case true: - return subNode.InnerText; - } - } - } -} diff --git a/serializers/DragonFruit.Data.Serializers.Newtonsoft/ApiJsonSerializer.cs b/serializers/DragonFruit.Data.Serializers.Newtonsoft/ApiJsonSerializer.cs deleted file mode 100644 index 9f9daef..0000000 --- a/serializers/DragonFruit.Data.Serializers.Newtonsoft/ApiJsonSerializer.cs +++ /dev/null @@ -1,67 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System.IO; -using System.Net.Http; -using DragonFruit.Data.Utils; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace DragonFruit.Data.Serializers.Newtonsoft -{ - public class ApiJsonSerializer : ApiSerializer - { - private JsonSerializer _serializer; - - public override string ContentType => "application/json"; - - public JsonSerializer Serializer - { - get => _serializer ??= new JsonSerializer { Culture = CultureUtils.DefaultCulture, Formatting = Formatting.Indented }; - set => _serializer = value; - } - - public override HttpContent Serialize(object input) - { - var stream = GetStream(false); - - // these must dispose before processing the stream, as we need any/all buffers flushed - using (var streamWriter = new StreamWriter(stream, Encoding, 4096, true)) - using (var jsonWriter = new JsonTextWriter(streamWriter)) - { - jsonWriter.ArrayPool = JsonArrayPool.Instance; - Serializer.Serialize(jsonWriter, input); - } - - return GetHttpContent(stream); - } - - public override T Deserialize(Stream input) where T : class - { - using var sr = AutoDetectEncoding switch - { - true => new StreamReader(input, true), - - false when Encoding is null => new StreamReader(input), - false => new StreamReader(input, Encoding) - }; - - using var reader = new JsonTextReader(sr) - { - ArrayPool = JsonArrayPool.Instance - }; - - return Serializer.Deserialize(reader); - } - - /// - /// Registers Newtonsoft.Json Linq objects to be resolved by this serializer - /// - public static void RegisterDefaults() - { - SerializerResolver.Register(); - SerializerResolver.Register(); - SerializerResolver.Register(); - } - } -} diff --git a/serializers/DragonFruit.Data.Serializers.Newtonsoft/DragonFruit.Data.Serializers.Newtonsoft.csproj b/serializers/DragonFruit.Data.Serializers.Newtonsoft/DragonFruit.Data.Serializers.Newtonsoft.csproj deleted file mode 100644 index 3f76d3c..0000000 --- a/serializers/DragonFruit.Data.Serializers.Newtonsoft/DragonFruit.Data.Serializers.Newtonsoft.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - DragonFruit.Data.Serializers.Newtonsoft - Newtonsoft.Json serializer for DragonFruit.Data - - - - - - - - - diff --git a/serializers/DragonFruit.Data.Serializers.Newtonsoft/JsonArrayPool.cs b/serializers/DragonFruit.Data.Serializers.Newtonsoft/JsonArrayPool.cs deleted file mode 100644 index 6d9c970..0000000 --- a/serializers/DragonFruit.Data.Serializers.Newtonsoft/JsonArrayPool.cs +++ /dev/null @@ -1,29 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System.Buffers; -using Newtonsoft.Json; - -namespace DragonFruit.Data.Serializers.Newtonsoft -{ - /// - /// A wrapper for the that implements - /// - /// - /// Taken from https://github.com/JamesNK/Newtonsoft.Json/blob/52e257ee57899296d81a868b32300f0b3cfeacbe/Src/Newtonsoft.Json.Tests/DemoTests.cs#L709 - /// - internal class JsonArrayPool : IArrayPool - { - public static readonly JsonArrayPool Instance = new JsonArrayPool(); - - public char[] Rent(int minimumLength) - { - return ArrayPool.Shared.Rent(minimumLength); - } - - public void Return(char[] array) - { - ArrayPool.Shared.Return(array); - } - } -} diff --git a/serializers/DragonFruit.Data.Serializers.Newtonsoft/JsonExtensions.cs b/serializers/DragonFruit.Data.Serializers.Newtonsoft/JsonExtensions.cs deleted file mode 100644 index 52a539a..0000000 --- a/serializers/DragonFruit.Data.Serializers.Newtonsoft/JsonExtensions.cs +++ /dev/null @@ -1,101 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -#nullable enable - -using System; -using System.Collections.Generic; -using Newtonsoft.Json.Linq; - -namespace DragonFruit.Data.Serializers.Newtonsoft -{ - public static class JsonExtensions - { - public static string GetString(this JObject source, string key, string @default = "") - { - return (string?)GetBase(source, key) ?? @default; - } - - public static bool GetBool(this JObject source, string key, bool @default = false) - { - return ((bool?)GetBase(source, key)).GetValueOrDefault(@default); - } - - public static byte GetByte(this JObject source, string key, byte @default = 0) - { - return ((byte?)GetBase(source, key)).GetValueOrDefault(@default); - } - - public static short GetShort(this JObject source, string key, short @default = 0) - { - return ((short?)GetBase(source, key)).GetValueOrDefault(@default); - } - - public static int GetInt(this JObject source, string key, int @default = 0) - { - return ((int?)GetBase(source, key)).GetValueOrDefault(@default); - } - - public static long GetLong(this JObject source, string key, long @default = 0) - { - return ((long?)GetBase(source, key)).GetValueOrDefault(@default); - } - - public static sbyte GetSByte(this JObject source, string key, sbyte @default = 0) - { - return ((sbyte?)GetBase(source, key)).GetValueOrDefault(@default); - } - - public static ushort GetUShort(this JObject source, string key, ushort @default = 0) - { - return ((ushort?)GetBase(source, key)).GetValueOrDefault(@default); - } - - public static uint GetUInt(this JObject source, string key, uint @default = 0) - { - return ((uint?)GetBase(source, key)).GetValueOrDefault(@default); - } - - public static ulong GetULong(this JObject source, string key, ulong @default = 0) - { - return ((ulong?)GetBase(source, key)).GetValueOrDefault(@default); - } - - public static double GetDouble(this JObject source, string key, double @default = 0) - { - return ((double?)GetBase(source, key)).GetValueOrDefault(@default); - } - - public static float GetFloat(this JObject source, string key, float @default = 0) - { - return ((float?)GetBase(source, key)).GetValueOrDefault(@default); - } - - public static decimal GetDecimal(this JObject source, string key, decimal @default = 0) - { - return ((decimal?)GetBase(source, key)).GetValueOrDefault(@default); - } - - public static IEnumerable? GetArray(this JObject source, string key) - { - try - { - return (IEnumerable?)GetBase(source, key); - } - catch - { - return Array.Empty(); - } - } - - /// - /// Gets the value as a type from , returning null in event of issue. - /// - private static JToken? GetBase(this JObject source, string key) - { - source.TryGetValue(key, out var value); - - return value; // returns null when result from line above == false - } - } -} diff --git a/serializers/DragonFruit.Data.Serializers.Newtonsoft/JsonFileServices.cs b/serializers/DragonFruit.Data.Serializers.Newtonsoft/JsonFileServices.cs deleted file mode 100644 index 639e53f..0000000 --- a/serializers/DragonFruit.Data.Serializers.Newtonsoft/JsonFileServices.cs +++ /dev/null @@ -1,112 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using System.IO; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace DragonFruit.Data.Serializers.Newtonsoft -{ - /// - /// Lock-enabled file based I/O Methods - /// - public static class FileServices - { - public static JsonSerializer DefaultSerializer { get; set; } = new JsonSerializer(); - - /// - /// Read data from file as specified type - /// - /// Type the data was saved in - /// Location of the file - /// Type with populated data - public static T ReadFile(string location) - { - return ReadFile(location, DefaultSerializer); - } - - /// - /// Read data from file as specified type, or return default value if the file doesn't exist - /// - /// Type the data was saved in - /// Location of the file - /// Type with populated data - public static T ReadFileOrDefault(string location) - { - return ReadFileOrDefault(location, DefaultSerializer); - } - - /// - /// Read data from file as specified type, or return default value if the file doesn't exist - /// - /// Type the data was saved in - /// Location of the file - /// The to use - /// Type with populated data - public static T ReadFileOrDefault(string location, JsonSerializer serializer) - { - return File.Exists(location) ? ReadFile(location, serializer) : default; - } - - /// - /// Read data from file as JObject - /// - /// Location of the file - /// JObject with data - public static JObject ReadFile(string location) => ReadFile(location, JObject.Load); - - /// - /// Read data from file as specified type, or return default value if the file doesn't exist - /// - /// Type the data was saved in - /// Location of the file - /// The to use - /// Type with populated data - public static T ReadFile(string location, JsonSerializer serializer) => ReadFile(location, serializer.Deserialize); - - /// - /// Writes data to a file. If the file exists then it is overwritten with no notice - /// - /// Location of the file - /// Data to be written - public static void WriteFile(string location, T data) => WriteFile(location, data, DefaultSerializer); - - /// - /// Writes data to a file. If the file exists then it is overwritten with no notice - /// - /// Location of the file - /// Data to be written - /// The to use - public static void WriteFile(string location, T data, JsonSerializer serializer) - { - lock (location) - { - using (var reader = File.Open(location, FileMode.Create)) - using (var textWriter = new StreamWriter(reader)) - using (var jsonWriter = new JsonTextWriter(textWriter)) - { - serializer.Serialize(jsonWriter, data); - } - } - } - - private static T ReadFile(string location, Func deserializeAction) - { - lock (location) - { - if (!File.Exists(location)) - { - throw new FileNotFoundException($"The File, {Path.GetFileName(location)}, does not exist in directory, {Path.GetDirectoryName(location)}."); - } - - using (var reader = File.OpenRead(location)) - using (var textReader = new StreamReader(reader)) - using (var jsonReader = new JsonTextReader(textReader)) - { - return deserializeAction.Invoke(jsonReader); - } - } - } - } -} diff --git a/serializers/DragonFruit.Data.Serializers.SystemJson/ApiSystemTextJsonSerializer.cs b/serializers/DragonFruit.Data.Serializers.SystemJson/ApiSystemTextJsonSerializer.cs deleted file mode 100644 index 0cd9249..0000000 --- a/serializers/DragonFruit.Data.Serializers.SystemJson/ApiSystemTextJsonSerializer.cs +++ /dev/null @@ -1,50 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using System.IO; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; - -namespace DragonFruit.Data.Serializers.SystemJson -{ - public class ApiSystemTextJsonSerializer : ApiSerializer, IAsyncSerializer - { - private JsonSerializerOptions _serializerOptions; - - public override string ContentType => "application/json"; - - public override Encoding Encoding - { - get => base.Encoding; - set => throw new NotSupportedException("System.Text.Json is UTF-8 only"); - } - - public JsonSerializerOptions SerializerOptions - { - get => _serializerOptions ??= new JsonSerializerOptions(); - set => _serializerOptions = value; - } - - public override HttpContent Serialize(object input) - { - var bytes = JsonSerializer.SerializeToUtf8Bytes(input, input.GetType(), SerializerOptions); - var content = new ByteArrayContent(bytes); - - SetContentHeader(content); - - return content; - } - - public override T Deserialize(Stream input) => JsonSerializer.Deserialize(input, SerializerOptions); - - public ValueTask DeserializeAsync(Stream input) where T : class => JsonSerializer.DeserializeAsync(input, SerializerOptions); - - /// - /// Registers to always use the - /// - public static void RegisterDefaults() => SerializerResolver.Register(); - } -} diff --git a/serializers/DragonFruit.Data.Serializers.SystemJson/DragonFruit.Data.Serializers.SystemJson.csproj b/serializers/DragonFruit.Data.Serializers.SystemJson/DragonFruit.Data.Serializers.SystemJson.csproj deleted file mode 100644 index 14a95f6..0000000 --- a/serializers/DragonFruit.Data.Serializers.SystemJson/DragonFruit.Data.Serializers.SystemJson.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - DragonFruit.Data.Serializers.SystemJson - System.Text.Json serializer for DragonFruit.Data - - - - - - - - - diff --git a/serializers/DragonFruit.Data.Serializers.SystemJson/JsonFileServices.cs b/serializers/DragonFruit.Data.Serializers.SystemJson/JsonFileServices.cs deleted file mode 100644 index b31fbb0..0000000 --- a/serializers/DragonFruit.Data.Serializers.SystemJson/JsonFileServices.cs +++ /dev/null @@ -1,103 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using System.IO; -using System.Text.Json; - -namespace DragonFruit.Data.Serializers.SystemJson -{ - /// - /// Lock-enabled file based I/O Methods - /// - public static class FileServices - { - public static JsonSerializerOptions DefaultSerializer { get; set; } - - /// - /// Read data from file as specified type - /// - /// Type the data was saved in - /// Location of the file - /// Type with populated data - public static T ReadFile(string location) - { - return ReadFile(location, DefaultSerializer); - } - - /// - /// Read data from file as specified type, or return default value if the file doesn't exist - /// - /// Type the data was saved in - /// Location of the file - /// Type with populated data - public static T ReadFileOrDefault(string location) - { - return ReadFileOrDefault(location, DefaultSerializer); - } - - /// - /// Read data from file as specified type, or return default value if the file doesn't exist - /// - /// Type the data was saved in - /// Location of the file - /// The to use - /// Type with populated data - public static T ReadFileOrDefault(string location, JsonSerializerOptions serializer) - { - return File.Exists(location) ? ReadFile(location, serializer) : default; - } - - /// - /// Read data from file as a - /// - /// Location of the file - /// with data - public static JsonDocument ReadFile(string location) => ReadFile(location, s => JsonDocument.Parse(s)); - - /// - /// Read data from file as specified type, or return default value if the file doesn't exist - /// - /// Type the data was saved in - /// Location of the file - /// The to use - /// Type with populated data - public static T ReadFile(string location, JsonSerializerOptions serializer) => ReadFile(location, s => JsonSerializer.Deserialize(s, serializer)); - - /// - /// Writes data to a file. If the file exists then it is overwritten with no notice - /// - /// Location of the file - /// Data to be written - public static void WriteFile(string location, T data) => WriteFile(location, data, DefaultSerializer); - - /// - /// Writes data to a file. If the file exists then it is overwritten with no notice - /// - /// Location of the file - /// Data to be written - /// The to use - public static void WriteFile(string location, T data, JsonSerializerOptions serializer) - { - lock (location) - { - using var writer = File.Open(location, FileMode.Create); - JsonSerializer.Serialize(writer, data, data.GetType(), serializer); - } - } - - private static T ReadFile(string location, Func deserializeAction) - { - lock (location) - { - if (!File.Exists(location)) - { - throw new FileNotFoundException($"The File, {Path.GetFileName(location)}, does not exist in directory, {Path.GetDirectoryName(location)}."); - } - - using var reader = File.OpenRead(location); - return deserializeAction.Invoke(reader); - } - } - } -} diff --git a/src/ApiClient.cs b/src/ApiClient.cs deleted file mode 100644 index d1d30bf..0000000 --- a/src/ApiClient.cs +++ /dev/null @@ -1,305 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using System.Xml; -using DragonFruit.Data.Exceptions; -using DragonFruit.Data.Headers; -using DragonFruit.Data.Requests; -using DragonFruit.Data.Serializers; -using DragonFruit.Data.Utils; -using Nito.AsyncEx; - -#pragma warning disable 618 - -namespace DragonFruit.Data -{ - /// - /// Managed wrapper for a allowing easy header access, handler, serializing/deserializing and memory management - /// - public partial class ApiClient - { - private HttpClient _client; - private Version _httpVersion; - private Func _handler; - private long _clientAdjustmentRequestSignal; - private readonly AsyncReaderWriterLock _lock; - - /// - /// Initialises a new using a user-set - /// - public ApiClient(ApiSerializer serializer) - { - Headers = new HeaderCollection(this); - Serializer = new SerializerResolver(serializer); - - _lock = new AsyncReaderWriterLock(); - - RequestClientReset(true); - } - - ~ApiClient() - { - _client?.Dispose(); - } - - static ApiClient() - { - // register generic xml document type - SerializerResolver.Register(); - - // register stream resolver types (inwards only) - SerializerResolver.Register(DataDirection.In); - SerializerResolver.Register(DataDirection.In); - SerializerResolver.Register(DataDirection.In); - } - - #region Factories - - /// - /// Checks the current and replaces it if headers or has been modified - /// - private (HttpClient Client, IDisposable Lock) GetClient() - { - // return current client if there are no changes - var resetLevel = Interlocked.Exchange(ref _clientAdjustmentRequestSignal, 0); - - if (resetLevel > 0) - { - // block all reads and let all current requests finish - using (_lock.WriterLock()) - { - // only reset the client if the handler has changed (signal = 2) - var resetClient = resetLevel == 2; - - if (resetClient) - { - var handler = CreateHandler(); - - _client?.Dispose(); - _client = handler != null ? new HttpClient(handler, true) : new HttpClient(); - } - - // apply new headers - Headers.ApplyTo(_client); - - // allow the consumer to change the client - SetupClient(_client, resetClient); - - // reset the state - Interlocked.Exchange(ref _clientAdjustmentRequestSignal, 0); - } - } - - return (_client, _lock.ReaderLock()); - } - - #endregion - - /// - /// Internal procedure for performing a web-request - /// - /// - /// While the consumer has the option to prevent disposal of the produced, - /// the passed is always disposed at the end of the request. - /// - /// The request to perform - /// to process the - /// Whether to dispose of the produced after has been invoked. - /// (optional) - protected async Task InternalPerform(HttpRequestMessage request, Func> processResult, bool disposeResponse, CancellationToken token = default) - { - var (client, clientLock) = GetClient(); - - using (clientLock) - { - // post-modification - await SetupRequest(request).ConfigureAwait(false); - HttpResponseMessage response = null; - - try - { - // send request - response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false); - - // evaluate task status and update monitor - return await processResult.Invoke(response).ConfigureAwait(false); - } - finally - { - request.Dispose(); - - if (disposeResponse) - { - response?.Dispose(); - } - } - } - } - - /// - /// Validates the and uses the to deserialize data (if successful) - /// - protected virtual async Task ValidateAndProcess(HttpResponseMessage response) where T : class - { - response.EnsureSuccessStatusCode(); - - using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - var serializer = Serializer.Resolve(DataDirection.In); - - if (serializer is IAsyncSerializer asyncSerializer) - { - return await asyncSerializer.DeserializeAsync(stream).ConfigureAwait(false); - } - - return serializer.Deserialize(stream); - } - - /// - /// An overridable method for validating the request against the current - /// - /// The request to validate - /// The request can't be performed due to a poorly-formed url - /// The client can't be used because there is no auth url. - protected virtual async Task ValidateRequest(ApiRequest request) - { - if (request is IRequestExecutingCallback syncCallback) - { - syncCallback.OnRequestExecuting(this); - } - - if (request is IAsyncRequestExecutingCallback asyncCallback) - { - await asyncCallback.OnRequestExecutingAsync(this).ConfigureAwait(false); - } - - // note request path is validated on build - if (request.RequireAuth && string.IsNullOrEmpty(Authorization)) - { - // check if we have a custom headerset in the request - if (!request.CustomHeaderCollectionCreated || !request.Headers.Any(x => x.Key.Equals("Authorization"))) - { - throw new ClientValidationException("Authorization header was expected, but not found (in request or client)"); - } - } - } - - /// - /// Requests the client is reset on the next request - /// - /// Whether to reset the as well as the headers - public void RequestClientReset(bool fullReset) - { - if (fullReset) - { - Interlocked.Exchange(ref _clientAdjustmentRequestSignal, 2); - } - else - { - Interlocked.CompareExchange(ref _clientAdjustmentRequestSignal, 1, 0); - } - } - - #region Properties - - /// - /// The User-Agent header to pass in all requests - /// - public string UserAgent - { - get => Headers["User-Agent"]; - set => Headers["User-Agent"] = value; - } - - /// - /// The Authorization header value - /// - public string Authorization - { - get => Headers["Authorization"]; - set => Headers["Authorization"] = value; - } - - /// - /// Headers to be sent with the requests - /// - public HeaderCollection Headers { get; } - - /// - /// Gets or sets the HTTP version to use on requests passed through the . - /// When setting this property, consider all target devices and whether they have support for the version targeted. - /// - public Version HttpVersion - { - get => _httpVersion ??= HttpVersionUtils.DefaultHttpVersion; - set => _httpVersion = value; - } - - /// - /// Optional factory to be consumed by the - /// - /// - /// This must create a new handler each time as they are disposed alongside the client - /// - public Func Handler - { - get => _handler; - set - { - _handler = value; - RequestClientReset(true); - } - } - - /// - /// The container for s. The default serializer can be set at - /// - public SerializerResolver Serializer { get; } - - #endregion - - #region Empty Overrides (Inherited) - - /// - /// Overridable method for creating a to use with the - /// - /// - /// This should be used when a library needs to enforce a is wrapped over the . - /// If overriden, the client should be sealed to prevent unintended changes - /// - protected virtual HttpMessageHandler CreateHandler() => Handler?.Invoke(); - - /// - /// Overridable method to customise the . - /// - /// Custom headers can be included here, but should be done in the dictionary. - /// - /// - /// - /// This is called when the client or it's headers are reset. - /// The is set to true to allow you to configure client settings (not headers) after creation. - /// - /// The to modify - /// Whether the client were reset/disposed - protected virtual void SetupClient(HttpClient client, bool clientReset) - { - } - - /// - /// Sets properties of the to be sent. - /// - /// The request to be sent to the remote server - protected virtual async ValueTask SetupRequest(HttpRequestMessage request) - { - // HTTP versions need to be overriden at the request level - targeting the client won't work - request.Version = HttpVersion; - } - - #endregion - } -} diff --git a/src/ApiClient_Async.cs b/src/ApiClient_Async.cs deleted file mode 100644 index e2390ab..0000000 --- a/src/ApiClient_Async.cs +++ /dev/null @@ -1,124 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using System.Buffers; -using System.IO; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using DragonFruit.Data.Exceptions; - -namespace DragonFruit.Data -{ - public partial class ApiClient - { - /// - /// Perform a request to the specified that returns a strongly-typed class. - /// - public Task PerformAsync(string url, CancellationToken token = default) where T : class - { - var request = new HttpRequestMessage(HttpMethod.Get, url); - return PerformAsync(request, token); - } - - /// - /// Perform an with a specified return type. - /// - public async Task PerformAsync(ApiRequest requestData, CancellationToken token = default) where T : class - { - await ValidateRequest(requestData).ConfigureAwait(false); - return await PerformAsync(requestData.Build(this), token).ConfigureAwait(false); - } - - /// - /// Perform a pre-fabricated and deserialize the result to the specified type - /// - public Task PerformAsync(HttpRequestMessage request, CancellationToken token = default) where T : class - { - return InternalPerform(request, ValidateAndProcess, true, token); - } - - /// - /// Perform a request to the specified that returns a . - /// - public Task PerformAsync(string url, CancellationToken token = default) - { - var request = new HttpRequestMessage(HttpMethod.Get, url); - return PerformAsync(request, token); - } - - /// - /// Perform a that returns the response message. - /// - public async Task PerformAsync(ApiRequest requestData, CancellationToken token = default) - { - await ValidateRequest(requestData).ConfigureAwait(false); - return await PerformAsync(requestData.Build(this), token).ConfigureAwait(false); - } - - /// - /// Perform a pre-fabricated - /// - public Task PerformAsync(HttpRequestMessage request, CancellationToken token = default) - { - return InternalPerform(request, Task.FromResult, false, token); - } - - /// - /// Download a file with an - /// - /// - /// Bypasses - /// - public async Task PerformAsync(ApiFileRequest request, Action progressUpdated = null, CancellationToken token = default) - { - // check request data is valid - await ValidateRequest(request).ConfigureAwait(false); - - if (string.IsNullOrWhiteSpace(request.Destination)) - { - throw new NullRequestException(); - } - - // get raw response - var response = await InternalPerform(request.Build(this), Task.FromResult, false, token).ConfigureAwait(false); - - // validate - response.EnsureSuccessStatusCode(); - - // create a new filestream and copy all data into - using var stream = File.Open(request.Destination, request.FileCreationMode); - using var networkStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - - var buffer = ArrayPool.Shared.Rent(4096); - - try - { - int count; - var iterations = 0; - - while ((count = await networkStream.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false)) > 0) - { - Interlocked.Increment(ref iterations); - await stream.WriteAsync(buffer, 0, count, token).ConfigureAwait(false); - - // check every 10th time to stop bottlenecks (use CompareExchange to stop the int from overflowing from insanely large file downloads) - if (Interlocked.CompareExchange(ref iterations, 0, 100) == 100) - { - progressUpdated?.Invoke(stream.Length, response.Content.Headers.ContentLength); - } - } - - // flush, return buffer and send a final update - await stream.FlushAsync(token).ConfigureAwait(false); - } - finally - { - ArrayPool.Shared.Return(buffer); - } - - progressUpdated?.Invoke(stream.Length, response.Content.Headers.ContentLength); - } - } -} diff --git a/src/ApiFileRequest.cs b/src/ApiFileRequest.cs deleted file mode 100644 index 09aca44..0000000 --- a/src/ApiFileRequest.cs +++ /dev/null @@ -1,20 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System.IO; - -namespace DragonFruit.Data -{ - public abstract class ApiFileRequest : ApiRequest - { - /// - /// The location, on the disk, to put the resultant file - /// - public abstract string Destination { get; } - - /// - /// The mode of file creation - /// - public virtual FileMode FileCreationMode => FileMode.Create; - } -} diff --git a/src/Basic/BasicApiFileRequest.cs b/src/Basic/BasicApiFileRequest.cs deleted file mode 100644 index 9d3bfd0..0000000 --- a/src/Basic/BasicApiFileRequest.cs +++ /dev/null @@ -1,48 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using System.Collections.Generic; -using DragonFruit.Data.Utils; -using SystemPath = System.IO.Path; - -namespace DragonFruit.Data.Basic -{ - public class BasicApiFileRequest : ApiFileRequest, IBasicApiRequest - { - public BasicApiFileRequest(string path) - : this(path, SystemPath.GetFileName(path)) - { - } - - public BasicApiFileRequest(string path, Environment.SpecialFolder baseFolder) - : this(path, SystemPath.Combine(Environment.GetFolderPath(baseFolder), SystemPath.GetFileName(path))) - - { - } - - public BasicApiFileRequest(string path, string destination) - { - Path = path; - - if (string.IsNullOrEmpty(SystemPath.GetExtension(destination)) && !(destination.StartsWith(".") || destination.EndsWith("."))) - { - // we were provided with a path - Destination = SystemPath.Combine(destination, SystemPath.GetFileName(path)); - } - else - { - Destination = destination; - } - } - - internal override string UrlCompiler => Queries.IsValueCreated - ? Path + QueryUtils.QueryStringFrom(Queries.Value) - : Path; - - public override string Path { get; } - public override string Destination { get; } - - public Lazy>> Queries { get; } = new Lazy>>(() => new List>()); - } -} diff --git a/src/Basic/BasicApiRequest.cs b/src/Basic/BasicApiRequest.cs deleted file mode 100644 index 74618eb..0000000 --- a/src/Basic/BasicApiRequest.cs +++ /dev/null @@ -1,32 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using System.Collections.Generic; -using DragonFruit.Data.Utils; - -namespace DragonFruit.Data.Basic -{ - public class BasicApiRequest : ApiRequest, IBasicApiRequest - { - /// - /// Initialises a new with a path to the resource - /// - /// - public BasicApiRequest(string path) - { - Path = path; - } - - public override string Path { get; } - - internal override string UrlCompiler => Queries.IsValueCreated - ? Path + QueryUtils.QueryStringFrom(Queries.Value) - : Path; - - /// - /// Collection of s to use as a query string - /// - public Lazy>> Queries { get; } = new Lazy>>(() => new List>()); - } -} diff --git a/src/Basic/BasicApiRequestExtensions.cs b/src/Basic/BasicApiRequestExtensions.cs deleted file mode 100644 index 85c353d..0000000 --- a/src/Basic/BasicApiRequestExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System.Collections.Generic; - -namespace DragonFruit.Data.Basic -{ - public static class BasicApiRequestExtensions - { - /// - /// Appends a query parameter to the current - /// - public static T WithQuery(this T request, string key, object value) where T : IBasicApiRequest - { - return request.WithQuery(key, value.ToString()); - } - - /// - /// Appends a query parameter to the current - /// - public static T WithQuery(this T request, string key, string value) where T : IBasicApiRequest - { - request.Queries.Value.Add(new KeyValuePair(key, value)); - return request; - } - } -} diff --git a/src/Basic/IBasicApiRequest.cs b/src/Basic/IBasicApiRequest.cs deleted file mode 100644 index 6586caa..0000000 --- a/src/Basic/IBasicApiRequest.cs +++ /dev/null @@ -1,16 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using System.Collections.Generic; - -namespace DragonFruit.Data.Basic -{ - public interface IBasicApiRequest - { - /// - /// Collection of s to use as a query string - /// - public Lazy>> Queries { get; } - } -} diff --git a/src/Exceptions/ClientValidationException.cs b/src/Exceptions/ClientValidationException.cs deleted file mode 100644 index f152e11..0000000 --- a/src/Exceptions/ClientValidationException.cs +++ /dev/null @@ -1,15 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; - -namespace DragonFruit.Data.Exceptions -{ - public class ClientValidationException : Exception - { - public ClientValidationException(string message) - : base(message) - { - } - } -} diff --git a/src/Exceptions/NullRequestException.cs b/src/Exceptions/NullRequestException.cs deleted file mode 100644 index 91690d5..0000000 --- a/src/Exceptions/NullRequestException.cs +++ /dev/null @@ -1,15 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; - -namespace DragonFruit.Data.Exceptions -{ - public class NullRequestException : Exception - { - public NullRequestException() - : base("The Request provided was null or has no path") - { - } - } -} diff --git a/src/Extensions/RequestExtensions.cs b/src/Extensions/RequestExtensions.cs deleted file mode 100644 index 5f0e033..0000000 --- a/src/Extensions/RequestExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System.Collections.Generic; - -namespace DragonFruit.Data.Extensions -{ - public static class RequestExtensions - { - /// - /// Sets the specified header for this request - /// - /// The to set the header on - /// The header name - /// The header value - public static T WithHeader(this T request, string key, string value) where T : ApiRequest - { - request.Headers.Add(new KeyValuePair(key, value)); - return request; - } - - /// - /// Sets the Authorization header for this request - /// - /// The to set the header on - /// The auth header - public static T WithAuthHeader(this T request, string value) where T : ApiRequest - { - request.Headers.Add(new KeyValuePair("Authorization", value)); - return request; - } - } -} diff --git a/src/Extensions/ResponseExtensions.cs b/src/Extensions/ResponseExtensions.cs deleted file mode 100644 index b8cd151..0000000 --- a/src/Extensions/ResponseExtensions.cs +++ /dev/null @@ -1,38 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using System.Net.Http; - -namespace DragonFruit.Data.Extensions -{ - public static class ResponseExtensions - { - public static T To(this HttpResponseMessage response, bool disposeResponse = true) - { - try - { - response.EnsureSuccessStatusCode(); - - var targetType = typeof(T); - - switch (targetType.IsPrimitive) - { - case true: - case false when targetType == typeof(string): - return (T)Convert.ChangeType(response.Content.ReadAsStringAsync().Result, targetType); - - default: - throw new ArgumentException($"Cannot convert HTTP response to {targetType}. It must be a primitive type or a string", nameof(T)); - } - } - finally - { - if (disposeResponse) - { - response.Dispose(); - } - } - } - } -} diff --git a/src/Headers/HeaderCollection.cs b/src/Headers/HeaderCollection.cs deleted file mode 100644 index 7df2e34..0000000 --- a/src/Headers/HeaderCollection.cs +++ /dev/null @@ -1,69 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Net.Http; - -namespace DragonFruit.Data.Headers -{ - public class HeaderCollection - { - private readonly ApiClient _client; - private readonly ConcurrentDictionary _values; - - public HeaderCollection(ApiClient client) - { - _values = new ConcurrentDictionary(); - _client = client; - } - - /// - /// Gets or sets the specified value for the key provided. - /// - /// Getting the value for a non-existent key will return null - /// - /// - /// To queue a removal, pass null as the value - /// - /// - /// - /// This will check the queued changes first, and if there are no matching changes, attempt to find it in the "live" headers. - /// - public string this[string key] - { - get - { - _values.TryGetValue(key, out var value); - return value; - } - - set - { - _values[key] = value; - _client.RequestClientReset(false); - } - } - - /// - /// Clears all queued changes and queues all active headers to be removed - /// - public void Clear() - { - _values.Clear(); - } - - /// - /// Applies the s to the provided - /// - internal void ApplyTo(HttpClient client) - { - client.DefaultRequestHeaders.Clear(); - - foreach (var header in _values) - { - client.DefaultRequestHeaders.Add(header.Key, header.Value); - } - } - } -} diff --git a/src/Methods.cs b/src/Methods.cs deleted file mode 100644 index b139b19..0000000 --- a/src/Methods.cs +++ /dev/null @@ -1,87 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using DragonFruit.Data.Parameters; - -namespace DragonFruit.Data -{ - public enum Methods - { - Get, - Head, - Post, - Put, - Patch, - Delete, - Trace - } - - public enum BodyType - { - /// - /// Finds all properties marked with and creates a url-form encoded content from them - /// - Encoded, - - /// - /// Serializes the current and sends it as a string using the serialization method specified - /// - Serialized, - - /// - /// Finds the single -marked property and serializes it - /// - SerializedProperty, - - /// - /// Tells the client to use the property override to get the content - /// - Custom - } - - public enum CollectionConversionMode - { - /// - /// The query name is repeated and a new element created for each (a=1&a=2&a=3) - /// - Recursive, - - /// - /// The query name has indexer symbols appended with no order (a[]=1&a[]=2&a[]=3) - /// - Unordered, - - /// - /// The query name has indexer symbols appended explicit order inserted (a[0]=1&a[1]=2&a[2]=3) - /// - Ordered, - - /// - /// The query is concatenated with a string and merged with one key (a=1,2,3) - /// - Concatenated - } - - public enum EnumHandlingMode - { - /// - /// Convert to the integer representation - /// - Numeric, - - /// - /// Convert to string form - /// - String, - - /// - /// Convert to lowercase string form - /// - StringLower, - - /// - /// Convert to uppercase string form - /// - StringUpper - } -} diff --git a/src/Parameters/FormParameter.cs b/src/Parameters/FormParameter.cs deleted file mode 100644 index f668568..0000000 --- a/src/Parameters/FormParameter.cs +++ /dev/null @@ -1,40 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; - -#nullable enable - -namespace DragonFruit.Data.Parameters -{ - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] - public class FormParameter : Attribute, IProperty - { - public FormParameter() - { - } - - public FormParameter(string name) - { - Name = name; - } - - public FormParameter(string name, EnumHandlingMode enumHandling) - : this(name) - { - EnumHandling = enumHandling; - } - - public FormParameter(string name, CollectionConversionMode collectionHandling) - : this(name) - { - CollectionHandling = collectionHandling; - } - - public string? Name { get; set; } - public CollectionConversionMode? CollectionHandling { get; set; } - public EnumHandlingMode? EnumHandling { get; set; } - - public string? CollectionSeparator { get; set; } - } -} diff --git a/src/Parameters/IProperty.cs b/src/Parameters/IProperty.cs deleted file mode 100644 index 58305ee..0000000 --- a/src/Parameters/IProperty.cs +++ /dev/null @@ -1,18 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -#nullable enable - -namespace DragonFruit.Data.Parameters -{ - public interface IProperty - { - string? Name { get; set; } - - CollectionConversionMode? CollectionHandling { get; set; } - - EnumHandlingMode? EnumHandling { get; set; } - - string? CollectionSeparator { get; set; } - } -} diff --git a/src/Parameters/QueryParameter.cs b/src/Parameters/QueryParameter.cs deleted file mode 100644 index 24323a1..0000000 --- a/src/Parameters/QueryParameter.cs +++ /dev/null @@ -1,40 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; - -#nullable enable - -namespace DragonFruit.Data.Parameters -{ - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] - public class QueryParameter : Attribute, IProperty - { - public QueryParameter() - { - } - - public QueryParameter(string name) - { - Name = name; - } - - public QueryParameter(string name, EnumHandlingMode enumHandling) - : this(name) - { - EnumHandling = enumHandling; - } - - public QueryParameter(string name, CollectionConversionMode collectionConversionMode) - : this(name) - { - CollectionHandling = collectionConversionMode; - } - - public string? Name { get; set; } - public CollectionConversionMode? CollectionHandling { get; set; } - public EnumHandlingMode? EnumHandling { get; set; } - - public string? CollectionSeparator { get; set; } - } -} diff --git a/src/Parameters/RequestBody.cs b/src/Parameters/RequestBody.cs deleted file mode 100644 index b297101..0000000 --- a/src/Parameters/RequestBody.cs +++ /dev/null @@ -1,12 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; - -namespace DragonFruit.Data.Parameters -{ - [AttributeUsage(AttributeTargets.Property)] - public class RequestBody : Attribute - { - } -} diff --git a/src/Requests/IAsyncRequestExecutingCallback.cs b/src/Requests/IAsyncRequestExecutingCallback.cs deleted file mode 100644 index 4d7431b..0000000 --- a/src/Requests/IAsyncRequestExecutingCallback.cs +++ /dev/null @@ -1,20 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System.Net.Http; -using System.Threading.Tasks; - -namespace DragonFruit.Data.Requests -{ - /// - /// Specifies the should have its method called after when the request is being executed - /// - public interface IAsyncRequestExecutingCallback - { - /// - /// Overridable method for specifying an action to occur before sending the request to the . - /// Unlike , this will be run asynchronously and must return a . - /// - ValueTask OnRequestExecutingAsync(ApiClient client); - } -} diff --git a/src/Requests/IRequestExecutingCallback.cs b/src/Requests/IRequestExecutingCallback.cs deleted file mode 100644 index 6bb3f79..0000000 --- a/src/Requests/IRequestExecutingCallback.cs +++ /dev/null @@ -1,16 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -namespace DragonFruit.Data.Requests -{ - /// - /// Specifies the should have its method called after when the request is being executed - /// - public interface IRequestExecutingCallback - { - /// - /// Overridable method for specifying an action to occur before sending the request to the - /// - void OnRequestExecuting(ApiClient client); - } -} diff --git a/src/Serializers/ApiSerializer.cs b/src/Serializers/ApiSerializer.cs deleted file mode 100644 index da4b823..0000000 --- a/src/Serializers/ApiSerializer.cs +++ /dev/null @@ -1,95 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System.IO; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; - -#pragma warning disable 618 - -namespace DragonFruit.Data.Serializers -{ - /// - /// Represents the base of a serializer used by the and classes - /// - public abstract class ApiSerializer - { - private Encoding _encoding; - - /// - /// Whether types can use the disk as a buffer. - /// Defaults to true - /// - public static bool AllowDiskBuffering { get; set; } = true; - - /// - /// The content-type header value - /// - public abstract string ContentType { get; } - - /// - /// Whether this is generic (meaning any class can be serialized to/from). - /// - /// - /// Setting this to false will throw an exception if the serializer is set as a default in a client. - /// - public virtual bool IsGeneric => true; - - /// - /// Gets or sets the encoding the uses - /// - public virtual Encoding Encoding - { - get => _encoding ??= new UTF8Encoding(false); - set => _encoding = value; - } - - /// - /// Whether deserialization should attempt to automatically detect the encoding used - /// - public bool AutoDetectEncoding { get; set; } = true; - - public abstract HttpContent Serialize(object input); - public abstract T Deserialize(Stream input) where T : class; - - /// - /// Gets a 0-positioned with seek support - /// - protected Stream GetStream(bool largeBody) - { - if (largeBody && AllowDiskBuffering) - { - return File.Create(Path.GetTempFileName(), 4096, FileOptions.SequentialScan | FileOptions.Asynchronous | FileOptions.DeleteOnClose); - } - - return new MemoryStream(4096); - } - - /// - /// Converts a serialized in the to the equivalent - /// - /// The stream to convert - protected HttpContent GetHttpContent(Stream stream) - { - stream.Seek(0, SeekOrigin.Begin); - var content = new StreamContent(stream); - - SetContentHeader(content); - content.Headers.ContentLength = stream.Length; - - return content; - } - - /// - /// Applies the Content-Type header to the provided - /// - protected void SetContentHeader(HttpContent content) - { - content.Headers.ContentType = new MediaTypeHeaderValue(ContentType) - { - CharSet = Encoding.HeaderName - }; - } - } -} diff --git a/src/Serializers/ApiXmlSerializer.cs b/src/Serializers/ApiXmlSerializer.cs deleted file mode 100644 index 55a1c7c..0000000 --- a/src/Serializers/ApiXmlSerializer.cs +++ /dev/null @@ -1,40 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System.IO; -using System.Net.Http; -using System.Xml.Serialization; - -namespace DragonFruit.Data.Serializers -{ - public class ApiXmlSerializer : ApiSerializer - { - public override string ContentType => "application/xml"; - - public override HttpContent Serialize(object input) - { - var stream = GetStream(false); - - using (var writer = new StreamWriter(stream, Encoding, 4096, true)) - { - new XmlSerializer(input.GetType()).Serialize(writer, input); - } - - return GetHttpContent(stream); - } - - public override T Deserialize(Stream input) where T : class - { - var serializer = new XmlSerializer(typeof(T)); - using TextReader reader = AutoDetectEncoding switch - { - true => new StreamReader(input, true), - - false when Encoding is null => new StreamReader(input), - false => new StreamReader(input, Encoding) - }; - - return (T)serializer.Deserialize(reader); - } - } -} diff --git a/src/Serializers/IAsyncSerializer.cs b/src/Serializers/IAsyncSerializer.cs deleted file mode 100644 index 8a6960a..0000000 --- a/src/Serializers/IAsyncSerializer.cs +++ /dev/null @@ -1,16 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System.IO; -using System.Threading.Tasks; - -namespace DragonFruit.Data.Serializers -{ - /// - /// Extends a by providing async support - /// - public interface IAsyncSerializer - { - ValueTask DeserializeAsync(Stream input) where T : class; - } -} diff --git a/src/Serializers/InternalStreamSerializer.cs b/src/Serializers/InternalStreamSerializer.cs deleted file mode 100644 index 817b314..0000000 --- a/src/Serializers/InternalStreamSerializer.cs +++ /dev/null @@ -1,52 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; - -namespace DragonFruit.Data.Serializers -{ - internal class InternalStreamSerializer : ApiSerializer, IAsyncSerializer - { - public override string ContentType => "application/octet-stream"; - - public async ValueTask DeserializeAsync(Stream input) where T : class - { - var stream = GetStream(); - await input.CopyToAsync(stream).ConfigureAwait(false); - - await stream.FlushAsync().ConfigureAwait(false); - stream.Seek(0, SeekOrigin.Begin); - - return stream as T; - } - - public override HttpContent Serialize(object input) - { - throw new NotSupportedException("Stream serialization is currently one-way"); - } - - public override T Deserialize(Stream input) - { - var stream = GetStream(); - input.CopyTo(stream); - - stream.Flush(); - stream.Seek(0, SeekOrigin.Begin); - - return stream as T; - } - - private Stream GetStream() - { - if (typeof(T) == typeof(MemoryStream)) - { - return new MemoryStream(); - } - - return File.Create(Path.GetTempFileName(), 4096, FileOptions.Asynchronous | FileOptions.SequentialScan | FileOptions.DeleteOnClose); - } - } -} diff --git a/src/Serializers/SerializerResolver.cs b/src/Serializers/SerializerResolver.cs deleted file mode 100644 index a4dfa8f..0000000 --- a/src/Serializers/SerializerResolver.cs +++ /dev/null @@ -1,184 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; - -#pragma warning disable 618 - -namespace DragonFruit.Data.Serializers -{ - public class SerializerResolver - { - private static readonly Dictionary SerializerMap = new Dictionary(); - private static readonly Dictionary DeserializerMap = new Dictionary(); - - private readonly ConcurrentDictionary _serializerCache = new ConcurrentDictionary(); - - /// - /// Initialises a new instance of , providing a default - /// - /// - internal SerializerResolver(ApiSerializer @default) - { - Default = @default; - } - - /// - /// The default in use. - /// - public ApiSerializer Default { get; } - - /// - /// Registers a serializer for the specified type. This applies to all s - /// - /// Whether this serializer should apply to incoming/outgoing data - /// The object type to specify the serializer for - /// The serializer to apply - public static void Register(DataDirection direction = DataDirection.All) - where T : class - where TSerializer : ApiSerializer, new() - { - Register(typeof(T), direction); - } - - /// - /// Registers a serializer for the specified type. This applies to all s - /// - /// The object type to specify the serializer for - /// Whether this serializer should apply to incoming/outgoing data - /// The serializer to apply - public static void Register(Type targetType, DataDirection direction = DataDirection.All) where TSerializer : ApiSerializer, new() - { - if (!targetType.IsClass) - { - throw new ArgumentException($"{targetType.Name} is not a class", nameof(targetType)); - } - - if (direction.HasFlag(DataDirection.In)) - { - DeserializerMap[targetType] = typeof(TSerializer); - } - - if (direction.HasFlag(DataDirection.Out)) - { - SerializerMap[targetType] = typeof(TSerializer); - } - } - - /// - /// Removes the registered serializer for the type. This applies to all s - /// - /// Whether this serializer should be removed from incoming/outgoing data - /// The object type to remove the serializer for - public static void Unregister(DataDirection direction = DataDirection.All) where T : class - { - Unregister(typeof(T), direction); - } - - /// - /// Removes the registered serializer for the type. This applies to all s - /// - /// The object type to remove the serializer for - /// Whether this serializer should be removed from incoming/outgoing data - public static void Unregister(Type targetType, DataDirection direction = DataDirection.All) - { - if (!targetType.IsClass) - { - throw new ArgumentException($"{targetType.Name} is not a class", nameof(targetType)); - } - - if (direction.HasFlag(DataDirection.In)) - { - DeserializerMap.Remove(targetType); - } - - if (direction.HasFlag(DataDirection.Out)) - { - SerializerMap.Remove(targetType); - } - } - - /// - /// Resolves the for the type provided - /// - /// The type to resolve - public ApiSerializer Resolve(DataDirection direction) where T : class - { - return Resolve(typeof(T), direction); - } - - /// - /// Resolves the for the type provided - /// - public ApiSerializer Resolve(Type objectType, DataDirection direction) - { - if (!objectType.IsClass) - { - // at this point in time, we only support non-generic class - // this is because this isn't designed to filter generic types - return Default; - } - - var mapping = direction switch - { - DataDirection.In => DeserializerMap, - DataDirection.Out => SerializerMap, - - _ => throw new ArgumentException(nameof(direction)) - }; - - Type serializerType; - - // if the map has the type registered, check the type in cache - if (mapping.TryGetValue(objectType, out serializerType) || (objectType.IsConstructedGenericType && mapping.TryGetValue(objectType.GetGenericTypeDefinition(), out serializerType))) - { - return _serializerCache.GetOrAdd(serializerType, _ => (ApiSerializer)Activator.CreateInstance(serializerType)); - } - - // use generic - return Default; - } - - /// - /// Configures the specified , creating a client-specific instance if needed - /// - /// The options to set - /// The to configure - public void Configure(Action options) where TSerializer : ApiSerializer - { - if (Default.GetType() == typeof(TSerializer)) - { - options?.Invoke((TSerializer)Default); - } - else if (DeserializerMap.ContainsValue(typeof(TSerializer)) || SerializerMap.ContainsValue(typeof(TSerializer))) - { - var serializer = _serializerCache.GetOrAdd(typeof(TSerializer), _ => Activator.CreateInstance()); - options?.Invoke((TSerializer)serializer); - } - else - { - throw new ArgumentException("The specified serializer was not registered anywhere. It needs to be registered or set as the default before configuration can occur", nameof(TSerializer)); - } - } - } - - public enum DataDirection - { - /// - /// Applies to incoming data (deserialization) - /// - In, - - /// - /// Applies to outgoing data (serialization) - /// - Out, - - /// - /// Applies to all data directions - /// - All = In | Out - } -} diff --git a/src/TargetTypedApiClient.cs b/src/TargetTypedApiClient.cs deleted file mode 100644 index 12b71f5..0000000 --- a/src/TargetTypedApiClient.cs +++ /dev/null @@ -1,24 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using DragonFruit.Data.Serializers; - -namespace DragonFruit.Data -{ - /// - /// A superclass designed to allow better serializer configuration - /// - /// The to use - public class ApiClient : ApiClient where T : ApiSerializer, new() - { - public ApiClient(Action configurationOptions = null) - : base(Activator.CreateInstance()) - { - if (configurationOptions != null) - { - Serializer.Configure(configurationOptions); - } - } - } -} diff --git a/src/Utils/CultureUtils.cs b/src/Utils/CultureUtils.cs deleted file mode 100644 index c52e12c..0000000 --- a/src/Utils/CultureUtils.cs +++ /dev/null @@ -1,32 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using System.Globalization; - -namespace DragonFruit.Data.Utils -{ - public static class CultureUtils - { - private static CultureInfo _defaultCulture; - - public static CultureInfo DefaultCulture - { - get => _defaultCulture ?? CultureInfo.InvariantCulture; - set => _defaultCulture = value; - } - - internal static string AsString(this object value, CultureInfo culture = null) - { - culture ??= DefaultCulture; - return value switch - { - null => null, - bool boolVar => boolVar.ToString().ToLower(), - IFormattable formattableVar => formattableVar.ToString(null, culture), - - _ => value.ToString() - }; - } - } -} diff --git a/src/Utils/HttpVersionUtils.cs b/src/Utils/HttpVersionUtils.cs deleted file mode 100644 index 095670c..0000000 --- a/src/Utils/HttpVersionUtils.cs +++ /dev/null @@ -1,32 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; - -namespace DragonFruit.Data.Utils -{ - internal static class HttpVersionUtils - { - /// - /// Gets the default HTTP suitable for the host system - /// - public static Version DefaultHttpVersion - { - get - { -#if NETSTANDARD - return System.Net.HttpVersion.Version11; -#else - if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows) && Environment.OSVersion.Version.Major < 10) - { - // because the WinHttpHandler exists, versions prior to Windows 10 might fallover and cause unexpected breakage. - // if this isn't the case, a developer can simply override the version on initialisation, bypassing this check. - return System.Net.HttpVersion.Version11; - } - - return System.Net.HttpVersion.Version20; -#endif - } - } - } -} diff --git a/src/Utils/ParameterUtils.cs b/src/Utils/ParameterUtils.cs deleted file mode 100644 index 63f0f79..0000000 --- a/src/Utils/ParameterUtils.cs +++ /dev/null @@ -1,154 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using DragonFruit.Data.Parameters; - -namespace DragonFruit.Data.Utils -{ - public static class ParameterUtils - { - private const string DefaultConcatenationCharacter = ","; - - /// - /// Gets an of s from properties with a specified -inheriting attribute. - /// - internal static IEnumerable> GetParameter(object host, CultureInfo culture) where T : IProperty - { - foreach (var property in host.GetType().GetTargetProperties()) - { - if (!property.CanRead || !(Attribute.GetCustomAttribute(property, typeof(T)) is T attribute)) - { - continue; - } - - var keyName = attribute.Name ?? property.Name; - var propertyValue = property.GetValue(host); - - if (propertyValue == null) - { - // ignore null values - continue; - } - - // check if the type we've got is an IEnumerable of anything AND we have a valid collection handler mode - if (attribute.CollectionHandling.HasValue && typeof(IEnumerable).IsAssignableFrom(property.PropertyType)) - { - Func>> entityConverter = attribute.CollectionHandling switch - { - CollectionConversionMode.Ordered => values => ApplyOrderedConversion(values, keyName, culture), - CollectionConversionMode.Recursive => values => values.Cast().Select(x => x.ToKeyValuePair(keyName, culture)), - CollectionConversionMode.Unordered => values => values.Cast().Select(x => x.ToKeyValuePair($"{keyName}[]", culture)), - CollectionConversionMode.Concatenated => values => ApplyConcatenation(values, keyName, culture, attribute.CollectionSeparator ?? DefaultConcatenationCharacter), - - _ => throw new ArgumentOutOfRangeException() - }; - - // we purposely keep nulls in here, as it might affect the ordering. - foreach (var entry in entityConverter.Invoke((IEnumerable)propertyValue)) - { - yield return entry; - } - } - else if (property.PropertyType.IsEnum) - { - switch (attribute.EnumHandling) - { - case EnumHandlingMode.Numeric: - yield return ((int)propertyValue).ToKeyValuePair(keyName, culture); - break; - - case EnumHandlingMode.StringLower: - yield return propertyValue.ToString().ToLower(culture).Replace(" ", string.Empty).ToKeyValuePair(keyName, culture); - break; - - case EnumHandlingMode.StringUpper: - yield return propertyValue.ToString().ToUpper(culture).Replace(" ", string.Empty).ToKeyValuePair(keyName, culture); - break; - - default: - yield return propertyValue.ToKeyValuePair(keyName, culture); - break; - } - } - else - { - yield return propertyValue.ToKeyValuePair(keyName, culture); - } - } - } - - /// - /// Gets the single attribute of its kind from a class. - /// - internal static object GetSingleParameterObject(object host) where T : Attribute - { - var targetType = typeof(T); - var attributedProperty = host.GetType() - .GetTargetProperties() - .SingleOrDefault(x => Attribute.GetCustomAttribute(x, targetType) is T); - - if (attributedProperty == default) - { - throw new KeyNotFoundException($"No valid {targetType.Name} was attributed. There must be a single attributed property"); - } - - if (!attributedProperty.CanRead) - { - throw new MemberAccessException($"Unable to read contents of property {attributedProperty.Name}"); - } - - return attributedProperty.GetValue(host); - } - - private static IEnumerable> ApplyOrderedConversion(IEnumerable values, string keyName, CultureInfo culture) - { - var counter = 0; - var enumerator = values.GetEnumerator(); - - while (enumerator.MoveNext()) - { - yield return enumerator.Current.ToKeyValuePair($"{keyName}[{counter++}]", culture); - } - - // dispose if possible - (enumerator as IDisposable)?.Dispose(); - } - - private static IEnumerable GetTargetProperties(this Type target) - { -#if NET6_0 && ANDROID - // android has an issue where nonpublic properties aren't returned from base classes (see https://github.com/dotnet/runtime/pull/77169) - var props = target.GetRuntimeProperties(); - var baseType = target.BaseType; - - while (baseType != null && baseType != typeof(ApiRequest)) - { - props = props.Concat(baseType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)); - baseType = baseType.BaseType; - } - - return props; -#else - return target.GetRuntimeProperties(); -#endif - } - - private static IEnumerable> ApplyConcatenation(IEnumerable values, string keyName, CultureInfo culture, string concatCharacter) - { - yield return new KeyValuePair(keyName, string.Join(concatCharacter, values.Cast().Select(x => x.AsString(culture)))); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static KeyValuePair ToKeyValuePair(this object value, string key, CultureInfo culture) - { - return new KeyValuePair(key, value.AsString(culture)); - } - } -} diff --git a/src/Utils/QueryUtils.cs b/src/Utils/QueryUtils.cs deleted file mode 100644 index ad8b2d8..0000000 --- a/src/Utils/QueryUtils.cs +++ /dev/null @@ -1,21 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System.Collections.Generic; -using System.Linq; - -namespace DragonFruit.Data.Utils -{ - internal static class QueryUtils - { - /// - /// Produces a query string from an of s - /// - public static string QueryStringFrom(IEnumerable> queries) - { - return !queries.Any() - ? string.Empty - : $"?{string.Join("&", queries.Select(kvp => $"{kvp.Key}={kvp.Value}"))}"; - } - } -} diff --git a/tests/ApiTest.cs b/tests/ApiTest.cs deleted file mode 100644 index 5cea132..0000000 --- a/tests/ApiTest.cs +++ /dev/null @@ -1,19 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using DragonFruit.Data.Serializers.Newtonsoft; -using DragonFruit.Data.Serializers.SystemJson; - -namespace DragonFruit.Data.Tests -{ - public abstract class ApiTest - { - protected static readonly ApiClient Client = new ApiClient(); - - static ApiTest() - { - ApiJsonSerializer.RegisterDefaults(); - ApiSystemTextJsonSerializer.RegisterDefaults(); - } - } -} diff --git a/tests/DragonFruit.Data.Tests.csproj b/tests/DragonFruit.Data.Tests.csproj deleted file mode 100644 index 8ea3947..0000000 --- a/tests/DragonFruit.Data.Tests.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 9 - false - net6.0 - - - - - - - - - - - - - - - diff --git a/tests/Header/HeaderLevelTests.cs b/tests/Header/HeaderLevelTests.cs deleted file mode 100644 index ade9bd6..0000000 --- a/tests/Header/HeaderLevelTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using System.Threading.Tasks; -using DragonFruit.Data.Extensions; -using DragonFruit.Data.Tests.Requests; -using Newtonsoft.Json.Linq; -using NUnit.Framework; - -namespace DragonFruit.Data.Tests.Header -{ - [TestFixture] - public class HeaderLevelTests : ApiTest - { - private const string HeaderName = "x-dfn-test"; - private const string GlobalHeaderName = "x-dfn-global"; - - /// - /// Test whether request-headers and default headers are sent together successfully - /// - [TestCase] - public async Task LevelSpecificHeaderTest() - { - var request = new EchoRequest(); - - var globalHeaderValue = Guid.NewGuid().ToString(); - var requestHeaderValue = Guid.NewGuid().ToString(); - - Client.Headers[GlobalHeaderName] = globalHeaderValue; - request.WithHeader(HeaderName, requestHeaderValue); - - var response = await Client.PerformAsync(request); - Assert.AreEqual(requestHeaderValue, (string)response["headers"]![HeaderName]); - Assert.AreEqual(globalHeaderValue, (string)response["headers"][GlobalHeaderName]); - } - - /// - /// Test whether two headers with the same key (one in request and one in global) override each other - /// with the request header taking priority - /// - [TestCase] - public async Task LevelOverrideHeaderTest() - { - var request = new EchoRequest(); - - var globalHeaderValue = Guid.NewGuid().ToString(); - var requestHeaderValue = Guid.NewGuid().ToString(); - - Client.Headers[GlobalHeaderName] = globalHeaderValue; - request.WithHeader(GlobalHeaderName, requestHeaderValue); - - var response = await Client.PerformAsync(request); - Assert.AreEqual(requestHeaderValue, (string)response["headers"]![GlobalHeaderName]); - } - } -} diff --git a/tests/Header/HeaderTests.cs b/tests/Header/HeaderTests.cs deleted file mode 100644 index 30fd2b2..0000000 --- a/tests/Header/HeaderTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using System.Threading.Tasks; -using DragonFruit.Data.Extensions; -using DragonFruit.Data.Tests.Requests; -using Newtonsoft.Json.Linq; -using NUnit.Framework; - -namespace DragonFruit.Data.Tests.Header -{ - [TestFixture] - public class HeaderTests : ApiTest - { - private const string HeaderName = "x-dfn-test"; - private static readonly Random Rng = new(); - - /// - /// Test whether client headers are sent and changed successfully - /// - [TestCase] - public async Task HeaderTest() - { - var headerValue = Rng.Next().ToString(); - var request = new EchoRequest(); - - Client.Headers[HeaderName] = headerValue; - var response = await Client.PerformAsync(request); - Assert.AreEqual(headerValue, (string)response["headers"][HeaderName]); - - headerValue = Rng.Next().ToString(); - Client.Headers[HeaderName] = headerValue; - - response = await Client.PerformAsync(request); - Assert.AreEqual(headerValue, (string)response["headers"][HeaderName]); - } - - /// - /// Test whether a header sent in a request is recieved successfully - /// - [TestCase] - public async Task PerRequestHeaderTest() - { - var headerValue = Rng.Next().ToString(); - - var request = new EchoRequest().WithHeader(HeaderName, headerValue); - var response = await Client.PerformAsync(request); - - Assert.AreEqual((string)response["headers"]![HeaderName], headerValue); - } - } -} diff --git a/tests/RequestDataCompilationTests.cs b/tests/RequestDataCompilationTests.cs deleted file mode 100644 index 8a64698..0000000 --- a/tests/RequestDataCompilationTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System.Collections.Generic; -using System.Linq; -using DragonFruit.Data.Parameters; -using DragonFruit.Data.Utils; -using NUnit.Framework; - -namespace DragonFruit.Data.Tests -{ - [TestFixture] - public class RequestDataCompilationTests - { - [Test] - public void TestQueries() - { - var query = new TestRequest().FullUrl.Split('?').Last().Split('&'); - - for (var i = 0; i < TestRequest.TestDataset.Length; i++) - { - var testString = TestRequest.TestDataset[i]; - Assert.IsTrue(query.Contains($"{TestRequest.QueryName}={testString}")); - Assert.IsTrue(query.Contains($"{TestRequest.QueryName}[]={testString}")); - Assert.IsTrue(query.Contains($"{TestRequest.QueryName}[{i}]={testString}")); - } - - Assert.IsTrue(query.Contains($"{TestRequest.QueryName}={string.Join(":", TestRequest.TestDataset)}")); - } - - [Test] - public void TestEnumHandling() - { - var request = new TestRequest(); - var query = request.FullUrl.Split('?').Last().Split('&'); - - Assert.IsTrue(query.Contains($"enum={nameof(EnumValues.Red)}")); - Assert.IsTrue(query.Contains($"enum={nameof(EnumValues.Blue).ToLower(CultureUtils.DefaultCulture)}")); - Assert.IsTrue(query.Contains($"enum={(int)EnumValues.Green}")); - } - - [Test] - public void TestAdditionalQueryHandling() - { - var request = new TestRequest(); - var query = request.FullUrl.Split('?').Last().Split('&'); - - Assert.IsTrue(query.Contains("a=x")); - } - } - - internal class TestRequest : ApiRequest - { - internal const string QueryName = "data"; - internal static readonly string[] TestDataset = { "a", "b", "c" }; - - public override string Path => "http://example.com"; - - protected override IEnumerable> AdditionalQueries => new[] - { - new KeyValuePair("a", "x") - }; - - [QueryParameter(QueryName, CollectionConversionMode.Recursive)] - public string[] RecursiveData { get; set; } = TestDataset; - - [QueryParameter(QueryName, CollectionConversionMode.Ordered)] - public string[] OrderedData { get; set; } = TestDataset; - - [QueryParameter(QueryName, CollectionConversionMode.Unordered)] - public string[] UnorderedData { get; set; } = TestDataset; - - [QueryParameter(QueryName, CollectionConversionMode.Concatenated, CollectionSeparator = ":")] - public string[] ConcatenatedData { get; set; } = TestDataset; - - [QueryParameter("enum", EnumHandlingMode.String)] - public EnumValues StringEnum => EnumValues.Red; - - [QueryParameter("enum", EnumHandlingMode.StringLower)] - public EnumValues SmallStringEnum => EnumValues.Blue; - - [QueryParameter("enum", EnumHandlingMode.Numeric)] - public EnumValues NumericEnum => EnumValues.Green; - } - - public enum EnumValues - { - Red, - Blue, - Green = 512 - } -} diff --git a/tests/Requests/EchoRequest.cs b/tests/Requests/EchoRequest.cs deleted file mode 100644 index f08dcbe..0000000 --- a/tests/Requests/EchoRequest.cs +++ /dev/null @@ -1,16 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -namespace DragonFruit.Data.Tests.Requests -{ - public class EchoRequest : ApiRequest - { - public override string Path => $"https://postman-echo.com/{Method.ToString().ToLowerInvariant()}"; - protected override Methods Method { get; } - - public EchoRequest(Methods method = Methods.Get) - { - Method = method; - } - } -} diff --git a/tests/Requests/FileRequestTests.cs b/tests/Requests/FileRequestTests.cs deleted file mode 100644 index 1b7f080..0000000 --- a/tests/Requests/FileRequestTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using System.IO; -using System.Threading.Tasks; -using DragonFruit.Data.Basic; -using NUnit.Framework; - -namespace DragonFruit.Data.Tests.Requests -{ - [TestFixture] - public class FileRequestTests : ApiTest - { - [TestCase("https://github.com/ppy/osu/archive/2020.1121.0.zip", 19018589)] - public async Task FileDownloadTest(string path, long expectedFileSize) - { - var request = new BasicApiFileRequest(path, Path.GetTempPath()); - - if (File.Exists(request.Destination)) - { - try - { - File.Delete(request.Destination); - } - catch - { - Assert.Inconclusive("Failed to remove file needed for test"); - } - } - - try - { - await Client.PerformAsync(request, (progress, total) => TestContext.Out.WriteLine($"Progress: {progress:n0}/{total:n0} ({Convert.ToSingle(progress) / Convert.ToSingle(total):F2}%)")); - - Assert.IsTrue(File.Exists(request.Destination)); - Assert.GreaterOrEqual(new FileInfo(request.Destination).Length, expectedFileSize); - } - finally - { - if (File.Exists(request.Destination)) - { - File.Delete(request.Destination); - } - } - } - } -} diff --git a/tests/Requests/RequestFilterTests.cs b/tests/Requests/RequestFilterTests.cs deleted file mode 100644 index 473e460..0000000 --- a/tests/Requests/RequestFilterTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using DragonFruit.Data.Requests; -using NUnit.Framework; - -namespace DragonFruit.Data.Tests.Requests -{ - [TestFixture] - public class RequestFilterTests : ApiTest - { - [Test] - public void TestFilteredRequests() - { - Assert.CatchAsync(() => Client.PerformAsync(new InheritedRequest())); - } - - internal class FilteredRequest : ApiRequest, IRequestExecutingCallback - { - public override string Path { get; } - - void IRequestExecutingCallback.OnRequestExecuting(ApiClient client) - { - throw new ArgumentException(); - } - } - - internal class InheritedRequest : FilteredRequest - { - // this should have the exception filter applied to it as well - } - } -} diff --git a/tests/Requests/RequestTests.cs b/tests/Requests/RequestTests.cs deleted file mode 100644 index 216466d..0000000 --- a/tests/Requests/RequestTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using DragonFruit.Data.Basic; -using DragonFruit.Data.Parameters; -using NUnit.Framework; - -namespace DragonFruit.Data.Tests.Requests -{ - [TestFixture] - public class RequestTests : ApiTest - { - [TestCase(Methods.Get)] - [TestCase(Methods.Post)] - [TestCase(Methods.Head)] - [TestCase(Methods.Patch)] - [TestCase(Methods.Delete)] - public async Task TestMethodRequest(Methods method) - { - var request = new EchoRequest(method); - using var result = await Client.PerformAsync(request); - - Assert.IsTrue(result.IsSuccessStatusCode); - } - - [Test] - public async Task TestBasicRequest() - { - var request = new BasicApiRequest("https://api.steampowered.com/ISteamNews/GetNewsForApp/v0002/") - .WithQuery("appid", "440") - .WithQuery("count", "3") - .WithQuery("maxlength", "300") - .WithQuery("format", "json"); - - using var result = await Client.PerformAsync(request); - - Assert.IsTrue(result.IsSuccessStatusCode); - } - - [Test] - public async Task TestHttp2Request() - { - var request = new HttpRequestMessage(HttpMethod.Get, "https://google.com") { Version = HttpVersion.Version20 }; - using var result = await Client.PerformAsync(request); - - Assert.IsTrue(result.IsSuccessStatusCode); - Assert.AreEqual(request.Version, result.Version); - } - - [Test] - public void TestConcatEnumerable() - { - var req = new EnumerableTest(Enumerable.Range(1, 5)).FullUrl; - - Assert.True(req.Contains("sdata=1,2,3")); - Assert.True(req.Contains("numbers=1,2,3")); - } - - private class EnumerableTest : ApiRequest - { - public override string Path => "https://example.com"; - - public EnumerableTest(IEnumerable data) - { - Data = data; - } - - [QueryParameter("numbers", CollectionConversionMode.Concatenated)] - public IEnumerable Data { get; set; } - - [QueryParameter("sdata", CollectionConversionMode.Concatenated)] - public IEnumerable StringData => Data.Select(x => x.ToString()); - } - } -} diff --git a/tests/Requests/StreamTests.cs b/tests/Requests/StreamTests.cs deleted file mode 100644 index e22dbc6..0000000 --- a/tests/Requests/StreamTests.cs +++ /dev/null @@ -1,25 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using System.IO; -using System.Threading.Tasks; -using NUnit.Framework; - -namespace DragonFruit.Data.Tests.Requests -{ - [TestFixture] - public class StreamTests : ApiTest - { - [Test] - public async Task TestStreamRequests() - { - var networkStream = await Client.PerformAsync("https://google.com"); - var fileStream = await Client.PerformAsync("https://google.com"); - - // make sure stream lengths are _almost_ the same length - var difference = Math.Abs(networkStream.Length - fileStream.Length); - Assert.IsTrue(difference <= 10); - } - } -} diff --git a/tests/Serializers/SerializerResolverTests.cs b/tests/Serializers/SerializerResolverTests.cs deleted file mode 100644 index 0323af8..0000000 --- a/tests/Serializers/SerializerResolverTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System.IO; -using System.Net.Http; -using DragonFruit.Data.Serializers; -using DragonFruit.Data.Serializers.Newtonsoft; -using NUnit.Framework; - -namespace DragonFruit.Data.Tests.Serializers -{ - [TestFixture] - public class SerializerResolverTests : ApiTest - { - [SetUp] - public void Setup() - { - // testobject will use xml - SerializerResolver.Register(); - - // anothertestobject will use the dummyserializer - SerializerResolver.Register(); - - SerializerResolver.Register(typeof(TestContainer<>)); - } - - [Test] - public void TestResolution() - { - Assert.AreEqual(typeof(ApiXmlSerializer), Client.Serializer.Resolve(DataDirection.In).GetType()); - Assert.AreEqual(typeof(ApiJsonSerializer), Client.Serializer.Resolve(DataDirection.Out).GetType()); - - Assert.AreEqual(typeof(DummySerializer), Client.Serializer.Resolve>(DataDirection.In).GetType()); - } - - [Test] - public void TestRemovalResolution() - { - Assert.AreEqual(typeof(DummySerializer), Client.Serializer.Resolve(DataDirection.In).GetType()); - - SerializerResolver.Unregister(); - Assert.AreEqual(typeof(ApiJsonSerializer), Client.Serializer.Resolve(DataDirection.In).GetType()); - } - - [Test] - public void TestConfigure() - { - var a = string.Empty; - Client.Serializer.Configure(o => a = o.ContentType); - Assert.AreEqual("nothing", a); - } - - [TearDown] - public void Cleanup() - { - SerializerResolver.Unregister(); - SerializerResolver.Unregister(); - } - } - - public class TestObject - { - } - - public class AnotherTestObject - { - } - - public class YetAnotherTestObject - { - } - - public class TestContainer - { - public T Item { get; set; } - } - - public class DummySerializer : ApiSerializer - { - public override string ContentType => "nothing"; - public override bool IsGeneric => true; - - public override HttpContent Serialize(object input) => throw new System.NotImplementedException(); - - public override T Deserialize(Stream input) where T : class => throw new System.NotImplementedException(); - } -} diff --git a/tests/Serializers/SerializerTests.cs b/tests/Serializers/SerializerTests.cs deleted file mode 100644 index ac73109..0000000 --- a/tests/Serializers/SerializerTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System.Text.Json; -using System.Threading.Tasks; -using System.Xml; -using DragonFruit.Data.Basic; -using Newtonsoft.Json.Linq; -using NUnit.Framework; - -namespace DragonFruit.Data.Tests.Serializers -{ - public class SerializerTests : ApiTest - { - private const int ArticleCount = 5; - - [Test] - public async Task TestNewtonsoftSerializer() - { - var response = await Client.PerformAsync(CreateRequest()); - var entries = response["appnews"]["newsitems"] as JArray; - Assert.AreEqual(entries.Count, ArticleCount); - } - - [Test] - public async Task TestSystemTextJsonSerializer() - { - var response = await Client.PerformAsync(CreateRequest()); - var articleCount = response.RootElement.GetProperty("appnews").GetProperty("newsitems").GetArrayLength(); - - Assert.AreEqual(articleCount, ArticleCount); - } - - [Test] - public async Task TestXmlSerializer() - { - var response = await Client.PerformAsync(CreateRequest("xml")); - var articleNodes = response.SelectNodes("/appnews/newsitems/newsitem"); - - Assert.AreEqual(articleNodes?.Count, ArticleCount); - } - - private ApiRequest CreateRequest(string format = "json") => new BasicApiRequest("https://api.steampowered.com/ISteamNews/GetNewsForApp/v0002/") - .WithQuery("appid", "440") - .WithQuery("count", ArticleCount) - .WithQuery("maxlength", "300") - .WithQuery("format", format); - } -} From 66bd304d058ca602cc7122f2cd6402596558e93d Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 1 Dec 2023 09:10:25 +0000 Subject: [PATCH 002/151] add apirequests for new project --- res/DragonFruit.Data.Nuget.props | 4 +- src/ApiRequest.cs | 206 +----------------- src/DragonFruit.Data.csproj | 13 +- src/Requests/Converters/EnumOptions.cs | 13 ++ src/Requests/ParameterType.cs | 15 ++ src/Requests/RequestBodyAttribute.cs | 20 ++ src/Requests/RequestCallbackAttribute.cs | 20 ++ src/Requests/RequestParameterAttribute.cs | 30 +++ .../RequestParameterAttributeAliases.cs | 21 ++ 9 files changed, 135 insertions(+), 207 deletions(-) create mode 100644 src/Requests/Converters/EnumOptions.cs create mode 100644 src/Requests/ParameterType.cs create mode 100644 src/Requests/RequestBodyAttribute.cs create mode 100644 src/Requests/RequestCallbackAttribute.cs create mode 100644 src/Requests/RequestParameterAttribute.cs create mode 100644 src/Requests/RequestParameterAttributeAliases.cs diff --git a/res/DragonFruit.Data.Nuget.props b/res/DragonFruit.Data.Nuget.props index 368a2d8..304f7af 100644 --- a/res/DragonFruit.Data.Nuget.props +++ b/res/DragonFruit.Data.Nuget.props @@ -13,8 +13,8 @@ MIT Copyright 2023 (C) DragonFruit Network file, api, web, io, framework, dragonfruit, common - https://github.com/dragonfruitnetwork/dragonfruit-common - https://github.com/dragonfruitnetwork/dragonfruit-common + https://github.com/dragonfruitnetwork/rest-client + https://github.com/dragonfruitnetwork/rest-client diff --git a/src/ApiRequest.cs b/src/ApiRequest.cs index 1e0ccf5..450d5ca 100644 --- a/src/ApiRequest.cs +++ b/src/ApiRequest.cs @@ -1,210 +1,16 @@ -// DragonFruit.Data Copyright DragonFruit Network +// DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; using System.Net.Http; -using System.Net.Http.Headers; -using DragonFruit.Data.Exceptions; -using DragonFruit.Data.Parameters; -using DragonFruit.Data.Serializers; -using DragonFruit.Data.Utils; namespace DragonFruit.Data { public abstract class ApiRequest { - private List> _headers; - - /// - /// The path (including host, protocol and non-standard port) to the web resource - /// - public abstract string Path { get; } - - /// - /// The method to use/request verb - /// - protected virtual Methods Method => Methods.Get; - - /// - /// The to use (if there is a body to be sent) - /// - protected virtual BodyType BodyType { get; } - - /// - /// Whether an auth header is required. Throws on check failure (before sending request) - /// - protected internal virtual bool RequireAuth => false; - - /// - /// Custom Headers to send with this request. Overrides any custom header set in the with the same name. - /// - /// - /// Headers to be set in all requests should be set at -level, using the Dictionary. - /// - public List> Headers => _headers ??= new List>(); - - /// - /// Internal check for whether the custom header collection () has been initialised - /// - internal bool CustomHeaderCollectionCreated => _headers != null; - - /// - /// The fully compiled url - /// - public string FullUrl => UrlCompiler; - - /// - /// Getter for fully compiled url (internally visible) - /// - internal virtual string UrlCompiler => Path + QueryString; - - /// - /// Overridable property for configuring a custom body for this request - /// - /// Only used when the is equal to - /// - /// - protected virtual HttpContent BodyContent { get; } - - /// - /// Overridable culture for serialising requests. - /// Defaults to - /// - protected virtual CultureInfo RequestCulture => CultureUtils.DefaultCulture; - - /// - /// Query string generated from all filled -attributed properties - /// - internal string QueryString - { - get - { - var queries = ParameterUtils.GetParameter(this, RequestCulture); - - if (AdditionalQueries != null) - { - queries = queries.Concat(AdditionalQueries); - } - - return QueryUtils.QueryStringFrom(queries); - } - } - - /// - /// Additional abstract collection of queries provided as an of - /// - protected virtual IEnumerable> AdditionalQueries { get; } - - /// - /// Create a for this , which can then be modified manually or overriden by - /// - public HttpRequestMessage Build(ApiClient client) - { - return Build(client.Serializer); - } - - /// - /// Create a for this , which can then be modified manually or overriden by - /// - /// - /// This validates the and properties, throwing a if it's unsatisfied with the constraints - /// - public HttpRequestMessage Build(SerializerResolver serializer) - { - if (!Path.StartsWith("http") && !Path.StartsWith("//")) - { - throw new HttpRequestException("The request path is invalid (it must start with http or https)"); - } - - var request = new HttpRequestMessage - { - RequestUri = new Uri(FullUrl) - }; - - // generic setup - switch (Method) - { - case Methods.Get: - request.Method = HttpMethod.Get; - break; - - case Methods.Post: - request.Method = HttpMethod.Post; - request.Content = GetContent(serializer); - break; - - case Methods.Put: - request.Method = HttpMethod.Put; - request.Content = GetContent(serializer); - break; - - case Methods.Patch: -#if NETSTANDARD2_0 - // .NET Standard 2.0 doesn't have a PATCH method... - request.Method = new HttpMethod("PATCH"); -#else - request.Method = HttpMethod.Patch; -#endif - request.Content = GetContent(serializer); - break; - - case Methods.Delete: - request.Method = HttpMethod.Delete; - request.Content = GetContent(serializer); - break; - - case Methods.Head: - request.Method = HttpMethod.Head; - break; - - case Methods.Trace: - request.Method = HttpMethod.Trace; - break; - - default: - throw new NotImplementedException(); - } - - if (CustomHeaderCollectionCreated) - { - foreach (var header in Headers) - { - request.Headers.Add(header.Key, header.Value); - } - } - - if (!request.Headers.Contains("Accept")) - { - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(serializer.Resolve(GetType(), DataDirection.In).ContentType)); - } - - return request; - } - - private HttpContent GetContent(SerializerResolver serializer) - { - switch (BodyType) - { - case BodyType.Encoded: - return new FormUrlEncodedContent(ParameterUtils.GetParameter(this, RequestCulture)); - - case BodyType.Serialized: - return serializer.Resolve(GetType(), DataDirection.Out).Serialize(this); - - case BodyType.SerializedProperty: - var body = serializer.Resolve(GetType(), DataDirection.Out).Serialize(ParameterUtils.GetSingleParameterObject(this)); - return body; - - case BodyType.Custom: - return BodyContent; - - default: - //todo custom exception - there should have been a datatype specified - throw new ArgumentOutOfRangeException(); - } - } + public abstract string RequestPath { get; } + + public virtual HttpMethod RequestMethod => HttpMethod.Get; + + } } diff --git a/src/DragonFruit.Data.csproj b/src/DragonFruit.Data.csproj index 97ddb22..abec3ed 100644 --- a/src/DragonFruit.Data.csproj +++ b/src/DragonFruit.Data.csproj @@ -1,12 +1,13 @@ - + - + + netstandard2.0;net6.0 + 8 + - $(TargetFrameworks);net6.0-android - DragonFruit.Data - A web requests framework for .NET that powers the DragonFruit APIs + A http/rest request framework for .NET that powers the DragonFruit APIs @@ -15,4 +16,6 @@ + + diff --git a/src/Requests/Converters/EnumOptions.cs b/src/Requests/Converters/EnumOptions.cs new file mode 100644 index 0000000..3b2e693 --- /dev/null +++ b/src/Requests/Converters/EnumOptions.cs @@ -0,0 +1,13 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +namespace DragonFruit.Data.Requests.Converters +{ + public enum EnumOptions + { + None = 0, + Numeric = 1, + StringLower = 2, + StringUpper = 3, + } +} diff --git a/src/Requests/ParameterType.cs b/src/Requests/ParameterType.cs new file mode 100644 index 0000000..38c254b --- /dev/null +++ b/src/Requests/ParameterType.cs @@ -0,0 +1,15 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +namespace DragonFruit.Data.Requests +{ + /// + /// Describes the destination location of the parameter being decorated + /// + public enum ParameterType + { + Query = 1, + Form = 2, + Header = 3 + } +} diff --git a/src/Requests/RequestBodyAttribute.cs b/src/Requests/RequestBodyAttribute.cs new file mode 100644 index 0000000..819b44e --- /dev/null +++ b/src/Requests/RequestBodyAttribute.cs @@ -0,0 +1,20 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System; +using System.Net.Http; + +namespace DragonFruit.Data.Requests +{ + /// + /// Marks the property or method as being the body of the request. + /// If the return type does not derive from , the body will be serialized using the client + /// + /// + /// There must not be any more than one property/method that is decorated with this attribute. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Method)] + public class RequestBodyAttribute : Attribute + { + } +} diff --git a/src/Requests/RequestCallbackAttribute.cs b/src/Requests/RequestCallbackAttribute.cs new file mode 100644 index 0000000..dde0a37 --- /dev/null +++ b/src/Requests/RequestCallbackAttribute.cs @@ -0,0 +1,20 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System; + +namespace DragonFruit.Data.Requests +{ + /// + /// Marks the annotated method as a callback before returning the final request object + /// + [AttributeUsage(AttributeTargets.Method)] + public class RequestCallbackAttribute : Attribute + { + /// + /// The order to invoke the callback. + /// Lower value results in earlier execution. + /// + public int Order { get; } = 0; + } +} diff --git a/src/Requests/RequestParameterAttribute.cs b/src/Requests/RequestParameterAttribute.cs new file mode 100644 index 0000000..453ecbd --- /dev/null +++ b/src/Requests/RequestParameterAttribute.cs @@ -0,0 +1,30 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System; + +namespace DragonFruit.Data.Requests +{ + /// + /// Marks a property or method to be included as a parameter in the HTTP request + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] + public class RequestParameterAttribute : Attribute + { + public RequestParameterAttribute(ParameterType parameterType, string name) + { + ParameterType = parameterType; + Name = name; + } + + /// + /// The type/location the value should be stored under + /// + public ParameterType ParameterType { get; } + + /// + /// The name the value should be written under + /// + public string Name { get; } + } +} diff --git a/src/Requests/RequestParameterAttributeAliases.cs b/src/Requests/RequestParameterAttributeAliases.cs new file mode 100644 index 0000000..5462888 --- /dev/null +++ b/src/Requests/RequestParameterAttributeAliases.cs @@ -0,0 +1,21 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +namespace DragonFruit.Data.Requests +{ + public class QueryParameterAttribute : RequestParameterAttribute + { + public QueryParameterAttribute(string name) + : base(ParameterType.Query, name) + { + } + } + + public class FormParameterAttribute : RequestParameterAttribute + { + public FormParameterAttribute(string name) + : base(ParameterType.Form, name) + { + } + } +} From 1a26a9b6d1d2aa76b4dcc9403a3da93f3b248b89 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 1 Dec 2023 10:33:45 +0000 Subject: [PATCH 003/151] add request builder interface (sourcegen) --- src/Requests/IRequestBuilder.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/Requests/IRequestBuilder.cs diff --git a/src/Requests/IRequestBuilder.cs b/src/Requests/IRequestBuilder.cs new file mode 100644 index 0000000..437125a --- /dev/null +++ b/src/Requests/IRequestBuilder.cs @@ -0,0 +1,12 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System.Net.Http; + +namespace DragonFruit.Data.Requests +{ + public interface IRequestBuilder + { + HttpRequestMessage BuildRequest(); + } +} From c9a474471c76f49cc8055f648e209e4453cf1f3c Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 1 Dec 2023 10:34:21 +0000 Subject: [PATCH 004/151] add serializer base, xml/json modes --- src/ApiSerializer.cs | 65 +++++++++++++++++++ src/DragonFruit.Data.csproj | 1 + .../Converters/CollectionConverter.cs | 56 ++++++++++++++++ src/Requests/Converters/CollectionOptions.cs | 28 ++++++++ src/Requests/Converters/EnumConverter.cs | 28 ++++++++ src/Serializers/ApiJsonSerializer.cs | 52 +++++++++++++++ src/Serializers/ApiXmlSerializer.cs | 49 ++++++++++++++ src/Serializers/IAsyncSerializer.cs | 16 +++++ 8 files changed, 295 insertions(+) create mode 100644 src/ApiSerializer.cs create mode 100644 src/Requests/Converters/CollectionConverter.cs create mode 100644 src/Requests/Converters/CollectionOptions.cs create mode 100644 src/Requests/Converters/EnumConverter.cs create mode 100644 src/Serializers/ApiJsonSerializer.cs create mode 100644 src/Serializers/ApiXmlSerializer.cs create mode 100644 src/Serializers/IAsyncSerializer.cs diff --git a/src/ApiSerializer.cs b/src/ApiSerializer.cs new file mode 100644 index 0000000..7412aa1 --- /dev/null +++ b/src/ApiSerializer.cs @@ -0,0 +1,65 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; + +namespace DragonFruit.Data +{ + public abstract class ApiSerializer + { + private Encoding _encoding; + + /// + /// The Content-Type/Accept header value + /// + public abstract string ContentType { get; } + + /// + /// Whether this is generic (meaning any class can be serialized to/from). + /// + /// + /// Setting this to false will throw an exception if the serializer is set as a default in a client. + /// + public virtual bool IsGeneric => true; + + /// + /// Gets or sets the encoding the uses + /// + public virtual Encoding Encoding + { + get => _encoding ??= new UTF8Encoding(false); + set => _encoding = value; + } + + /// + /// Serializes the provided input to a object + /// + public abstract HttpContent Serialize(T input) where T : class; + + /// + /// Deserializes the provided to the specified type + /// + public abstract T Deserialize(Stream input) where T : class; + + /// + /// Converts a serialized in the to the equivalent + /// + /// The stream to convert + protected HttpContent GetHttpContent(Stream stream) + { + stream.Seek(0, SeekOrigin.Begin); + var content = new StreamContent(stream); + + content.Headers.ContentLength = stream.Length; + content.Headers.ContentType = new MediaTypeHeaderValue(ContentType) + { + CharSet = Encoding.HeaderName + }; + + return content; + } + } +} diff --git a/src/DragonFruit.Data.csproj b/src/DragonFruit.Data.csproj index abec3ed..398a2e5 100644 --- a/src/DragonFruit.Data.csproj +++ b/src/DragonFruit.Data.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Requests/Converters/CollectionConverter.cs b/src/Requests/Converters/CollectionConverter.cs new file mode 100644 index 0000000..3feb8e3 --- /dev/null +++ b/src/Requests/Converters/CollectionConverter.cs @@ -0,0 +1,56 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System.Collections.Generic; +using System.Text; + +namespace DragonFruit.Data.Requests.Converters +{ + public class CollectionConverter + { + // todo move to sourcegen + public static void WriteCollectionValue(IEnumerable collection, string keyPrefix, CollectionOptions mode, StringBuilder destination) + { + switch (mode) + { + case CollectionOptions.Recursive: + { + foreach (var item in collection) + { + destination.Append($"{keyPrefix}={item}&"); + } + + break; + } + + case CollectionOptions.Unordered: + { + foreach (var item in collection) + { + destination.Append($"{keyPrefix}[]={item}&"); + } + + break; + } + + case CollectionOptions.Indexed: + { + var index = 0; + + foreach (var item in collection) + { + destination.Append($"{keyPrefix}[{index++}]={item}&"); + } + + break; + } + + case CollectionOptions.Concatenated: + { + destination.Append(string.Join(",", collection)); + break; + } + } + } + } +} diff --git a/src/Requests/Converters/CollectionOptions.cs b/src/Requests/Converters/CollectionOptions.cs new file mode 100644 index 0000000..89f465f --- /dev/null +++ b/src/Requests/Converters/CollectionOptions.cs @@ -0,0 +1,28 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +namespace DragonFruit.Data.Requests.Converters +{ + public enum CollectionOptions + { + /// + /// The query name is repeated and a new element created for each (a=1&a=2&a=3) + /// + Recursive, + + /// + /// The query name has indexer symbols appended with no order (a[]=1&a[]=2&a[]=3) + /// + Unordered, + + /// + /// The query name has indexer symbols appended explicit order inserted (a[0]=1&a[1]=2&a[2]=3) + /// + Indexed, + + /// + /// The query is concatenated with a string and merged with one key (a=1,2,3) + /// + Concatenated + } +} diff --git a/src/Requests/Converters/EnumConverter.cs b/src/Requests/Converters/EnumConverter.cs new file mode 100644 index 0000000..b6583be --- /dev/null +++ b/src/Requests/Converters/EnumConverter.cs @@ -0,0 +1,28 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System.Text; + +namespace DragonFruit.Data.Requests.Converters +{ + public static class EnumConverter + { + public static void WriteEnumValue(object enumValue, EnumOptions mode, StringBuilder target) + { + switch (mode) + { + case EnumOptions.Numeric: + target.Append((int)enumValue); + break; + + case EnumOptions.StringLower: + target.Append(enumValue.ToString().ToLowerInvariant().Replace(" ", string.Empty)); + break; + + case EnumOptions.StringUpper: + target.Append(enumValue.ToString().ToUpperInvariant().Replace(" ", string.Empty)); + break; + } + } + } +} diff --git a/src/Serializers/ApiJsonSerializer.cs b/src/Serializers/ApiJsonSerializer.cs new file mode 100644 index 0000000..f4ab386 --- /dev/null +++ b/src/Serializers/ApiJsonSerializer.cs @@ -0,0 +1,52 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace DragonFruit.Data.Serializers +{ + public class ApiJsonSerializer : ApiSerializer, IAsyncSerializer + { + public override string ContentType => "application/json"; + + public override Encoding Encoding + { + get => Encoding.UTF8; + set => throw new InvalidOperationException("System.Text.Json Serializer does not support custom encodings"); + } + + /// + /// Gets or sets the current used. + /// + public JsonSerializerOptions SerializerOptions { get; set; } = JsonSerializerOptions.Default; + + public override HttpContent Serialize(T input) + { + var utf8Bytes = JsonSerializer.SerializeToUtf8Bytes(input, typeof(T)); + var httpContent = new ByteArrayContent(utf8Bytes); + + httpContent.Headers.ContentType = new MediaTypeHeaderValue(ContentType) + { + CharSet = Encoding.HeaderName + }; + + return httpContent; + } + + public override T Deserialize(Stream input) + { + return JsonSerializer.Deserialize(input, SerializerOptions); + } + + public ValueTask DeserializeAsync(Stream input) where T : class + { + return JsonSerializer.DeserializeAsync(input, SerializerOptions); + } + } +} diff --git a/src/Serializers/ApiXmlSerializer.cs b/src/Serializers/ApiXmlSerializer.cs new file mode 100644 index 0000000..115177f --- /dev/null +++ b/src/Serializers/ApiXmlSerializer.cs @@ -0,0 +1,49 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Xml; +using System.Xml.Serialization; + +namespace DragonFruit.Data.Serializers +{ + public class ApiXmlSerializer : ApiSerializer + { + public override string ContentType => "application/xml"; + + public override HttpContent Serialize(T input) + { + var stream = new MemoryStream(4096); + + using (var writer = new StreamWriter(stream, Encoding, 4096, true)) + { + new XmlSerializer(input.GetType()).Serialize(writer, input); + } + + var xmlContent = new ByteArrayContent(stream.GetBuffer(), 0, (int)stream.Length); + + xmlContent.Headers.ContentType = new MediaTypeHeaderValue(ContentType) + { + CharSet = Encoding.HeaderName + }; + + return xmlContent; + } + + public override T Deserialize(Stream input) where T : class + { + var serializer = new XmlSerializer(typeof(T)); + using var reader = XmlReader.Create(input); + + if (serializer.CanDeserialize(reader)) + { + return (T)serializer.Deserialize(reader); + } + + throw new InvalidOperationException($"Unable to deserialize stream to type {typeof(T).Name}"); + } + } +} diff --git a/src/Serializers/IAsyncSerializer.cs b/src/Serializers/IAsyncSerializer.cs new file mode 100644 index 0000000..1111c1f --- /dev/null +++ b/src/Serializers/IAsyncSerializer.cs @@ -0,0 +1,16 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System.IO; +using System.Threading.Tasks; + +namespace DragonFruit.Data.Serializers +{ + public interface IAsyncSerializer + { + /// + /// Asynchronously deserialize the provided into an object of the specified type + /// + ValueTask DeserializeAsync(Stream input) where T : class; + } +} From f83cfd18b6fe3a9b90c98c9a62a1ee5e422c26f2 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 1 Dec 2023 12:48:40 +0000 Subject: [PATCH 005/151] add apirequest analyzer --- .../ApiRequestAnalyzerTests.cs | 35 ++++++++++ .../DragonFruit.Data.Roslyn.Tests.csproj | 25 +++++++ .../ApiRequestClassAnalyzer.cs | 70 +++++++++++++++++++ .../ApiRequestClassFixProvider.cs | 46 ++++++++++++ .../DragonFruit.Data.Roslyn.csproj | 36 ++++++++++ DragonFruit.Data.sln | 12 ++++ 6 files changed, 224 insertions(+) create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestClassAnalyzer.cs create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestClassFixProvider.cs create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs new file mode 100644 index 0000000..b808ad8 --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using Xunit; +using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.XUnit.CodeFixVerifier; + +namespace DragonFruit.Data.Roslyn.Tests; + +public class ApiRequestAnalyzerTests +{ + [Fact] + public async Task TestNonPartialClassDetectionAndFix() + { + const string text = @" +namespace DragonFruit.Data; + +public class ApiRequest { } +public class TestRequest : ApiRequest +{ + public string RequestPath => ""https://google.com""; +} +"; + + const string newText = @" +namespace DragonFruit.Data; + +public class ApiRequest { } +public partial class TestRequest : ApiRequest +{ + public string RequestPath => ""https://google.com""; +} +"; + + var expectedDiagnostic = Verifier.Diagnostic().WithSpan(5, 14, 5, 25).WithArguments("TestRequest"); + await Verifier.VerifyCodeFixAsync(text, expectedDiagnostic, newText).ConfigureAwait(false); + } +} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj new file mode 100644 index 0000000..b56cb22 --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestClassAnalyzer.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestClassAnalyzer.cs new file mode 100644 index 0000000..709d663 --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestClassAnalyzer.cs @@ -0,0 +1,70 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace DragonFruit.Data.Roslyn +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class ApiRequestClassAnalyzer : DiagnosticAnalyzer + { + public const string DiagnosticId = "DA0001"; + + private static readonly DiagnosticDescriptor Rule = new(DiagnosticId, "Partial class expected", "Class '{0}' should be marked as partial", "Design", DiagnosticSeverity.Warning, isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(AnalyzeClassDecl, SyntaxKind.ClassDeclaration); + } + + private void AnalyzeClassDecl(SyntaxNodeAnalysisContext context) + { + if (context.Node is not ClassDeclarationSyntax classDeclarationNode) + { + return; + } + + // check if class inherits from apiRequestType using type checking + var apiRequestType = context.Compilation.GetTypeByMetadataName(typeof(ApiRequest).FullName); + var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationNode); + + if (apiRequestType == null || classSymbol == null || !InheritsFrom(classSymbol, apiRequestType)) + { + return; + } + + // check if class has partial keyword + if (classDeclarationNode.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) + { + return; + } + + var diagnostic = Diagnostic.Create(Rule, classDeclarationNode.Identifier.GetLocation(), classDeclarationNode.Identifier.Text); + context.ReportDiagnostic(diagnostic); + } + + private static bool InheritsFrom(INamedTypeSymbol symbol, ITypeSymbol type) + { + var baseType = symbol.BaseType; + + while (baseType != null) + { + if (type.Equals(baseType, SymbolEqualityComparer.Default)) + { + return true; + } + + baseType = baseType.BaseType; + } + + return false; + } + } +} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestClassFixProvider.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestClassFixProvider.cs new file mode 100644 index 0000000..2b38814 --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestClassFixProvider.cs @@ -0,0 +1,46 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SyntaxFactory = Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using SyntaxKind = Microsoft.CodeAnalysis.CSharp.SyntaxKind; + +namespace DragonFruit.Data.Roslyn; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ApiRequestClassFixProvider)), Shared] +public class ApiRequestClassFixProvider : CodeFixProvider +{ + public sealed override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(ApiRequestClassAnalyzer.DiagnosticId); + + public override FixAllProvider GetFixAllProvider() => null; + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics.Single(); + + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var diagnosticNode = root?.FindNode(diagnostic.Location.SourceSpan); + + if (diagnosticNode is not ClassDeclarationSyntax declaration) + { + return; + } + + context.RegisterCodeFix(CodeAction.Create(title: "Make class partial", createChangedSolution: c => MakeClassPartial(context.Document, declaration, c)), diagnostic); + } + + private async Task MakeClassPartial(Document document, MemberDeclarationSyntax classDeclaration, CancellationToken cancellationToken) + { + var newClassDeclaration = classDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword)); + + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newRoot = root.ReplaceNode(classDeclaration, newClassDeclaration); + + return document.WithSyntaxRoot(newRoot).Project.Solution; + } +} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj new file mode 100644 index 0000000..6ba51ca --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj @@ -0,0 +1,36 @@ + + + + netstandard2.0 + latest + false + + true + true + + DragonFruit.Data.Roslyn + DragonFruit.Data.Roslyn + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + diff --git a/DragonFruit.Data.sln b/DragonFruit.Data.sln index b6c3fc1..e471fcf 100644 --- a/DragonFruit.Data.sln +++ b/DragonFruit.Data.sln @@ -15,6 +15,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data", "src\Dra EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Serializers", "Serializers", "{5A8982CD-EEF9-4B9F-AF74-C8D45241E137}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Roslyn", "DragonFruit.Data.Roslyn\DragonFruit.Data.Roslyn\DragonFruit.Data.Roslyn.csproj", "{B840D80F-BD03-46A6-A18B-524D099F9F64}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Roslyn.Tests", "DragonFruit.Data.Roslyn\DragonFruit.Data.Roslyn.Tests\DragonFruit.Data.Roslyn.Tests.csproj", "{DE9CFE49-BB70-47CC-BAF2-0243150829E5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -25,6 +29,14 @@ Global {0A0921D2-637F-4245-B993-054983A97EAC}.Debug|Any CPU.Build.0 = Debug|Any CPU {0A0921D2-637F-4245-B993-054983A97EAC}.Release|Any CPU.ActiveCfg = Release|Any CPU {0A0921D2-637F-4245-B993-054983A97EAC}.Release|Any CPU.Build.0 = Release|Any CPU + {B840D80F-BD03-46A6-A18B-524D099F9F64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B840D80F-BD03-46A6-A18B-524D099F9F64}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B840D80F-BD03-46A6-A18B-524D099F9F64}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B840D80F-BD03-46A6-A18B-524D099F9F64}.Release|Any CPU.Build.0 = Release|Any CPU + {DE9CFE49-BB70-47CC-BAF2-0243150829E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE9CFE49-BB70-47CC-BAF2-0243150829E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE9CFE49-BB70-47CC-BAF2-0243150829E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE9CFE49-BB70-47CC-BAF2-0243150829E5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From b92a2e9b362414460fbcfdbb59ee0207f069a88c Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 1 Dec 2023 13:09:46 +0000 Subject: [PATCH 006/151] reformat apirequest --- src/ApiRequest.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ApiRequest.cs b/src/ApiRequest.cs index 450d5ca..e97d1fb 100644 --- a/src/ApiRequest.cs +++ b/src/ApiRequest.cs @@ -8,9 +8,7 @@ namespace DragonFruit.Data public abstract class ApiRequest { public abstract string RequestPath { get; } - + public virtual HttpMethod RequestMethod => HttpMethod.Get; - - } } From 885367a547d7d47df5d20368ee264c9f8f916777 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 1 Dec 2023 16:43:08 +0000 Subject: [PATCH 007/151] update analyzers and add base for sourcegens --- .../ApiRequestAnalyzerTests.cs | 2 +- .../ApiRequestClassAnalyzer.cs | 4 +- .../Analyzers/ApiRequestClassFixProvider.cs | 47 +++++++++++ .../ApiRequestClassFixProvider.cs | 46 ----------- .../ApiRequestSourceGenerator.cs | 81 +++++++++++++++++++ .../DragonFruit.Data.Roslyn.csproj | 21 ++--- 6 files changed, 138 insertions(+), 63 deletions(-) rename DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/{ => Analyzers}/ApiRequestClassAnalyzer.cs (96%) create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassFixProvider.cs delete mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestClassFixProvider.cs create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs index b808ad8..33af453 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; using Xunit; -using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.XUnit.CodeFixVerifier; +using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.XUnit.CodeFixVerifier; namespace DragonFruit.Data.Roslyn.Tests; diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestClassAnalyzer.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassAnalyzer.cs similarity index 96% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestClassAnalyzer.cs rename to DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassAnalyzer.cs index 709d663..7c1455c 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestClassAnalyzer.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassAnalyzer.cs @@ -5,7 +5,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace DragonFruit.Data.Roslyn +namespace DragonFruit.Data.Roslyn.Analyzers { [DiagnosticAnalyzer(LanguageNames.CSharp)] public class ApiRequestClassAnalyzer : DiagnosticAnalyzer @@ -32,7 +32,7 @@ private void AnalyzeClassDecl(SyntaxNodeAnalysisContext context) } // check if class inherits from apiRequestType using type checking - var apiRequestType = context.Compilation.GetTypeByMetadataName(typeof(ApiRequest).FullName); + var apiRequestType = context.Compilation.GetTypeByMetadataName("DragonFruit.Data.ApiRequest"); var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationNode); if (apiRequestType == null || classSymbol == null || !InheritsFrom(classSymbol, apiRequestType)) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassFixProvider.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassFixProvider.cs new file mode 100644 index 0000000..4599151 --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassFixProvider.cs @@ -0,0 +1,47 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SyntaxFactory = Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using SyntaxKind = Microsoft.CodeAnalysis.CSharp.SyntaxKind; + +namespace DragonFruit.Data.Roslyn.Analyzers +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ApiRequestClassFixProvider)), Shared] + public class ApiRequestClassFixProvider : CodeFixProvider + { + public sealed override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(ApiRequestClassAnalyzer.DiagnosticId); + + public override FixAllProvider GetFixAllProvider() => null; + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics.Single(); + + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var diagnosticNode = root?.FindNode(diagnostic.Location.SourceSpan); + + if (diagnosticNode is not ClassDeclarationSyntax declaration) + { + return; + } + + context.RegisterCodeFix(CodeAction.Create(title: "Make class partial", createChangedSolution: c => MakeClassPartial(context.Document, declaration, c)), diagnostic); + } + + private async Task MakeClassPartial(Document document, MemberDeclarationSyntax classDeclaration, CancellationToken cancellationToken) + { + var newClassDeclaration = classDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword)); + + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newRoot = root.ReplaceNode(classDeclaration, newClassDeclaration); + + return document.WithSyntaxRoot(newRoot).Project.Solution; + } + } +} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestClassFixProvider.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestClassFixProvider.cs deleted file mode 100644 index 2b38814..0000000 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestClassFixProvider.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Collections.Immutable; -using System.Composition; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CodeActions; -using Microsoft.CodeAnalysis.CodeFixes; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using SyntaxFactory = Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -using SyntaxKind = Microsoft.CodeAnalysis.CSharp.SyntaxKind; - -namespace DragonFruit.Data.Roslyn; - -[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ApiRequestClassFixProvider)), Shared] -public class ApiRequestClassFixProvider : CodeFixProvider -{ - public sealed override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(ApiRequestClassAnalyzer.DiagnosticId); - - public override FixAllProvider GetFixAllProvider() => null; - - public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var diagnostic = context.Diagnostics.Single(); - - var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - var diagnosticNode = root?.FindNode(diagnostic.Location.SourceSpan); - - if (diagnosticNode is not ClassDeclarationSyntax declaration) - { - return; - } - - context.RegisterCodeFix(CodeAction.Create(title: "Make class partial", createChangedSolution: c => MakeClassPartial(context.Document, declaration, c)), diagnostic); - } - - private async Task MakeClassPartial(Document document, MemberDeclarationSyntax classDeclaration, CancellationToken cancellationToken) - { - var newClassDeclaration = classDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword)); - - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var newRoot = root.ReplaceNode(classDeclaration, newClassDeclaration); - - return document.WithSyntaxRoot(newRoot).Project.Solution; - } -} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs new file mode 100644 index 0000000..d4ee894 --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -0,0 +1,81 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace DragonFruit.Data.Roslyn +{ + [Generator] + public class ApiRequestSourceGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var apiRequestDerivedClasses = context.SyntaxProvider.CreateSyntaxProvider( + predicate: (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax classDecl && classDecl.Modifiers.Any(x => x.IsKind(SyntaxKind.PartialKeyword)), + transform: (generatorSyntaxContext, _) => GetSemanticTarget(generatorSyntaxContext)); + + IncrementalValueProvider<(Compilation, ImmutableArray)> targets = context.CompilationProvider.Combine(apiRequestDerivedClasses.Collect()); + context.RegisterSourceOutput(targets, static (spc, source) => Execute(source.Item1, source.Item2, spc)); + } + + private static ClassDeclarationSyntax GetSemanticTarget(GeneratorSyntaxContext context) + { + var model = context.SemanticModel; + var classDeclaration = (ClassDeclarationSyntax)context.Node; + + var classSymbol = ModelExtensions.GetDeclaredSymbol(model, classDeclaration) as INamedTypeSymbol; + + // ensure the class isn't abstract + if (classSymbol?.IsAbstract != false) + { + return null; + } + + while (classSymbol != null) + { + if (classSymbol.ToString() == "DragonFruit.Data.ApiRequest") + { + return classDeclaration; + } + + classSymbol = classSymbol.BaseType; + } + + return null; + } + + private static void Execute(Compilation compilation, ImmutableArray requestClasses, SourceProductionContext context) + { + if (requestClasses.IsDefaultOrEmpty) + { + return; + } + + foreach (var classDeclaration in requestClasses.Distinct()) + { + var model = compilation.GetSemanticModel(classDeclaration.SyntaxTree, true); + var classSymbol = model.GetDeclaredSymbol(classDeclaration)!; + + var sourceBuilder = new StringBuilder("// "); + + sourceBuilder.AppendLine($"namespace {classSymbol.ContainingNamespace.ToDisplayString()} {{"); + sourceBuilder.AppendLine($"partial class {classSymbol.Name} : DragonFruit.Data.Requests.IRequestBuilder {{"); + sourceBuilder.AppendLine("public System.Net.Http.HttpRequestMessage BuildRequest() {"); + + // create request + sourceBuilder.AppendLine("var request = new System.Net.Http.HttpRequestMessage(RequestMethod, RequestPath);"); + + // return request, close method, partial class, namespace + sourceBuilder.AppendLine("return request;\n}}}"); + + context.AddSource($"{classSymbol.Name}.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8)); + } + } + } +} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj index 6ba51ca..e22bbce 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj @@ -1,15 +1,16 @@ - netstandard2.0 - latest false + latest + false + netstandard2.0 + + DragonFruit.Data.Roslyn + DragonFruit.Data.Roslyn true true - - DragonFruit.Data.Roslyn - DragonFruit.Data.Roslyn @@ -22,15 +23,7 @@ - - - - - - - - - + From 10975e615e648132e62f72ae44c2c13054d07bc6 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 1 Dec 2023 22:02:23 +0000 Subject: [PATCH 008/151] add query and basic body to source generation --- .../ApiRequestSourceGenerator.cs | 90 +++++++++++++++++-- 1 file changed, 85 insertions(+), 5 deletions(-) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index d4ee894..e84e3ab 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -1,6 +1,7 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text; @@ -64,18 +65,97 @@ private static void Execute(Compilation compilation, ImmutableArray"); + // using statements + sourceBuilder.AppendLine("using System;"); + sourceBuilder.AppendLine("using System.Text;"); + sourceBuilder.AppendLine("using System.Net.Http;"); + sourceBuilder.AppendLine("using DragonFruit.Data;"); + sourceBuilder.AppendLine("using DragonFruit.Data.Requests;"); + sourceBuilder.AppendLine($"namespace {classSymbol.ContainingNamespace.ToDisplayString()} {{"); - sourceBuilder.AppendLine($"partial class {classSymbol.Name} : DragonFruit.Data.Requests.IRequestBuilder {{"); - sourceBuilder.AppendLine("public System.Net.Http.HttpRequestMessage BuildRequest() {"); + sourceBuilder.AppendLine($"partial class {classSymbol.Name} : IRequestBuilder {{"); + sourceBuilder.AppendLine("public HttpRequestMessage BuildRequest() {"); + + // create request uri + sourceBuilder.AppendLine("UriBuilder uriBuilder = new UriBuilder(RequestPath);"); + sourceBuilder.AppendLine("StringBuilder builder = new StringBuilder();"); + + var parameters = GetParameters(classSymbol, "DragonFruit.Data.Requests.RequestParameterAttribute") + .Select(x => + { + var attributeArgs = x.GetAttributes().Single(y => y.AttributeClass?.ToString() == "DragonFruit.Data.Requests.RequestParameterAttribute").ConstructorArguments; + + return new + { + Symbol = x, + Accessor = x is IPropertySymbol propertySymbol ? $"this.{propertySymbol.Name}" : $"this.{x.Name}()", + ParameterType = (ParameterType)attributeArgs[0].Value!, + PropertyName = attributeArgs[1].Value + }; + }) + .ToLookup(x => x.ParameterType); + + // insert queries + foreach (var query in parameters[ParameterType.Query]) + { + // check if the value is null + sourceBuilder.AppendLine($"if ({query.Accessor} != null)"); + sourceBuilder.AppendLine("{"); + + // append value to query string todo if enum or collection, use special converters + sourceBuilder.AppendLine($"builder.Append(\"{query.PropertyName}=\");"); + sourceBuilder.AppendLine($"builder.Append(Uri.EscapeDataString({query.Accessor}.ToString()));"); + sourceBuilder.AppendLine("builder.Append(\"&\");"); + + // close null check + sourceBuilder.AppendLine("}"); + } - // create request - sourceBuilder.AppendLine("var request = new System.Net.Http.HttpRequestMessage(RequestMethod, RequestPath);"); + // trim off excess ampersand (if one) + sourceBuilder.AppendLine("if (builder.Length > 0)"); + sourceBuilder.AppendLine("{"); + sourceBuilder.AppendLine("builder.Length--;"); + sourceBuilder.AppendLine("}"); + + // set query string as part of uri, create HTTP request message + sourceBuilder.AppendLine("uriBuilder.Query = builder.ToString();"); + sourceBuilder.AppendLine("HttpRequestMessage request = new HttpRequestMessage(RequestMethod, uriBuilder.Uri);"); + + // todo add headers, add form body content + + // add request body - if it is derived from HttpContent then set it directly, otherwise pass to serializer + var requestBody = GetParameters(classSymbol, "DragonFruit.Data.Attributes.RequestBodyAttribute").FirstOrDefault(); + + if (requestBody != null) + { + var bodyAccessor = requestBody is IPropertySymbol propertySymbol ? $"this.{propertySymbol.Name}" : $"this.{requestBody.Name}()"; + sourceBuilder.AppendLine($"request.Content = {bodyAccessor} as HttpContent"); // todo add fallback to serializer + } // return request, close method, partial class, namespace sourceBuilder.AppendLine("return request;\n}}}"); - context.AddSource($"{classSymbol.Name}.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8)); } } + + private static IEnumerable GetParameters(INamespaceOrTypeSymbol symbol, string typeName) + { + // check if member is a method with no parameters or a property then check if it has the [RequestBody] attribute + var candidates = symbol.GetMembers() + .Where(x => x is IPropertySymbol or IMethodSymbol { Parameters.Length: 0 }) + .Where(x => x.GetAttributes().Any(y => y.AttributeClass?.ToString() == typeName)); + + return candidates; + } + + /// + /// Clone of "DragonFruit.Data.Requests.ParameterType" to avoid a dependency on the main library + /// + private enum ParameterType + { + Query = 1, + Form = 2, + Header = 3 + } } } From 1716d10777965f2f7653772e4fd96a97c6db6655 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 1 Dec 2023 22:02:53 +0000 Subject: [PATCH 009/151] specify the generator produces c# --- .../DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index e84e3ab..5df9d21 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -12,7 +12,7 @@ namespace DragonFruit.Data.Roslyn { - [Generator] + [Generator(LanguageNames.CSharp)] public class ApiRequestSourceGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) From b4a683755c2f00310d26cb308020a156f22ad140 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 2 Dec 2023 20:43:35 +0000 Subject: [PATCH 010/151] add copies of enums --- .../Generators/CollectionOptions.cs | 31 +++++++++++++++++++ .../Generators/EnumOptions.cs | 16 ++++++++++ 2 files changed, 47 insertions(+) create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/CollectionOptions.cs create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/EnumOptions.cs diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/CollectionOptions.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/CollectionOptions.cs new file mode 100644 index 0000000..0684f73 --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/CollectionOptions.cs @@ -0,0 +1,31 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +namespace DragonFruit.Data.Roslyn.Generators +{ + /// + /// Copy of for use in the generator + /// + public enum CollectionOptions + { + /// + /// The query name is repeated and a new element created for each (a=1&a=2&a=3) + /// + Recursive, + + /// + /// The query name has indexer symbols appended with no order (a[]=1&a[]=2&a[]=3) + /// + Unordered, + + /// + /// The query name has indexer symbols appended explicit order inserted (a[0]=1&a[1]=2&a[2]=3) + /// + Indexed, + + /// + /// The query is concatenated with a string and merged with one key (a=1,2,3) + /// + Concatenated + } +} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/EnumOptions.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/EnumOptions.cs new file mode 100644 index 0000000..31a316f --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/EnumOptions.cs @@ -0,0 +1,16 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +namespace DragonFruit.Data.Roslyn.Generators +{ + /// + /// Copy of for use in the generator + /// + public enum EnumOptions + { + None = 0, + Numeric = 1, + StringLower = 2, + StringUpper = 3, + } +} From 24ef1e020941450eeb003175c6c96b1c36709944 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 2 Dec 2023 20:44:00 +0000 Subject: [PATCH 011/151] add request symbol base --- .../Generators/RequestSymbol.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbol.cs diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbol.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbol.cs new file mode 100644 index 0000000..608e517 --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbol.cs @@ -0,0 +1,20 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using Microsoft.CodeAnalysis; + +namespace DragonFruit.Data.Roslyn.Generators +{ + public class RequestSymbol + { + public int Depth { get; set; } + + public string Name { get; set; } + public string Accessor { get; set; } + + public ISymbol Symbol { get; set; } + + public EnumOptions? EnumOptions { get; set; } + public CollectionOptions? CollectionOptions { get; set; } + } +} From 3874a219d8ac176a138871091f5204afe387a4cc Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sun, 3 Dec 2023 10:31:51 +0000 Subject: [PATCH 012/151] refactor generators and move copies of enums to references --- .../{ => Generators}/ApiRequestSourceGenerator.cs | 13 ++----------- .../{RequestSymbol.cs => RequestSymbolMetadata.cs} | 3 ++- .../{Generators => References}/CollectionOptions.cs | 4 ++-- .../{Generators => References}/EnumOptions.cs | 4 ++-- .../References/ParameterType.cs | 12 ++++++++++++ 5 files changed, 20 insertions(+), 16 deletions(-) rename DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/{ => Generators}/ApiRequestSourceGenerator.cs (96%) rename DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/{RequestSymbol.cs => RequestSymbolMetadata.cs} (87%) rename DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/{Generators => References}/CollectionOptions.cs (91%) rename DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/{Generators => References}/EnumOptions.cs (83%) create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/ParameterType.cs diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs similarity index 96% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs rename to DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs index 5df9d21..9d6e549 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs @@ -5,12 +5,13 @@ using System.Collections.Immutable; using System.Linq; using System.Text; +using DragonFruit.Data.Roslyn.References; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; -namespace DragonFruit.Data.Roslyn +namespace DragonFruit.Data.Roslyn.Generators { [Generator(LanguageNames.CSharp)] public class ApiRequestSourceGenerator : IIncrementalGenerator @@ -147,15 +148,5 @@ private static IEnumerable GetParameters(INamespaceOrTypeSymbol symbol, return candidates; } - - /// - /// Clone of "DragonFruit.Data.Requests.ParameterType" to avoid a dependency on the main library - /// - private enum ParameterType - { - Query = 1, - Form = 2, - Header = 3 - } } } diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbol.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbolMetadata.cs similarity index 87% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbol.cs rename to DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbolMetadata.cs index 608e517..a766333 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbol.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbolMetadata.cs @@ -1,11 +1,12 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details +using DragonFruit.Data.Roslyn.References; using Microsoft.CodeAnalysis; namespace DragonFruit.Data.Roslyn.Generators { - public class RequestSymbol + public class RequestSymbolMetadata { public int Depth { get; set; } diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/CollectionOptions.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/CollectionOptions.cs similarity index 91% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/CollectionOptions.cs rename to DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/CollectionOptions.cs index 0684f73..d61976d 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/CollectionOptions.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/CollectionOptions.cs @@ -1,12 +1,12 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details -namespace DragonFruit.Data.Roslyn.Generators +namespace DragonFruit.Data.Roslyn.References { /// /// Copy of for use in the generator /// - public enum CollectionOptions + internal enum CollectionOptions { /// /// The query name is repeated and a new element created for each (a=1&a=2&a=3) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/EnumOptions.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/EnumOptions.cs similarity index 83% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/EnumOptions.cs rename to DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/EnumOptions.cs index 31a316f..f7946e3 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/EnumOptions.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/EnumOptions.cs @@ -1,12 +1,12 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details -namespace DragonFruit.Data.Roslyn.Generators +namespace DragonFruit.Data.Roslyn.References { /// /// Copy of for use in the generator /// - public enum EnumOptions + internal enum EnumOptions { None = 0, Numeric = 1, diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/ParameterType.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/ParameterType.cs new file mode 100644 index 0000000..c261b91 --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/ParameterType.cs @@ -0,0 +1,12 @@ +namespace DragonFruit.Data.Roslyn.References +{ + /// + /// Clone of "DragonFruit.Data.Requests.ParameterType" to avoid a dependency on the main library + /// + internal enum ParameterType + { + Query = 1, + Form = 2, + Header = 3 + } +} \ No newline at end of file From ad040bce76f8bd71e078a7e873816eca9f48de57 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sun, 3 Dec 2023 12:43:02 +0000 Subject: [PATCH 013/151] begin adding new preprocessor logic for sourcegen --- .../DragonFruit.Data.Roslyn.csproj | 4 + .../Generators/ApiRequestSourceGenerator.cs | 118 ++++++++++++++---- .../Metadata/RequestSymbolMetadata.cs | 32 +++++ .../Generators/RequestSymbolMetadata.cs | 21 ---- .../References/CollectionOptions.cs | 31 ----- .../References/EnumOptions.cs | 16 --- .../References/ParameterType.cs | 12 -- .../Converters/CollectionConverter.cs | 56 --------- src/Requests/Converters/EnumConverter.cs | 28 ----- .../EnumOptions.cs => EnumOption.cs} | 4 +- src/Requests/EnumOptionsAttribute.cs | 18 +++ ...llectionOptions.cs => EnumerableOption.cs} | 4 +- src/Requests/EnumerableOptionsAttribute.cs | 18 +++ 13 files changed, 172 insertions(+), 190 deletions(-) create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Metadata/RequestSymbolMetadata.cs delete mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbolMetadata.cs delete mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/CollectionOptions.cs delete mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/EnumOptions.cs delete mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/ParameterType.cs delete mode 100644 src/Requests/Converters/CollectionConverter.cs delete mode 100644 src/Requests/Converters/EnumConverter.cs rename src/Requests/{Converters/EnumOptions.cs => EnumOption.cs} (77%) create mode 100644 src/Requests/EnumOptionsAttribute.cs rename src/Requests/{Converters/CollectionOptions.cs => EnumerableOption.cs} (90%) create mode 100644 src/Requests/EnumerableOptionsAttribute.cs diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj index e22bbce..d52b3cf 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj @@ -26,4 +26,8 @@ + + + + diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs index 9d6e549..b4ec956 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs @@ -1,11 +1,15 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details +using System; +using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text; -using DragonFruit.Data.Roslyn.References; +using DragonFruit.Data.Requests; +using DragonFruit.Data.Requests.Converters; +using DragonFruit.Data.Roslyn.Generators.Metadata; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -81,20 +85,7 @@ private static void Execute(Compilation compilation, ImmutableArray - { - var attributeArgs = x.GetAttributes().Single(y => y.AttributeClass?.ToString() == "DragonFruit.Data.Requests.RequestParameterAttribute").ConstructorArguments; - - return new - { - Symbol = x, - Accessor = x is IPropertySymbol propertySymbol ? $"this.{propertySymbol.Name}" : $"this.{x.Name}()", - ParameterType = (ParameterType)attributeArgs[0].Value!, - PropertyName = attributeArgs[1].Value - }; - }) - .ToLookup(x => x.ParameterType); + var parameters = GetRequestSymbolMetadata(compilation, classSymbol); // insert queries foreach (var query in parameters[ParameterType.Query]) @@ -125,7 +116,9 @@ private static void Execute(Compilation compilation, ImmutableArray x is IPropertySymbol or IMethodSymbol { Parameters.Length: 0 }) + .FirstOrDefault(x => x.GetAttributes().Any(y => y.AttributeClass?.ToString() == typeof(RequestBodyAttribute).FullName)); if (requestBody != null) { @@ -139,14 +132,95 @@ private static void Execute(Compilation compilation, ImmutableArray GetParameters(INamespaceOrTypeSymbol symbol, string typeName) + private static IReadOnlyDictionary> GetRequestSymbolMetadata(Compilation compilation, INamespaceOrTypeSymbol symbol) { - // check if member is a method with no parameters or a property then check if it has the [RequestBody] attribute - var candidates = symbol.GetMembers() - .Where(x => x is IPropertySymbol or IMethodSymbol { Parameters.Length: 0 }) - .Where(x => x.GetAttributes().Any(y => y.AttributeClass?.ToString() == typeName)); + var symbols = Enum.GetValues(typeof(ParameterType)).Cast().ToDictionary(x => x, _ => (IList)new List()); - return candidates; + // get types used in member processing + var enumerableParameterAttribute = compilation.GetTypeByMetadataName(typeof(EnumerableOptionsAttribute).FullName); + var requestParameterAttribute = compilation.GetTypeByMetadataName(typeof(RequestParameterAttribute).FullName); + var enumParameterAttribute = compilation.GetTypeByMetadataName(typeof(EnumOptionsAttribute).FullName); + + var enumerableTypeSymbol = compilation.GetTypeByMetadataName(typeof(IEnumerable<>).FullName); + + // todo recursively select all derived types' members as well + + foreach (var candidate in symbol.GetMembers().Where(x => x is IPropertySymbol or IMethodSymbol { Parameters.Length: 0 })) + { + var requestAttribute = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(requestParameterAttribute, SymbolEqualityComparer.Default) == true); + + if (requestAttribute == null) + { + continue; + } + + var returnType = candidate switch + { + IPropertySymbol propertySymbol => propertySymbol.Type, + IMethodSymbol methodSymbol => methodSymbol.ReturnType, + + _ => throw new NotSupportedException() + }; + + var parameterType = (ParameterType)requestAttribute.ConstructorArguments[0].Value!; + var parameterName = (string)requestAttribute.ConstructorArguments.ElementAtOrDefault(1).Value ?? candidate.Name; + + RequestSymbolMetadata metadata = null; + + // handle enums + if (returnType.TypeKind == TypeKind.Enum) + { + var enumOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumParameterAttribute, SymbolEqualityComparer.Default) == true); + + metadata = new EnumRequestSymbolMetadata + { + Options = (EnumOption?)enumOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumOption.None + }; + } + // handle common types - strings, primitives and structs + else if (returnType.SpecialType == SpecialType.System_String || returnType.IsValueType) + { + metadata = new RequestSymbolMetadata(); + } + // handle arrays, IEnumerable, IEnumerable + else if (returnType.SpecialType == SpecialType.System_Array || returnType.AllInterfaces.Any(i => enumerableTypeSymbol!.Equals(i.OriginalDefinition, SymbolEqualityComparer.Default))) + { + var enumerableInterfaces = returnType.AllInterfaces.Where(i => enumerableTypeSymbol!.Equals(i.OriginalDefinition, SymbolEqualityComparer.Default)); + + // if ienumerable check T is primitive or string + if (returnType is INamedTypeSymbol { TypeArguments.Length: 1 } namedTypeSymbol) + { + var typeArgument = namedTypeSymbol.TypeArguments[0]; + + if (typeArgument.SpecialType != SpecialType.System_String && typeArgument.IsValueType) + { + continue; + } + } + + var enumerableOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumerableParameterAttribute, SymbolEqualityComparer.Default) == true); + + metadata = new EnumerableRequestSymbolMetadata + { + Separator = (string)enumerableOptions?.ConstructorArguments.ElementAtOrDefault(1).Value ?? ",", + Options = (EnumerableOption?)enumerableOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumerableOption.Concatenated + }; + } + + if (metadata == null) + { + continue; + } + + metadata.Symbol = candidate; + metadata.Name = parameterName; + metadata.Accessor = candidate is IPropertySymbol ps ? $"this.{ps.Name}" : $"this.{candidate.Name}()"; + metadata.Nullable = returnType.IsReferenceType || (returnType.IsValueType && returnType.NullableAnnotation == NullableAnnotation.Annotated); + + symbols[parameterType].Add(metadata); + } + + return symbols; } } } diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Metadata/RequestSymbolMetadata.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Metadata/RequestSymbolMetadata.cs new file mode 100644 index 0000000..4030503 --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Metadata/RequestSymbolMetadata.cs @@ -0,0 +1,32 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using DragonFruit.Data.Requests; +using Microsoft.CodeAnalysis; + +namespace DragonFruit.Data.Roslyn.Generators.Metadata +{ + internal class RequestSymbolMetadata + { + public int Depth { get; set; } + + public bool Nullable { get; set; } + + public string Name { get; set; } + public string Accessor { get; set; } + + public ISymbol Symbol { get; set; } + } + + internal class EnumRequestSymbolMetadata : RequestSymbolMetadata + { + public EnumOption? Options { get; set; } + } + + internal class EnumerableRequestSymbolMetadata : RequestSymbolMetadata + { + public string Separator { get; set; } + public string EnumerableType { get; set; } + public EnumerableOption? Options { get; set; } + } +} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbolMetadata.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbolMetadata.cs deleted file mode 100644 index a766333..0000000 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbolMetadata.cs +++ /dev/null @@ -1,21 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using DragonFruit.Data.Roslyn.References; -using Microsoft.CodeAnalysis; - -namespace DragonFruit.Data.Roslyn.Generators -{ - public class RequestSymbolMetadata - { - public int Depth { get; set; } - - public string Name { get; set; } - public string Accessor { get; set; } - - public ISymbol Symbol { get; set; } - - public EnumOptions? EnumOptions { get; set; } - public CollectionOptions? CollectionOptions { get; set; } - } -} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/CollectionOptions.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/CollectionOptions.cs deleted file mode 100644 index d61976d..0000000 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/CollectionOptions.cs +++ /dev/null @@ -1,31 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -namespace DragonFruit.Data.Roslyn.References -{ - /// - /// Copy of for use in the generator - /// - internal enum CollectionOptions - { - /// - /// The query name is repeated and a new element created for each (a=1&a=2&a=3) - /// - Recursive, - - /// - /// The query name has indexer symbols appended with no order (a[]=1&a[]=2&a[]=3) - /// - Unordered, - - /// - /// The query name has indexer symbols appended explicit order inserted (a[0]=1&a[1]=2&a[2]=3) - /// - Indexed, - - /// - /// The query is concatenated with a string and merged with one key (a=1,2,3) - /// - Concatenated - } -} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/EnumOptions.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/EnumOptions.cs deleted file mode 100644 index f7946e3..0000000 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/EnumOptions.cs +++ /dev/null @@ -1,16 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -namespace DragonFruit.Data.Roslyn.References -{ - /// - /// Copy of for use in the generator - /// - internal enum EnumOptions - { - None = 0, - Numeric = 1, - StringLower = 2, - StringUpper = 3, - } -} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/ParameterType.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/ParameterType.cs deleted file mode 100644 index c261b91..0000000 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/References/ParameterType.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace DragonFruit.Data.Roslyn.References -{ - /// - /// Clone of "DragonFruit.Data.Requests.ParameterType" to avoid a dependency on the main library - /// - internal enum ParameterType - { - Query = 1, - Form = 2, - Header = 3 - } -} \ No newline at end of file diff --git a/src/Requests/Converters/CollectionConverter.cs b/src/Requests/Converters/CollectionConverter.cs deleted file mode 100644 index 3feb8e3..0000000 --- a/src/Requests/Converters/CollectionConverter.cs +++ /dev/null @@ -1,56 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System.Collections.Generic; -using System.Text; - -namespace DragonFruit.Data.Requests.Converters -{ - public class CollectionConverter - { - // todo move to sourcegen - public static void WriteCollectionValue(IEnumerable collection, string keyPrefix, CollectionOptions mode, StringBuilder destination) - { - switch (mode) - { - case CollectionOptions.Recursive: - { - foreach (var item in collection) - { - destination.Append($"{keyPrefix}={item}&"); - } - - break; - } - - case CollectionOptions.Unordered: - { - foreach (var item in collection) - { - destination.Append($"{keyPrefix}[]={item}&"); - } - - break; - } - - case CollectionOptions.Indexed: - { - var index = 0; - - foreach (var item in collection) - { - destination.Append($"{keyPrefix}[{index++}]={item}&"); - } - - break; - } - - case CollectionOptions.Concatenated: - { - destination.Append(string.Join(",", collection)); - break; - } - } - } - } -} diff --git a/src/Requests/Converters/EnumConverter.cs b/src/Requests/Converters/EnumConverter.cs deleted file mode 100644 index b6583be..0000000 --- a/src/Requests/Converters/EnumConverter.cs +++ /dev/null @@ -1,28 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System.Text; - -namespace DragonFruit.Data.Requests.Converters -{ - public static class EnumConverter - { - public static void WriteEnumValue(object enumValue, EnumOptions mode, StringBuilder target) - { - switch (mode) - { - case EnumOptions.Numeric: - target.Append((int)enumValue); - break; - - case EnumOptions.StringLower: - target.Append(enumValue.ToString().ToLowerInvariant().Replace(" ", string.Empty)); - break; - - case EnumOptions.StringUpper: - target.Append(enumValue.ToString().ToUpperInvariant().Replace(" ", string.Empty)); - break; - } - } - } -} diff --git a/src/Requests/Converters/EnumOptions.cs b/src/Requests/EnumOption.cs similarity index 77% rename from src/Requests/Converters/EnumOptions.cs rename to src/Requests/EnumOption.cs index 3b2e693..e323003 100644 --- a/src/Requests/Converters/EnumOptions.cs +++ b/src/Requests/EnumOption.cs @@ -1,9 +1,9 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details -namespace DragonFruit.Data.Requests.Converters +namespace DragonFruit.Data.Requests { - public enum EnumOptions + public enum EnumOption { None = 0, Numeric = 1, diff --git a/src/Requests/EnumOptionsAttribute.cs b/src/Requests/EnumOptionsAttribute.cs new file mode 100644 index 0000000..3dc5eac --- /dev/null +++ b/src/Requests/EnumOptionsAttribute.cs @@ -0,0 +1,18 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System; + +namespace DragonFruit.Data.Requests +{ + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] + public class EnumOptionsAttribute : Attribute + { + public EnumOptionsAttribute(EnumOption options) + { + Options = options; + } + + public EnumOption Options { get; set; } + } +} diff --git a/src/Requests/Converters/CollectionOptions.cs b/src/Requests/EnumerableOption.cs similarity index 90% rename from src/Requests/Converters/CollectionOptions.cs rename to src/Requests/EnumerableOption.cs index 89f465f..857801a 100644 --- a/src/Requests/Converters/CollectionOptions.cs +++ b/src/Requests/EnumerableOption.cs @@ -1,9 +1,9 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details -namespace DragonFruit.Data.Requests.Converters +namespace DragonFruit.Data.Requests { - public enum CollectionOptions + public enum EnumerableOption { /// /// The query name is repeated and a new element created for each (a=1&a=2&a=3) diff --git a/src/Requests/EnumerableOptionsAttribute.cs b/src/Requests/EnumerableOptionsAttribute.cs new file mode 100644 index 0000000..19911ab --- /dev/null +++ b/src/Requests/EnumerableOptionsAttribute.cs @@ -0,0 +1,18 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System; + +namespace DragonFruit.Data.Requests +{ + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] + public class EnumerableOptionsAttribute : Attribute + { + public EnumerableOptionsAttribute(EnumerableOption options) + { + Options = options; + } + + public EnumerableOption Options { get; set; } + } +} From 988ed35d3916e06f63a81afedbb7f40302aa6caa Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 5 Dec 2023 11:42:44 +0000 Subject: [PATCH 014/151] add explicit IsString property --- .../Generators/Metadata/RequestSymbolMetadata.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Metadata/RequestSymbolMetadata.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Metadata/RequestSymbolMetadata.cs index 4030503..bd3fc75 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Metadata/RequestSymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Metadata/RequestSymbolMetadata.cs @@ -10,6 +10,7 @@ internal class RequestSymbolMetadata { public int Depth { get; set; } + public bool IsString { get; set; } public bool Nullable { get; set; } public string Name { get; set; } From 76e1db747953875b0418217a84766f91afac44ea Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 5 Dec 2023 11:43:11 +0000 Subject: [PATCH 015/151] add derived class and match ienumerable handling to old system --- .../Generators/ApiRequestSourceGenerator.cs | 121 +++++++++--------- 1 file changed, 60 insertions(+), 61 deletions(-) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs index b4ec956..454a2c9 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Text; using DragonFruit.Data.Requests; -using DragonFruit.Data.Requests.Converters; using DragonFruit.Data.Roslyn.Generators.Metadata; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -132,7 +131,7 @@ private static void Execute(Compilation compilation, ImmutableArray> GetRequestSymbolMetadata(Compilation compilation, INamespaceOrTypeSymbol symbol) + private static IReadOnlyDictionary> GetRequestSymbolMetadata(Compilation compilation, INamedTypeSymbol symbol) { var symbols = Enum.GetValues(typeof(ParameterType)).Cast().ToDictionary(x => x, _ => (IList)new List()); @@ -141,84 +140,84 @@ private static IReadOnlyDictionary> var requestParameterAttribute = compilation.GetTypeByMetadataName(typeof(RequestParameterAttribute).FullName); var enumParameterAttribute = compilation.GetTypeByMetadataName(typeof(EnumOptionsAttribute).FullName); - var enumerableTypeSymbol = compilation.GetTypeByMetadataName(typeof(IEnumerable<>).FullName); + var enumerableTypeSymbol = compilation.GetTypeByMetadataName(typeof(IEnumerable).FullName); + var apiRequestBaseType = compilation.GetTypeByMetadataName(typeof(ApiRequest).FullName); - // todo recursively select all derived types' members as well + // track properties already visited + var currentSymbol = symbol; + var consumedProperties = new HashSet(); - foreach (var candidate in symbol.GetMembers().Where(x => x is IPropertySymbol or IMethodSymbol { Parameters.Length: 0 })) + do { - var requestAttribute = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(requestParameterAttribute, SymbolEqualityComparer.Default) == true); - - if (requestAttribute == null) + foreach (var candidate in currentSymbol.GetMembers().Where(x => x is IPropertySymbol or IMethodSymbol { Parameters.Length: 0 })) { - continue; - } + var requestAttribute = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(requestParameterAttribute, SymbolEqualityComparer.Default) == true); - var returnType = candidate switch - { - IPropertySymbol propertySymbol => propertySymbol.Type, - IMethodSymbol methodSymbol => methodSymbol.ReturnType, + // ensure that properties ovewritten using "new" are not processed twice + if (requestAttribute == null || !consumedProperties.Add(candidate.MetadataName)) + { + continue; + } - _ => throw new NotSupportedException() - }; + var returnType = candidate switch + { + IPropertySymbol propertySymbol => propertySymbol.Type, + IMethodSymbol methodSymbol => methodSymbol.ReturnType, - var parameterType = (ParameterType)requestAttribute.ConstructorArguments[0].Value!; - var parameterName = (string)requestAttribute.ConstructorArguments.ElementAtOrDefault(1).Value ?? candidate.Name; + _ => throw new NotSupportedException() + }; - RequestSymbolMetadata metadata = null; + if (returnType.SpecialType == SpecialType.System_Void) + { + // todo return diagnostic warning + continue; + } - // handle enums - if (returnType.TypeKind == TypeKind.Enum) - { - var enumOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumParameterAttribute, SymbolEqualityComparer.Default) == true); + var parameterType = (ParameterType)requestAttribute.ConstructorArguments[0].Value!; + var parameterName = (string)requestAttribute.ConstructorArguments.ElementAtOrDefault(1).Value ?? candidate.Name; - metadata = new EnumRequestSymbolMetadata - { - Options = (EnumOption?)enumOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumOption.None - }; - } - // handle common types - strings, primitives and structs - else if (returnType.SpecialType == SpecialType.System_String || returnType.IsValueType) - { - metadata = new RequestSymbolMetadata(); - } - // handle arrays, IEnumerable, IEnumerable - else if (returnType.SpecialType == SpecialType.System_Array || returnType.AllInterfaces.Any(i => enumerableTypeSymbol!.Equals(i.OriginalDefinition, SymbolEqualityComparer.Default))) - { - var enumerableInterfaces = returnType.AllInterfaces.Where(i => enumerableTypeSymbol!.Equals(i.OriginalDefinition, SymbolEqualityComparer.Default)); - - // if ienumerable check T is primitive or string - if (returnType is INamedTypeSymbol { TypeArguments.Length: 1 } namedTypeSymbol) + RequestSymbolMetadata metadata; + + // handle enums + if (returnType.TypeKind == TypeKind.Enum) { - var typeArgument = namedTypeSymbol.TypeArguments[0]; + var enumOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumParameterAttribute, SymbolEqualityComparer.Default) == true); - if (typeArgument.SpecialType != SpecialType.System_String && typeArgument.IsValueType) + metadata = new EnumRequestSymbolMetadata { - continue; - } + Options = (EnumOption?)enumOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumOption.None + }; } + // handle arrays/IEnumerable + else if (returnType.SpecialType == SpecialType.System_Array || returnType.FindImplementationForInterfaceMember(enumerableTypeSymbol) != null) + { + var enumerableOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumerableParameterAttribute, SymbolEqualityComparer.Default) == true); - var enumerableOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumerableParameterAttribute, SymbolEqualityComparer.Default) == true); - - metadata = new EnumerableRequestSymbolMetadata + metadata = new EnumerableRequestSymbolMetadata + { + Separator = (string)enumerableOptions?.ConstructorArguments.ElementAtOrDefault(1).Value ?? ",", + Options = (EnumerableOption?)enumerableOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumerableOption.Concatenated + }; + } + else { - Separator = (string)enumerableOptions?.ConstructorArguments.ElementAtOrDefault(1).Value ?? ",", - Options = (EnumerableOption?)enumerableOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumerableOption.Concatenated - }; - } + metadata = new RequestSymbolMetadata + { + IsString = returnType.SpecialType == SpecialType.System_String + }; + } - if (metadata == null) - { - continue; - } + metadata.Symbol = candidate; + metadata.Name = parameterName; + metadata.Accessor = candidate is IPropertySymbol ps ? $"this.{ps.Name}" : $"this.{candidate.Name}()"; + metadata.Nullable = returnType.IsReferenceType || (returnType.IsValueType && returnType.NullableAnnotation == NullableAnnotation.Annotated); - metadata.Symbol = candidate; - metadata.Name = parameterName; - metadata.Accessor = candidate is IPropertySymbol ps ? $"this.{ps.Name}" : $"this.{candidate.Name}()"; - metadata.Nullable = returnType.IsReferenceType || (returnType.IsValueType && returnType.NullableAnnotation == NullableAnnotation.Annotated); + symbols[parameterType].Add(metadata); + } - symbols[parameterType].Add(metadata); - } + // get derived class from currentSymbol + currentSymbol = currentSymbol.BaseType; + } while (currentSymbol?.Equals(apiRequestBaseType, SymbolEqualityComparer.Default) == false); return symbols; } From 79067931c4340602cea4df6c5d18158cf5d117c1 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 5 Dec 2023 12:20:19 +0000 Subject: [PATCH 016/151] track depth of each member --- .../Generators/ApiRequestSourceGenerator.cs | 4 +++- .../Generators/Metadata/RequestSymbolMetadata.cs | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs index 454a2c9..25ac4fd 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs @@ -144,6 +144,7 @@ private static IReadOnlyDictionary> var apiRequestBaseType = compilation.GetTypeByMetadataName(typeof(ApiRequest).FullName); // track properties already visited + var depth = 0; var currentSymbol = symbol; var consumedProperties = new HashSet(); @@ -207,9 +208,9 @@ private static IReadOnlyDictionary> }; } + metadata.Depth = depth; metadata.Symbol = candidate; metadata.Name = parameterName; - metadata.Accessor = candidate is IPropertySymbol ps ? $"this.{ps.Name}" : $"this.{candidate.Name}()"; metadata.Nullable = returnType.IsReferenceType || (returnType.IsValueType && returnType.NullableAnnotation == NullableAnnotation.Annotated); symbols[parameterType].Add(metadata); @@ -217,6 +218,7 @@ private static IReadOnlyDictionary> // get derived class from currentSymbol currentSymbol = currentSymbol.BaseType; + depth++; } while (currentSymbol?.Equals(apiRequestBaseType, SymbolEqualityComparer.Default) == false); return symbols; diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Metadata/RequestSymbolMetadata.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Metadata/RequestSymbolMetadata.cs index bd3fc75..73c00a2 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Metadata/RequestSymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Metadata/RequestSymbolMetadata.cs @@ -13,10 +13,10 @@ internal class RequestSymbolMetadata public bool IsString { get; set; } public bool Nullable { get; set; } + public ISymbol Symbol { get; set; } public string Name { get; set; } - public string Accessor { get; set; } - public ISymbol Symbol { get; set; } + public string Accessor => Symbol is IPropertySymbol ps ? $"this.{ps.Name}" : $"this.{Symbol.Name}()"; } internal class EnumRequestSymbolMetadata : RequestSymbolMetadata From bc44b388cfc6e9b15f9cf840bb7d170e1638457c Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 5 Dec 2023 12:26:01 +0000 Subject: [PATCH 017/151] update supported collection listing --- .../Generators/ApiRequestSourceGenerator.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs index 25ac4fd..dfd27cb 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs @@ -19,6 +19,17 @@ namespace DragonFruit.Data.Roslyn.Generators [Generator(LanguageNames.CSharp)] public class ApiRequestSourceGenerator : IIncrementalGenerator { + private static readonly HashSet SupportedCollectionTypes = new(new[] + { + SpecialType.System_Array, + SpecialType.System_Collections_IEnumerable, + SpecialType.System_Collections_Generic_IList_T, + SpecialType.System_Collections_Generic_ICollection_T, + SpecialType.System_Collections_Generic_IEnumerable_T, + SpecialType.System_Collections_Generic_IReadOnlyList_T, + SpecialType.System_Collections_Generic_IReadOnlyCollection_T, + }); + public void Initialize(IncrementalGeneratorInitializationContext context) { var apiRequestDerivedClasses = context.SyntaxProvider.CreateSyntaxProvider( @@ -190,7 +201,7 @@ private static IReadOnlyDictionary> }; } // handle arrays/IEnumerable - else if (returnType.SpecialType == SpecialType.System_Array || returnType.FindImplementationForInterfaceMember(enumerableTypeSymbol) != null) + else if (SupportedCollectionTypes.Contains(returnType.SpecialType) || returnType.FindImplementationForInterfaceMember(enumerableTypeSymbol) != null) { var enumerableOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumerableParameterAttribute, SymbolEqualityComparer.Default) == true); From 4ccede30cdb53d52cb175622f14716dce74c25fe Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 5 Dec 2023 17:35:48 +0000 Subject: [PATCH 018/151] add liquid template format --- .../DragonFruit.Data.Roslyn.csproj | 7 ++- .../{Metadata => }/RequestSymbolMetadata.cs | 4 +- .../Generators/Templates/ApiRequest.liquid | 57 +++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) rename DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/{Metadata => }/RequestSymbolMetadata.cs (90%) create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj index d52b3cf..45f909e 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj @@ -20,14 +20,19 @@ + - + + + + + diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Metadata/RequestSymbolMetadata.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbolMetadata.cs similarity index 90% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Metadata/RequestSymbolMetadata.cs rename to DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbolMetadata.cs index 73c00a2..d53fbb0 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Metadata/RequestSymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbolMetadata.cs @@ -4,7 +4,7 @@ using DragonFruit.Data.Requests; using Microsoft.CodeAnalysis; -namespace DragonFruit.Data.Roslyn.Generators.Metadata +namespace DragonFruit.Data.Roslyn.Generators { internal class RequestSymbolMetadata { @@ -14,7 +14,7 @@ internal class RequestSymbolMetadata public bool Nullable { get; set; } public ISymbol Symbol { get; set; } - public string Name { get; set; } + public string ParameterName { get; set; } public string Accessor => Symbol is IPropertySymbol ps ? $"this.{ps.Name}" : $"this.{Symbol.Name}()"; } diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid new file mode 100644 index 0000000..b48988e --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid @@ -0,0 +1,57 @@ +using System; +using System.Text; +using System.Net.Http; +using DragonFruit.Data; +using DragonFruit.Data.Requests; + +namespace {{ Namespace }} +{ + partial class {{ ClassName }} : global::DragonFruit.Data.Requests.IRequestBuilder + { + public global::System.Net.Http.HttpRequestMessage BuildRequest() + { + global::System.UriBuilder uriBuilder = new global::System.UriBuilder(RequestPath); + global::System.Text.StringBuilder queryBuilder = new global::System.Text.StringBuilder(); + + {%- for query in query_parameters -%} + {% capture query_append %} + builder.Append($"{{ query.parameter_name }}={global::System.Uri.EscapeDataString({{ query.accessor }}.ToString())}&"); + {% endcapture %} + + {% if query.nullable %} + if ({{ query.accessor }} != null) + { + {{ query_append }} + } + {% else %} + {{ query_append }} + {% endif %} + {% endfor -%} + + if (queryBuilder.Length > 0) + { + queryBuilder.Length--; // remove trailing & + uriBuilder.Query = queryBuilder.ToString(); + } + + global::System.Net.Http.HttpRequestMessage request = new global::System.Net.Http.HttpRequestMessage(RequestMethod, uriBuilder.Uri); + + {%- for header in header_parameters -%} + {% capture header_append %} + request.Headers.Add("{{ header.parameter_name }}", {{ header.accessor }}.ToString()); + {% endcapture %} + + {% if header.nullable %} + if ({{ header.accessor }} != null) + { + {{ header_append }} + } + {% else %} + {{ header_append }} + {% endif %} + {% endfor -%} + + return request; + } + } +} From 296f4d1c6ee844e49b03d249978a60ff8018890d Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 5 Dec 2023 17:36:17 +0000 Subject: [PATCH 019/151] add template stream loader and use liquid templates --- .../Generators/ApiRequestSourceGenerator.cs | 82 ++++++------------- 1 file changed, 26 insertions(+), 56 deletions(-) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs index dfd27cb..1a63577 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs @@ -8,17 +8,19 @@ using System.Linq; using System.Text; using DragonFruit.Data.Requests; -using DragonFruit.Data.Roslyn.Generators.Metadata; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; +using Scriban; namespace DragonFruit.Data.Roslyn.Generators { [Generator(LanguageNames.CSharp)] public class ApiRequestSourceGenerator : IIncrementalGenerator { + private static readonly Template RequestTemplate; + private static readonly HashSet SupportedCollectionTypes = new(new[] { SpecialType.System_Array, @@ -30,6 +32,19 @@ public class ApiRequestSourceGenerator : IIncrementalGenerator SpecialType.System_Collections_Generic_IReadOnlyCollection_T, }); + static ApiRequestSourceGenerator() + { + using var templateStream = typeof(ApiRequestSourceGenerator).Assembly.GetManifestResourceStream("DragonFruit.Data.Roslyn.Generators.Templates.ApiRequest.liquid"); + + if (templateStream == null) + { + throw new NullReferenceException("Could not find template"); + } + + using var reader = new System.IO.StreamReader(templateStream); + RequestTemplate = Template.ParseLiquid(reader.ReadToEnd()); + } + public void Initialize(IncrementalGeneratorInitializationContext context) { var apiRequestDerivedClasses = context.SyntaxProvider.CreateSyntaxProvider( @@ -79,65 +94,20 @@ private static void Execute(Compilation compilation, ImmutableArray"); - - // using statements - sourceBuilder.AppendLine("using System;"); - sourceBuilder.AppendLine("using System.Text;"); - sourceBuilder.AppendLine("using System.Net.Http;"); - sourceBuilder.AppendLine("using DragonFruit.Data;"); - sourceBuilder.AppendLine("using DragonFruit.Data.Requests;"); - - sourceBuilder.AppendLine($"namespace {classSymbol.ContainingNamespace.ToDisplayString()} {{"); - sourceBuilder.AppendLine($"partial class {classSymbol.Name} : IRequestBuilder {{"); - sourceBuilder.AppendLine("public HttpRequestMessage BuildRequest() {"); - - // create request uri - sourceBuilder.AppendLine("UriBuilder uriBuilder = new UriBuilder(RequestPath);"); - sourceBuilder.AppendLine("StringBuilder builder = new StringBuilder();"); - var parameters = GetRequestSymbolMetadata(compilation, classSymbol); - // insert queries - foreach (var query in parameters[ParameterType.Query]) + var parameterInfo = new { - // check if the value is null - sourceBuilder.AppendLine($"if ({query.Accessor} != null)"); - sourceBuilder.AppendLine("{"); - - // append value to query string todo if enum or collection, use special converters - sourceBuilder.AppendLine($"builder.Append(\"{query.PropertyName}=\");"); - sourceBuilder.AppendLine($"builder.Append(Uri.EscapeDataString({query.Accessor}.ToString()));"); - sourceBuilder.AppendLine("builder.Append(\"&\");"); - - // close null check - sourceBuilder.AppendLine("}"); - } - - // trim off excess ampersand (if one) - sourceBuilder.AppendLine("if (builder.Length > 0)"); - sourceBuilder.AppendLine("{"); - sourceBuilder.AppendLine("builder.Length--;"); - sourceBuilder.AppendLine("}"); + Namespace = classSymbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + ClassName = classSymbol.Name, + HeaderParameters = parameters[ParameterType.Header], + QueryParameters = parameters[ParameterType.Query], - // set query string as part of uri, create HTTP request message - sourceBuilder.AppendLine("uriBuilder.Query = builder.ToString();"); - sourceBuilder.AppendLine("HttpRequestMessage request = new HttpRequestMessage(RequestMethod, uriBuilder.Uri);"); - - // todo add headers, add form body content - - // add request body - if it is derived from HttpContent then set it directly, otherwise pass to serializer - var requestBody = classSymbol.GetMembers() - .Where(x => x is IPropertySymbol or IMethodSymbol { Parameters.Length: 0 }) - .FirstOrDefault(x => x.GetAttributes().Any(y => y.AttributeClass?.ToString() == typeof(RequestBodyAttribute).FullName)); - - if (requestBody != null) - { - var bodyAccessor = requestBody is IPropertySymbol propertySymbol ? $"this.{propertySymbol.Name}" : $"this.{requestBody.Name}()"; - sourceBuilder.AppendLine($"request.Content = {bodyAccessor} as HttpContent"); // todo add fallback to serializer - } + // todo add form body content + }; - // return request, close method, partial class, namespace - sourceBuilder.AppendLine("return request;\n}}}"); + sourceBuilder.Append("\n\n"); + sourceBuilder.Append(RequestTemplate.Render(parameterInfo)); context.AddSource($"{classSymbol.Name}.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8)); } } @@ -221,7 +191,7 @@ private static IReadOnlyDictionary> metadata.Depth = depth; metadata.Symbol = candidate; - metadata.Name = parameterName; + metadata.ParameterName = parameterName; metadata.Nullable = returnType.IsReferenceType || (returnType.IsValueType && returnType.NullableAnnotation == NullableAnnotation.Annotated); symbols[parameterType].Add(metadata); From d00b868de92ba44489fe3227a3627871ea956aea Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 14:55:46 +0000 Subject: [PATCH 020/151] add enumerable converter for stringbuilders --- src/Requests/EnumerableConverter.cs | 57 +++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/Requests/EnumerableConverter.cs diff --git a/src/Requests/EnumerableConverter.cs b/src/Requests/EnumerableConverter.cs new file mode 100644 index 0000000..fd51d3d --- /dev/null +++ b/src/Requests/EnumerableConverter.cs @@ -0,0 +1,57 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System; +using System.Collections; +using System.Linq; +using System.Text; + +namespace DragonFruit.Data.Requests +{ + public static class EnumerableConverter + { + /// + /// Writes the provided to the using the specified + /// + /// The destination + /// The source collection + /// The to use. If none provided, defaults to + /// The name of the property to use when writing values to + /// The separator to use, if required. + public static void WriteEnumerable(StringBuilder destination, IEnumerable source, EnumerableOption mode, string propertyName, string separator) + { + switch (mode) + { + case EnumerableOption.Recursive: + foreach (var item in source) + { + destination.Append($"{propertyName}={Uri.EscapeDataString(item.ToString())}&"); + } + + break; + + case EnumerableOption.Indexed: + var counter = 0; + + foreach (var item in source) + { + destination.AppendFormat("{0}[{1}]={2}&", propertyName, counter++, Uri.EscapeDataString(item.ToString())); + } + + break; + + case EnumerableOption.Unordered: + foreach (var item in source) + { + destination.AppendFormat("{0}[]={1}&", propertyName, Uri.EscapeDataString(item.ToString())); + } + + break; + + default: + destination.AppendFormat("{0}={1}&", propertyName, string.Join(separator, source.Cast().Select(x => Uri.EscapeDataString(x.ToString())))); + break; + } + } + } +} From 7695d1721eb792e260b291da1e61b940e915b770 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 14:55:57 +0000 Subject: [PATCH 021/151] refactor vars --- .../Generators/ApiRequestSourceGenerator.cs | 4 ++-- .../Generators/RequestSymbolMetadata.cs | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs index 1a63577..f267f0b 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs @@ -167,7 +167,7 @@ private static IReadOnlyDictionary> metadata = new EnumRequestSymbolMetadata { - Options = (EnumOption?)enumOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumOption.None + EnumOption = (EnumOption?)enumOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumOption.None }; } // handle arrays/IEnumerable @@ -178,7 +178,7 @@ private static IReadOnlyDictionary> metadata = new EnumerableRequestSymbolMetadata { Separator = (string)enumerableOptions?.ConstructorArguments.ElementAtOrDefault(1).Value ?? ",", - Options = (EnumerableOption?)enumerableOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumerableOption.Concatenated + EnumerableOption = (EnumerableOption?)enumerableOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumerableOption.Concatenated }; } else diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbolMetadata.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbolMetadata.cs index d53fbb0..9e3288b 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbolMetadata.cs @@ -16,18 +16,29 @@ internal class RequestSymbolMetadata public ISymbol Symbol { get; set; } public string ParameterName { get; set; } + public virtual RequestSymbolType Type => RequestSymbolType.Standard; public string Accessor => Symbol is IPropertySymbol ps ? $"this.{ps.Name}" : $"this.{Symbol.Name}()"; } internal class EnumRequestSymbolMetadata : RequestSymbolMetadata { - public EnumOption? Options { get; set; } + public override RequestSymbolType Type => RequestSymbolType.Enum; + + public EnumOption EnumOption { get; set; } } internal class EnumerableRequestSymbolMetadata : RequestSymbolMetadata { + public override RequestSymbolType Type => RequestSymbolType.Enumerable; + public string Separator { get; set; } - public string EnumerableType { get; set; } - public EnumerableOption? Options { get; set; } + public EnumerableOption EnumerableOption { get; set; } + } + + internal enum RequestSymbolType + { + Standard = 0, + Enumerable = 1, + Enum = 2, } } From 32a54f46ef7657f76f5b5c88a0d9e2a1a3cc9a75 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 14:56:07 +0000 Subject: [PATCH 022/151] add enumconverter --- .../Generators/Templates/ApiRequest.liquid | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid index b48988e..28334d7 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid @@ -15,7 +15,12 @@ namespace {{ Namespace }} {%- for query in query_parameters -%} {% capture query_append %} - builder.Append($"{{ query.parameter_name }}={global::System.Uri.EscapeDataString({{ query.accessor }}.ToString())}&"); + {% case query.type %} + {% when 1 %} + global::DragonFruit.Data.Requests.EnumerableConverter.AppendEnumerable(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ query.options }}, "{{ query.parameter_name }}", "{{ query.separator }}"); + {% else %} + builder.Append($"{{ query.parameter_name }}={global::System.Uri.EscapeDataString({{ query.accessor }}.ToString())}&"); + {% endcase %} {% endcapture %} {% if query.nullable %} @@ -34,7 +39,7 @@ namespace {{ Namespace }} uriBuilder.Query = queryBuilder.ToString(); } - global::System.Net.Http.HttpRequestMessage request = new global::System.Net.Http.HttpRequestMessage(RequestMethod, uriBuilder.Uri); + global::System.Net.Http.HttpRequestMessage request = new global::System.Net.Http.HttpRequestMessage(this.RequestMethod, uriBuilder.Uri); {%- for header in header_parameters -%} {% capture header_append %} From 5b2990caed804efa649d40e983eb9e4f15203e5e Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 15:02:34 +0000 Subject: [PATCH 023/151] change to use appendformat --- .../Generators/Templates/ApiRequest.liquid | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid index 28334d7..c748add 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid @@ -19,7 +19,7 @@ namespace {{ Namespace }} {% when 1 %} global::DragonFruit.Data.Requests.EnumerableConverter.AppendEnumerable(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ query.options }}, "{{ query.parameter_name }}", "{{ query.separator }}"); {% else %} - builder.Append($"{{ query.parameter_name }}={global::System.Uri.EscapeDataString({{ query.accessor }}.ToString())}&"); + builder.AppendFormat("{0}={1}&", "{{ query.parameter_name }}", global::System.Uri.EscapeDataString({{ query.accessor }}.ToString()); {% endcase %} {% endcapture %} From 04bc90f662bd850cd4e216f2e186a78e0d9083b1 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 15:04:53 +0000 Subject: [PATCH 024/151] fix bad names --- .../Generators/Templates/ApiRequest.liquid | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid index c748add..f9685f2 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid @@ -4,9 +4,9 @@ using System.Net.Http; using DragonFruit.Data; using DragonFruit.Data.Requests; -namespace {{ Namespace }} +namespace {{ namespace }} { - partial class {{ ClassName }} : global::DragonFruit.Data.Requests.IRequestBuilder + partial class {{ class_name }} : global::DragonFruit.Data.Requests.IRequestBuilder { public global::System.Net.Http.HttpRequestMessage BuildRequest() { From cfe0763d75db3e55135a19b95de49b81c3360727 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 15:51:50 +0000 Subject: [PATCH 025/151] add enum processing --- src/Requests/EnumConverter.cs | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/Requests/EnumConverter.cs diff --git a/src/Requests/EnumConverter.cs b/src/Requests/EnumConverter.cs new file mode 100644 index 0000000..8cc536d --- /dev/null +++ b/src/Requests/EnumConverter.cs @@ -0,0 +1,36 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System; +using System.Globalization; +using System.Text; + +namespace DragonFruit.Data.Requests +{ + public static class EnumConverter + { + public static void AppendEnum(StringBuilder destination, T value, EnumOption mode, string propertyName) where T : Enum + { + destination.AppendFormat("{0}={1}&", propertyName, GetEnumValue(value, mode)); + } + + public static string GetEnumValue(T value, EnumOption mode) where T : Enum + { + switch (mode) + { + case EnumOption.Numeric: + return Convert.ToInt32(value, NumberFormatInfo.InvariantInfo).ToString(); + + case EnumOption.StringLower: + return value.ToString().ToLowerInvariant(); + + case EnumOption.StringUpper: + return value.ToString().ToUpperInvariant(); + + case EnumOption.None: + default: + return value.ToString(); + } + } + } +} From 8a7663450c87460f275d3e9a2a995b9e11878fca Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 15:51:57 +0000 Subject: [PATCH 026/151] add switch braces --- src/Requests/EnumerableConverter.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Requests/EnumerableConverter.cs b/src/Requests/EnumerableConverter.cs index fd51d3d..4de9e3f 100644 --- a/src/Requests/EnumerableConverter.cs +++ b/src/Requests/EnumerableConverter.cs @@ -23,14 +23,17 @@ public static void WriteEnumerable(StringBuilder destination, IEnumerable source switch (mode) { case EnumerableOption.Recursive: + { foreach (var item in source) { destination.Append($"{propertyName}={Uri.EscapeDataString(item.ToString())}&"); } break; + } case EnumerableOption.Indexed: + { var counter = 0; foreach (var item in source) @@ -39,18 +42,23 @@ public static void WriteEnumerable(StringBuilder destination, IEnumerable source } break; + } case EnumerableOption.Unordered: + { foreach (var item in source) { destination.AppendFormat("{0}[]={1}&", propertyName, Uri.EscapeDataString(item.ToString())); } break; + } default: + { destination.AppendFormat("{0}={1}&", propertyName, string.Join(separator, source.Cast().Select(x => Uri.EscapeDataString(x.ToString())))); break; + } } } } From 27bff0f96f84254f1a5b195df23ca15cb82c91de Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 15:52:16 +0000 Subject: [PATCH 027/151] add enum handling for queries --- .../Generators/Templates/ApiRequest.liquid | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid index f9685f2..9e9158a 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid @@ -18,6 +18,8 @@ namespace {{ namespace }} {% case query.type %} {% when 1 %} global::DragonFruit.Data.Requests.EnumerableConverter.AppendEnumerable(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ query.options }}, "{{ query.parameter_name }}", "{{ query.separator }}"); + {% when 2 %} + global::DragonFruit.Data.Requests.EnumConverter.AppendEnum(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ query.options }}, "{{ query.parameter_name }}"); {% else %} builder.AppendFormat("{0}={1}&", "{{ query.parameter_name }}", global::System.Uri.EscapeDataString({{ query.accessor }}.ToString()); {% endcase %} From fa87441c2c12d4ae71576153275f1b98d9743ae8 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 15:58:21 +0000 Subject: [PATCH 028/151] add more checks to request properties --- .../Generators/ApiRequestSourceGenerator.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs index f267f0b..96c3adc 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs @@ -60,6 +60,7 @@ private static ClassDeclarationSyntax GetSemanticTarget(GeneratorSyntaxContext c var model = context.SemanticModel; var classDeclaration = (ClassDeclarationSyntax)context.Node; + var apiRequestSymbol = context.SemanticModel.Compilation.GetTypeByMetadataName(typeof(ApiRequest).FullName); var classSymbol = ModelExtensions.GetDeclaredSymbol(model, classDeclaration) as INamedTypeSymbol; // ensure the class isn't abstract @@ -70,7 +71,7 @@ private static ClassDeclarationSyntax GetSemanticTarget(GeneratorSyntaxContext c while (classSymbol != null) { - if (classSymbol.ToString() == "DragonFruit.Data.ApiRequest") + if (classSymbol.Equals(apiRequestSymbol, SymbolEqualityComparer.Default)) { return classDeclaration; } @@ -141,6 +142,13 @@ private static IReadOnlyDictionary> continue; } + // only allow public, protected and protected internal properties + if (candidate.DeclaredAccessibility is Accessibility.Private or Accessibility.Internal) + { + // todo diagnostic warning + continue; + } + var returnType = candidate switch { IPropertySymbol propertySymbol => propertySymbol.Type, @@ -149,6 +157,7 @@ private static IReadOnlyDictionary> _ => throw new NotSupportedException() }; + // if a method is declared, ensure it's not void if (returnType.SpecialType == SpecialType.System_Void) { // todo return diagnostic warning From 580ef6af28c1a822b66eb3212b1d83b3205b3a17 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 17:41:26 +0000 Subject: [PATCH 029/151] add body type enum and attribute --- src/Requests/FormBodyType.cs | 11 +++++++++++ src/Requests/FormBodyTypeAttribute.cs | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/Requests/FormBodyType.cs create mode 100644 src/Requests/FormBodyTypeAttribute.cs diff --git a/src/Requests/FormBodyType.cs b/src/Requests/FormBodyType.cs new file mode 100644 index 0000000..da4f955 --- /dev/null +++ b/src/Requests/FormBodyType.cs @@ -0,0 +1,11 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +namespace DragonFruit.Data.Requests +{ + public enum FormBodyType + { + UriEncoded, + MultipartForm + } +} diff --git a/src/Requests/FormBodyTypeAttribute.cs b/src/Requests/FormBodyTypeAttribute.cs new file mode 100644 index 0000000..7fa0ee6 --- /dev/null +++ b/src/Requests/FormBodyTypeAttribute.cs @@ -0,0 +1,18 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System; + +namespace DragonFruit.Data.Requests +{ + [AttributeUsage(AttributeTargets.Class)] + public class FormBodyTypeAttribute : Attribute + { + public FormBodyTypeAttribute(FormBodyType bodyType) + { + BodyType = bodyType; + } + + public FormBodyType BodyType { get; } + } +} From 44f66888d15132dcc355330c8b8e0a9a94fd30e7 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 17:41:58 +0000 Subject: [PATCH 030/151] move liquid template, change option types to strings --- .../ApiRequestSourceGenerator.cs | 85 +++++++++++-------- .../DragonFruit.Data.Roslyn.csproj | 1 + .../Enums/RequestSymbolType.cs | 12 +++ .../{Generators => }/RequestSymbolMetadata.cs | 18 ++-- .../Templates/ApiRequest.liquid | 4 +- 5 files changed, 71 insertions(+), 49 deletions(-) rename DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/{Generators => }/ApiRequestSourceGenerator.cs (89%) create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Enums/RequestSymbolType.cs rename DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/{Generators => }/RequestSymbolMetadata.cs (77%) rename DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/{Generators => }/Templates/ApiRequest.liquid (93%) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs similarity index 89% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs rename to DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 96c3adc..5dd2ebe 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -14,7 +14,7 @@ using Microsoft.CodeAnalysis.Text; using Scriban; -namespace DragonFruit.Data.Roslyn.Generators +namespace DragonFruit.Data.Roslyn { [Generator(LanguageNames.CSharp)] public class ApiRequestSourceGenerator : IIncrementalGenerator @@ -34,7 +34,7 @@ public class ApiRequestSourceGenerator : IIncrementalGenerator static ApiRequestSourceGenerator() { - using var templateStream = typeof(ApiRequestSourceGenerator).Assembly.GetManifestResourceStream("DragonFruit.Data.Roslyn.Generators.Templates.ApiRequest.liquid"); + using var templateStream = typeof(ApiRequestSourceGenerator).Assembly.GetManifestResourceStream("DragonFruit.Data.Roslyn.Templates.ApiRequest.liquid"); if (templateStream == null) { @@ -55,33 +55,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput(targets, static (spc, source) => Execute(source.Item1, source.Item2, spc)); } - private static ClassDeclarationSyntax GetSemanticTarget(GeneratorSyntaxContext context) - { - var model = context.SemanticModel; - var classDeclaration = (ClassDeclarationSyntax)context.Node; - - var apiRequestSymbol = context.SemanticModel.Compilation.GetTypeByMetadataName(typeof(ApiRequest).FullName); - var classSymbol = ModelExtensions.GetDeclaredSymbol(model, classDeclaration) as INamedTypeSymbol; - - // ensure the class isn't abstract - if (classSymbol?.IsAbstract != false) - { - return null; - } - - while (classSymbol != null) - { - if (classSymbol.Equals(apiRequestSymbol, SymbolEqualityComparer.Default)) - { - return classDeclaration; - } - - classSymbol = classSymbol.BaseType; - } - - return null; - } - private static void Execute(Compilation compilation, ImmutableArray requestClasses, SourceProductionContext context) { if (requestClasses.IsDefaultOrEmpty) @@ -95,14 +68,17 @@ private static void Execute(Compilation compilation, ImmutableArray"); - var parameters = GetRequestSymbolMetadata(compilation, classSymbol); + var parameters = GetRequestSymbolMetadata(compilation, classSymbol, out var formBodyType); var parameterInfo = new { - Namespace = classSymbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), ClassName = classSymbol.Name, - HeaderParameters = parameters[ParameterType.Header], + Namespace = classSymbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + + FormBodyType = formBodyType, + QueryParameters = parameters[ParameterType.Query], + HeaderParameters = parameters[ParameterType.Header] // todo add form body content }; @@ -113,7 +89,34 @@ private static void Execute(Compilation compilation, ImmutableArray> GetRequestSymbolMetadata(Compilation compilation, INamedTypeSymbol symbol) + private static ClassDeclarationSyntax GetSemanticTarget(GeneratorSyntaxContext context) + { + var model = context.SemanticModel; + var classDeclaration = (ClassDeclarationSyntax)context.Node; + + var apiRequestSymbol = context.SemanticModel.Compilation.GetTypeByMetadataName(typeof(ApiRequest).FullName); + var classSymbol = ModelExtensions.GetDeclaredSymbol(model, classDeclaration) as INamedTypeSymbol; + + // ensure the class isn't abstract + if (classSymbol?.IsAbstract != false) + { + return null; + } + + while (classSymbol != null) + { + if (classSymbol.Equals(apiRequestSymbol, SymbolEqualityComparer.Default)) + { + return classDeclaration; + } + + classSymbol = classSymbol.BaseType; + } + + return null; + } + + private static IReadOnlyDictionary> GetRequestSymbolMetadata(Compilation compilation, INamedTypeSymbol symbol, out FormBodyType? formBodyType) { var symbols = Enum.GetValues(typeof(ParameterType)).Cast().ToDictionary(x => x, _ => (IList)new List()); @@ -121,6 +124,7 @@ private static IReadOnlyDictionary> var enumerableParameterAttribute = compilation.GetTypeByMetadataName(typeof(EnumerableOptionsAttribute).FullName); var requestParameterAttribute = compilation.GetTypeByMetadataName(typeof(RequestParameterAttribute).FullName); var enumParameterAttribute = compilation.GetTypeByMetadataName(typeof(EnumOptionsAttribute).FullName); + var formBodyTypeAttribute = compilation.GetTypeByMetadataName(typeof(FormBodyTypeAttribute).FullName); var enumerableTypeSymbol = compilation.GetTypeByMetadataName(typeof(IEnumerable).FullName); var apiRequestBaseType = compilation.GetTypeByMetadataName(typeof(ApiRequest).FullName); @@ -130,8 +134,19 @@ private static IReadOnlyDictionary> var currentSymbol = symbol; var consumedProperties = new HashSet(); + formBodyType = null; + do { + // check for body type attribute + var formBodyInfo = currentSymbol.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(formBodyTypeAttribute, SymbolEqualityComparer.Default) == true); + + if (formBodyInfo != null) + { + formBodyType = (FormBodyType)formBodyInfo.ConstructorArguments[0].Value!; + } + + // locate and add symbol metadata foreach (var candidate in currentSymbol.GetMembers().Where(x => x is IPropertySymbol or IMethodSymbol { Parameters.Length: 0 })) { var requestAttribute = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(requestParameterAttribute, SymbolEqualityComparer.Default) == true); @@ -176,7 +191,7 @@ private static IReadOnlyDictionary> metadata = new EnumRequestSymbolMetadata { - EnumOption = (EnumOption?)enumOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumOption.None + EnumOption = ((EnumOption?)enumOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumOption.None).ToString() }; } // handle arrays/IEnumerable @@ -187,7 +202,7 @@ private static IReadOnlyDictionary> metadata = new EnumerableRequestSymbolMetadata { Separator = (string)enumerableOptions?.ConstructorArguments.ElementAtOrDefault(1).Value ?? ",", - EnumerableOption = (EnumerableOption?)enumerableOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumerableOption.Concatenated + EnumerableOption = ((EnumerableOption?)enumerableOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumerableOption.Concatenated).ToString() }; } else diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj index 45f909e..ce113fa 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj @@ -25,6 +25,7 @@ + diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Enums/RequestSymbolType.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Enums/RequestSymbolType.cs new file mode 100644 index 0000000..a15edaf --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Enums/RequestSymbolType.cs @@ -0,0 +1,12 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +namespace DragonFruit.Data.Roslyn.Enums +{ + internal enum RequestSymbolType + { + Standard = 0, + Enumerable = 1, + Enum = 2, + } +} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbolMetadata.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/RequestSymbolMetadata.cs similarity index 77% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbolMetadata.cs rename to DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/RequestSymbolMetadata.cs index 9e3288b..3ec8d2a 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/RequestSymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/RequestSymbolMetadata.cs @@ -1,13 +1,15 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details -using DragonFruit.Data.Requests; +using DragonFruit.Data.Roslyn.Enums; using Microsoft.CodeAnalysis; -namespace DragonFruit.Data.Roslyn.Generators +namespace DragonFruit.Data.Roslyn { internal class RequestSymbolMetadata { + public virtual RequestSymbolType Type => RequestSymbolType.Standard; + public int Depth { get; set; } public bool IsString { get; set; } @@ -16,7 +18,6 @@ internal class RequestSymbolMetadata public ISymbol Symbol { get; set; } public string ParameterName { get; set; } - public virtual RequestSymbolType Type => RequestSymbolType.Standard; public string Accessor => Symbol is IPropertySymbol ps ? $"this.{ps.Name}" : $"this.{Symbol.Name}()"; } @@ -24,7 +25,7 @@ internal class EnumRequestSymbolMetadata : RequestSymbolMetadata { public override RequestSymbolType Type => RequestSymbolType.Enum; - public EnumOption EnumOption { get; set; } + public string EnumOption { get; set; } } internal class EnumerableRequestSymbolMetadata : RequestSymbolMetadata @@ -32,13 +33,6 @@ internal class EnumerableRequestSymbolMetadata : RequestSymbolMetadata public override RequestSymbolType Type => RequestSymbolType.Enumerable; public string Separator { get; set; } - public EnumerableOption EnumerableOption { get; set; } - } - - internal enum RequestSymbolType - { - Standard = 0, - Enumerable = 1, - Enum = 2, + public string EnumerableOption { get; set; } } } diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid similarity index 93% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid rename to DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid index 9e9158a..88b30e8 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Generators/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid @@ -17,9 +17,9 @@ namespace {{ namespace }} {% capture query_append %} {% case query.type %} {% when 1 %} - global::DragonFruit.Data.Requests.EnumerableConverter.AppendEnumerable(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ query.options }}, "{{ query.parameter_name }}", "{{ query.separator }}"); + global::DragonFruit.Data.Requests.EnumerableConverter.AppendEnumerable(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ query.enum_option }}, "{{ query.parameter_name }}", "{{ query.separator }}"); {% when 2 %} - global::DragonFruit.Data.Requests.EnumConverter.AppendEnum(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ query.options }}, "{{ query.parameter_name }}"); + global::DragonFruit.Data.Requests.EnumConverter.AppendEnum(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ query.enumerable_option }}, "{{ query.parameter_name }}"); {% else %} builder.AppendFormat("{0}={1}&", "{{ query.parameter_name }}", global::System.Uri.EscapeDataString({{ query.accessor }}.ToString()); {% endcase %} From dac63be631b9c42bf104639e2a39ac114b47770a Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 17:42:35 +0000 Subject: [PATCH 031/151] fix typo --- .../DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 5dd2ebe..59295f7 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -151,7 +151,7 @@ private static IReadOnlyDictionary> { var requestAttribute = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(requestParameterAttribute, SymbolEqualityComparer.Default) == true); - // ensure that properties ovewritten using "new" are not processed twice + // ensure properties overwritten using "new" are not processed twice if (requestAttribute == null || !consumedProperties.Add(candidate.MetadataName)) { continue; From 6af4e569dac902bb0c899ff3c6fd71670da0fc1e Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 17:46:39 +0000 Subject: [PATCH 032/151] formatting fixes --- .../DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 59295f7..36f1692 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -188,21 +188,23 @@ private static IReadOnlyDictionary> if (returnType.TypeKind == TypeKind.Enum) { var enumOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumParameterAttribute, SymbolEqualityComparer.Default) == true); + var enumType = (EnumOption?)enumOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumOption.None; metadata = new EnumRequestSymbolMetadata { - EnumOption = ((EnumOption?)enumOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumOption.None).ToString() + EnumOption = enumType.ToString() }; } // handle arrays/IEnumerable else if (SupportedCollectionTypes.Contains(returnType.SpecialType) || returnType.FindImplementationForInterfaceMember(enumerableTypeSymbol) != null) { var enumerableOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumerableParameterAttribute, SymbolEqualityComparer.Default) == true); + var enumerableType = (EnumerableOption?)enumerableOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumerableOption.Concatenated; metadata = new EnumerableRequestSymbolMetadata { Separator = (string)enumerableOptions?.ConstructorArguments.ElementAtOrDefault(1).Value ?? ",", - EnumerableOption = ((EnumerableOption?)enumerableOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumerableOption.Concatenated).ToString() + EnumerableOption = enumerableType.ToString() }; } else From e5c501b3b8738490a2b21e272687f7d75b7d2872 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 18:05:56 +0000 Subject: [PATCH 033/151] add case for header-type --- .../DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid index 88b30e8..50ac84a 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid @@ -45,7 +45,11 @@ namespace {{ namespace }} {%- for header in header_parameters -%} {% capture header_append %} - request.Headers.Add("{{ header.parameter_name }}", {{ header.accessor }}.ToString()); + {% case header.type %} + {% when 1 %} + {% else %} + request.Headers.Add("{{ header.parameter_name }}", {{ header.accessor }}.ToString()); + {% endcase %} {% endcapture %} {% if header.nullable %} From c6af00a1f634eaf96bd31ff4a04fc4435d838176 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 18:21:02 +0000 Subject: [PATCH 034/151] force ints --- .../DragonFruit.Data.Roslyn/RequestSymbolMetadata.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/RequestSymbolMetadata.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/RequestSymbolMetadata.cs index 3ec8d2a..ade206c 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/RequestSymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/RequestSymbolMetadata.cs @@ -8,7 +8,7 @@ namespace DragonFruit.Data.Roslyn { internal class RequestSymbolMetadata { - public virtual RequestSymbolType Type => RequestSymbolType.Standard; + public virtual int Type => (int)RequestSymbolType.Standard; public int Depth { get; set; } @@ -23,14 +23,14 @@ internal class RequestSymbolMetadata internal class EnumRequestSymbolMetadata : RequestSymbolMetadata { - public override RequestSymbolType Type => RequestSymbolType.Enum; + public override int Type => (int)RequestSymbolType.Enum; public string EnumOption { get; set; } } internal class EnumerableRequestSymbolMetadata : RequestSymbolMetadata { - public override RequestSymbolType Type => RequestSymbolType.Enumerable; + public override int Type => (int)RequestSymbolType.Enumerable; public string Separator { get; set; } public string EnumerableOption { get; set; } From a9da1662aa59edbea9c8c6aeac226b7a45c20230 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 18:21:46 +0000 Subject: [PATCH 035/151] add getpair to enumerableconverter --- src/Requests/EnumerableConverter.cs | 53 +++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/Requests/EnumerableConverter.cs b/src/Requests/EnumerableConverter.cs index 4de9e3f..e10a66b 100644 --- a/src/Requests/EnumerableConverter.cs +++ b/src/Requests/EnumerableConverter.cs @@ -3,6 +3,7 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Linq; using System.Text; @@ -61,5 +62,57 @@ public static void WriteEnumerable(StringBuilder destination, IEnumerable source } } } + + /// + /// Produces a collection of from the provided using the specified + /// + /// The to derive pairs from + /// The to use + /// The name of the property to use + /// The separator to use, if is set to Concatenated + public static IEnumerable> GetPairs(IEnumerable source, EnumerableOption mode, string propertyName, string separator) + { + switch (mode) + { + case EnumerableOption.Recursive: + { + foreach (var item in source) + { + yield return new KeyValuePair(propertyName, Uri.EscapeDataString(item.ToString())); + } + + break; + } + + case EnumerableOption.Indexed: + { + var counter = 0; + + foreach (var item in source) + { + yield return new KeyValuePair($"{propertyName}[{counter++}]", Uri.EscapeDataString(item.ToString())); + } + + break; + } + + case EnumerableOption.Unordered: + { + foreach (var item in source) + { + yield return new KeyValuePair($"{propertyName}[]", Uri.EscapeDataString(item.ToString())); + } + + break; + } + + default: + { + yield return new KeyValuePair(propertyName, string.Join(separator, source.Cast().Select(x => Uri.EscapeDataString(x.ToString())))); + + break; + } + } + } } } From 42b630ad1f67e9ebf034813562f525ec13b327cf Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 18:22:36 +0000 Subject: [PATCH 036/151] add missing cases for headers --- .../DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid index 50ac84a..dcc2d0e 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid @@ -47,6 +47,12 @@ namespace {{ namespace }} {% capture header_append %} {% case header.type %} {% when 1 %} + foreach (var kvp in global::DragonFruit.Data.Requests.EnumerableConverter.GetPairs({{ header.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ header.enumumerable_option }})) + { + request.Headers.Add(kvp.Key, kvp.Value); + } + {% when 2 %} + request.Headers.Add("{{ header.parameter_name }}", global::DragonFruit.Data.Requests.EnumConverter.GetString({{ header.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ header.enum_option }})); {% else %} request.Headers.Add("{{ header.parameter_name }}", {{ header.accessor }}.ToString()); {% endcase %} From 8f4fdcdee99c9cb41ce52a4700b2d02049bc9a4c Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 18:25:46 +0000 Subject: [PATCH 037/151] prevent nested classes --- .../DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 36f1692..2a55c59 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -103,6 +103,12 @@ private static ClassDeclarationSyntax GetSemanticTarget(GeneratorSyntaxContext c return null; } + // don't allow processing nested classes + if (classSymbol.ContainingType != null) + { + return null; + } + while (classSymbol != null) { if (classSymbol.Equals(apiRequestSymbol, SymbolEqualityComparer.Default)) From 1d5516e82b921cf1d9e9756ae84e2382c81553c7 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 20:15:39 +0000 Subject: [PATCH 038/151] split metadata --- .../ApiRequestSourceGenerator.cs | 50 ++++++++++--------- .../Entities/EnumSymbolMetadata.cs | 20 ++++++++ .../Entities/EnumerableSymbolMetadata.cs | 23 +++++++++ .../Entities/PropertySymbolMetadata.cs | 21 ++++++++ .../Entities/RequestSymbolMetadata.cs | 17 +++++++ .../Entities/SymbolMetadata.cs | 25 ++++++++++ .../RequestSymbolMetadata.cs | 38 -------------- src/Requests/FormBodyType.cs | 4 +- 8 files changed, 135 insertions(+), 63 deletions(-) create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/RequestSymbolMetadata.cs create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/SymbolMetadata.cs delete mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/RequestSymbolMetadata.cs diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 2a55c59..04231e7 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text; using DragonFruit.Data.Requests; +using DragonFruit.Data.Roslyn.Entities; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -68,19 +69,20 @@ private static void Execute(Compilation compilation, ImmutableArray"); - var parameters = GetRequestSymbolMetadata(compilation, classSymbol, out var formBodyType); + var metadata = GetRequestSymbolMetadata(compilation, classSymbol); var parameterInfo = new { ClassName = classSymbol.Name, Namespace = classSymbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - FormBodyType = formBodyType, + FormBodyType = (int?)metadata.FormBodyType ?? 0, - QueryParameters = parameters[ParameterType.Query], - HeaderParameters = parameters[ParameterType.Header] + RequestBodySymbol = metadata.BodyProperty, + FormBodyParameters = metadata.Properties[ParameterType.Form], - // todo add form body content + QueryParameters = metadata.Properties[ParameterType.Query], + HeaderParameters = metadata.Properties[ParameterType.Header], }; sourceBuilder.Append("\n\n"); @@ -122,14 +124,18 @@ private static ClassDeclarationSyntax GetSemanticTarget(GeneratorSyntaxContext c return null; } - private static IReadOnlyDictionary> GetRequestSymbolMetadata(Compilation compilation, INamedTypeSymbol symbol, out FormBodyType? formBodyType) + private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compilation, INamedTypeSymbol symbol) { - var symbols = Enum.GetValues(typeof(ParameterType)).Cast().ToDictionary(x => x, _ => (IList)new List()); + var metadata = new RequestSymbolMetadata + { + Properties = Enum.GetValues(typeof(ParameterType)).Cast().ToDictionary(x => x, _ => (IList)new List()) + }; // get types used in member processing var enumerableParameterAttribute = compilation.GetTypeByMetadataName(typeof(EnumerableOptionsAttribute).FullName); var requestParameterAttribute = compilation.GetTypeByMetadataName(typeof(RequestParameterAttribute).FullName); var enumParameterAttribute = compilation.GetTypeByMetadataName(typeof(EnumOptionsAttribute).FullName); + var requestBodyAttribute = compilation.GetTypeByMetadataName(typeof(RequestBodyAttribute).FullName); var formBodyTypeAttribute = compilation.GetTypeByMetadataName(typeof(FormBodyTypeAttribute).FullName); var enumerableTypeSymbol = compilation.GetTypeByMetadataName(typeof(IEnumerable).FullName); @@ -140,8 +146,6 @@ private static IReadOnlyDictionary> var currentSymbol = symbol; var consumedProperties = new HashSet(); - formBodyType = null; - do { // check for body type attribute @@ -149,7 +153,7 @@ private static IReadOnlyDictionary> if (formBodyInfo != null) { - formBodyType = (FormBodyType)formBodyInfo.ConstructorArguments[0].Value!; + metadata.FormBodyType ??= (FormBodyType)formBodyInfo.ConstructorArguments[0].Value!; } // locate and add symbol metadata @@ -185,10 +189,16 @@ private static IReadOnlyDictionary> continue; } + // check if value is decorated with RequestBodyAttribute + if (metadata.BodyProperty != null && candidate.GetAttributes().Any(x => x.AttributeClass?.Equals(requestBodyAttribute, SymbolEqualityComparer.Default) == true)) + { + metadata.BodyProperty = new SymbolMetadata(symbol, returnType); + } + var parameterType = (ParameterType)requestAttribute.ConstructorArguments[0].Value!; var parameterName = (string)requestAttribute.ConstructorArguments.ElementAtOrDefault(1).Value ?? candidate.Name; - RequestSymbolMetadata metadata; + SymbolMetadata symbolMetadata; // handle enums if (returnType.TypeKind == TypeKind.Enum) @@ -196,7 +206,7 @@ private static IReadOnlyDictionary> var enumOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumParameterAttribute, SymbolEqualityComparer.Default) == true); var enumType = (EnumOption?)enumOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumOption.None; - metadata = new EnumRequestSymbolMetadata + symbolMetadata = new EnumSymbolMetadata(symbol, returnType, parameterName) { EnumOption = enumType.ToString() }; @@ -207,7 +217,7 @@ private static IReadOnlyDictionary> var enumerableOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumerableParameterAttribute, SymbolEqualityComparer.Default) == true); var enumerableType = (EnumerableOption?)enumerableOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumerableOption.Concatenated; - metadata = new EnumerableRequestSymbolMetadata + symbolMetadata = new EnumerableSymbolMetadata(symbol, returnType, parameterName) { Separator = (string)enumerableOptions?.ConstructorArguments.ElementAtOrDefault(1).Value ?? ",", EnumerableOption = enumerableType.ToString() @@ -215,18 +225,12 @@ private static IReadOnlyDictionary> } else { - metadata = new RequestSymbolMetadata - { - IsString = returnType.SpecialType == SpecialType.System_String - }; + symbolMetadata = new PropertySymbolMetadata(symbol, returnType, parameterName); } - metadata.Depth = depth; - metadata.Symbol = candidate; - metadata.ParameterName = parameterName; - metadata.Nullable = returnType.IsReferenceType || (returnType.IsValueType && returnType.NullableAnnotation == NullableAnnotation.Annotated); + symbolMetadata.Depth = depth; - symbols[parameterType].Add(metadata); + metadata.Properties[parameterType].Add(symbolMetadata); } // get derived class from currentSymbol @@ -234,7 +238,7 @@ private static IReadOnlyDictionary> depth++; } while (currentSymbol?.Equals(apiRequestBaseType, SymbolEqualityComparer.Default) == false); - return symbols; + return metadata; } } } diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs new file mode 100644 index 0000000..98f93c9 --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs @@ -0,0 +1,20 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using DragonFruit.Data.Roslyn.Enums; +using Microsoft.CodeAnalysis; + +namespace DragonFruit.Data.Roslyn.Entities +{ + internal class EnumSymbolMetadata : PropertySymbolMetadata + { + public override int Type => (int)RequestSymbolType.Enum; + + public EnumSymbolMetadata(ISymbol symbol, ITypeSymbol returnType, string parameterName) + : base(symbol, returnType, parameterName) + { + } + + public string EnumOption { get; set; } + } +} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs new file mode 100644 index 0000000..17b6210 --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs @@ -0,0 +1,23 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using DragonFruit.Data.Roslyn.Enums; +using Microsoft.CodeAnalysis; + +namespace DragonFruit.Data.Roslyn.Entities +{ + internal class EnumerableSymbolMetadata : PropertySymbolMetadata + { + public override int Type => (int)RequestSymbolType.Enumerable; + + public EnumerableSymbolMetadata(ISymbol symbol, ITypeSymbol returnType, string parameterName) + : base(symbol, returnType, parameterName) + { + } + + public string Separator { get; set; } + public string EnumerableOption { get; set; } + + public bool IsByteArray => ReturnType is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte }; + } +} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs new file mode 100644 index 0000000..e62bc61 --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs @@ -0,0 +1,21 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using DragonFruit.Data.Roslyn.Enums; +using Microsoft.CodeAnalysis; + +namespace DragonFruit.Data.Roslyn.Entities +{ + internal class PropertySymbolMetadata : SymbolMetadata + { + public virtual int Type => (int)RequestSymbolType.Standard; + + public PropertySymbolMetadata(ISymbol symbol, ITypeSymbol returnType, string parameterName) + : base(symbol, returnType) + { + ParameterName = parameterName; + } + + public string ParameterName { get; } + } +} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/RequestSymbolMetadata.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/RequestSymbolMetadata.cs new file mode 100644 index 0000000..940a0ba --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/RequestSymbolMetadata.cs @@ -0,0 +1,17 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System.Collections.Generic; +using DragonFruit.Data.Requests; + +namespace DragonFruit.Data.Roslyn.Entities +{ + internal class RequestSymbolMetadata + { + public IReadOnlyDictionary> Properties { get; set; } + + public SymbolMetadata BodyProperty { get; set; } + + public FormBodyType? FormBodyType { get; set; } + } +} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/SymbolMetadata.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/SymbolMetadata.cs new file mode 100644 index 0000000..156f158 --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/SymbolMetadata.cs @@ -0,0 +1,25 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using Microsoft.CodeAnalysis; + +namespace DragonFruit.Data.Roslyn.Entities +{ + internal class SymbolMetadata + { + protected readonly ITypeSymbol ReturnType; + + public SymbolMetadata(ISymbol symbol, ITypeSymbol returnType) + { + ReturnType = returnType; + Symbol = symbol; + } + + public int Depth { get; set; } + + public ISymbol Symbol { get; } + + public string Accessor => Symbol is IPropertySymbol ps ? $"this.{ps.Name}" : $"this.{Symbol.Name}()"; + public bool Nullable => ReturnType.IsReferenceType || (ReturnType.IsValueType && ReturnType.NullableAnnotation == NullableAnnotation.Annotated); + } +} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/RequestSymbolMetadata.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/RequestSymbolMetadata.cs deleted file mode 100644 index ade206c..0000000 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/RequestSymbolMetadata.cs +++ /dev/null @@ -1,38 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using DragonFruit.Data.Roslyn.Enums; -using Microsoft.CodeAnalysis; - -namespace DragonFruit.Data.Roslyn -{ - internal class RequestSymbolMetadata - { - public virtual int Type => (int)RequestSymbolType.Standard; - - public int Depth { get; set; } - - public bool IsString { get; set; } - public bool Nullable { get; set; } - - public ISymbol Symbol { get; set; } - public string ParameterName { get; set; } - - public string Accessor => Symbol is IPropertySymbol ps ? $"this.{ps.Name}" : $"this.{Symbol.Name}()"; - } - - internal class EnumRequestSymbolMetadata : RequestSymbolMetadata - { - public override int Type => (int)RequestSymbolType.Enum; - - public string EnumOption { get; set; } - } - - internal class EnumerableRequestSymbolMetadata : RequestSymbolMetadata - { - public override int Type => (int)RequestSymbolType.Enumerable; - - public string Separator { get; set; } - public string EnumerableOption { get; set; } - } -} diff --git a/src/Requests/FormBodyType.cs b/src/Requests/FormBodyType.cs index da4f955..2d6d8a0 100644 --- a/src/Requests/FormBodyType.cs +++ b/src/Requests/FormBodyType.cs @@ -5,7 +5,7 @@ namespace DragonFruit.Data.Requests { public enum FormBodyType { - UriEncoded, - MultipartForm + UriEncoded = 1, + MultipartForm = 2 } } From 39cb0bef94940aca9c6d7652b05e02dab717114f Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 20:57:42 +0000 Subject: [PATCH 039/151] update symbol metadata --- .../Entities/SymbolMetadata.cs | 3 +-- .../Enums/RequestBodyType.cs | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Enums/RequestBodyType.cs diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/SymbolMetadata.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/SymbolMetadata.cs index 156f158..537c18f 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/SymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/SymbolMetadata.cs @@ -7,8 +7,6 @@ namespace DragonFruit.Data.Roslyn.Entities { internal class SymbolMetadata { - protected readonly ITypeSymbol ReturnType; - public SymbolMetadata(ISymbol symbol, ITypeSymbol returnType) { ReturnType = returnType; @@ -18,6 +16,7 @@ public SymbolMetadata(ISymbol symbol, ITypeSymbol returnType) public int Depth { get; set; } public ISymbol Symbol { get; } + public ITypeSymbol ReturnType { get; } public string Accessor => Symbol is IPropertySymbol ps ? $"this.{ps.Name}" : $"this.{Symbol.Name}()"; public bool Nullable => ReturnType.IsReferenceType || (ReturnType.IsValueType && ReturnType.NullableAnnotation == NullableAnnotation.Annotated); diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Enums/RequestBodyType.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Enums/RequestBodyType.cs new file mode 100644 index 0000000..badcdd1 --- /dev/null +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Enums/RequestBodyType.cs @@ -0,0 +1,15 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +namespace DragonFruit.Data.Roslyn.Enums +{ + internal enum RequestBodyType + { + None = 0, + + FormMultipart = 1, + FormUriEncoded = 2, + CustomBodyDirect = 3, + CustomBodySerialized = 4 + } +} From b8d3d879f319f8b2cd3adf7b2e76eee48a6e5a96 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 6 Dec 2023 20:58:27 +0000 Subject: [PATCH 040/151] update request body types --- .../ApiRequestSourceGenerator.cs | 42 +++++++++++++++++-- .../Templates/ApiRequest.liquid | 5 +++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 04231e7..8f12bc2 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -6,9 +6,11 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Net.Http; using System.Text; using DragonFruit.Data.Requests; using DragonFruit.Data.Roslyn.Entities; +using DragonFruit.Data.Roslyn.Enums; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -63,6 +65,8 @@ private static void Execute(Compilation compilation, ImmutableArray"); var metadata = GetRequestSymbolMetadata(compilation, classSymbol); + // check if body is derived from httpcontent + var requestBodyType = RequestBodyType.None; + + if (metadata.BodyProperty != null) + { + requestBodyType = DerivesFrom(metadata.BodyProperty.ReturnType, httpContentSymbol) ? RequestBodyType.CustomBodyDirect : RequestBodyType.CustomBodySerialized; + } + else if (metadata.FormBodyType == FormBodyType.UriEncoded) + { + requestBodyType = RequestBodyType.FormUriEncoded; + } + else if (metadata.FormBodyType == FormBodyType.MultipartForm) + { + requestBodyType = RequestBodyType.FormMultipart; + } + var parameterInfo = new { ClassName = classSymbol.Name, Namespace = classSymbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - FormBodyType = (int?)metadata.FormBodyType ?? 0, - + RequestBodyType = requestBodyType, RequestBodySymbol = metadata.BodyProperty, - FormBodyParameters = metadata.Properties[ParameterType.Form], QueryParameters = metadata.Properties[ParameterType.Query], HeaderParameters = metadata.Properties[ParameterType.Header], + FormBodyParameters = metadata.Properties[ParameterType.Form], }; sourceBuilder.Append("\n\n"); @@ -240,5 +259,22 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil return metadata; } + + private static bool DerivesFrom(ITypeSymbol type, ITypeSymbol baseType) + { + var classSymbol = type; + + while (classSymbol != null) + { + if (classSymbol.Equals(baseType, SymbolEqualityComparer.Default)) + { + return true; + } + + classSymbol = classSymbol.BaseType; + } + + return false; + } } } diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid index dcc2d0e..dfb0b06 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid @@ -43,6 +43,11 @@ namespace {{ namespace }} global::System.Net.Http.HttpRequestMessage request = new global::System.Net.Http.HttpRequestMessage(this.RequestMethod, uriBuilder.Uri); + {% case request_body_type %} + {% when 3 %} + request.Content = {{ request_body_symbol.accessor }}; + {% endcase %} + {%- for header in header_parameters -%} {% capture header_append %} {% case header.type %} From aeae0fa473058f0105a4509c52fb0d94593ae300 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Thu, 7 Dec 2023 11:38:36 +0000 Subject: [PATCH 041/151] Add special type handling --- .../ApiRequestSourceGenerator.cs | 23 +++++++++++++------ .../Entities/PropertySymbolMetadata.cs | 1 + .../Enums/SpecialRequestParameter.cs | 11 +++++++++ 3 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Enums/SpecialRequestParameter.cs diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 8f12bc2..1f0dfb2 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -5,6 +5,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; +using System.IO; using System.Linq; using System.Net.Http; using System.Text; @@ -82,13 +83,9 @@ private static void Execute(Compilation compilation, ImmutableArray Date: Thu, 7 Dec 2023 11:39:39 +0000 Subject: [PATCH 042/151] add form body processing --- .../Templates/ApiRequest.liquid | 135 ++++++++++++++---- 1 file changed, 106 insertions(+), 29 deletions(-) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid index dfb0b06..c78b7bb 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid @@ -11,44 +11,121 @@ namespace {{ namespace }} public global::System.Net.Http.HttpRequestMessage BuildRequest() { global::System.UriBuilder uriBuilder = new global::System.UriBuilder(RequestPath); - global::System.Text.StringBuilder queryBuilder = new global::System.Text.StringBuilder(); - - {%- for query in query_parameters -%} - {% capture query_append %} - {% case query.type %} - {% when 1 %} - global::DragonFruit.Data.Requests.EnumerableConverter.AppendEnumerable(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ query.enum_option }}, "{{ query.parameter_name }}", "{{ query.separator }}"); - {% when 2 %} - global::DragonFruit.Data.Requests.EnumConverter.AppendEnum(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ query.enumerable_option }}, "{{ query.parameter_name }}"); - {% else %} - builder.AppendFormat("{0}={1}&", "{{ query.parameter_name }}", global::System.Uri.EscapeDataString({{ query.accessor }}.ToString()); - {% endcase %} - {% endcapture %} - {% if query.nullable %} - if ({{ query.accessor }} != null) - { + {% comment %} Process Query Parameters {% endcomment %} + {% if query_parameters.size > 0 %} + global::System.Text.StringBuilder queryBuilder = new global::System.Text.StringBuilder(); + + {% for query in query_parameters %} + {% capture query_append %} + {% case query.type %} + {% when 1 %} + global::DragonFruit.Data.Requests.EnumerableConverter.AppendEnumerable(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ query.enumerable_option }}, "{{ query.parameter_name }}", "{{ query.separator }}"); + {% when 2 %} + global::DragonFruit.Data.Requests.EnumConverter.AppendEnum(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ query.enum_option }}, "{{ query.parameter_name }}"); + {% else %} + builder.AppendFormat("{0}={1}&", "{{ query.parameter_name }}", global::System.Uri.EscapeDataString({{ query.accessor }}.ToString())); + {% endcase %} + {% endcapture %} + + {% if query.nullable %} + if ({{ query.accessor }} != null) + { + {{ query_append }} + } + {% else %} {{ query_append }} - } - {% else %} - {{ query_append }} - {% endif %} - {% endfor -%} - - if (queryBuilder.Length > 0) - { - queryBuilder.Length--; // remove trailing & - uriBuilder.Query = queryBuilder.ToString(); - } + {% endif %} + {% endfor %} + + if (queryBuilder.Length > 0) + { + queryBuilder.Length--; // remove trailing & + uriBuilder.Query = queryBuilder.ToString(); + } + {% endif %} global::System.Net.Http.HttpRequestMessage request = new global::System.Net.Http.HttpRequestMessage(this.RequestMethod, uriBuilder.Uri); {% case request_body_type %} + {% comment %} 1 - Multipart {% endcomment %} + {% when 1 %} + global::System.Net.Http.MultipartContent content = new global::System.Net.Http.MultipartContent(); + + {% for multipart in form_body_parameters %} + {% capture multipart_append %} + {% case multipart.type %} + {% when 1 %} + foreach (var kvp in global::DragonFruit.Data.Requests.EnumerableConverter.GetPairs({{ multipart.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ multipart.enumerable_option }})) + { + content.Add(new global::System.Net.Http.StringContent(kvp.Value), "{{ multipart.parameter_name }}"); + } + {% when 2 %} + content.Add(new global::System.Net.Http.StringContent(global::DragonFruit.Data.Requests.EnumConverter.GetString({{ multipart.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ multipart.enum_option }})), "{{ multipart.parameter_name }}"); + {% else %} + {% case multipart.special_request_parameter %} + {% comment %} 0 - ByteArray {% endcomment %} + {% when 0 %} + content.Add(new global::System.Net.Http.ByteArrayContent({{ multipart.accessor }}), "{{ multipart.parameter_name }}"); + {% comment %} 1 - Stream {% endcomment %} + {% when 1 %} + content.Add(new global::System.Net.Http.StreamContent({{ multipart.accessor }}), "{{ multipart.parameter_name }}"); + {% comment %} Handle other types using ToString {% endcomment %} + {% else %} + content.Add(new global::System.Net.Http.StringContent({{ multipart.accessor }}), "{{ multipart.parameter_name }}"); + {% endcase %} + {% endcase %} + {% endcapture %} + + {% if multipart.nullable %} + if ({{ multipart.accessor }} != null) + { + {{ multipart_append }} + } + {% else %} + {{ multipart_append }} + {% endif %} + {% endfor %} + + request.Content = content; + + {% comment %} 2 - UriEncoded {% endcomment %} + {% when 2 %} + global::System.Text.StringBuilder formBuilder = new global::System.Text.StringBuilder(); + + {% for uriparam in form_body_parameters %} + {% capture uriparam_append %} + {% case uriparam.type %} + {% when 1 %} + global::DragonFruit.Data.Requests.EnumerableConverter.AppendEnumerable(formBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ query.enumerable_option }}, "{{ query.parameter_name }}", "{{ query.separator }}"); + {% when 2 %} + global::DragonFruit.Data.Requests.EnumConverter.AppendEnum(formBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ query.enum_option }}, "{{ query.parameter_name }}"); + {% else %} + formBuilder.AppendFormat("{0}={1}&", "{{ query.parameter_name }}", global::System.Uri.EscapeDataString({{ query.accessor }}.ToString())); + {% endcase %} + {% endcapture %} + + {% if uriparam.nullable %} + if ({{ uriparam.accessor }} != null) + { + {{ uriparam_append }} + } + {% else %} + {{ uriparam_append }} + {% endif %} + {% endfor %} + + request.Content = new global::System.Net.Http.StringContent(formBuilder.ToString(), global::System.Text.Encoding.UTF8, "application/x-www-form-urlencoded"); + + {% comment %} 3 - Custom Body (HttpContent) {% endcomment %} {% when 3 %} request.Content = {{ request_body_symbol.accessor }}; + + {% comment %} 4 - todo Custom Body (Serialized) {% endcomment %} {% endcase %} - {%- for header in header_parameters -%} + {% comment %} Process Headers {% endcomment %} + {% for header in header_parameters %} {% capture header_append %} {% case header.type %} {% when 1 %} @@ -71,7 +148,7 @@ namespace {{ namespace }} {% else %} {{ header_append }} {% endif %} - {% endfor -%} + {% endfor %} return request; } From fcb64ea15f224f231867d1c3f4d996efefcded3e Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Thu, 7 Dec 2023 12:12:45 +0000 Subject: [PATCH 043/151] add comment --- .../DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 1f0dfb2..e4ea0e9 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -127,6 +127,7 @@ private static ClassDeclarationSyntax GetSemanticTarget(GeneratorSyntaxContext c return null; } + // check if class is derived from ApiRequest while (classSymbol != null) { if (classSymbol.Equals(apiRequestSymbol, SymbolEqualityComparer.Default)) From 6f6bc4910e49229f2acef2ab172af7a52ff18392 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Thu, 7 Dec 2023 17:44:30 +0000 Subject: [PATCH 044/151] move all files --- .../ApiRequestAnalyzerTests.cs | 0 .../ApiRequestTemplateTests.cs | 30 +++++++++++++++++ .../DragonFruit.Data.Roslyn.Tests.csproj | 4 +-- .../TestEntities/BasicHttpRequest.cs | 21 ++++++++++++ .../Analyzers/ApiRequestClassAnalyzer.cs | 0 .../Analyzers/ApiRequestClassFixProvider.cs | 0 .../ApiRequestSourceGenerator.cs | 28 ++++++++-------- .../DragonFruit.Data.Roslyn.csproj | 12 +++---- .../Entities/EnumSymbolMetadata.cs | 0 .../Entities/EnumerableSymbolMetadata.cs | 0 .../Entities/PropertySymbolMetadata.cs | 0 .../Entities/RequestSymbolMetadata.cs | 0 .../Entities/SymbolMetadata.cs | 0 .../Enums/RequestBodyType.cs | 0 .../Enums/RequestSymbolType.cs | 0 .../Enums/SpecialRequestParameter.cs | 0 .../Templates/ApiRequest.liquid | 23 ++++++------- DragonFruit.Data.sln | 32 +++++++++---------- DragonFruit.Data.sln.DotSettings | 1 + {src => DragonFruit.Data}/ApiRequest.cs | 0 {src => DragonFruit.Data}/ApiSerializer.cs | 0 .../DragonFruit.Data.csproj | 2 +- .../Requests/EnumConverter.cs | 0 .../Requests/EnumOption.cs | 0 .../Requests/EnumOptionsAttribute.cs | 0 .../Requests/EnumerableConverter.cs | 0 .../Requests/EnumerableOption.cs | 0 .../Requests/EnumerableOptionsAttribute.cs | 0 .../Requests/FormBodyType.cs | 0 .../Requests/FormBodyTypeAttribute.cs | 0 .../Requests/IRequestBuilder.cs | 0 .../Requests/ParameterType.cs | 0 .../Requests/RequestBodyAttribute.cs | 0 .../Requests/RequestCallbackAttribute.cs | 0 .../Requests/RequestParameterAttribute.cs | 0 .../RequestParameterAttributeAliases.cs | 0 .../Serializers/ApiJsonSerializer.cs | 0 .../Serializers/ApiXmlSerializer.cs | 0 .../Serializers/IAsyncSerializer.cs | 0 res/DragonFruit.Data.props | 15 --------- 40 files changed, 101 insertions(+), 67 deletions(-) rename {DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.Tests => DragonFruit.Data.Roslyn.Tests}/ApiRequestAnalyzerTests.cs (100%) create mode 100644 DragonFruit.Data.Roslyn.Tests/ApiRequestTemplateTests.cs rename {DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.Tests => DragonFruit.Data.Roslyn.Tests}/DragonFruit.Data.Roslyn.Tests.csproj (93%) create mode 100644 DragonFruit.Data.Roslyn.Tests/TestEntities/BasicHttpRequest.cs rename DragonFruit.Data.Roslyn/{DragonFruit.Data.Roslyn => }/Analyzers/ApiRequestClassAnalyzer.cs (100%) rename DragonFruit.Data.Roslyn/{DragonFruit.Data.Roslyn => }/Analyzers/ApiRequestClassFixProvider.cs (100%) rename DragonFruit.Data.Roslyn/{DragonFruit.Data.Roslyn => }/ApiRequestSourceGenerator.cs (90%) rename DragonFruit.Data.Roslyn/{DragonFruit.Data.Roslyn => }/DragonFruit.Data.Roslyn.csproj (75%) rename DragonFruit.Data.Roslyn/{DragonFruit.Data.Roslyn => }/Entities/EnumSymbolMetadata.cs (100%) rename DragonFruit.Data.Roslyn/{DragonFruit.Data.Roslyn => }/Entities/EnumerableSymbolMetadata.cs (100%) rename DragonFruit.Data.Roslyn/{DragonFruit.Data.Roslyn => }/Entities/PropertySymbolMetadata.cs (100%) rename DragonFruit.Data.Roslyn/{DragonFruit.Data.Roslyn => }/Entities/RequestSymbolMetadata.cs (100%) rename DragonFruit.Data.Roslyn/{DragonFruit.Data.Roslyn => }/Entities/SymbolMetadata.cs (100%) rename DragonFruit.Data.Roslyn/{DragonFruit.Data.Roslyn => }/Enums/RequestBodyType.cs (100%) rename DragonFruit.Data.Roslyn/{DragonFruit.Data.Roslyn => }/Enums/RequestSymbolType.cs (100%) rename DragonFruit.Data.Roslyn/{DragonFruit.Data.Roslyn => }/Enums/SpecialRequestParameter.cs (100%) rename DragonFruit.Data.Roslyn/{DragonFruit.Data.Roslyn => }/Templates/ApiRequest.liquid (97%) rename {src => DragonFruit.Data}/ApiRequest.cs (100%) rename {src => DragonFruit.Data}/ApiSerializer.cs (100%) rename {src => DragonFruit.Data}/DragonFruit.Data.csproj (91%) rename {src => DragonFruit.Data}/Requests/EnumConverter.cs (100%) rename {src => DragonFruit.Data}/Requests/EnumOption.cs (100%) rename {src => DragonFruit.Data}/Requests/EnumOptionsAttribute.cs (100%) rename {src => DragonFruit.Data}/Requests/EnumerableConverter.cs (100%) rename {src => DragonFruit.Data}/Requests/EnumerableOption.cs (100%) rename {src => DragonFruit.Data}/Requests/EnumerableOptionsAttribute.cs (100%) rename {src => DragonFruit.Data}/Requests/FormBodyType.cs (100%) rename {src => DragonFruit.Data}/Requests/FormBodyTypeAttribute.cs (100%) rename {src => DragonFruit.Data}/Requests/IRequestBuilder.cs (100%) rename {src => DragonFruit.Data}/Requests/ParameterType.cs (100%) rename {src => DragonFruit.Data}/Requests/RequestBodyAttribute.cs (100%) rename {src => DragonFruit.Data}/Requests/RequestCallbackAttribute.cs (100%) rename {src => DragonFruit.Data}/Requests/RequestParameterAttribute.cs (100%) rename {src => DragonFruit.Data}/Requests/RequestParameterAttributeAliases.cs (100%) rename {src => DragonFruit.Data}/Serializers/ApiJsonSerializer.cs (100%) rename {src => DragonFruit.Data}/Serializers/ApiXmlSerializer.cs (100%) rename {src => DragonFruit.Data}/Serializers/IAsyncSerializer.cs (100%) delete mode 100644 res/DragonFruit.Data.props diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs b/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs similarity index 100% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs rename to DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs diff --git a/DragonFruit.Data.Roslyn.Tests/ApiRequestTemplateTests.cs b/DragonFruit.Data.Roslyn.Tests/ApiRequestTemplateTests.cs new file mode 100644 index 0000000..4146e07 --- /dev/null +++ b/DragonFruit.Data.Roslyn.Tests/ApiRequestTemplateTests.cs @@ -0,0 +1,30 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System.IO; +using System.Threading.Tasks; +using Scriban; +using Xunit; + +namespace DragonFruit.Data.Roslyn.Tests; + +public class ApiRequestTemplateTests +{ + [Fact] + public async Task TestTemplateParse() + { + var assembly = typeof(ApiRequestSourceGenerator).Assembly; + using var template = assembly.GetManifestResourceStream(ApiRequestSourceGenerator.TemplateName); + + Assert.NotNull(template); + + using var templateReader = new StreamReader(template); + var templateText = await templateReader.ReadToEndAsync().ConfigureAwait(false); + + Assert.True(templateText.Length > 0); + + var templateAst = Template.ParseLiquid(templateText); + + Assert.False(templateAst.HasErrors); + } +} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj b/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj similarity index 93% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj rename to DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj index b56cb22..5da0cb0 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj +++ b/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj @@ -2,8 +2,6 @@ net8.0 - enable - false @@ -19,7 +17,7 @@ - + diff --git a/DragonFruit.Data.Roslyn.Tests/TestEntities/BasicHttpRequest.cs b/DragonFruit.Data.Roslyn.Tests/TestEntities/BasicHttpRequest.cs new file mode 100644 index 0000000..719a512 --- /dev/null +++ b/DragonFruit.Data.Roslyn.Tests/TestEntities/BasicHttpRequest.cs @@ -0,0 +1,21 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using DragonFruit.Data.Requests; + +namespace DragonFruit.Data.Roslyn.Tests.TestEntities +{ + public partial class BasicHttpRequest : ApiRequest + { + public override string RequestPath => "https://postman-echo.com/get"; + + [RequestParameter(ParameterType.Query, "q1")] + public string Query1 { get; set; } + + [RequestParameter(ParameterType.Query, "q2")] + public string Query2 { get; set; } + + [RequestParameter(ParameterType.Query, "q3")] + public string Query3 { get; set; } + } +} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassAnalyzer.cs b/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassAnalyzer.cs similarity index 100% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassAnalyzer.cs rename to DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassAnalyzer.cs diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassFixProvider.cs b/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassFixProvider.cs similarity index 100% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassFixProvider.cs rename to DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassFixProvider.cs diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs similarity index 90% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs rename to DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index e4ea0e9..5688fb2 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -23,7 +23,9 @@ namespace DragonFruit.Data.Roslyn [Generator(LanguageNames.CSharp)] public class ApiRequestSourceGenerator : IIncrementalGenerator { - private static readonly Template RequestTemplate; + public static readonly string TemplateName = "DragonFruit.Data.Roslyn.Templates.ApiRequest.liquid"; + + private static readonly Template PartialRequestTemplate; private static readonly HashSet SupportedCollectionTypes = new(new[] { @@ -33,33 +35,33 @@ public class ApiRequestSourceGenerator : IIncrementalGenerator SpecialType.System_Collections_Generic_ICollection_T, SpecialType.System_Collections_Generic_IEnumerable_T, SpecialType.System_Collections_Generic_IReadOnlyList_T, - SpecialType.System_Collections_Generic_IReadOnlyCollection_T, + SpecialType.System_Collections_Generic_IReadOnlyCollection_T }); static ApiRequestSourceGenerator() { - using var templateStream = typeof(ApiRequestSourceGenerator).Assembly.GetManifestResourceStream("DragonFruit.Data.Roslyn.Templates.ApiRequest.liquid"); + using var templateStream = typeof(ApiRequestSourceGenerator).Assembly.GetManifestResourceStream(TemplateName); if (templateStream == null) { throw new NullReferenceException("Could not find template"); } - using var reader = new System.IO.StreamReader(templateStream); - RequestTemplate = Template.ParseLiquid(reader.ReadToEnd()); + using var reader = new StreamReader(templateStream); + PartialRequestTemplate = Template.ParseLiquid(reader.ReadToEnd()); } public void Initialize(IncrementalGeneratorInitializationContext context) { - var apiRequestDerivedClasses = context.SyntaxProvider.CreateSyntaxProvider( - predicate: (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax classDecl && classDecl.Modifiers.Any(x => x.IsKind(SyntaxKind.PartialKeyword)), - transform: (generatorSyntaxContext, _) => GetSemanticTarget(generatorSyntaxContext)); - - IncrementalValueProvider<(Compilation, ImmutableArray)> targets = context.CompilationProvider.Combine(apiRequestDerivedClasses.Collect()); - context.RegisterSourceOutput(targets, static (spc, source) => Execute(source.Item1, source.Item2, spc)); + // var apiRequestDerivedClasses = context.SyntaxProvider.CreateSyntaxProvider( + // predicate: (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax classDecl && classDecl.Modifiers.Any(x => x.IsKind(SyntaxKind.PartialKeyword)), + // transform: (generatorSyntaxContext, _) => GetSemanticTarget(generatorSyntaxContext)); + // + // IncrementalValueProvider<(Compilation, ImmutableArray)> targets = context.CompilationProvider.Combine(apiRequestDerivedClasses.Collect()); + // context.RegisterSourceOutput(targets, (spc, source) => Execute(source.Item1, source.Item2, spc)); } - private static void Execute(Compilation compilation, ImmutableArray requestClasses, SourceProductionContext context) + private void Execute(Compilation compilation, ImmutableArray requestClasses, SourceProductionContext context) { if (requestClasses.IsDefaultOrEmpty) { @@ -102,7 +104,7 @@ private static void Execute(Compilation compilation, ImmutableArray - + - - + + - - - - - + diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs b/DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs similarity index 100% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs rename to DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs b/DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs similarity index 100% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs rename to DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs b/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs similarity index 100% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs rename to DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/RequestSymbolMetadata.cs b/DragonFruit.Data.Roslyn/Entities/RequestSymbolMetadata.cs similarity index 100% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/RequestSymbolMetadata.cs rename to DragonFruit.Data.Roslyn/Entities/RequestSymbolMetadata.cs diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/SymbolMetadata.cs b/DragonFruit.Data.Roslyn/Entities/SymbolMetadata.cs similarity index 100% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Entities/SymbolMetadata.cs rename to DragonFruit.Data.Roslyn/Entities/SymbolMetadata.cs diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Enums/RequestBodyType.cs b/DragonFruit.Data.Roslyn/Enums/RequestBodyType.cs similarity index 100% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Enums/RequestBodyType.cs rename to DragonFruit.Data.Roslyn/Enums/RequestBodyType.cs diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Enums/RequestSymbolType.cs b/DragonFruit.Data.Roslyn/Enums/RequestSymbolType.cs similarity index 100% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Enums/RequestSymbolType.cs rename to DragonFruit.Data.Roslyn/Enums/RequestSymbolType.cs diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Enums/SpecialRequestParameter.cs b/DragonFruit.Data.Roslyn/Enums/SpecialRequestParameter.cs similarity index 100% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Enums/SpecialRequestParameter.cs rename to DragonFruit.Data.Roslyn/Enums/SpecialRequestParameter.cs diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid similarity index 97% rename from DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid rename to DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid index c78b7bb..3fb88c2 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid @@ -15,7 +15,7 @@ namespace {{ namespace }} {% comment %} Process Query Parameters {% endcomment %} {% if query_parameters.size > 0 %} global::System.Text.StringBuilder queryBuilder = new global::System.Text.StringBuilder(); - + {% for query in query_parameters %} {% capture query_append %} {% case query.type %} @@ -27,7 +27,7 @@ namespace {{ namespace }} builder.AppendFormat("{0}={1}&", "{{ query.parameter_name }}", global::System.Uri.EscapeDataString({{ query.accessor }}.ToString())); {% endcase %} {% endcapture %} - + {% if query.nullable %} if ({{ query.accessor }} != null) { @@ -37,10 +37,11 @@ namespace {{ namespace }} {{ query_append }} {% endif %} {% endfor %} - + + {% comment %} remove trailing & {% endcomment %} if (queryBuilder.Length > 0) { - queryBuilder.Length--; // remove trailing & + queryBuilder.Length--; uriBuilder.Query = queryBuilder.ToString(); } {% endif %} @@ -51,7 +52,7 @@ namespace {{ namespace }} {% comment %} 1 - Multipart {% endcomment %} {% when 1 %} global::System.Net.Http.MultipartContent content = new global::System.Net.Http.MultipartContent(); - + {% for multipart in form_body_parameters %} {% capture multipart_append %} {% case multipart.type %} @@ -76,7 +77,7 @@ namespace {{ namespace }} {% endcase %} {% endcase %} {% endcapture %} - + {% if multipart.nullable %} if ({{ multipart.accessor }} != null) { @@ -86,13 +87,13 @@ namespace {{ namespace }} {{ multipart_append }} {% endif %} {% endfor %} - + request.Content = content; {% comment %} 2 - UriEncoded {% endcomment %} {% when 2 %} global::System.Text.StringBuilder formBuilder = new global::System.Text.StringBuilder(); - + {% for uriparam in form_body_parameters %} {% capture uriparam_append %} {% case uriparam.type %} @@ -104,7 +105,7 @@ namespace {{ namespace }} formBuilder.AppendFormat("{0}={1}&", "{{ query.parameter_name }}", global::System.Uri.EscapeDataString({{ query.accessor }}.ToString())); {% endcase %} {% endcapture %} - + {% if uriparam.nullable %} if ({{ uriparam.accessor }} != null) { @@ -114,7 +115,7 @@ namespace {{ namespace }} {{ uriparam_append }} {% endif %} {% endfor %} - + request.Content = new global::System.Net.Http.StringContent(formBuilder.ToString(), global::System.Text.Encoding.UTF8, "application/x-www-form-urlencoded"); {% comment %} 3 - Custom Body (HttpContent) {% endcomment %} @@ -139,7 +140,7 @@ namespace {{ namespace }} request.Headers.Add("{{ header.parameter_name }}", {{ header.accessor }}.ToString()); {% endcase %} {% endcapture %} - + {% if header.nullable %} if ({{ header.accessor }} != null) { diff --git a/DragonFruit.Data.sln b/DragonFruit.Data.sln index e471fcf..34c67fe 100644 --- a/DragonFruit.Data.sln +++ b/DragonFruit.Data.sln @@ -11,13 +11,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution res\DragonFruit.Data.Nuget.props = res\DragonFruit.Data.Nuget.props EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data", "src\DragonFruit.Data.csproj", "{0A0921D2-637F-4245-B993-054983A97EAC}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Serializers", "Serializers", "{5A8982CD-EEF9-4B9F-AF74-C8D45241E137}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Roslyn", "DragonFruit.Data.Roslyn\DragonFruit.Data.Roslyn\DragonFruit.Data.Roslyn.csproj", "{B840D80F-BD03-46A6-A18B-524D099F9F64}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data", "DragonFruit.Data\DragonFruit.Data.csproj", "{70A4BB10-49BA-48AB-B7A9-EBB7332474D2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Roslyn", "DragonFruit.Data.Roslyn\DragonFruit.Data.Roslyn.csproj", "{A6A2942C-9AB9-4387-8F5F-BEA5E5C247CC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Roslyn.Tests", "DragonFruit.Data.Roslyn\DragonFruit.Data.Roslyn.Tests\DragonFruit.Data.Roslyn.Tests.csproj", "{DE9CFE49-BB70-47CC-BAF2-0243150829E5}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Roslyn.Tests", "DragonFruit.Data.Roslyn.Tests\DragonFruit.Data.Roslyn.Tests.csproj", "{D2CF29A3-BB55-44C2-9471-A994DA8BBAD2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -25,18 +25,18 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0A0921D2-637F-4245-B993-054983A97EAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0A0921D2-637F-4245-B993-054983A97EAC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0A0921D2-637F-4245-B993-054983A97EAC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0A0921D2-637F-4245-B993-054983A97EAC}.Release|Any CPU.Build.0 = Release|Any CPU - {B840D80F-BD03-46A6-A18B-524D099F9F64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B840D80F-BD03-46A6-A18B-524D099F9F64}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B840D80F-BD03-46A6-A18B-524D099F9F64}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B840D80F-BD03-46A6-A18B-524D099F9F64}.Release|Any CPU.Build.0 = Release|Any CPU - {DE9CFE49-BB70-47CC-BAF2-0243150829E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DE9CFE49-BB70-47CC-BAF2-0243150829E5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DE9CFE49-BB70-47CC-BAF2-0243150829E5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DE9CFE49-BB70-47CC-BAF2-0243150829E5}.Release|Any CPU.Build.0 = Release|Any CPU + {70A4BB10-49BA-48AB-B7A9-EBB7332474D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {70A4BB10-49BA-48AB-B7A9-EBB7332474D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70A4BB10-49BA-48AB-B7A9-EBB7332474D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {70A4BB10-49BA-48AB-B7A9-EBB7332474D2}.Release|Any CPU.Build.0 = Release|Any CPU + {A6A2942C-9AB9-4387-8F5F-BEA5E5C247CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6A2942C-9AB9-4387-8F5F-BEA5E5C247CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6A2942C-9AB9-4387-8F5F-BEA5E5C247CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6A2942C-9AB9-4387-8F5F-BEA5E5C247CC}.Release|Any CPU.Build.0 = Release|Any CPU + {D2CF29A3-BB55-44C2-9471-A994DA8BBAD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D2CF29A3-BB55-44C2-9471-A994DA8BBAD2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D2CF29A3-BB55-44C2-9471-A994DA8BBAD2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D2CF29A3-BB55-44C2-9471-A994DA8BBAD2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/DragonFruit.Data.sln.DotSettings b/DragonFruit.Data.sln.DotSettings index db3ab58..7d5ff9f 100644 --- a/DragonFruit.Data.sln.DotSettings +++ b/DragonFruit.Data.sln.DotSettings @@ -294,6 +294,7 @@ Licensed under the MIT License. Please refer to the LICENSE file at the root of CHOP_IF_LONG False False + True True True True diff --git a/src/ApiRequest.cs b/DragonFruit.Data/ApiRequest.cs similarity index 100% rename from src/ApiRequest.cs rename to DragonFruit.Data/ApiRequest.cs diff --git a/src/ApiSerializer.cs b/DragonFruit.Data/ApiSerializer.cs similarity index 100% rename from src/ApiSerializer.cs rename to DragonFruit.Data/ApiSerializer.cs diff --git a/src/DragonFruit.Data.csproj b/DragonFruit.Data/DragonFruit.Data.csproj similarity index 91% rename from src/DragonFruit.Data.csproj rename to DragonFruit.Data/DragonFruit.Data.csproj index 398a2e5..1bdaa07 100644 --- a/src/DragonFruit.Data.csproj +++ b/DragonFruit.Data/DragonFruit.Data.csproj @@ -17,6 +17,6 @@ - + diff --git a/src/Requests/EnumConverter.cs b/DragonFruit.Data/Requests/EnumConverter.cs similarity index 100% rename from src/Requests/EnumConverter.cs rename to DragonFruit.Data/Requests/EnumConverter.cs diff --git a/src/Requests/EnumOption.cs b/DragonFruit.Data/Requests/EnumOption.cs similarity index 100% rename from src/Requests/EnumOption.cs rename to DragonFruit.Data/Requests/EnumOption.cs diff --git a/src/Requests/EnumOptionsAttribute.cs b/DragonFruit.Data/Requests/EnumOptionsAttribute.cs similarity index 100% rename from src/Requests/EnumOptionsAttribute.cs rename to DragonFruit.Data/Requests/EnumOptionsAttribute.cs diff --git a/src/Requests/EnumerableConverter.cs b/DragonFruit.Data/Requests/EnumerableConverter.cs similarity index 100% rename from src/Requests/EnumerableConverter.cs rename to DragonFruit.Data/Requests/EnumerableConverter.cs diff --git a/src/Requests/EnumerableOption.cs b/DragonFruit.Data/Requests/EnumerableOption.cs similarity index 100% rename from src/Requests/EnumerableOption.cs rename to DragonFruit.Data/Requests/EnumerableOption.cs diff --git a/src/Requests/EnumerableOptionsAttribute.cs b/DragonFruit.Data/Requests/EnumerableOptionsAttribute.cs similarity index 100% rename from src/Requests/EnumerableOptionsAttribute.cs rename to DragonFruit.Data/Requests/EnumerableOptionsAttribute.cs diff --git a/src/Requests/FormBodyType.cs b/DragonFruit.Data/Requests/FormBodyType.cs similarity index 100% rename from src/Requests/FormBodyType.cs rename to DragonFruit.Data/Requests/FormBodyType.cs diff --git a/src/Requests/FormBodyTypeAttribute.cs b/DragonFruit.Data/Requests/FormBodyTypeAttribute.cs similarity index 100% rename from src/Requests/FormBodyTypeAttribute.cs rename to DragonFruit.Data/Requests/FormBodyTypeAttribute.cs diff --git a/src/Requests/IRequestBuilder.cs b/DragonFruit.Data/Requests/IRequestBuilder.cs similarity index 100% rename from src/Requests/IRequestBuilder.cs rename to DragonFruit.Data/Requests/IRequestBuilder.cs diff --git a/src/Requests/ParameterType.cs b/DragonFruit.Data/Requests/ParameterType.cs similarity index 100% rename from src/Requests/ParameterType.cs rename to DragonFruit.Data/Requests/ParameterType.cs diff --git a/src/Requests/RequestBodyAttribute.cs b/DragonFruit.Data/Requests/RequestBodyAttribute.cs similarity index 100% rename from src/Requests/RequestBodyAttribute.cs rename to DragonFruit.Data/Requests/RequestBodyAttribute.cs diff --git a/src/Requests/RequestCallbackAttribute.cs b/DragonFruit.Data/Requests/RequestCallbackAttribute.cs similarity index 100% rename from src/Requests/RequestCallbackAttribute.cs rename to DragonFruit.Data/Requests/RequestCallbackAttribute.cs diff --git a/src/Requests/RequestParameterAttribute.cs b/DragonFruit.Data/Requests/RequestParameterAttribute.cs similarity index 100% rename from src/Requests/RequestParameterAttribute.cs rename to DragonFruit.Data/Requests/RequestParameterAttribute.cs diff --git a/src/Requests/RequestParameterAttributeAliases.cs b/DragonFruit.Data/Requests/RequestParameterAttributeAliases.cs similarity index 100% rename from src/Requests/RequestParameterAttributeAliases.cs rename to DragonFruit.Data/Requests/RequestParameterAttributeAliases.cs diff --git a/src/Serializers/ApiJsonSerializer.cs b/DragonFruit.Data/Serializers/ApiJsonSerializer.cs similarity index 100% rename from src/Serializers/ApiJsonSerializer.cs rename to DragonFruit.Data/Serializers/ApiJsonSerializer.cs diff --git a/src/Serializers/ApiXmlSerializer.cs b/DragonFruit.Data/Serializers/ApiXmlSerializer.cs similarity index 100% rename from src/Serializers/ApiXmlSerializer.cs rename to DragonFruit.Data/Serializers/ApiXmlSerializer.cs diff --git a/src/Serializers/IAsyncSerializer.cs b/DragonFruit.Data/Serializers/IAsyncSerializer.cs similarity index 100% rename from src/Serializers/IAsyncSerializer.cs rename to DragonFruit.Data/Serializers/IAsyncSerializer.cs diff --git a/res/DragonFruit.Data.props b/res/DragonFruit.Data.props deleted file mode 100644 index e3760d7..0000000 --- a/res/DragonFruit.Data.props +++ /dev/null @@ -1,15 +0,0 @@ - - - - netstandard2.0;net6.0 - 8 - - - - DragonFruit Data - I/O Framework for Web, APIs and Files - - - - - \ No newline at end of file From 690381aa6e1ddfcdd594afccaa2044b38c1f840a Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 9 Dec 2023 18:52:47 +0000 Subject: [PATCH 045/151] fix source gen not running --- .../ApiRequestSourceGenerator.cs | 49 ++++++++++--------- .../DragonFruit.Data.Roslyn.csproj | 17 +++++-- .../ExternalDependencyLoader.cs | 48 ++++++++++++++++++ .../Templates/ApiRequest.liquid | 2 +- 4 files changed, 89 insertions(+), 27 deletions(-) create mode 100644 DragonFruit.Data.Roslyn/ExternalDependencyLoader.cs diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 5688fb2..4ff3b96 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -25,20 +25,28 @@ public class ApiRequestSourceGenerator : IIncrementalGenerator { public static readonly string TemplateName = "DragonFruit.Data.Roslyn.Templates.ApiRequest.liquid"; - private static readonly Template PartialRequestTemplate; + private static readonly HashSet SupportedCollectionTypes = + [ + ..new[] + { + SpecialType.System_Array, + SpecialType.System_Collections_IEnumerable, + SpecialType.System_Collections_Generic_IList_T, + SpecialType.System_Collections_Generic_ICollection_T, + SpecialType.System_Collections_Generic_IEnumerable_T, + SpecialType.System_Collections_Generic_IReadOnlyList_T, + SpecialType.System_Collections_Generic_IReadOnlyCollection_T + } + ]; - private static readonly HashSet SupportedCollectionTypes = new(new[] - { - SpecialType.System_Array, - SpecialType.System_Collections_IEnumerable, - SpecialType.System_Collections_Generic_IList_T, - SpecialType.System_Collections_Generic_ICollection_T, - SpecialType.System_Collections_Generic_IEnumerable_T, - SpecialType.System_Collections_Generic_IReadOnlyList_T, - SpecialType.System_Collections_Generic_IReadOnlyCollection_T - }); + private Template _partialRequestTemplate; static ApiRequestSourceGenerator() + { + ExternalDependencyLoader.RegisterDependencyLoader(); + } + + public void Initialize(IncrementalGeneratorInitializationContext context) { using var templateStream = typeof(ApiRequestSourceGenerator).Assembly.GetManifestResourceStream(TemplateName); @@ -48,17 +56,14 @@ static ApiRequestSourceGenerator() } using var reader = new StreamReader(templateStream); - PartialRequestTemplate = Template.ParseLiquid(reader.ReadToEnd()); - } + _partialRequestTemplate = Template.ParseLiquid(reader.ReadToEnd()); - public void Initialize(IncrementalGeneratorInitializationContext context) - { - // var apiRequestDerivedClasses = context.SyntaxProvider.CreateSyntaxProvider( - // predicate: (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax classDecl && classDecl.Modifiers.Any(x => x.IsKind(SyntaxKind.PartialKeyword)), - // transform: (generatorSyntaxContext, _) => GetSemanticTarget(generatorSyntaxContext)); - // - // IncrementalValueProvider<(Compilation, ImmutableArray)> targets = context.CompilationProvider.Combine(apiRequestDerivedClasses.Collect()); - // context.RegisterSourceOutput(targets, (spc, source) => Execute(source.Item1, source.Item2, spc)); + var apiRequestDerivedClasses = context.SyntaxProvider.CreateSyntaxProvider( + predicate: (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax classDecl && classDecl.Modifiers.Any(x => x.IsKind(SyntaxKind.PartialKeyword)), + transform: (generatorSyntaxContext, _) => GetSemanticTarget(generatorSyntaxContext)); + + IncrementalValueProvider<(Compilation, ImmutableArray)> targets = context.CompilationProvider.Combine(apiRequestDerivedClasses.Collect()); + context.RegisterSourceOutput(targets, (spc, source) => Execute(source.Item1, source.Item2, spc)); } private void Execute(Compilation compilation, ImmutableArray requestClasses, SourceProductionContext context) @@ -104,7 +109,7 @@ private void Execute(Compilation compilation, ImmutableArrayDragonFruit.Data.Roslyn true - true + false + + + + + + + + + all @@ -20,12 +29,12 @@ - + - - + + diff --git a/DragonFruit.Data.Roslyn/ExternalDependencyLoader.cs b/DragonFruit.Data.Roslyn/ExternalDependencyLoader.cs new file mode 100644 index 0000000..b289ff0 --- /dev/null +++ b/DragonFruit.Data.Roslyn/ExternalDependencyLoader.cs @@ -0,0 +1,48 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace DragonFruit.Data.Roslyn; + +public static class ExternalDependencyLoader +{ + private static bool _loaded; + + public static void RegisterDependencyLoader() + { + if (_loaded) + { + return; + } + + _loaded = true; + AppDomain.CurrentDomain.AssemblyResolve += HandleAssemblyResolve; + } + + private static Assembly HandleAssemblyResolve(object _, ResolveEventArgs args) + { + var name = new AssemblyName(args.Name); + var loadedAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().FullName == name.FullName); + + if (loadedAssembly != null) + { + return loadedAssembly; + } + + using var resourceStream = typeof(ExternalDependencyLoader).Assembly.GetManifestResourceStream($"{typeof(ExternalDependencyLoader).Namespace}.{name.Name}.dll"); + + if (resourceStream == null) + { + return null; + } + + using var memoryStream = new MemoryStream(); + resourceStream.CopyTo(memoryStream); + + return Assembly.Load(memoryStream.ToArray()); + } +} diff --git a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid index 3fb88c2..0bccf00 100644 --- a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid @@ -10,7 +10,7 @@ namespace {{ namespace }} { public global::System.Net.Http.HttpRequestMessage BuildRequest() { - global::System.UriBuilder uriBuilder = new global::System.UriBuilder(RequestPath); + global::System.UriBuilder uriBuilder = new global::System.UriBuilder(this.RequestPath); {% comment %} Process Query Parameters {% endcomment %} {% if query_parameters.size > 0 %} From b3805641120c99c73df0beab5446535a3783e901 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 9 Dec 2023 18:55:49 +0000 Subject: [PATCH 046/151] fix bad name --- DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid index 0bccf00..d9a2c4c 100644 --- a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid @@ -24,7 +24,7 @@ namespace {{ namespace }} {% when 2 %} global::DragonFruit.Data.Requests.EnumConverter.AppendEnum(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ query.enum_option }}, "{{ query.parameter_name }}"); {% else %} - builder.AppendFormat("{0}={1}&", "{{ query.parameter_name }}", global::System.Uri.EscapeDataString({{ query.accessor }}.ToString())); + queryBuilder.AppendFormat("{0}={1}&", "{{ query.parameter_name }}", global::System.Uri.EscapeDataString({{ query.accessor }}.ToString())); {% endcase %} {% endcapture %} From ef6ba9e347f18ec46e5585dddf9ed368ad988cbd Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 9 Dec 2023 18:55:57 +0000 Subject: [PATCH 047/151] change symbol to candidate --- DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 4ff3b96..10b3797 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -217,7 +217,7 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil // check if value is decorated with RequestBodyAttribute if (metadata.BodyProperty != null && candidate.GetAttributes().Any(x => x.AttributeClass?.Equals(requestBodyAttribute, SymbolEqualityComparer.Default) == true)) { - metadata.BodyProperty = new SymbolMetadata(symbol, returnType); + metadata.BodyProperty = new SymbolMetadata(candidate, returnType); } var parameterType = (ParameterType)requestAttribute.ConstructorArguments[0].Value!; @@ -231,7 +231,7 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil var enumOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumParameterAttribute, SymbolEqualityComparer.Default) == true); var enumType = (EnumOption?)enumOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumOption.None; - symbolMetadata = new EnumSymbolMetadata(symbol, returnType, parameterName) + symbolMetadata = new EnumSymbolMetadata(candidate, returnType, parameterName) { EnumOption = enumType.ToString() }; @@ -242,7 +242,7 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil var enumerableOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumerableParameterAttribute, SymbolEqualityComparer.Default) == true); var enumerableType = (EnumerableOption?)enumerableOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumerableOption.Concatenated; - symbolMetadata = new EnumerableSymbolMetadata(symbol, returnType, parameterName) + symbolMetadata = new EnumerableSymbolMetadata(candidate, returnType, parameterName) { Separator = (string)enumerableOptions?.ConstructorArguments.ElementAtOrDefault(1).Value ?? ",", EnumerableOption = enumerableType.ToString() @@ -250,7 +250,7 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil } else { - var psm = new PropertySymbolMetadata(symbol, returnType, parameterName); + var psm = new PropertySymbolMetadata(candidate, returnType, parameterName); if (DerivesFrom(returnType, streamTypeSymbol)) { From 338534611925ac82ae4c670e0c08c16e67e3aab2 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 9 Dec 2023 19:02:16 +0000 Subject: [PATCH 048/151] add comment --- DragonFruit.Data.Roslyn/ExternalDependencyLoader.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/DragonFruit.Data.Roslyn/ExternalDependencyLoader.cs b/DragonFruit.Data.Roslyn/ExternalDependencyLoader.cs index b289ff0..3d1b927 100644 --- a/DragonFruit.Data.Roslyn/ExternalDependencyLoader.cs +++ b/DragonFruit.Data.Roslyn/ExternalDependencyLoader.cs @@ -23,6 +23,7 @@ public static void RegisterDependencyLoader() AppDomain.CurrentDomain.AssemblyResolve += HandleAssemblyResolve; } + // derived from https://stackoverflow.com/a/67074009 private static Assembly HandleAssemblyResolve(object _, ResolveEventArgs args) { var name = new AssemblyName(args.Name); From d30626a9f315a2e0a34182758221201022393ce5 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 9 Dec 2023 19:02:28 +0000 Subject: [PATCH 049/151] fix bad generation (global::) --- DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 10b3797..e870223 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -98,7 +98,7 @@ private void Execute(Compilation compilation, ImmutableArray Date: Sat, 9 Dec 2023 19:02:41 +0000 Subject: [PATCH 050/151] include scriban in deps --- .../DragonFruit.Data.Roslyn.Tests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj b/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj index 5da0cb0..a701dcb 100644 --- a/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj +++ b/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj @@ -9,6 +9,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive From eea561f1fec185e10bdc364ed8faca65a03195cd Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 9 Dec 2023 19:25:29 +0000 Subject: [PATCH 051/151] bad formatting --- .../Templates/ApiRequest.liquid | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid index d9a2c4c..1bf5cdd 100644 --- a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid @@ -25,9 +25,8 @@ namespace {{ namespace }} global::DragonFruit.Data.Requests.EnumConverter.AppendEnum(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ query.enum_option }}, "{{ query.parameter_name }}"); {% else %} queryBuilder.AppendFormat("{0}={1}&", "{{ query.parameter_name }}", global::System.Uri.EscapeDataString({{ query.accessor }}.ToString())); - {% endcase %} - {% endcapture %} - + {% endcase -%} + {% endcapture -%} {% if query.nullable %} if ({{ query.accessor }} != null) { @@ -35,8 +34,8 @@ namespace {{ namespace }} } {% else %} {{ query_append }} - {% endif %} - {% endfor %} + {% endif -%} + {% endfor -%} {% comment %} remove trailing & {% endcomment %} if (queryBuilder.Length > 0) @@ -44,7 +43,7 @@ namespace {{ namespace }} queryBuilder.Length--; uriBuilder.Query = queryBuilder.ToString(); } - {% endif %} + {% endif -%} global::System.Net.Http.HttpRequestMessage request = new global::System.Net.Http.HttpRequestMessage(this.RequestMethod, uriBuilder.Uri); @@ -74,9 +73,9 @@ namespace {{ namespace }} {% comment %} Handle other types using ToString {% endcomment %} {% else %} content.Add(new global::System.Net.Http.StringContent({{ multipart.accessor }}), "{{ multipart.parameter_name }}"); - {% endcase %} - {% endcase %} - {% endcapture %} + {% endcase -%} + {% endcase -%} + {% endcapture -%} {% if multipart.nullable %} if ({{ multipart.accessor }} != null) @@ -85,8 +84,8 @@ namespace {{ namespace }} } {% else %} {{ multipart_append }} - {% endif %} - {% endfor %} + {% endif -%} + {% endfor -%} request.Content = content; @@ -98,13 +97,13 @@ namespace {{ namespace }} {% capture uriparam_append %} {% case uriparam.type %} {% when 1 %} - global::DragonFruit.Data.Requests.EnumerableConverter.AppendEnumerable(formBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ query.enumerable_option }}, "{{ query.parameter_name }}", "{{ query.separator }}"); + global::DragonFruit.Data.Requests.EnumerableConverter.AppendEnumerable(formBuilder, {{ uriparam.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ uriparam.enumerable_option }}, "{{ uriparam.parameter_name }}", "{{ uriparam.separator }}"); {% when 2 %} - global::DragonFruit.Data.Requests.EnumConverter.AppendEnum(formBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ query.enum_option }}, "{{ query.parameter_name }}"); + global::DragonFruit.Data.Requests.EnumConverter.AppendEnum(formBuilder, {{ uriparam.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ uriparam.enum_option }}, "{{ uriparam.parameter_name }}"); {% else %} - formBuilder.AppendFormat("{0}={1}&", "{{ query.parameter_name }}", global::System.Uri.EscapeDataString({{ query.accessor }}.ToString())); - {% endcase %} - {% endcapture %} + formBuilder.AppendFormat("{0}={1}&", "{{ uriparam.parameter_name }}", global::System.Uri.EscapeDataString({{ uriparam.accessor }}.ToString())); + {% endcase -%} + {% endcapture -%} {% if uriparam.nullable %} if ({{ uriparam.accessor }} != null) @@ -113,8 +112,8 @@ namespace {{ namespace }} } {% else %} {{ uriparam_append }} - {% endif %} - {% endfor %} + {% endif -%} + {% endfor -%} request.Content = new global::System.Net.Http.StringContent(formBuilder.ToString(), global::System.Text.Encoding.UTF8, "application/x-www-form-urlencoded"); @@ -138,8 +137,8 @@ namespace {{ namespace }} request.Headers.Add("{{ header.parameter_name }}", global::DragonFruit.Data.Requests.EnumConverter.GetString({{ header.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ header.enum_option }})); {% else %} request.Headers.Add("{{ header.parameter_name }}", {{ header.accessor }}.ToString()); - {% endcase %} - {% endcapture %} + {% endcase -%} + {% endcapture -%} {% if header.nullable %} if ({{ header.accessor }} != null) @@ -148,8 +147,8 @@ namespace {{ namespace }} } {% else %} {{ header_append }} - {% endif %} - {% endfor %} + {% endif -%} + {% endfor -%} return request; } From 49b0f4a81606fd6f9526cd5f64f4e33fd7979222 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 9 Dec 2023 19:25:42 +0000 Subject: [PATCH 052/151] update packages --- DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj index d59c5c7..dbdf57a 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj @@ -27,8 +27,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + From 3fa347c3be89d995c2983cc55e7a3b1c00418a5d Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 9 Dec 2023 19:34:43 +0000 Subject: [PATCH 053/151] fix incorrect multipart content type --- .../Templates/ApiRequest.liquid | 84 +++++++++---------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid index 1bf5cdd..e25a240 100644 --- a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid @@ -13,17 +13,17 @@ namespace {{ namespace }} global::System.UriBuilder uriBuilder = new global::System.UriBuilder(this.RequestPath); {% comment %} Process Query Parameters {% endcomment %} - {% if query_parameters.size > 0 %} + {% if query_parameters.size > 0 -%} global::System.Text.StringBuilder queryBuilder = new global::System.Text.StringBuilder(); - {% for query in query_parameters %} - {% capture query_append %} - {% case query.type %} - {% when 1 %} + {% for query in query_parameters -%} + {% capture query_append -%} + {% case query.type -%} + {% when 1 -%} global::DragonFruit.Data.Requests.EnumerableConverter.AppendEnumerable(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ query.enumerable_option }}, "{{ query.parameter_name }}", "{{ query.separator }}"); - {% when 2 %} + {% when 2 -%} global::DragonFruit.Data.Requests.EnumConverter.AppendEnum(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ query.enum_option }}, "{{ query.parameter_name }}"); - {% else %} + {% else -%} queryBuilder.AppendFormat("{0}={1}&", "{{ query.parameter_name }}", global::System.Uri.EscapeDataString({{ query.accessor }}.ToString())); {% endcase -%} {% endcapture -%} @@ -32,7 +32,7 @@ namespace {{ namespace }} { {{ query_append }} } - {% else %} + {% else -%} {{ query_append }} {% endif -%} {% endfor -%} @@ -43,36 +43,36 @@ namespace {{ namespace }} queryBuilder.Length--; uriBuilder.Query = queryBuilder.ToString(); } - {% endif -%} + {% endif %} global::System.Net.Http.HttpRequestMessage request = new global::System.Net.Http.HttpRequestMessage(this.RequestMethod, uriBuilder.Uri); - {% case request_body_type %} + {% case request_body_type -%} {% comment %} 1 - Multipart {% endcomment %} - {% when 1 %} - global::System.Net.Http.MultipartContent content = new global::System.Net.Http.MultipartContent(); + {% when 1 -%} + global::System.Net.Http.MultipartFormDataContent content = new global::System.Net.Http.MultipartFormDataContent(); - {% for multipart in form_body_parameters %} - {% capture multipart_append %} - {% case multipart.type %} - {% when 1 %} + {% for multipart in form_body_parameters -%} + {% capture multipart_append -%} + {% case multipart.type -%} + {% when 1 -%} foreach (var kvp in global::DragonFruit.Data.Requests.EnumerableConverter.GetPairs({{ multipart.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ multipart.enumerable_option }})) { content.Add(new global::System.Net.Http.StringContent(kvp.Value), "{{ multipart.parameter_name }}"); } - {% when 2 %} + {% when 2 -%} content.Add(new global::System.Net.Http.StringContent(global::DragonFruit.Data.Requests.EnumConverter.GetString({{ multipart.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ multipart.enum_option }})), "{{ multipart.parameter_name }}"); - {% else %} - {% case multipart.special_request_parameter %} + {% else -%} + {% case multipart.special_request_parameter -%} {% comment %} 0 - ByteArray {% endcomment %} - {% when 0 %} + {% when 0 -%} content.Add(new global::System.Net.Http.ByteArrayContent({{ multipart.accessor }}), "{{ multipart.parameter_name }}"); {% comment %} 1 - Stream {% endcomment %} - {% when 1 %} + {% when 1 -%} content.Add(new global::System.Net.Http.StreamContent({{ multipart.accessor }}), "{{ multipart.parameter_name }}"); {% comment %} Handle other types using ToString {% endcomment %} - {% else %} - content.Add(new global::System.Net.Http.StringContent({{ multipart.accessor }}), "{{ multipart.parameter_name }}"); + {% else -%} + content.Add(new global::System.Net.Http.StringContent({{ multipart.accessor }}.ToString()), "{{ multipart.parameter_name }}"); {% endcase -%} {% endcase -%} {% endcapture -%} @@ -82,7 +82,7 @@ namespace {{ namespace }} { {{ multipart_append }} } - {% else %} + {% else -%} {{ multipart_append }} {% endif -%} {% endfor -%} @@ -90,17 +90,17 @@ namespace {{ namespace }} request.Content = content; {% comment %} 2 - UriEncoded {% endcomment %} - {% when 2 %} + {% when 2 -%} global::System.Text.StringBuilder formBuilder = new global::System.Text.StringBuilder(); - {% for uriparam in form_body_parameters %} - {% capture uriparam_append %} - {% case uriparam.type %} - {% when 1 %} + {% for uriparam in form_body_parameters -%} + {% capture uriparam_append -%} + {% case uriparam.type -%} + {% when 1 -%} global::DragonFruit.Data.Requests.EnumerableConverter.AppendEnumerable(formBuilder, {{ uriparam.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ uriparam.enumerable_option }}, "{{ uriparam.parameter_name }}", "{{ uriparam.separator }}"); - {% when 2 %} + {% when 2 -%} global::DragonFruit.Data.Requests.EnumConverter.AppendEnum(formBuilder, {{ uriparam.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ uriparam.enum_option }}, "{{ uriparam.parameter_name }}"); - {% else %} + {% else -%} formBuilder.AppendFormat("{0}={1}&", "{{ uriparam.parameter_name }}", global::System.Uri.EscapeDataString({{ uriparam.accessor }}.ToString())); {% endcase -%} {% endcapture -%} @@ -110,7 +110,7 @@ namespace {{ namespace }} { {{ uriparam_append }} } - {% else %} + {% else -%} {{ uriparam_append }} {% endif -%} {% endfor -%} @@ -118,34 +118,34 @@ namespace {{ namespace }} request.Content = new global::System.Net.Http.StringContent(formBuilder.ToString(), global::System.Text.Encoding.UTF8, "application/x-www-form-urlencoded"); {% comment %} 3 - Custom Body (HttpContent) {% endcomment %} - {% when 3 %} + {% when 3 -%} request.Content = {{ request_body_symbol.accessor }}; {% comment %} 4 - todo Custom Body (Serialized) {% endcomment %} - {% endcase %} + {% endcase -%} {% comment %} Process Headers {% endcomment %} - {% for header in header_parameters %} - {% capture header_append %} - {% case header.type %} - {% when 1 %} + {% for header in header_parameters -%} + {% capture header_append -%} + {% case header.type -%} + {% when 1 -%} foreach (var kvp in global::DragonFruit.Data.Requests.EnumerableConverter.GetPairs({{ header.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ header.enumumerable_option }})) { request.Headers.Add(kvp.Key, kvp.Value); } - {% when 2 %} + {% when 2 -%} request.Headers.Add("{{ header.parameter_name }}", global::DragonFruit.Data.Requests.EnumConverter.GetString({{ header.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ header.enum_option }})); - {% else %} + {% else -%} request.Headers.Add("{{ header.parameter_name }}", {{ header.accessor }}.ToString()); {% endcase -%} {% endcapture -%} - {% if header.nullable %} + {% if header.nullable -%} if ({{ header.accessor }} != null) { {{ header_append }} } - {% else %} + {% else -%} {{ header_append }} {% endif -%} {% endfor -%} From 59930dda8af92268b73fd24d04a001ceb4efb868 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 9 Dec 2023 19:36:07 +0000 Subject: [PATCH 054/151] fix warning --- DragonFruit.Data.Roslyn.Tests/ApiRequestTemplateTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DragonFruit.Data.Roslyn.Tests/ApiRequestTemplateTests.cs b/DragonFruit.Data.Roslyn.Tests/ApiRequestTemplateTests.cs index 4146e07..d772e33 100644 --- a/DragonFruit.Data.Roslyn.Tests/ApiRequestTemplateTests.cs +++ b/DragonFruit.Data.Roslyn.Tests/ApiRequestTemplateTests.cs @@ -19,7 +19,7 @@ public async Task TestTemplateParse() Assert.NotNull(template); using var templateReader = new StreamReader(template); - var templateText = await templateReader.ReadToEndAsync().ConfigureAwait(false); + var templateText = await templateReader.ReadToEndAsync(); Assert.True(templateText.Length > 0); From 933c44175336bae0fa5ea70f525292937be12eb6 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sun, 10 Dec 2023 11:41:28 +0000 Subject: [PATCH 055/151] add getter checks, access checks --- .../ApiRequestSourceGenerator.cs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index e870223..e67cf50 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -214,6 +214,28 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil continue; } + // check if property and has a getter + var candidateMethod = candidate switch + { + IPropertySymbol propertySymbol => propertySymbol.GetMethod, + IMethodSymbol methodSymbol => methodSymbol, + + _ => throw new NotSupportedException() + }; + + if (candidateMethod == null) + { + // todo return diagnostic warning (no getter) + continue; + } + + // inherited properties that are private or internal are ignored + if (depth > 0 && candidateMethod.DeclaredAccessibility is Accessibility.Private or Accessibility.Internal) + { + // todo return diagnostic warning (getter not accessible) + continue; + } + // check if value is decorated with RequestBodyAttribute if (metadata.BodyProperty != null && candidate.GetAttributes().Any(x => x.AttributeClass?.Equals(requestBodyAttribute, SymbolEqualityComparer.Default) == true)) { @@ -265,7 +287,6 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil } symbolMetadata.Depth = depth; - metadata.Properties[parameterType].Add(symbolMetadata); } From d0b4b00cbbf26214d9e0bc7c61c6f2f7fa01d764 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sun, 10 Dec 2023 19:33:36 +0000 Subject: [PATCH 056/151] remove duplicate --- .../Analyzers/ApiRequestClassAnalyzer.cs | 19 +------------------ .../ApiRequestSourceGenerator.cs | 2 +- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassAnalyzer.cs b/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassAnalyzer.cs index 7c1455c..a2088f6 100644 --- a/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassAnalyzer.cs +++ b/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassAnalyzer.cs @@ -35,7 +35,7 @@ private void AnalyzeClassDecl(SyntaxNodeAnalysisContext context) var apiRequestType = context.Compilation.GetTypeByMetadataName("DragonFruit.Data.ApiRequest"); var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationNode); - if (apiRequestType == null || classSymbol == null || !InheritsFrom(classSymbol, apiRequestType)) + if (apiRequestType == null || classSymbol == null || !ApiRequestSourceGenerator.DerivesFrom(classSymbol, apiRequestType)) { return; } @@ -49,22 +49,5 @@ private void AnalyzeClassDecl(SyntaxNodeAnalysisContext context) var diagnostic = Diagnostic.Create(Rule, classDeclarationNode.Identifier.GetLocation(), classDeclarationNode.Identifier.Text); context.ReportDiagnostic(diagnostic); } - - private static bool InheritsFrom(INamedTypeSymbol symbol, ITypeSymbol type) - { - var baseType = symbol.BaseType; - - while (baseType != null) - { - if (type.Equals(baseType, SymbolEqualityComparer.Default)) - { - return true; - } - - baseType = baseType.BaseType; - } - - return false; - } } } diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index e67cf50..24d5797 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -298,7 +298,7 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil return metadata; } - private static bool DerivesFrom(ITypeSymbol type, ITypeSymbol baseType) + internal static bool DerivesFrom(ITypeSymbol type, ITypeSymbol baseType) { var classSymbol = type; From 5a959e1b4d409eb45dff1d89f64c57808383f085 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Mon, 11 Dec 2023 09:22:07 +0000 Subject: [PATCH 057/151] update analyzer --- .../ApiRequestAnalyzerTests.cs | 2 +- .../Analyzers/ApiRequestAnalyzer.cs | 139 ++++++++++++++++++ .../Analyzers/ApiRequestClassAnalyzer.cs | 53 ------- .../Analyzers/ApiRequestClassFixProvider.cs | 2 +- 4 files changed, 141 insertions(+), 55 deletions(-) create mode 100644 DragonFruit.Data.Roslyn/Analyzers/ApiRequestAnalyzer.cs delete mode 100644 DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassAnalyzer.cs diff --git a/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs b/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs index 33af453..290b639 100644 --- a/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs +++ b/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; using Xunit; -using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.XUnit.CodeFixVerifier; +using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.XUnit.CodeFixVerifier; namespace DragonFruit.Data.Roslyn.Tests; diff --git a/DragonFruit.Data.Roslyn/Analyzers/ApiRequestAnalyzer.cs b/DragonFruit.Data.Roslyn/Analyzers/ApiRequestAnalyzer.cs new file mode 100644 index 0000000..7d27531 --- /dev/null +++ b/DragonFruit.Data.Roslyn/Analyzers/ApiRequestAnalyzer.cs @@ -0,0 +1,139 @@ +using System.Collections.Immutable; +using System.Linq; +using DragonFruit.Data.Requests; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace DragonFruit.Data.Roslyn.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class ApiRequestAnalyzer : DiagnosticAnalyzer + { + internal static readonly DiagnosticDescriptor PartialClassRule = new("DA0001", "Partial class expected", "Class '{0}' should be marked as partial", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor PropertyNoGetterRule = new("DA0002", "Property has no getter", "Property '{0}' has no accessible getter", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); + internal static readonly DiagnosticDescriptor PropertyNotInApiRequestRule = new("DA0003", "Property not in ApiRequest", "'{0}' is not in an ApiRequest class", "Design", DiagnosticSeverity.Warning, isEnabledByDefault: true); + internal static readonly DiagnosticDescriptor PropertyOrMethodInaccessibleRule = new("DA0004", "Property or Method is inaccessible", "'{0}' is inaccessible. Properties should either be public, protected or protected internal.", "Design", DiagnosticSeverity.Warning, isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor MethodReturnsVoidRule = new("DA0005", "Method returns void", "Method '{0}' used to provide request values returns void", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); + internal static readonly DiagnosticDescriptor MethodHasParametersRule = new("DA0006", "Method has parameters", "Method '{0}' used to provide request values takes arguments", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(PartialClassRule, + PropertyNoGetterRule, PropertyNotInApiRequestRule, PropertyOrMethodInaccessibleRule, + MethodReturnsVoidRule, MethodHasParametersRule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(AnalyzeClassDecl, SyntaxKind.ClassDeclaration); + context.RegisterSyntaxNodeAction(AnalyzeMethodDecl, SyntaxKind.MethodDeclaration); + context.RegisterSyntaxNodeAction(AnalyzePropertyDecl, SyntaxKind.PropertyDeclaration); + } + + private void AnalyzeClassDecl(SyntaxNodeAnalysisContext context) + { + if (context.Node is not ClassDeclarationSyntax classDeclarationNode) + { + return; + } + + // check if class inherits from apiRequestType using type checking + var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationNode); + var apiRequestType = context.Compilation.GetTypeByMetadataName(typeof(ApiRequest).FullName); + + if (classSymbol == null || !ApiRequestSourceGenerator.DerivesFrom(classSymbol, apiRequestType)) + { + return; + } + + // check if class has partial keyword + if (!classDeclarationNode.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) + { + context.ReportDiagnostic(Diagnostic.Create(PartialClassRule, classDeclarationNode.Identifier.GetLocation(), classDeclarationNode.Identifier.Text)); + } + } + + private void AnalyzeMethodDecl(SyntaxNodeAnalysisContext context) + { + if (context.Node is not MethodDeclarationSyntax methodDeclarationSyntax) + { + return; + } + + var symbol = context.SemanticModel.GetDeclaredSymbol(methodDeclarationSyntax); + + if (symbol?.GetAttributes().Any(a => a.AttributeClass?.Name is nameof(RequestParameterAttribute) or nameof(RequestBodyAttribute)) != true) + { + return; + } + + // check for attributes (and null check symbol) + if (!ApiRequestSourceGenerator.DerivesFrom(symbol.ContainingType, context.Compilation.GetTypeByMetadataName(typeof(ApiRequest).FullName))) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyNotInApiRequestRule, methodDeclarationSyntax.Identifier.GetLocation(), methodDeclarationSyntax.Identifier.Text)); + return; + } + + // check for method accessibility + if (symbol.DeclaredAccessibility is not Accessibility.Public and not Accessibility.Protected and not Accessibility.ProtectedOrInternal) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyOrMethodInaccessibleRule, methodDeclarationSyntax.Identifier.GetLocation(), methodDeclarationSyntax.Identifier.Text)); + } + + // check if method takes parameters + if (methodDeclarationSyntax.ParameterList.Parameters.Any()) + { + context.ReportDiagnostic(Diagnostic.Create(MethodHasParametersRule, methodDeclarationSyntax.Identifier.GetLocation(), methodDeclarationSyntax.Identifier.Text)); + } + + // check if method returns void + if (methodDeclarationSyntax.ReturnType is PredefinedTypeSyntax { Keyword.ValueText: "void" }) + { + context.ReportDiagnostic(Diagnostic.Create(MethodReturnsVoidRule, methodDeclarationSyntax.Identifier.GetLocation(), methodDeclarationSyntax.Identifier.Text)); + } + } + + private void AnalyzePropertyDecl(SyntaxNodeAnalysisContext context) + { + if (context.Node is not PropertyDeclarationSyntax propertyDeclarationSyntax) + { + return; + } + + var symbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclarationSyntax); + + if (symbol?.GetAttributes().Any(a => a.AttributeClass?.Name is nameof(RequestParameterAttribute) or nameof(RequestBodyAttribute)) != true) + { + return; + } + + if (!ApiRequestSourceGenerator.DerivesFrom(symbol.ContainingType, context.Compilation.GetTypeByMetadataName(typeof(ApiRequest).FullName))) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyNotInApiRequestRule, propertyDeclarationSyntax.Identifier.GetLocation(), propertyDeclarationSyntax.Identifier.Text)); + return; + } + + if (symbol.DeclaredAccessibility is not Accessibility.Public and not Accessibility.Protected and not Accessibility.ProtectedOrInternal) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyOrMethodInaccessibleRule, propertyDeclarationSyntax.Identifier.GetLocation(), propertyDeclarationSyntax.Identifier.Text)); + } + + // check for getter + if (symbol.GetMethod == null) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyNoGetterRule, propertyDeclarationSyntax.Identifier.GetLocation(), propertyDeclarationSyntax.Identifier.Text)); + return; + } + + // check getter is public, protected or protected internal + if (symbol.GetMethod.DeclaredAccessibility is Accessibility.Private or Accessibility.Internal) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyOrMethodInaccessibleRule, propertyDeclarationSyntax.Identifier.GetLocation(), propertyDeclarationSyntax.Identifier.Text)); + } + } + } +} diff --git a/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassAnalyzer.cs b/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassAnalyzer.cs deleted file mode 100644 index a2088f6..0000000 --- a/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassAnalyzer.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Immutable; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace DragonFruit.Data.Roslyn.Analyzers -{ - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class ApiRequestClassAnalyzer : DiagnosticAnalyzer - { - public const string DiagnosticId = "DA0001"; - - private static readonly DiagnosticDescriptor Rule = new(DiagnosticId, "Partial class expected", "Class '{0}' should be marked as partial", "Design", DiagnosticSeverity.Warning, isEnabledByDefault: true); - - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); - - public override void Initialize(AnalysisContext context) - { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - - context.RegisterSyntaxNodeAction(AnalyzeClassDecl, SyntaxKind.ClassDeclaration); - } - - private void AnalyzeClassDecl(SyntaxNodeAnalysisContext context) - { - if (context.Node is not ClassDeclarationSyntax classDeclarationNode) - { - return; - } - - // check if class inherits from apiRequestType using type checking - var apiRequestType = context.Compilation.GetTypeByMetadataName("DragonFruit.Data.ApiRequest"); - var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationNode); - - if (apiRequestType == null || classSymbol == null || !ApiRequestSourceGenerator.DerivesFrom(classSymbol, apiRequestType)) - { - return; - } - - // check if class has partial keyword - if (classDeclarationNode.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) - { - return; - } - - var diagnostic = Diagnostic.Create(Rule, classDeclarationNode.Identifier.GetLocation(), classDeclarationNode.Identifier.Text); - context.ReportDiagnostic(diagnostic); - } - } -} diff --git a/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassFixProvider.cs b/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassFixProvider.cs index 4599151..22d9ea1 100644 --- a/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassFixProvider.cs +++ b/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassFixProvider.cs @@ -15,7 +15,7 @@ namespace DragonFruit.Data.Roslyn.Analyzers [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ApiRequestClassFixProvider)), Shared] public class ApiRequestClassFixProvider : CodeFixProvider { - public sealed override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(ApiRequestClassAnalyzer.DiagnosticId); + public sealed override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(ApiRequestAnalyzer.PartialClassRule.Id); public override FixAllProvider GetFixAllProvider() => null; From db7b36da4332b8373e45462e4e5b0bb4ff552428 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Mon, 11 Dec 2023 09:22:19 +0000 Subject: [PATCH 058/151] fix invalid attributeusage --- DragonFruit.Data/Requests/RequestBodyAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DragonFruit.Data/Requests/RequestBodyAttribute.cs b/DragonFruit.Data/Requests/RequestBodyAttribute.cs index 819b44e..038e94c 100644 --- a/DragonFruit.Data/Requests/RequestBodyAttribute.cs +++ b/DragonFruit.Data/Requests/RequestBodyAttribute.cs @@ -13,7 +13,7 @@ namespace DragonFruit.Data.Requests /// /// There must not be any more than one property/method that is decorated with this attribute. /// - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Method)] + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] public class RequestBodyAttribute : Attribute { } From b41a80b5adcbb68ab35c194be6c852a0bbbb582a Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Mon, 11 Dec 2023 09:51:36 +0000 Subject: [PATCH 059/151] add analyzerreleases files --- DragonFruit.Data.Roslyn/AnalyzerReleases.Shipped.md | 12 ++++++++++++ .../AnalyzerReleases.Unshipped.md | 0 .../DragonFruit.Data.Roslyn.csproj | 2 ++ 3 files changed, 14 insertions(+) create mode 100644 DragonFruit.Data.Roslyn/AnalyzerReleases.Shipped.md create mode 100644 DragonFruit.Data.Roslyn/AnalyzerReleases.Unshipped.md diff --git a/DragonFruit.Data.Roslyn/AnalyzerReleases.Shipped.md b/DragonFruit.Data.Roslyn/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..efcd2f1 --- /dev/null +++ b/DragonFruit.Data.Roslyn/AnalyzerReleases.Shipped.md @@ -0,0 +1,12 @@ +## Release 1.0 + +### New Rules + +| Rule ID | Category | Severity | Notes | +|---------|----------|----------|--------------------------------------------| +| DA0001 | Design | Error | Class not marked as partial | +| DA0002 | Design | Warning | Property has no getter | +| DA0003 | Usage | Warning | Property or Method not in ApiRequest class | +| DA0004 | Design | Warning | Property or Method is inaccessible | +| DA0005 | Design | Error | Method returns void | +| DA0006 | Design | Error | Method accepts arguments | \ No newline at end of file diff --git a/DragonFruit.Data.Roslyn/AnalyzerReleases.Unshipped.md b/DragonFruit.Data.Roslyn/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..e69de29 diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj index dbdf57a..1413033 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj @@ -15,6 +15,8 @@ + + From 6e697c92f57119ca58f5257cfb581c0a05bb6be7 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Mon, 11 Dec 2023 09:51:50 +0000 Subject: [PATCH 060/151] move analyzer to base project dir --- .../{Analyzers => }/ApiRequestAnalyzer.cs | 12 ++++++------ .../ApiRequestClassFixProvider.cs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) rename DragonFruit.Data.Roslyn/{Analyzers => }/ApiRequestAnalyzer.cs (90%) rename DragonFruit.Data.Roslyn/{Analyzers => Fixes}/ApiRequestClassFixProvider.cs (97%) diff --git a/DragonFruit.Data.Roslyn/Analyzers/ApiRequestAnalyzer.cs b/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs similarity index 90% rename from DragonFruit.Data.Roslyn/Analyzers/ApiRequestAnalyzer.cs rename to DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs index 7d27531..f47364f 100644 --- a/DragonFruit.Data.Roslyn/Analyzers/ApiRequestAnalyzer.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs @@ -6,22 +6,22 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace DragonFruit.Data.Roslyn.Analyzers +namespace DragonFruit.Data.Roslyn { [DiagnosticAnalyzer(LanguageNames.CSharp)] public class ApiRequestAnalyzer : DiagnosticAnalyzer { internal static readonly DiagnosticDescriptor PartialClassRule = new("DA0001", "Partial class expected", "Class '{0}' should be marked as partial", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); - internal static readonly DiagnosticDescriptor PropertyNoGetterRule = new("DA0002", "Property has no getter", "Property '{0}' has no accessible getter", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); - internal static readonly DiagnosticDescriptor PropertyNotInApiRequestRule = new("DA0003", "Property not in ApiRequest", "'{0}' is not in an ApiRequest class", "Design", DiagnosticSeverity.Warning, isEnabledByDefault: true); + internal static readonly DiagnosticDescriptor PropertyNoGetterRule = new("DA0002", "Property has no getter", "Property '{0}' has no accessible getter", "Design", DiagnosticSeverity.Warning, isEnabledByDefault: true); + internal static readonly DiagnosticDescriptor PropertyOrMethodNotInApiRequestRule = new("DA0003", "Property or Method not in ApiRequest", "'{0}' is not in an ApiRequest class", "Usage", DiagnosticSeverity.Warning, isEnabledByDefault: true); internal static readonly DiagnosticDescriptor PropertyOrMethodInaccessibleRule = new("DA0004", "Property or Method is inaccessible", "'{0}' is inaccessible. Properties should either be public, protected or protected internal.", "Design", DiagnosticSeverity.Warning, isEnabledByDefault: true); internal static readonly DiagnosticDescriptor MethodReturnsVoidRule = new("DA0005", "Method returns void", "Method '{0}' used to provide request values returns void", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); internal static readonly DiagnosticDescriptor MethodHasParametersRule = new("DA0006", "Method has parameters", "Method '{0}' used to provide request values takes arguments", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(PartialClassRule, - PropertyNoGetterRule, PropertyNotInApiRequestRule, PropertyOrMethodInaccessibleRule, + PropertyNoGetterRule, PropertyOrMethodNotInApiRequestRule, PropertyOrMethodInaccessibleRule, MethodReturnsVoidRule, MethodHasParametersRule); public override void Initialize(AnalysisContext context) @@ -74,7 +74,7 @@ private void AnalyzeMethodDecl(SyntaxNodeAnalysisContext context) // check for attributes (and null check symbol) if (!ApiRequestSourceGenerator.DerivesFrom(symbol.ContainingType, context.Compilation.GetTypeByMetadataName(typeof(ApiRequest).FullName))) { - context.ReportDiagnostic(Diagnostic.Create(PropertyNotInApiRequestRule, methodDeclarationSyntax.Identifier.GetLocation(), methodDeclarationSyntax.Identifier.Text)); + context.ReportDiagnostic(Diagnostic.Create(PropertyOrMethodNotInApiRequestRule, methodDeclarationSyntax.Identifier.GetLocation(), methodDeclarationSyntax.Identifier.Text)); return; } @@ -113,7 +113,7 @@ private void AnalyzePropertyDecl(SyntaxNodeAnalysisContext context) if (!ApiRequestSourceGenerator.DerivesFrom(symbol.ContainingType, context.Compilation.GetTypeByMetadataName(typeof(ApiRequest).FullName))) { - context.ReportDiagnostic(Diagnostic.Create(PropertyNotInApiRequestRule, propertyDeclarationSyntax.Identifier.GetLocation(), propertyDeclarationSyntax.Identifier.Text)); + context.ReportDiagnostic(Diagnostic.Create(PropertyOrMethodNotInApiRequestRule, propertyDeclarationSyntax.Identifier.GetLocation(), propertyDeclarationSyntax.Identifier.Text)); return; } diff --git a/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassFixProvider.cs b/DragonFruit.Data.Roslyn/Fixes/ApiRequestClassFixProvider.cs similarity index 97% rename from DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassFixProvider.cs rename to DragonFruit.Data.Roslyn/Fixes/ApiRequestClassFixProvider.cs index 22d9ea1..7c011ac 100644 --- a/DragonFruit.Data.Roslyn/Analyzers/ApiRequestClassFixProvider.cs +++ b/DragonFruit.Data.Roslyn/Fixes/ApiRequestClassFixProvider.cs @@ -10,7 +10,7 @@ using SyntaxFactory = Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using SyntaxKind = Microsoft.CodeAnalysis.CSharp.SyntaxKind; -namespace DragonFruit.Data.Roslyn.Analyzers +namespace DragonFruit.Data.Roslyn.Fixes { [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ApiRequestClassFixProvider)), Shared] public class ApiRequestClassFixProvider : CodeFixProvider From 1f5141a2b4a4887cef6378cba0095bd7f23a6210 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Mon, 11 Dec 2023 11:33:30 +0000 Subject: [PATCH 061/151] add nested class analyzer rule --- .../AnalyzerReleases.Shipped.md | 17 +++++------ DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs | 28 +++++++++++++------ 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/DragonFruit.Data.Roslyn/AnalyzerReleases.Shipped.md b/DragonFruit.Data.Roslyn/AnalyzerReleases.Shipped.md index efcd2f1..6861331 100644 --- a/DragonFruit.Data.Roslyn/AnalyzerReleases.Shipped.md +++ b/DragonFruit.Data.Roslyn/AnalyzerReleases.Shipped.md @@ -2,11 +2,12 @@ ### New Rules -| Rule ID | Category | Severity | Notes | -|---------|----------|----------|--------------------------------------------| -| DA0001 | Design | Error | Class not marked as partial | -| DA0002 | Design | Warning | Property has no getter | -| DA0003 | Usage | Warning | Property or Method not in ApiRequest class | -| DA0004 | Design | Warning | Property or Method is inaccessible | -| DA0005 | Design | Error | Method returns void | -| DA0006 | Design | Error | Method accepts arguments | \ No newline at end of file + Rule ID | Category | Severity | Notes +---------|----------|----------|-------------------------------------------- + DA0001 | Design | Error | Class not marked as partial + DA0002 | Design | Error | Class was nested within another class + DA0003 | Design | Warning | Property has no getter + DA0004 | Usage | Warning | Property or Method not in ApiRequest class + DA0005 | Design | Warning | Property or Method is inaccessible + DA0006 | Design | Error | Method returns void + DA0007 | Design | Error | Method accepts arguments \ No newline at end of file diff --git a/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs b/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs index f47364f..b7959cb 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs @@ -11,14 +11,15 @@ namespace DragonFruit.Data.Roslyn [DiagnosticAnalyzer(LanguageNames.CSharp)] public class ApiRequestAnalyzer : DiagnosticAnalyzer { - internal static readonly DiagnosticDescriptor PartialClassRule = new("DA0001", "Partial class expected", "Class '{0}' should be marked as partial", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor PartialClassRule = new("DA0001", "Partial class expected", "Class '{0}' should be marked as partial", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor NestedClassRule = new("DA0002", "Nested class not allowed", "Class '{0}' should not be nested", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); - internal static readonly DiagnosticDescriptor PropertyNoGetterRule = new("DA0002", "Property has no getter", "Property '{0}' has no accessible getter", "Design", DiagnosticSeverity.Warning, isEnabledByDefault: true); - internal static readonly DiagnosticDescriptor PropertyOrMethodNotInApiRequestRule = new("DA0003", "Property or Method not in ApiRequest", "'{0}' is not in an ApiRequest class", "Usage", DiagnosticSeverity.Warning, isEnabledByDefault: true); - internal static readonly DiagnosticDescriptor PropertyOrMethodInaccessibleRule = new("DA0004", "Property or Method is inaccessible", "'{0}' is inaccessible. Properties should either be public, protected or protected internal.", "Design", DiagnosticSeverity.Warning, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor PropertyNoGetterRule = new("DA0003", "Property has no getter", "Property '{0}' has no accessible getter", "Design", DiagnosticSeverity.Warning, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor PropertyOrMethodNotInApiRequestRule = new("DA0004", "Property or Method not in ApiRequest", "'{0}' is not in an ApiRequest class", "Usage", DiagnosticSeverity.Warning, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor PropertyOrMethodInaccessibleRule = new("DA0005", "Property or Method is inaccessible", "'{0}' is inaccessible. Properties should either be public, protected or protected internal.", "Design", DiagnosticSeverity.Warning, isEnabledByDefault: true); - internal static readonly DiagnosticDescriptor MethodReturnsVoidRule = new("DA0005", "Method returns void", "Method '{0}' used to provide request values returns void", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); - internal static readonly DiagnosticDescriptor MethodHasParametersRule = new("DA0006", "Method has parameters", "Method '{0}' used to provide request values takes arguments", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor MethodReturnsVoidRule = new("DA0006", "Method returns void", "Method '{0}' used to provide request values returns void", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor MethodHasParametersRule = new("DA0007", "Method has parameters", "Method '{0}' used to provide request values takes arguments", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(PartialClassRule, PropertyNoGetterRule, PropertyOrMethodNotInApiRequestRule, PropertyOrMethodInaccessibleRule, @@ -55,6 +56,12 @@ private void AnalyzeClassDecl(SyntaxNodeAnalysisContext context) { context.ReportDiagnostic(Diagnostic.Create(PartialClassRule, classDeclarationNode.Identifier.GetLocation(), classDeclarationNode.Identifier.Text)); } + + // check if class is nested + if (classDeclarationNode.Parent is not NamespaceDeclarationSyntax) + { + context.ReportDiagnostic(Diagnostic.Create(NestedClassRule, classDeclarationNode.Identifier.GetLocation(), classDeclarationNode.Identifier.Text)); + } } private void AnalyzeMethodDecl(SyntaxNodeAnalysisContext context) @@ -65,8 +72,11 @@ private void AnalyzeMethodDecl(SyntaxNodeAnalysisContext context) } var symbol = context.SemanticModel.GetDeclaredSymbol(methodDeclarationSyntax); + var bodyAttributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(RequestBodyAttribute).FullName); + var parameterAttributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(RequestParameterAttribute).FullName); - if (symbol?.GetAttributes().Any(a => a.AttributeClass?.Name is nameof(RequestParameterAttribute) or nameof(RequestBodyAttribute)) != true) + // check if symbol attributes are either body or parameter + if (symbol?.GetAttributes().Where(x => x.AttributeClass != null).Any(a => a.AttributeClass.Equals(bodyAttributeSymbol, SymbolEqualityComparer.Default) || a.AttributeClass.Equals(parameterAttributeSymbol, SymbolEqualityComparer.Default)) != true) { return; } @@ -105,8 +115,10 @@ private void AnalyzePropertyDecl(SyntaxNodeAnalysisContext context) } var symbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclarationSyntax); + var bodyAttributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(RequestBodyAttribute).FullName); + var parameterAttributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(RequestParameterAttribute).FullName); - if (symbol?.GetAttributes().Any(a => a.AttributeClass?.Name is nameof(RequestParameterAttribute) or nameof(RequestBodyAttribute)) != true) + if (symbol?.GetAttributes().Any(a => a.AttributeClass.Equals(bodyAttributeSymbol, SymbolEqualityComparer.Default) || a.AttributeClass.Equals(parameterAttributeSymbol, SymbolEqualityComparer.Default)) != true) { return; } From 694675348b7e124b1940d87925c693d4eb4a963a Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Mon, 11 Dec 2023 11:34:09 +0000 Subject: [PATCH 062/151] add basic analyzer source files --- .../DragonFruit.Data.Roslyn.Tests.csproj | 27 +++++++++++++++---- .../Common.cs | 15 +++++++++++ .../DA0001.Fix.cs | 10 +++++++ .../DA0001.cs | 13 +++++++++ .../DA0002.cs | 16 +++++++++++ .../DA0003.cs | 19 +++++++++++++ .../DA0004.cs | 16 +++++++++++ .../DA0005.cs | 15 +++++++++++ .../DA0006.cs | 15 +++++++++++ .../DA0007.cs | 15 +++++++++++ ...gonFruit.Data.Roslyn.Tests.TestData.csproj | 12 +++++++++ ...DragonFruit.Data.Roslyn.Tests.TestData.sln | 16 +++++++++++ 12 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/Common.cs create mode 100644 DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0001.Fix.cs create mode 100644 DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0001.cs create mode 100644 DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0002.cs create mode 100644 DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0003.cs create mode 100644 DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0004.cs create mode 100644 DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0005.cs create mode 100644 DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0006.cs create mode 100644 DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0007.cs create mode 100644 DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DragonFruit.Data.Roslyn.Tests.TestData.csproj create mode 100644 DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DragonFruit.Data.Roslyn.Tests.TestData.sln diff --git a/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj b/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj index a701dcb..e03d28a 100644 --- a/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj +++ b/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj @@ -4,21 +4,38 @@ net8.0 false + + + true + generated + - - - + + + + + + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all + + + + + + + + + diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/Common.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/Common.cs new file mode 100644 index 0000000..59fa514 --- /dev/null +++ b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/Common.cs @@ -0,0 +1,15 @@ +using System; + +namespace DragonFruit.Data +{ + public class ApiRequest; +} + +namespace DragonFruit.Data.Requests +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] + public class RequestParameterAttribute : Attribute; + + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] + public class RequestBodyAttribute : Attribute; +} diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0001.Fix.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0001.Fix.cs new file mode 100644 index 0000000..52bdc36 --- /dev/null +++ b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0001.Fix.cs @@ -0,0 +1,10 @@ +namespace DragonFruit.Data.Roslyn.Tests.TestData; + +/// +/// Dummy request with no partial class modifier (DA0001) +/// +public partial class DA0001 : ApiRequest +{ + [RequestParameter] + public string TestParam { get; set; } +} \ No newline at end of file diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0001.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0001.cs new file mode 100644 index 0000000..f759d02 --- /dev/null +++ b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0001.cs @@ -0,0 +1,13 @@ +using DragonFruit.Data.Requests; + +namespace DragonFruit.Data.Roslyn.Tests.TestData +{ + /// + /// Dummy request with no partial class modifier (DA0001) + /// + public class DA0001 : ApiRequest + { + [RequestParameter] + public string TestParam { get; set; } + } +} \ No newline at end of file diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0002.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0002.cs new file mode 100644 index 0000000..d9b5cf9 --- /dev/null +++ b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0002.cs @@ -0,0 +1,16 @@ +using DragonFruit.Data.Requests; + +namespace DragonFruit.Data.Roslyn.Tests.TestData +{ + /// + /// Dummy request nested in another class (DA0002) + /// + public class DA0002 + { + public class DA0002_Req : ApiRequest + { + [RequestParameter] + public int Id { get; set; } + } + } +} \ No newline at end of file diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0003.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0003.cs new file mode 100644 index 0000000..57735f2 --- /dev/null +++ b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0003.cs @@ -0,0 +1,19 @@ +using DragonFruit.Data.Requests; + +namespace DragonFruit.Data.Roslyn.Tests.TestData +{ + /// + /// Dummy request, property with no getter (DA0003) + /// + public class DA0003 : ApiRequest + { + [RequestParameter] + public string Param1 { get; set; } + + [RequestParameter] + public int Param2 + { + set => Param1 = value.ToString(); + } + } +} \ No newline at end of file diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0004.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0004.cs new file mode 100644 index 0000000..1d75162 --- /dev/null +++ b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0004.cs @@ -0,0 +1,16 @@ +using DragonFruit.Data.Requests; + +namespace DragonFruit.Data.Roslyn.Tests.TestData +{ + /// + /// Parameters not in an ApiRequest (DA0004) + /// + public class DA0004 + { + [RequestParameter] + public string NotAParam { get; set; } + + [RequestParameter] + public int TestData() => 10; + } +} \ No newline at end of file diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0005.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0005.cs new file mode 100644 index 0000000..268e1bf --- /dev/null +++ b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0005.cs @@ -0,0 +1,15 @@ +using DragonFruit.Data.Requests; + +namespace DragonFruit.Data.Roslyn.Tests.TestData; + +/// +/// Dummy request with private getter/private method (DA0005) +/// +public class DA0005 : ApiRequest +{ + [RequestParameter] + public string Parameter { private get; set; } + + [RequestParameter] + private bool IsParameterSet() => Parameter != null; +} \ No newline at end of file diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0006.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0006.cs new file mode 100644 index 0000000..07b3e35 --- /dev/null +++ b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0006.cs @@ -0,0 +1,15 @@ +using DragonFruit.Data.Requests; + +namespace DragonFruit.Data.Roslyn.Tests.TestData; + +/// +/// Dummy request with a void return type (DA0006) +/// +public class DA0006 : ApiRequest +{ + [RequestParameter] + public void GetUserData() + { + + } +} \ No newline at end of file diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0007.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0007.cs new file mode 100644 index 0000000..243e750 --- /dev/null +++ b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0007.cs @@ -0,0 +1,15 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using DragonFruit.Data.Requests; + +namespace DragonFruit.Data.Roslyn.Tests.TestData; + +/// +/// Dummy request, method with parameters (DA0007) +/// +public class DA0007 : ApiRequest +{ + [RequestParameter] + public string UserId(string originalId) => Convert.ToHexString(MD5.HashData(Encoding.ASCII.GetBytes(originalId))); +} \ No newline at end of file diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DragonFruit.Data.Roslyn.Tests.TestData.csproj b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DragonFruit.Data.Roslyn.Tests.TestData.csproj new file mode 100644 index 0000000..30d2f86 --- /dev/null +++ b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DragonFruit.Data.Roslyn.Tests.TestData.csproj @@ -0,0 +1,12 @@ + + + + net8.0 + + + + + + + + diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DragonFruit.Data.Roslyn.Tests.TestData.sln b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DragonFruit.Data.Roslyn.Tests.TestData.sln new file mode 100644 index 0000000..7389638 --- /dev/null +++ b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DragonFruit.Data.Roslyn.Tests.TestData.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Roslyn.Tests.TestData", "DragonFruit.Data.Roslyn.Tests.TestData.csproj", "{B4D5D8D2-92A9-498C-A234-91BEAB9B8C4E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B4D5D8D2-92A9-498C-A234-91BEAB9B8C4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B4D5D8D2-92A9-498C-A234-91BEAB9B8C4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B4D5D8D2-92A9-498C-A234-91BEAB9B8C4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B4D5D8D2-92A9-498C-A234-91BEAB9B8C4E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal From 6a5c5da996c92ccac1a7b0607b8b52310eec28dc Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Mon, 11 Dec 2023 11:37:10 +0000 Subject: [PATCH 063/151] move test source files --- .../{DragonFruit.Data.Roslyn.Tests.TestData => }/Common.cs | 0 .../{DragonFruit.Data.Roslyn.Tests.TestData => }/DA0001.Fix.cs | 0 .../{DragonFruit.Data.Roslyn.Tests.TestData => }/DA0001.cs | 0 .../{DragonFruit.Data.Roslyn.Tests.TestData => }/DA0002.cs | 0 .../{DragonFruit.Data.Roslyn.Tests.TestData => }/DA0003.cs | 0 .../{DragonFruit.Data.Roslyn.Tests.TestData => }/DA0004.cs | 0 .../{DragonFruit.Data.Roslyn.Tests.TestData => }/DA0005.cs | 0 .../{DragonFruit.Data.Roslyn.Tests.TestData => }/DA0006.cs | 0 .../{DragonFruit.Data.Roslyn.Tests.TestData => }/DA0007.cs | 0 .../DragonFruit.Data.Roslyn.Tests.TestData.csproj | 0 .../DragonFruit.Data.Roslyn.Tests.TestData.sln | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename DragonFruit.Data.Roslyn.Tests/_TestData/{DragonFruit.Data.Roslyn.Tests.TestData => }/Common.cs (100%) rename DragonFruit.Data.Roslyn.Tests/_TestData/{DragonFruit.Data.Roslyn.Tests.TestData => }/DA0001.Fix.cs (100%) rename DragonFruit.Data.Roslyn.Tests/_TestData/{DragonFruit.Data.Roslyn.Tests.TestData => }/DA0001.cs (100%) rename DragonFruit.Data.Roslyn.Tests/_TestData/{DragonFruit.Data.Roslyn.Tests.TestData => }/DA0002.cs (100%) rename DragonFruit.Data.Roslyn.Tests/_TestData/{DragonFruit.Data.Roslyn.Tests.TestData => }/DA0003.cs (100%) rename DragonFruit.Data.Roslyn.Tests/_TestData/{DragonFruit.Data.Roslyn.Tests.TestData => }/DA0004.cs (100%) rename DragonFruit.Data.Roslyn.Tests/_TestData/{DragonFruit.Data.Roslyn.Tests.TestData => }/DA0005.cs (100%) rename DragonFruit.Data.Roslyn.Tests/_TestData/{DragonFruit.Data.Roslyn.Tests.TestData => }/DA0006.cs (100%) rename DragonFruit.Data.Roslyn.Tests/_TestData/{DragonFruit.Data.Roslyn.Tests.TestData => }/DA0007.cs (100%) rename DragonFruit.Data.Roslyn.Tests/_TestData/{DragonFruit.Data.Roslyn.Tests.TestData => }/DragonFruit.Data.Roslyn.Tests.TestData.csproj (100%) rename DragonFruit.Data.Roslyn.Tests/_TestData/{DragonFruit.Data.Roslyn.Tests.TestData => }/DragonFruit.Data.Roslyn.Tests.TestData.sln (100%) diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/Common.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/Common.cs similarity index 100% rename from DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/Common.cs rename to DragonFruit.Data.Roslyn.Tests/_TestData/Common.cs diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0001.Fix.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0001.Fix.cs similarity index 100% rename from DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0001.Fix.cs rename to DragonFruit.Data.Roslyn.Tests/_TestData/DA0001.Fix.cs diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0001.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0001.cs similarity index 100% rename from DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0001.cs rename to DragonFruit.Data.Roslyn.Tests/_TestData/DA0001.cs diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0002.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0002.cs similarity index 100% rename from DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0002.cs rename to DragonFruit.Data.Roslyn.Tests/_TestData/DA0002.cs diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0003.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0003.cs similarity index 100% rename from DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0003.cs rename to DragonFruit.Data.Roslyn.Tests/_TestData/DA0003.cs diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0004.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0004.cs similarity index 100% rename from DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0004.cs rename to DragonFruit.Data.Roslyn.Tests/_TestData/DA0004.cs diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0005.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0005.cs similarity index 100% rename from DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0005.cs rename to DragonFruit.Data.Roslyn.Tests/_TestData/DA0005.cs diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0006.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0006.cs similarity index 100% rename from DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0006.cs rename to DragonFruit.Data.Roslyn.Tests/_TestData/DA0006.cs diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0007.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0007.cs similarity index 100% rename from DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DA0007.cs rename to DragonFruit.Data.Roslyn.Tests/_TestData/DA0007.cs diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DragonFruit.Data.Roslyn.Tests.TestData.csproj b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData.csproj similarity index 100% rename from DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DragonFruit.Data.Roslyn.Tests.TestData.csproj rename to DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData.csproj diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DragonFruit.Data.Roslyn.Tests.TestData.sln b/DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData.sln similarity index 100% rename from DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData/DragonFruit.Data.Roslyn.Tests.TestData.sln rename to DragonFruit.Data.Roslyn.Tests/_TestData/DragonFruit.Data.Roslyn.Tests.TestData.sln From 81e010d2058b504e0d1bfe3cb1cd6e931454f0af Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Mon, 11 Dec 2023 12:57:56 +0000 Subject: [PATCH 064/151] update derivesfrom to ignore original class type --- DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 24d5797..4fae6c6 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -274,7 +274,7 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil { var psm = new PropertySymbolMetadata(candidate, returnType, parameterName); - if (DerivesFrom(returnType, streamTypeSymbol)) + if (streamTypeSymbol.Equals(returnType, SymbolEqualityComparer.Default) || DerivesFrom(returnType, streamTypeSymbol)) { psm.SpecialRequestParameter = SpecialRequestParameter.Stream; } @@ -300,7 +300,7 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil internal static bool DerivesFrom(ITypeSymbol type, ITypeSymbol baseType) { - var classSymbol = type; + var classSymbol = type.BaseType; while (classSymbol != null) { From f1508c9f14b5608d6f9639231a3fef1d638fa717 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Mon, 11 Dec 2023 12:58:32 +0000 Subject: [PATCH 065/151] fix DA0001 test, add base for other tests --- .../ApiRequestAnalyzerTests.cs | 60 +++++++++++++------ .../DragonFruit.Data.Roslyn.Tests.csproj | 10 +--- .../TestEntities/BasicHttpRequest.cs | 21 ------- .../_TestData/DA0001.Fix.cs | 19 +++--- .../_TestData/DA0001.cs | 2 +- 5 files changed, 54 insertions(+), 58 deletions(-) delete mode 100644 DragonFruit.Data.Roslyn.Tests/TestEntities/BasicHttpRequest.cs diff --git a/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs b/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs index 290b639..b86873e 100644 --- a/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs +++ b/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs @@ -1,35 +1,57 @@ +using System.IO; +using System.Linq; using System.Threading.Tasks; +using DragonFruit.Data.Roslyn.Fixes; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; using Xunit; -using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.XUnit.CodeFixVerifier; namespace DragonFruit.Data.Roslyn.Tests; public class ApiRequestAnalyzerTests { + private readonly string _testDataPath; + + public ApiRequestAnalyzerTests() + { + _testDataPath = Path.Combine(GetSolutionRoot(), "DragonFruit.Data.Roslyn.Tests", "_TestData"); + } + [Fact] public async Task TestNonPartialClassDetectionAndFix() { - const string text = @" -namespace DragonFruit.Data; + var test = new CSharpCodeFixTest + { + TestCode = await File.ReadAllTextAsync(Path.Combine(_testDataPath, "DA0001.cs")), + FixedCode = await File.ReadAllTextAsync(Path.Combine(_testDataPath, "DA0001.Fix.cs")), + ExpectedDiagnostics = { DiagnosticResult.CompilerError(ApiRequestAnalyzer.PartialClassRule.Id).WithSpan(8, 18, 8, 24).WithArguments("DA0001") } + }; -public class ApiRequest { } -public class TestRequest : ApiRequest -{ - public string RequestPath => ""https://google.com""; -} -"; + await PerformTest(test); + } - const string newText = @" -namespace DragonFruit.Data; + private async Task PerformTest(AnalyzerTest test) + { + var content = ("Common.cs", await File.ReadAllTextAsync(Path.Combine(_testDataPath, "Common.cs"))); + test.TestState.Sources.Add(content); -public class ApiRequest { } -public partial class TestRequest : ApiRequest -{ - public string RequestPath => ""https://google.com""; -} -"; + if (test is CodeFixTest verifier) + { + verifier.FixedState.Sources.Add(content); + } + + await test.RunAsync(); + } + + private string GetSolutionRoot() + { + var currentDirectory = Directory.GetCurrentDirectory(); + + while (Directory.EnumerateFiles(currentDirectory).All(x => Path.GetFileName(x) != "DragonFruit.Data.sln")) + { + currentDirectory = Path.Combine(currentDirectory, ".."); + } - var expectedDiagnostic = Verifier.Diagnostic().WithSpan(5, 14, 5, 25).WithArguments("TestRequest"); - await Verifier.VerifyCodeFixAsync(text, expectedDiagnostic, newText).ConfigureAwait(false); + return Path.GetFullPath(currentDirectory); } } diff --git a/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj b/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj index e03d28a..59c7927 100644 --- a/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj +++ b/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj @@ -4,18 +4,10 @@ net8.0 false - - - true - generated - - - - @@ -26,7 +18,7 @@ - + diff --git a/DragonFruit.Data.Roslyn.Tests/TestEntities/BasicHttpRequest.cs b/DragonFruit.Data.Roslyn.Tests/TestEntities/BasicHttpRequest.cs deleted file mode 100644 index 719a512..0000000 --- a/DragonFruit.Data.Roslyn.Tests/TestEntities/BasicHttpRequest.cs +++ /dev/null @@ -1,21 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using DragonFruit.Data.Requests; - -namespace DragonFruit.Data.Roslyn.Tests.TestEntities -{ - public partial class BasicHttpRequest : ApiRequest - { - public override string RequestPath => "https://postman-echo.com/get"; - - [RequestParameter(ParameterType.Query, "q1")] - public string Query1 { get; set; } - - [RequestParameter(ParameterType.Query, "q2")] - public string Query2 { get; set; } - - [RequestParameter(ParameterType.Query, "q3")] - public string Query3 { get; set; } - } -} diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DA0001.Fix.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0001.Fix.cs index 52bdc36..97f8481 100644 --- a/DragonFruit.Data.Roslyn.Tests/_TestData/DA0001.Fix.cs +++ b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0001.Fix.cs @@ -1,10 +1,13 @@ -namespace DragonFruit.Data.Roslyn.Tests.TestData; +using DragonFruit.Data.Requests; -/// -/// Dummy request with no partial class modifier (DA0001) -/// -public partial class DA0001 : ApiRequest +namespace DragonFruit.Data.Roslyn.Tests.TestData { - [RequestParameter] - public string TestParam { get; set; } -} \ No newline at end of file + /// + /// Dummy request with no partial class modifier (DA0001) + /// + public partial class DA0001 : ApiRequest + { + [RequestParameter] + public string TestParam { get; set; } + } +} diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DA0001.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0001.cs index f759d02..fa6ab33 100644 --- a/DragonFruit.Data.Roslyn.Tests/_TestData/DA0001.cs +++ b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0001.cs @@ -10,4 +10,4 @@ public class DA0001 : ApiRequest [RequestParameter] public string TestParam { get; set; } } -} \ No newline at end of file +} From ba02b4289965afe85f892fde43f57a95351af74e Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Mon, 11 Dec 2023 12:59:22 +0000 Subject: [PATCH 066/151] add comment --- DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs b/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs index b86873e..40552ee 100644 --- a/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs +++ b/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs @@ -33,6 +33,8 @@ public async Task TestNonPartialClassDetectionAndFix() private async Task PerformTest(AnalyzerTest test) { var content = ("Common.cs", await File.ReadAllTextAsync(Path.Combine(_testDataPath, "Common.cs"))); + + // add common.cs to test sources test.TestState.Sources.Add(content); if (test is CodeFixTest verifier) From 8b96cfd0911271e62186ae913c8fb4e80fc446ad Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 12 Dec 2023 10:13:41 +0000 Subject: [PATCH 067/151] add nested class rule to supported diagnostics list --- DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs b/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs index b7959cb..00ec311 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs @@ -12,7 +12,7 @@ namespace DragonFruit.Data.Roslyn public class ApiRequestAnalyzer : DiagnosticAnalyzer { public static readonly DiagnosticDescriptor PartialClassRule = new("DA0001", "Partial class expected", "Class '{0}' should be marked as partial", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor NestedClassRule = new("DA0002", "Nested class not allowed", "Class '{0}' should not be nested", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor NestedClassNotAllowedRule = new("DA0002", "Nested class not allowed", "Class '{0}' should not be nested", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); public static readonly DiagnosticDescriptor PropertyNoGetterRule = new("DA0003", "Property has no getter", "Property '{0}' has no accessible getter", "Design", DiagnosticSeverity.Warning, isEnabledByDefault: true); public static readonly DiagnosticDescriptor PropertyOrMethodNotInApiRequestRule = new("DA0004", "Property or Method not in ApiRequest", "'{0}' is not in an ApiRequest class", "Usage", DiagnosticSeverity.Warning, isEnabledByDefault: true); @@ -21,7 +21,7 @@ public class ApiRequestAnalyzer : DiagnosticAnalyzer public static readonly DiagnosticDescriptor MethodReturnsVoidRule = new("DA0006", "Method returns void", "Method '{0}' used to provide request values returns void", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); public static readonly DiagnosticDescriptor MethodHasParametersRule = new("DA0007", "Method has parameters", "Method '{0}' used to provide request values takes arguments", "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(PartialClassRule, + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(PartialClassRule, NestedClassNotAllowedRule, PropertyNoGetterRule, PropertyOrMethodNotInApiRequestRule, PropertyOrMethodInaccessibleRule, MethodReturnsVoidRule, MethodHasParametersRule); @@ -60,7 +60,7 @@ private void AnalyzeClassDecl(SyntaxNodeAnalysisContext context) // check if class is nested if (classDeclarationNode.Parent is not NamespaceDeclarationSyntax) { - context.ReportDiagnostic(Diagnostic.Create(NestedClassRule, classDeclarationNode.Identifier.GetLocation(), classDeclarationNode.Identifier.Text)); + context.ReportDiagnostic(Diagnostic.Create(NestedClassNotAllowedRule, classDeclarationNode.Identifier.GetLocation(), classDeclarationNode.Identifier.Text)); } } From 43dc5f88533458698027c032819a371fc2e8b40c Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 12 Dec 2023 10:18:29 +0000 Subject: [PATCH 068/151] add options to serialize method of json serializer --- DragonFruit.Data/Serializers/ApiJsonSerializer.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/DragonFruit.Data/Serializers/ApiJsonSerializer.cs b/DragonFruit.Data/Serializers/ApiJsonSerializer.cs index f4ab386..7f72795 100644 --- a/DragonFruit.Data/Serializers/ApiJsonSerializer.cs +++ b/DragonFruit.Data/Serializers/ApiJsonSerializer.cs @@ -1,7 +1,6 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details -using System; using System.IO; using System.Net.Http; using System.Net.Http.Headers; @@ -15,11 +14,7 @@ public class ApiJsonSerializer : ApiSerializer, IAsyncSerializer { public override string ContentType => "application/json"; - public override Encoding Encoding - { - get => Encoding.UTF8; - set => throw new InvalidOperationException("System.Text.Json Serializer does not support custom encodings"); - } + public override Encoding Encoding => Encoding.UTF8; /// /// Gets or sets the current used. @@ -28,7 +23,7 @@ public override Encoding Encoding public override HttpContent Serialize(T input) { - var utf8Bytes = JsonSerializer.SerializeToUtf8Bytes(input, typeof(T)); + var utf8Bytes = JsonSerializer.SerializeToUtf8Bytes(input, typeof(T), SerializerOptions); var httpContent = new ByteArrayContent(utf8Bytes); httpContent.Headers.ContentType = new MediaTypeHeaderValue(ContentType) From 261defd884a139eb4973e77a598ad8c2ea736bc6 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 12 Dec 2023 10:23:29 +0000 Subject: [PATCH 069/151] add xmldoc to apirequest --- DragonFruit.Data/ApiRequest.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/DragonFruit.Data/ApiRequest.cs b/DragonFruit.Data/ApiRequest.cs index e97d1fb..3eac403 100644 --- a/DragonFruit.Data/ApiRequest.cs +++ b/DragonFruit.Data/ApiRequest.cs @@ -2,13 +2,31 @@ // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details using System.Net.Http; +using System.Threading.Tasks; +using DragonFruit.Data.Requests; namespace DragonFruit.Data { + /// + /// The base class for all requests. + /// Properties and methods decorated with are used to build the request. + /// public abstract class ApiRequest { + /// + /// The base url of the request + /// public abstract string RequestPath { get; } + /// + /// The to use when making the request + /// public virtual HttpMethod RequestMethod => HttpMethod.Get; + + /// + /// Overridable method to be called when the request is about to be built. + /// This can be used to inject headers with data managed by the client, etc. + /// + protected virtual ValueTask RequestCreatingCallback() => default; } } From 5ad03cb71f8f49d2ea05549ce6d990360dcdf794 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 12 Dec 2023 10:29:43 +0000 Subject: [PATCH 070/151] port serializerresolver from old project --- .../Serializers/SerializerResolver.cs | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 DragonFruit.Data/Serializers/SerializerResolver.cs diff --git a/DragonFruit.Data/Serializers/SerializerResolver.cs b/DragonFruit.Data/Serializers/SerializerResolver.cs new file mode 100644 index 0000000..1d50361 --- /dev/null +++ b/DragonFruit.Data/Serializers/SerializerResolver.cs @@ -0,0 +1,182 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace DragonFruit.Data.Serializers +{ + public class SerializerResolver + { + private static readonly Dictionary SerializerMap = new Dictionary(); + private static readonly Dictionary DeserializerMap = new Dictionary(); + + private readonly ConcurrentDictionary _serializerCache = new ConcurrentDictionary(); + + /// + /// Initialises a new instance of , providing a default + /// + internal SerializerResolver(ApiSerializer @default) + { + Default = @default; + } + + /// + /// The default in use. + /// + public ApiSerializer Default { get; } + + /// + /// Registers a serializer for the specified type. This applies to all s + /// + /// Whether this serializer should apply to incoming/outgoing data + /// The object type to specify the serializer for + /// The serializer to apply + public static void Register(DataDirection direction = DataDirection.All) + where T : class + where TSerializer : ApiSerializer, new() + { + Register(typeof(T), direction); + } + + /// + /// Registers a serializer for the specified type. This applies to all s + /// + /// The object type to specify the serializer for + /// Whether this serializer should apply to incoming/outgoing data + /// The serializer to apply + public static void Register(Type targetType, DataDirection direction = DataDirection.All) where TSerializer : ApiSerializer, new() + { + if (!targetType.IsClass) + { + throw new ArgumentException($"{targetType.Name} is not a class", nameof(targetType)); + } + + if ((direction & DataDirection.In) == DataDirection.In) + { + DeserializerMap[targetType] = typeof(TSerializer); + } + + if ((direction & DataDirection.Out) == DataDirection.Out) + { + SerializerMap[targetType] = typeof(TSerializer); + } + } + + /// + /// Removes the registered serializer for the type. This applies to all s + /// + /// Whether this serializer should be removed from incoming/outgoing data + /// The object type to remove the serializer for + public static void Unregister(DataDirection direction = DataDirection.All) where T : class + { + Unregister(typeof(T), direction); + } + + /// + /// Removes the registered serializer for the type. This applies to all s + /// + /// The object type to remove the serializer for + /// Whether this serializer should be removed from incoming/outgoing data + public static void Unregister(Type targetType, DataDirection direction = DataDirection.All) + { + if (!targetType.IsClass) + { + throw new ArgumentException($"{targetType.Name} is not a class", nameof(targetType)); + } + + if ((direction & DataDirection.In) == DataDirection.In) + { + DeserializerMap.Remove(targetType); + } + + if ((direction & DataDirection.Out) == DataDirection.Out) + { + SerializerMap.Remove(targetType); + } + } + + /// + /// Resolves the for the type provided + /// + /// The type to resolve + public ApiSerializer Resolve(DataDirection direction) where T : class + { + return Resolve(typeof(T), direction); + } + + /// + /// Resolves the for the type provided + /// + public ApiSerializer Resolve(Type objectType, DataDirection direction) + { + if (!objectType.IsClass) + { + // at this point in time, we only support non-generic class + // this is because this isn't designed to filter generic types + return Default; + } + + var mapping = direction switch + { + DataDirection.In => DeserializerMap, + DataDirection.Out => SerializerMap, + + _ => throw new ArgumentException(nameof(direction)) + }; + + Type serializerType; + + // if the map has the type registered, check the type in cache + if (mapping.TryGetValue(objectType, out serializerType) || (objectType.IsConstructedGenericType && mapping.TryGetValue(objectType.GetGenericTypeDefinition(), out serializerType))) + { + return _serializerCache.GetOrAdd(serializerType, _ => (ApiSerializer)Activator.CreateInstance(serializerType)); + } + + // use generic + return Default; + } + + /// + /// Configures the specified , creating a client-specific instance if needed + /// + /// The options to set + /// The to configure + public void Configure(Action options) where TSerializer : ApiSerializer + { + if (Default.GetType() == typeof(TSerializer)) + { + options?.Invoke((TSerializer)Default); + } + else if (DeserializerMap.ContainsValue(typeof(TSerializer)) || SerializerMap.ContainsValue(typeof(TSerializer))) + { + var serializer = _serializerCache.GetOrAdd(typeof(TSerializer), _ => Activator.CreateInstance()); + options?.Invoke((TSerializer)serializer); + } + else + { + throw new ArgumentException("The specified serializer was not registered anywhere. It needs to be registered or set as the default before configuration can occur", nameof(TSerializer)); + } + } + } + + [Flags] + public enum DataDirection + { + /// + /// Applies to incoming data (deserialization) + /// + In = 1, + + /// + /// Applies to outgoing data (serialization) + /// + Out = 2, + + /// + /// Applies to all data directions + /// + All = In | Out + } +} \ No newline at end of file From 1bdbd26ddad7863e4f472c2fd3fde871b697a88f Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 12 Dec 2023 10:29:59 +0000 Subject: [PATCH 071/151] add resolver to request builder template --- DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid | 6 ++++-- DragonFruit.Data/Requests/IRequestBuilder.cs | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid index e25a240..8aa238e 100644 --- a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid @@ -8,7 +8,7 @@ namespace {{ namespace }} { partial class {{ class_name }} : global::DragonFruit.Data.Requests.IRequestBuilder { - public global::System.Net.Http.HttpRequestMessage BuildRequest() + public global::System.Net.Http.HttpRequestMessage BuildRequest(global::DragonFruit.Data.Serializers.SerializerResolver serializerResolver) { global::System.UriBuilder uriBuilder = new global::System.UriBuilder(this.RequestPath); @@ -121,7 +121,9 @@ namespace {{ namespace }} {% when 3 -%} request.Content = {{ request_body_symbol.accessor }}; - {% comment %} 4 - todo Custom Body (Serialized) {% endcomment %} + {% comment %} 4 - Custom Body (Serialized) {% endcomment %} + {% when 4 -%} + request.Content = serializerResolver.Resolve({{ request_body_symbol.accessor }}.GetType(), global::DragonFruit.Data.Serializers.DataDirection.Out).Serialize({{ request_body_symbol.accessor }}); {% endcase -%} {% comment %} Process Headers {% endcomment %} diff --git a/DragonFruit.Data/Requests/IRequestBuilder.cs b/DragonFruit.Data/Requests/IRequestBuilder.cs index 437125a..1975eef 100644 --- a/DragonFruit.Data/Requests/IRequestBuilder.cs +++ b/DragonFruit.Data/Requests/IRequestBuilder.cs @@ -2,11 +2,12 @@ // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details using System.Net.Http; +using DragonFruit.Data.Serializers; namespace DragonFruit.Data.Requests { public interface IRequestBuilder { - HttpRequestMessage BuildRequest(); + HttpRequestMessage BuildRequest(SerializerResolver serializerResolver); } } From 7d8305ec15099e99711f5f7e39d0f28762be1f9d Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 12 Dec 2023 10:30:43 +0000 Subject: [PATCH 072/151] make serializer resolver public --- DragonFruit.Data/Serializers/SerializerResolver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DragonFruit.Data/Serializers/SerializerResolver.cs b/DragonFruit.Data/Serializers/SerializerResolver.cs index 1d50361..35cbb38 100644 --- a/DragonFruit.Data/Serializers/SerializerResolver.cs +++ b/DragonFruit.Data/Serializers/SerializerResolver.cs @@ -17,7 +17,7 @@ public class SerializerResolver /// /// Initialises a new instance of , providing a default /// - internal SerializerResolver(ApiSerializer @default) + public SerializerResolver(ApiSerializer @default) { Default = @default; } From dee4c5e2975368869e3cef2badd8aaf94dd6d6e8 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 12 Dec 2023 13:14:46 +0000 Subject: [PATCH 073/151] add request execution callback --- .../Requests/IRequestExecutingCallback.cs | 17 ++++++++++++++++ .../Requests/RequestCallbackAttribute.cs | 20 ------------------- 2 files changed, 17 insertions(+), 20 deletions(-) create mode 100644 DragonFruit.Data/Requests/IRequestExecutingCallback.cs delete mode 100644 DragonFruit.Data/Requests/RequestCallbackAttribute.cs diff --git a/DragonFruit.Data/Requests/IRequestExecutingCallback.cs b/DragonFruit.Data/Requests/IRequestExecutingCallback.cs new file mode 100644 index 0000000..2854793 --- /dev/null +++ b/DragonFruit.Data/Requests/IRequestExecutingCallback.cs @@ -0,0 +1,17 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System.Threading.Tasks; + +namespace DragonFruit.Data.Requests +{ + public interface IRequestExecutingCallback + { + public void OnRequestExecuting(ApiClient client); + } + + public interface IAsyncRequestExecutingCallback + { + public ValueTask OnRequestExecuting(ApiClient client); + } +} diff --git a/DragonFruit.Data/Requests/RequestCallbackAttribute.cs b/DragonFruit.Data/Requests/RequestCallbackAttribute.cs deleted file mode 100644 index dde0a37..0000000 --- a/DragonFruit.Data/Requests/RequestCallbackAttribute.cs +++ /dev/null @@ -1,20 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; - -namespace DragonFruit.Data.Requests -{ - /// - /// Marks the annotated method as a callback before returning the final request object - /// - [AttributeUsage(AttributeTargets.Method)] - public class RequestCallbackAttribute : Attribute - { - /// - /// The order to invoke the callback. - /// Lower value results in earlier execution. - /// - public int Order { get; } = 0; - } -} From a24c9d319e7a9cc22033f56df508008941e629c1 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 12 Dec 2023 13:15:08 +0000 Subject: [PATCH 074/151] add base implementation of ApiClient and ApiClient --- DragonFruit.Data/ApiClient.cs | 189 ++++++++++++++++++ .../ReflectionRequestMessageBuilder.cs | 16 ++ 2 files changed, 205 insertions(+) create mode 100644 DragonFruit.Data/ApiClient.cs create mode 100644 DragonFruit.Data/Requests/ReflectionRequestMessageBuilder.cs diff --git a/DragonFruit.Data/ApiClient.cs b/DragonFruit.Data/ApiClient.cs new file mode 100644 index 0000000..069cf1e --- /dev/null +++ b/DragonFruit.Data/ApiClient.cs @@ -0,0 +1,189 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using DragonFruit.Data.Requests; +using DragonFruit.Data.Serializers; + +namespace DragonFruit.Data +{ + /// + /// The responsible for building, submitting and processing HTTP requests + /// + public class ApiClient + { + private HttpClient _client; + + public ApiClient(ApiSerializer serializer) + { + Serializers = new SerializerResolver(serializer); + } + + ~ApiClient() + { + _client?.Dispose(); + } + + /// + /// Gets or sets the User-Agent used in HTTP requests + /// + public string UserAgent + { + get => Client.DefaultRequestHeaders.UserAgent.ToString(); + set + { + Client.DefaultRequestHeaders.UserAgent.Clear(); + Client.DefaultRequestHeaders.UserAgent.ParseAdd(value); + } + } + + /// + /// The instance used to resolve serializers for requests. + /// Caches and reused serializers where possible. + /// + public SerializerResolver Serializers { get; } + + /// + /// Gets the header container for the underlying + /// + public HttpRequestHeaders Headers => Client.DefaultRequestHeaders; + + /// + /// User-Controlled method to create a + /// + public Func Handler { get; set; } + + /// + /// Gets the used across all requests. + /// + private HttpClient Client + { + get + { + if (_client != null) + { + return _client; + } + + _client = new HttpClient(CreateHandler(), true); + +#if !NETSTANDARD2_0 + // on newer platforms, enable HTTP/2 (and HTTP/3) + _client.DefaultRequestVersion = HttpVersion.Version11; + _client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; +#endif + + return _client; + } + } + + /// + /// Overridable method used to control creation of a used by the internal HTTP client. + /// + /// + /// This is designed to be used by libraries requiring overall control of handlers (i.e. wrap the user-selected handler to provide additional functionality) + /// + protected virtual HttpMessageHandler CreateHandler() => Handler?.Invoke() ?? CreateDefaultHandler(); + + /// + /// Overridable handler for validating and processing a + /// + protected virtual async Task ValidateAndProcess(HttpResponseMessage response, CancellationToken cancellationToken) where T : class + { + response.EnsureSuccessStatusCode(); + +#if NETSTANDARD2_0 + using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); +#else + using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); +#endif + + var serializer = Serializers.Resolve(DataDirection.In); + + if (serializer is IAsyncSerializer asyncSerializer) + { + return await asyncSerializer.DeserializeAsync(stream).ConfigureAwait(false); + } + + return serializer.Deserialize(stream); + } + + /// + /// Performs a request, deserializing the results into the specified type. + /// + /// + /// If source generation is enabled, the source generated method will be used to build the request, otherwise a legacy reflection handler will build the request. + /// + public async Task PerformAsync(ApiRequest request, CancellationToken cancellationToken = default) where T : class + { + using var requestMessage = await BuildRequest(request).ConfigureAwait(false); + using var responseMessage = await Client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + + return await ValidateAndProcess(responseMessage, cancellationToken).ConfigureAwait(false); + } + + /// + /// Performs a request, returning the raw for the caller to process + /// + public async Task PerformAsync(ApiRequest request, CancellationToken cancellationToken = default) + { + using var requestMessage = await BuildRequest(request).ConfigureAwait(false); + return await Client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + } + + /// + /// Overridable method used to build a from an + /// + /// The request to build a for + /// The to send + protected virtual async ValueTask BuildRequest(ApiRequest request) + { + if (request is IRequestExecutingCallback callback) + { + callback.OnRequestExecuting(this); + } + + if (request is IAsyncRequestExecutingCallback asyncCallback) + { + await asyncCallback.OnRequestExecuting(this); + } + + return request is IRequestBuilder rb ? rb.BuildRequest(Serializers) : ReflectionRequestMessageBuilder.CreateHttpRequestMessage(request, this); + } + + private HttpMessageHandler CreateDefaultHandler() + { +#if NETSTANDARD2_0 + return new HttpClientHandler + { + UseCookies = true, + AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip + }; +#else + return new SocketsHttpHandler + { + UseCookies = false, + AutomaticDecompression = DecompressionMethods.All, + PooledConnectionLifetime = TimeSpan.FromMinutes(10) + }; +#endif + } + } + + /// + /// Represents a strongly-typed serializer version of + /// + /// The type of the + public class ApiClient : ApiClient where T : ApiSerializer, new() + { + public ApiClient() + : base(Activator.CreateInstance()) + { + } + } +} diff --git a/DragonFruit.Data/Requests/ReflectionRequestMessageBuilder.cs b/DragonFruit.Data/Requests/ReflectionRequestMessageBuilder.cs new file mode 100644 index 0000000..36cb223 --- /dev/null +++ b/DragonFruit.Data/Requests/ReflectionRequestMessageBuilder.cs @@ -0,0 +1,16 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System; +using System.Net.Http; + +namespace DragonFruit.Data.Requests +{ + internal static class ReflectionRequestMessageBuilder + { + public static HttpRequestMessage CreateHttpRequestMessage(ApiRequest request, ApiClient client) + { + throw new NotImplementedException(); + } + } +} From 0669c3455fe1c366b819c6cb8dc921bf4a46033b Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 12 Dec 2023 13:18:57 +0000 Subject: [PATCH 075/151] add Accept header to all requests --- DragonFruit.Data/ApiClient.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/DragonFruit.Data/ApiClient.cs b/DragonFruit.Data/ApiClient.cs index 069cf1e..963499c 100644 --- a/DragonFruit.Data/ApiClient.cs +++ b/DragonFruit.Data/ApiClient.cs @@ -121,7 +121,7 @@ protected virtual async Task ValidateAndProcess(HttpResponseMessage respon /// public async Task PerformAsync(ApiRequest request, CancellationToken cancellationToken = default) where T : class { - using var requestMessage = await BuildRequest(request).ConfigureAwait(false); + using var requestMessage = await BuildRequest(request, Serializers.Resolve(DataDirection.In).ContentType).ConfigureAwait(false); using var responseMessage = await Client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); return await ValidateAndProcess(responseMessage, cancellationToken).ConfigureAwait(false); @@ -132,7 +132,7 @@ public async Task PerformAsync(ApiRequest request, CancellationToken cance /// public async Task PerformAsync(ApiRequest request, CancellationToken cancellationToken = default) { - using var requestMessage = await BuildRequest(request).ConfigureAwait(false); + using var requestMessage = await BuildRequest(request, "*/*").ConfigureAwait(false); return await Client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); } @@ -140,8 +140,9 @@ public async Task PerformAsync(ApiRequest request, Cancella /// Overridable method used to build a from an /// /// The request to build a for + /// The Content-Type expected to be returned /// The to send - protected virtual async ValueTask BuildRequest(ApiRequest request) + protected virtual async ValueTask BuildRequest(ApiRequest request, string expectedContentType) { if (request is IRequestExecutingCallback callback) { @@ -153,7 +154,12 @@ protected virtual async ValueTask BuildRequest(ApiRequest re await asyncCallback.OnRequestExecuting(this); } - return request is IRequestBuilder rb ? rb.BuildRequest(Serializers) : ReflectionRequestMessageBuilder.CreateHttpRequestMessage(request, this); + var requestMessage = request is IRequestBuilder rb + ? rb.BuildRequest(Serializers) + : ReflectionRequestMessageBuilder.CreateHttpRequestMessage(request, this); + + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(expectedContentType)); + return requestMessage; } private HttpMessageHandler CreateDefaultHandler() From 7c219004c750b5e96167d3f0b7894d8d091a06fb Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 12 Dec 2023 13:20:28 +0000 Subject: [PATCH 076/151] hide sourcegen interface from intellisense --- DragonFruit.Data/Requests/IRequestBuilder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DragonFruit.Data/Requests/IRequestBuilder.cs b/DragonFruit.Data/Requests/IRequestBuilder.cs index 1975eef..0b83400 100644 --- a/DragonFruit.Data/Requests/IRequestBuilder.cs +++ b/DragonFruit.Data/Requests/IRequestBuilder.cs @@ -1,11 +1,13 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details +using System.ComponentModel; using System.Net.Http; using DragonFruit.Data.Serializers; namespace DragonFruit.Data.Requests { + [EditorBrowsable(EditorBrowsableState.Never)] public interface IRequestBuilder { HttpRequestMessage BuildRequest(SerializerResolver serializerResolver); From a02d10979a953a7a80d3357a4379d6df4ca1164d Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 12 Dec 2023 13:27:58 +0000 Subject: [PATCH 077/151] fix nested class check --- DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs b/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs index 00ec311..32726f9 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs @@ -58,7 +58,7 @@ private void AnalyzeClassDecl(SyntaxNodeAnalysisContext context) } // check if class is nested - if (classDeclarationNode.Parent is not NamespaceDeclarationSyntax) + if (classDeclarationNode.Parent is ClassDeclarationSyntax) { context.ReportDiagnostic(Diagnostic.Create(NestedClassNotAllowedRule, classDeclarationNode.Identifier.GetLocation(), classDeclarationNode.Identifier.Text)); } From 9726e9616ecb06a7134e16151572b3e13b51ab4f Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 12 Dec 2023 13:38:01 +0000 Subject: [PATCH 078/151] fix request bodies being ignored from generation --- DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 4fae6c6..e14e476 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -184,10 +184,11 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil // locate and add symbol metadata foreach (var candidate in currentSymbol.GetMembers().Where(x => x is IPropertySymbol or IMethodSymbol { Parameters.Length: 0 })) { - var requestAttribute = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(requestParameterAttribute, SymbolEqualityComparer.Default) == true); + var parameterAttribute = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(requestParameterAttribute, SymbolEqualityComparer.Default) == true); + var bodyAttribute = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(requestBodyAttribute, SymbolEqualityComparer.Default) == true); // ensure properties overwritten using "new" are not processed twice - if (requestAttribute == null || !consumedProperties.Add(candidate.MetadataName)) + if ((parameterAttribute == null && bodyAttribute == null) || !consumedProperties.Add(candidate.MetadataName)) { continue; } @@ -237,13 +238,14 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil } // check if value is decorated with RequestBodyAttribute - if (metadata.BodyProperty != null && candidate.GetAttributes().Any(x => x.AttributeClass?.Equals(requestBodyAttribute, SymbolEqualityComparer.Default) == true)) + if (bodyAttribute != null && metadata.BodyProperty == null) { metadata.BodyProperty = new SymbolMetadata(candidate, returnType); + continue; } - var parameterType = (ParameterType)requestAttribute.ConstructorArguments[0].Value!; - var parameterName = (string)requestAttribute.ConstructorArguments.ElementAtOrDefault(1).Value ?? candidate.Name; + var parameterType = (ParameterType)parameterAttribute.ConstructorArguments[0].Value!; + var parameterName = (string)parameterAttribute.ConstructorArguments.ElementAtOrDefault(1).Value ?? candidate.Name; SymbolMetadata symbolMetadata; From c60eb1c9ba1b13caa672748fd75f5ccc35a05f4e Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 12 Dec 2023 18:26:34 +0000 Subject: [PATCH 079/151] rearrange properties --- DragonFruit.Data/ApiClient.cs | 91 ++++++++++---------- DragonFruit.Data/Requests/IRequestBuilder.cs | 2 - 2 files changed, 44 insertions(+), 49 deletions(-) diff --git a/DragonFruit.Data/ApiClient.cs b/DragonFruit.Data/ApiClient.cs index 963499c..bc621b8 100644 --- a/DragonFruit.Data/ApiClient.cs +++ b/DragonFruit.Data/ApiClient.cs @@ -49,38 +49,19 @@ public string UserAgent public SerializerResolver Serializers { get; } /// - /// Gets the header container for the underlying + /// User-Controlled method to create a /// - public HttpRequestHeaders Headers => Client.DefaultRequestHeaders; + public Func Handler { get; set; } /// - /// User-Controlled method to create a + /// Gets the header container for the underlying /// - public Func Handler { get; set; } + public HttpRequestHeaders Headers => Client.DefaultRequestHeaders; /// /// Gets the used across all requests. /// - private HttpClient Client - { - get - { - if (_client != null) - { - return _client; - } - - _client = new HttpClient(CreateHandler(), true); - -#if !NETSTANDARD2_0 - // on newer platforms, enable HTTP/2 (and HTTP/3) - _client.DefaultRequestVersion = HttpVersion.Version11; - _client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; -#endif - - return _client; - } - } + protected HttpClient Client => _client ??= CreateClient(); /// /// Overridable method used to control creation of a used by the internal HTTP client. @@ -90,29 +71,6 @@ private HttpClient Client /// protected virtual HttpMessageHandler CreateHandler() => Handler?.Invoke() ?? CreateDefaultHandler(); - /// - /// Overridable handler for validating and processing a - /// - protected virtual async Task ValidateAndProcess(HttpResponseMessage response, CancellationToken cancellationToken) where T : class - { - response.EnsureSuccessStatusCode(); - -#if NETSTANDARD2_0 - using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); -#else - using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); -#endif - - var serializer = Serializers.Resolve(DataDirection.In); - - if (serializer is IAsyncSerializer asyncSerializer) - { - return await asyncSerializer.DeserializeAsync(stream).ConfigureAwait(false); - } - - return serializer.Deserialize(stream); - } - /// /// Performs a request, deserializing the results into the specified type. /// @@ -136,6 +94,45 @@ public async Task PerformAsync(ApiRequest request, Cancella return await Client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); } + /// + /// Overridable method used to control creation of a used by the internal HTTP client. + /// + protected virtual HttpClient CreateClient() + { + var client = new HttpClient(CreateHandler(), true); + +#if !NETSTANDARD2_0 + // on newer platforms, enable HTTP/2 (and HTTP/3) + client.DefaultRequestVersion = HttpVersion.Version11; + client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; +#endif + + return client; + } + + /// + /// Overridable handler for validating and processing a + /// + protected virtual async Task ValidateAndProcess(HttpResponseMessage response, CancellationToken cancellationToken) where T : class + { + response.EnsureSuccessStatusCode(); + +#if NETSTANDARD2_0 + using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); +#else + using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); +#endif + + var serializer = Serializers.Resolve(DataDirection.In); + + if (serializer is IAsyncSerializer asyncSerializer) + { + return await asyncSerializer.DeserializeAsync(stream).ConfigureAwait(false); + } + + return serializer.Deserialize(stream); + } + /// /// Overridable method used to build a from an /// diff --git a/DragonFruit.Data/Requests/IRequestBuilder.cs b/DragonFruit.Data/Requests/IRequestBuilder.cs index 0b83400..1975eef 100644 --- a/DragonFruit.Data/Requests/IRequestBuilder.cs +++ b/DragonFruit.Data/Requests/IRequestBuilder.cs @@ -1,13 +1,11 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details -using System.ComponentModel; using System.Net.Http; using DragonFruit.Data.Serializers; namespace DragonFruit.Data.Requests { - [EditorBrowsable(EditorBrowsableState.Never)] public interface IRequestBuilder { HttpRequestMessage BuildRequest(SerializerResolver serializerResolver); From 803aa230b4c340e3ae8a815c1e77f7c8dae3960c Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 12 Dec 2023 19:22:59 +0000 Subject: [PATCH 080/151] add file download methods --- DragonFruit.Data/ApiClient.cs | 162 ++++++++++++++++++++++++++++++++-- 1 file changed, 154 insertions(+), 8 deletions(-) diff --git a/DragonFruit.Data/ApiClient.cs b/DragonFruit.Data/ApiClient.cs index bc621b8..7c2f565 100644 --- a/DragonFruit.Data/ApiClient.cs +++ b/DragonFruit.Data/ApiClient.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details using System; +using System.Buffers; +using System.IO; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -63,14 +65,6 @@ public string UserAgent /// protected HttpClient Client => _client ??= CreateClient(); - /// - /// Overridable method used to control creation of a used by the internal HTTP client. - /// - /// - /// This is designed to be used by libraries requiring overall control of handlers (i.e. wrap the user-selected handler to provide additional functionality) - /// - protected virtual HttpMessageHandler CreateHandler() => Handler?.Invoke() ?? CreateDefaultHandler(); - /// /// Performs a request, deserializing the results into the specified type. /// @@ -94,6 +88,158 @@ public async Task PerformAsync(ApiRequest request, Cancella return await Client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); } + /// + /// Performs a request, writing the response to the specified + /// + /// + /// This method does *not* seek or modify the position of the stream, nor will it dispose of the stream. + /// + /// The to make + /// A to write to. + /// (Optional) progress callback + /// (Optional) whether to truncate the destination stream, if content is written. Defaults to true + /// (Optional) whether to copy to a temporary buffer before writing to destination. When enabled provides greater redundancy from network failure. Defaults to false + /// (Optional) cancellation request + public async Task PerformDownload(ApiRequest request, Stream destination, IProgress<(long, long?)> progress = null, bool truncate = true, bool safe = false, CancellationToken cancellationToken = default) + { + using var requestMessage = await BuildRequest(request, "*/*").ConfigureAwait(false); + return await PerformDownload(requestMessage, destination, progress, truncate, safe, cancellationToken).ConfigureAwait(false); + } + + /// + /// Performs a request, writing the response to the specified + /// + /// + /// This method does *not* seek or modify the position of the stream, nor will it dispose of the stream. + /// + /// The to make + /// A to write to. + /// (Optional) progress callback + /// (Optional) whether to truncate the destination stream, if content is written. Defaults to true + /// (Optional) whether to copy to a temporary buffer before writing to destination. When enabled provides greater redundancy from network failure. Defaults to false + /// (Optional) cancellation request + public async Task PerformDownload(HttpRequestMessage request, Stream destination, IProgress<(long, long?)> progress = null, bool truncate = true, bool safe = false, CancellationToken cancellationToken = default) + { + using var responseMessage = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + + if (responseMessage.StatusCode != HttpStatusCode.OK) + { + return responseMessage.StatusCode; + } + + if (!destination.CanWrite) + { + throw new ArgumentException("Destination Stream must be writable.", nameof(destination)); + } + + if (!destination.CanSeek && truncate) + { + throw new ArgumentException("Destination Stream must be seekable to use truncate.", nameof(destination)); + } + + var buffer = ArrayPool.Shared.Rent(4096); + var totalLength = responseMessage.Content.Headers.ContentLength; + var destinationStream = safe switch + { + // when less than 80kb is copied, use a memory stream + true when totalLength < 80000 => new MemoryStream(), + + // file stream for unknown or large files + true => new FileStream(Path.GetTempFileName(), FileMode.Open, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.Asynchronous | FileOptions.SequentialScan | FileOptions.DeleteOnClose), + + // write directly when not using safe mode + _ => destination + }; + + try + { + int read; + + var loopCount = 0; + var totalRead = 0L; + + // 0.5% of the total length or 250kb progress increments + // get the number of read/write cycles to do before reporting progress + var copies = (int)Math.Ceiling((totalLength / 200 ?? 2.5e+5) / buffer.Length); + + void UpdateProgress() + { + totalRead += read; + + if (++loopCount % copies != 0) + { + return; + } + + progress?.Report((totalRead, totalLength)); + loopCount = 0; + } + +#if NETSTANDARD2_0 + using var stream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false); + + while ((read = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) > 0) + { + await destinationStream.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false); + UpdateProgress(); + } +#else + using var stream = await responseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + var memory = buffer.AsMemory(); + + while ((read = await stream.ReadAsync(memory, cancellationToken).ConfigureAwait(false)) > 0) + { + await destinationStream.WriteAsync(memory[..read], cancellationToken).ConfigureAwait(false); + UpdateProgress(); + } +#endif + + // flush stream contents before truncating or copying + await destinationStream.FlushAsync(cancellationToken).ConfigureAwait(false); + + if (safe) + { + // safe mode: copy temp file contents to destination + destinationStream.Seek(0, SeekOrigin.Begin); + +#if NETSTANDARD2_0 + await destinationStream.CopyToAsync(destination).ConfigureAwait(false); +#else + await destinationStream.CopyToAsync(destination, cancellationToken).ConfigureAwait(false); +#endif + } + + // perform final truncate (if needed) + if (truncate && destination.Length > destination.Position) + { + destination.SetLength(destination.Position); + } + + return HttpStatusCode.OK; + } + finally + { + ArrayPool.Shared.Return(buffer); + + if (safe) + { +#if NETSTANDARD2_0 + destinationStream.Dispose(); +#else + await destinationStream.DisposeAsync().ConfigureAwait(false); +#endif + } + } + } + + /// + /// Overridable method used to control creation of a used by the internal HTTP client. + /// + /// + /// This is designed to be used by libraries requiring overall control of handlers (i.e. wrap the user-selected handler to provide additional functionality) + /// + protected virtual HttpMessageHandler CreateHandler() => Handler?.Invoke() ?? CreateDefaultHandler(); + /// /// Overridable method used to control creation of a used by the internal HTTP client. /// From 4ae3efe1e4e50e6e43bb80cbcff2d4d3fad99770 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Tue, 12 Dec 2023 19:24:40 +0000 Subject: [PATCH 081/151] add performasync for httprequestmessage --- DragonFruit.Data/ApiClient.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/DragonFruit.Data/ApiClient.cs b/DragonFruit.Data/ApiClient.cs index 7c2f565..560e6a4 100644 --- a/DragonFruit.Data/ApiClient.cs +++ b/DragonFruit.Data/ApiClient.cs @@ -66,7 +66,7 @@ public string UserAgent protected HttpClient Client => _client ??= CreateClient(); /// - /// Performs a request, deserializing the results into the specified type. + /// Builds and performs an , deserializing the results into the specified type. /// /// /// If source generation is enabled, the source generated method will be used to build the request, otherwise a legacy reflection handler will build the request. @@ -74,8 +74,15 @@ public string UserAgent public async Task PerformAsync(ApiRequest request, CancellationToken cancellationToken = default) where T : class { using var requestMessage = await BuildRequest(request, Serializers.Resolve(DataDirection.In).ContentType).ConfigureAwait(false); - using var responseMessage = await Client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + return await PerformAsync(requestMessage, cancellationToken).ConfigureAwait(false); + } + /// + /// Performs a prebuilt , deserializing the results into the specified type. + /// + public async Task PerformAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) where T : class + { + using var responseMessage = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); return await ValidateAndProcess(responseMessage, cancellationToken).ConfigureAwait(false); } From ed7fee7fb7a1433823d75ad08569694f88081100 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 13 Dec 2023 10:47:00 +0000 Subject: [PATCH 082/151] add key-value pair writer --- .../Entities/KeyValuePairSymbolMetadata.cs | 18 +++++++++++++++++ .../Converters/KeyValuePairConverter.cs | 20 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 DragonFruit.Data.Roslyn/Entities/KeyValuePairSymbolMetadata.cs create mode 100644 DragonFruit.Data/Converters/KeyValuePairConverter.cs diff --git a/DragonFruit.Data.Roslyn/Entities/KeyValuePairSymbolMetadata.cs b/DragonFruit.Data.Roslyn/Entities/KeyValuePairSymbolMetadata.cs new file mode 100644 index 0000000..546ec95 --- /dev/null +++ b/DragonFruit.Data.Roslyn/Entities/KeyValuePairSymbolMetadata.cs @@ -0,0 +1,18 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using DragonFruit.Data.Roslyn.Enums; +using Microsoft.CodeAnalysis; + +namespace DragonFruit.Data.Roslyn.Entities +{ + internal class KeyValuePairSymbolMetadata : PropertySymbolMetadata + { + public override RequestSymbolType Type => RequestSymbolType.KeyValuePair; + + public KeyValuePairSymbolMetadata(ISymbol symbol, ITypeSymbol returnType, string parameterName) + : base(symbol, returnType, parameterName) + { + } + } +} diff --git a/DragonFruit.Data/Converters/KeyValuePairConverter.cs b/DragonFruit.Data/Converters/KeyValuePairConverter.cs new file mode 100644 index 0000000..d3cc53b --- /dev/null +++ b/DragonFruit.Data/Converters/KeyValuePairConverter.cs @@ -0,0 +1,20 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System; +using System.Collections.Generic; +using System.Text; + +namespace DragonFruit.Data.Converters +{ + public class KeyValuePairConverter + { + public static void WriteKeyValuePairs(StringBuilder destination, IEnumerable> pairs) + { + foreach (var pair in pairs) + { + destination.AppendFormat("{0}={1}&", pair.Key, Uri.EscapeDataString(pair.Value)); + } + } + } +} From 656640f62499132ad896a3f76d5fde4108bb8710 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 13 Dec 2023 10:47:24 +0000 Subject: [PATCH 083/151] cleanup types, remove redundant casts --- DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs | 2 +- DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs | 2 +- DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs | 2 +- DragonFruit.Data.Roslyn/Enums/RequestSymbolType.cs | 1 + DragonFruit.Data.Roslyn/Enums/SpecialRequestParameter.cs | 4 ++-- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs b/DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs index 98f93c9..f6f608a 100644 --- a/DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs @@ -8,7 +8,7 @@ namespace DragonFruit.Data.Roslyn.Entities { internal class EnumSymbolMetadata : PropertySymbolMetadata { - public override int Type => (int)RequestSymbolType.Enum; + public override RequestSymbolType Type => RequestSymbolType.Enum; public EnumSymbolMetadata(ISymbol symbol, ITypeSymbol returnType, string parameterName) : base(symbol, returnType, parameterName) diff --git a/DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs b/DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs index 17b6210..4376756 100644 --- a/DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs @@ -8,7 +8,7 @@ namespace DragonFruit.Data.Roslyn.Entities { internal class EnumerableSymbolMetadata : PropertySymbolMetadata { - public override int Type => (int)RequestSymbolType.Enumerable; + public override RequestSymbolType Type => RequestSymbolType.Enumerable; public EnumerableSymbolMetadata(ISymbol symbol, ITypeSymbol returnType, string parameterName) : base(symbol, returnType, parameterName) diff --git a/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs b/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs index 1d60af2..87557a7 100644 --- a/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs @@ -8,7 +8,7 @@ namespace DragonFruit.Data.Roslyn.Entities { internal class PropertySymbolMetadata : SymbolMetadata { - public virtual int Type => (int)RequestSymbolType.Standard; + public virtual RequestSymbolType Type => RequestSymbolType.Standard; public PropertySymbolMetadata(ISymbol symbol, ITypeSymbol returnType, string parameterName) : base(symbol, returnType) diff --git a/DragonFruit.Data.Roslyn/Enums/RequestSymbolType.cs b/DragonFruit.Data.Roslyn/Enums/RequestSymbolType.cs index a15edaf..7cc6fbe 100644 --- a/DragonFruit.Data.Roslyn/Enums/RequestSymbolType.cs +++ b/DragonFruit.Data.Roslyn/Enums/RequestSymbolType.cs @@ -8,5 +8,6 @@ internal enum RequestSymbolType Standard = 0, Enumerable = 1, Enum = 2, + KeyValuePair = 3, } } diff --git a/DragonFruit.Data.Roslyn/Enums/SpecialRequestParameter.cs b/DragonFruit.Data.Roslyn/Enums/SpecialRequestParameter.cs index b644cad..11cbc3f 100644 --- a/DragonFruit.Data.Roslyn/Enums/SpecialRequestParameter.cs +++ b/DragonFruit.Data.Roslyn/Enums/SpecialRequestParameter.cs @@ -5,7 +5,7 @@ namespace DragonFruit.Data.Roslyn.Enums { public enum SpecialRequestParameter { - ByteArray = 0, - Stream = 1 + Stream = 0, + ByteArray = 1 } } From ceb9417a344c06066e2fc2b16e33dee04b0a3efc Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 13 Dec 2023 10:47:36 +0000 Subject: [PATCH 084/151] move converters to own namespace --- DragonFruit.Data/{Requests => Converters}/EnumConverter.cs | 5 +++-- .../{Requests => Converters}/EnumerableConverter.cs | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) rename DragonFruit.Data/{Requests => Converters}/EnumConverter.cs (83%) rename DragonFruit.Data/{Requests => Converters}/EnumerableConverter.cs (98%) diff --git a/DragonFruit.Data/Requests/EnumConverter.cs b/DragonFruit.Data/Converters/EnumConverter.cs similarity index 83% rename from DragonFruit.Data/Requests/EnumConverter.cs rename to DragonFruit.Data/Converters/EnumConverter.cs index 8cc536d..eccf1ee 100644 --- a/DragonFruit.Data/Requests/EnumConverter.cs +++ b/DragonFruit.Data/Converters/EnumConverter.cs @@ -4,12 +4,13 @@ using System; using System.Globalization; using System.Text; +using DragonFruit.Data.Requests; -namespace DragonFruit.Data.Requests +namespace DragonFruit.Data.Converters { public static class EnumConverter { - public static void AppendEnum(StringBuilder destination, T value, EnumOption mode, string propertyName) where T : Enum + public static void WriteEnum(StringBuilder destination, T value, EnumOption mode, string propertyName) where T : Enum { destination.AppendFormat("{0}={1}&", propertyName, GetEnumValue(value, mode)); } diff --git a/DragonFruit.Data/Requests/EnumerableConverter.cs b/DragonFruit.Data/Converters/EnumerableConverter.cs similarity index 98% rename from DragonFruit.Data/Requests/EnumerableConverter.cs rename to DragonFruit.Data/Converters/EnumerableConverter.cs index e10a66b..298378c 100644 --- a/DragonFruit.Data/Requests/EnumerableConverter.cs +++ b/DragonFruit.Data/Converters/EnumerableConverter.cs @@ -6,8 +6,9 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using DragonFruit.Data.Requests; -namespace DragonFruit.Data.Requests +namespace DragonFruit.Data.Converters { public static class EnumerableConverter { From 4af6ce6d661022f423d3b4a08319962b9bffbaee Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 13 Dec 2023 10:47:48 +0000 Subject: [PATCH 085/151] add key-value pair serialization --- .../ApiRequestSourceGenerator.cs | 24 +++++++---- .../Templates/ApiRequest.liquid | 40 +++++++++++++------ 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index e14e476..303438f 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -162,6 +162,7 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil var requestBodyAttribute = compilation.GetTypeByMetadataName(typeof(RequestBodyAttribute).FullName); var formBodyTypeAttribute = compilation.GetTypeByMetadataName(typeof(FormBodyTypeAttribute).FullName); + var keyValueEnumerableTypeSymbol = compilation.GetTypeByMetadataName(typeof(IEnumerable>).FullName); var enumerableTypeSymbol = compilation.GetTypeByMetadataName(typeof(IEnumerable).FullName); var apiRequestBaseType = compilation.GetTypeByMetadataName(typeof(ApiRequest).FullName); var streamTypeSymbol = compilation.GetTypeByMetadataName(typeof(Stream).FullName); @@ -249,18 +250,14 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil SymbolMetadata symbolMetadata; - // handle enums - if (returnType.TypeKind == TypeKind.Enum) + // handle IEnumerable> + if (returnType.FindImplementationForInterfaceMember(keyValueEnumerableTypeSymbol) != null) { - var enumOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumParameterAttribute, SymbolEqualityComparer.Default) == true); - var enumType = (EnumOption?)enumOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumOption.None; - - symbolMetadata = new EnumSymbolMetadata(candidate, returnType, parameterName) + symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName) { - EnumOption = enumType.ToString() }; } - // handle arrays/IEnumerable + // handle IEnumerable/T[] else if (SupportedCollectionTypes.Contains(returnType.SpecialType) || returnType.FindImplementationForInterfaceMember(enumerableTypeSymbol) != null) { var enumerableOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumerableParameterAttribute, SymbolEqualityComparer.Default) == true); @@ -272,6 +269,17 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil EnumerableOption = enumerableType.ToString() }; } + // handle enums + else if (returnType.TypeKind == TypeKind.Enum) + { + var enumOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumParameterAttribute, SymbolEqualityComparer.Default) == true); + var enumType = (EnumOption?)enumOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumOption.None; + + symbolMetadata = new EnumSymbolMetadata(candidate, returnType, parameterName) + { + EnumOption = enumType.ToString() + }; + } else { var psm = new PropertySymbolMetadata(candidate, returnType, parameterName); diff --git a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid index 8aa238e..3002045 100644 --- a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid @@ -20,9 +20,11 @@ namespace {{ namespace }} {% capture query_append -%} {% case query.type -%} {% when 1 -%} - global::DragonFruit.Data.Requests.EnumerableConverter.AppendEnumerable(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ query.enumerable_option }}, "{{ query.parameter_name }}", "{{ query.separator }}"); + global::DragonFruit.Data.Converters.EnumerableConverter.WriteEnumerable(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ query.enumerable_option }}, "{{ query.parameter_name }}", "{{ query.separator }}"); {% when 2 -%} - global::DragonFruit.Data.Requests.EnumConverter.AppendEnum(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ query.enum_option }}, "{{ query.parameter_name }}"); + global::DragonFruit.Data.Converters.EnumConverter.WriteEnum(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ query.enum_option }}, "{{ query.parameter_name }}"); + {% when 3 -%} + global::DragonFruit.Data.Converters.KeyValuePairConverter.WriteKeyValuePair(queryBuilder, {{ query.accessor }}); {% else -%} queryBuilder.AppendFormat("{0}={1}&", "{{ query.parameter_name }}", global::System.Uri.EscapeDataString({{ query.accessor }}.ToString())); {% endcase -%} @@ -56,20 +58,25 @@ namespace {{ namespace }} {% capture multipart_append -%} {% case multipart.type -%} {% when 1 -%} - foreach (var kvp in global::DragonFruit.Data.Requests.EnumerableConverter.GetPairs({{ multipart.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ multipart.enumerable_option }})) + foreach (var kvp in global::DragonFruit.Data.Converters.EnumerableConverter.GetPairs({{ multipart.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ multipart.enumerable_option }})) { content.Add(new global::System.Net.Http.StringContent(kvp.Value), "{{ multipart.parameter_name }}"); } {% when 2 -%} - content.Add(new global::System.Net.Http.StringContent(global::DragonFruit.Data.Requests.EnumConverter.GetString({{ multipart.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ multipart.enum_option }})), "{{ multipart.parameter_name }}"); + content.Add(new global::System.Net.Http.StringContent(global::DragonFruit.Data.Converters.EnumConverter.GetEnumValue({{ multipart.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ multipart.enum_option }})), "{{ multipart.parameter_name }}"); + {% when 3 %} + foreach (var kvp in (global::System.Collections.Generic.IEnumerable>){{ multipart.accessor }}) + { + content.Add(new global::System.Net.Http.StringContent(kvp.Value), kvp.Key); + } {% else -%} {% case multipart.special_request_parameter -%} - {% comment %} 0 - ByteArray {% endcomment %} + {% comment %} 0 - Stream {% endcomment %} {% when 0 -%} - content.Add(new global::System.Net.Http.ByteArrayContent({{ multipart.accessor }}), "{{ multipart.parameter_name }}"); - {% comment %} 1 - Stream {% endcomment %} - {% when 1 -%} content.Add(new global::System.Net.Http.StreamContent({{ multipart.accessor }}), "{{ multipart.parameter_name }}"); + {% comment %} 1 - ByteArray {% endcomment %} + {% when 1 -%} + content.Add(new global::System.Net.Http.ByteArrayContent({{ multipart.accessor }}), "{{ multipart.parameter_name }}"); {% comment %} Handle other types using ToString {% endcomment %} {% else -%} content.Add(new global::System.Net.Http.StringContent({{ multipart.accessor }}.ToString()), "{{ multipart.parameter_name }}"); @@ -97,9 +104,11 @@ namespace {{ namespace }} {% capture uriparam_append -%} {% case uriparam.type -%} {% when 1 -%} - global::DragonFruit.Data.Requests.EnumerableConverter.AppendEnumerable(formBuilder, {{ uriparam.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ uriparam.enumerable_option }}, "{{ uriparam.parameter_name }}", "{{ uriparam.separator }}"); + global::DragonFruit.Data.Converters.EnumerableConverter.AppendEnumerable(formBuilder, {{ uriparam.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ uriparam.enumerable_option }}, "{{ uriparam.parameter_name }}", "{{ uriparam.separator }}"); {% when 2 -%} - global::DragonFruit.Data.Requests.EnumConverter.AppendEnum(formBuilder, {{ uriparam.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ uriparam.enum_option }}, "{{ uriparam.parameter_name }}"); + global::DragonFruit.Data.Converters.EnumConverter.WriteEnum(formBuilder, {{ uriparam.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ uriparam.enum_option }}, "{{ uriparam.parameter_name }}"); + {% when 3 -%} + global::DragonFruit.Data.Converters.KeyValuePairConverter.WriteKeyValuePair(formBuilder, {{ uriparam.accessor }}); {% else -%} formBuilder.AppendFormat("{0}={1}&", "{{ uriparam.parameter_name }}", global::System.Uri.EscapeDataString({{ uriparam.accessor }}.ToString())); {% endcase -%} @@ -120,7 +129,7 @@ namespace {{ namespace }} {% comment %} 3 - Custom Body (HttpContent) {% endcomment %} {% when 3 -%} request.Content = {{ request_body_symbol.accessor }}; - + {% comment %} 4 - Custom Body (Serialized) {% endcomment %} {% when 4 -%} request.Content = serializerResolver.Resolve({{ request_body_symbol.accessor }}.GetType(), global::DragonFruit.Data.Serializers.DataDirection.Out).Serialize({{ request_body_symbol.accessor }}); @@ -131,12 +140,17 @@ namespace {{ namespace }} {% capture header_append -%} {% case header.type -%} {% when 1 -%} - foreach (var kvp in global::DragonFruit.Data.Requests.EnumerableConverter.GetPairs({{ header.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ header.enumumerable_option }})) + foreach (var kvp in global::DragonFruit.Data.Converters.EnumerableConverter.GetPairs({{ header.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ header.enumumerable_option }})) { request.Headers.Add(kvp.Key, kvp.Value); } {% when 2 -%} - request.Headers.Add("{{ header.parameter_name }}", global::DragonFruit.Data.Requests.EnumConverter.GetString({{ header.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ header.enum_option }})); + request.Headers.Add("{{ header.parameter_name }}", global::DragonFruit.Data.Converters.EnumConverter.GetString({{ header.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ header.enum_option }})); + {% when 3 -%} + foreach (var kvp in (global::System.Collections.Generic.IEnumerable>){{ header.accessor }}) + { + request.Headers.Add(kvp.Key, kvp.Value); + } {% else -%} request.Headers.Add("{{ header.parameter_name }}", {{ header.accessor }}.ToString()); {% endcase -%} From e55ef388ccec4b450e3299bafe7611438796c4e3 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 13 Dec 2023 11:09:09 +0000 Subject: [PATCH 086/151] fix bad method name --- DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid index 3002045..fac0ae8 100644 --- a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid @@ -24,7 +24,7 @@ namespace {{ namespace }} {% when 2 -%} global::DragonFruit.Data.Converters.EnumConverter.WriteEnum(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ query.enum_option }}, "{{ query.parameter_name }}"); {% when 3 -%} - global::DragonFruit.Data.Converters.KeyValuePairConverter.WriteKeyValuePair(queryBuilder, {{ query.accessor }}); + global::DragonFruit.Data.Converters.KeyValuePairConverter.WriteKeyValuePairs(queryBuilder, {{ query.accessor }}); {% else -%} queryBuilder.AppendFormat("{0}={1}&", "{{ query.parameter_name }}", global::System.Uri.EscapeDataString({{ query.accessor }}.ToString())); {% endcase -%} @@ -108,7 +108,7 @@ namespace {{ namespace }} {% when 2 -%} global::DragonFruit.Data.Converters.EnumConverter.WriteEnum(formBuilder, {{ uriparam.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ uriparam.enum_option }}, "{{ uriparam.parameter_name }}"); {% when 3 -%} - global::DragonFruit.Data.Converters.KeyValuePairConverter.WriteKeyValuePair(formBuilder, {{ uriparam.accessor }}); + global::DragonFruit.Data.Converters.KeyValuePairConverter.WriteKeyValuePairs(formBuilder, {{ uriparam.accessor }}); {% else -%} formBuilder.AppendFormat("{0}={1}&", "{{ uriparam.parameter_name }}", global::System.Uri.EscapeDataString({{ uriparam.accessor }}.ToString())); {% endcase -%} From df54ac07477b802d949d2ec2134426044ad5d591 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 13 Dec 2023 11:09:21 +0000 Subject: [PATCH 087/151] cleanup project --- .../DragonFruit.Data.Roslyn.Tests.csproj | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj b/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj index 59c7927..4fd1b49 100644 --- a/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj +++ b/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj @@ -18,16 +18,13 @@ - + + - - - - From 57fc4de64ba91839fc1dc9419d91ca92a1662a3a Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 13 Dec 2023 11:09:51 +0000 Subject: [PATCH 088/151] improve KeyValuePair checking --- .../ApiRequestSourceGenerator.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 303438f..e337f81 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -162,11 +162,15 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil var requestBodyAttribute = compilation.GetTypeByMetadataName(typeof(RequestBodyAttribute).FullName); var formBodyTypeAttribute = compilation.GetTypeByMetadataName(typeof(FormBodyTypeAttribute).FullName); - var keyValueEnumerableTypeSymbol = compilation.GetTypeByMetadataName(typeof(IEnumerable>).FullName); var enumerableTypeSymbol = compilation.GetTypeByMetadataName(typeof(IEnumerable).FullName); var apiRequestBaseType = compilation.GetTypeByMetadataName(typeof(ApiRequest).FullName); var streamTypeSymbol = compilation.GetTypeByMetadataName(typeof(Stream).FullName); + // create IEnumerable> impl + var stringTypeSymbol = compilation.GetSpecialType(SpecialType.System_String); + var constructedKeyValuePairTypeSymbol = compilation.GetTypeByMetadataName(typeof(KeyValuePair<,>).FullName).Construct(stringTypeSymbol, stringTypeSymbol); + var keyValuePairEnumerableTypeSymbol = compilation.GetTypeByMetadataName(typeof(IEnumerable<>).FullName)!.Construct(constructedKeyValuePairTypeSymbol); + // track properties already visited var depth = 0; var currentSymbol = symbol; @@ -245,20 +249,20 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil continue; } + SymbolMetadata symbolMetadata; + var parameterType = (ParameterType)parameterAttribute.ConstructorArguments[0].Value!; var parameterName = (string)parameterAttribute.ConstructorArguments.ElementAtOrDefault(1).Value ?? candidate.Name; - SymbolMetadata symbolMetadata; + var isEnumerable = SupportedCollectionTypes.Contains(returnType.SpecialType) || returnType.AllInterfaces.Any(x => x.Equals(enumerableTypeSymbol, SymbolEqualityComparer.Default)); // handle IEnumerable> - if (returnType.FindImplementationForInterfaceMember(keyValueEnumerableTypeSymbol) != null) + if (isEnumerable && returnType.AllInterfaces.Any(x => x.Equals(keyValuePairEnumerableTypeSymbol, SymbolEqualityComparer.Default))) { - symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName) - { - }; + symbolMetadata = new KeyValuePairSymbolMetadata(candidate, returnType, parameterName); } - // handle IEnumerable/T[] - else if (SupportedCollectionTypes.Contains(returnType.SpecialType) || returnType.FindImplementationForInterfaceMember(enumerableTypeSymbol) != null) + // handle IEnumerable, Array[], etc. + else if (isEnumerable) { var enumerableOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumerableParameterAttribute, SymbolEqualityComparer.Default) == true); var enumerableType = (EnumerableOption?)enumerableOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumerableOption.Concatenated; From c1d537f8220994096f710ca13f0b588b687fbcf2 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 13 Dec 2023 11:22:26 +0000 Subject: [PATCH 089/151] fix build warning --- .../AnalyzerReleases.Shipped.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/DragonFruit.Data.Roslyn/AnalyzerReleases.Shipped.md b/DragonFruit.Data.Roslyn/AnalyzerReleases.Shipped.md index 6861331..945ee03 100644 --- a/DragonFruit.Data.Roslyn/AnalyzerReleases.Shipped.md +++ b/DragonFruit.Data.Roslyn/AnalyzerReleases.Shipped.md @@ -2,12 +2,12 @@ ### New Rules - Rule ID | Category | Severity | Notes ----------|----------|----------|-------------------------------------------- - DA0001 | Design | Error | Class not marked as partial - DA0002 | Design | Error | Class was nested within another class - DA0003 | Design | Warning | Property has no getter - DA0004 | Usage | Warning | Property or Method not in ApiRequest class - DA0005 | Design | Warning | Property or Method is inaccessible - DA0006 | Design | Error | Method returns void - DA0007 | Design | Error | Method accepts arguments \ No newline at end of file +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +DA0001 | Design | Error | Class not marked as partial +DA0002 | Design | Error | Class was nested within another class +DA0003 | Design | Warning | Property has no getter +DA0004 | Usage | Warning | Property or Method not in ApiRequest class +DA0005 | Design | Warning | Property or Method is inaccessible +DA0006 | Design | Error | Method returns void +DA0007 | Design | Error | Method accepts arguments \ No newline at end of file From fc02a8ed075838f45147531dd33ad6ff6f72b440 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 13 Dec 2023 11:23:39 +0000 Subject: [PATCH 090/151] fix warning (equivalenceKey) --- DragonFruit.Data.Roslyn/Fixes/ApiRequestClassFixProvider.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/DragonFruit.Data.Roslyn/Fixes/ApiRequestClassFixProvider.cs b/DragonFruit.Data.Roslyn/Fixes/ApiRequestClassFixProvider.cs index 7c011ac..6f872f0 100644 --- a/DragonFruit.Data.Roslyn/Fixes/ApiRequestClassFixProvider.cs +++ b/DragonFruit.Data.Roslyn/Fixes/ApiRequestClassFixProvider.cs @@ -31,7 +31,11 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) return; } - context.RegisterCodeFix(CodeAction.Create(title: "Make class partial", createChangedSolution: c => MakeClassPartial(context.Document, declaration, c)), diagnostic); + var fix = CodeAction.Create(title: "Make class partial", + equivalenceKey: "addPartialModifier", + createChangedSolution: c => MakeClassPartial(context.Document, declaration, c)); + + context.RegisterCodeFix(fix, diagnostic); } private async Task MakeClassPartial(Document document, MemberDeclarationSyntax classDeclaration, CancellationToken cancellationToken) From d1cf3f43963eaa20c27e1358f0fb885b51892cc2 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 13 Dec 2023 11:31:51 +0000 Subject: [PATCH 091/151] remove unused method override --- DragonFruit.Data/ApiRequest.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/DragonFruit.Data/ApiRequest.cs b/DragonFruit.Data/ApiRequest.cs index 3eac403..84a07d6 100644 --- a/DragonFruit.Data/ApiRequest.cs +++ b/DragonFruit.Data/ApiRequest.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details using System.Net.Http; -using System.Threading.Tasks; using DragonFruit.Data.Requests; namespace DragonFruit.Data @@ -22,11 +21,5 @@ public abstract class ApiRequest /// The to use when making the request /// public virtual HttpMethod RequestMethod => HttpMethod.Get; - - /// - /// Overridable method to be called when the request is about to be built. - /// This can be used to inject headers with data managed by the client, etc. - /// - protected virtual ValueTask RequestCreatingCallback() => default; } } From 22dbd02c0ffcf3530b0f2387788da973f14b5480 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 13 Dec 2023 12:30:39 +0000 Subject: [PATCH 092/151] remove redundant aliases --- .../RequestParameterAttributeAliases.cs | 21 ------------------- 1 file changed, 21 deletions(-) delete mode 100644 DragonFruit.Data/Requests/RequestParameterAttributeAliases.cs diff --git a/DragonFruit.Data/Requests/RequestParameterAttributeAliases.cs b/DragonFruit.Data/Requests/RequestParameterAttributeAliases.cs deleted file mode 100644 index 5462888..0000000 --- a/DragonFruit.Data/Requests/RequestParameterAttributeAliases.cs +++ /dev/null @@ -1,21 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -namespace DragonFruit.Data.Requests -{ - public class QueryParameterAttribute : RequestParameterAttribute - { - public QueryParameterAttribute(string name) - : base(ParameterType.Query, name) - { - } - } - - public class FormParameterAttribute : RequestParameterAttribute - { - public FormParameterAttribute(string name) - : base(ParameterType.Form, name) - { - } - } -} From e32b027f3b25aac6ca49ae23998ca494e134003d Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 13 Dec 2023 12:30:54 +0000 Subject: [PATCH 093/151] add missing separator option --- DragonFruit.Data/Requests/EnumerableOptionsAttribute.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/DragonFruit.Data/Requests/EnumerableOptionsAttribute.cs b/DragonFruit.Data/Requests/EnumerableOptionsAttribute.cs index 19911ab..20a8391 100644 --- a/DragonFruit.Data/Requests/EnumerableOptionsAttribute.cs +++ b/DragonFruit.Data/Requests/EnumerableOptionsAttribute.cs @@ -13,6 +13,13 @@ public EnumerableOptionsAttribute(EnumerableOption options) Options = options; } - public EnumerableOption Options { get; set; } + public EnumerableOptionsAttribute(EnumerableOption options, string separator) + : this(options) + { + Separator = separator; + } + + public EnumerableOption Options { get; } + public string Separator { get; } } } From 966f8f660a3adbb6806d5bb48c20d797dc42df25 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 13 Dec 2023 12:31:18 +0000 Subject: [PATCH 094/151] prefer form properties to body properties --- DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index e337f81..d1370be 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -101,7 +101,7 @@ private void Execute(Compilation compilation, ImmutableArray> impl var stringTypeSymbol = compilation.GetSpecialType(SpecialType.System_String); - var constructedKeyValuePairTypeSymbol = compilation.GetTypeByMetadataName(typeof(KeyValuePair<,>).FullName).Construct(stringTypeSymbol, stringTypeSymbol); + var constructedKeyValuePairTypeSymbol = compilation.GetTypeByMetadataName(typeof(KeyValuePair<,>).FullName)!.Construct(stringTypeSymbol, stringTypeSymbol); var keyValuePairEnumerableTypeSymbol = compilation.GetTypeByMetadataName(typeof(IEnumerable<>).FullName)!.Construct(constructedKeyValuePairTypeSymbol); // track properties already visited From f5805db4ef09a976043bd0017fee201931243ce1 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 13 Dec 2023 12:55:13 +0000 Subject: [PATCH 095/151] move ReflectionRequestMessageBuilder and add first implementation --- DragonFruit.Data/ApiClient.cs | 1 + .../ReflectionRequestMessageBuilder.cs | 255 ++++++++++++++++++ .../ReflectionRequestMessageBuilder.cs | 16 -- 3 files changed, 256 insertions(+), 16 deletions(-) create mode 100644 DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs delete mode 100644 DragonFruit.Data/Requests/ReflectionRequestMessageBuilder.cs diff --git a/DragonFruit.Data/ApiClient.cs b/DragonFruit.Data/ApiClient.cs index 560e6a4..332da8f 100644 --- a/DragonFruit.Data/ApiClient.cs +++ b/DragonFruit.Data/ApiClient.cs @@ -9,6 +9,7 @@ using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; +using DragonFruit.Data.Converters; using DragonFruit.Data.Requests; using DragonFruit.Data.Serializers; diff --git a/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs b/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs new file mode 100644 index 0000000..44ebc94 --- /dev/null +++ b/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs @@ -0,0 +1,255 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Reflection; +using System.Text; +using DragonFruit.Data.Requests; +using DragonFruit.Data.Serializers; + +namespace DragonFruit.Data.Converters +{ + internal static class ReflectionRequestMessageBuilder + { + public static HttpRequestMessage CreateHttpRequestMessage(ApiRequest request, ApiClient client) + { + var requestType = request.GetType(); + var requestProperties = requestType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); + var requestParams = requestProperties.Select(GetPropertyInfo).Where(x => x != null).ToLookup(x => x.Value.PropertyType, x => (x.Value.PropertyName, x.Value.Accessor)); + + var requestUri = new UriBuilder(request.RequestPath); + + // build query + if (requestParams[ParameterType.Query].Any()) + { + var queryBuilder = new StringBuilder(); + + foreach (var queryParameter in requestParams[ParameterType.Query]) + { + WriteUriProperty(queryBuilder, queryParameter.PropertyName, queryParameter.Accessor); + } + + if (queryBuilder.Length > 0) + { + // trim trailing & + queryBuilder.Length--; + requestUri.Query = queryBuilder.ToString(); + } + } + + var requestMessage = new HttpRequestMessage(request.RequestMethod, requestUri.Uri); + + // check if there are any form params, if not, then check for a body property + if (!requestParams[ParameterType.Form].Any()) + { + var bodyProperty = requestType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public) + .FirstOrDefault(x => x.GetCustomAttribute() != null); + + if (bodyProperty != null) + { + var bodyContent = bodyProperty.GetValue(request); + requestMessage.Content = bodyContent switch + { + HttpContent httpContent => httpContent, + Stream stream => new StreamContent(stream), + byte[] byteArray => new ByteArrayContent(byteArray), + + _ => client.Serializers.Resolve(bodyProperty.PropertyType, DataDirection.Out).Serialize(bodyContent) + }; + } + } + else + { + switch (requestType.GetCustomAttribute()?.BodyType ?? FormBodyType.UriEncoded) + { + case FormBodyType.UriEncoded: + { + var formBuilder = new StringBuilder(); + + foreach (var formParameter in requestParams[ParameterType.Form]) + { + WriteUriProperty(formBuilder, formParameter.PropertyName, formParameter.Accessor); + } + + if (formBuilder.Length > 0) + { + // trim trailing & + formBuilder.Length--; + } + + requestMessage.Content = new StringContent(formBuilder.ToString(), Encoding.UTF8, "application/x-www-form-urlencoded"); + break; + } + + case FormBodyType.MultipartForm: + { + var multipartForm = new MultipartFormDataContent(); + + foreach (var formParameter in requestParams[ParameterType.Form]) + { + WriteMultipartProperty(multipartForm, formParameter.PropertyName, formParameter.Accessor); + } + + requestMessage.Content = multipartForm; + break; + } + + default: + throw new ArgumentOutOfRangeException(); + } + } + + // add headers + foreach (var headerParameter in requestParams[ParameterType.Header]) + { + WriteHeaderProperty(requestMessage.Headers, headerParameter.PropertyName, headerParameter.Accessor); + } + + return requestMessage; + } + + private static void WriteUriProperty(StringBuilder destination, string parameterName, PropertyInfo accessor) + { + var propertyValue = accessor.GetValue(null); + + if (accessor.PropertyType.IsEnum) + { + var options = accessor.GetCustomAttribute()?.Options ?? EnumOption.None; + EnumConverter.WriteEnum(destination, (Enum)propertyValue, options, parameterName); + + return; + } + + switch (propertyValue) + { + // KeyValuePair + case IEnumerable> dynamicPairs: + KeyValuePairConverter.WriteKeyValuePairs(destination, dynamicPairs); + break; + + // any enumerable + case IEnumerable enumerable: + { + var options = accessor.GetCustomAttribute(); + EnumerableConverter.WriteEnumerable(destination, enumerable, options?.Options, parameterName, options?.Separator); + break; + } + + // default handling + default: + destination.Append($"{parameterName}={Uri.EscapeDataString(propertyValue.ToString())}&"); + break; + } + } + + private static void WriteHeaderProperty(HttpHeaders collection, string parameterName, PropertyInfo accessor) + { + var propertyValue = accessor.GetValue(null); + + if (accessor.PropertyType.IsEnum) + { + var options = accessor.GetCustomAttribute()?.Options ?? EnumOption.None; + collection.Add(parameterName, EnumConverter.GetEnumValue((Enum)propertyValue, options)); + return; + } + + switch (propertyValue) + { + case IEnumerable> dynamicPairs: + { + foreach (var kvp in dynamicPairs) + { + collection.Add(kvp.Key, kvp.Value); + } + + break; + } + + case IEnumerable enumerable: + { + var options = accessor.GetCustomAttribute(); + + foreach (var kvp in EnumerableConverter.GetPairs(enumerable, options?.Options, parameterName, options?.Separator)) + { + collection.Add(kvp.Key, kvp.Value); + } + + break; + } + + default: + collection.Add(parameterName, propertyValue.ToString()); + break; + } + } + + private static void WriteMultipartProperty(MultipartFormDataContent multipartForm, string parameterName, PropertyInfo accessor) + { + var value = accessor.GetValue(null); + + if (accessor.PropertyType.IsEnum) + { + var options = accessor.GetCustomAttribute()?.Options ?? EnumOption.None; + multipartForm.Add(new StringContent(EnumConverter.GetEnumValue((Enum)value, options)), parameterName); + } + else + { + switch (value) + { + case Stream stream: + multipartForm.Add(new StreamContent(stream), parameterName); + break; + + case byte[] byteArray: + multipartForm.Add(new ByteArrayContent(byteArray), parameterName); + break; + + case IEnumerable> dynamicPairs: + { + foreach (var kvp in dynamicPairs) + { + multipartForm.Add(new StringContent(kvp.Value), kvp.Key); + } + + break; + } + + case IEnumerable enumerable: + { + var options = accessor.GetCustomAttribute(); + + foreach (var kvp in EnumerableConverter.GetPairs(enumerable, options?.Options, parameterName, options?.Separator)) + { + multipartForm.Add(new StringContent(kvp.Value), kvp.Key); + } + + break; + } + + default: + multipartForm.Add(new StringContent(value.ToString()), parameterName); + break; + } + } + } + + private static (ParameterType PropertyType, string PropertyName, PropertyInfo Accessor)? GetPropertyInfo(PropertyInfo property) + { + var name = property.Name; + var attribute = property.GetCustomAttribute(); + + if (attribute != null) + { + return (attribute.ParameterType, attribute.Name ?? name, property); + } + + return null; + } + } +} diff --git a/DragonFruit.Data/Requests/ReflectionRequestMessageBuilder.cs b/DragonFruit.Data/Requests/ReflectionRequestMessageBuilder.cs deleted file mode 100644 index 36cb223..0000000 --- a/DragonFruit.Data/Requests/ReflectionRequestMessageBuilder.cs +++ /dev/null @@ -1,16 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System; -using System.Net.Http; - -namespace DragonFruit.Data.Requests -{ - internal static class ReflectionRequestMessageBuilder - { - public static HttpRequestMessage CreateHttpRequestMessage(ApiRequest request, ApiClient client) - { - throw new NotImplementedException(); - } - } -} From ffe03f57d7f85365ad6e0aecb3a7d4594a1163f5 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 13 Dec 2023 12:56:26 +0000 Subject: [PATCH 096/151] allow null options, set defaults in single location --- .../Converters/EnumerableConverter.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/DragonFruit.Data/Converters/EnumerableConverter.cs b/DragonFruit.Data/Converters/EnumerableConverter.cs index 298378c..54bb084 100644 --- a/DragonFruit.Data/Converters/EnumerableConverter.cs +++ b/DragonFruit.Data/Converters/EnumerableConverter.cs @@ -12,6 +12,9 @@ namespace DragonFruit.Data.Converters { public static class EnumerableConverter { + private const EnumerableOption DefaultOption = EnumerableOption.Concatenated; + private const string DefaultSeparator = ","; + /// /// Writes the provided to the using the specified /// @@ -20,9 +23,9 @@ public static class EnumerableConverter /// The to use. If none provided, defaults to /// The name of the property to use when writing values to /// The separator to use, if required. - public static void WriteEnumerable(StringBuilder destination, IEnumerable source, EnumerableOption mode, string propertyName, string separator) + public static void WriteEnumerable(StringBuilder destination, IEnumerable source, EnumerableOption? mode, string propertyName, string separator) { - switch (mode) + switch (mode ?? DefaultOption) { case EnumerableOption.Recursive: { @@ -58,7 +61,7 @@ public static void WriteEnumerable(StringBuilder destination, IEnumerable source default: { - destination.AppendFormat("{0}={1}&", propertyName, string.Join(separator, source.Cast().Select(x => Uri.EscapeDataString(x.ToString())))); + destination.AppendFormat("{0}={1}&", propertyName, string.Join(separator ?? DefaultSeparator, source.Cast().Select(x => Uri.EscapeDataString(x.ToString())))); break; } } @@ -71,9 +74,9 @@ public static void WriteEnumerable(StringBuilder destination, IEnumerable source /// The to use /// The name of the property to use /// The separator to use, if is set to Concatenated - public static IEnumerable> GetPairs(IEnumerable source, EnumerableOption mode, string propertyName, string separator) + public static IEnumerable> GetPairs(IEnumerable source, EnumerableOption? mode, string propertyName, string separator) { - switch (mode) + switch (mode ?? DefaultOption) { case EnumerableOption.Recursive: { @@ -109,7 +112,7 @@ public static IEnumerable> GetPairs(IEnumerable sou default: { - yield return new KeyValuePair(propertyName, string.Join(separator, source.Cast().Select(x => Uri.EscapeDataString(x.ToString())))); + yield return new KeyValuePair(propertyName, string.Join(separator ?? DefaultSeparator, source.Cast().Select(x => Uri.EscapeDataString(x.ToString())))); break; } From e1f7aaccb431d7696701c111e2f306f3fc389964 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 13 Dec 2023 13:00:41 +0000 Subject: [PATCH 097/151] rearrange header logic --- .../Converters/ReflectionRequestMessageBuilder.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs b/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs index 44ebc94..00f79c0 100644 --- a/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs +++ b/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs @@ -45,6 +45,12 @@ public static HttpRequestMessage CreateHttpRequestMessage(ApiRequest request, Ap var requestMessage = new HttpRequestMessage(request.RequestMethod, requestUri.Uri); + // add headers + foreach (var headerParameter in requestParams[ParameterType.Header]) + { + WriteHeaderProperty(requestMessage.Headers, headerParameter.PropertyName, headerParameter.Accessor); + } + // check if there are any form params, if not, then check for a body property if (!requestParams[ParameterType.Form].Any()) { @@ -105,12 +111,6 @@ public static HttpRequestMessage CreateHttpRequestMessage(ApiRequest request, Ap } } - // add headers - foreach (var headerParameter in requestParams[ParameterType.Header]) - { - WriteHeaderProperty(requestMessage.Headers, headerParameter.PropertyName, headerParameter.Accessor); - } - return requestMessage; } @@ -241,12 +241,11 @@ private static void WriteMultipartProperty(MultipartFormDataContent multipartFor private static (ParameterType PropertyType, string PropertyName, PropertyInfo Accessor)? GetPropertyInfo(PropertyInfo property) { - var name = property.Name; var attribute = property.GetCustomAttribute(); if (attribute != null) { - return (attribute.ParameterType, attribute.Name ?? name, property); + return (attribute.ParameterType, attribute.Name ?? property.Name, property); } return null; From e4e816f6b1a7ba63683b11df0a3ab7c2295b7efe Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 13 Dec 2023 15:14:35 +0000 Subject: [PATCH 098/151] add tests for the rest of the DA000* ids --- .../ApiRequestAnalyzerTests.cs | 131 +++++++++++++----- .../ApiRequestTemplateTests.cs | 27 ++-- .../_TestData/DA0002.cs | 4 +- .../_TestData/DA0003.cs | 2 +- .../_TestData/DA0004.cs | 2 +- .../_TestData/DA0005.cs | 2 +- .../_TestData/DA0006.cs | 2 +- .../_TestData/DA0007.cs | 9 +- DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs | 13 +- 9 files changed, 127 insertions(+), 65 deletions(-) diff --git a/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs b/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs index 40552ee..d2d738b 100644 --- a/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs +++ b/DragonFruit.Data.Roslyn.Tests/ApiRequestAnalyzerTests.cs @@ -6,54 +6,117 @@ using Microsoft.CodeAnalysis.Testing; using Xunit; -namespace DragonFruit.Data.Roslyn.Tests; - -public class ApiRequestAnalyzerTests +namespace DragonFruit.Data.Roslyn.Tests { - private readonly string _testDataPath; - - public ApiRequestAnalyzerTests() + public class ApiRequestAnalyzerTests { - _testDataPath = Path.Combine(GetSolutionRoot(), "DragonFruit.Data.Roslyn.Tests", "_TestData"); - } + private readonly string _testDataPath; - [Fact] - public async Task TestNonPartialClassDetectionAndFix() - { - var test = new CSharpCodeFixTest + public ApiRequestAnalyzerTests() { - TestCode = await File.ReadAllTextAsync(Path.Combine(_testDataPath, "DA0001.cs")), - FixedCode = await File.ReadAllTextAsync(Path.Combine(_testDataPath, "DA0001.Fix.cs")), - ExpectedDiagnostics = { DiagnosticResult.CompilerError(ApiRequestAnalyzer.PartialClassRule.Id).WithSpan(8, 18, 8, 24).WithArguments("DA0001") } - }; + _testDataPath = Path.Combine(GetSolutionRoot(), "DragonFruit.Data.Roslyn.Tests", "_TestData"); + } - await PerformTest(test); - } + [Fact] + public async Task TestNonPartialClassDetectionAndFix() + { + var test = new CSharpCodeFixTest + { + TestCode = await File.ReadAllTextAsync(Path.Combine(_testDataPath, "DA0001.cs")), + FixedCode = await File.ReadAllTextAsync(Path.Combine(_testDataPath, "DA0001.Fix.cs")), + ExpectedDiagnostics = { DiagnosticResult.CompilerError(ApiRequestAnalyzer.PartialClassRule.Id).WithSpan(8, 18, 8, 24).WithArguments("DA0001") } + }; - private async Task PerformTest(AnalyzerTest test) - { - var content = ("Common.cs", await File.ReadAllTextAsync(Path.Combine(_testDataPath, "Common.cs"))); + await PerformTest(test); + } - // add common.cs to test sources - test.TestState.Sources.Add(content); + public static readonly TheoryData AnalyzerDetectionData = new() + { + { + // DA0002: nested class + "DA0002.cs", new[] { DiagnosticResult.CompilerError(ApiRequestAnalyzer.NestedClassNotAllowedRule.Id).WithSpan(10, 30, 10, 40).WithArguments("DA0002_Req") } + }, + { + // DA0003: property has no getter + "DA0003.cs", new[] { DiagnosticResult.CompilerWarning(ApiRequestAnalyzer.PropertyNoGetterRule.Id).WithSpan(14, 20, 14, 26).WithArguments("Param2") } + }, + { + // DA0004: property or method not in apirequest + "DA0004.cs", new[] + { + // method not in apirequest + DiagnosticResult.CompilerWarning(ApiRequestAnalyzer.PropertyOrMethodNotInApiRequestRule.Id).WithSpan(11, 23, 11, 32).WithArguments("NotAParam"), + + // property not in apirequest + DiagnosticResult.CompilerWarning(ApiRequestAnalyzer.PropertyOrMethodNotInApiRequestRule.Id).WithSpan(14, 20, 14, 28).WithArguments("TestData"), + } + }, + { + // DA0005: property or method is inaccessible + "DA0005.cs", new[] + { + // private getter with public setter + DiagnosticResult.CompilerWarning(ApiRequestAnalyzer.PropertyOrMethodInaccessibleRule.Id).WithSpan(11, 19, 11, 28).WithArguments("Parameter"), - if (test is CodeFixTest verifier) + // private method + DiagnosticResult.CompilerWarning(ApiRequestAnalyzer.PropertyOrMethodInaccessibleRule.Id).WithSpan(14, 18, 14, 32).WithArguments("IsParameterSet"), + } + }, + { + // DA0006: method returns void + "DA0006.cs", new[] + { + DiagnosticResult.CompilerError(ApiRequestAnalyzer.MethodReturnsVoidRule.Id).WithSpan(11, 17, 11, 28).WithArguments("GetUserData") + } + }, + { + // DA0007: method has parameters + "DA0007.cs", new[] + { + DiagnosticResult.CompilerError(ApiRequestAnalyzer.MethodHasParametersRule.Id).WithSpan(11, 19, 11, 25).WithArguments("UserId") + } + } + }; + + [Theory] + [MemberData(nameof(AnalyzerDetectionData))] + public async Task TestRequestAnalysisDetections(string fileName, DiagnosticResult[] diagnosticResults) { - verifier.FixedState.Sources.Add(content); - } + var test = new CSharpAnalyzerTest + { + TestCode = await File.ReadAllTextAsync(Path.Combine(_testDataPath, fileName)) + }; - await test.RunAsync(); - } + test.ExpectedDiagnostics.AddRange(diagnosticResults); - private string GetSolutionRoot() - { - var currentDirectory = Directory.GetCurrentDirectory(); + await PerformTest(test); + } - while (Directory.EnumerateFiles(currentDirectory).All(x => Path.GetFileName(x) != "DragonFruit.Data.sln")) + private async Task PerformTest(AnalyzerTest test) { - currentDirectory = Path.Combine(currentDirectory, ".."); + var content = ("Common.cs", await File.ReadAllTextAsync(Path.Combine(_testDataPath, "Common.cs"))); + + // add common.cs to test sources + test.TestState.Sources.Add(content); + + if (test is CodeFixTest verifier) + { + verifier.FixedState.Sources.Add(content); + } + + await test.RunAsync(); } - return Path.GetFullPath(currentDirectory); + private string GetSolutionRoot() + { + var currentDirectory = Directory.GetCurrentDirectory(); + + while (Directory.EnumerateFiles(currentDirectory).All(x => Path.GetFileName(x) != "DragonFruit.Data.sln")) + { + currentDirectory = Path.Combine(currentDirectory, ".."); + } + + return Path.GetFullPath(currentDirectory); + } } } diff --git a/DragonFruit.Data.Roslyn.Tests/ApiRequestTemplateTests.cs b/DragonFruit.Data.Roslyn.Tests/ApiRequestTemplateTests.cs index d772e33..cb4c57e 100644 --- a/DragonFruit.Data.Roslyn.Tests/ApiRequestTemplateTests.cs +++ b/DragonFruit.Data.Roslyn.Tests/ApiRequestTemplateTests.cs @@ -6,25 +6,26 @@ using Scriban; using Xunit; -namespace DragonFruit.Data.Roslyn.Tests; - -public class ApiRequestTemplateTests +namespace DragonFruit.Data.Roslyn.Tests { - [Fact] - public async Task TestTemplateParse() + public class ApiRequestTemplateTests { - var assembly = typeof(ApiRequestSourceGenerator).Assembly; - using var template = assembly.GetManifestResourceStream(ApiRequestSourceGenerator.TemplateName); + [Fact] + public async Task TestTemplateParse() + { + var assembly = typeof(ApiRequestSourceGenerator).Assembly; + using var template = assembly.GetManifestResourceStream(ApiRequestSourceGenerator.TemplateName); - Assert.NotNull(template); + Assert.NotNull(template); - using var templateReader = new StreamReader(template); - var templateText = await templateReader.ReadToEndAsync(); + using var templateReader = new StreamReader(template); + var templateText = await templateReader.ReadToEndAsync(); - Assert.True(templateText.Length > 0); + Assert.True(templateText.Length > 0); - var templateAst = Template.ParseLiquid(templateText); + var templateAst = Template.ParseLiquid(templateText); - Assert.False(templateAst.HasErrors); + Assert.False(templateAst.HasErrors); + } } } diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DA0002.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0002.cs index d9b5cf9..6f9b719 100644 --- a/DragonFruit.Data.Roslyn.Tests/_TestData/DA0002.cs +++ b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0002.cs @@ -5,9 +5,9 @@ namespace DragonFruit.Data.Roslyn.Tests.TestData /// /// Dummy request nested in another class (DA0002) /// - public class DA0002 + public partial class DA0002 { - public class DA0002_Req : ApiRequest + public partial class DA0002_Req : ApiRequest { [RequestParameter] public int Id { get; set; } diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DA0003.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0003.cs index 57735f2..a91802c 100644 --- a/DragonFruit.Data.Roslyn.Tests/_TestData/DA0003.cs +++ b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0003.cs @@ -5,7 +5,7 @@ namespace DragonFruit.Data.Roslyn.Tests.TestData /// /// Dummy request, property with no getter (DA0003) /// - public class DA0003 : ApiRequest + public partial class DA0003 : ApiRequest { [RequestParameter] public string Param1 { get; set; } diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DA0004.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0004.cs index 1d75162..2e572a2 100644 --- a/DragonFruit.Data.Roslyn.Tests/_TestData/DA0004.cs +++ b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0004.cs @@ -5,7 +5,7 @@ namespace DragonFruit.Data.Roslyn.Tests.TestData /// /// Parameters not in an ApiRequest (DA0004) /// - public class DA0004 + public partial class DA0004 { [RequestParameter] public string NotAParam { get; set; } diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DA0005.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0005.cs index 268e1bf..ae2dd34 100644 --- a/DragonFruit.Data.Roslyn.Tests/_TestData/DA0005.cs +++ b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0005.cs @@ -5,7 +5,7 @@ namespace DragonFruit.Data.Roslyn.Tests.TestData; /// /// Dummy request with private getter/private method (DA0005) /// -public class DA0005 : ApiRequest +public partial class DA0005 : ApiRequest { [RequestParameter] public string Parameter { private get; set; } diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DA0006.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0006.cs index 07b3e35..b196109 100644 --- a/DragonFruit.Data.Roslyn.Tests/_TestData/DA0006.cs +++ b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0006.cs @@ -5,7 +5,7 @@ namespace DragonFruit.Data.Roslyn.Tests.TestData; /// /// Dummy request with a void return type (DA0006) /// -public class DA0006 : ApiRequest +public partial class DA0006 : ApiRequest { [RequestParameter] public void GetUserData() diff --git a/DragonFruit.Data.Roslyn.Tests/_TestData/DA0007.cs b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0007.cs index 243e750..22de1e3 100644 --- a/DragonFruit.Data.Roslyn.Tests/_TestData/DA0007.cs +++ b/DragonFruit.Data.Roslyn.Tests/_TestData/DA0007.cs @@ -1,15 +1,12 @@ -using System; -using System.Security.Cryptography; -using System.Text; -using DragonFruit.Data.Requests; +using DragonFruit.Data.Requests; namespace DragonFruit.Data.Roslyn.Tests.TestData; /// /// Dummy request, method with parameters (DA0007) /// -public class DA0007 : ApiRequest +public partial class DA0007 : ApiRequest { [RequestParameter] - public string UserId(string originalId) => Convert.ToHexString(MD5.HashData(Encoding.ASCII.GetBytes(originalId))); + public string UserId(string originalId) => originalId.ToLowerInvariant(); } \ No newline at end of file diff --git a/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs b/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs index 32726f9..48ef74e 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs @@ -51,16 +51,17 @@ private void AnalyzeClassDecl(SyntaxNodeAnalysisContext context) return; } - // check if class has partial keyword - if (!classDeclarationNode.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) - { - context.ReportDiagnostic(Diagnostic.Create(PartialClassRule, classDeclarationNode.Identifier.GetLocation(), classDeclarationNode.Identifier.Text)); - } - // check if class is nested if (classDeclarationNode.Parent is ClassDeclarationSyntax) { context.ReportDiagnostic(Diagnostic.Create(NestedClassNotAllowedRule, classDeclarationNode.Identifier.GetLocation(), classDeclarationNode.Identifier.Text)); + return; + } + + // check if class has partial keyword + if (!classDeclarationNode.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) + { + context.ReportDiagnostic(Diagnostic.Create(PartialClassRule, classDeclarationNode.Identifier.GetLocation(), classDeclarationNode.Identifier.Text)); } } From 6a0b0ffefd90dc79095fde7362a71b221a48fa9b Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Wed, 13 Dec 2023 15:14:52 +0000 Subject: [PATCH 099/151] change namespace style --- .../ExternalDependencyLoader.cs | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/DragonFruit.Data.Roslyn/ExternalDependencyLoader.cs b/DragonFruit.Data.Roslyn/ExternalDependencyLoader.cs index 3d1b927..5dfda6d 100644 --- a/DragonFruit.Data.Roslyn/ExternalDependencyLoader.cs +++ b/DragonFruit.Data.Roslyn/ExternalDependencyLoader.cs @@ -6,44 +6,45 @@ using System.Linq; using System.Reflection; -namespace DragonFruit.Data.Roslyn; - -public static class ExternalDependencyLoader +namespace DragonFruit.Data.Roslyn { - private static bool _loaded; - - public static void RegisterDependencyLoader() + public static class ExternalDependencyLoader { - if (_loaded) - { - return; - } + private static bool _loaded; - _loaded = true; - AppDomain.CurrentDomain.AssemblyResolve += HandleAssemblyResolve; - } + public static void RegisterDependencyLoader() + { + if (_loaded) + { + return; + } - // derived from https://stackoverflow.com/a/67074009 - private static Assembly HandleAssemblyResolve(object _, ResolveEventArgs args) - { - var name = new AssemblyName(args.Name); - var loadedAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().FullName == name.FullName); + _loaded = true; + AppDomain.CurrentDomain.AssemblyResolve += HandleAssemblyResolve; + } - if (loadedAssembly != null) + // derived from https://stackoverflow.com/a/67074009 + private static Assembly HandleAssemblyResolve(object _, ResolveEventArgs args) { - return loadedAssembly; - } + var name = new AssemblyName(args.Name); + var loadedAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().FullName == name.FullName); - using var resourceStream = typeof(ExternalDependencyLoader).Assembly.GetManifestResourceStream($"{typeof(ExternalDependencyLoader).Namespace}.{name.Name}.dll"); + if (loadedAssembly != null) + { + return loadedAssembly; + } - if (resourceStream == null) - { - return null; - } + using var resourceStream = typeof(ExternalDependencyLoader).Assembly.GetManifestResourceStream($"{typeof(ExternalDependencyLoader).Namespace}.{name.Name}.dll"); + + if (resourceStream == null) + { + return null; + } - using var memoryStream = new MemoryStream(); - resourceStream.CopyTo(memoryStream); + using var memoryStream = new MemoryStream(); + resourceStream.CopyTo(memoryStream); - return Assembly.Load(memoryStream.ToArray()); + return Assembly.Load(memoryStream.ToArray()); + } } } From 11c3e1f9601ed88ff66577032b54facda1aeab1f Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Thu, 14 Dec 2023 15:19:26 +0000 Subject: [PATCH 100/151] allow non-partial abstract classes --- DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs b/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs index 48ef74e..e569248 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestAnalyzer.cs @@ -58,8 +58,8 @@ private void AnalyzeClassDecl(SyntaxNodeAnalysisContext context) return; } - // check if class has partial keyword - if (!classDeclarationNode.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) + // check if class has partial keyword (and is not abstract) + if (!classDeclarationNode.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)) && !classDeclarationNode.Modifiers.Any(m => m.IsKind(SyntaxKind.AbstractKeyword))) { context.ReportDiagnostic(Diagnostic.Create(PartialClassRule, classDeclarationNode.Identifier.GetLocation(), classDeclarationNode.Identifier.Text)); } From 780398e1c79732b491e93b365635dc884c98078c Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Thu, 14 Dec 2023 15:36:06 +0000 Subject: [PATCH 101/151] update sourcegen and template --- .../ApiRequestSourceGenerator.cs | 50 ++++++++----------- .../Entities/EnumSymbolMetadata.cs | 3 +- .../Entities/EnumerableSymbolMetadata.cs | 3 +- .../Entities/PropertySymbolMetadata.cs | 8 +-- .../Enums/RequestSymbolType.cs | 3 ++ .../Templates/ApiRequest.liquid | 23 ++++----- 6 files changed, 42 insertions(+), 48 deletions(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index d1370be..ad936e1 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -256,48 +256,38 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil var isEnumerable = SupportedCollectionTypes.Contains(returnType.SpecialType) || returnType.AllInterfaces.Any(x => x.Equals(enumerableTypeSymbol, SymbolEqualityComparer.Default)); - // handle IEnumerable> - if (isEnumerable && returnType.AllInterfaces.Any(x => x.Equals(keyValuePairEnumerableTypeSymbol, SymbolEqualityComparer.Default))) + if (returnType.TypeKind == TypeKind.Enum) // handle enums + { + var enumOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumParameterAttribute, SymbolEqualityComparer.Default) == true); + symbolMetadata = new EnumSymbolMetadata(candidate, returnType, parameterName) + { + EnumOption = enumOptions != null ? (EnumOption)enumOptions.ConstructorArguments.ElementAt(0).Value : EnumOption.None + }; + } + else if (streamTypeSymbol.Equals(returnType, SymbolEqualityComparer.Default) || DerivesFrom(returnType, streamTypeSymbol)) // check for Stream + { + symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName, RequestSymbolType.Stream); + } + else if (returnType is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte }) // check for byte[] + { + symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName, RequestSymbolType.ByteArray); + } + else if (isEnumerable && returnType.AllInterfaces.Any(x => x.Equals(keyValuePairEnumerableTypeSymbol, SymbolEqualityComparer.Default))) // IEnumerable> { symbolMetadata = new KeyValuePairSymbolMetadata(candidate, returnType, parameterName); } - // handle IEnumerable, Array[], etc. - else if (isEnumerable) + else if (isEnumerable) // byte[] { var enumerableOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumerableParameterAttribute, SymbolEqualityComparer.Default) == true); - var enumerableType = (EnumerableOption?)enumerableOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumerableOption.Concatenated; - symbolMetadata = new EnumerableSymbolMetadata(candidate, returnType, parameterName) { Separator = (string)enumerableOptions?.ConstructorArguments.ElementAtOrDefault(1).Value ?? ",", - EnumerableOption = enumerableType.ToString() - }; - } - // handle enums - else if (returnType.TypeKind == TypeKind.Enum) - { - var enumOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumParameterAttribute, SymbolEqualityComparer.Default) == true); - var enumType = (EnumOption?)enumOptions?.ConstructorArguments.ElementAt(0).Value ?? EnumOption.None; - - symbolMetadata = new EnumSymbolMetadata(candidate, returnType, parameterName) - { - EnumOption = enumType.ToString() + EnumerableOption = enumerableOptions != null ? (EnumerableOption)enumerableOptions.ConstructorArguments.ElementAt(0).Value : EnumerableOption.Concatenated }; } else { - var psm = new PropertySymbolMetadata(candidate, returnType, parameterName); - - if (streamTypeSymbol.Equals(returnType, SymbolEqualityComparer.Default) || DerivesFrom(returnType, streamTypeSymbol)) - { - psm.SpecialRequestParameter = SpecialRequestParameter.Stream; - } - else if (returnType is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte }) - { - psm.SpecialRequestParameter = SpecialRequestParameter.ByteArray; - } - - symbolMetadata = psm; + symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName, RequestSymbolType.Standard); } symbolMetadata.Depth = depth; diff --git a/DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs b/DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs index f6f608a..62c8d44 100644 --- a/DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs @@ -1,6 +1,7 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details +using DragonFruit.Data.Requests; using DragonFruit.Data.Roslyn.Enums; using Microsoft.CodeAnalysis; @@ -15,6 +16,6 @@ public EnumSymbolMetadata(ISymbol symbol, ITypeSymbol returnType, string paramet { } - public string EnumOption { get; set; } + public EnumOption EnumOption { get; set; } } } diff --git a/DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs b/DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs index 4376756..a8d5808 100644 --- a/DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs @@ -1,6 +1,7 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details +using DragonFruit.Data.Requests; using DragonFruit.Data.Roslyn.Enums; using Microsoft.CodeAnalysis; @@ -16,7 +17,7 @@ public EnumerableSymbolMetadata(ISymbol symbol, ITypeSymbol returnType, string p } public string Separator { get; set; } - public string EnumerableOption { get; set; } + public EnumerableOption EnumerableOption { get; set; } public bool IsByteArray => ReturnType is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte }; } diff --git a/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs b/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs index 87557a7..124a2d9 100644 --- a/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs @@ -8,15 +8,17 @@ namespace DragonFruit.Data.Roslyn.Entities { internal class PropertySymbolMetadata : SymbolMetadata { - public virtual RequestSymbolType Type => RequestSymbolType.Standard; + private readonly RequestSymbolType _type; - public PropertySymbolMetadata(ISymbol symbol, ITypeSymbol returnType, string parameterName) + public virtual RequestSymbolType Type => _type; + + public PropertySymbolMetadata(ISymbol symbol, ITypeSymbol returnType, string parameterName, RequestSymbolType type = RequestSymbolType.Standard) : base(symbol, returnType) { + _type = type; ParameterName = parameterName; } public string ParameterName { get; } - public SpecialRequestParameter? SpecialRequestParameter { get; set; } } } diff --git a/DragonFruit.Data.Roslyn/Enums/RequestSymbolType.cs b/DragonFruit.Data.Roslyn/Enums/RequestSymbolType.cs index 7cc6fbe..18ac795 100644 --- a/DragonFruit.Data.Roslyn/Enums/RequestSymbolType.cs +++ b/DragonFruit.Data.Roslyn/Enums/RequestSymbolType.cs @@ -6,8 +6,11 @@ namespace DragonFruit.Data.Roslyn.Enums internal enum RequestSymbolType { Standard = 0, + Enumerable = 1, Enum = 2, KeyValuePair = 3, + Stream = 4, + ByteArray = 5 } } diff --git a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid index fac0ae8..7754d13 100644 --- a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid @@ -8,7 +8,7 @@ namespace {{ namespace }} { partial class {{ class_name }} : global::DragonFruit.Data.Requests.IRequestBuilder { - public global::System.Net.Http.HttpRequestMessage BuildRequest(global::DragonFruit.Data.Serializers.SerializerResolver serializerResolver) + public new global::System.Net.Http.HttpRequestMessage BuildRequest(global::DragonFruit.Data.Serializers.SerializerResolver serializerResolver) { global::System.UriBuilder uriBuilder = new global::System.UriBuilder(this.RequestPath); @@ -69,18 +69,15 @@ namespace {{ namespace }} { content.Add(new global::System.Net.Http.StringContent(kvp.Value), kvp.Key); } + {% comment %} 4 - Stream {% endcomment %} + {% when 4 -%} + content.Add(new global::System.Net.Http.StreamContent({{ multipart.accessor }}), "{{ multipart.parameter_name }}"); + {% comment %} 5 - ByteArray {% endcomment %} + {% when 5 -%} + content.Add(new global::System.Net.Http.ByteArrayContent({{ multipart.accessor }}), "{{ multipart.parameter_name }}"); + {% comment %} Handle other types using ToString {% endcomment %} {% else -%} - {% case multipart.special_request_parameter -%} - {% comment %} 0 - Stream {% endcomment %} - {% when 0 -%} - content.Add(new global::System.Net.Http.StreamContent({{ multipart.accessor }}), "{{ multipart.parameter_name }}"); - {% comment %} 1 - ByteArray {% endcomment %} - {% when 1 -%} - content.Add(new global::System.Net.Http.ByteArrayContent({{ multipart.accessor }}), "{{ multipart.parameter_name }}"); - {% comment %} Handle other types using ToString {% endcomment %} - {% else -%} - content.Add(new global::System.Net.Http.StringContent({{ multipart.accessor }}.ToString()), "{{ multipart.parameter_name }}"); - {% endcase -%} + content.Add(new global::System.Net.Http.StringContent({{ multipart.accessor }}.ToString()), "{{ multipart.parameter_name }}"); {% endcase -%} {% endcapture -%} @@ -104,7 +101,7 @@ namespace {{ namespace }} {% capture uriparam_append -%} {% case uriparam.type -%} {% when 1 -%} - global::DragonFruit.Data.Converters.EnumerableConverter.AppendEnumerable(formBuilder, {{ uriparam.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ uriparam.enumerable_option }}, "{{ uriparam.parameter_name }}", "{{ uriparam.separator }}"); + global::DragonFruit.Data.Converters.EnumerableConverter.WriteEnumerable(formBuilder, {{ uriparam.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ uriparam.enumerable_option }}, "{{ uriparam.parameter_name }}", "{{ uriparam.separator }}"); {% when 2 -%} global::DragonFruit.Data.Converters.EnumConverter.WriteEnum(formBuilder, {{ uriparam.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ uriparam.enum_option }}, "{{ uriparam.parameter_name }}"); {% when 3 -%} From ef5cffb5dfd234d0db01f0ff29c65bf1dddbac42 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Thu, 14 Dec 2023 16:06:51 +0000 Subject: [PATCH 102/151] update property ordering, limit use of "new" keyword --- .../ApiRequestSourceGenerator.cs | 66 +++++++++++++++++-- .../Entities/RequestSymbolMetadata.cs | 2 +- .../Templates/ApiRequest.liquid | 2 +- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index ad936e1..5861771 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -100,6 +100,8 @@ private void Execute(Compilation compilation, ImmutableArray + /// Collects information from classes regarding properties and methods decorated with and to use in source generation. + /// Applies inheritance rules to ensure that candidates are not duplicated. + /// private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compilation, INamedTypeSymbol symbol) { var metadata = new RequestSymbolMetadata { - Properties = Enum.GetValues(typeof(ParameterType)).Cast().ToDictionary(x => x, _ => (IList)new List()) + Properties = Enum.GetValues(typeof(ParameterType)).Cast().ToDictionary(x => x, _ => new List()) }; // get types used in member processing @@ -259,26 +265,32 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil if (returnType.TypeKind == TypeKind.Enum) // handle enums { var enumOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumParameterAttribute, SymbolEqualityComparer.Default) == true); + symbolMetadata = new EnumSymbolMetadata(candidate, returnType, parameterName) { EnumOption = enumOptions != null ? (EnumOption)enumOptions.ConstructorArguments.ElementAt(0).Value : EnumOption.None }; } - else if (streamTypeSymbol.Equals(returnType, SymbolEqualityComparer.Default) || DerivesFrom(returnType, streamTypeSymbol)) // check for Stream + // check for Stream + else if (streamTypeSymbol.Equals(returnType, SymbolEqualityComparer.Default) || DerivesFrom(returnType, streamTypeSymbol)) { symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName, RequestSymbolType.Stream); } - else if (returnType is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte }) // check for byte[] + // check for byte[] + else if (isEnumerable && returnType is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte }) { symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName, RequestSymbolType.ByteArray); } - else if (isEnumerable && returnType.AllInterfaces.Any(x => x.Equals(keyValuePairEnumerableTypeSymbol, SymbolEqualityComparer.Default))) // IEnumerable> + // IEnumerable> + else if (isEnumerable && returnType.AllInterfaces.Any(x => x.Equals(keyValuePairEnumerableTypeSymbol, SymbolEqualityComparer.Default))) { symbolMetadata = new KeyValuePairSymbolMetadata(candidate, returnType, parameterName); } - else if (isEnumerable) // byte[] + // IEnumerable + else if (isEnumerable) { var enumerableOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumerableParameterAttribute, SymbolEqualityComparer.Default) == true); + symbolMetadata = new EnumerableSymbolMetadata(candidate, returnType, parameterName) { Separator = (string)enumerableOptions?.ConstructorArguments.ElementAtOrDefault(1).Value ?? ",", @@ -287,7 +299,7 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil } else { - symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName, RequestSymbolType.Standard); + symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName); } symbolMetadata.Depth = depth; @@ -299,9 +311,21 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil depth++; } while (currentSymbol?.Equals(apiRequestBaseType, SymbolEqualityComparer.Default) == false); + // reverse by depth to put base properties first but retain order within each depth + foreach (var list in metadata.Properties.Values) + { + list.Sort((a, b) => b.Depth - a.Depth); + } + return metadata; } + /// + /// Determines if a type derives from another type + /// + /// The to check + /// The the is supposed to inherit from + /// true if derives from , else false internal static bool DerivesFrom(ITypeSymbol type, ITypeSymbol baseType) { var classSymbol = type.BaseType; @@ -318,5 +342,35 @@ internal static bool DerivesFrom(ITypeSymbol type, ITypeSymbol baseType) return false; } + + /// + /// Determines whether the specified type will overwrite any generated methods. + /// + /// The type to check + /// The type to stop checking at + /// true if the type will generate a member that hides a base implementation, otherwise false + private static bool WillHideOtherMembers(ITypeSymbol type, ISymbol baseType) + { + // if the type directly inherits from the base type, it will not overwrite any members + if (type.BaseType?.Equals(baseType, SymbolEqualityComparer.Default) == true) + { + return false; + } + + // otherwise, all derived types until the baseType must be abstract + var next = type.BaseType; + + while (next?.Equals(baseType, SymbolEqualityComparer.Default) == false) + { + if (!next.IsAbstract) + { + return true; + } + + next = next.BaseType; + } + + return false; + } } } diff --git a/DragonFruit.Data.Roslyn/Entities/RequestSymbolMetadata.cs b/DragonFruit.Data.Roslyn/Entities/RequestSymbolMetadata.cs index 940a0ba..a3f908c 100644 --- a/DragonFruit.Data.Roslyn/Entities/RequestSymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/Entities/RequestSymbolMetadata.cs @@ -8,7 +8,7 @@ namespace DragonFruit.Data.Roslyn.Entities { internal class RequestSymbolMetadata { - public IReadOnlyDictionary> Properties { get; set; } + public IReadOnlyDictionary> Properties { get; set; } public SymbolMetadata BodyProperty { get; set; } diff --git a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid index 7754d13..bb56e3b 100644 --- a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid @@ -8,7 +8,7 @@ namespace {{ namespace }} { partial class {{ class_name }} : global::DragonFruit.Data.Requests.IRequestBuilder { - public new global::System.Net.Http.HttpRequestMessage BuildRequest(global::DragonFruit.Data.Serializers.SerializerResolver serializerResolver) + public {% if require_new_keyword %}new{% endif %} global::System.Net.Http.HttpRequestMessage BuildRequest(global::DragonFruit.Data.Serializers.SerializerResolver serializerResolver) { global::System.UriBuilder uriBuilder = new global::System.UriBuilder(this.RequestPath); From 5850a49e9675d8d82a54722efae348eef7204f0c Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Thu, 14 Dec 2023 16:08:07 +0000 Subject: [PATCH 103/151] add test project, example request classes --- .../DragonFruit.Data.Tests.csproj | 31 +++++++++++++++++++ .../Requests/BasicPostRequest.cs | 25 +++++++++++++++ .../Requests/InheritedPostRequest.cs | 13 ++++++++ .../Requests/MultipartFormRequest.cs | 22 +++++++++++++ DragonFruit.Data.sln | 6 ++++ 5 files changed, 97 insertions(+) create mode 100644 DragonFruit.Data.Tests/DragonFruit.Data.Tests.csproj create mode 100644 DragonFruit.Data.Tests/Requests/BasicPostRequest.cs create mode 100644 DragonFruit.Data.Tests/Requests/InheritedPostRequest.cs create mode 100644 DragonFruit.Data.Tests/Requests/MultipartFormRequest.cs diff --git a/DragonFruit.Data.Tests/DragonFruit.Data.Tests.csproj b/DragonFruit.Data.Tests/DragonFruit.Data.Tests.csproj new file mode 100644 index 0000000..c205203 --- /dev/null +++ b/DragonFruit.Data.Tests/DragonFruit.Data.Tests.csproj @@ -0,0 +1,31 @@ + + + + false + true + net8.0 + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + diff --git a/DragonFruit.Data.Tests/Requests/BasicPostRequest.cs b/DragonFruit.Data.Tests/Requests/BasicPostRequest.cs new file mode 100644 index 0000000..a31b7d0 --- /dev/null +++ b/DragonFruit.Data.Tests/Requests/BasicPostRequest.cs @@ -0,0 +1,25 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System.Collections.Generic; +using System.Net.Http; +using DragonFruit.Data.Requests; + +namespace DragonFruit.Data.Tests.Requests +{ + public partial class BasicPostRequest : ApiRequest + { + public override HttpMethod RequestMethod => HttpMethod.Post; + public override string RequestPath => "https://postman-echo.com/post"; + + [RequestParameter(ParameterType.Query, "q1")] + public string Query1 => "test_query_1"; + + [RequestParameter(ParameterType.Query, "q2")] + public string Query2 => "test_query_2"; + + [RequestParameter(ParameterType.Form, "f1")] + [EnumerableOptions(EnumerableOption.Concatenated, ":")] + public IEnumerable FormListing => new[] { "test_3", "test_3a" }; + } +} diff --git a/DragonFruit.Data.Tests/Requests/InheritedPostRequest.cs b/DragonFruit.Data.Tests/Requests/InheritedPostRequest.cs new file mode 100644 index 0000000..e1a20dc --- /dev/null +++ b/DragonFruit.Data.Tests/Requests/InheritedPostRequest.cs @@ -0,0 +1,13 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using DragonFruit.Data.Requests; + +namespace DragonFruit.Data.Tests.Requests +{ + public partial class InheritedPostRequest : BasicPostRequest + { + [RequestParameter(ParameterType.Query, "extra")] + public string Additional => "additional_content"; + } +} diff --git a/DragonFruit.Data.Tests/Requests/MultipartFormRequest.cs b/DragonFruit.Data.Tests/Requests/MultipartFormRequest.cs new file mode 100644 index 0000000..5a58842 --- /dev/null +++ b/DragonFruit.Data.Tests/Requests/MultipartFormRequest.cs @@ -0,0 +1,22 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System.Net.Http; +using System.Text; +using DragonFruit.Data.Requests; + +namespace DragonFruit.Data.Tests.Requests +{ + [FormBodyType(FormBodyType.MultipartForm)] + public partial class MultipartFormRequest : ApiRequest + { + public override string RequestPath => "https://postman-echo.com/post"; + public override HttpMethod RequestMethod => HttpMethod.Post; + + [RequestParameter(ParameterType.Query, "c")] + public string Content => "content"; + + [RequestParameter(ParameterType.Form, "bytes")] + public byte[] ContentBytes => Encoding.UTF8.GetBytes(Content); + } +} diff --git a/DragonFruit.Data.sln b/DragonFruit.Data.sln index 34c67fe..94e71ed 100644 --- a/DragonFruit.Data.sln +++ b/DragonFruit.Data.sln @@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Roslyn", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Roslyn.Tests", "DragonFruit.Data.Roslyn.Tests\DragonFruit.Data.Roslyn.Tests.csproj", "{D2CF29A3-BB55-44C2-9471-A994DA8BBAD2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Tests", "DragonFruit.Data.Tests\DragonFruit.Data.Tests.csproj", "{3526B57B-615F-48AC-ABC4-A2A8E5F659AD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -37,6 +39,10 @@ Global {D2CF29A3-BB55-44C2-9471-A994DA8BBAD2}.Debug|Any CPU.Build.0 = Debug|Any CPU {D2CF29A3-BB55-44C2-9471-A994DA8BBAD2}.Release|Any CPU.ActiveCfg = Release|Any CPU {D2CF29A3-BB55-44C2-9471-A994DA8BBAD2}.Release|Any CPU.Build.0 = Release|Any CPU + {3526B57B-615F-48AC-ABC4-A2A8E5F659AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3526B57B-615F-48AC-ABC4-A2A8E5F659AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3526B57B-615F-48AC-ABC4-A2A8E5F659AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3526B57B-615F-48AC-ABC4-A2A8E5F659AD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 786bf94e0c5352d04c4c2531105222504b58d28b Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 10:26:13 +0000 Subject: [PATCH 104/151] change multipartform to multipart (enum) --- DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs | 9 +++++---- DragonFruit.Data/Requests/FormBodyType.cs | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 5861771..21b7689 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -92,7 +92,7 @@ private void Execute(Compilation compilation, ImmutableArray x.Equals(enumerableTypeSymbol, SymbolEqualityComparer.Default)); - if (returnType.TypeKind == TypeKind.Enum) // handle enums + // handle enums + if (returnType.TypeKind == TypeKind.Enum) { var enumOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumParameterAttribute, SymbolEqualityComparer.Default) == true); @@ -271,12 +272,12 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil EnumOption = enumOptions != null ? (EnumOption)enumOptions.ConstructorArguments.ElementAt(0).Value : EnumOption.None }; } - // check for Stream + // Stream else if (streamTypeSymbol.Equals(returnType, SymbolEqualityComparer.Default) || DerivesFrom(returnType, streamTypeSymbol)) { symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName, RequestSymbolType.Stream); } - // check for byte[] + // byte[] else if (isEnumerable && returnType is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte }) { symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName, RequestSymbolType.ByteArray); diff --git a/DragonFruit.Data/Requests/FormBodyType.cs b/DragonFruit.Data/Requests/FormBodyType.cs index 2d6d8a0..c032dce 100644 --- a/DragonFruit.Data/Requests/FormBodyType.cs +++ b/DragonFruit.Data/Requests/FormBodyType.cs @@ -6,6 +6,6 @@ namespace DragonFruit.Data.Requests public enum FormBodyType { UriEncoded = 1, - MultipartForm = 2 + Multipart = 2 } } From 6374c9d348b417d8b1aaf7bd335592e597ef7225 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 10:26:26 +0000 Subject: [PATCH 105/151] change multipartform to multipart (refactor) --- DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs b/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs index 00f79c0..252db71 100644 --- a/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs +++ b/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs @@ -93,7 +93,7 @@ public static HttpRequestMessage CreateHttpRequestMessage(ApiRequest request, Ap break; } - case FormBodyType.MultipartForm: + case FormBodyType.Multipart: { var multipartForm = new MultipartFormDataContent(); From b3b45fc679c82a692dd642e9a73a4ffc16daf71f Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 10:27:30 +0000 Subject: [PATCH 106/151] refactor propertyName -> parameterName --- DragonFruit.Data/Converters/EnumConverter.cs | 4 ++-- .../Converters/EnumerableConverter.cs | 24 +++++++++---------- .../ReflectionRequestMessageBuilder.cs | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/DragonFruit.Data/Converters/EnumConverter.cs b/DragonFruit.Data/Converters/EnumConverter.cs index eccf1ee..c3b93de 100644 --- a/DragonFruit.Data/Converters/EnumConverter.cs +++ b/DragonFruit.Data/Converters/EnumConverter.cs @@ -10,9 +10,9 @@ namespace DragonFruit.Data.Converters { public static class EnumConverter { - public static void WriteEnum(StringBuilder destination, T value, EnumOption mode, string propertyName) where T : Enum + public static void WriteEnum(StringBuilder destination, T value, EnumOption mode, string parameterName) where T : Enum { - destination.AppendFormat("{0}={1}&", propertyName, GetEnumValue(value, mode)); + destination.AppendFormat("{0}={1}&", parameterName, GetEnumValue(value, mode)); } public static string GetEnumValue(T value, EnumOption mode) where T : Enum diff --git a/DragonFruit.Data/Converters/EnumerableConverter.cs b/DragonFruit.Data/Converters/EnumerableConverter.cs index 54bb084..2fd7800 100644 --- a/DragonFruit.Data/Converters/EnumerableConverter.cs +++ b/DragonFruit.Data/Converters/EnumerableConverter.cs @@ -21,9 +21,9 @@ public static class EnumerableConverter /// The destination /// The source collection /// The to use. If none provided, defaults to - /// The name of the property to use when writing values to + /// The name of the parameter to use when writing values to /// The separator to use, if required. - public static void WriteEnumerable(StringBuilder destination, IEnumerable source, EnumerableOption? mode, string propertyName, string separator) + public static void WriteEnumerable(StringBuilder destination, IEnumerable source, EnumerableOption? mode, string parameterName, string separator) { switch (mode ?? DefaultOption) { @@ -31,7 +31,7 @@ public static void WriteEnumerable(StringBuilder destination, IEnumerable source { foreach (var item in source) { - destination.Append($"{propertyName}={Uri.EscapeDataString(item.ToString())}&"); + destination.Append($"{parameterName}={Uri.EscapeDataString(item.ToString())}&"); } break; @@ -43,7 +43,7 @@ public static void WriteEnumerable(StringBuilder destination, IEnumerable source foreach (var item in source) { - destination.AppendFormat("{0}[{1}]={2}&", propertyName, counter++, Uri.EscapeDataString(item.ToString())); + destination.AppendFormat("{0}[{1}]={2}&", parameterName, counter++, Uri.EscapeDataString(item.ToString())); } break; @@ -53,7 +53,7 @@ public static void WriteEnumerable(StringBuilder destination, IEnumerable source { foreach (var item in source) { - destination.AppendFormat("{0}[]={1}&", propertyName, Uri.EscapeDataString(item.ToString())); + destination.AppendFormat("{0}[]={1}&", parameterName, Uri.EscapeDataString(item.ToString())); } break; @@ -61,7 +61,7 @@ public static void WriteEnumerable(StringBuilder destination, IEnumerable source default: { - destination.AppendFormat("{0}={1}&", propertyName, string.Join(separator ?? DefaultSeparator, source.Cast().Select(x => Uri.EscapeDataString(x.ToString())))); + destination.AppendFormat("{0}={1}&", parameterName, string.Join(separator ?? DefaultSeparator, source.Cast().Select(x => Uri.EscapeDataString(x.ToString())))); break; } } @@ -72,9 +72,9 @@ public static void WriteEnumerable(StringBuilder destination, IEnumerable source /// /// The to derive pairs from /// The to use - /// The name of the property to use + /// The name of the parameter to use /// The separator to use, if is set to Concatenated - public static IEnumerable> GetPairs(IEnumerable source, EnumerableOption? mode, string propertyName, string separator) + public static IEnumerable> GetPairs(IEnumerable source, EnumerableOption? mode, string parameterName, string separator) { switch (mode ?? DefaultOption) { @@ -82,7 +82,7 @@ public static IEnumerable> GetPairs(IEnumerable sou { foreach (var item in source) { - yield return new KeyValuePair(propertyName, Uri.EscapeDataString(item.ToString())); + yield return new KeyValuePair(parameterName, Uri.EscapeDataString(item.ToString())); } break; @@ -94,7 +94,7 @@ public static IEnumerable> GetPairs(IEnumerable sou foreach (var item in source) { - yield return new KeyValuePair($"{propertyName}[{counter++}]", Uri.EscapeDataString(item.ToString())); + yield return new KeyValuePair($"{parameterName}[{counter++}]", Uri.EscapeDataString(item.ToString())); } break; @@ -104,7 +104,7 @@ public static IEnumerable> GetPairs(IEnumerable sou { foreach (var item in source) { - yield return new KeyValuePair($"{propertyName}[]", Uri.EscapeDataString(item.ToString())); + yield return new KeyValuePair($"{parameterName}[]", Uri.EscapeDataString(item.ToString())); } break; @@ -112,7 +112,7 @@ public static IEnumerable> GetPairs(IEnumerable sou default: { - yield return new KeyValuePair(propertyName, string.Join(separator ?? DefaultSeparator, source.Cast().Select(x => Uri.EscapeDataString(x.ToString())))); + yield return new KeyValuePair(parameterName, string.Join(separator ?? DefaultSeparator, source.Cast().Select(x => Uri.EscapeDataString(x.ToString())))); break; } diff --git a/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs b/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs index 252db71..2b70e1d 100644 --- a/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs +++ b/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs @@ -21,7 +21,7 @@ public static HttpRequestMessage CreateHttpRequestMessage(ApiRequest request, Ap { var requestType = request.GetType(); var requestProperties = requestType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); - var requestParams = requestProperties.Select(GetPropertyInfo).Where(x => x != null).ToLookup(x => x.Value.PropertyType, x => (x.Value.PropertyName, x.Value.Accessor)); + var requestParams = requestProperties.Select(GetPropertyInfo).Where(x => x != null).ToLookup(x => x.Value.ParameterType, x => (PropertyName: x.Value.ParameterName, x.Value.Accessor)); var requestUri = new UriBuilder(request.RequestPath); @@ -239,7 +239,7 @@ private static void WriteMultipartProperty(MultipartFormDataContent multipartFor } } - private static (ParameterType PropertyType, string PropertyName, PropertyInfo Accessor)? GetPropertyInfo(PropertyInfo property) + private static (ParameterType ParameterType, string ParameterName, PropertyInfo Accessor)? GetPropertyInfo(PropertyInfo property) { var attribute = property.GetCustomAttribute(); From fad658f4119ec497dc42b2a601f76bbfb7433747 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 10:27:45 +0000 Subject: [PATCH 107/151] add remaining apirequests to test --- .../DragonFruit.Data.Tests.csproj | 8 ------- .../Requests/BasicPostRequest.cs | 5 ----- .../Requests/MultipartFormRequest.cs | 6 +++++- .../Requests/SpecialTypeRequest.cs | 21 +++++++++++++++++++ 4 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 DragonFruit.Data.Tests/Requests/SpecialTypeRequest.cs diff --git a/DragonFruit.Data.Tests/DragonFruit.Data.Tests.csproj b/DragonFruit.Data.Tests/DragonFruit.Data.Tests.csproj index c205203..2546aad 100644 --- a/DragonFruit.Data.Tests/DragonFruit.Data.Tests.csproj +++ b/DragonFruit.Data.Tests/DragonFruit.Data.Tests.csproj @@ -20,12 +20,4 @@ - - - - - - - - diff --git a/DragonFruit.Data.Tests/Requests/BasicPostRequest.cs b/DragonFruit.Data.Tests/Requests/BasicPostRequest.cs index a31b7d0..81b51fa 100644 --- a/DragonFruit.Data.Tests/Requests/BasicPostRequest.cs +++ b/DragonFruit.Data.Tests/Requests/BasicPostRequest.cs @@ -1,7 +1,6 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details -using System.Collections.Generic; using System.Net.Http; using DragonFruit.Data.Requests; @@ -17,9 +16,5 @@ public partial class BasicPostRequest : ApiRequest [RequestParameter(ParameterType.Query, "q2")] public string Query2 => "test_query_2"; - - [RequestParameter(ParameterType.Form, "f1")] - [EnumerableOptions(EnumerableOption.Concatenated, ":")] - public IEnumerable FormListing => new[] { "test_3", "test_3a" }; } } diff --git a/DragonFruit.Data.Tests/Requests/MultipartFormRequest.cs b/DragonFruit.Data.Tests/Requests/MultipartFormRequest.cs index 5a58842..db5759e 100644 --- a/DragonFruit.Data.Tests/Requests/MultipartFormRequest.cs +++ b/DragonFruit.Data.Tests/Requests/MultipartFormRequest.cs @@ -1,13 +1,14 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details +using System.IO; using System.Net.Http; using System.Text; using DragonFruit.Data.Requests; namespace DragonFruit.Data.Tests.Requests { - [FormBodyType(FormBodyType.MultipartForm)] + [FormBodyType(FormBodyType.Multipart)] public partial class MultipartFormRequest : ApiRequest { public override string RequestPath => "https://postman-echo.com/post"; @@ -18,5 +19,8 @@ public partial class MultipartFormRequest : ApiRequest [RequestParameter(ParameterType.Form, "bytes")] public byte[] ContentBytes => Encoding.UTF8.GetBytes(Content); + + [RequestParameter(ParameterType.Form, "file")] + public Stream File => new MemoryStream(ContentBytes); } } diff --git a/DragonFruit.Data.Tests/Requests/SpecialTypeRequest.cs b/DragonFruit.Data.Tests/Requests/SpecialTypeRequest.cs new file mode 100644 index 0000000..98f3cb5 --- /dev/null +++ b/DragonFruit.Data.Tests/Requests/SpecialTypeRequest.cs @@ -0,0 +1,21 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System.Collections.Generic; +using DragonFruit.Data.Requests; + +namespace DragonFruit.Data.Tests.Requests +{ + public partial class SpecialTypeRequest : ApiRequest + { + public override string RequestPath => "https://postman-echo.com/get"; + + [RequestParameter(ParameterType.Query, "users")] + [EnumerableOptions(EnumerableOption.Concatenated, ":")] + public IReadOnlyCollection Usernames => new[] { "test", "test_1a" }; + + [RequestParameter(ParameterType.Query, "ids")] + [EnumerableOptions(EnumerableOption.Concatenated)] + public IEnumerable UserIds => new[] { 1, 2 }; + } +} From 3ec35b2803829cdce4abcd17b56c4efc2ecbd7dc Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 10:38:45 +0000 Subject: [PATCH 108/151] add tests for converters --- DragonFruit.Data.Tests/ConverterTests.cs | 96 ++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 DragonFruit.Data.Tests/ConverterTests.cs diff --git a/DragonFruit.Data.Tests/ConverterTests.cs b/DragonFruit.Data.Tests/ConverterTests.cs new file mode 100644 index 0000000..e730d5c --- /dev/null +++ b/DragonFruit.Data.Tests/ConverterTests.cs @@ -0,0 +1,96 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using DragonFruit.Data.Converters; +using DragonFruit.Data.Requests; +using Xunit; + +namespace DragonFruit.Data.Tests +{ + public class ConverterTests + { + private const string DefaultParameterName = "property"; + + [Theory] + [InlineData(EnumOption.None, TestEnum.Option3, "Option3")] + [InlineData(EnumOption.Numeric, TestEnum.Option1 | TestEnum.Option3, "3")] + [InlineData(EnumOption.StringLower, TestEnum.Option1 | TestEnum.Option3, "option1, option3")] + [InlineData(EnumOption.StringUpper, TestEnum.Option1 | TestEnum.Option3, "OPTION1, OPTION3")] + public void TestEnumConverter(EnumOption mode, TestEnum input, string expectedOutput) + { + var builder = new StringBuilder(); + EnumConverter.WriteEnum(builder, input, mode, DefaultParameterName); + + if (builder.Length == 0) + { + Assert.True(false, "Builder was empty"); + } + + builder.Length--; // remove the trailing & + Assert.Equal($"{DefaultParameterName}={expectedOutput}", builder.ToString()); + } + + [Theory] + [InlineData(EnumerableOption.Concatenated, $"{DefaultParameterName}=s1,s2,s3,a1,a2")] + [InlineData(EnumerableOption.Recursive, $"{DefaultParameterName}=s1&{DefaultParameterName}=s2&{DefaultParameterName}=s3&{DefaultParameterName}=a1&{DefaultParameterName}=a2")] + [InlineData(EnumerableOption.Unordered, $"{DefaultParameterName}[]=s1&{DefaultParameterName}[]=s2&{DefaultParameterName}[]=s3&{DefaultParameterName}[]=a1&{DefaultParameterName}[]=a2")] + [InlineData(EnumerableOption.Indexed, $"{DefaultParameterName}[0]=s1&{DefaultParameterName}[1]=s2&{DefaultParameterName}[2]=s3&{DefaultParameterName}[3]=a1&{DefaultParameterName}[4]=a2")] + public void TestEnumerableConverter(EnumerableOption mode, string expectedOutput) + { + var builder = new StringBuilder(); + var testData = new[] { "s1", "s2", "s3", "a1", "a2" }; + + EnumerableConverter.WriteEnumerable(builder, testData, mode, DefaultParameterName, null); + + if (builder.Length == 0) + { + Assert.True(false, "Builder was empty"); + } + + builder.Length--; // remove the trailing & + Assert.Equal(expectedOutput, builder.ToString()); + + // test getpairs method to ensure consistent outputs + var resultPairs = expectedOutput.Split('&').Select(x => + { + var segments = x.Split('='); + return new KeyValuePair(segments[0], segments[1]); + }); + + Assert.True(EnumerableConverter.GetPairs(testData, mode, DefaultParameterName, null).SequenceEqual(resultPairs)); + } + + [Fact] + public void TestKeyValuePairConverter() + { + var pairs = new Dictionary + { + ["user"] = "test", + ["user_id"] = "494c0d6dad004a9d8c2d8f086c674a81", + ["last_login"] = "2020-01-01T00:00:00Z" + }; + + var builder = new StringBuilder(); + KeyValuePairConverter.WriteKeyValuePairs(builder, pairs); + + if (builder.Length == 0) + { + Assert.True(false, "Builder was empty"); + } + + builder.Length--; // remove the trailing & + Assert.Equal("user=test&user_id=494c0d6dad004a9d8c2d8f086c674a81&last_login=2020-01-01T00%3A00%3A00Z", builder.ToString()); + } + + [Flags] + public enum TestEnum + { + Option1 = 1, + Option3 = 2 + } + } +} From d4d2f5b0c917d9f077adf3668c672bf51fd07330 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 10:44:38 +0000 Subject: [PATCH 109/151] add static property/method support --- DragonFruit.Data.Roslyn/Entities/SymbolMetadata.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/DragonFruit.Data.Roslyn/Entities/SymbolMetadata.cs b/DragonFruit.Data.Roslyn/Entities/SymbolMetadata.cs index 537c18f..c4c0e57 100644 --- a/DragonFruit.Data.Roslyn/Entities/SymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/Entities/SymbolMetadata.cs @@ -18,7 +18,14 @@ public SymbolMetadata(ISymbol symbol, ITypeSymbol returnType) public ISymbol Symbol { get; } public ITypeSymbol ReturnType { get; } - public string Accessor => Symbol is IPropertySymbol ps ? $"this.{ps.Name}" : $"this.{Symbol.Name}()"; + public string Accessor => Symbol switch + { + IPropertySymbol propertySymbol when Symbol.IsStatic => $"{propertySymbol.ContainingType.Name}.{propertySymbol.Name}", + IPropertySymbol propertySymbol => $"this.{propertySymbol.Name}", + + _ => Symbol.IsStatic ? $"{Symbol.ContainingType.Name}.{Symbol.Name}" : $"this.{Symbol.Name}()" + }; + public bool Nullable => ReturnType.IsReferenceType || (ReturnType.IsValueType && ReturnType.NullableAnnotation == NullableAnnotation.Annotated); } } From f24ae58c1e23a7991cd69488777481fd050e41e9 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 10:44:50 +0000 Subject: [PATCH 110/151] convert to auto property --- DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs b/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs index 124a2d9..b9a5b03 100644 --- a/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs @@ -8,14 +8,12 @@ namespace DragonFruit.Data.Roslyn.Entities { internal class PropertySymbolMetadata : SymbolMetadata { - private readonly RequestSymbolType _type; - - public virtual RequestSymbolType Type => _type; + public virtual RequestSymbolType Type { get; } public PropertySymbolMetadata(ISymbol symbol, ITypeSymbol returnType, string parameterName, RequestSymbolType type = RequestSymbolType.Standard) : base(symbol, returnType) { - _type = type; + Type = type; ParameterName = parameterName; } From e05f42d805cfae66b0ee07ecf7ff6eb120ae462c Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 10:46:24 +0000 Subject: [PATCH 111/151] update test to use a static variable --- .../{BasicPostRequest.cs => BasicEchoRequest.cs} | 9 +++++---- .../{InheritedPostRequest.cs => InheritedEchoRequest.cs} | 8 ++++++-- 2 files changed, 11 insertions(+), 6 deletions(-) rename DragonFruit.Data.Tests/Requests/{BasicPostRequest.cs => BasicEchoRequest.cs} (73%) rename DragonFruit.Data.Tests/Requests/{InheritedPostRequest.cs => InheritedEchoRequest.cs} (51%) diff --git a/DragonFruit.Data.Tests/Requests/BasicPostRequest.cs b/DragonFruit.Data.Tests/Requests/BasicEchoRequest.cs similarity index 73% rename from DragonFruit.Data.Tests/Requests/BasicPostRequest.cs rename to DragonFruit.Data.Tests/Requests/BasicEchoRequest.cs index 81b51fa..3f87ee6 100644 --- a/DragonFruit.Data.Tests/Requests/BasicPostRequest.cs +++ b/DragonFruit.Data.Tests/Requests/BasicEchoRequest.cs @@ -1,20 +1,21 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details -using System.Net.Http; using DragonFruit.Data.Requests; namespace DragonFruit.Data.Tests.Requests { - public partial class BasicPostRequest : ApiRequest + public partial class BasicEchoRequest : ApiRequest { - public override HttpMethod RequestMethod => HttpMethod.Post; - public override string RequestPath => "https://postman-echo.com/post"; + public override string RequestPath => "https://postman-echo.com/get"; [RequestParameter(ParameterType.Query, "q1")] public string Query1 => "test_query_1"; [RequestParameter(ParameterType.Query, "q2")] public string Query2 => "test_query_2"; + + [RequestParameter(ParameterType.Query, "q3")] + public static string StaticQuery3 => "test_query_3"; } } diff --git a/DragonFruit.Data.Tests/Requests/InheritedPostRequest.cs b/DragonFruit.Data.Tests/Requests/InheritedEchoRequest.cs similarity index 51% rename from DragonFruit.Data.Tests/Requests/InheritedPostRequest.cs rename to DragonFruit.Data.Tests/Requests/InheritedEchoRequest.cs index e1a20dc..7e9b1ec 100644 --- a/DragonFruit.Data.Tests/Requests/InheritedPostRequest.cs +++ b/DragonFruit.Data.Tests/Requests/InheritedEchoRequest.cs @@ -1,13 +1,17 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details +using System.Net.Http; using DragonFruit.Data.Requests; namespace DragonFruit.Data.Tests.Requests { - public partial class InheritedPostRequest : BasicPostRequest + public partial class InheritedEchoRequest : BasicEchoRequest { - [RequestParameter(ParameterType.Query, "extra")] + public override string RequestPath => "https://postman-echo.com/post"; + public override HttpMethod RequestMethod => HttpMethod.Post; + + [RequestParameter(ParameterType.Form, "extra")] public string Additional => "additional_content"; } } From 81ec191c40323a01aa1912c4faebac6b8723cdb2 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 11:09:33 +0000 Subject: [PATCH 112/151] update reflection signature, make public --- .../Converters/ReflectionRequestMessageBuilder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs b/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs index 2b70e1d..a957976 100644 --- a/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs +++ b/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs @@ -15,9 +15,9 @@ namespace DragonFruit.Data.Converters { - internal static class ReflectionRequestMessageBuilder + public static class ReflectionRequestMessageBuilder { - public static HttpRequestMessage CreateHttpRequestMessage(ApiRequest request, ApiClient client) + public static HttpRequestMessage CreateHttpRequestMessage(ApiRequest request, SerializerResolver serializers) { var requestType = request.GetType(); var requestProperties = requestType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); @@ -66,7 +66,7 @@ public static HttpRequestMessage CreateHttpRequestMessage(ApiRequest request, Ap Stream stream => new StreamContent(stream), byte[] byteArray => new ByteArrayContent(byteArray), - _ => client.Serializers.Resolve(bodyProperty.PropertyType, DataDirection.Out).Serialize(bodyContent) + _ => serializers.Resolve(bodyProperty.PropertyType, DataDirection.Out).Serialize(bodyContent) }; } } From cc1d44066b18f1fe1b5765ffd5d2422d71fb219b Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 11:09:58 +0000 Subject: [PATCH 113/151] fix reflection not using an instance when needed --- .../ReflectionRequestMessageBuilder.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs b/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs index a957976..50ca139 100644 --- a/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs +++ b/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs @@ -32,7 +32,7 @@ public static HttpRequestMessage CreateHttpRequestMessage(ApiRequest request, Se foreach (var queryParameter in requestParams[ParameterType.Query]) { - WriteUriProperty(queryBuilder, queryParameter.PropertyName, queryParameter.Accessor); + WriteUriProperty(queryBuilder, queryParameter.PropertyName, queryParameter.Accessor, request); } if (queryBuilder.Length > 0) @@ -48,7 +48,7 @@ public static HttpRequestMessage CreateHttpRequestMessage(ApiRequest request, Se // add headers foreach (var headerParameter in requestParams[ParameterType.Header]) { - WriteHeaderProperty(requestMessage.Headers, headerParameter.PropertyName, headerParameter.Accessor); + WriteHeaderProperty(requestMessage.Headers, headerParameter.PropertyName, headerParameter.Accessor, request); } // check if there are any form params, if not, then check for a body property @@ -80,7 +80,7 @@ public static HttpRequestMessage CreateHttpRequestMessage(ApiRequest request, Se foreach (var formParameter in requestParams[ParameterType.Form]) { - WriteUriProperty(formBuilder, formParameter.PropertyName, formParameter.Accessor); + WriteUriProperty(formBuilder, formParameter.PropertyName, formParameter.Accessor, request); } if (formBuilder.Length > 0) @@ -99,7 +99,7 @@ public static HttpRequestMessage CreateHttpRequestMessage(ApiRequest request, Se foreach (var formParameter in requestParams[ParameterType.Form]) { - WriteMultipartProperty(multipartForm, formParameter.PropertyName, formParameter.Accessor); + WriteMultipartProperty(multipartForm, formParameter.PropertyName, formParameter.Accessor, request); } requestMessage.Content = multipartForm; @@ -114,9 +114,9 @@ public static HttpRequestMessage CreateHttpRequestMessage(ApiRequest request, Se return requestMessage; } - private static void WriteUriProperty(StringBuilder destination, string parameterName, PropertyInfo accessor) + private static void WriteUriProperty(StringBuilder destination, string parameterName, PropertyInfo accessor, object source) { - var propertyValue = accessor.GetValue(null); + var propertyValue = accessor.GetValue(source); if (accessor.PropertyType.IsEnum) { @@ -148,9 +148,9 @@ private static void WriteUriProperty(StringBuilder destination, string parameter } } - private static void WriteHeaderProperty(HttpHeaders collection, string parameterName, PropertyInfo accessor) + private static void WriteHeaderProperty(HttpHeaders collection, string parameterName, PropertyInfo accessor, object source) { - var propertyValue = accessor.GetValue(null); + var propertyValue = accessor.GetValue(source); if (accessor.PropertyType.IsEnum) { @@ -189,9 +189,9 @@ private static void WriteHeaderProperty(HttpHeaders collection, string parameter } } - private static void WriteMultipartProperty(MultipartFormDataContent multipartForm, string parameterName, PropertyInfo accessor) + private static void WriteMultipartProperty(MultipartFormDataContent multipartForm, string parameterName, PropertyInfo accessor, object source) { - var value = accessor.GetValue(null); + var value = accessor.GetValue(source); if (accessor.PropertyType.IsEnum) { From fc3eae7d05312e367506d7a731831c809ea4f7e4 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 11:10:57 +0000 Subject: [PATCH 114/151] add special handling for strings (IEnumerable) --- DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs | 7 ++++++- .../Converters/ReflectionRequestMessageBuilder.cs | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 21b7689..ff0f3c2 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -263,7 +263,7 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil var isEnumerable = SupportedCollectionTypes.Contains(returnType.SpecialType) || returnType.AllInterfaces.Any(x => x.Equals(enumerableTypeSymbol, SymbolEqualityComparer.Default)); // handle enums - if (returnType.TypeKind == TypeKind.Enum) + if (returnType.SpecialType == SpecialType.System_Enum) { var enumOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumParameterAttribute, SymbolEqualityComparer.Default) == true); @@ -272,6 +272,11 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil EnumOption = enumOptions != null ? (EnumOption)enumOptions.ConstructorArguments.ElementAt(0).Value : EnumOption.None }; } + // string (IEnumerable) + else if (returnType.SpecialType == SpecialType.System_String) + { + symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName); + } // Stream else if (streamTypeSymbol.Equals(returnType, SymbolEqualityComparer.Default) || DerivesFrom(returnType, streamTypeSymbol)) { diff --git a/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs b/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs index 50ca139..6978d76 100644 --- a/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs +++ b/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs @@ -128,6 +128,9 @@ private static void WriteUriProperty(StringBuilder destination, string parameter switch (propertyValue) { + case string _: + goto default; + // KeyValuePair case IEnumerable> dynamicPairs: KeyValuePairConverter.WriteKeyValuePairs(destination, dynamicPairs); @@ -161,6 +164,9 @@ private static void WriteHeaderProperty(HttpHeaders collection, string parameter switch (propertyValue) { + case string _: + goto default; + case IEnumerable> dynamicPairs: { foreach (var kvp in dynamicPairs) @@ -202,6 +208,9 @@ private static void WriteMultipartProperty(MultipartFormDataContent multipartFor { switch (value) { + case string _: + goto default; + case Stream stream: multipartForm.Add(new StreamContent(stream), parameterName); break; From 479ca030b5e15c5f0b9e148271a23e0b1f6b1bbd Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 11:11:04 +0000 Subject: [PATCH 115/151] update apiclient to use new signature --- DragonFruit.Data/ApiClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DragonFruit.Data/ApiClient.cs b/DragonFruit.Data/ApiClient.cs index 332da8f..f1483f9 100644 --- a/DragonFruit.Data/ApiClient.cs +++ b/DragonFruit.Data/ApiClient.cs @@ -307,7 +307,7 @@ protected virtual async ValueTask BuildRequest(ApiRequest re var requestMessage = request is IRequestBuilder rb ? rb.BuildRequest(Serializers) - : ReflectionRequestMessageBuilder.CreateHttpRequestMessage(request, this); + : ReflectionRequestMessageBuilder.CreateHttpRequestMessage(request, Serializers); requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(expectedContentType)); return requestMessage; From 07b8bd0d1f6aca5286722071f8b8a03b08e830e2 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 11:11:16 +0000 Subject: [PATCH 116/151] add first basic request message test --- .../RequestBuildingTests.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 DragonFruit.Data.Tests/RequestBuildingTests.cs diff --git a/DragonFruit.Data.Tests/RequestBuildingTests.cs b/DragonFruit.Data.Tests/RequestBuildingTests.cs new file mode 100644 index 0000000..68f018e --- /dev/null +++ b/DragonFruit.Data.Tests/RequestBuildingTests.cs @@ -0,0 +1,32 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using DragonFruit.Data.Converters; +using DragonFruit.Data.Requests; +using DragonFruit.Data.Tests.Requests; +using Xunit; + +namespace DragonFruit.Data.Tests +{ + public class RequestBuildingTests + { + [Fact] + public void TestBasicEchoRequest() + { + var request = new BasicEchoRequest(); + using var sourceGenMessage = ((IRequestBuilder)request).BuildRequest(null); + using var reflectionGenMessage = ReflectionRequestMessageBuilder.CreateHttpRequestMessage(request, null); + + Assert.NotNull(sourceGenMessage.RequestUri); + Assert.NotNull(reflectionGenMessage.RequestUri); + + // test reflection-generated requests match source-generated ones. + Assert.Equal(sourceGenMessage.RequestUri, reflectionGenMessage.RequestUri); + + // test query string contains correct parameters + Assert.Contains("q1=test_query_1", sourceGenMessage.RequestUri.Query); + Assert.Contains("q2=test_query_2", sourceGenMessage.RequestUri.Query); + Assert.Contains("q3=test_query_3", sourceGenMessage.RequestUri.Query); // static property + } + } +} From 98e671d0f7bc02a713dccb1058aaad6512e22cf6 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 11:28:05 +0000 Subject: [PATCH 117/151] update reflection to include static properties from inheritors --- .../Converters/ReflectionRequestMessageBuilder.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs b/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs index 6978d76..5741aa0 100644 --- a/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs +++ b/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs @@ -20,7 +20,7 @@ public static class ReflectionRequestMessageBuilder public static HttpRequestMessage CreateHttpRequestMessage(ApiRequest request, SerializerResolver serializers) { var requestType = request.GetType(); - var requestProperties = requestType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); + var requestProperties = requestType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy); var requestParams = requestProperties.Select(GetPropertyInfo).Where(x => x != null).ToLookup(x => x.Value.ParameterType, x => (PropertyName: x.Value.ParameterName, x.Value.Accessor)); var requestUri = new UriBuilder(request.RequestPath); @@ -54,8 +54,7 @@ public static HttpRequestMessage CreateHttpRequestMessage(ApiRequest request, Se // check if there are any form params, if not, then check for a body property if (!requestParams[ParameterType.Form].Any()) { - var bodyProperty = requestType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public) - .FirstOrDefault(x => x.GetCustomAttribute() != null); + var bodyProperty = requestProperties.FirstOrDefault(x => x.GetCustomAttribute() != null); if (bodyProperty != null) { From f28a93c6e363f2fb68845a9e41db0770c17ba327 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 11:28:20 +0000 Subject: [PATCH 118/151] update request test to include inherited requests --- DragonFruit.Data.Tests/RequestBuildingTests.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/DragonFruit.Data.Tests/RequestBuildingTests.cs b/DragonFruit.Data.Tests/RequestBuildingTests.cs index 68f018e..7d683ab 100644 --- a/DragonFruit.Data.Tests/RequestBuildingTests.cs +++ b/DragonFruit.Data.Tests/RequestBuildingTests.cs @@ -1,6 +1,7 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details +using System; using DragonFruit.Data.Converters; using DragonFruit.Data.Requests; using DragonFruit.Data.Tests.Requests; @@ -10,10 +11,15 @@ namespace DragonFruit.Data.Tests { public class RequestBuildingTests { - [Fact] - public void TestBasicEchoRequest() + [Theory] + [InlineData(typeof(BasicEchoRequest))] + [InlineData(typeof(InheritedEchoRequest))] + public void TestBasicEchoRequest(Type requestType) { - var request = new BasicEchoRequest(); + var request = Activator.CreateInstance(requestType) as ApiRequest; + + Assert.NotNull(request); + using var sourceGenMessage = ((IRequestBuilder)request).BuildRequest(null); using var reflectionGenMessage = ReflectionRequestMessageBuilder.CreateHttpRequestMessage(request, null); From 5c98be6784665742753bdbf565f1cee48d6b9e14 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 11:31:16 +0000 Subject: [PATCH 119/151] add missing ampersand removal check --- DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid index bb56e3b..1a64ac5 100644 --- a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid @@ -120,6 +120,12 @@ namespace {{ namespace }} {{ uriparam_append }} {% endif -%} {% endfor -%} + + {% comment %} remove trailing & {% endcomment %} + if (formBuilder.Length > 0) + { + formBuilder.Length--; + } request.Content = new global::System.Net.Http.StringContent(formBuilder.ToString(), global::System.Text.Encoding.UTF8, "application/x-www-form-urlencoded"); From 0524ffc2f491238dd8c5374331d541adb90b1689 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 11:31:25 +0000 Subject: [PATCH 120/151] add basic form inspection test --- DragonFruit.Data.Tests/RequestBuildingTests.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/DragonFruit.Data.Tests/RequestBuildingTests.cs b/DragonFruit.Data.Tests/RequestBuildingTests.cs index 7d683ab..c0b0fdf 100644 --- a/DragonFruit.Data.Tests/RequestBuildingTests.cs +++ b/DragonFruit.Data.Tests/RequestBuildingTests.cs @@ -34,5 +34,23 @@ public void TestBasicEchoRequest(Type requestType) Assert.Contains("q2=test_query_2", sourceGenMessage.RequestUri.Query); Assert.Contains("q3=test_query_3", sourceGenMessage.RequestUri.Query); // static property } + + [Fact] + public async void TestInheritedRequest() + { + var request = new InheritedEchoRequest(); + + using var sourceGenMessage = ((IRequestBuilder)request).BuildRequest(null); + using var reflectionGenMessage = ReflectionRequestMessageBuilder.CreateHttpRequestMessage(request, null); + + Assert.NotNull(sourceGenMessage.Content); + Assert.NotNull(reflectionGenMessage.Content); + + // check form contents match + var sourceGenContent = await sourceGenMessage.Content.ReadAsStringAsync(); + var reflectionGenContent = await reflectionGenMessage.Content.ReadAsStringAsync(); + + Assert.Equal(sourceGenContent, reflectionGenContent); + } } } From 5e67df276be67df6c271005b61fdc1ac97655b45 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 13:03:26 +0000 Subject: [PATCH 121/151] add multipart form request test --- .../RequestBuildingTests.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/DragonFruit.Data.Tests/RequestBuildingTests.cs b/DragonFruit.Data.Tests/RequestBuildingTests.cs index c0b0fdf..544abb5 100644 --- a/DragonFruit.Data.Tests/RequestBuildingTests.cs +++ b/DragonFruit.Data.Tests/RequestBuildingTests.cs @@ -2,6 +2,10 @@ // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Nodes; using DragonFruit.Data.Converters; using DragonFruit.Data.Requests; using DragonFruit.Data.Tests.Requests; @@ -52,5 +56,50 @@ public async void TestInheritedRequest() Assert.Equal(sourceGenContent, reflectionGenContent); } + + [Fact] + public async void TestMultipartFormRequest() + { + // actually send requests because it's easier than inspecting the multipart content + using var httpClient = new HttpClient(); + + var request = new MultipartFormRequest(); + var processedResponses = new List(); + + var formats = new[] + { + // sourcegen + ((IRequestBuilder)request).BuildRequest(null), + + // reflection + ReflectionRequestMessageBuilder.CreateHttpRequestMessage(request, null) + }; + + foreach (var message in formats) + { + JsonObject json; + + using (message) + { + using var response = await httpClient.SendAsync(message); + using var contentStream = await response.Content.ReadAsStreamAsync(); + + json = await JsonSerializer.DeserializeAsync(contentStream); + } + + // check querystring + Assert.Equal("content", json["args"]["c"].ToString()); + + // check form contents + Assert.Equal("content", json["form"]["file"].ToString()); + Assert.Equal("content", json["form"]["bytes"].ToString()); + + json.Remove("headers"); + processedResponses.Add(JsonSerializer.SerializeToUtf8Bytes(json)); + } + + // compare remaining json + Assert.All(processedResponses, x => Assert.Equal(processedResponses[0], x)); + } } } From e1b049a8b8c3ef875fe68904fa26960bbe910b8a Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 13:24:28 +0000 Subject: [PATCH 122/151] fix typo --- DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid index 1a64ac5..d619e7f 100644 --- a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid @@ -143,7 +143,7 @@ namespace {{ namespace }} {% capture header_append -%} {% case header.type -%} {% when 1 -%} - foreach (var kvp in global::DragonFruit.Data.Converters.EnumerableConverter.GetPairs({{ header.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ header.enumumerable_option }})) + foreach (var kvp in global::DragonFruit.Data.Converters.EnumerableConverter.GetPairs({{ header.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ header.enumerable_option }})) { request.Headers.Add(kvp.Key, kvp.Value); } From ae38d598f062e41878e2a1a555e05006a8c87dc6 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 13:27:54 +0000 Subject: [PATCH 123/151] further header fixes --- DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid index d619e7f..096c050 100644 --- a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid @@ -143,7 +143,7 @@ namespace {{ namespace }} {% capture header_append -%} {% case header.type -%} {% when 1 -%} - foreach (var kvp in global::DragonFruit.Data.Converters.EnumerableConverter.GetPairs({{ header.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ header.enumerable_option }})) + foreach (var kvp in global::DragonFruit.Data.Converters.EnumerableConverter.GetPairs({{ header.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ header.enumerable_option }}, "{{ header.parameter_name }}", "{{ header.separator }}")) { request.Headers.Add(kvp.Key, kvp.Value); } From a44f72788bde9903a50b8b0615374b7e93be84d3 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 13:28:40 +0000 Subject: [PATCH 124/151] add "additional locations special types could occur" test --- .../AdditionalSpecialTypeLocationsRequest.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 DragonFruit.Data.Tests/Requests/AdditionalSpecialTypeLocationsRequest.cs diff --git a/DragonFruit.Data.Tests/Requests/AdditionalSpecialTypeLocationsRequest.cs b/DragonFruit.Data.Tests/Requests/AdditionalSpecialTypeLocationsRequest.cs new file mode 100644 index 0000000..ed72ec9 --- /dev/null +++ b/DragonFruit.Data.Tests/Requests/AdditionalSpecialTypeLocationsRequest.cs @@ -0,0 +1,41 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System.Collections.Generic; +using DragonFruit.Data.Requests; + +namespace DragonFruit.Data.Tests.Requests +{ + public partial class AdditionalSpecialTypeLocationsRequest : ApiRequest + { + private readonly string[] _ids = + { + "5c2a5585-2682-4c60-9bc7-b11874582af6", + "d85cf845-a35a-48e9-b629-700f77ab1c5d", + "885f37a8-6bf6-4d52-8092-71276ab706fd" + }; + + public override string RequestPath => "https://example.com"; + + [RequestParameter(ParameterType.Form, "ids")] + [EnumerableOptions(EnumerableOption.Concatenated)] + public IEnumerable Ids => _ids; + + [RequestParameter(ParameterType.Header, "X-User-Id-Value")] + [EnumerableOptions(EnumerableOption.Recursive)] + public IEnumerable HeaderIds => _ids; + + [RequestParameter(ParameterType.Form, "id_opt")] + public SourceGenTestEnum Enums => SourceGenTestEnum.One; + + [RequestParameter(ParameterType.Header, "X-User-Option")] + [EnumOptions(EnumOption.Numeric)] + public SourceGenTestEnum HeaderEnums => SourceGenTestEnum.Two; + } + + public enum SourceGenTestEnum + { + One, + Two + } +} From a6fc373a30af8e8ccab91a065c621f05340eee38 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 13:28:51 +0000 Subject: [PATCH 125/151] add base special type handling test --- DragonFruit.Data.Tests/RequestBuildingTests.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/DragonFruit.Data.Tests/RequestBuildingTests.cs b/DragonFruit.Data.Tests/RequestBuildingTests.cs index 544abb5..cce5399 100644 --- a/DragonFruit.Data.Tests/RequestBuildingTests.cs +++ b/DragonFruit.Data.Tests/RequestBuildingTests.cs @@ -101,5 +101,20 @@ public async void TestMultipartFormRequest() // compare remaining json Assert.All(processedResponses, x => Assert.Equal(processedResponses[0], x)); } + + [Fact] + public void TestSpecialTypeHandling() + { + var request = new SpecialTypeRequest(); + + using var sourceGenMessage = ((IRequestBuilder)request).BuildRequest(null); + using var reflectionGenMessage = ReflectionRequestMessageBuilder.CreateHttpRequestMessage(request, null); + + // check query strings match expected output + Assert.Equal(sourceGenMessage.RequestUri!.Query, reflectionGenMessage.RequestUri!.Query); + + Assert.Contains("users=test:test_1a", sourceGenMessage.RequestUri.Query); + Assert.Contains("ids=1,2", sourceGenMessage.RequestUri.Query); + } } } From a238a33f027b7ac3be5afa33bcad1d15579e47ed Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 13:32:23 +0000 Subject: [PATCH 126/151] refactoring --- .../{RequestBuildingTests.cs => RequestTests.cs} | 2 +- ...tionsRequest.cs => SpecialTypeAdditionalLocationsRequest.cs} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename DragonFruit.Data.Tests/{RequestBuildingTests.cs => RequestTests.cs} (99%) rename DragonFruit.Data.Tests/Requests/{AdditionalSpecialTypeLocationsRequest.cs => SpecialTypeAdditionalLocationsRequest.cs} (95%) diff --git a/DragonFruit.Data.Tests/RequestBuildingTests.cs b/DragonFruit.Data.Tests/RequestTests.cs similarity index 99% rename from DragonFruit.Data.Tests/RequestBuildingTests.cs rename to DragonFruit.Data.Tests/RequestTests.cs index cce5399..661e209 100644 --- a/DragonFruit.Data.Tests/RequestBuildingTests.cs +++ b/DragonFruit.Data.Tests/RequestTests.cs @@ -13,7 +13,7 @@ namespace DragonFruit.Data.Tests { - public class RequestBuildingTests + public class RequestTests { [Theory] [InlineData(typeof(BasicEchoRequest))] diff --git a/DragonFruit.Data.Tests/Requests/AdditionalSpecialTypeLocationsRequest.cs b/DragonFruit.Data.Tests/Requests/SpecialTypeAdditionalLocationsRequest.cs similarity index 95% rename from DragonFruit.Data.Tests/Requests/AdditionalSpecialTypeLocationsRequest.cs rename to DragonFruit.Data.Tests/Requests/SpecialTypeAdditionalLocationsRequest.cs index ed72ec9..6eb2d13 100644 --- a/DragonFruit.Data.Tests/Requests/AdditionalSpecialTypeLocationsRequest.cs +++ b/DragonFruit.Data.Tests/Requests/SpecialTypeAdditionalLocationsRequest.cs @@ -6,7 +6,7 @@ namespace DragonFruit.Data.Tests.Requests { - public partial class AdditionalSpecialTypeLocationsRequest : ApiRequest + public partial class SpecialTypeAdditionalLocationsRequest : ApiRequest { private readonly string[] _ids = { From 777afa6ae6ec71b6c50c59eda1977409dc59cff3 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 13:50:26 +0000 Subject: [PATCH 127/151] fix enums not getting picked up properly (revert previous commit) --- DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs | 7 ++++--- DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index ff0f3c2..0e2b22c 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Net.Http; using System.Text; +using DragonFruit.Data.Converters; using DragonFruit.Data.Requests; using DragonFruit.Data.Roslyn.Entities; using DragonFruit.Data.Roslyn.Enums; @@ -263,7 +264,7 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil var isEnumerable = SupportedCollectionTypes.Contains(returnType.SpecialType) || returnType.AllInterfaces.Any(x => x.Equals(enumerableTypeSymbol, SymbolEqualityComparer.Default)); // handle enums - if (returnType.SpecialType == SpecialType.System_Enum) + if (returnType.TypeKind == TypeKind.Enum) { var enumOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumParameterAttribute, SymbolEqualityComparer.Default) == true); @@ -299,8 +300,8 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil symbolMetadata = new EnumerableSymbolMetadata(candidate, returnType, parameterName) { - Separator = (string)enumerableOptions?.ConstructorArguments.ElementAtOrDefault(1).Value ?? ",", - EnumerableOption = enumerableOptions != null ? (EnumerableOption)enumerableOptions.ConstructorArguments.ElementAt(0).Value : EnumerableOption.Concatenated + Separator = (string)enumerableOptions?.ConstructorArguments.ElementAtOrDefault(1).Value ?? EnumerableConverter.DefaultSeparator, + EnumerableOption = enumerableOptions != null ? (EnumerableOption)enumerableOptions.ConstructorArguments.ElementAt(0).Value : EnumerableConverter.DefaultOption }; } else diff --git a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid index 096c050..937bfe3 100644 --- a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid +++ b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid @@ -148,7 +148,7 @@ namespace {{ namespace }} request.Headers.Add(kvp.Key, kvp.Value); } {% when 2 -%} - request.Headers.Add("{{ header.parameter_name }}", global::DragonFruit.Data.Converters.EnumConverter.GetString({{ header.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ header.enum_option }})); + request.Headers.Add("{{ header.parameter_name }}", global::DragonFruit.Data.Converters.EnumConverter.GetEnumValue({{ header.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ header.enum_option }})); {% when 3 -%} foreach (var kvp in (global::System.Collections.Generic.IEnumerable>){{ header.accessor }}) { From 4548fc4c15e1cc48a2b8b4128dad628e21261bff Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 13:50:35 +0000 Subject: [PATCH 128/151] expose defaults for enumerables --- DragonFruit.Data/Converters/EnumerableConverter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DragonFruit.Data/Converters/EnumerableConverter.cs b/DragonFruit.Data/Converters/EnumerableConverter.cs index 2fd7800..7fa646d 100644 --- a/DragonFruit.Data/Converters/EnumerableConverter.cs +++ b/DragonFruit.Data/Converters/EnumerableConverter.cs @@ -12,8 +12,8 @@ namespace DragonFruit.Data.Converters { public static class EnumerableConverter { - private const EnumerableOption DefaultOption = EnumerableOption.Concatenated; - private const string DefaultSeparator = ","; + public const EnumerableOption DefaultOption = EnumerableOption.Concatenated; + public const string DefaultSeparator = ","; /// /// Writes the provided to the using the specified From 7e4d589b38b9f2c2517798839ff5e0ec6a950f6c Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 13:53:32 +0000 Subject: [PATCH 129/151] add additional request type tests --- DragonFruit.Data.Tests/RequestTests.cs | 23 +++++++++++++++++++ .../SpecialTypeAdditionalLocationsRequest.cs | 4 ++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/DragonFruit.Data.Tests/RequestTests.cs b/DragonFruit.Data.Tests/RequestTests.cs index 661e209..3f20c64 100644 --- a/DragonFruit.Data.Tests/RequestTests.cs +++ b/DragonFruit.Data.Tests/RequestTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Text.Json; using System.Text.Json.Nodes; @@ -116,5 +117,27 @@ public void TestSpecialTypeHandling() Assert.Contains("users=test:test_1a", sourceGenMessage.RequestUri.Query); Assert.Contains("ids=1,2", sourceGenMessage.RequestUri.Query); } + + [Fact] + public async void TestAdditionalSpecialTypeHandling() + { + var request = new SpecialTypeAdditionalLocationsRequest(); + + using var sourceGenMessage = ((IRequestBuilder)request).BuildRequest(null); + using var reflectionGenMessage = ReflectionRequestMessageBuilder.CreateHttpRequestMessage(request, null); + + // test headers + Assert.Equal("2", sourceGenMessage.Headers.GetValues("X-User-Option").Single()); + Assert.Equal(3, sourceGenMessage.Headers.SingleOrDefault(x => x.Key == "X-User-Id-Value").Value.Count()); + + // check form contents match + var sourceGenContent = await sourceGenMessage.Content!.ReadAsStringAsync(); + var reflectionGenContent = await reflectionGenMessage.Content!.ReadAsStringAsync(); + + Assert.Equal(sourceGenContent, reflectionGenContent); + + Assert.Contains("id_opt=One", sourceGenContent); + Assert.Contains("ids=5c2a5585-2682-4c60-9bc7-b11874582af6,d85cf845-a35a-48e9-b629-700f77ab1c5d,885f37a8-6bf6-4d52-8092-71276ab706fd", sourceGenContent); + } } } diff --git a/DragonFruit.Data.Tests/Requests/SpecialTypeAdditionalLocationsRequest.cs b/DragonFruit.Data.Tests/Requests/SpecialTypeAdditionalLocationsRequest.cs index 6eb2d13..f18e064 100644 --- a/DragonFruit.Data.Tests/Requests/SpecialTypeAdditionalLocationsRequest.cs +++ b/DragonFruit.Data.Tests/Requests/SpecialTypeAdditionalLocationsRequest.cs @@ -35,7 +35,7 @@ public partial class SpecialTypeAdditionalLocationsRequest : ApiRequest public enum SourceGenTestEnum { - One, - Two + One = 1, + Two = 2 } } From a66fa952359c226166f4f3c9d832bf6c0dd539db Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 14:16:06 +0000 Subject: [PATCH 130/151] optimise metadata generation --- .../ApiRequestSourceGenerator.cs | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 0e2b22c..5ce5c34 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -261,8 +261,6 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil var parameterType = (ParameterType)parameterAttribute.ConstructorArguments[0].Value!; var parameterName = (string)parameterAttribute.ConstructorArguments.ElementAtOrDefault(1).Value ?? candidate.Name; - var isEnumerable = SupportedCollectionTypes.Contains(returnType.SpecialType) || returnType.AllInterfaces.Any(x => x.Equals(enumerableTypeSymbol, SymbolEqualityComparer.Default)); - // handle enums if (returnType.TypeKind == TypeKind.Enum) { @@ -283,30 +281,38 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil { symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName, RequestSymbolType.Stream); } - // byte[] - else if (isEnumerable && returnType is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte }) - { - symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName, RequestSymbolType.ByteArray); - } - // IEnumerable> - else if (isEnumerable && returnType.AllInterfaces.Any(x => x.Equals(keyValuePairEnumerableTypeSymbol, SymbolEqualityComparer.Default))) - { - symbolMetadata = new KeyValuePairSymbolMetadata(candidate, returnType, parameterName); - } - // IEnumerable - else if (isEnumerable) - { - var enumerableOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumerableParameterAttribute, SymbolEqualityComparer.Default) == true); - - symbolMetadata = new EnumerableSymbolMetadata(candidate, returnType, parameterName) - { - Separator = (string)enumerableOptions?.ConstructorArguments.ElementAtOrDefault(1).Value ?? EnumerableConverter.DefaultSeparator, - EnumerableOption = enumerableOptions != null ? (EnumerableOption)enumerableOptions.ConstructorArguments.ElementAt(0).Value : EnumerableConverter.DefaultOption - }; - } else { - symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName); + // other enumerable types (and default) + switch (SupportedCollectionTypes.Contains(returnType.SpecialType) || returnType.AllInterfaces.Any(x => x.Equals(enumerableTypeSymbol, SymbolEqualityComparer.Default))) + { + // byte[] + case true when returnType is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte }: + symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName, RequestSymbolType.ByteArray); + break; + + // IEnumerable> + case true when returnType.AllInterfaces.Any(x => x.Equals(keyValuePairEnumerableTypeSymbol, SymbolEqualityComparer.Default)): + symbolMetadata = new KeyValuePairSymbolMetadata(candidate, returnType, parameterName); + break; + + // IEnumerable + case true: + { + var enumerableOptions = candidate.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Equals(enumerableParameterAttribute, SymbolEqualityComparer.Default) == true); + + symbolMetadata = new EnumerableSymbolMetadata(candidate, returnType, parameterName) + { + Separator = (string)enumerableOptions?.ConstructorArguments.ElementAtOrDefault(1).Value ?? EnumerableConverter.DefaultSeparator, + EnumerableOption = enumerableOptions != null ? (EnumerableOption)enumerableOptions.ConstructorArguments.ElementAt(0).Value : EnumerableConverter.DefaultOption + }; + break; + } + + default: + symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName); + break; + } } symbolMetadata.Depth = depth; From 174223a146c20ab6d21055547a36c557deae9c56 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 14:24:56 +0000 Subject: [PATCH 131/151] add inherited request with different form body type (plus tests) --- DragonFruit.Data.Tests/RequestTests.cs | 18 ++++++++++++++++++ .../Requests/InheritedMultipartEchoRequest.cs | 13 +++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 DragonFruit.Data.Tests/Requests/InheritedMultipartEchoRequest.cs diff --git a/DragonFruit.Data.Tests/RequestTests.cs b/DragonFruit.Data.Tests/RequestTests.cs index 3f20c64..775211d 100644 --- a/DragonFruit.Data.Tests/RequestTests.cs +++ b/DragonFruit.Data.Tests/RequestTests.cs @@ -58,6 +58,24 @@ public async void TestInheritedRequest() Assert.Equal(sourceGenContent, reflectionGenContent); } + [Fact] + public void TestInheritedMultipartRequest() + { + var request = new InheritedMultipartEchoRequest(); + using var sourceGenMessage = ((IRequestBuilder)request).BuildRequest(null); + using var reflectionGenMessage = ReflectionRequestMessageBuilder.CreateHttpRequestMessage(request, null); + + Assert.True(sourceGenMessage.Content is MultipartFormDataContent); + Assert.True(reflectionGenMessage.Content is MultipartFormDataContent); + + var baseRequest = new InheritedEchoRequest(); + using var baseSourceGenMessage = ((IRequestBuilder)baseRequest).BuildRequest(null); + using var baseReflectionGenMessage = ReflectionRequestMessageBuilder.CreateHttpRequestMessage(baseRequest, null); + + Assert.True(baseSourceGenMessage.Content is StringContent); + Assert.True(baseReflectionGenMessage.Content is StringContent); + } + [Fact] public async void TestMultipartFormRequest() { diff --git a/DragonFruit.Data.Tests/Requests/InheritedMultipartEchoRequest.cs b/DragonFruit.Data.Tests/Requests/InheritedMultipartEchoRequest.cs new file mode 100644 index 0000000..8795c0f --- /dev/null +++ b/DragonFruit.Data.Tests/Requests/InheritedMultipartEchoRequest.cs @@ -0,0 +1,13 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using DragonFruit.Data.Requests; + +namespace DragonFruit.Data.Tests.Requests +{ + [FormBodyType(FormBodyType.Multipart)] + public partial class InheritedMultipartEchoRequest : InheritedEchoRequest + { + // no additional properties, just changed body type + } +} From aa095010031fbf22e94d60731cd23923b0e56598 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 14:30:37 +0000 Subject: [PATCH 132/151] update HttpContent check (on source-generated methods) --- DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 5ce5c34..6b8d9f8 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -89,7 +89,8 @@ private void Execute(Compilation compilation, ImmutableArray Date: Fri, 15 Dec 2023 14:38:49 +0000 Subject: [PATCH 133/151] add generic type name generation --- .../ApiRequestSourceGenerator.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 6b8d9f8..9994b3b 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -97,9 +97,24 @@ private void Execute(Compilation compilation, ImmutableArray x.Name))); + genericNameBuilder.Append(">"); + + className = genericNameBuilder.ToString(); + } + + // create template info object var parameterInfo = new { - ClassName = classSymbol.Name, + ClassName = className, Namespace = classSymbol.ContainingNamespace.ToDisplayString(), RequireNewKeyword = WillHideOtherMembers(classSymbol, compilation.GetTypeByMetadataName(typeof(ApiRequest).FullName)), From 838d283c883d995b6daf448e4fe57c2436763331 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 15 Dec 2023 14:39:19 +0000 Subject: [PATCH 134/151] add custom body types --- .../Requests/CustomHttpContentRequest.cs | 17 ++++++++++++++ .../CustomSerializedContentRequest.cs | 22 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 DragonFruit.Data.Tests/Requests/CustomHttpContentRequest.cs create mode 100644 DragonFruit.Data.Tests/Requests/CustomSerializedContentRequest.cs diff --git a/DragonFruit.Data.Tests/Requests/CustomHttpContentRequest.cs b/DragonFruit.Data.Tests/Requests/CustomHttpContentRequest.cs new file mode 100644 index 0000000..87103a2 --- /dev/null +++ b/DragonFruit.Data.Tests/Requests/CustomHttpContentRequest.cs @@ -0,0 +1,17 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System.Net.Http; +using DragonFruit.Data.Requests; + +namespace DragonFruit.Data.Tests.Requests +{ + public partial class CustomHttpContentRequest : ApiRequest + { + public override string RequestPath => "https://postman-echo.com/patch"; + public override HttpMethod RequestMethod => HttpMethod.Patch; + + [RequestBody] + public HttpContent RequestContent => new StringContent("Test Content"); + } +} diff --git a/DragonFruit.Data.Tests/Requests/CustomSerializedContentRequest.cs b/DragonFruit.Data.Tests/Requests/CustomSerializedContentRequest.cs new file mode 100644 index 0000000..2b00d57 --- /dev/null +++ b/DragonFruit.Data.Tests/Requests/CustomSerializedContentRequest.cs @@ -0,0 +1,22 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System.Net.Http; +using DragonFruit.Data.Requests; + +namespace DragonFruit.Data.Tests.Requests +{ + public partial class CustomSerializedContentRequest : ApiRequest where T : class + { + public override string RequestPath => "https://postman-echo.com/patch"; + public override HttpMethod RequestMethod => HttpMethod.Patch; + + public CustomSerializedContentRequest(T requestContent) + { + RequestContent = requestContent; + } + + [RequestBody] + public T RequestContent { get; } + } +} From b81146cc0ac3ea835e8d8fe01042d4bfe3f8caad Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 16 Dec 2023 10:37:58 +0000 Subject: [PATCH 135/151] add custom http content test --- DragonFruit.Data.Tests/RequestTests.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/DragonFruit.Data.Tests/RequestTests.cs b/DragonFruit.Data.Tests/RequestTests.cs index 775211d..8c2f16a 100644 --- a/DragonFruit.Data.Tests/RequestTests.cs +++ b/DragonFruit.Data.Tests/RequestTests.cs @@ -7,6 +7,7 @@ using System.Net.Http; using System.Text.Json; using System.Text.Json.Nodes; +using System.Threading.Tasks; using DragonFruit.Data.Converters; using DragonFruit.Data.Requests; using DragonFruit.Data.Tests.Requests; @@ -157,5 +158,24 @@ public async void TestAdditionalSpecialTypeHandling() Assert.Contains("id_opt=One", sourceGenContent); Assert.Contains("ids=5c2a5585-2682-4c60-9bc7-b11874582af6,d85cf845-a35a-48e9-b629-700f77ab1c5d,885f37a8-6bf6-4d52-8092-71276ab706fd", sourceGenContent); } + + [Fact] + public async Task TestCustomHttpContentRequest() + { + var request = new CustomHttpContentRequest(); + + using var sourceGenMessage = ((IRequestBuilder)request).BuildRequest(null); + using var reflectionGenMessage = ReflectionRequestMessageBuilder.CreateHttpRequestMessage(request, null); + + Assert.NotNull(sourceGenMessage.Content); + Assert.NotNull(reflectionGenMessage.Content); + + // check form contents match + var sourceGenContent = await sourceGenMessage.Content.ReadAsStringAsync(); + var reflectionGenContent = await reflectionGenMessage.Content.ReadAsStringAsync(); + + Assert.Equal(sourceGenContent, reflectionGenContent); + Assert.False(string.IsNullOrEmpty(sourceGenContent)); + } } } From 0b5d8ad3f5a5bc2cfe68efde067754d01e62a327 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 16 Dec 2023 11:00:25 +0000 Subject: [PATCH 136/151] add serializer test --- DragonFruit.Data.Tests/RequestTests.cs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/DragonFruit.Data.Tests/RequestTests.cs b/DragonFruit.Data.Tests/RequestTests.cs index 8c2f16a..cb94cb7 100644 --- a/DragonFruit.Data.Tests/RequestTests.cs +++ b/DragonFruit.Data.Tests/RequestTests.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using DragonFruit.Data.Converters; using DragonFruit.Data.Requests; +using DragonFruit.Data.Serializers; using DragonFruit.Data.Tests.Requests; using Xunit; @@ -177,5 +178,30 @@ public async Task TestCustomHttpContentRequest() Assert.Equal(sourceGenContent, reflectionGenContent); Assert.False(string.IsNullOrEmpty(sourceGenContent)); } + + private record TestRecord(string TestProperty); + + [Fact] + public async Task TestCustomSerializedContentRequest() + { + var record = new TestRecord("Test Content"); + var request = new CustomSerializedContentRequest(record); + var serializers = new SerializerResolver(new ApiJsonSerializer()); + + using var sourceGenMessage = ((IRequestBuilder)request).BuildRequest(serializers); + using var reflectionGenMessage = ReflectionRequestMessageBuilder.CreateHttpRequestMessage(request, serializers); + + using (var sourceGenStream = await sourceGenMessage.Content!.ReadAsStreamAsync()) + { + var sourceGenContent = serializers.Resolve(DataDirection.In).Deserialize(sourceGenStream); + Assert.True(sourceGenContent.Equals(record)); + } + + using (var reflectionGenStream = await reflectionGenMessage.Content!.ReadAsStreamAsync()) + { + var reflectionGenContent = serializers.Resolve(DataDirection.In).Deserialize(reflectionGenStream); + Assert.True(reflectionGenContent.Equals(record)); + } + } } } From 7bcf2656ce6fb6e7ad5ee26ab9678da0dd3d048e Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 16 Dec 2023 16:11:03 +0000 Subject: [PATCH 137/151] add additional Perform* methods --- DragonFruit.Data/ApiClient.cs | 74 +++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 17 deletions(-) diff --git a/DragonFruit.Data/ApiClient.cs b/DragonFruit.Data/ApiClient.cs index f1483f9..e653af0 100644 --- a/DragonFruit.Data/ApiClient.cs +++ b/DragonFruit.Data/ApiClient.cs @@ -66,6 +66,19 @@ public string UserAgent /// protected HttpClient Client => _client ??= CreateClient(); + /// + /// Sends a GET request to the provided , returning a deserialized response. + /// + public Task PerformAsync(string url, CancellationToken cancellationToken = default) where T : class + { + var serializer = Serializers.Resolve(DataDirection.In); + var requestMessage = new HttpRequestMessage(HttpMethod.Get, new Uri(url)); + + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(serializer.ContentType)); + + return PerformAsyncInternal(requestMessage, serializer, cancellationToken); + } + /// /// Builds and performs an , deserializing the results into the specified type. /// @@ -74,17 +87,32 @@ public string UserAgent /// public async Task PerformAsync(ApiRequest request, CancellationToken cancellationToken = default) where T : class { - using var requestMessage = await BuildRequest(request, Serializers.Resolve(DataDirection.In).ContentType).ConfigureAwait(false); - return await PerformAsync(requestMessage, cancellationToken).ConfigureAwait(false); + var serializer = Serializers.Resolve(DataDirection.In); + var requestMessage = await BuildRequest(request, serializer.ContentType).ConfigureAwait(false); + + return await PerformAsyncInternal(requestMessage, serializer, cancellationToken).ConfigureAwait(false); } /// /// Performs a prebuilt , deserializing the results into the specified type. /// - public async Task PerformAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) where T : class + public Task PerformAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) where T : class { - using var responseMessage = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - return await ValidateAndProcess(responseMessage, cancellationToken).ConfigureAwait(false); + return PerformAsyncInternal(request, Serializers.Resolve(DataDirection.In), cancellationToken); + } + + private async Task PerformAsyncInternal(HttpRequestMessage request, ApiSerializer serializer, CancellationToken cancellationToken) where T : class + { + var responseMessage = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + return await ValidateAndProcess(responseMessage, serializer, cancellationToken).ConfigureAwait(false); + } + + /// + /// Sends a GET request to the provided , returning the raw + /// + public Task PerformAsync(string url, CancellationToken cancellationToken = default) + { + return PerformAsync(new HttpRequestMessage(HttpMethod.Get, url), cancellationToken); } /// @@ -92,8 +120,19 @@ public async Task PerformAsync(HttpRequestMessage request, CancellationTok /// public async Task PerformAsync(ApiRequest request, CancellationToken cancellationToken = default) { - using var requestMessage = await BuildRequest(request, "*/*").ConfigureAwait(false); - return await Client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var requestMessage = await BuildRequest(request, "*/*").ConfigureAwait(false); + return await PerformAsync(requestMessage, cancellationToken).ConfigureAwait(false); + } + + /// + /// Sends a prebuilt , returning the raw + /// + public async Task PerformAsync(HttpRequestMessage requestMessage, CancellationToken cancellationToken = default) + { + using (requestMessage) + { + return await Client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + } } /// @@ -267,24 +306,25 @@ protected virtual HttpClient CreateClient() /// /// Overridable handler for validating and processing a /// - protected virtual async Task ValidateAndProcess(HttpResponseMessage response, CancellationToken cancellationToken) where T : class + protected virtual async Task ValidateAndProcess(HttpResponseMessage response, ApiSerializer serializer, CancellationToken cancellationToken) where T : class { - response.EnsureSuccessStatusCode(); + using (response) + { + response.EnsureSuccessStatusCode(); #if NETSTANDARD2_0 - using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); #else - using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); #endif - var serializer = Serializers.Resolve(DataDirection.In); + if (serializer is IAsyncSerializer asyncSerializer) + { + return await asyncSerializer.DeserializeAsync(stream).ConfigureAwait(false); + } - if (serializer is IAsyncSerializer asyncSerializer) - { - return await asyncSerializer.DeserializeAsync(stream).ConfigureAwait(false); + return serializer.Deserialize(stream); } - - return serializer.Deserialize(stream); } /// From 1268256a5a781d51386f9260dc7d32e98fb85ea3 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 16 Dec 2023 16:58:08 +0000 Subject: [PATCH 138/151] ensure request is disposed in deserialized requests --- DragonFruit.Data/ApiClient.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/DragonFruit.Data/ApiClient.cs b/DragonFruit.Data/ApiClient.cs index e653af0..f5e2906 100644 --- a/DragonFruit.Data/ApiClient.cs +++ b/DragonFruit.Data/ApiClient.cs @@ -103,8 +103,11 @@ public Task PerformAsync(HttpRequestMessage request, CancellationToken can private async Task PerformAsyncInternal(HttpRequestMessage request, ApiSerializer serializer, CancellationToken cancellationToken) where T : class { - var responseMessage = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - return await ValidateAndProcess(responseMessage, serializer, cancellationToken).ConfigureAwait(false); + using (request) + { + var responseMessage = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + return await ValidateAndProcess(responseMessage, serializer, cancellationToken).ConfigureAwait(false); + } } /// @@ -285,7 +288,7 @@ void UpdateProgress() /// /// This is designed to be used by libraries requiring overall control of handlers (i.e. wrap the user-selected handler to provide additional functionality) /// - protected virtual HttpMessageHandler CreateHandler() => Handler?.Invoke() ?? CreateDefaultHandler(); + protected virtual HttpMessageHandler CreateHandler() => Handler?.Invoke() ?? ApiClient.CreateDefaultHandler(); /// /// Overridable method used to control creation of a used by the internal HTTP client. @@ -353,7 +356,7 @@ protected virtual async ValueTask BuildRequest(ApiRequest re return requestMessage; } - private HttpMessageHandler CreateDefaultHandler() + private static HttpMessageHandler CreateDefaultHandler() { #if NETSTANDARD2_0 return new HttpClientHandler From c9c27565c6b6af18a7a3da33f3de358c0bd837e7 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 16 Dec 2023 16:58:20 +0000 Subject: [PATCH 139/151] add ApiClient tests --- DragonFruit.Data.Tests/ClientTests.cs | 95 +++++++++++++++++++ .../Requests/DummyFileDownloadRequest.cs | 17 ++++ 2 files changed, 112 insertions(+) create mode 100644 DragonFruit.Data.Tests/ClientTests.cs create mode 100644 DragonFruit.Data.Tests/Requests/DummyFileDownloadRequest.cs diff --git a/DragonFruit.Data.Tests/ClientTests.cs b/DragonFruit.Data.Tests/ClientTests.cs new file mode 100644 index 0000000..c24fab0 --- /dev/null +++ b/DragonFruit.Data.Tests/ClientTests.cs @@ -0,0 +1,95 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System; +using System.IO; +using System.Net; +using System.Runtime.InteropServices; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using DragonFruit.Data.Serializers; +using DragonFruit.Data.Tests.Requests; +using Xunit; + +namespace DragonFruit.Data.Tests +{ + public class ClientTests : IDisposable + { + private readonly Lazy _fileStream = new(() => new FileStream(Path.GetTempFileName(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.Asynchronous | FileOptions.SequentialScan | FileOptions.DeleteOnClose)); + + private readonly ApiClient _client = new ApiClient + { + UserAgent = "DragonFruit.Data.Tests" + }; + + [Fact] + public async Task TestClientVersionDefaults() + { + using var response = await _client.PerformAsync("https://cloudflare-quic.com"); + Assert.Equal(RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? HttpVersion.Version11 : HttpVersion.Version30, response.Version); + } + + [Fact] + public async Task TestUserAgentHeader() + { + var response = await _client.PerformAsync(new BasicEchoRequest()); + Assert.Equal(_client.UserAgent, response["headers"]?["user-agent"]?.GetValue()); + } + + [Fact] + public async Task TestClientDeserialization() + { + var echoResponse = await _client.PerformAsync(new InheritedEchoRequest()); + Assert.Equal("additional_content", echoResponse["form"]?["extra"]?.GetValue()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task TestClientDownload(bool safe) + { + _fileStream.Value.Seek(0, SeekOrigin.Begin); + var downloadResult = await _client.PerformDownload(new DummyFileDownloadRequest("5MB"), _fileStream.Value, safe: safe); + + Assert.Equal(HttpStatusCode.OK, downloadResult); + Assert.Equal(5242880, _fileStream.Value.Length); + } + + [Fact] + public async Task TestClientDownloadTruncation() + { + _fileStream.Value.Seek(0, SeekOrigin.Begin); + await _client.PerformDownload(new DummyFileDownloadRequest("10MB"), _fileStream.Value, truncate: true); + + Assert.Equal(10485760, _fileStream.Value.Length); + + _fileStream.Value.Seek(0, SeekOrigin.Begin); + await _client.PerformDownload(new DummyFileDownloadRequest("5MB"), _fileStream.Value, truncate: true); + + // if truncate worked, the file should have halved in size + Assert.Equal(5242880, _fileStream.Value.Length); + } + + [Fact] + public async Task TestClientDownloadProgressReporting() + { + _fileStream.Value.Seek(0, SeekOrigin.Begin); + + var progressHits = 0d; + var progressHandler = new Progress<(long, long?)>(delegate { progressHits++; }); + + var downloadResult = await _client.PerformDownload(new DummyFileDownloadRequest("5MB"), _fileStream.Value, progressHandler); + + Assert.Equal(HttpStatusCode.OK, downloadResult); + Assert.True(progressHits >= 20); + } + + public void Dispose() + { + if (_fileStream.IsValueCreated) + { + _fileStream.Value.Dispose(); + } + } + } +} diff --git a/DragonFruit.Data.Tests/Requests/DummyFileDownloadRequest.cs b/DragonFruit.Data.Tests/Requests/DummyFileDownloadRequest.cs new file mode 100644 index 0000000..f1a2952 --- /dev/null +++ b/DragonFruit.Data.Tests/Requests/DummyFileDownloadRequest.cs @@ -0,0 +1,17 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +namespace DragonFruit.Data.Tests.Requests +{ + public partial class DummyFileDownloadRequest : ApiRequest + { + public override string RequestPath => $"http://xcal1.vodafone.co.uk/{FileSize}.zip"; + + public DummyFileDownloadRequest(string fileSize) + { + FileSize = fileSize; + } + + public string FileSize { get; set; } + } +} From 0340d4f79835af3f2fb5093a9d76a3649cef3bd1 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 16 Dec 2023 17:24:51 +0000 Subject: [PATCH 140/151] add newtonsoft.json and html serializers --- DragonFruit.Data.sln | 14 +++++ .../DragonFruit.Data.Serializers.Html.csproj | 17 ++++++ .../HtmlSerializer.cs | 45 ++++++++++++++ ...onFruit.Data.Serializers.Newtonsoft.csproj | 15 +++++ .../JsonArrayPool.cs | 29 ++++++++++ .../NewtonsoftJsonSerializer.cs | 58 +++++++++++++++++++ 6 files changed, 178 insertions(+) create mode 100644 serializers/DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj create mode 100644 serializers/DragonFruit.Data.Serializers.Html/HtmlSerializer.cs create mode 100644 serializers/DragonFruit.Data.Serializers.Newtonsoft/DragonFruit.Data.Serializers.Newtonsoft.csproj create mode 100644 serializers/DragonFruit.Data.Serializers.Newtonsoft/JsonArrayPool.cs create mode 100644 serializers/DragonFruit.Data.Serializers.Newtonsoft/NewtonsoftJsonSerializer.cs diff --git a/DragonFruit.Data.sln b/DragonFruit.Data.sln index 94e71ed..682421e 100644 --- a/DragonFruit.Data.sln +++ b/DragonFruit.Data.sln @@ -21,6 +21,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Roslyn.Tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Tests", "DragonFruit.Data.Tests\DragonFruit.Data.Tests.csproj", "{3526B57B-615F-48AC-ABC4-A2A8E5F659AD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Serializers.Newtonsoft", "serializers\DragonFruit.Data.Serializers.Newtonsoft\DragonFruit.Data.Serializers.Newtonsoft.csproj", "{58B033DF-A94A-4C97-A5DF-6AFD2C1E924C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Serializers.Html", "serializers\DragonFruit.Data.Serializers.Html\DragonFruit.Data.Serializers.Html.csproj", "{C2CB8DC2-D583-4612-9B4E-837EFB8BE6C5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,6 +47,14 @@ Global {3526B57B-615F-48AC-ABC4-A2A8E5F659AD}.Debug|Any CPU.Build.0 = Debug|Any CPU {3526B57B-615F-48AC-ABC4-A2A8E5F659AD}.Release|Any CPU.ActiveCfg = Release|Any CPU {3526B57B-615F-48AC-ABC4-A2A8E5F659AD}.Release|Any CPU.Build.0 = Release|Any CPU + {58B033DF-A94A-4C97-A5DF-6AFD2C1E924C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58B033DF-A94A-4C97-A5DF-6AFD2C1E924C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58B033DF-A94A-4C97-A5DF-6AFD2C1E924C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58B033DF-A94A-4C97-A5DF-6AFD2C1E924C}.Release|Any CPU.Build.0 = Release|Any CPU + {C2CB8DC2-D583-4612-9B4E-837EFB8BE6C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2CB8DC2-D583-4612-9B4E-837EFB8BE6C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2CB8DC2-D583-4612-9B4E-837EFB8BE6C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2CB8DC2-D583-4612-9B4E-837EFB8BE6C5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -51,5 +63,7 @@ Global SolutionGuid = {368819C7-3F6F-4B76-BBF1-F581D180EA8F} EndGlobalSection GlobalSection(NestedProjects) = preSolution + {58B033DF-A94A-4C97-A5DF-6AFD2C1E924C} = {5A8982CD-EEF9-4B9F-AF74-C8D45241E137} + {C2CB8DC2-D583-4612-9B4E-837EFB8BE6C5} = {5A8982CD-EEF9-4B9F-AF74-C8D45241E137} EndGlobalSection EndGlobal diff --git a/serializers/DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj b/serializers/DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj new file mode 100644 index 0000000..aa8a23a --- /dev/null +++ b/serializers/DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + diff --git a/serializers/DragonFruit.Data.Serializers.Html/HtmlSerializer.cs b/serializers/DragonFruit.Data.Serializers.Html/HtmlSerializer.cs new file mode 100644 index 0000000..c53e677 --- /dev/null +++ b/serializers/DragonFruit.Data.Serializers.Html/HtmlSerializer.cs @@ -0,0 +1,45 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using HtmlAgilityPack; + +namespace DragonFruit.Data.Serializers.Html +{ + public class HtmlSerializer : ApiSerializer + { + public override string ContentType => "text/html"; + + public override bool IsGeneric => false; + + public override HttpContent Serialize(T input) + { + if (input is not HtmlDocument document) + { + throw new ArgumentException($"Cannot process {input.GetType().Name}", nameof(input)); + } + + var stream = new MemoryStream(); + document.Save(stream, Encoding); + + return GetHttpContent(stream); + } + + public override T Deserialize(Stream input) + { + if (typeof(T) != typeof(HtmlDocument)) + { + throw new ArgumentException($"Cannot process {typeof(T).Name}", nameof(T)); + } + + var document = new HtmlDocument(); + document.Load(input, Encoding); + + return document as T; // T is already validated as a HtmlDocument + } + + /// + /// Registers the to resolve objects + /// + public static void RegisterDefaults() => SerializerResolver.Register(); + } +} diff --git a/serializers/DragonFruit.Data.Serializers.Newtonsoft/DragonFruit.Data.Serializers.Newtonsoft.csproj b/serializers/DragonFruit.Data.Serializers.Newtonsoft/DragonFruit.Data.Serializers.Newtonsoft.csproj new file mode 100644 index 0000000..72ade8f --- /dev/null +++ b/serializers/DragonFruit.Data.Serializers.Newtonsoft/DragonFruit.Data.Serializers.Newtonsoft.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + + + + + + + + + + + diff --git a/serializers/DragonFruit.Data.Serializers.Newtonsoft/JsonArrayPool.cs b/serializers/DragonFruit.Data.Serializers.Newtonsoft/JsonArrayPool.cs new file mode 100644 index 0000000..ccb4e7e --- /dev/null +++ b/serializers/DragonFruit.Data.Serializers.Newtonsoft/JsonArrayPool.cs @@ -0,0 +1,29 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System.Buffers; +using Newtonsoft.Json; + +namespace DragonFruit.Data.Serializers.Newtonsoft +{ + /// + /// A wrapper for the that implements + /// + /// + /// Taken from https://github.com/JamesNK/Newtonsoft.Json/blob/52e257ee57899296d81a868b32300f0b3cfeacbe/Src/Newtonsoft.Json.Tests/DemoTests.cs#L709 + /// + internal class JsonArrayPool : IArrayPool + { + public static readonly JsonArrayPool Instance = new JsonArrayPool(); + + public char[] Rent(int minimumLength) + { + return ArrayPool.Shared.Rent(minimumLength); + } + + public void Return(char[] array) + { + ArrayPool.Shared.Return(array); + } + } +} diff --git a/serializers/DragonFruit.Data.Serializers.Newtonsoft/NewtonsoftJsonSerializer.cs b/serializers/DragonFruit.Data.Serializers.Newtonsoft/NewtonsoftJsonSerializer.cs new file mode 100644 index 0000000..73e6efe --- /dev/null +++ b/serializers/DragonFruit.Data.Serializers.Newtonsoft/NewtonsoftJsonSerializer.cs @@ -0,0 +1,58 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System.Globalization; +using System.IO; +using System.Net.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace DragonFruit.Data.Serializers.Newtonsoft +{ + public class NewtonsoftJsonSerializer : ApiSerializer + { + private JsonSerializer _serializer; + + public override string ContentType => "application/json"; + + public JsonSerializer Serializer + { + get => _serializer ??= new JsonSerializer { Culture = CultureInfo.InvariantCulture }; + set => _serializer = value; + } + + public override HttpContent Serialize(T input) + { + var stream = new MemoryStream(); + + // these must dispose before processing the stream, as we need any/all buffers flushed + using (var streamWriter = new StreamWriter(stream, Encoding, 4096, true)) + using (var jsonWriter = new JsonTextWriter(streamWriter)) + { + jsonWriter.ArrayPool = JsonArrayPool.Instance; + Serializer.Serialize(jsonWriter, input); + } + + return GetHttpContent(stream); + } + + public override T Deserialize(Stream input) where T : class + { + using var streamReader = new StreamReader(input, Encoding, true, 4096); + using var reader = new JsonTextReader(streamReader); + + reader.ArrayPool = JsonArrayPool.Instance; + return Serializer.Deserialize(reader); + } + + /// + /// Registers Newtonsoft.Json Linq objects to be resolved by this serializer + /// + public static void RegisterDefaults() + { + SerializerResolver.Register(); + SerializerResolver.Register(); + SerializerResolver.Register(); + } + } +} From f2a7f6da1e9032b6b39eba6063f03ee264aced77 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 16 Dec 2023 17:37:04 +0000 Subject: [PATCH 141/151] update props --- res/DragonFruit.Data.Nuget.props | 26 ++++++++++++++------------ res/DragonFruit.Data.Serializers.props | 6 +++--- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/res/DragonFruit.Data.Nuget.props b/res/DragonFruit.Data.Nuget.props index 304f7af..3c3c1d1 100644 --- a/res/DragonFruit.Data.Nuget.props +++ b/res/DragonFruit.Data.Nuget.props @@ -1,24 +1,26 @@ + + + DragonFruit Data + DragonFruit Network + Copyright 2023 (C) DragonFruit Network + - + true snupkg - - - - DragonFruit Data + icon.png - git - DragonFruit Network MIT - Copyright 2023 (C) DragonFruit Network - file, api, web, io, framework, dragonfruit, common - https://github.com/dragonfruitnetwork/rest-client + api, rest, http, sourcegen, client, httpclient https://github.com/dragonfruitnetwork/rest-client - + git + https://github.com/dragonfruitnetwork/rest-client + + - + \ No newline at end of file diff --git a/res/DragonFruit.Data.Serializers.props b/res/DragonFruit.Data.Serializers.props index 97400b3..07cb35b 100644 --- a/res/DragonFruit.Data.Serializers.props +++ b/res/DragonFruit.Data.Serializers.props @@ -1,15 +1,15 @@ - 8 + latest netstandard2.0;net6.0 - + - + \ No newline at end of file From 971709b41cc0310d6cda3a8d661450748c7eb3b4 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 16 Dec 2023 17:37:19 +0000 Subject: [PATCH 142/151] move serializers around --- .../DragonFruit.Data.Serializers.Html.csproj | 23 ++++++++++++++++++ .../HtmlSerializer.cs | 3 +++ ...onFruit.Data.Serializers.Newtonsoft.csproj | 23 ++++++++++++++++++ .../JsonArrayPool.cs | 0 .../NewtonsoftJsonSerializer.cs | 0 DragonFruit.Data.sln | 24 +++++++++---------- .../DragonFruit.Data.Serializers.Html.csproj | 17 ------------- ...onFruit.Data.Serializers.Newtonsoft.csproj | 15 ------------ 8 files changed, 61 insertions(+), 44 deletions(-) create mode 100644 DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj rename {serializers/DragonFruit.Data.Serializers.Html => DragonFruit.Data.Serializers.Html}/HtmlSerializer.cs (96%) create mode 100644 DragonFruit.Data.Serializers.Newtonsoft/DragonFruit.Data.Serializers.Newtonsoft.csproj rename {serializers/DragonFruit.Data.Serializers.Newtonsoft => DragonFruit.Data.Serializers.Newtonsoft}/JsonArrayPool.cs (100%) rename {serializers/DragonFruit.Data.Serializers.Newtonsoft => DragonFruit.Data.Serializers.Newtonsoft}/NewtonsoftJsonSerializer.cs (100%) delete mode 100644 serializers/DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj delete mode 100644 serializers/DragonFruit.Data.Serializers.Newtonsoft/DragonFruit.Data.Serializers.Newtonsoft.csproj diff --git a/DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj b/DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj new file mode 100644 index 0000000..a1c0c5b --- /dev/null +++ b/DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj @@ -0,0 +1,23 @@ + + + + latest + netstandard2.0;net6.0 + + + + DragonFruit.Data.Serializers.Html + HtmlDocument processing support for DragonFruit.Data + + + + + + + + + + + + + diff --git a/serializers/DragonFruit.Data.Serializers.Html/HtmlSerializer.cs b/DragonFruit.Data.Serializers.Html/HtmlSerializer.cs similarity index 96% rename from serializers/DragonFruit.Data.Serializers.Html/HtmlSerializer.cs rename to DragonFruit.Data.Serializers.Html/HtmlSerializer.cs index c53e677..c92bf7e 100644 --- a/serializers/DragonFruit.Data.Serializers.Html/HtmlSerializer.cs +++ b/DragonFruit.Data.Serializers.Html/HtmlSerializer.cs @@ -1,6 +1,9 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details +using System; +using System.IO; +using System.Net.Http; using HtmlAgilityPack; namespace DragonFruit.Data.Serializers.Html diff --git a/DragonFruit.Data.Serializers.Newtonsoft/DragonFruit.Data.Serializers.Newtonsoft.csproj b/DragonFruit.Data.Serializers.Newtonsoft/DragonFruit.Data.Serializers.Newtonsoft.csproj new file mode 100644 index 0000000..c9217bb --- /dev/null +++ b/DragonFruit.Data.Serializers.Newtonsoft/DragonFruit.Data.Serializers.Newtonsoft.csproj @@ -0,0 +1,23 @@ + + + + latest + netstandard2.0;net6.0 + + + + DragonFruit.Data.Serializers.Newtonsoft + Newtonsoft.Json serializer support for DragonFruit.Data + + + + + + + + + + + + + diff --git a/serializers/DragonFruit.Data.Serializers.Newtonsoft/JsonArrayPool.cs b/DragonFruit.Data.Serializers.Newtonsoft/JsonArrayPool.cs similarity index 100% rename from serializers/DragonFruit.Data.Serializers.Newtonsoft/JsonArrayPool.cs rename to DragonFruit.Data.Serializers.Newtonsoft/JsonArrayPool.cs diff --git a/serializers/DragonFruit.Data.Serializers.Newtonsoft/NewtonsoftJsonSerializer.cs b/DragonFruit.Data.Serializers.Newtonsoft/NewtonsoftJsonSerializer.cs similarity index 100% rename from serializers/DragonFruit.Data.Serializers.Newtonsoft/NewtonsoftJsonSerializer.cs rename to DragonFruit.Data.Serializers.Newtonsoft/NewtonsoftJsonSerializer.cs diff --git a/DragonFruit.Data.sln b/DragonFruit.Data.sln index 682421e..29cd4e7 100644 --- a/DragonFruit.Data.sln +++ b/DragonFruit.Data.sln @@ -21,9 +21,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Roslyn.Tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Tests", "DragonFruit.Data.Tests\DragonFruit.Data.Tests.csproj", "{3526B57B-615F-48AC-ABC4-A2A8E5F659AD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Serializers.Newtonsoft", "serializers\DragonFruit.Data.Serializers.Newtonsoft\DragonFruit.Data.Serializers.Newtonsoft.csproj", "{58B033DF-A94A-4C97-A5DF-6AFD2C1E924C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Serializers.Html", "DragonFruit.Data.Serializers.Html\DragonFruit.Data.Serializers.Html.csproj", "{DDC1C284-917D-487D-BB7D-D0A2F85F3D93}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Serializers.Html", "serializers\DragonFruit.Data.Serializers.Html\DragonFruit.Data.Serializers.Html.csproj", "{C2CB8DC2-D583-4612-9B4E-837EFB8BE6C5}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DragonFruit.Data.Serializers.Newtonsoft", "DragonFruit.Data.Serializers.Newtonsoft\DragonFruit.Data.Serializers.Newtonsoft.csproj", "{BA2CC215-DE9C-40BD-9FAF-2090EFB647A1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -47,14 +47,14 @@ Global {3526B57B-615F-48AC-ABC4-A2A8E5F659AD}.Debug|Any CPU.Build.0 = Debug|Any CPU {3526B57B-615F-48AC-ABC4-A2A8E5F659AD}.Release|Any CPU.ActiveCfg = Release|Any CPU {3526B57B-615F-48AC-ABC4-A2A8E5F659AD}.Release|Any CPU.Build.0 = Release|Any CPU - {58B033DF-A94A-4C97-A5DF-6AFD2C1E924C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {58B033DF-A94A-4C97-A5DF-6AFD2C1E924C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {58B033DF-A94A-4C97-A5DF-6AFD2C1E924C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {58B033DF-A94A-4C97-A5DF-6AFD2C1E924C}.Release|Any CPU.Build.0 = Release|Any CPU - {C2CB8DC2-D583-4612-9B4E-837EFB8BE6C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C2CB8DC2-D583-4612-9B4E-837EFB8BE6C5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C2CB8DC2-D583-4612-9B4E-837EFB8BE6C5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C2CB8DC2-D583-4612-9B4E-837EFB8BE6C5}.Release|Any CPU.Build.0 = Release|Any CPU + {DDC1C284-917D-487D-BB7D-D0A2F85F3D93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DDC1C284-917D-487D-BB7D-D0A2F85F3D93}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DDC1C284-917D-487D-BB7D-D0A2F85F3D93}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DDC1C284-917D-487D-BB7D-D0A2F85F3D93}.Release|Any CPU.Build.0 = Release|Any CPU + {BA2CC215-DE9C-40BD-9FAF-2090EFB647A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA2CC215-DE9C-40BD-9FAF-2090EFB647A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA2CC215-DE9C-40BD-9FAF-2090EFB647A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA2CC215-DE9C-40BD-9FAF-2090EFB647A1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -63,7 +63,7 @@ Global SolutionGuid = {368819C7-3F6F-4B76-BBF1-F581D180EA8F} EndGlobalSection GlobalSection(NestedProjects) = preSolution - {58B033DF-A94A-4C97-A5DF-6AFD2C1E924C} = {5A8982CD-EEF9-4B9F-AF74-C8D45241E137} - {C2CB8DC2-D583-4612-9B4E-837EFB8BE6C5} = {5A8982CD-EEF9-4B9F-AF74-C8D45241E137} + {DDC1C284-917D-487D-BB7D-D0A2F85F3D93} = {5A8982CD-EEF9-4B9F-AF74-C8D45241E137} + {BA2CC215-DE9C-40BD-9FAF-2090EFB647A1} = {5A8982CD-EEF9-4B9F-AF74-C8D45241E137} EndGlobalSection EndGlobal diff --git a/serializers/DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj b/serializers/DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj deleted file mode 100644 index aa8a23a..0000000 --- a/serializers/DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - net6.0 - enable - enable - - - - - - - - - - - diff --git a/serializers/DragonFruit.Data.Serializers.Newtonsoft/DragonFruit.Data.Serializers.Newtonsoft.csproj b/serializers/DragonFruit.Data.Serializers.Newtonsoft/DragonFruit.Data.Serializers.Newtonsoft.csproj deleted file mode 100644 index 72ade8f..0000000 --- a/serializers/DragonFruit.Data.Serializers.Newtonsoft/DragonFruit.Data.Serializers.Newtonsoft.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net6.0 - - - - - - - - - - - From 815f4bdcc19f2b5e332fd8f995a0559632c537ea Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 16 Dec 2023 17:37:36 +0000 Subject: [PATCH 143/151] split libraries by target platform, update descriptions --- DragonFruit.Data/DragonFruit.Data.csproj | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/DragonFruit.Data/DragonFruit.Data.csproj b/DragonFruit.Data/DragonFruit.Data.csproj index 1bdaa07..f349e9e 100644 --- a/DragonFruit.Data/DragonFruit.Data.csproj +++ b/DragonFruit.Data/DragonFruit.Data.csproj @@ -1,19 +1,21 @@  + latest netstandard2.0;net6.0 - 8 DragonFruit.Data - A http/rest request framework for .NET that powers the DragonFruit APIs + A lightweight, extensible HTTP/REST framework for .NET - + - - + + + + From 8c6cb7f91adc786dee6d5dcdcda75b1fba2ab2b9 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 16 Dec 2023 17:38:35 +0000 Subject: [PATCH 144/151] consolidate typed apiclient declaration --- DragonFruit.Data/ApiClient.cs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/DragonFruit.Data/ApiClient.cs b/DragonFruit.Data/ApiClient.cs index f5e2906..ba42243 100644 --- a/DragonFruit.Data/ApiClient.cs +++ b/DragonFruit.Data/ApiClient.cs @@ -15,6 +15,12 @@ namespace DragonFruit.Data { + /// + /// Represents a strongly-typed serializer version of + /// + /// The type of the + public class ApiClient() : ApiClient(Activator.CreateInstance()) where T : ApiSerializer, new(); + /// /// The responsible for building, submitting and processing HTTP requests /// @@ -374,16 +380,4 @@ private static HttpMessageHandler CreateDefaultHandler() #endif } } - - /// - /// Represents a strongly-typed serializer version of - /// - /// The type of the - public class ApiClient : ApiClient where T : ApiSerializer, new() - { - public ApiClient() - : base(Activator.CreateInstance()) - { - } - } } From 3bd421d5581a86c5f55ef1990df207c4c217407b Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 16 Dec 2023 17:39:11 +0000 Subject: [PATCH 145/151] make default httpclient constructor public --- DragonFruit.Data/ApiClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DragonFruit.Data/ApiClient.cs b/DragonFruit.Data/ApiClient.cs index ba42243..9283ea1 100644 --- a/DragonFruit.Data/ApiClient.cs +++ b/DragonFruit.Data/ApiClient.cs @@ -362,7 +362,7 @@ protected virtual async ValueTask BuildRequest(ApiRequest re return requestMessage; } - private static HttpMessageHandler CreateDefaultHandler() + public static HttpMessageHandler CreateDefaultHandler() { #if NETSTANDARD2_0 return new HttpClientHandler From 3dd096e1febe28ee1ba059db5a3b85250b9bd09f Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 16 Dec 2023 17:47:37 +0000 Subject: [PATCH 146/151] consolidate roslyn project file --- .../DragonFruit.Data.Roslyn.csproj | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj index 1413033..004ac3e 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj @@ -1,7 +1,6 @@ - false latest false netstandard2.0 @@ -13,15 +12,27 @@ false + + DragonFruit.Data.Roslyn + Source generation and analysis tools for DragonFruit.Data + + + - - - + + + + + + + - + + + @@ -29,18 +40,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - - - - - - - - + From 5b4cdfde7ae714fc05cfa3e5ef9c2558e0e612aa Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 16 Dec 2023 18:04:32 +0000 Subject: [PATCH 147/151] update workflows --- .github/workflows/codequality.yaml | 44 +++++++++++++----------------- .github/workflows/publish.yaml | 27 ++++++++---------- .github/workflows/unit-tests.yaml | 25 ++++++----------- 3 files changed, 38 insertions(+), 58 deletions(-) diff --git a/.github/workflows/codequality.yaml b/.github/workflows/codequality.yaml index 475ece9..e9504e4 100644 --- a/.github/workflows/codequality.yaml +++ b/.github/workflows/codequality.yaml @@ -6,36 +6,30 @@ on: push: branches: master +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: false + jobs: quality: runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Install .NET 3.1.x LTS - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "3.1.x" - - name: Install .NET 5.0.x - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "5.0.x" - - - name: Install .NET 6.0.x - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "6.0.x" + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 8.0.x - - name: Restore Tools - run: dotnet tool restore + - name: Restore Tools + run: dotnet tool restore - - name: Restore Packages - run: dotnet restore + - name: Restore Packages + run: dotnet restore - - name: InspectCode - run: dotnet jb inspectcode ${{github.workspace}}/DragonFruit.Data.sln --output=${{github.workspace}}/inspectcodereport.xml --cachesDir=${{github.workspace}}/inspectcode --verbosity=WARN --no-build + - name: InspectCode + run: dotnet jb inspectcode DragonFruit.Data.sln --output=inspectcodereport.xml --cachesDir=inspectcode --verbosity=WARN --no-build - - name: NVika - run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" + - name: NVika + run: dotnet nvika parsereport inspectcodereport.xml diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index e170c67..c1b360f 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -5,33 +5,28 @@ on: types: [ published ] jobs: - deploy: - runs-on: windows-latest - - steps: - - name: Checkout - uses: actions/checkout@v3 + publish: + runs-on: ubuntu-latest - - name: Install .NET - uses: actions/setup-dotnet@v2 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: "6.0.x" - name: Restore - run: | - dotnet workload install android - dotnet restore + run: dotnet restore - name: Build - run: dotnet build -c Release -v normal -p:EnableAndroid=true -p:Version=${{ github.ref_name }} + run: dotnet build -c Release -v normal -p:Version=${{ github.event.release.tag_name }} - name: Pack (Beta) - run: dotnet pack -c Release --include-symbols --no-build -v normal -o . -p:EnableAndroid=true -p:PackageVersion=${{ github.ref_name }}-beta + run: dotnet pack -c Release --include-symbols --no-build -v normal -o . -p:PackageVersion=${{ github.event.release.tag_name }}-beta if: "github.event.release.prerelease" - name: Pack (Stable) - run: dotnet pack -c Release --include-symbols --no-build -v normal -o . -p:EnableAndroid=true -p:PackageVersion=${{ github.ref_name }} + run: dotnet pack -c Release --include-symbols --no-build -v normal -o . -p:PackageVersion=${{ github.event.release.tag_name }} if: "!github.event.release.prerelease" - name: Publish - run: dotnet nuget push "*.nupkg" -k ${{ secrets.NUGET_KEY }} --skip-duplicate -s https://api.nuget.org/v3/index.json + run: dotnet nuget push "*.nupkg" -k ${{ secrets.NUGET_KEY }} --skip-duplicate -s https://api.nuget.org/v3/index.json \ No newline at end of file diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index a43e6ed..94cdb4d 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -8,30 +8,21 @@ env: DOTNET_NOLOGO: true DOTNET_CLI_TELEMETRY_OPTOUT: false - testproject: './DragonFruit.Common.Data.Tests/DragonFruit.Common.Data.Tests.csproj' - jobs: test: runs-on: ubuntu-latest - strategy: - matrix: - include: - - dotnet: '6.0.x' - dotnet-tfm: 'net6.0' - dotnet-test-tfm: 'net6.0' - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Install .NET - uses: actions/setup-dotnet@v1 + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 with: - dotnet-version: ${{ matrix.dotnet }} + dotnet-version: 8.0.x - name: Build - run: dotnet build -c Debug + run: dotnet build -c Release + + - name: Unit Tests (Roslyn) + run: dotnet test -f net8.0 -c Release --no-build --no-restore --verbosity normal DragonFruit.Data.Roslyn.Tests - name: Unit Tests - run: dotnet test -f ${{ matrix.dotnet-test-tfm }} \ No newline at end of file + run: dotnet test -f net8.0 -c Release --no-build --no-restore --verbosity normal DragonFruit.Data.Tests From 43d5635b956740419e5ec95d8211d206f16d2c29 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 16 Dec 2023 18:04:41 +0000 Subject: [PATCH 148/151] update dotnet tools to new versions --- .config/dotnet-tools.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 589a42a..1e2b3b3 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,13 +3,13 @@ "isRoot": true, "tools": { "jetbrains.resharper.globaltools": { - "version": "2021.2.2", + "version": "2023.3.1", "commands": [ "jb" ] }, "nvika": { - "version": "2.2.0", + "version": "3.0.0", "commands": [ "nvika" ] From 091d473a70d6ababdf9c8c14f34cd1dc672724ff Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 16 Dec 2023 18:37:44 +0000 Subject: [PATCH 149/151] cleanup redundant class name --- DragonFruit.Data/ApiClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DragonFruit.Data/ApiClient.cs b/DragonFruit.Data/ApiClient.cs index 9283ea1..74d3cbc 100644 --- a/DragonFruit.Data/ApiClient.cs +++ b/DragonFruit.Data/ApiClient.cs @@ -294,7 +294,7 @@ void UpdateProgress() /// /// This is designed to be used by libraries requiring overall control of handlers (i.e. wrap the user-selected handler to provide additional functionality) /// - protected virtual HttpMessageHandler CreateHandler() => Handler?.Invoke() ?? ApiClient.CreateDefaultHandler(); + protected virtual HttpMessageHandler CreateHandler() => Handler?.Invoke() ?? CreateDefaultHandler(); /// /// Overridable method used to control creation of a used by the internal HTTP client. @@ -367,7 +367,7 @@ public static HttpMessageHandler CreateDefaultHandler() #if NETSTANDARD2_0 return new HttpClientHandler { - UseCookies = true, + UseCookies = false, AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip }; #else From 637b59f2c634afc3a115980ed3844d971f598412 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 16 Dec 2023 18:37:56 +0000 Subject: [PATCH 150/151] install msquic for http/3 test --- .github/workflows/unit-tests.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index 94cdb4d..d415af0 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -10,13 +10,20 @@ env: jobs: test: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x + + - name: Install libmsquic + run: | + wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb + sudo dpkg -i packages-microsoft-prod.deb + sudo apt-get update + sudo apt-get install libmsquic - name: Build run: dotnet build -c Release From 4ed82c66e9d690db1684862ecab98e4a6940638c Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 16 Dec 2023 18:40:19 +0000 Subject: [PATCH 151/151] remove http/3 test (can't repo on ci) --- .github/workflows/unit-tests.yaml | 9 +-------- DragonFruit.Data.Tests/ClientTests.cs | 8 -------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index d415af0..94cdb4d 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -10,20 +10,13 @@ env: jobs: test: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x - - - name: Install libmsquic - run: | - wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb - sudo dpkg -i packages-microsoft-prod.deb - sudo apt-get update - sudo apt-get install libmsquic - name: Build run: dotnet build -c Release diff --git a/DragonFruit.Data.Tests/ClientTests.cs b/DragonFruit.Data.Tests/ClientTests.cs index c24fab0..8074daf 100644 --- a/DragonFruit.Data.Tests/ClientTests.cs +++ b/DragonFruit.Data.Tests/ClientTests.cs @@ -4,7 +4,6 @@ using System; using System.IO; using System.Net; -using System.Runtime.InteropServices; using System.Text.Json.Nodes; using System.Threading.Tasks; using DragonFruit.Data.Serializers; @@ -22,13 +21,6 @@ public class ClientTests : IDisposable UserAgent = "DragonFruit.Data.Tests" }; - [Fact] - public async Task TestClientVersionDefaults() - { - using var response = await _client.PerformAsync("https://cloudflare-quic.com"); - Assert.Equal(RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? HttpVersion.Version11 : HttpVersion.Version30, response.Version); - } - [Fact] public async Task TestUserAgentHeader() {