├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug-template.md │ └── config.yml └── pull-request-template.md ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── build.csx ├── icon.png ├── icon.svg └── src ├── .editorconfig ├── Common ├── AllowAnyStatusCodeAttribute.cs ├── BaseAddressAttribute.cs ├── BasePathAttribute.cs ├── BodyAttribute.cs ├── BodySerializationMethod.cs ├── HeaderAttribute.cs ├── HttpRequestMessagePropertyAttribute.cs ├── Implementation │ ├── Analysis │ │ ├── AttributeModel.Reflection.cs │ │ ├── AttributeModel.Roslyn.cs │ │ ├── AttributeModel.cs │ │ ├── DiagnosticModel.Reflection.cs │ │ ├── DiagnosticModel.cs │ │ ├── EventModel.Reflection.cs │ │ ├── EventModel.Roslyn.cs │ │ ├── EventModel.cs │ │ ├── MethodModel.Reflection.cs │ │ ├── MethodModel.Roslyn.cs │ │ ├── MethodModel.cs │ │ ├── MethodSignatureEqualityComparer.Reflection.cs │ │ ├── MethodSignatureEqualityComparer.Roslyn.cs │ │ ├── MethodSignatureEqualityComparer.cs │ │ ├── ParameterModel.Reflection.cs │ │ ├── ParameterModel.Roslyn.cs │ │ ├── ParameterModel.cs │ │ ├── PropertyModel.Reflection.cs │ │ ├── PropertyModel.Roslyn.cs │ │ ├── PropertyModel.cs │ │ ├── TypeModel.Reflection.cs │ │ ├── TypeModel.Roslyn.cs │ │ └── TypeModel.cs │ ├── DiagnosticCode.cs │ ├── Emission │ │ ├── DiagnosticReporter.Emit.cs │ │ ├── DiagnosticReporter.Roslyn.cs │ │ ├── EmittedProperty.Emit.cs │ │ ├── EmittedProperty.Roslyn.cs │ │ ├── EmittedProperty.cs │ │ ├── EmittedType.Emit.cs │ │ ├── EmittedType.Roslyn.cs │ │ ├── Emitter.Emit.cs │ │ ├── Emitter.Roslyn.cs │ │ ├── MethodEmitter.Emit.cs │ │ ├── MethodEmitter.Roslyn.cs │ │ ├── TypeEmitter.Emit.cs │ │ └── TypeEmitter.Roslyn.cs │ ├── ImplementationGenerator.cs │ └── ResolvedSerializationMethods.cs ├── NullableAttributes.cs ├── PathAttribute.cs ├── PathSerializationMethod.cs ├── QueryAttribute.cs ├── QueryMapAttribute.cs ├── QuerySerialializationMethod.cs ├── RawQueryStringAttribute.cs ├── RequestAttribute.cs └── SerializationMethodsAttribute.cs ├── RestEase.HttpClientFactory.UnitTests ├── HttpClientFactoryExtensionsTests.cs └── RestEase.HttpClientFactory.UnitTests.csproj ├── RestEase.HttpClientFactory ├── CompatibilitySuppressions.xml ├── HttpClientFactoryExtensions.Obsolete.cs ├── HttpClientFactoryExtensions.cs ├── Options.cs ├── RestEase.HttpClientFactory.csproj └── RestEase.HttpClientFactory.nuspec ├── RestEase.SourceGenerator.UnitTests ├── ImplementationFactoryTests │ └── Helpers │ │ └── DiagnosticVerifier.cs └── RestEase.SourceGenerator.UnitTests.csproj ├── RestEase.SourceGenerator ├── Implementation │ ├── AllowedRestEaseVersionRangeAttribute.cs │ ├── AttributeInstantiator.cs │ ├── Processor.cs │ ├── RoslynEmitUtils.cs │ ├── RoslynImplementationFactory.cs │ ├── RoslynTypeAnalyzer.cs │ ├── SymbolDisplayFormats.cs │ ├── SyntaxReceiver.cs │ ├── WellKnownNames.cs │ └── WellKnownSymbols.cs ├── RestEase.SourceGenerator.csproj └── RestEaseSourceGenerator.cs ├── RestEase.UnitTests ├── Extensions │ └── TypeExtensions.cs ├── ImplementationFactoryTests │ ├── AllowAnyStatusCodeTests.cs │ ├── BaseAddressTests.cs │ ├── BasePathTests.cs │ ├── BodyTests.cs │ ├── CancellationTokenTests.cs │ ├── DefaultValueTests.cs │ ├── DisposableTests.cs │ ├── EdgeCaseTests.cs │ ├── GenericsTests.cs │ ├── HeaderTests.cs │ ├── Helpers │ │ └── DiagnosticResult.cs │ ├── HttpRequestMessagePropertyTests.cs │ ├── ImplementationFactoryTestsBase.cs │ ├── InterfaceInheritanceTests.cs │ ├── KeywordEscapeTests.cs │ ├── MethodInfoTests.cs │ ├── MultipleParameterAttributesTests.cs │ ├── PathParamTests.cs │ ├── QueryMapTests.cs │ ├── QueryParamTests.cs │ ├── RawQueryStringTests.cs │ ├── RequesterIntegrationTests.cs │ ├── RequesterPropertyTests.cs │ ├── SanityCheckTests.cs │ └── ThreadSafetyTests.cs ├── RequesterTests │ ├── ContentConstructionTests.cs │ ├── DictionaryIteratorTests.cs │ ├── HeadersTests.cs │ ├── PathParameterTests.cs │ ├── PublicRequester.cs │ ├── QueryParameterTests.cs │ ├── SendRequestTests.cs │ └── UriConstructionTests.cs ├── RestClientTests.cs ├── RestEase.UnitTests.csproj └── StringEnumRequestPathParamSerializerTests.cs ├── RestEase.sln ├── RestEase ├── ApiException.cs ├── ApiExceptionContentDeserializer.cs ├── CompatibilitySuppressions.xml ├── IRequestBodySerializer.cs ├── IRequestInfo.cs ├── IRequestQueryParamSerializer.cs ├── IRequester.cs ├── IResponseDeserializer.cs ├── Implementation │ ├── BodyParameterInfo.cs │ ├── DictionaryIterator.cs │ ├── EmitEmitUtils.cs │ ├── EmitImplementationFactory.cs │ ├── EnumerableExtensions.cs │ ├── HeaderParameterInfo.cs │ ├── HttpRequestMessagePropertyInfo.cs │ ├── ImplementationCreationException.cs │ ├── ImplementationFactory.cs │ ├── ImplementationHelpers.cs │ ├── MethodInfos.cs │ ├── ModifyingClientHttpHandler.cs │ ├── PathParameterInfo.cs │ ├── QueryParameterInfo.cs │ ├── RawQueryParameterInfo.cs │ ├── ReflectionTypeAnalyzer.cs │ ├── RequestInfo.cs │ ├── Requester.cs │ ├── RestEaseInterfaceImplementationAttribute.cs │ └── ToStringHelper.cs ├── JsonRequestBodySerializer.cs ├── JsonRequestQueryParamSerializer.cs ├── JsonResponseDeserializer.cs ├── Platform │ ├── ArrayUtil.cs │ └── TypeHelpers.cs ├── QueryStringBuilder.cs ├── QueryStringBuilderInfo.cs ├── RequestBodySerializer.cs ├── RequestBodySerializerInfo.cs ├── RequestModifier.cs ├── RequestPathParamSerializer.cs ├── RequestPathParamSerializerInfo.cs ├── RequestQueryParamSerializer.cs ├── RequestQueryParamSerializerInfo.cs ├── Response.cs ├── ResponseDeserializer.cs ├── ResponseDeserializerInfo.cs ├── RestClient.cs ├── RestEase.csproj └── StringEnumRequestPathParamSerializer.cs └── SourceGeneratorSandbox ├── Program.cs └── SourceGeneratorSandbox.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report a bug 3 | about: If you've definitely found something wrong, use this. Not sure? Open a discussion. 4 | --- 5 | 6 | **Description** 7 | A clear and concise description of what the bug is. Use screenshots as necessary. 8 | 9 | **To Reproduce** 10 | Code to reproduce the bug, which someone else can run. 11 | 12 | **Version Info** 13 | - RestEase version: [e.g. 1.2.3] 14 | - Target framework version: [e.g. net5.0] 15 | 16 | **Additional Info** 17 | Add any other context about the problem here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Open a Discussion 4 | url: https://github.com/canton7/restease/discussions/new 5 | about: If you've got a question, suggestion, or something you're not sure about, please open a discussion. -------------------------------------------------------------------------------- /.github/pull-request-template.md: -------------------------------------------------------------------------------- 1 | **Checklist** 2 | 3 | Thanks for contributing! Before we start, there are a few things we need to check: 4 | 5 | 1. This Pull Request has a corresponding Issue. 6 | 2. You've discussed your intention to work on this feature/bug fix. 7 | 3. This feature branch is based on develop (**not** master). The bar above should say "base: develop". 8 | 9 | Thanks! -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2022 Antony Male 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /build.csx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet-script 2 | 3 | #r "nuget: SimpleTasks, 0.9.4" 4 | 5 | using SimpleTasks; 6 | using static SimpleTasks.SimpleTask; 7 | 8 | #nullable enable 9 | 10 | string restEaseDir = "src/RestEase"; 11 | string httpClientFactoryDir = "src/RestEase.HttpClientFactory"; 12 | string sourceGeneratorDir = "src/RestEase.SourceGenerator"; 13 | 14 | string testsDir = "src/RestEase.UnitTests"; 15 | string httpClientFactoryTestsDir = "src/RestEase.HttpClientFactory.UnitTests"; 16 | string sourceGeneratorTestsDir = "src/RestEase.SourceGenerator.UnitTests"; 17 | 18 | CreateTask("build").Run((string versionOpt, string configurationOpt, bool updateCompatSuppression) => 19 | { 20 | // We can't have separate build and package steps due to https://github.com/dotnet/sdk/issues/24943 21 | // We can't run package validation on every build, as it needs a version higher than the baseline (so not 0.0.0) 22 | // We therefore package on every build (true), and only turn on package validation when we 23 | // specify a version. 24 | string flags = $"--configuration={configurationOpt ?? "Release"} -p:VersionPrefix=\"{versionOpt ?? "0.0.0"}\""; 25 | 26 | string validationFlags = ""; 27 | if (versionOpt != null) 28 | { 29 | flags += " -p:GeneratePackageOnBuild=true"; 30 | validationFlags = "-p:EnablePackageValidation=true"; 31 | if (updateCompatSuppression) 32 | { 33 | validationFlags += " -p:GenerateCompatibilitySuppressionFile=true"; 34 | } 35 | } 36 | else if (updateCompatSuppression) 37 | { 38 | throw new Exception("--updateCompatSuppression requires --version"); 39 | } 40 | 41 | Command.Run("dotnet", $"build {flags} {validationFlags} \"{restEaseDir}\""); 42 | Command.Run("dotnet", $"build {flags} {validationFlags} \"{httpClientFactoryDir}\""); 43 | Command.Run("dotnet", $"build {flags} \"{sourceGeneratorDir}\""); 44 | }); 45 | 46 | CreateTask("test").Run(() => 47 | { 48 | Command.Run("dotnet", $"test \"{testsDir}\""); 49 | Command.Run("dotnet", $"test \"{httpClientFactoryTestsDir}\""); 50 | Command.Run("dotnet", $"test \"{sourceGeneratorTestsDir}\""); 51 | }); 52 | 53 | return InvokeTask(Args); 54 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canton7/RestEase/00dfdfba00e12811b31f8063f742af4acaf41fda/icon.png -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /src/Common/AllowAnyStatusCodeAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase 4 | { 5 | /// 6 | /// Controls whether the given method, or all methods within the given interface, will throw an exception if the response status code does not indicate success 7 | /// 8 | [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Method, Inherited = true, AllowMultiple = false)] 9 | public sealed class AllowAnyStatusCodeAttribute : Attribute 10 | { 11 | /// 12 | /// Gets or sets a value indicating whether to suppress the exception normally thrown on responses that do not indicate success 13 | /// 14 | public bool AllowAnyStatusCode { get; set; } 15 | 16 | /// 17 | /// Initialises a new instance of the class, which does allow any status code 18 | /// 19 | public AllowAnyStatusCodeAttribute() 20 | : this(true) 21 | { 22 | } 23 | 24 | /// 25 | /// Initialises a new instance of the classe whi 26 | /// 27 | /// True to allow any response status code; False to throw an exception on response status codes that do not indicate success 28 | public AllowAnyStatusCodeAttribute(bool allowAnyStatusCode) 29 | { 30 | this.AllowAnyStatusCode = allowAnyStatusCode; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Common/BaseAddressAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase 4 | { 5 | /// 6 | /// Attribute applied to the interface, giving a base address which is used if is null 7 | /// 8 | [AttributeUsage(AttributeTargets.Interface, Inherited = true, AllowMultiple = false)] 9 | public sealed class BaseAddressAttribute : Attribute 10 | { 11 | /// 12 | /// Gets the base address set in this attribute 13 | /// 14 | public string BaseAddress { get; } 15 | 16 | /// 17 | /// Initialises a new instance of the class with the given base address 18 | /// 19 | /// Base path to use 20 | public BaseAddressAttribute(string baseAddress) 21 | { 22 | this.BaseAddress = baseAddress; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Common/BasePathAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase 4 | { 5 | /// 6 | /// Attribute applied to the interface, giving a path which is prepended to all paths 7 | /// 8 | [AttributeUsage(AttributeTargets.Interface, Inherited = true, AllowMultiple = false)] 9 | public sealed class BasePathAttribute : Attribute 10 | { 11 | /// 12 | /// Gets the base path set in this attribute 13 | /// 14 | public string BasePath { get; } 15 | 16 | /// 17 | /// Initialises a new instance of the class with the given base path 18 | /// 19 | /// Base path to use 20 | public BasePathAttribute(string basePath) 21 | { 22 | this.BasePath = basePath; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Common/BodyAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase 4 | { 5 | /// 6 | /// Attribute specifying that this parameter should be interpreted as the request body 7 | /// 8 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)] 9 | public sealed class BodyAttribute : Attribute 10 | { 11 | /// 12 | /// Gets the serialization method to use. Defaults to BodySerializationMethod.Serialized 13 | /// 14 | public BodySerializationMethod SerializationMethod { get; } 15 | 16 | /// 17 | /// Initialises a new instance of the class 18 | /// 19 | public BodyAttribute() 20 | : this(BodySerializationMethod.Default) 21 | { 22 | } 23 | 24 | /// 25 | /// Initialises a new instance of the class, using the given body serialization method 26 | /// 27 | /// Serialization method to use when serializing the body object 28 | public BodyAttribute(BodySerializationMethod serializationMethod) 29 | { 30 | this.SerializationMethod = serializationMethod; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Common/BodySerializationMethod.cs: -------------------------------------------------------------------------------- 1 | namespace RestEase 2 | { 3 | /// 4 | /// Type of serialization that should be applied to the body 5 | /// 6 | public enum BodySerializationMethod 7 | { 8 | /// 9 | /// Serialized using the configured IRequestBodySerializer (uses Json.NET by default) 10 | /// 11 | Serialized, 12 | 13 | /// 14 | /// Serialized using Form URL Encoding. The body must implement IDictionary 15 | /// 16 | UrlEncoded, 17 | 18 | /// 19 | /// Use the default serialization method. You probably don't want to specify this yourself 20 | /// 21 | Default, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Common/HeaderAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase 4 | { 5 | /// 6 | /// Attribute allowing interface-level, method-level, or parameter-level headers to be defined. See the docs for details 7 | /// 8 | [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = true, AllowMultiple = true)] 9 | public sealed class HeaderAttribute : Attribute 10 | { 11 | /// 12 | /// Gets the Name of the header 13 | /// 14 | public string Name { get; } 15 | 16 | /// 17 | /// Gets the value of the header, if present 18 | /// 19 | public string? Value { get; } 20 | 21 | /// 22 | /// Gets or sets the format string used to format the value, if this is used as a variable header 23 | /// (i.e. is null). 24 | /// 25 | /// 26 | /// If this looks like a format string which can be passed to , 27 | /// (i.e. it contains at least one format placeholder), then this happens with the value passed as the first arg. 28 | /// Otherwise, if the value implements , this is passed to the value's 29 | /// method. Otherwise this is ignored. 30 | /// Example values: "X2", "{0:X2}", "test{0}". 31 | /// 32 | public string? Format { get; set; } 33 | 34 | /// 35 | /// Initialises a new instance of the class 36 | /// # 37 | /// 38 | /// Name of the header 39 | public HeaderAttribute(string name) 40 | { 41 | this.Name = name; 42 | this.Value = null; 43 | } 44 | 45 | /// 46 | /// Initialises a new instance of the class 47 | /// 48 | /// Name of the header 49 | /// Value of the header 50 | public HeaderAttribute(string name, string? value) 51 | { 52 | this.Name = name; 53 | this.Value = value; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Common/HttpRequestMessagePropertyAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase 4 | { 5 | /// 6 | /// Marks a parameter as HTTP request message property 7 | /// 8 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] 9 | public class HttpRequestMessagePropertyAttribute : Attribute 10 | { 11 | /// 12 | /// Gets or sets the optional key of the parameter. Will use the parameter name if null 13 | /// 14 | public string? Key { get; set; } 15 | 16 | /// 17 | /// Initialises a new instance of the class 18 | /// 19 | public HttpRequestMessagePropertyAttribute() 20 | { } 21 | 22 | /// 23 | /// Initialises a new instance of the class, with the given key 24 | /// 25 | /// key 26 | public HttpRequestMessagePropertyAttribute(string key) 27 | { 28 | this.Key = key; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/AttributeModel.Reflection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace RestEase.Implementation.Analysis 5 | { 6 | internal abstract partial class AttributeModel 7 | { 8 | // May be null, if it was declared on parameters 9 | public MemberInfo? DeclaringMember { get; } 10 | 11 | public AttributeModel(MemberInfo? declaringMember) 12 | { 13 | this.DeclaringMember = declaringMember; 14 | } 15 | 16 | public static AttributeModel Create(T attribute, MemberInfo? declaringMember) where T : Attribute => 17 | new(attribute, declaringMember); 18 | 19 | public bool IsDeclaredOn(TypeModel typeModel) => typeModel.Type.Equals(this.DeclaringMember); 20 | } 21 | 22 | 23 | internal partial class AttributeModel : AttributeModel where T : Attribute 24 | { 25 | public AttributeModel(T attribute, MemberInfo? declaringMember) 26 | : base(declaringMember) 27 | { 28 | this.Attribute = attribute; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/AttributeModel.Roslyn.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.CodeAnalysis; 3 | 4 | namespace RestEase.Implementation.Analysis 5 | { 6 | internal abstract partial class AttributeModel 7 | { 8 | public AttributeData AttributeData { get; } 9 | 10 | public ISymbol DeclaringSymbol { get; } 11 | 12 | protected AttributeModel(AttributeData attributeData, ISymbol declaringSymbol) 13 | { 14 | this.AttributeData = attributeData; 15 | this.DeclaringSymbol = declaringSymbol; 16 | } 17 | 18 | public static AttributeModel Create(T attribute, AttributeData attributeData, ISymbol declaringSymbol) where T : Attribute => 19 | new(attribute, attributeData, declaringSymbol); 20 | 21 | public bool IsDeclaredOn(TypeModel typeModel) => 22 | SymbolEqualityComparer.Default.Equals(this.DeclaringSymbol, typeModel.NamedTypeSymbol); 23 | } 24 | 25 | internal partial class AttributeModel : AttributeModel where T : Attribute 26 | { 27 | public AttributeModel(T attribute, AttributeData attributeData, ISymbol declaringSymbol) 28 | : base(attributeData, declaringSymbol) 29 | { 30 | this.Attribute = attribute; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/AttributeModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase.Implementation.Analysis 4 | { 5 | internal abstract partial class AttributeModel 6 | { 7 | public abstract string AttributeName { get; } 8 | } 9 | 10 | internal partial class AttributeModel : AttributeModel where T : Attribute 11 | { 12 | public T Attribute { get; } 13 | 14 | public override string AttributeName => this.Attribute.GetType().Name; 15 | } 16 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/DiagnosticModel.Reflection.cs: -------------------------------------------------------------------------------- 1 | namespace RestEase.Implementation.Analysis 2 | { 3 | internal partial class DiagnosticModel 4 | { 5 | public string Message { get; } 6 | 7 | public DiagnosticModel(string message) 8 | { 9 | this.Message = message; 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/DiagnosticModel.cs: -------------------------------------------------------------------------------- 1 | namespace RestEase.Implementation.Analysis 2 | { 3 | internal partial class DiagnosticModel 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/EventModel.Reflection.cs: -------------------------------------------------------------------------------- 1 | namespace RestEase.Implementation.Analysis 2 | { 3 | internal partial class EventModel 4 | { 5 | public static EventModel Instance { get; } = new EventModel(); 6 | } 7 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/EventModel.Roslyn.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace RestEase.Implementation.Analysis 4 | { 5 | internal partial class EventModel 6 | { 7 | public IEventSymbol EventSymbol { get; } 8 | 9 | public EventModel(IEventSymbol eventSymbol) 10 | { 11 | this.EventSymbol = eventSymbol; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/EventModel.cs: -------------------------------------------------------------------------------- 1 | namespace RestEase.Implementation.Analysis 2 | { 3 | internal partial class EventModel 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/MethodModel.Reflection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace RestEase.Implementation.Analysis 5 | { 6 | internal partial class MethodModel 7 | { 8 | public MethodInfo MethodInfo { get; } 9 | 10 | public MethodModel(MethodInfo methodInfo) 11 | { 12 | this.MethodInfo = methodInfo; 13 | } 14 | 15 | public bool IsDeclaredOn(TypeModel typeModel) => this.MethodInfo.DeclaringType == typeModel.Type; 16 | } 17 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/MethodModel.Roslyn.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace RestEase.Implementation.Analysis 4 | { 5 | internal partial class MethodModel 6 | { 7 | public IMethodSymbol MethodSymbol { get; } 8 | 9 | public MethodModel(IMethodSymbol methodSymbol) 10 | { 11 | this.MethodSymbol = methodSymbol; 12 | } 13 | 14 | public bool IsDeclaredOn(TypeModel typeModel) => 15 | SymbolEqualityComparer.Default.Equals(typeModel.NamedTypeSymbol, this.MethodSymbol.ContainingType); 16 | } 17 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/MethodModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace RestEase.Implementation.Analysis 4 | { 5 | internal partial class MethodModel 6 | { 7 | public List> RequestAttributes { get; } = new List>(); 8 | public AttributeModel? AllowAnyStatusCodeAttribute { get; set; } 9 | public AttributeModel? SerializationMethodsAttribute { get; set; } 10 | public List> HeaderAttributes { get; } = new List>(); 11 | public bool IsDisposeMethod { get; set; } 12 | 13 | public List Parameters { get; } = new List(); 14 | 15 | // Set by the ImplementationGenerator, not the TypeAnalyzer 16 | public bool IsExplicit { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/MethodSignatureEqualityComparer.Reflection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase.Implementation.Analysis 4 | { 5 | internal partial class MethodSignatureEqualityComparer 6 | { 7 | public bool Equals(MethodModel? x, MethodModel? y) 8 | { 9 | var xInfo = x?.MethodInfo; 10 | var yInfo = y?.MethodInfo; 11 | if (xInfo == yInfo) 12 | return true; 13 | if (xInfo == null || yInfo == null) 14 | return false; 15 | 16 | if (xInfo.Name != yInfo.Name) 17 | return false; 18 | var xParameters = xInfo.GetParameters(); 19 | var yParameters = yInfo.GetParameters(); 20 | if (xParameters.Length != yParameters.Length) 21 | return false; 22 | if (xInfo.IsGenericMethod != yInfo.IsGenericMethod) 23 | return false; 24 | var xGenericArgs = xInfo.GetGenericArguments(); 25 | var yGenericArgs = yInfo.GetGenericArguments(); 26 | if (xInfo.IsGenericMethod && xGenericArgs.Length != yGenericArgs.Length) 27 | return false; 28 | for (int i = 0; i < xParameters.Length; i++) 29 | { 30 | var xParam = xParameters[i]; 31 | var yParam = yParameters[i]; 32 | if (xParam.ParameterType.IsGenericParameter != yParam.ParameterType.IsGenericParameter) 33 | { 34 | return false; 35 | } 36 | if (xParam.ParameterType.IsByRef != yParam.ParameterType.IsByRef) 37 | { 38 | return false; 39 | } 40 | 41 | if (xParam.ParameterType.IsGenericParameter) 42 | { 43 | if (Array.IndexOf(xGenericArgs, xParam.ParameterType) != Array.IndexOf(yGenericArgs, yParam.ParameterType)) 44 | return false; 45 | } 46 | else if (xParam.ParameterType != yParam.ParameterType) 47 | { 48 | return false; 49 | } 50 | } 51 | 52 | return true; 53 | } 54 | 55 | public int GetHashCode(MethodModel model) 56 | { 57 | var obj = model?.MethodInfo; 58 | if (obj == null) 59 | return 0; 60 | 61 | // We don't need everything, just be sensible 62 | unchecked 63 | { 64 | int hash = 17; 65 | hash = hash * 23 + obj.Name.GetHashCode(); 66 | hash = hash * 23 + obj.GetParameters().Length; 67 | return hash; 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/MethodSignatureEqualityComparer.Roslyn.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.CodeAnalysis; 3 | 4 | namespace RestEase.Implementation.Analysis 5 | { 6 | internal partial class MethodSignatureEqualityComparer 7 | { 8 | public bool Equals(MethodModel x, MethodModel y) 9 | { 10 | var xSymbol = x?.MethodSymbol; 11 | var ySymbol = y?.MethodSymbol; 12 | if (SymbolEqualityComparer.Default.Equals(xSymbol, ySymbol)) 13 | return true; 14 | if (xSymbol == null || ySymbol == null) 15 | return false; 16 | 17 | if (xSymbol.Name != ySymbol.Name) 18 | return false; 19 | var xParameters = xSymbol.Parameters; 20 | var yParameters = ySymbol.Parameters; 21 | if (xParameters.Length != yParameters.Length) 22 | return false; 23 | if (xSymbol.IsGenericMethod != ySymbol.IsGenericMethod) 24 | return false; 25 | var xGenericArgs = xSymbol.TypeArguments; 26 | var yGenericArgs = ySymbol.TypeArguments; 27 | if (xSymbol.IsGenericMethod && xGenericArgs.Length != yGenericArgs.Length) 28 | return false; 29 | for (int i = 0; i < xParameters.Length; i++) 30 | { 31 | var xParam = xParameters[i]; 32 | var yParam = yParameters[i]; 33 | if (xParam.Type.Kind != yParam.Type.Kind) 34 | { 35 | return false; 36 | } 37 | if ((xParam.RefKind == RefKind.None) != (yParam.RefKind == RefKind.None)) 38 | { 39 | return false; 40 | } 41 | 42 | if (xParam.Type.Kind == SymbolKind.TypeParameter) 43 | { 44 | if (xGenericArgs.IndexOf(xParam.Type) != yGenericArgs.IndexOf(yParam.Type)) 45 | return false; 46 | } 47 | else if (!SymbolEqualityComparer.Default.Equals(xParam.Type, yParam.Type)) 48 | { 49 | return false; 50 | } 51 | } 52 | 53 | return true; 54 | } 55 | 56 | public int GetHashCode(MethodModel model) 57 | { 58 | var obj = model?.MethodSymbol; 59 | if (obj == null) 60 | return 0; 61 | 62 | // We don't need everything, just be sensible 63 | unchecked 64 | { 65 | int hash = 17; 66 | hash = hash * 23 + obj.Name.GetHashCode(); 67 | hash = hash * 23 + obj.Parameters.Length; 68 | return hash; 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/MethodSignatureEqualityComparer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace RestEase.Implementation.Analysis 4 | { 5 | internal partial class MethodSignatureEqualityComparer : IEqualityComparer 6 | { 7 | public static MethodSignatureEqualityComparer Instance { get; } = new MethodSignatureEqualityComparer(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/ParameterModel.Reflection.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace RestEase.Implementation.Analysis 4 | { 5 | internal partial class ParameterModel 6 | { 7 | public ParameterInfo ParameterInfo { get; } 8 | 9 | public string Name => this.ParameterInfo.Name!; 10 | 11 | public ParameterModel(ParameterInfo parameterInfo) 12 | { 13 | this.ParameterInfo = parameterInfo; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/ParameterModel.Roslyn.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace RestEase.Implementation.Analysis 4 | { 5 | internal partial class ParameterModel 6 | { 7 | public IParameterSymbol ParameterSymbol { get; } 8 | 9 | public string Name => this.ParameterSymbol.Name; 10 | 11 | public ParameterModel(IParameterSymbol parameterSymbol) 12 | { 13 | this.ParameterSymbol = parameterSymbol; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/ParameterModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace RestEase.Implementation.Analysis 5 | { 6 | internal partial class ParameterModel 7 | { 8 | public AttributeModel? HeaderAttribute { get; set; } 9 | [MemberNotNull(nameof(PathAttributeName))] 10 | public AttributeModel? PathAttribute { get; set; } 11 | public string? PathAttributeName => this.PathAttribute == null ? null : this.PathAttribute.Attribute.Name ?? this.Name; 12 | [MemberNotNull(nameof(QueryAttributeName))] 13 | public AttributeModel? QueryAttribute { get; set; } 14 | public string? QueryAttributeName => this.QueryAttribute == null ? null : (this.QueryAttribute.Attribute.HasName ? this.QueryAttribute.Attribute.Name : this.Name); 15 | [MemberNotNull(nameof(HttpRequestMessagePropertyAttributeKey))] 16 | public AttributeModel? HttpRequestMessagePropertyAttribute { get; set; } 17 | public string? HttpRequestMessagePropertyAttributeKey => this.HttpRequestMessagePropertyAttribute == null ? null : this.HttpRequestMessagePropertyAttribute.Attribute.Key ?? this.Name; 18 | public AttributeModel? RawQueryStringAttribute { get; set; } 19 | public AttributeModel? QueryMapAttribute { get; set; } 20 | public AttributeModel? BodyAttribute { get; set; } 21 | public bool IsCancellationToken { get; set; } 22 | public bool IsByRef { get; set; } 23 | 24 | public IEnumerable GetAllSetAttributes() 25 | { 26 | if (this.HeaderAttribute != null) 27 | yield return this.HeaderAttribute; 28 | if (this.PathAttribute != null) 29 | yield return this.PathAttribute; 30 | if (this.QueryAttribute != null) 31 | yield return this.QueryAttribute; 32 | if (this.HttpRequestMessagePropertyAttribute != null) 33 | yield return this.HttpRequestMessagePropertyAttribute; 34 | if (this.RawQueryStringAttribute != null) 35 | yield return this.RawQueryStringAttribute; 36 | if (this.QueryMapAttribute != null) 37 | yield return this.QueryMapAttribute; 38 | if (this.BodyAttribute != null) 39 | yield return this.BodyAttribute; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/PropertyModel.Reflection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace RestEase.Implementation.Analysis 5 | { 6 | internal partial class PropertyModel 7 | { 8 | public PropertyInfo PropertyInfo { get; } 9 | 10 | public string Name => this.PropertyInfo.Name; 11 | 12 | public bool IsNullable => !this.PropertyInfo.PropertyType.GetTypeInfo().IsValueType || 13 | Nullable.GetUnderlyingType(this.PropertyInfo.PropertyType) != null; 14 | 15 | public PropertyModel(PropertyInfo propertyInfo) 16 | { 17 | this.PropertyInfo = propertyInfo; 18 | } 19 | 20 | public bool IsDeclaredOn(TypeModel typeModel) => this.PropertyInfo.DeclaringType == typeModel.Type; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/PropertyModel.Roslyn.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace RestEase.Implementation.Analysis 4 | { 5 | internal partial class PropertyModel 6 | { 7 | public IPropertySymbol PropertySymbol { get; } 8 | 9 | public string Name => this.PropertySymbol.Name; 10 | 11 | public bool IsNullable => this.PropertySymbol.Type.IsReferenceType || 12 | this.PropertySymbol.Type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T; 13 | 14 | public PropertyModel(IPropertySymbol propertySymbol) 15 | { 16 | this.PropertySymbol = propertySymbol; 17 | } 18 | 19 | public bool IsDeclaredOn(TypeModel typeModel) => 20 | SymbolEqualityComparer.Default.Equals(typeModel.NamedTypeSymbol, this.PropertySymbol.ContainingType); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/PropertyModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace RestEase.Implementation.Analysis 5 | { 6 | internal partial class PropertyModel 7 | { 8 | public AttributeModel? HeaderAttribute { get; set; } 9 | [MemberNotNull(nameof(PathAttributeName))] 10 | public AttributeModel? PathAttribute { get; set; } 11 | public string? PathAttributeName => this.PathAttribute == null ? null : this.PathAttribute.Attribute.Name ?? this.Name; 12 | [MemberNotNull(nameof(QueryAttributeName))] 13 | public AttributeModel? QueryAttribute { get; set; } 14 | public string? QueryAttributeName => this.QueryAttribute == null ? null : this.QueryAttribute.Attribute.Name ?? this.Name; 15 | [MemberNotNull(nameof(HttpRequestMessagePropertyAttributeKey))] 16 | public AttributeModel? HttpRequestMessagePropertyAttribute { get; set; } 17 | public string? HttpRequestMessagePropertyAttributeKey => this.HttpRequestMessagePropertyAttribute == null ? null : this.HttpRequestMessagePropertyAttribute.Attribute.Key ?? this.Name; 18 | public bool IsRequester { get; set; } 19 | public bool HasGetter { get; set; } 20 | public bool HasSetter { get; set; } 21 | 22 | // Set by the ImplementationGenerator, not the TypeAnalyzer 23 | public bool IsExplicit { get; set; } 24 | 25 | public IEnumerable GetAllSetAttributes() 26 | { 27 | if (this.HeaderAttribute != null) 28 | yield return this.HeaderAttribute; 29 | if (this.PathAttribute != null) 30 | yield return this.PathAttribute; 31 | if (this.QueryAttribute != null) 32 | yield return this.QueryAttribute; 33 | if (this.HttpRequestMessagePropertyAttribute != null) 34 | yield return this.HttpRequestMessagePropertyAttribute; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/TypeModel.Reflection.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | 4 | namespace RestEase.Implementation.Analysis 5 | { 6 | internal partial class TypeModel 7 | { 8 | public Type Type { get; } 9 | 10 | public TypeModel(Type type) 11 | { 12 | this.Type = type; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/TypeModel.Roslyn.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace RestEase.Implementation.Analysis 4 | { 5 | internal partial class TypeModel 6 | { 7 | public INamedTypeSymbol NamedTypeSymbol { get; } 8 | 9 | public TypeModel(INamedTypeSymbol namedTypeSymbol) 10 | { 11 | this.NamedTypeSymbol = namedTypeSymbol; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Analysis/TypeModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace RestEase.Implementation.Analysis 5 | { 6 | internal partial class TypeModel 7 | { 8 | public List> HeaderAttributes { get; } = new List>(); 9 | public List> AllowAnyStatusCodeAttributes { get; } = new List>(); 10 | public AttributeModel? TypeAllowAnyStatusCodeAttribute => this.AllowAnyStatusCodeAttributes.FirstOrDefault(x => x.IsDeclaredOn(this)); 11 | public AttributeModel? SerializationMethodsAttribute { get; set; } 12 | public AttributeModel? BaseAddressAttribute { get; set; } 13 | public AttributeModel? BasePathAttribute { get; set; } 14 | public bool IsAccessible { get; set; } 15 | public List Events { get; } = new List(); 16 | public List Properties { get; } = new List(); 17 | public List Methods { get; } = new List(); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Common/Implementation/DiagnosticCode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase.Implementation 4 | { 5 | /// 6 | /// Identifies the type of error / diagnostic encountered during emission 7 | /// 8 | public enum DiagnosticCode 9 | { 10 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 11 | None = 0, 12 | MultipleCancellationTokenParameters = 1, 13 | MissingPathPropertyForBasePathPlaceholder = 2, 14 | MissingPathPropertyOrParameterForPlaceholder = 3, 15 | MissingPlaceholderForPathParameter = 4, 16 | MultiplePathPropertiesForKey = 5, 17 | MultiplePathParametersForKey = 6, 18 | MultipleBodyParameters = 7, 19 | HeaderOnInterfaceMustHaveValue = 8, 20 | HeaderParameterMustNotHaveValue = 9, 21 | HeaderMustNotHaveColonInName = 10, 22 | PropertyMustBeReadWrite = 11, 23 | HeaderPropertyWithValueMustBeNullable = 12, 24 | QueryMapParameterIsNotADictionary = 13, 25 | AllowAnyStatusCodeAttributeNotAllowedOnParentInterface = 14, 26 | EventsNotAllowed = 15, 27 | PropertyMustBeReadOnly = 16, 28 | MultipleRequesterProperties = 17, 29 | MethodMustHaveRequestAttribute = 18, 30 | MethodMustHaveValidReturnType = 19, 31 | PropertyMustHaveOneAttribute = 20, 32 | RequesterPropertyMustHaveZeroAttributes = 21, 33 | MultipleHttpRequestMessagePropertiesForKey = 22, 34 | HttpRequestMessageParamDuplicatesPropertyForKey = 23, 35 | MultipleHttpRequestMessageParametersForKey = 24, 36 | [Obsolete("No longer used")] 37 | ParameterMustHaveZeroOrOneAttributes = 25, 38 | CancellationTokenMustHaveZeroAttributes = 26, 39 | CouldNotFindRestEaseType = 27, 40 | CouldNotFindSystemType = 28, 41 | ExpressionsNotAvailable = 29, 42 | ParameterMustNotBeByRef = 30, 43 | InterfaceTypeMustBeAccessible = 31, 44 | AttributeConstructorNotRecognised = 32, 45 | AttributePropertyNotRecognised = 33, 46 | CouldNotFindRestEaseAssembly = 34, 47 | MissingPathPropertyForBaseAddressPlaceholder = 35, 48 | BaseAddressMustBeAbsolute = 36, 49 | RestEaseVersionTooOld = 37, 50 | RestEaseVersionTooNew = 38, 51 | MethodMustHaveOneRequestAttribute = 39, 52 | QueryConflictWithRawQueryString = 40, 53 | #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member 54 | } 55 | 56 | /// 57 | /// Extension methods on 58 | /// 59 | public static class DiagnosticCodeExtensions 60 | { 61 | /// 62 | /// Format the code as e.g. REST001 63 | /// 64 | public static string Format(this DiagnosticCode code) 65 | { 66 | return $"REST{(int)code:D3}"; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Common/Implementation/Emission/EmittedProperty.Emit.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection.Emit; 2 | using RestEase.Implementation.Analysis; 3 | 4 | namespace RestEase.Implementation.Emission 5 | { 6 | internal partial class EmittedProperty 7 | { 8 | public FieldBuilder FieldBuilder { get; } 9 | 10 | public EmittedProperty(PropertyModel propertyModel, FieldBuilder fieldBuilder) 11 | { 12 | this.PropertyModel = propertyModel; 13 | this.FieldBuilder = fieldBuilder; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Emission/EmittedProperty.Roslyn.cs: -------------------------------------------------------------------------------- 1 | using RestEase.Implementation.Analysis; 2 | 3 | namespace RestEase.Implementation.Emission 4 | { 5 | internal partial class EmittedProperty 6 | { 7 | public EmittedProperty(PropertyModel propertyModel) 8 | { 9 | this.PropertyModel = propertyModel; 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Emission/EmittedProperty.cs: -------------------------------------------------------------------------------- 1 | using RestEase.Implementation.Analysis; 2 | 3 | namespace RestEase.Implementation.Emission 4 | { 5 | internal partial class EmittedProperty 6 | { 7 | public PropertyModel PropertyModel { get; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Emission/EmittedType.Emit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase.Implementation.Emission 4 | { 5 | internal class EmittedType 6 | { 7 | public Type Type { get; } 8 | 9 | public EmittedType(Type type) => this.Type = type; 10 | } 11 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Emission/EmittedType.Roslyn.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.Text; 2 | 3 | namespace RestEase.Implementation.Emission 4 | { 5 | internal class EmittedType 6 | { 7 | public SourceText SourceText { get; } 8 | 9 | public EmittedType(SourceText sourceText) 10 | { 11 | this.SourceText = sourceText; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Emission/Emitter.Emit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Reflection.Emit; 4 | using System.Text; 5 | using System.Threading; 6 | using RestEase.Implementation.Analysis; 7 | using RestEase.Platform; 8 | 9 | namespace RestEase.Implementation.Emission 10 | { 11 | internal class Emitter 12 | { 13 | private readonly ModuleBuilder moduleBuilder; 14 | private int numTypes; 15 | 16 | public Emitter(ModuleBuilder moduleBuilder) 17 | { 18 | this.moduleBuilder = moduleBuilder; 19 | } 20 | 21 | public TypeEmitter EmitType(TypeModel type) 22 | { 23 | var typeBuilder = this.moduleBuilder.DefineType(this.CreateImplementationName(type.Type), TypeAttributes.Public | TypeAttributes.Sealed); 24 | 25 | return new TypeEmitter(typeBuilder, type); 26 | } 27 | 28 | private string CreateImplementationName(Type interfaceType) 29 | { 30 | int numTypes = Interlocked.Increment(ref this.numTypes); 31 | var typeInfo = interfaceType.GetTypeInfo(); 32 | string name = typeInfo.IsGenericType ? typeInfo.GetGenericTypeDefinition().FullName! : typeInfo.FullName!; 33 | return "RestEase.AutoGenerated.<>" + name.Replace('.', '+') + "_" + numTypes; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/Common/Implementation/Emission/Emitter.Roslyn.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using RestEase.Implementation.Analysis; 3 | using RestEase.SourceGenerator.Implementation; 4 | 5 | namespace RestEase.Implementation.Emission 6 | { 7 | internal class Emitter 8 | { 9 | private readonly Compilation compilation; 10 | private readonly WellKnownSymbols wellKnownSymbols; 11 | private int numTypes; 12 | 13 | public Emitter(Compilation compilation, WellKnownSymbols wellKnownSymbols) 14 | { 15 | this.compilation = compilation; 16 | this.wellKnownSymbols = wellKnownSymbols; 17 | } 18 | 19 | public TypeEmitter EmitType(TypeModel type) 20 | { 21 | this.numTypes++; 22 | return new TypeEmitter(type, this.compilation, this.wellKnownSymbols, this.numTypes); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/Common/Implementation/ResolvedSerializationMethods.cs: -------------------------------------------------------------------------------- 1 | namespace RestEase.Implementation 2 | { 3 | internal class ResolvedSerializationMethods 4 | { 5 | public const BodySerializationMethod DefaultBodySerializationMethod = BodySerializationMethod.Serialized; 6 | public const QuerySerializationMethod DefaultQuerySerializationMethod = QuerySerializationMethod.ToString; 7 | public const PathSerializationMethod DefaultPathSerializationMethod = PathSerializationMethod.ToString; 8 | 9 | public SerializationMethodsAttribute? ClassAttribute { get; private set; } 10 | 11 | public SerializationMethodsAttribute? MethodAttribute { get; private set; } 12 | 13 | 14 | public ResolvedSerializationMethods(SerializationMethodsAttribute? classAttribute, SerializationMethodsAttribute? methodAttribute) 15 | { 16 | this.ClassAttribute = classAttribute; 17 | this.MethodAttribute = methodAttribute; 18 | } 19 | 20 | public BodySerializationMethod ResolveBody(BodySerializationMethod parameterMethod) 21 | { 22 | if (parameterMethod != BodySerializationMethod.Default) 23 | return parameterMethod; 24 | 25 | if (this.MethodAttribute != null && this.MethodAttribute.Body != BodySerializationMethod.Default) 26 | return this.MethodAttribute.Body; 27 | 28 | if (this.ClassAttribute != null && this.ClassAttribute.Body != BodySerializationMethod.Default) 29 | return this.ClassAttribute.Body; 30 | 31 | return DefaultBodySerializationMethod; 32 | } 33 | 34 | public QuerySerializationMethod ResolveQuery(QuerySerializationMethod parameterMethod) 35 | { 36 | if (parameterMethod != QuerySerializationMethod.Default) 37 | return parameterMethod; 38 | 39 | if (this.MethodAttribute != null && this.MethodAttribute.Query != QuerySerializationMethod.Default) 40 | return this.MethodAttribute.Query; 41 | 42 | if (this.ClassAttribute != null && this.ClassAttribute.Query != QuerySerializationMethod.Default) 43 | return this.ClassAttribute.Query; 44 | 45 | return DefaultQuerySerializationMethod; 46 | } 47 | 48 | public PathSerializationMethod ResolvePath(PathSerializationMethod parameterMethod) 49 | { 50 | if (parameterMethod != PathSerializationMethod.Default) 51 | return parameterMethod; 52 | 53 | if (this.MethodAttribute != null && this.MethodAttribute.Path != PathSerializationMethod.Default) 54 | return this.MethodAttribute.Path; 55 | 56 | if (this.ClassAttribute != null && this.ClassAttribute.Path != PathSerializationMethod.Default) 57 | return this.ClassAttribute.Path; 58 | 59 | return DefaultPathSerializationMethod; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Common/PathAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase 4 | { 5 | /// 6 | /// Marks a parameter as able to substitute a placeholder in this method's path 7 | /// 8 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] 9 | public sealed class PathAttribute : Attribute 10 | { 11 | /// 12 | /// Gets or sets the optional name of the placeholder. Will use the parameter name if null 13 | /// 14 | public string? Name { get; set; } 15 | 16 | /// 17 | /// Gets the serialization method to use to serialize the value. Defaults to PathSerializationMethod.ToString 18 | /// 19 | public PathSerializationMethod SerializationMethod { get; set; } 20 | 21 | /// 22 | /// Gets or sets the format string used to format the value 23 | /// 24 | /// 25 | /// If is , this is passed to the serializer 26 | /// as . 27 | /// Otherwise, if this looks like a format string which can be passed to , 28 | /// (i.e. it contains at least one format placeholder), then this happens with the value passed as the first arg. 29 | /// Otherwise, if the value implements , this is passed to the value's 30 | /// method. Otherwise this is ignored. 31 | /// Example values: "X2", "{0:X2}", "test{0}". 32 | /// 33 | public string? Format { get; set; } 34 | 35 | /// 36 | /// Gets or sets a value indicating whether this path parameter should be URL-encoded. Defaults to true. 37 | /// 38 | public bool UrlEncode { get; set; } = true; 39 | 40 | /// 41 | /// Initialises a new instance of the class 42 | /// 43 | public PathAttribute() 44 | : this(PathSerializationMethod.Default) 45 | { 46 | } 47 | 48 | /// 49 | /// Initializes a new instance of the class, with the given serialization method 50 | /// 51 | /// Serialization method to use to serialize the value 52 | public PathAttribute(PathSerializationMethod serializationMethod) 53 | { 54 | // Don't set this.Name 55 | this.SerializationMethod = serializationMethod; 56 | } 57 | 58 | /// 59 | /// Initialises a new instance of the class, with the given name 60 | /// 61 | /// Placeholder in the path to replace 62 | public PathAttribute(string name) 63 | : this(name, PathSerializationMethod.Default) 64 | { 65 | } 66 | 67 | /// 68 | /// Initialises a new instance of the class, with the given name and serialization method 69 | /// 70 | /// Placeholder in the path to replace 71 | /// Serialization method to use to serialize the value 72 | public PathAttribute(string name, PathSerializationMethod serializationMethod) 73 | { 74 | this.Name = name; 75 | this.SerializationMethod = serializationMethod; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Common/PathSerializationMethod.cs: -------------------------------------------------------------------------------- 1 | namespace RestEase 2 | { 3 | /// 4 | /// Type of serialization that should be applied to the path parameter's value 5 | /// 6 | public enum PathSerializationMethod 7 | { 8 | /// 9 | /// Serialized using its .ToString() method 10 | /// 11 | ToString, 12 | 13 | /// 14 | /// Serialized using the configured RequestPathParamSerializer 15 | /// 16 | Serialized, 17 | 18 | /// 19 | /// Use the default serialization method. You probably don't want to specify this yourself 20 | /// 21 | Default, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Common/QueryAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase 4 | { 5 | /// 6 | /// Marks a parameter as being a query param 7 | /// 8 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] 9 | public sealed class QueryAttribute : Attribute 10 | { 11 | private string? _name; 12 | 13 | /// 14 | /// Gets or sets the name of the query param. Will use the parameter / property name if unset. 15 | /// 16 | public string? Name 17 | { 18 | get => this._name; 19 | set 20 | { 21 | this._name = value; 22 | this.HasName = true; 23 | } 24 | } 25 | 26 | /// 27 | /// Gets a value indicating whether the user has set the name attribute 28 | /// 29 | public bool HasName { get; private set; } 30 | 31 | /// 32 | /// Gets the serialization method to use to serialize the value. Defaults to QuerySerializationMethod.ToString 33 | /// 34 | public QuerySerializationMethod SerializationMethod { get; set; } 35 | 36 | /// 37 | /// Gets or sets the format string used to format the value 38 | /// 39 | /// 40 | /// If is , this is passed to the serializer 41 | /// as . 42 | /// Otherwise, if this looks like a format string which can be passed to , 43 | /// (i.e. it contains at least one format placeholder), then this happens with the value passed as the first arg. 44 | /// Otherwise, if the value implements , this is passed to the value's 45 | /// method. Otherwise this is ignored. 46 | /// Example values: "X2", "{0:X2}", "test{0}". 47 | /// 48 | public string? Format { get; set; } 49 | 50 | /// 51 | /// Initialises a new instance of the class 52 | /// 53 | public QueryAttribute() 54 | : this(QuerySerializationMethod.Default) 55 | { 56 | } 57 | 58 | /// 59 | /// Initialises a new instance of the class, with the given serialization method 60 | /// 61 | /// Serialization method to use to serialize the value 62 | public QueryAttribute(QuerySerializationMethod serializationMethod) 63 | { 64 | // Don't set this.Name 65 | this.SerializationMethod = serializationMethod; 66 | } 67 | 68 | /// 69 | /// Initialises a new instance of the class, with the given name 70 | /// 71 | /// Name of the query parameter 72 | public QueryAttribute(string? name) 73 | : this(name, QuerySerializationMethod.Default) 74 | { 75 | } 76 | 77 | /// 78 | /// Initialises a new instance of the class, with the given name and serialization method 79 | /// 80 | /// Name of the query parameter 81 | /// Serialization method to use to serialize the value 82 | public QueryAttribute(string? name, QuerySerializationMethod serializationMethod) 83 | { 84 | this.Name = name; 85 | this.SerializationMethod = serializationMethod; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Common/QueryMapAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase 4 | { 5 | /// 6 | /// Marks a parameter as being the method's Query Map 7 | /// 8 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = true)] 9 | public sealed class QueryMapAttribute : Attribute 10 | { 11 | /// 12 | /// Gets and sets the serialization method to use to serialize the value. Defaults to QuerySerializationMethod.ToString 13 | /// 14 | public QuerySerializationMethod SerializationMethod { get; set; } 15 | 16 | /// 17 | /// Initialises a new instance of the class 18 | /// 19 | public QueryMapAttribute() 20 | : this(QuerySerializationMethod.Default) 21 | { 22 | } 23 | 24 | /// 25 | /// Initialises a new instance of the with the given serialization method 26 | /// 27 | /// Serialization method to use to serialize the value 28 | public QueryMapAttribute(QuerySerializationMethod serializationMethod) 29 | { 30 | this.SerializationMethod = serializationMethod; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Common/QuerySerialializationMethod.cs: -------------------------------------------------------------------------------- 1 | namespace RestEase 2 | { 3 | /// 4 | /// Type of serialization that should be applied to the query parameter's value 5 | /// 6 | public enum QuerySerializationMethod 7 | { 8 | /// 9 | /// Serialized using its .ToString() method 10 | /// 11 | ToString, 12 | 13 | /// 14 | /// Serialized using the configured IRequestQueryParamSerializer (uses Json.NET by default) 15 | /// 16 | Serialized, 17 | 18 | /// 19 | /// Use the default serialization method. You probably don't want to specify this yourself 20 | /// 21 | Default, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Common/RawQueryStringAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase 4 | { 5 | /// 6 | /// Marks a parameter as being a raw query string, which is inserted as-is into the query string 7 | /// 8 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)] 9 | public class RawQueryStringAttribute : Attribute 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Common/SerializationMethodsAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase 4 | { 5 | /// 6 | /// Specifies the default serialization methods for query parameters and request bodies (which can then be overridden) 7 | /// 8 | [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Method, Inherited = false, AllowMultiple = false)] 9 | public sealed class SerializationMethodsAttribute : Attribute 10 | { 11 | /// 12 | /// Gets and sets the serialization method used to serialize request bodies. Defaults to BodySerializationMethod.Serialized 13 | /// 14 | public BodySerializationMethod Body { get; set; } 15 | 16 | /// 17 | /// Gets and sets the serialization method used to serialize query parameters. Defaults to QuerySerializationMethod.ToString 18 | /// 19 | public QuerySerializationMethod Query { get; set; } 20 | 21 | /// 22 | /// Gets and sets the serialization method used to serialize path parameters. Default to PathSerializationMethod.ToString 23 | /// 24 | public PathSerializationMethod Path { get; set; } 25 | 26 | /// 27 | /// Initialises a new instance of the class 28 | /// 29 | public SerializationMethodsAttribute() 30 | { 31 | this.Body = BodySerializationMethod.Default; 32 | this.Query = QuerySerializationMethod.Default; 33 | this.Path = PathSerializationMethod.Default; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/RestEase.HttpClientFactory.UnitTests/HttpClientFactoryExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Xunit; 8 | using RestEase.HttpClientFactory; 9 | using Moq; 10 | using System.Net.Http; 11 | using Moq.Protected; 12 | using System.Threading; 13 | 14 | namespace RestEase.UnitTests.HttpClientFactoryTests 15 | { 16 | public class HttpClientFactoryExtensionsTests 17 | { 18 | public interface ISomeApi 19 | { 20 | [Get] 21 | Task FooAsync(); 22 | } 23 | public interface ISomeApi2 24 | { 25 | [Get] 26 | Task FooAsync(); 27 | } 28 | 29 | private class TestMessageHandler : HttpMessageHandler 30 | { 31 | public int CallCount { get; set; } 32 | 33 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 34 | { 35 | this.CallCount++; 36 | Assert.Equal("http://localhost/", request.RequestUri?.ToString()); 37 | return Task.FromResult(new HttpResponseMessage()); 38 | } 39 | } 40 | 41 | [Fact] 42 | public void RegistersGenericTransient() 43 | { 44 | var services = new ServiceCollection(); 45 | 46 | services.AddRestEaseClient(); 47 | 48 | var serviceProvider = services.BuildServiceProvider(); 49 | 50 | var instance1 = serviceProvider.GetRequiredService(); 51 | var instance2 = serviceProvider.GetRequiredService(); 52 | Assert.NotSame(instance1, instance2); 53 | } 54 | 55 | [Fact] 56 | public void RegistersNonGenericTransient() 57 | { 58 | var services = new ServiceCollection(); 59 | 60 | services.AddRestEaseClient(typeof(ISomeApi)); 61 | 62 | var serviceProvider = services.BuildServiceProvider(); 63 | 64 | var instance1 = serviceProvider.GetRequiredService(); 65 | var instance2 = serviceProvider.GetRequiredService(); 66 | Assert.NotSame(instance1, instance2); 67 | } 68 | 69 | [Fact] 70 | public void RegistersClientOnExistingHttpClientBuilder() 71 | { 72 | // Test that they resolve, and call the configured handler (and so use the HttpClient we created) 73 | 74 | var services = new ServiceCollection(); 75 | 76 | var handler = new TestMessageHandler(); 77 | 78 | services.AddHttpClient("test") 79 | .ConfigureHttpClient(x => x.BaseAddress = new Uri("http://localhost")) 80 | .ConfigurePrimaryHttpMessageHandler(() => handler) 81 | .UseWithRestEaseClient() 82 | .UseWithRestEaseClient(); 83 | 84 | var serviceProvider = services.BuildServiceProvider(); 85 | 86 | var instance1 = serviceProvider.GetRequiredService(); 87 | var instance2 = serviceProvider.GetRequiredService(); 88 | 89 | instance1.FooAsync().Wait(); 90 | instance2.FooAsync().Wait(); 91 | 92 | Assert.Equal(2, handler.CallCount); 93 | } 94 | 95 | [Fact] 96 | public void RegistersRequestModifierAndPrimaryHandler() 97 | { 98 | var services = new ServiceCollection(); 99 | 100 | int callCount = 0; 101 | var handler = new TestMessageHandler(); 102 | 103 | services.AddRestEaseClient(options: new() 104 | { 105 | RequestModifier = (request, cancellationToken) => 106 | { 107 | callCount++; 108 | return Task.CompletedTask; 109 | } 110 | }).ConfigureHttpClient(x => x.BaseAddress = new Uri("http://localhost")) 111 | .ConfigurePrimaryHttpMessageHandler(() => handler); 112 | 113 | var serviceProvider = services.BuildServiceProvider(); 114 | 115 | var instance = serviceProvider.GetRequiredService(); 116 | instance.FooAsync().Wait(); 117 | 118 | Assert.Equal(1, callCount); 119 | Assert.Equal(1, handler.CallCount); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/RestEase.HttpClientFactory.UnitTests/RestEase.HttpClientFactory.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0.0.0 5 | net6.0 6 | RestEase.HttpClientFactory.UnitTests 7 | 10.0 8 | enable 9 | 10 | false 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/RestEase.HttpClientFactory/CompatibilitySuppressions.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | CP0003 5 | RestEase.HttpClientFactory, Version=1.2.3.0, Culture=neutral, PublicKeyToken=null 6 | lib/netstandard2.0/RestEase.HttpClientFactory.dll 7 | lib/netstandard2.0/RestEase.HttpClientFactory.dll 8 | true 9 | 10 | -------------------------------------------------------------------------------- /src/RestEase.HttpClientFactory/RestEase.HttpClientFactory.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 10.0 6 | enable 7 | true 8 | false 9 | 10 | 1.5.5 11 | 12 | 13 | 0.0.0 14 | RestEase.HttpClientFactory.nuspec 15 | ../../NuGet 16 | true 17 | 18 | 19 | true 20 | snupkg 21 | true 22 | portable 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | outputPath=$(OutputPath);version=$(PackageVersion) 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/RestEase.HttpClientFactory/RestEase.HttpClientFactory.nuspec: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | RestEase.HttpClientFactory 5 | $version$ 6 | Antony Male 7 | Antony Male 8 | false 9 | MIT 10 | README.md 11 | icon.png 12 | https://github.com/canton7/RestEase 13 | HttpClientFactory adapter for ASP.NET Core for RestEase: the easy-to-use typesafe REST API client library, which is simple and customisable. 14 | Copyright © Antony Male 2015-2022 15 | REST JSON RestEase SourceGenerator 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/RestEase.SourceGenerator.UnitTests/RestEase.SourceGenerator.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 0.0.0 5 | net6.0 6 | RestEase.SourceGenerator.UnitTests 7 | RestEase.UnitTests 8 | 10.0 9 | annotations 10 | SOURCE_GENERATOR 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/RestEase.SourceGenerator/Implementation/AllowedRestEaseVersionRangeAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase.SourceGenerator.Implementation 4 | { 5 | [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] 6 | internal class AllowedRestEaseVersionRangeAttribute : Attribute 7 | { 8 | public string MinVersionInclusive { get; } 9 | public string MaxVersionExclusive { get; } 10 | 11 | public AllowedRestEaseVersionRangeAttribute(string minVersionInclusive, string maxVersionExlusive) 12 | { 13 | this.MinVersionInclusive = minVersionInclusive; 14 | this.MaxVersionExclusive = maxVersionExlusive; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/RestEase.SourceGenerator/Implementation/Processor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp.Syntax; 5 | 6 | namespace RestEase.SourceGenerator.Implementation 7 | { 8 | internal class Processor 9 | { 10 | private readonly GeneratorExecutionContext context; 11 | private readonly RoslynImplementationFactory factory; 12 | 13 | public Processor(GeneratorExecutionContext context) 14 | { 15 | this.context = context; 16 | this.factory = new RoslynImplementationFactory(context.Compilation); 17 | } 18 | 19 | public void Process() 20 | { 21 | if (this.context.SyntaxReceiver is not SyntaxReceiver syntaxReceiver) 22 | return; 23 | 24 | try 25 | { 26 | this.ProcessMemberSyntaxes(syntaxReceiver.MemberSyntaxes); 27 | } 28 | finally // Just in case we crash... 29 | { 30 | // Report the compilation-level diagnostics 31 | foreach (var diagnostic in this.factory.GetCompilationDiagnostics()) 32 | { 33 | this.context.ReportDiagnostic(diagnostic); 34 | } 35 | } 36 | } 37 | 38 | private void ProcessMemberSyntaxes(IEnumerable memberSyntaxes) 39 | { 40 | var visitedTypes = new HashSet(); 41 | foreach (var memberSyntax in memberSyntaxes) 42 | { 43 | var memberSymbol = this.context.Compilation 44 | .GetSemanticModel(memberSyntax.SyntaxTree) 45 | .GetDeclaredSymbol(memberSyntax); 46 | var containingType = memberSymbol?.ContainingType; 47 | if (containingType != null 48 | && containingType.TypeKind == TypeKind.Interface 49 | && visitedTypes.Add(containingType)) 50 | { 51 | foreach (var attributeData in memberSymbol!.GetAttributes()) 52 | { 53 | if (attributeData.AttributeClass != null && 54 | this.factory.IsRestEaseAttribute(attributeData.AttributeClass)) 55 | { 56 | this.ProcessType(containingType); 57 | break; 58 | } 59 | } 60 | } 61 | 62 | this.context.CancellationToken.ThrowIfCancellationRequested(); 63 | } 64 | } 65 | 66 | private void ProcessType(INamedTypeSymbol namedTypeSymbol) 67 | { 68 | var (sourceText, diagnostics) = this.factory.CreateImplementation(namedTypeSymbol); 69 | foreach (var diagnostic in diagnostics) 70 | { 71 | this.context.ReportDiagnostic(diagnostic); 72 | } 73 | 74 | if (sourceText != null) 75 | { 76 | var nameBuilder = new StringBuilder(); 77 | foreach (var part in namedTypeSymbol.ToDisplayParts(SymbolDisplayFormats.GeneratedFileName)) 78 | { 79 | nameBuilder.Append(part.ToString()); 80 | if (part.Symbol is INamedTypeSymbol typeSymbol && typeSymbol.Arity > 0) 81 | { 82 | nameBuilder.Append('`').Append(typeSymbol.Arity); 83 | } 84 | } 85 | this.context.AddSource(nameBuilder.ToString() + ".g", sourceText); 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/RestEase.SourceGenerator/Implementation/RoslynEmitUtils.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace RestEase.SourceGenerator.Implementation 4 | { 5 | internal static class RoslynEmitUtils 6 | { 7 | public static string QuoteString(string? s) => s == null ? "null" : "@\"" + s.Replace("\"", "\"\"") + "\""; 8 | 9 | public static string AddBareAngles(INamedTypeSymbol symbol, string name) 10 | { 11 | return symbol.IsGenericType 12 | ? name + "<" + new string(',', symbol.TypeParameters.Length - 1) + ">" 13 | : name; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/RestEase.SourceGenerator/Implementation/RoslynImplementationFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.Text; 6 | using RestEase.Implementation; 7 | using RestEase.Implementation.Emission; 8 | 9 | namespace RestEase.SourceGenerator.Implementation 10 | { 11 | public class RoslynImplementationFactory 12 | { 13 | private readonly Compilation compilation; 14 | private readonly DiagnosticReporter symbolsDiagnosticReporter; 15 | private readonly WellKnownSymbols wellKnownSymbols; 16 | private readonly AttributeInstantiator attributeInstantiator; 17 | private readonly Emitter emitter; 18 | private readonly HashSet symbolsDiagnostics = new(); 19 | 20 | public RoslynImplementationFactory(Compilation compilation) 21 | { 22 | this.compilation = compilation; 23 | this.symbolsDiagnosticReporter = new DiagnosticReporter(); 24 | this.wellKnownSymbols = new WellKnownSymbols(compilation, this.symbolsDiagnosticReporter); 25 | this.attributeInstantiator = new AttributeInstantiator(this.wellKnownSymbols); 26 | this.emitter = new Emitter(compilation, this.wellKnownSymbols); 27 | 28 | // Catch any symbols errors from just instantiating WellKnownSymbols 29 | this.symbolsDiagnostics.UnionWith(this.symbolsDiagnosticReporter.Diagnostics); 30 | } 31 | 32 | public bool IsRestEaseAttribute(INamedTypeSymbol namedTypeSymbol) => 33 | this.attributeInstantiator.IsRestEaseAttribute(namedTypeSymbol); 34 | 35 | public (SourceText? source, IReadOnlyList diagnostics) CreateImplementation(INamedTypeSymbol namedTypeSymbol) 36 | { 37 | // If we've got any symbols errors from just instantiating it, we're going to have a bad time. Give up now. 38 | if (this.symbolsDiagnosticReporter.HasErrors) 39 | { 40 | return (null, Array.Empty()); 41 | } 42 | 43 | var diagnosticReporter = new DiagnosticReporter(); 44 | var analyzer = new RoslynTypeAnalyzer(this.compilation, namedTypeSymbol, this.wellKnownSymbols, this.attributeInstantiator, diagnosticReporter); 45 | var typeModel = analyzer.Analyze(); 46 | var generator = new ImplementationGenerator(typeModel, this.emitter, diagnosticReporter); 47 | var emittedType = generator.Generate(); 48 | 49 | // If there are symbols diagnostic errors, we have to fail this type. 50 | // However, we'll then clear the symbols diagnostics, so we can move onto the next type 51 | // (which might succeed). 52 | // We'll report the symbols diagnostics in one go at the end. We might well end up with duplicates 53 | // (WellKnownSymbols will keep reporting the same diagnostics), so use a HashSet. 54 | 55 | this.symbolsDiagnostics.UnionWith(this.symbolsDiagnosticReporter.Diagnostics); 56 | bool hasSymbolsErrors = this.symbolsDiagnosticReporter.HasErrors; 57 | this.symbolsDiagnosticReporter.Clear(); 58 | 59 | return (hasSymbolsErrors || diagnosticReporter.HasErrors 60 | ? null 61 | : emittedType.SourceText, diagnosticReporter.Diagnostics); 62 | } 63 | 64 | public List GetCompilationDiagnostics() => this.symbolsDiagnostics.ToList(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/RestEase.SourceGenerator/Implementation/SyntaxReceiver.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | 5 | namespace RestEase.SourceGenerator.Implementation 6 | { 7 | public class SyntaxReceiver : ISyntaxReceiver 8 | { 9 | public List MemberSyntaxes { get; } = new(); 10 | 11 | public void OnVisitSyntaxNode(SyntaxNode syntaxNode) 12 | { 13 | // Actually matching the attributes is hard -- they don't have to be qualified, and we've 14 | // lot loads of attribute types. Just pick up on all members which have attributes, and we'll 15 | // filter them properly in Processor. 16 | 17 | if (syntaxNode is MemberDeclarationSyntax member && 18 | member.SyntaxTree != null && 19 | (member is PropertyDeclarationSyntax || member is MethodDeclarationSyntax) && 20 | member.AttributeLists.Count > 0) 21 | { 22 | this.MemberSyntaxes.Add(member); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/RestEase.SourceGenerator/Implementation/WellKnownNames.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net.Http; 3 | 4 | namespace RestEase.SourceGenerator.Implementation 5 | { 6 | internal static class WellKnownNames 7 | { 8 | public static IReadOnlyDictionary HttpMethodProperties { get; } = new Dictionary() 9 | { 10 | { HttpMethod.Delete, "global::System.Net.Http.HttpMethod.Delete" }, 11 | { HttpMethod.Get, "global::System.Net.Http.HttpMethod.Get" }, 12 | { HttpMethod.Head, "global::System.Net.Http.HttpMethod.Head" }, 13 | { HttpMethod.Options, "global::System.Net.Http.HttpMethod.Options" }, 14 | { HttpMethod.Post, "global::System.Net.Http.HttpMethod.Post" }, 15 | { HttpMethod.Put, "global::System.Net.Http.HttpMethod.Put" }, 16 | { HttpMethod.Trace, "global::System.Net.Http.HttpMethod.Trace" }, 17 | { PatchAttribute.PatchMethod, "global::RestEase.PatchAttribute.PatchMethod" }, 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/RestEase.SourceGenerator/RestEase.SourceGenerator.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 10.0 5 | enable 6 | 7 | 0.0.0 8 | ../../NuGet 9 | false 10 | RestEase.SourceGenerator 11 | REST;JSON;SourceGenerator 12 | Copyright © Antony Male 2015-2022 13 | README.md 14 | icon.png 15 | https://github.com/canton7/RestEase 16 | MIT 17 | git 18 | https://github.com/canton7/RestEase 19 | Antony Male 20 | true 21 | 22 | Source generator for RestEase: the easy-to-use typesafe REST API client library, which is simple and customisable 23 | 24 | Generates implementations for RestEase interfaces at compile-time, to provide error-checking, faster execution, and support for platforms which don't support runtime code generation (such as iOS and .NET Native). 25 | 26 | You must be using the .NET 5 SDK (or higher) to use this. You will also need to install the RestEase package. 27 | 28 | For more details, see https://github.com/canton7/RestEase#using-resteasesourcegenerator 29 | 30 | true 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | <_Parameter1>$(VersionPrefix) 49 | <_Parameter2>2.0 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/RestEase.SourceGenerator/RestEaseSourceGenerator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using RestEase.SourceGenerator.Implementation; 3 | 4 | namespace RestEase.SourceGenerator 5 | { 6 | [Generator] 7 | public class RestEaseSourceGenerator : ISourceGenerator 8 | { 9 | public void Execute(GeneratorExecutionContext context) 10 | { 11 | new Processor(context).Process(); 12 | } 13 | 14 | public void Initialize(GeneratorInitializationContext context) 15 | { 16 | context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/RestEase.UnitTests/Extensions/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace RestEase.UnitTests.Extensions 5 | { 6 | public static class TypeExtensions 7 | { 8 | #if NETCOREAPP1_0 9 | public static Type[] GetGenericParameterConstraints(this Type type) 10 | { 11 | return type.GetTypeInfo().GetGenericParameterConstraints(); 12 | } 13 | #endif 14 | 15 | public static GenericParameterAttributes GetGenericParameterAttributes(this Type type) 16 | { 17 | #if NETCOREAPP1_0 18 | return type.GetTypeInfo().GenericParameterAttributes; 19 | #else 20 | return type.GenericParameterAttributes; 21 | #endif 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/RestEase.UnitTests/ImplementationFactoryTests/AllowAnyStatusCodeTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Xunit; 3 | using Xunit.Abstractions; 4 | 5 | namespace RestEase.UnitTests.ImplementationFactoryTests 6 | { 7 | public class AllowAnyStatusCodeTests : ImplementationFactoryTestsBase 8 | { 9 | public interface IHasNoAllowAnyStatusCode 10 | { 11 | [Get("foo")] 12 | Task FooAsync(); 13 | } 14 | 15 | public interface IHasMethodWithAllowAnyStatusCode 16 | { 17 | [AllowAnyStatusCode] 18 | [Get("foo")] 19 | Task FooAsync(); 20 | } 21 | 22 | [AllowAnyStatusCode] 23 | public interface IHasAllowAnyStatusCode 24 | { 25 | [Get("foo")] 26 | Task NoAttributeAsync(); 27 | 28 | [Get("bar")] 29 | [AllowAnyStatusCode(false)] 30 | Task HasAttributeAsync(); 31 | } 32 | 33 | public AllowAnyStatusCodeTests(ITestOutputHelper output) : base(output) { } 34 | 35 | [Fact] 36 | public void DefaultsToFalse() 37 | { 38 | var requestInfo = this.Request(x => x.FooAsync()); 39 | 40 | Assert.False(requestInfo.AllowAnyStatusCode); 41 | } 42 | 43 | [Fact] 44 | public void RespectsAllowAnyStatusCodeOnMethod() 45 | { 46 | var requestInfo = this.Request(x => x.FooAsync()); 47 | 48 | Assert.True(requestInfo.AllowAnyStatusCode); 49 | } 50 | 51 | [Fact] 52 | public void RespectsAllowAnyStatusCodeOnInterface() 53 | { 54 | var requestInfo = this.Request(x => x.NoAttributeAsync()); 55 | 56 | Assert.True(requestInfo.AllowAnyStatusCode); 57 | } 58 | 59 | [Fact] 60 | public void AllowsAllowAnyStatusCodeOnMethodToOverrideThatOnInterface() 61 | { 62 | var requestInfo = this.Request(x => x.HasAttributeAsync()); 63 | 64 | Assert.False(requestInfo.AllowAnyStatusCode); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/RestEase.UnitTests/ImplementationFactoryTests/BaseAddressTests.cs: -------------------------------------------------------------------------------- 1 | using RestEase.Implementation; 2 | using System.Threading.Tasks; 3 | using Xunit; 4 | using Xunit.Abstractions; 5 | 6 | namespace RestEase.UnitTests.ImplementationFactoryTests 7 | { 8 | public class BaseAddressTests : ImplementationFactoryTestsBase 9 | { 10 | public interface IHasNoBaseAddress 11 | { 12 | [Get("path")] 13 | Task FooAsync(); 14 | } 15 | 16 | [BaseAddress("http://foo/bar/baz")] 17 | public interface IHasSimpleBaseAddress 18 | { 19 | [Get("path")] 20 | Task FooAsync(); 21 | } 22 | 23 | [BaseAddress("http://foo/{bar}/baz")] 24 | public interface IHasBaseAddressWithPlaceholderWithoutProperty 25 | { 26 | [Get("{bar}")] 27 | Task FooAsync([Path] string bar); 28 | } 29 | 30 | [BaseAddress("http://foo/{bar}/baz")] 31 | public interface IHasBaseAddressWithPlaceholder 32 | { 33 | [Path("bar")] 34 | string Bar { get; set; } 35 | 36 | [Get] 37 | Task FooAsync(); 38 | } 39 | 40 | [BaseAddress("foo")] 41 | public interface IHasRelativeBaseAddress 42 | { 43 | } 44 | 45 | public BaseAddressTests(ITestOutputHelper output) : base(output) { } 46 | 47 | [Fact] 48 | public void DefaultsToNull() 49 | { 50 | var requestInfo = this.Request(x => x.FooAsync()); 51 | 52 | Assert.Null(requestInfo.BaseAddress); 53 | } 54 | 55 | [Fact] 56 | public void ForwardsSimpleBaseAddress() 57 | { 58 | var requestInfo = this.Request(x => x.FooAsync()); 59 | 60 | Assert.Equal("http://foo/bar/baz", requestInfo.BaseAddress); 61 | } 62 | 63 | [Fact] 64 | public void ThrowsIfBaseAddressPlaceholderMissingAddressProperty() 65 | { 66 | this.VerifyDiagnostics( 67 | // (1,10): Error REST035: Unable to find a [Path("bar")] property for the path placeholder '{bar}' in base address 'http://foo/{bar}/baz' 68 | // BaseAddress("http://foo/{bar}/baz") 69 | Diagnostic(DiagnosticCode.MissingPathPropertyForBaseAddressPlaceholder, @"BaseAddress(""http://foo/{bar}/baz"")").WithLocation(1, 10) 70 | ); 71 | } 72 | 73 | [Fact] 74 | public void FowardsBaseAddressWithPlaceholder() 75 | { 76 | var requestInfo = this.Request(x => x.FooAsync()); 77 | 78 | Assert.Equal("http://foo/{bar}/baz", requestInfo.BaseAddress); 79 | } 80 | 81 | [Fact] 82 | public void ThrowsIfBaseAddressIsNotAbsolute() 83 | { 84 | this.VerifyDiagnostics( 85 | // (1,10): Error REST036: Base address 'foo' must be an absolute URI 86 | // BaseAddress("foo") 87 | Diagnostic(DiagnosticCode.BaseAddressMustBeAbsolute, @"BaseAddress(""foo"")").WithLocation(1, 10) 88 | ); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/RestEase.UnitTests/ImplementationFactoryTests/BasePathTests.cs: -------------------------------------------------------------------------------- 1 | using RestEase.Implementation; 2 | using System.Threading.Tasks; 3 | using Xunit; 4 | using Xunit.Abstractions; 5 | 6 | namespace RestEase.UnitTests.ImplementationFactoryTests 7 | { 8 | public class BasePathTests : ImplementationFactoryTestsBase 9 | { 10 | public interface IHasNoBasePath 11 | { 12 | [Get("path")] 13 | Task FooAsync(); 14 | } 15 | 16 | [BasePath("foo/bar/baz")] 17 | public interface IHasSimpleBasePath 18 | { 19 | [Get("path")] 20 | Task FooAsync(); 21 | } 22 | 23 | [BasePath("foo/{bar}/baz")] 24 | public interface IHasBasePathWithPlaceholderWithoutProperty 25 | { 26 | [Get("{bar}")] 27 | Task FooAsync([Path] string bar); 28 | } 29 | 30 | [BasePath("foo/{bar}/baz")] 31 | public interface IHasBasePathWithPlaceholder 32 | { 33 | [Path("bar")] 34 | string Bar { get; set; } 35 | 36 | [Get] 37 | Task FooAsync(); 38 | } 39 | 40 | public BasePathTests(ITestOutputHelper output) : base(output) { } 41 | 42 | [Fact] 43 | public void DefaultsToNull() 44 | { 45 | var requestInfo = this.Request(x => x.FooAsync()); 46 | 47 | Assert.Null(requestInfo.BasePath); 48 | } 49 | 50 | [Fact] 51 | public void ForwardsSimpleBasePath() 52 | { 53 | var requestInfo = this.Request(x => x.FooAsync()); 54 | 55 | Assert.Equal("foo/bar/baz", requestInfo.BasePath); 56 | } 57 | 58 | [Fact] 59 | public void ThrowsIfBasePathPlaceholderMissingPathProperty() 60 | { 61 | this.VerifyDiagnostics( 62 | // (1,10): Error REST001: Unable to find a [Path("bar")] property for the path placeholder '{bar}' in base path 'foo/{bar}/baz' 63 | // BasePath("foo/{bar}/baz") 64 | Diagnostic(DiagnosticCode.MissingPathPropertyForBasePathPlaceholder, @"BasePath(""foo/{bar}/baz"")").WithLocation(1, 10) 65 | ); 66 | } 67 | 68 | [Fact] 69 | public void FowardsBasePathWithPlaceholder() 70 | { 71 | var requestInfo = this.Request(x => x.FooAsync()); 72 | 73 | Assert.Equal("foo/{bar}/baz", requestInfo.BasePath); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/RestEase.UnitTests/ImplementationFactoryTests/CancellationTokenTests.cs: -------------------------------------------------------------------------------- 1 | using RestEase.Implementation; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | namespace RestEase.UnitTests.ImplementationFactoryTests 9 | { 10 | public class CancellationTokenTests : ImplementationFactoryTestsBase 11 | { 12 | public interface ICancellationTokenOnlyNoReturn 13 | { 14 | [Get("baz")] 15 | Task BazAsync(CancellationToken cancellationToken); 16 | } 17 | 18 | public interface ITwoCancellationTokens 19 | { 20 | [Get("yay")] 21 | Task YayAsync(CancellationToken cancellationToken1, CancellationToken cancellationToken2); 22 | } 23 | 24 | public interface IHasCancellationTokenWithAttribute 25 | { 26 | [Get] 27 | Task FooAsync([Query] CancellationToken param); 28 | } 29 | 30 | public CancellationTokenTests(ITestOutputHelper output) : base(output) { } 31 | 32 | [Fact] 33 | public void CancellationTokenOnlyNoReturnCallsCorrectly() 34 | { 35 | var cts = new CancellationTokenSource(); 36 | var requestInfo = this.Request(x => x.BazAsync(cts.Token)); 37 | 38 | Assert.Equal(cts.Token, requestInfo.CancellationToken); 39 | Assert.Equal(HttpMethod.Get, requestInfo.Method); 40 | Assert.Empty(requestInfo.QueryParams); 41 | Assert.Equal("baz", requestInfo.Path); 42 | } 43 | 44 | [Fact] 45 | public void ThrowsIfTwoCancellationTokens() 46 | { 47 | this.VerifyDiagnostics( 48 | // (4,27): Error REST001: Method 'YayAsync': only a single CancellationToken parameter is allowed, found a duplicate parameter 'cancellationToken2' 49 | // CancellationToken cancellationToken1 50 | Diagnostic(DiagnosticCode.MultipleCancellationTokenParameters, "CancellationToken cancellationToken1").WithLocation(4, 27).WithLocation(4, 65) 51 | ); 52 | } 53 | 54 | [Fact] 55 | public void ThrowsIfHasAttribute() 56 | { 57 | this.VerifyDiagnostics( 58 | // (4,27): Error REST026: CancellationToken parameter 'param' must have zero attributes 59 | // [Query] CancellationToken param 60 | Diagnostic(DiagnosticCode.CancellationTokenMustHaveZeroAttributes, "[Query] CancellationToken param") 61 | .WithLocation(4, 27) 62 | ); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/RestEase.UnitTests/ImplementationFactoryTests/DisposableTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Xunit; 4 | using Xunit.Abstractions; 5 | 6 | namespace RestEase.UnitTests.ImplementationFactoryTests 7 | { 8 | public class DisposableTests : ImplementationFactoryTestsBase 9 | { 10 | public interface IDisposableApi : IDisposable 11 | { 12 | [Get("foo")] 13 | Task FooAsync(); 14 | } 15 | 16 | public DisposableTests(ITestOutputHelper output) : base(output) { } 17 | 18 | [Fact] 19 | public void DisposingDisposableImplementationDisposesRequester() 20 | { 21 | var implementation = this.CreateImplementation(); 22 | 23 | this.Requester.Setup(x => x.Dispose()).Verifiable(); 24 | implementation.Dispose(); 25 | this.Requester.Verify(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/RestEase.UnitTests/ImplementationFactoryTests/EdgeCaseTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using RestEase; 6 | using Xunit; 7 | using Xunit.Abstractions; 8 | 9 | #pragma warning disable CA1050 // Declare types in namespaces 10 | public interface IApiOutsideNamespace 11 | { 12 | [Get] 13 | Task FooAsync(); 14 | } 15 | #pragma warning restore CA1050 // Declare types in namespaces 16 | 17 | namespace RestEase.UnitTests.ImplementationFactoryTests 18 | { 19 | public class EdgeCaseTests : ImplementationFactoryTestsBase 20 | { 21 | public interface IHasNrts 22 | { 23 | [Query] 24 | string? Foo { get; set; } 25 | 26 | [Get] 27 | Task FooAsync(string? arg); 28 | } 29 | 30 | public EdgeCaseTests(ITestOutputHelper output) : base(output) { } 31 | 32 | [Fact] 33 | public void HandlesImplementationOutsideOfNamespace() 34 | { 35 | this.Request(x => x.FooAsync()); 36 | } 37 | 38 | [Fact] 39 | public void DoesNotGenerateNrtRelatedWarnings() 40 | { 41 | // We're looking for compiler warnings 42 | this.CreateImplementation(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/RestEase.UnitTests/ImplementationFactoryTests/Helpers/DiagnosticResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using RestEase.Implementation; 4 | 5 | namespace RestEase.UnitTests.ImplementationFactoryTests.Helpers 6 | { 7 | public class DiagnosticResult 8 | { 9 | public DiagnosticCode Code { get; } 10 | public string SquiggledText { get; } 11 | public bool IsError { get; set; } = true; 12 | public List Locations { get; } = new List(); 13 | 14 | public DiagnosticResult(DiagnosticCode code, string squiggledText) 15 | { 16 | this.Code = code; 17 | this.SquiggledText = squiggledText; 18 | } 19 | 20 | public DiagnosticResult WithLocation(int line, int column) 21 | { 22 | this.Locations.Add(new DiagnosticResultLocation(line, column)); 23 | return this; 24 | } 25 | 26 | } 27 | 28 | public readonly struct DiagnosticResultLocation 29 | { 30 | public int Line { get; } 31 | public int Column { get; } 32 | 33 | public DiagnosticResultLocation(int line, int column) 34 | { 35 | if (column < -1) 36 | { 37 | throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1"); 38 | } 39 | 40 | this.Line = line; 41 | this.Column = column; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/RestEase.UnitTests/ImplementationFactoryTests/MethodInfoTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | namespace RestEase.UnitTests.ImplementationFactoryTests 9 | { 10 | public class MethodInfoTests : ImplementationFactoryTestsBase 11 | { 12 | public interface IHasOverloads 13 | { 14 | [Get] 15 | Task FooAsync(string foo); 16 | 17 | [Get] 18 | Task FooAsync(int foo); 19 | } 20 | 21 | public interface IParent 22 | { 23 | [Get] 24 | Task FooAsync(string bar); 25 | } 26 | public interface IChild : IParent { } 27 | 28 | public interface IGenericParent 29 | { 30 | [Get] 31 | Task FooAsync(T bar); 32 | } 33 | 34 | public interface IChildWithTwoGenericParents : IGenericParent, IGenericParent { } 35 | 36 | public interface IHasTwoMethodsWithDifferentArity 37 | { 38 | [Get] 39 | Task FooAsync(); 40 | 41 | [Get] 42 | Task FooAsync(); 43 | } 44 | 45 | public interface IHasGenericParameter 46 | { 47 | [Get] 48 | Task FooAsync(T arg); 49 | } 50 | 51 | public MethodInfoTests(ITestOutputHelper output) : base(output) { } 52 | 53 | [Fact] 54 | public void GetsMethodInfoForOverloadedMethods() 55 | { 56 | var intOverload = this.Request(x => x.FooAsync(1)).MethodInfo; 57 | var expectedIntOverload = typeof(IHasOverloads).GetTypeInfo().GetMethod("FooAsync", new Type[] { typeof(int) }); 58 | Assert.Equal(expectedIntOverload, intOverload); 59 | 60 | var stringOverload = this.Request(x => x.FooAsync("test")).MethodInfo; 61 | var expectedstringOverload = typeof(IHasOverloads).GetTypeInfo().GetMethod("FooAsync", new Type[] { typeof(string) }); 62 | Assert.Equal(expectedstringOverload, stringOverload); 63 | } 64 | 65 | [Fact] 66 | public void GetsMethodInfoFromParentInterface() 67 | { 68 | var methodInfo = this.Request(x => x.FooAsync("testy")).MethodInfo; 69 | var expected = typeof(IParent).GetTypeInfo().GetMethod("FooAsync"); 70 | Assert.Equal(expected, methodInfo); 71 | } 72 | 73 | [Fact] 74 | public void GetsCorrectGenericMethod() 75 | { 76 | var intOverload = this.Request(x => x.FooAsync(1)).MethodInfo; 77 | var expectedIntOverload = typeof(IGenericParent).GetTypeInfo().GetMethod("FooAsync"); 78 | Assert.Equal(expectedIntOverload, intOverload); 79 | 80 | var stringOverload = this.Request(x => x.FooAsync("test")).MethodInfo; 81 | var expectedstringOverload = typeof(IGenericParent).GetTypeInfo().GetMethod("FooAsync"); 82 | Assert.Equal(expectedstringOverload, stringOverload); 83 | } 84 | 85 | [Fact] 86 | public void GetsMethodWithCorrectArity() 87 | { 88 | var zeroArity = this.Request(x => x.FooAsync()).MethodInfo; 89 | var expectedZeroArity = typeof(IHasTwoMethodsWithDifferentArity) 90 | .GetTypeInfo().GetDeclaredMethods("FooAsync").FirstOrDefault(x => x.GetGenericArguments().Length == 0); 91 | Assert.Equal(expectedZeroArity, zeroArity); 92 | 93 | var oneArity = this.Request(x => x.FooAsync()).MethodInfo; 94 | var expectedOneArity = typeof(IHasTwoMethodsWithDifferentArity) 95 | .GetTypeInfo().GetDeclaredMethods("FooAsync").FirstOrDefault(x => x.GetGenericArguments().Length == 1); 96 | Assert.Equal(expectedOneArity, oneArity); 97 | 98 | Assert.NotEqual(zeroArity, oneArity); 99 | } 100 | 101 | [Fact] 102 | public void GetsMethodWithGenericParameter() 103 | { 104 | var methodInfo = this.Request(x => x.FooAsync(3)).MethodInfo; 105 | var expected = typeof(IHasGenericParameter).GetTypeInfo().GetDeclaredMethods("FooAsync").Single(); 106 | Assert.Equal(expected, methodInfo); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/RestEase.UnitTests/ImplementationFactoryTests/MultipleParameterAttributesTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using RestEase.Implementation; 3 | using Xunit; 4 | using Xunit.Abstractions; 5 | 6 | namespace RestEase.UnitTests.ImplementationFactoryTests 7 | { 8 | public class MultipleParameterAttributesTests : ImplementationFactoryTestsBase 9 | { 10 | public interface IHasMultipleParameterAttributes 11 | { 12 | [Get("/{bar}")] 13 | Task FooAsync([Query, Header("header1"), Body, Path, HttpRequestMessageProperty("prop1")] string bar); 14 | } 15 | 16 | public interface IHasMethodParameterWithConflictAttributes 17 | { 18 | [Get] 19 | Task FooAsync([Query, RawQueryString] string foo); 20 | } 21 | 22 | public MultipleParameterAttributesTests(ITestOutputHelper output) : base(output) { } 23 | 24 | [Fact] 25 | public void HandlesMultipleParameterAttributes() 26 | { 27 | var requestInfo = this.Request(x => x.FooAsync("boom")); 28 | 29 | Assert.Single(requestInfo.QueryParams); 30 | Assert.Single(requestInfo.HeaderParams); 31 | Assert.NotNull(requestInfo.BodyParameterInfo); 32 | Assert.Single(requestInfo.PathParams); 33 | Assert.Single(requestInfo.HttpRequestMessageProperties); 34 | } 35 | 36 | [Fact] 37 | public void ThrowsIfQueryAttributeWithRawQueryStringAttribute() 38 | { 39 | this.VerifyDiagnostics( 40 | // (4,27): Error REST040: Method 'FooAsync': [Query] parameter must not specified along with [RawQueryString] 41 | // [Query, RawQueryString] string foo 42 | Diagnostic(DiagnosticCode.QueryConflictWithRawQueryString, @"[Query, RawQueryString] string foo") 43 | .WithLocation(4, 27) 44 | ); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/RestEase.UnitTests/ImplementationFactoryTests/RawQueryStringTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Moq; 4 | using Xunit; 5 | using System.Linq; 6 | using Xunit.Abstractions; 7 | 8 | namespace RestEase.UnitTests.ImplementationFactoryTests 9 | { 10 | public class RawQueryStringTests : ImplementationFactoryTestsBase 11 | { 12 | public class HasToString : IFormattable 13 | { 14 | public IFormatProvider LastFormatProvider { get; set; } 15 | 16 | public string ToString(string format, IFormatProvider formatProvider) 17 | { 18 | this.LastFormatProvider = formatProvider; 19 | 3.ToString(formatProvider); // Just call this 20 | return "HasToString"; 21 | } 22 | 23 | public override string ToString() => "HasToString"; 24 | } 25 | 26 | public interface ISimpleRawQueryString 27 | { 28 | [Get] 29 | Task FooAsync([RawQueryString] string rawQueryString); 30 | } 31 | 32 | public interface ITwoRawQueryStrings 33 | { 34 | [Get] 35 | Task FooAsync([RawQueryString] string one, [RawQueryString] string two); 36 | } 37 | 38 | public interface ICustomRawQueryString 39 | { 40 | [Get] 41 | Task FooAsync([RawQueryString] HasToString value); 42 | } 43 | 44 | public RawQueryStringTests(ITestOutputHelper output) : base(output) { } 45 | 46 | [Fact] 47 | public void AddsRawQueryParam() 48 | { 49 | var requestInfo = this.Request(x => x.FooAsync("test=test2")); 50 | 51 | Assert.NotNull(requestInfo.RawQueryParameters); 52 | Assert.Single(requestInfo.RawQueryParameters); 53 | Assert.Equal("test=test2", requestInfo.RawQueryParameters.First().SerializeToString(null)); 54 | } 55 | 56 | [Fact] 57 | public void AddsTwoRawQueryParam() 58 | { 59 | var requestInfo = this.Request(x => x.FooAsync("test=test2", "test3=test4")); 60 | 61 | Assert.NotNull(requestInfo.RawQueryParameters); 62 | var rawQueryParameters = requestInfo.RawQueryParameters.ToList(); 63 | Assert.Equal(2, rawQueryParameters.Count); 64 | Assert.Equal("test=test2", rawQueryParameters[0].SerializeToString(null)); 65 | Assert.Equal("test3=test4", rawQueryParameters[1].SerializeToString(null)); 66 | } 67 | 68 | [Fact] 69 | public void CallsToStringOnParam() 70 | { 71 | var requestInfo = this.Request(x => x.FooAsync(new HasToString())); 72 | 73 | Assert.NotNull(requestInfo.RawQueryParameters); 74 | var rawQueryParameters = requestInfo.RawQueryParameters.ToList(); 75 | Assert.Single(rawQueryParameters); 76 | Assert.Equal("HasToString", rawQueryParameters[0].SerializeToString(null)); 77 | } 78 | 79 | [Fact] 80 | public void SerializeToStringUsesGivenFormatProvider() 81 | { 82 | var hasToString = new HasToString(); 83 | var requestInfo = this.Request(x => x.FooAsync(hasToString)); 84 | 85 | var formatProvider = new Mock(); 86 | 87 | Assert.Single(requestInfo.RawQueryParameters); 88 | requestInfo.RawQueryParameters.First().SerializeToString(formatProvider.Object); 89 | 90 | Assert.Equal(formatProvider.Object, hasToString.LastFormatProvider); 91 | //formatProvider.Verify(x => x.GetFormat(typeof(NumberFormatInfo))); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/RestEase.UnitTests/ImplementationFactoryTests/RequesterPropertyTests.cs: -------------------------------------------------------------------------------- 1 | using RestEase.Implementation; 2 | using Xunit; 3 | using Xunit.Abstractions; 4 | 5 | namespace RestEase.UnitTests.ImplementationFactoryTests 6 | { 7 | public class RequesterPropertyTests : ImplementationFactoryTestsBase 8 | { 9 | public interface IHasRequesterProperty 10 | { 11 | IRequester Requester { get; } 12 | } 13 | 14 | public interface IHasBadlyNamedRequesterProperty 15 | { 16 | #pragma warning disable IDE1006 // Naming Styles 17 | IRequester @event { get; } 18 | #pragma warning restore IDE1006 // Naming Styles 19 | } 20 | 21 | public interface IHasSet 22 | { 23 | IRequester Requester { get; set; } 24 | } 25 | 26 | public interface IHasNoGet 27 | { 28 | IRequester Requester { set; } 29 | } 30 | 31 | public interface ITwoRequesterProperties 32 | { 33 | IRequester Requester1 { get; } 34 | IRequester Requester2 { get; } 35 | } 36 | 37 | public interface IHasRequesterPropertyWithAttribute 38 | { 39 | [Header("Foo")] 40 | [HttpRequestMessageProperty] 41 | IRequester Requester { get; } 42 | } 43 | 44 | public RequesterPropertyTests(ITestOutputHelper output) : base(output) { } 45 | 46 | [Fact] 47 | public void HandlesRequesterProperty() 48 | { 49 | var implementation = this.CreateImplementation(); 50 | Assert.Equal(this.Requester.Object, implementation.Requester); 51 | } 52 | 53 | [Fact] 54 | public void HandlesBadlyNamedRequesterProperty() 55 | { 56 | var implementation = this.CreateImplementation(); 57 | Assert.Equal(this.Requester.Object, implementation.@event); 58 | } 59 | 60 | [Fact] 61 | public void ThrowsIfHasSet() 62 | { 63 | this.VerifyDiagnostics( 64 | // (3,24): Error REST016: Property must have a getter but not a setter 65 | // Requester 66 | Diagnostic(DiagnosticCode.PropertyMustBeReadOnly, "Requester").WithLocation(3, 24) 67 | ); 68 | } 69 | 70 | [Fact] 71 | public void ThrowsIfHasNoGet() 72 | { 73 | this.VerifyDiagnostics( 74 | // (3,24): Error REST016: Property must have a getter but not a setter 75 | // Requester 76 | Diagnostic(DiagnosticCode.PropertyMustBeReadOnly, "Requester").WithLocation(3, 24) 77 | ); 78 | } 79 | 80 | [Fact] 81 | public void ThrowsIfTwoRequesters() 82 | { 83 | this.VerifyDiagnostics( 84 | // (4,24): Error REST017: There must not be more than one property of type IRequester 85 | // Requester2 86 | Diagnostic(DiagnosticCode.MultipleRequesterProperties, "Requester2").WithLocation(4, 24) 87 | ); 88 | } 89 | 90 | [Fact] 91 | public void ThrowsIfHasAttributes() 92 | { 93 | this.VerifyDiagnostics( 94 | // (3,14): Error REST021: IRequester property must not have any attribtues 95 | // Header("Foo") 96 | Diagnostic(DiagnosticCode.RequesterPropertyMustHaveZeroAttributes, @"Header(""Foo"")") 97 | .WithLocation(3, 14).WithLocation(4, 14) 98 | ); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/RestEase.UnitTests/ImplementationFactoryTests/ThreadSafetyTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using RestEase.Implementation; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace RestEase.UnitTests.ImplementationFactoryTests 7 | { 8 | #if !SOURCE_GENERATOR 9 | public class ThreadSafetyTests 10 | { 11 | public interface ISomeApi 12 | { 13 | [Get("foo")] 14 | Task GetFooAsync(); 15 | } 16 | 17 | [Fact] 18 | public void CreateImplementationIsThreadSafe() 19 | { 20 | // Test passes if it does not throw "Duplicate type name within an assembly" 21 | 22 | var factory = new ImplementationFactory(useSourceGenerator: false); 23 | var requester = new Mock(); 24 | 25 | // We can't really test this well... Just try lots, and see if we have any exceptions 26 | for (int i = 0; i < 100; i++) 27 | { 28 | var tasks = new Task[10]; 29 | for (int j = 0; j < tasks.Length; j++) 30 | { 31 | tasks[j] = Task.Run(() => factory.CreateImplementation(requester.Object)); 32 | } 33 | 34 | Task.WaitAll(tasks); 35 | } 36 | } 37 | } 38 | #endif 39 | } 40 | -------------------------------------------------------------------------------- /src/RestEase.UnitTests/RequesterTests/DictionaryIteratorTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using RestEase.Implementation; 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Dynamic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using Xunit; 10 | 11 | namespace RestEase.UnitTests.RequesterTests 12 | { 13 | public class DictionaryIteratorTests 14 | { 15 | [Fact] 16 | public void CanIterateIDictionary() 17 | { 18 | Assert.True(DictionaryIterator.CanIterate(typeof(IDictionary))); 19 | } 20 | 21 | [Fact] 22 | public void CanIterateIDictionarySubclass() 23 | { 24 | Assert.True(DictionaryIterator.CanIterate(typeof(Hashtable))); 25 | } 26 | 27 | [Fact] 28 | public void CanIterateIDictionaryKV() 29 | { 30 | Assert.True(DictionaryIterator.CanIterate(typeof(IDictionary))); 31 | } 32 | 33 | [Fact] 34 | public void CanIterateIDictionaryKVSubclass() 35 | { 36 | Assert.True(DictionaryIterator.CanIterate(typeof(Dictionary))); 37 | } 38 | 39 | [Fact] 40 | public void IteratesIDictionary() 41 | { 42 | var dict = new Hashtable() 43 | { 44 | { "k1", 1 }, 45 | { "k2", "v2" } 46 | }; 47 | var actual = DictionaryIterator.Iterate(dict).OrderBy(x => x.Key).ToList(); 48 | var expected = new List>() 49 | { 50 | new KeyValuePair("k1", 1), 51 | new KeyValuePair("k2", "v2"), 52 | }; 53 | Assert.Equal(expected, actual); 54 | } 55 | 56 | [Fact] 57 | public void IteratesIDictionaryKV() 58 | { 59 | dynamic dict = new ExpandoObject(); // Implements IDictionary, but not IDictionary 60 | dict.k1 = 1; 61 | dict.k2 = "v2"; 62 | var actual = DictionaryIterator.Iterate((object)dict).OrderBy(x => x.Key).ToList(); 63 | var expected = new List>() 64 | { 65 | new KeyValuePair("k1", 1), 66 | new KeyValuePair("k2", "v2"), 67 | }; 68 | Assert.Equal(expected, actual); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/RestEase.UnitTests/RequesterTests/PublicRequester.cs: -------------------------------------------------------------------------------- 1 | using RestEase; 2 | using RestEase.Implementation; 3 | using System; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | 7 | namespace RestEase.UnitTests.RequesterTests 8 | { 9 | public class PublicRequester : Requester 10 | { 11 | public PublicRequester(HttpClient httpClient) 12 | : base(httpClient) 13 | { } 14 | 15 | public Uri ConstructUri(string relativePath, IRequestInfo requestInfo) 16 | { 17 | return base.ConstructUri(null, null, relativePath, requestInfo); 18 | } 19 | 20 | public new Uri ConstructUri(string baseAddress, string basePath, string relativePath, IRequestInfo requestInfo) 21 | { 22 | return base.ConstructUri(baseAddress, basePath, relativePath, requestInfo); 23 | } 24 | 25 | public new string SubstitutePathParameters(string path, IRequestInfo requestInfo) 26 | { 27 | return base.SubstitutePathParameters(path, requestInfo); 28 | } 29 | 30 | public new HttpContent ConstructContent(IRequestInfo requestInfo) 31 | { 32 | return base.ConstructContent(requestInfo); 33 | } 34 | 35 | public new void ApplyHeaders(IRequestInfo requestInfo, HttpRequestMessage requestMessage) 36 | { 37 | base.ApplyHeaders(requestInfo, requestMessage); 38 | } 39 | 40 | public new Task SendRequestAsync(IRequestInfo requestInfo, bool readBody) 41 | { 42 | return base.SendRequestAsync(requestInfo, readBody); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/RestEase.UnitTests/RestClientTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using RestEase; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace RestEase.UnitTests 10 | { 11 | public class RestClientTests 12 | { 13 | public interface ISomeApi 14 | { 15 | [Get("foo")] 16 | Task FooAsync(); 17 | } 18 | 19 | [Fact] 20 | public void NonGenericInstanceForReturnsSameAsGenericFor() 21 | { 22 | var generic = RestClient.For("http://example.com"); 23 | object nonGeneric = new RestClient("http://example.com").For(typeof(ISomeApi)); 24 | Assert.Equal(generic.GetType(), nonGeneric.GetType()); 25 | } 26 | 27 | [Fact] 28 | public void NonGenericStaticForReturnsSameAsGenericFor() 29 | { 30 | var requester = new Mock().Object; 31 | var generic = RestClient.For("http://example.com"); 32 | object nonGeneric = RestClient.For(typeof(ISomeApi), requester); 33 | Assert.Equal(generic.GetType(), nonGeneric.GetType()); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/RestEase.UnitTests/RestEase.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 0.0.0 5 | 7 | net5.0;netcoreapp3.0;netcoreapp2.0;netcoreapp1.0;net452 8 | false 9 | RestEase.UnitTests 10 | 10.0 11 | annotations 12 | 13 | false 14 | 15 | true 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | all 27 | runtime; build; native; contentfiles; analyzers; buildtransitive 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/RestEase.UnitTests/StringEnumRequestPathParamSerializerTests.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Runtime.Serialization; 3 | using RestEase; 4 | using Xunit; 5 | 6 | #if !NET452 7 | using System.ComponentModel; 8 | #endif 9 | 10 | namespace RestEase.UnitTests 11 | { 12 | public class StringEnumRequestPathParamSerializerTests 13 | { 14 | private static readonly RequestPathParamSerializerInfo info = new(null, null, null); 15 | 16 | private static readonly RequestPathParamSerializer serializer = new StringEnumRequestPathParamSerializer(); 17 | 18 | private enum Foo 19 | { 20 | Bar, 21 | 22 | [EnumMember(Value = "enum_member")] 23 | Baz, 24 | 25 | [Display(Name = "display")] 26 | Fizz, 27 | 28 | #if !NET452 && !NETCOREAPP3_0 && !NET5_0 29 | [DisplayName("display_name")] 30 | #endif 31 | Buzz, 32 | 33 | [EnumMember(Value = "all_enum_member")] 34 | [Display(Name = "all_display")] 35 | #if !NET452 && !NETCOREAPP3_0 && !NET5_0 36 | [DisplayName("all_display_name")] 37 | #endif 38 | All, 39 | 40 | [Display(Name = "display+name_display")] 41 | #if !NET452 && !NETCOREAPP3_0 && !NET5_0 42 | [DisplayName("display+name_display_name")] 43 | #endif 44 | DisplayAndName 45 | } 46 | 47 | [Fact] 48 | public void UndecoratedEnumUsesToString() 49 | { 50 | string serialized = serializer.SerializePathParam(Foo.Bar, info); 51 | Assert.Equal("Bar", serialized); 52 | } 53 | 54 | [Fact] 55 | public void EnumMemberDecoratedUsesValue() 56 | { 57 | string serialized = serializer.SerializePathParam(Foo.Baz, info); 58 | 59 | #if NETCOREAPP1_0 60 | const string expected = "Baz"; 61 | #else 62 | const string expected = "enum_member"; 63 | #endif 64 | 65 | Assert.Equal(expected, serialized); 66 | } 67 | 68 | [Fact] 69 | public void DisplayDecoratedUsesName() 70 | { 71 | string serialized = serializer.SerializePathParam(Foo.Fizz, info); 72 | 73 | const string expected = "display"; 74 | 75 | Assert.Equal(expected, serialized); 76 | } 77 | 78 | [Fact] 79 | public void DisplayNameDecoratedUsesName() 80 | { 81 | string serialized = serializer.SerializePathParam(Foo.Buzz, info); 82 | 83 | #if NETCOREAPP2_0 84 | const string expected = "display_name"; 85 | #else 86 | const string expected = "Buzz"; 87 | #endif 88 | 89 | Assert.Equal(expected, serialized); 90 | } 91 | 92 | [Fact] 93 | public void PrioritisesEnumMemberAboveAll() 94 | { 95 | string serialized = serializer.SerializePathParam(Foo.All, info); 96 | 97 | #if NETCOREAPP1_0 98 | const string expected = "all_display"; 99 | #else 100 | const string expected = "all_enum_member"; 101 | #endif 102 | 103 | Assert.Equal(expected, serialized); 104 | } 105 | 106 | [Fact] 107 | public void PrioritisesDisplayNameOverDisplay() 108 | { 109 | string serialized = serializer.SerializePathParam(Foo.DisplayAndName, info); 110 | 111 | #if NETCOREAPP2_0 112 | const string expected = "display+name_display_name"; 113 | #else 114 | const string expected = "display+name_display"; 115 | #endif 116 | 117 | Assert.Equal(expected, serialized); 118 | } 119 | 120 | [Fact] 121 | public void NonEnumValueUsesToString() 122 | { 123 | string serialized = serializer.SerializePathParam(new NotAnEnum(), info); 124 | Assert.Equal("NotAnEnum", serialized); 125 | } 126 | 127 | private class NotAnEnum 128 | { 129 | public override string ToString() 130 | { 131 | return "NotAnEnum"; 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/RestEase.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.33103.201 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestEase", "RestEase\RestEase.csproj", "{6219B6B7-D110-4F46-B43A-1B41B031F2F3}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestEase.UnitTests", "RestEase.UnitTests\RestEase.UnitTests.csproj", "{34672726-3CDF-418B-B630-39B2CF41ACD4}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5197D787-981B-4A51-99D9-9D0FB2680503}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | EndProjectSection 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestEase.SourceGenerator", "RestEase.SourceGenerator\RestEase.SourceGenerator.csproj", "{62FF114A-846C-4904-8F45-1686FBE4FC7C}" 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGeneratorSandbox", "SourceGeneratorSandbox\SourceGeneratorSandbox.csproj", "{2F8D20B5-AE98-421E-A0AD-99D6624EA886}" 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestEase.SourceGenerator.UnitTests", "RestEase.SourceGenerator.UnitTests\RestEase.SourceGenerator.UnitTests.csproj", "{33E5FB2A-71D8-412B-A70F-CEF3C550E6F5}" 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestEase.HttpClientFactory", "RestEase.HttpClientFactory\RestEase.HttpClientFactory.csproj", "{267D0D34-8067-4F44-86A4-E6F425F5CAC6}" 22 | EndProject 23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestEase.HttpClientFactory.UnitTests", "RestEase.HttpClientFactory.UnitTests\RestEase.HttpClientFactory.UnitTests.csproj", "{1A9413FE-E17E-4089-A743-0566B9087CD3}" 24 | EndProject 25 | Global 26 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 27 | Debug|Any CPU = Debug|Any CPU 28 | Release|Any CPU = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {6219B6B7-D110-4F46-B43A-1B41B031F2F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {6219B6B7-D110-4F46-B43A-1B41B031F2F3}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {6219B6B7-D110-4F46-B43A-1B41B031F2F3}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {6219B6B7-D110-4F46-B43A-1B41B031F2F3}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {34672726-3CDF-418B-B630-39B2CF41ACD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {34672726-3CDF-418B-B630-39B2CF41ACD4}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {34672726-3CDF-418B-B630-39B2CF41ACD4}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {34672726-3CDF-418B-B630-39B2CF41ACD4}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {62FF114A-846C-4904-8F45-1686FBE4FC7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {62FF114A-846C-4904-8F45-1686FBE4FC7C}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {62FF114A-846C-4904-8F45-1686FBE4FC7C}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {62FF114A-846C-4904-8F45-1686FBE4FC7C}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {2F8D20B5-AE98-421E-A0AD-99D6624EA886}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {2F8D20B5-AE98-421E-A0AD-99D6624EA886}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {2F8D20B5-AE98-421E-A0AD-99D6624EA886}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {2F8D20B5-AE98-421E-A0AD-99D6624EA886}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {33E5FB2A-71D8-412B-A70F-CEF3C550E6F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {33E5FB2A-71D8-412B-A70F-CEF3C550E6F5}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {33E5FB2A-71D8-412B-A70F-CEF3C550E6F5}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {33E5FB2A-71D8-412B-A70F-CEF3C550E6F5}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {267D0D34-8067-4F44-86A4-E6F425F5CAC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {267D0D34-8067-4F44-86A4-E6F425F5CAC6}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {267D0D34-8067-4F44-86A4-E6F425F5CAC6}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {267D0D34-8067-4F44-86A4-E6F425F5CAC6}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {1A9413FE-E17E-4089-A743-0566B9087CD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {1A9413FE-E17E-4089-A743-0566B9087CD3}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {1A9413FE-E17E-4089-A743-0566B9087CD3}.Release|Any CPU.ActiveCfg = Release|Any CPU 58 | {1A9413FE-E17E-4089-A743-0566B9087CD3}.Release|Any CPU.Build.0 = Release|Any CPU 59 | EndGlobalSection 60 | GlobalSection(SolutionProperties) = preSolution 61 | HideSolutionNode = FALSE 62 | EndGlobalSection 63 | GlobalSection(ExtensibilityGlobals) = postSolution 64 | SolutionGuid = {9CF081C8-9E7E-423A-8A9F-D1B87B71DE52} 65 | EndGlobalSection 66 | EndGlobal 67 | -------------------------------------------------------------------------------- /src/RestEase/ApiExceptionContentDeserializer.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using RestEase.Implementation; 3 | 4 | namespace RestEase 5 | { 6 | /// 7 | /// Deserializer used by to deserialize its response content 8 | /// 9 | /// This is needed to encapsulate information required by the deserializer 10 | public interface IApiExceptionContentDeserializer 11 | { 12 | /// 13 | /// Deserialize the given content as the given type 14 | /// 15 | /// Type to deserialize as 16 | /// Content to deserialize 17 | /// Deserialized content 18 | T Deserialize(string? content); 19 | } 20 | 21 | internal class ApiExceptionContentDeserializer : IApiExceptionContentDeserializer 22 | { 23 | private readonly Requester requester; 24 | private readonly HttpResponseMessage responseMessage; 25 | private readonly IRequestInfo requestInfo; 26 | 27 | public ApiExceptionContentDeserializer(Requester requester, HttpResponseMessage responseMessage, IRequestInfo requestInfo) 28 | { 29 | this.requester = requester; 30 | this.responseMessage = responseMessage; 31 | this.requestInfo = requestInfo; 32 | } 33 | 34 | public T Deserialize(string? content) 35 | { 36 | return this.requester.Deserialize(content, this.responseMessage, this.requestInfo); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/RestEase/IRequestBodySerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | namespace RestEase 4 | { 5 | /// 6 | /// Helper which knows how to serialize a request body 7 | /// 8 | [Obsolete("Use RequestBodySerializer instead")] 9 | public interface IRequestBodySerializer 10 | { 11 | /// 12 | /// Serialize the given request body 13 | /// 14 | /// Body to serialize 15 | /// Type of the body to serialize 16 | /// HttpContent to assign to the request 17 | HttpContent SerializeBody(T body); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/RestEase/IRequestInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net.Http; 3 | using System.Reflection; 4 | using System.Threading; 5 | using RestEase.Implementation; 6 | 7 | namespace RestEase 8 | { 9 | /// 10 | /// Class containing information to construct a request from. 11 | /// An instance of this is created per request by the generated interface implementation 12 | /// 13 | public interface IRequestInfo 14 | { 15 | /// 16 | /// Gets the HttpMethod which should be used to make the request 17 | /// 18 | HttpMethod Method { get; } 19 | 20 | /// 21 | /// Gets the path which should be prepended to , if any 22 | /// 23 | string? BaseAddress { get; } 24 | 25 | /// 26 | /// Gets the path which should be prepended to unless starts with a '/', if any 27 | /// 28 | string? BasePath { get; } 29 | 30 | /// 31 | /// Gets the relative path to the resource to request 32 | /// 33 | string Path { get; } 34 | 35 | /// 36 | /// Gets the CancellationToken used to cancel the request 37 | /// 38 | CancellationToken CancellationToken { get; } 39 | 40 | /// 41 | /// Gets a value indicating whether to suppress the exception on invalid status codes 42 | /// 43 | bool AllowAnyStatusCode { get; } 44 | 45 | /// 46 | /// Gets the query parameters to append to the request URI 47 | /// 48 | IEnumerable QueryParams { get; } 49 | 50 | /// 51 | /// Gets the raw query parameter infos 52 | /// 53 | IEnumerable RawQueryParameters { get; } 54 | 55 | /// 56 | /// Gets the parameters which should be substituted into placeholders in the Path 57 | /// 58 | IEnumerable PathParams { get; } 59 | 60 | /// 61 | /// Gets the values from properties which should be substituted into placeholders in the Path 62 | /// 63 | IEnumerable PathProperties { get; } 64 | 65 | /// 66 | /// Gets the values from properties which should be added to all query strings 67 | /// 68 | IEnumerable QueryProperties { get; } 69 | 70 | /// 71 | /// Gets the values from properties which should be added as HTTP request message properties 72 | /// 73 | IEnumerable HttpRequestMessageProperties { get; } 74 | 75 | /// 76 | /// Gets the headers which were applied to the interface 77 | /// 78 | IEnumerable>? ClassHeaders { get; } 79 | 80 | /// 81 | /// Gets the headers which were applied using properties 82 | /// 83 | IEnumerable PropertyHeaders { get; } 84 | 85 | /// 86 | /// Gets the headers which were applied to the method being called 87 | /// 88 | IEnumerable> MethodHeaders { get; } 89 | 90 | /// 91 | /// Gets the headers which were passed to the method as parameters 92 | /// 93 | IEnumerable HeaderParams { get; } 94 | 95 | /// 96 | /// Gets information the [Body] method parameter, if it exists 97 | /// 98 | BodyParameterInfo? BodyParameterInfo { get; } 99 | 100 | /// 101 | /// Gets the MethodInfo of the interface method which was invoked 102 | /// 103 | MethodInfo MethodInfo { get; } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/RestEase/IRequestQueryParamSerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace RestEase 5 | { 6 | /// 7 | /// Helper which knows how to serialize query parmaeters 8 | /// 9 | [Obsolete("Use RequestQueryParamSerializer instead")] 10 | public interface IRequestQueryParamSerializer 11 | { 12 | /// 13 | /// Serialize a query parameter whose value is scalar (not a collection), into a collection of name -> value pairs 14 | /// 15 | /// 16 | /// Most of the time, you will only return a single KeyValuePair from this method. However, you are given the flexibility, 17 | /// to return multiple KeyValuePairs if you wish. Duplicate keys are allowed: they will be serialized as separate query parameters. 18 | /// 19 | /// Type of the value to serialize 20 | /// Name of the query parameter 21 | /// Value of the query parameter 22 | /// Extra info which may be useful to the serializer 23 | /// A colletion of name -> value pairs to use as query parameters 24 | IEnumerable> SerializeQueryParam(string name, T value, RequestQueryParamSerializerInfo info); 25 | 26 | /// 27 | /// Serialize a query parameter whose value is a collection, into a collection of name -> value pairs 28 | /// 29 | /// 30 | /// Most of the time, you will return a single KeyValuePair for each value in the collection, and all will have 31 | /// the same key. However this is not required: you can return whatever you want. 32 | /// 33 | /// Type of the value to serialize 34 | /// Name of the query parameter 35 | /// Values of the query parmaeter 36 | /// Extra info which may be useful to the serializer 37 | /// A colletion of name -> value pairs to use as query parameters 38 | IEnumerable> SerializeQueryCollectionParam(string name, IEnumerable values, RequestQueryParamSerializerInfo info); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/RestEase/IRequester.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading.Tasks; 3 | using System; 4 | using System.IO; 5 | 6 | namespace RestEase 7 | { 8 | /// 9 | /// Called by the generated REST API implementation, this knows how to invoke the API and return a suitable response 10 | /// 11 | public interface IRequester : IDisposable 12 | { 13 | /// 14 | /// Invoked when the API interface method being called returns a Task 15 | /// 16 | /// Object holding all information about the request 17 | /// Task to return to the API interface caller 18 | Task RequestVoidAsync(IRequestInfo requestInfo); 19 | 20 | /// 21 | /// Invoked when the API interface method being called returns a Task{T} 22 | /// 23 | /// Type of response object expected by the caller 24 | /// Object holding all information about the request 25 | /// Task to return to the API interface caller 26 | Task RequestAsync(IRequestInfo requestInfo); 27 | 28 | /// 29 | /// Invoked when the API interface method being called returns a Task{HttpResponseMessage} 30 | /// 31 | /// Object holding all information about the request 32 | /// Task to return to the API interface caller 33 | Task RequestWithResponseMessageAsync(IRequestInfo requestInfo); 34 | 35 | /// 36 | /// Invoked when the API interface method being called returns a Task{Response{T}} 37 | /// 38 | /// Type of response object expected by the caller 39 | /// Object holding all information about the request 40 | /// Task to return to the API interface caller 41 | Task> RequestWithResponseAsync(IRequestInfo requestInfo); 42 | 43 | /// 44 | /// Invoked when the API interface method being called returns a Task{Stream} 45 | /// 46 | /// Object holding all information about the request 47 | /// Task to return to the API interface caller 48 | Task RequestStreamAsync(IRequestInfo requestInfo); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/RestEase/IResponseDeserializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | 4 | namespace RestEase 5 | { 6 | /// 7 | /// Helper capable of deserializing a response, to return to the caller 8 | /// 9 | [Obsolete("Use ResponseDeserializer instead")] 10 | public interface IResponseDeserializer 11 | { 12 | /// 13 | /// Read the response string from the response, deserialize, and return a deserialized object 14 | /// 15 | /// Type of object to deserialize into 16 | /// String content read from the response 17 | /// HttpResponseMessage. Consider calling response.Content.ReadAsStringAsync() to retrieve a string 18 | /// Deserialized response 19 | T Deserialize(string? content, HttpResponseMessage response); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/RestEase/Implementation/BodyParameterInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | 4 | namespace RestEase.Implementation 5 | { 6 | /// 7 | /// INTERNAL TYPE! This type may break between minor releases. Use at your own risk! 8 | /// 9 | /// Class containing information about a desired request body 10 | /// 11 | public abstract class BodyParameterInfo 12 | { 13 | /// 14 | /// Gets or sets the method to use to serialize the body 15 | /// 16 | public BodySerializationMethod SerializationMethod { get; } 17 | 18 | /// 19 | /// Gets the body to serialize, as an object 20 | /// 21 | public object? ObjectValue { get; } 22 | 23 | /// 24 | /// Initialises a new instance of the class 25 | /// 26 | /// Method to use the serialize the body 27 | /// Body to serialize, as an object 28 | protected BodyParameterInfo(BodySerializationMethod serializationMethod, object? objectValue) 29 | { 30 | this.SerializationMethod = serializationMethod; 31 | this.ObjectValue = objectValue; 32 | } 33 | 34 | /// 35 | /// Serialize the (typed) value using the given serializer 36 | /// 37 | /// Serializer to use 38 | /// RequestInfo representing the request 39 | /// to use if the value implements 40 | /// Serialized value 41 | public abstract HttpContent? SerializeValue(RequestBodySerializer serializer, IRequestInfo requestInfo, IFormatProvider? formatProvider); 42 | } 43 | 44 | /// 45 | /// INTERNAL TYPE! This type may break between minor releases. Use at your own risk! 46 | /// 47 | /// Class containing information about a desired request body 48 | /// 49 | /// Type of the value 50 | public class BodyParameterInfo : BodyParameterInfo 51 | { 52 | /// 53 | /// Gets the body to serialize 54 | /// 55 | public T Value { get; } 56 | 57 | /// 58 | /// Initialises a new instance of the class 59 | /// 60 | /// Method to use the serialize the body 61 | /// Body to serialize 62 | public BodyParameterInfo(BodySerializationMethod serializationMethod, T value) 63 | : base(serializationMethod, value) 64 | { 65 | this.Value = value; 66 | } 67 | 68 | /// 69 | /// Serialize the (typed) value using the given serializer 70 | /// 71 | /// Serializer to use 72 | /// RequestInfo representing the request 73 | /// to use if the value implements 74 | /// Serialized value 75 | public override HttpContent? SerializeValue(RequestBodySerializer serializer, IRequestInfo requestInfo, IFormatProvider? formatProvider) 76 | { 77 | if (serializer == null) 78 | throw new ArgumentNullException(nameof(serializer)); 79 | 80 | return serializer.SerializeBody(this.Value, new RequestBodySerializerInfo(requestInfo, formatProvider)); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/RestEase/Implementation/DictionaryIterator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Linq; 6 | using System.Reflection; 7 | using RestEase.Platform; 8 | 9 | namespace RestEase.Implementation 10 | { 11 | /// 12 | /// INTERNAL TYPE! This type may break between minor releases. Use at your own risk! 13 | /// 14 | /// Helper to iterate both IDictionary and IDictionary{TKey, TValue} instances, as if both were IEnumerable{KeyValuePair{object, object}} 15 | /// 16 | public static class DictionaryIterator 17 | { 18 | #if NETSTANDARD 19 | private static readonly MethodInfo iterateGenericTypedMethod = typeof(DictionaryIterator).GetTypeInfo().GetDeclaredMethod("IterateGenericTyped"); 20 | #else 21 | private static readonly MethodInfo iterateGenericTypedMethod = typeof(DictionaryIterator).GetMethod("IterateGenericTyped", BindingFlags.NonPublic | BindingFlags.Static)!; 22 | #endif 23 | 24 | /// 25 | /// Returns true if we're capable of iterating the supplied type 26 | /// 27 | /// Type to check 28 | /// True if we're capable of iterating it 29 | public static bool CanIterate(Type dictionaryType) 30 | { 31 | var dictionaryTypeInfo = dictionaryType.GetTypeInfo(); 32 | return typeof(IDictionary).GetTypeInfo().IsAssignableFrom(dictionaryTypeInfo) || 33 | (dictionaryTypeInfo.IsGenericType && dictionaryType.GetGenericTypeDefinition() == typeof(IDictionary<,>)) || 34 | dictionaryTypeInfo.GetInterfaces().Any(x => x.GetTypeInfo().IsGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>)); 35 | } 36 | 37 | /// 38 | /// Iterates the given IDictionary or IDictionary{TKey, TValue} as if it was an IEnumerable{KeyValuePair{object, object}} 39 | /// 40 | /// Dictionary to iterate 41 | /// The equivalent IEnumerable{KeyValuePair{object, object}} 42 | public static IEnumerable> Iterate(object dictionary) 43 | { 44 | if (dictionary == null) 45 | throw new ArgumentNullException(nameof(dictionary)); 46 | 47 | if (dictionary is IDictionary nonGeneric) 48 | return IterateNonGeneric(nonGeneric); 49 | 50 | // 'dictionary' cannot be an interface, so we're safe skipping to see whether 51 | // dictionary.GetType().GetGenericTypeDefinition() == IDictionary<,> 52 | foreach (var interfaceType in dictionary.GetType().GetTypeInfo().GetInterfaces()) 53 | { 54 | var interfaceTypeInfo = interfaceType.GetTypeInfo(); 55 | if (interfaceTypeInfo.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IDictionary<,>)) 56 | return IterateGeneric(dictionary, interfaceType); 57 | } 58 | 59 | throw new ArgumentException("Dictionary does not implement IDictionary or IDictionary", nameof(dictionary)); 60 | } 61 | 62 | private static IEnumerable> IterateNonGeneric(IDictionary dictionary) 63 | { 64 | foreach (DictionaryEntry entry in dictionary) 65 | { 66 | yield return new KeyValuePair(entry.Key, entry.Value); 67 | } 68 | } 69 | 70 | private static IEnumerable> IterateGeneric(object dictionary, Type dictionaryType) 71 | { 72 | var genericArguments = dictionaryType.GetTypeInfo().GetGenericArguments(); 73 | var keyType = genericArguments[0]; 74 | var valueType = genericArguments[1]; 75 | 76 | var method = iterateGenericTypedMethod.MakeGenericMethod(keyType, valueType); 77 | return (IEnumerable>)method.Invoke(null, new[] { dictionary })!; 78 | } 79 | 80 | [SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Used via reflection")] 81 | private static IEnumerable> IterateGenericTyped(IDictionary dictionary) 82 | { 83 | foreach (var kvp in dictionary) 84 | { 85 | yield return new KeyValuePair(kvp.Key, kvp.Value); 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/RestEase/Implementation/EmitEmitUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using System.Reflection.Emit; 5 | using System.Text; 6 | using RestEase.Platform; 7 | 8 | namespace RestEase.Implementation 9 | { 10 | internal static class EmitEmitUtils 11 | { 12 | public static void AddGenericTypeConstraints(Type[] genericArguments, GenericTypeParameterBuilder[] builders) 13 | { 14 | for (int i = 0; i < genericArguments.Length; i++) 15 | { 16 | var genericArgumentType = genericArguments[i].GetTypeInfo(); 17 | var constraints = genericArgumentType.GetGenericParameterConstraints().Select(x => x.GetTypeInfo()).ToList(); 18 | // We're generating a class: we can't have variance 19 | var attributes = genericArgumentType.GenericParameterAttributes & ~GenericParameterAttributes.VarianceMask; 20 | builders[i].SetGenericParameterAttributes(attributes); 21 | var baseType = constraints.FirstOrDefault(x => x.IsClass); 22 | if (baseType != null) 23 | { 24 | builders[i].SetBaseTypeConstraint(baseType.AsType()); 25 | } 26 | var interfaceTypes = constraints.Where(x => !x.IsClass).Select(x => x.AsType()).ToArray(); 27 | if (interfaceTypes.Length > 0) 28 | { 29 | builders[i].SetInterfaceConstraints(interfaceTypes); 30 | } 31 | } 32 | } 33 | 34 | public static string FriendlyNameForType(Type type) 35 | { 36 | var sb = new StringBuilder(); 37 | Impl(type.GetTypeInfo()); 38 | return sb.ToString(); 39 | 40 | void Impl(TypeInfo typeInfo) 41 | { 42 | if (typeInfo.IsGenericType) 43 | { 44 | string fullName = type.GetGenericTypeDefinition().FullName!; 45 | sb.Append(fullName.Substring(0, fullName.LastIndexOf('`'))); 46 | sb.Append('<'); 47 | int i = 0; 48 | foreach (var arg in typeInfo.GetGenericArguments()) 49 | { 50 | if (i > 0) 51 | { 52 | sb.Append(','); 53 | } 54 | Impl(arg.GetTypeInfo()); 55 | i++; 56 | } 57 | sb.Append('>'); 58 | } 59 | else 60 | { 61 | sb.Append(typeInfo.FullName); 62 | } 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/RestEase/Implementation/EmitImplementationFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Reflection.Emit; 4 | using System.Threading; 5 | using RestEase.Implementation.Emission; 6 | 7 | namespace RestEase.Implementation 8 | { 9 | internal class EmitImplementationFactory 10 | { 11 | private static readonly string moduleBuilderName = "RestEaseAutoGeneratedModule"; 12 | private readonly Emitter emitter; 13 | 14 | // Make sure this is an actual proper lazy singleton: we don't want to be instantiating it if 15 | // S.R.E is disabled e.g. because we're on iOS 16 | private static readonly Lazy lazyFactory = new(() => new(), LazyThreadSafetyMode.ExecutionAndPublication); 17 | public static EmitImplementationFactory Instance => lazyFactory.Value; 18 | 19 | private EmitImplementationFactory() 20 | { 21 | var assemblyName = new AssemblyName(RestClient.FactoryAssemblyName); 22 | var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); 23 | 24 | var moduleBuilder = assemblyBuilder.DefineDynamicModule(moduleBuilderName); 25 | this.emitter = new Emitter(moduleBuilder); 26 | } 27 | 28 | public Type BuildEmitImplementation(Type interfaceType) 29 | { 30 | var analyzer = new ReflectionTypeAnalyzer(interfaceType); 31 | var typeModel = analyzer.Analyze(); 32 | var diagnosticReporter = new DiagnosticReporter(typeModel); 33 | var generator = new ImplementationGenerator(typeModel, this.emitter, diagnosticReporter); 34 | return generator.Generate().Type; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/RestEase/Implementation/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace RestEase.Implementation 4 | { 5 | internal static class EnumerableExtensions 6 | { 7 | public static IEnumerable Concat(T item1, IEnumerable rest) 8 | { 9 | yield return item1; 10 | foreach (var other in rest) 11 | { 12 | yield return other; 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/RestEase/Implementation/HeaderParameterInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace RestEase.Implementation 5 | { 6 | /// 7 | /// INTERNAL TYPE! This type may break between minor releases. Use at your own risk! 8 | /// 9 | /// Class containing information about a desired header parameter 10 | /// 11 | public abstract class HeaderParameterInfo 12 | { 13 | /// 14 | /// Serialize the value into a name -> value pair using its ToString method 15 | /// 16 | /// given to the , if any 17 | /// Serialized value 18 | public abstract KeyValuePair SerializeToString(IFormatProvider? formatProvider); 19 | } 20 | 21 | /// 22 | /// INTERNAL TYPE! This type may break between minor releases. Use at your own risk! 23 | /// 24 | /// Class containing information about a desired header parameter 25 | /// 26 | /// Type of the value 27 | public class HeaderParameterInfo : HeaderParameterInfo 28 | { 29 | private readonly string name; 30 | private readonly T value; 31 | private readonly string? defaultValue; 32 | private readonly string? format; 33 | 34 | /// 35 | /// Initialises a new instance of the class 36 | /// 37 | /// Name of the header 38 | /// Value of the header 39 | /// Default value of the header, used if is null 40 | /// 41 | public HeaderParameterInfo(string name, T value, string? defaultValue, string? format) 42 | { 43 | this.name = name; 44 | this.value = value; 45 | this.defaultValue = defaultValue; 46 | this.format = format; 47 | } 48 | 49 | /// 50 | /// Serialize the value into a name -> value pair using its ToString method 51 | /// 52 | /// given to the , if any 53 | /// Serialized value 54 | public override KeyValuePair SerializeToString(IFormatProvider? formatProvider) 55 | { 56 | string? value = this.defaultValue; 57 | if (this.value != null) 58 | value = ToStringHelper.ToString(this.value, this.format, formatProvider); 59 | return new KeyValuePair(this.name, value); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/RestEase/Implementation/HttpRequestMessagePropertyInfo.cs: -------------------------------------------------------------------------------- 1 | namespace RestEase.Implementation 2 | { 3 | /// 4 | /// INTERNAL TYPE! This type may break between minor releases. Use at your own risk! 5 | /// 6 | /// Structure containing information about a desired HTTP request message property 7 | /// 8 | public struct HttpRequestMessagePropertyInfo 9 | { 10 | /// 11 | /// Key of the key/value pair 12 | /// 13 | public string Key { get; private set; } 14 | 15 | /// 16 | /// Value of the key/value pair 17 | /// 18 | public object Value { get; private set; } 19 | 20 | /// 21 | /// Initialises a new instance of the Structure 22 | /// 23 | /// Key of the key/value pair 24 | /// Value of the key/value pair 25 | public HttpRequestMessagePropertyInfo(string key, object value) 26 | { 27 | this.Key = key; 28 | this.Value = value; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/RestEase/Implementation/ImplementationCreationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase.Implementation 4 | { 5 | /// 6 | /// INTERNAL TYPE! This type may break between minor releases. Use at your own risk! 7 | /// 8 | /// Exception thrown if an interface implementation cannot be created 9 | /// 10 | public class ImplementationCreationException : Exception 11 | { 12 | /// 13 | /// Gets the code of this error 14 | /// 15 | public DiagnosticCode Code { get; } = DiagnosticCode.None; 16 | 17 | /// 18 | /// Initialises a new instance of the class 19 | /// 20 | /// Code of this error 21 | /// Message to use 22 | public ImplementationCreationException(DiagnosticCode code, string message) 23 | : base(message) 24 | { 25 | this.Code = code; 26 | } 27 | 28 | /// 29 | /// Initialises a new instance of the class 30 | /// 31 | /// Message to use 32 | public ImplementationCreationException(string message) 33 | : base(message) 34 | { } 35 | 36 | /// 37 | /// Initialises a new instance of the class 38 | /// 39 | /// Message to use 40 | /// InnerException to use 41 | public ImplementationCreationException(string message, Exception innerException) 42 | : base(message, innerException) 43 | { } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/RestEase/Implementation/ImplementationHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Linq.Expressions; 4 | using System.Reflection; 5 | 6 | namespace RestEase.Implementation 7 | { 8 | /// 9 | /// Internal type. Do not use! Helper methods called from generated code 10 | /// 11 | [EditorBrowsable(EditorBrowsableState.Never)] 12 | public static class ImplementationHelpers 13 | { 14 | /// 15 | /// Internal method. Do not call. 16 | /// 17 | public static MethodInfo GetInterfaceMethodInfo( 18 | Expression> expr) 19 | { 20 | var methodInfo = ((MethodCallExpression)expr.Body).Method; 21 | return methodInfo.IsGenericMethod ? methodInfo.GetGenericMethodDefinition() : methodInfo; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/RestEase/Implementation/ModifyingClientHttpHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace RestEase.Implementation 7 | { 8 | /// 9 | /// INTERNAL TYPE! This type may break between minor releases. Use at your own risk! 10 | /// 11 | /// HttpClientHandler which uses a delegate to modify requests 12 | /// 13 | public class ModifyingClientHttpHandler : DelegatingHandler 14 | { 15 | private readonly RequestModifier requestModifier; 16 | 17 | /// 18 | /// Initialises a new instance of the class, 19 | /// using the given delegate to modify requests 20 | /// 21 | /// Delegate to use to modify requests 22 | public ModifyingClientHttpHandler(RequestModifier requestModifier) 23 | { 24 | this.requestModifier = requestModifier ?? throw new ArgumentNullException(nameof(requestModifier)); 25 | } 26 | 27 | /// 28 | /// Creates an instance of System.Net.Http.HttpResponseMessage based on the information 29 | /// provided in the System.Net.Http.HttpRequestMessage as an operation that will 30 | /// not block. 31 | /// 32 | /// The HTTP request message 33 | /// A cancellation token to cancel the operation 34 | /// Returns System.Threading.Tasks.Task{TResult}.The task object representing 35 | /// the asynchronous operation 36 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 37 | { 38 | await this.requestModifier(request, cancellationToken).ConfigureAwait(false); 39 | return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/RestEase/Implementation/PathParameterInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace RestEase.Implementation 5 | { 6 | /// 7 | /// INTERNAL TYPE! This type may break between minor releases. Use at your own risk! 8 | /// 9 | /// Class containing information about a desired path parameter 10 | /// 11 | public abstract class PathParameterInfo 12 | { 13 | /// 14 | /// Gets a value indicating whether this path parameter should be URL-encoded 15 | /// 16 | public bool UrlEncode { get; protected set; } 17 | 18 | /// 19 | /// Gets the method to use to serialize the path parameter. 20 | /// 21 | public PathSerializationMethod SerializationMethod { get; protected set; } 22 | 23 | /// 24 | /// Serialize the value into a name -> value pair using the given serializer 25 | /// 26 | /// Serializer to use 27 | /// RequestInfo representing the request 28 | /// given to the , if any 29 | /// Serialized value 30 | public abstract KeyValuePair SerializeValue(RequestPathParamSerializer serializer, IRequestInfo requestInfo, IFormatProvider? formatProvider); 31 | 32 | /// 33 | /// Serialize the value into a name -> value pair using its ToString method 34 | /// 35 | /// given to the , if any 36 | /// Serialized value 37 | public abstract KeyValuePair SerializeToString(IFormatProvider? formatProvider); 38 | } 39 | 40 | /// 41 | /// INTERNAL TYPE! This type may break between minor releases. Use at your own risk! 42 | /// 43 | /// Class containing information about a desired path parameter 44 | /// 45 | /// Type of the value 46 | public class PathParameterInfo : PathParameterInfo 47 | { 48 | private readonly string name; 49 | private readonly T value; 50 | private readonly string? format; 51 | 52 | /// 53 | /// Initialises a new instance of the Structure 54 | /// 55 | /// Name of the name/value pair 56 | /// Value of the name/value pair 57 | /// Format string to use 58 | /// Indicates whether this parameter should be url-encoded 59 | /// Method to use to serialize the path value. 60 | public PathParameterInfo(string name, T value, string? format, bool urlEncode, PathSerializationMethod serializationMethod) 61 | { 62 | this.name = name; 63 | this.value = value; 64 | this.format = format; 65 | this.UrlEncode = urlEncode; 66 | this.SerializationMethod = serializationMethod; 67 | } 68 | 69 | /// 70 | /// Serialize the value into a name -> value pair using the given serializer 71 | /// 72 | /// Serializer to use 73 | /// RequestInfo representing the request 74 | /// given to the , if any 75 | /// Serialized value 76 | public override KeyValuePair SerializeValue(RequestPathParamSerializer serializer, IRequestInfo requestInfo, IFormatProvider? formatProvider) 77 | { 78 | if (serializer == null) 79 | throw new ArgumentNullException(nameof(serializer)); 80 | if (requestInfo == null) 81 | throw new ArgumentNullException(nameof(requestInfo)); 82 | 83 | string? serializedValue = serializer.SerializePathParam(this.value, new RequestPathParamSerializerInfo(requestInfo, this.format, formatProvider)); 84 | return new KeyValuePair(this.name, serializedValue); 85 | } 86 | 87 | /// 88 | /// Serialize the value into a name -> value pair using its ToString method 89 | /// 90 | /// given to the , if any 91 | /// Serialized value 92 | public override KeyValuePair SerializeToString(IFormatProvider? formatProvider) 93 | { 94 | return new KeyValuePair(this.name, ToStringHelper.ToString(this.value, this.format, formatProvider)); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/RestEase/Implementation/RawQueryParameterInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase.Implementation 4 | { 5 | /// 6 | /// INTERNAL TYPE! This type may break between minor releases. Use at your own risk! 7 | /// 8 | /// Class containing information about a raw query parameter 9 | /// 10 | public abstract class RawQueryParameterInfo 11 | { 12 | /// 13 | /// Serialize the value into a string 14 | /// 15 | /// to use if the value implements 16 | /// Serialized value 17 | public abstract string SerializeToString(IFormatProvider? formatProvider); 18 | } 19 | 20 | /// 21 | /// INTERNAL TYPE! This type may break between minor releases. Use at your own risk! 22 | /// 23 | /// Class containing information about a raw query parameter 24 | /// 25 | /// Type of value providing the raw query parameter 26 | public class RawQueryParameterInfo : RawQueryParameterInfo 27 | { 28 | private readonly T value; 29 | 30 | /// 31 | /// Initialises a new instance of the class 32 | /// 33 | /// Value which provides the raw query parameter 34 | public RawQueryParameterInfo(T value) 35 | { 36 | this.value = value; 37 | } 38 | 39 | /// 40 | public override string SerializeToString(IFormatProvider? formatProvider) 41 | { 42 | return ToStringHelper.ToString(this.value, null, formatProvider) ?? string.Empty; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/RestEase/Implementation/RestEaseInterfaceImplementationAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | 4 | namespace RestEase.Implementation 5 | { 6 | /// 7 | /// Internal type. Do not use! 8 | /// 9 | [EditorBrowsable(EditorBrowsableState.Never)] 10 | [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] 11 | public sealed class RestEaseInterfaceImplementationAttribute : Attribute 12 | { 13 | /// 14 | /// Internal type. Do not use. 15 | /// 16 | public Type InterfaceType { get; } 17 | 18 | /// 19 | /// Internal type. Do not use. 20 | /// 21 | public Type ImplementationType { get; } 22 | 23 | /// 24 | /// Internal type. Do not use. 25 | /// 26 | public RestEaseInterfaceImplementationAttribute(Type interfaceType, Type implementationType) 27 | { 28 | this.InterfaceType = interfaceType; 29 | this.ImplementationType = implementationType; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/RestEase/Implementation/ToStringHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace RestEase.Implementation 5 | { 6 | /// 7 | /// INTERNAL TYPE! This type may break between minor releases. Use at your own risk! 8 | /// 9 | /// Helper methods to turn a value into a string 10 | /// 11 | public static class ToStringHelper 12 | { 13 | private static readonly Regex stringFormatRegex; 14 | 15 | static ToStringHelper() 16 | { 17 | // We know that format strings: 18 | // 1. Don't support a single { or }, unless they're the outermost { or } in a placeholder 19 | // 2. Placeholders start with a digit 20 | // 3. Placeholders have an optional alignment, and an optional format string. 21 | // 3a. The alignment can be a positive or negative integer ('+' is not allowed) 22 | // 3b. The format string cannot have a single { or } 23 | 24 | string noSingleBraces = @"(?:[^{}]|{{|}})"; 25 | string placeholder = @"{\d+(?:,-?\d+)?(?::" + noSingleBraces + @"*)?}"; 26 | string regex = $@"^{noSingleBraces}*(?:{placeholder}{noSingleBraces}*)+$"; 27 | stringFormatRegex = new Regex(regex); 28 | } 29 | 30 | /// 31 | /// Turns the given value into a string, passing it into if it 32 | /// looks like a format string, otherwise using its implementation if possible 33 | /// 34 | /// Type of value 35 | /// Value to turn into a string 36 | /// Format parameter, see description 37 | /// Format provider to pass to 38 | /// String version of the input value 39 | public static string? ToString(T value, string? format, IFormatProvider? formatProvider) 40 | { 41 | // If it looks like it's a ToString placeholder, use ToString 42 | if (format != null && stringFormatRegex.IsMatch(format)) 43 | return string.Format(formatProvider, format, value); 44 | if (value is IFormattable formattable) 45 | return formattable.ToString(format, formatProvider); 46 | else 47 | return value?.ToString(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/RestEase/JsonRequestBodySerializer.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Net.Http; 3 | using System.Net.Http.Headers; 4 | 5 | namespace RestEase 6 | { 7 | /// 8 | /// Default IRequestBodySerializer, using Json.NET 9 | /// 10 | public class JsonRequestBodySerializer : RequestBodySerializer 11 | { 12 | /// 13 | /// Gets or sets the serializer settings to pass to JsonConvert.SerializeObject 14 | /// 15 | public JsonSerializerSettings? JsonSerializerSettings { get; set; } 16 | 17 | /// 18 | public override HttpContent? SerializeBody(T body, RequestBodySerializerInfo info) 19 | { 20 | if (body == null) 21 | return null; 22 | 23 | var content = new StringContent(JsonConvert.SerializeObject(body, this.JsonSerializerSettings)); 24 | 25 | const string contentType = "application/json"; 26 | if (content.Headers.ContentType == null) 27 | { 28 | content.Headers.ContentType = new MediaTypeHeaderValue(contentType); 29 | } 30 | else 31 | { 32 | content.Headers.ContentType.MediaType = contentType; 33 | } 34 | return content; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/RestEase/JsonRequestQueryParamSerializer.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Collections.Generic; 3 | 4 | namespace RestEase 5 | { 6 | /// 7 | /// Default IRequestParamSerializer, using Json.NET 8 | /// 9 | public class JsonRequestQueryParamSerializer : RequestQueryParamSerializer 10 | { 11 | /// 12 | /// Gets or sets the serializer settings to pass to JsonConvert.SerializeObject 13 | /// 14 | public JsonSerializerSettings? JsonSerializerSettings { get; set; } 15 | /// 16 | /// Serialize a query parameter whose value is scalar (not a collection), into a collection of name -> value pairs 17 | /// 18 | /// 19 | /// Most of the time, you will only return a single KeyValuePair from this method. However, you are given the flexibility, 20 | /// to return multiple KeyValuePairs if you wish. Duplicate keys are allowed: they will be serialized as separate query parameters. 21 | /// 22 | /// Type of the value to serialize 23 | /// Name of the query parameter 24 | /// Value of the query parameter 25 | /// Extra information which may be useful 26 | /// A colletion of name -> value pairs to use as query parameters 27 | public override IEnumerable> SerializeQueryParam(string name, T value, RequestQueryParamSerializerInfo info) 28 | { 29 | if (value == null) 30 | yield break; 31 | 32 | yield return new KeyValuePair(name, JsonConvert.SerializeObject(value, this.JsonSerializerSettings)); 33 | } 34 | 35 | /// 36 | /// Serialize a query parameter whose value is a collection, into a collection of name -> value pairs 37 | /// 38 | /// 39 | /// Most of the time, you will return a single KeyValuePair for each value in the collection, and all will have 40 | /// the same key. However this is not required: you can return whatever you want. 41 | /// 42 | /// Type of the value to serialize 43 | /// Name of the query parameter 44 | /// Values of the query parmaeter 45 | /// Extra information which may be useful 46 | /// A colletion of name -> value pairs to use as query parameters 47 | public override IEnumerable> SerializeQueryCollectionParam(string name, IEnumerable values, RequestQueryParamSerializerInfo info) 48 | { 49 | if (values == null) 50 | yield break; 51 | 52 | foreach (var value in values) 53 | { 54 | if (value != null) 55 | yield return new KeyValuePair(name, JsonConvert.SerializeObject(value, this.JsonSerializerSettings)); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/RestEase/JsonResponseDeserializer.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Net.Http; 3 | 4 | namespace RestEase 5 | { 6 | /// 7 | /// Default implementation of IResponseDeserializer, using Json.NET 8 | /// 9 | public class JsonResponseDeserializer : ResponseDeserializer 10 | { 11 | /// 12 | /// Gets or sets the serializer settings to pass to JsonConvert.DeserializeObject{T} 13 | /// 14 | public JsonSerializerSettings? JsonSerializerSettings { get; set; } 15 | 16 | /// 17 | public override T Deserialize(string? content, HttpResponseMessage response, ResponseDeserializerInfo info) 18 | { 19 | // TODO: Figure out how best to handle nullables here. I don't think we can change the signature to return T? 20 | // without breaking backwards compat... In the meantime, this worked before json.net changed their nullable 21 | // annotations, so ignore the issue for now 22 | return JsonConvert.DeserializeObject(content!, this.JsonSerializerSettings)!; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/RestEase/Platform/ArrayUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase.Platform 4 | { 5 | internal static class ArrayUtil 6 | { 7 | #if NET452 || NETSTANDARD1_1 8 | private static class EmptyArrayCache 9 | { 10 | public static readonly T[] Instance = new T[0]; 11 | } 12 | 13 | public static T[] Empty() => EmptyArrayCache.Instance; 14 | #else 15 | public static T[] Empty() => Array.Empty(); 16 | #endif 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/RestEase/Platform/TypeHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace RestEase.Platform 7 | { 8 | internal static class TypeHelpers 9 | { 10 | #if NETSTANDARD 11 | public static MethodInfo GetMethod(this TypeInfo typeInfo, string name) 12 | { 13 | return typeInfo.GetDeclaredMethod(name); 14 | } 15 | 16 | public static IEnumerable GetMethods(this TypeInfo typeInfo) 17 | { 18 | return typeInfo.DeclaredMethods; 19 | } 20 | 21 | public static PropertyInfo GetProperty(this TypeInfo typeInfo, string name) 22 | { 23 | return typeInfo.GetDeclaredProperty(name); 24 | } 25 | 26 | public static IEnumerable GetProperties(this TypeInfo typeInfo) 27 | { 28 | return typeInfo.DeclaredProperties; 29 | } 30 | 31 | public static FieldInfo GetField(this TypeInfo typeInfo, string name) 32 | { 33 | return typeInfo.GetDeclaredField(name); 34 | } 35 | 36 | public static ConstructorInfo GetConstructor(this TypeInfo typeInfo, Type[] paramTypes) 37 | { 38 | return typeInfo.DeclaredConstructors.FirstOrDefault(x => x.GetParameters().Select(p => p.ParameterType).SequenceEqual(paramTypes)); 39 | } 40 | 41 | public static IEnumerable GetInterfaces(this TypeInfo typeInfo) 42 | { 43 | return typeInfo.ImplementedInterfaces; 44 | } 45 | 46 | public static IEnumerable GetEvents(this TypeInfo typeInfo) 47 | { 48 | return typeInfo.DeclaredEvents; 49 | } 50 | 51 | public static Type[] GetGenericArguments(this TypeInfo typeInfo) 52 | { 53 | return typeInfo.GenericTypeArguments; 54 | } 55 | #endif 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/RestEase/QueryStringBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace RestEase 2 | { 3 | /// 4 | /// Helper used to create a properly encoded query string for a request 5 | /// 6 | public abstract class QueryStringBuilder 7 | { 8 | /// 9 | /// Override this method to return a suitably escaped query string 10 | /// 11 | /// Information about the request 12 | /// The escaped query string 13 | public abstract string Build(QueryStringBuilderInfo info); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/RestEase/QueryStringBuilderInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace RestEase 6 | { 7 | /// 8 | /// Encapsulates information provided to 9 | /// 10 | public class QueryStringBuilderInfo 11 | { 12 | /// 13 | /// Gets the initial query string, present from the URI the user specified in the Get/etc parameter 14 | /// 15 | public string InitialQueryString { get; } 16 | 17 | /// 18 | /// Gets the raw query parameter, if any 19 | /// 20 | public IEnumerable RawQueryParameters { get; } 21 | 22 | /// 23 | /// Obsolete. Use 24 | /// 25 | [Obsolete("Use RawQueryParameters")] 26 | public string RawQueryParameter => this.RawQueryParameters.FirstOrDefault() ?? string.Empty; 27 | 28 | /// 29 | /// Gets the query parameters (or an empty collection) 30 | /// 31 | public IEnumerable> QueryParams { get; } 32 | 33 | /// 34 | /// Gets the query properties (or an empty collection) 35 | /// 36 | public IEnumerable> QueryProperties { get; } 37 | 38 | /// 39 | /// Gets the RequestInfo representing the request 40 | /// 41 | public IRequestInfo RequestInfo { get; } 42 | 43 | /// 44 | /// Gets the format provider, if any 45 | /// 46 | public IFormatProvider? FormatProvider { get; } 47 | 48 | /// 49 | /// Initialises a new instance of the class 50 | /// 51 | /// Initial query string, present from the URI the user specified in the Get/etc parameter 52 | /// The raw query parameters, if any 53 | /// The query parameters (or an empty collection) 54 | /// The query propeorties (or an empty collection) 55 | /// RequestInfo representing the request 56 | /// Format provider to use to format things 57 | public QueryStringBuilderInfo( 58 | string initialQueryString, 59 | IEnumerable rawQueryParameters, 60 | IEnumerable> queryParams, 61 | IEnumerable> queryProperties, 62 | IRequestInfo requestInfo, 63 | IFormatProvider? formatProvider) 64 | { 65 | this.InitialQueryString = initialQueryString; 66 | this.RawQueryParameters = rawQueryParameters; 67 | this.QueryParams = queryParams; 68 | this.QueryProperties = queryProperties; 69 | this.RequestInfo = requestInfo; 70 | this.FormatProvider = formatProvider; 71 | } 72 | 73 | /// 74 | /// Obsolete. Use the other constructor. 75 | /// 76 | [Obsolete("Use the other constructor")] 77 | public QueryStringBuilderInfo( 78 | string initialQueryString, 79 | string rawQueryParameter, 80 | IEnumerable> queryParams, 81 | IEnumerable> queryProperties, 82 | IRequestInfo requestInfo, 83 | IFormatProvider? formatProvider) 84 | : this(initialQueryString, new[] { rawQueryParameter }, queryParams, queryProperties, requestInfo, formatProvider) 85 | { 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/RestEase/RequestBodySerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | 4 | namespace RestEase 5 | { 6 | #pragma warning disable CS0618 // Type or member is obsolete 7 | /// 8 | /// Helper which knows how to serialize a request body 9 | /// 10 | public abstract class RequestBodySerializer : IRequestBodySerializer 11 | { 12 | [Obsolete("Override SerializeBody(T body, RequestBodySerializerInfo info) instead", error: true)] 13 | HttpContent IRequestBodySerializer.SerializeBody(T body) 14 | { 15 | // This exists only so that we can assign instances of ResponseDeserializer to the IResponseDeserializer in RestClient 16 | throw new InvalidOperationException("This should never be called"); 17 | } 18 | 19 | /// 20 | /// Serialize the given request body 21 | /// 22 | /// Body to serialize 23 | /// Extra information about the request 24 | /// Type of the body to serialize 25 | /// HttpContent to assign to the request 26 | public virtual HttpContent? SerializeBody(T body, RequestBodySerializerInfo info) 27 | { 28 | throw new NotImplementedException($"You must override and implement SerializeBody(T body, RequestBodySerializerInfo info) in {this.GetType().Name}"); 29 | } 30 | } 31 | #pragma warning restore CS0618 // Type or member is obsolete 32 | } 33 | -------------------------------------------------------------------------------- /src/RestEase/RequestBodySerializerInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase 4 | { 5 | /// 6 | /// Encapsulates extra information provided to 7 | /// 8 | /// 9 | /// This is broken out as a separate structure so that extra properties can be added without breaking backwards compatibility 10 | /// 11 | public readonly struct RequestBodySerializerInfo 12 | { 13 | /// 14 | /// Gets information about the request 15 | /// 16 | public IRequestInfo RequestInfo { get; } 17 | 18 | /// 19 | /// Gets the format provider. If this is null, the default will be used. 20 | /// Specified by the user on 21 | /// 22 | public IFormatProvider? FormatProvider { get; } 23 | 24 | /// 25 | /// Initialises a new instance of the structure 26 | /// 27 | /// Information about the request 28 | /// Format provider to use 29 | public RequestBodySerializerInfo(IRequestInfo requestInfo, IFormatProvider? formatProvider) 30 | { 31 | this.RequestInfo = requestInfo; 32 | this.FormatProvider = formatProvider; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/RestEase/RequestModifier.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace RestEase 6 | { 7 | /// 8 | /// Delegate used to modify outgoing HttpRequestMessages 9 | /// 10 | /// Request to modify 11 | /// CancellationToken to abort this request 12 | /// A task which completes when modification has occurred 13 | public delegate Task RequestModifier(HttpRequestMessage request, CancellationToken cancellationToken); 14 | } 15 | -------------------------------------------------------------------------------- /src/RestEase/RequestPathParamSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace RestEase 2 | { 3 | /// 4 | /// Helper which knows how to serialize path parameters 5 | /// 6 | public abstract class RequestPathParamSerializer 7 | { 8 | /// 9 | /// Serialize a path parameter whose value is scalar (not a collection), into a string value 10 | /// 11 | /// Type of the value to serialize 12 | /// Value of the path parameter 13 | /// Extra info which may be useful to the serializer 14 | /// A string value to use as path parameter 15 | public abstract string? SerializePathParam(T value, RequestPathParamSerializerInfo info); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/RestEase/RequestPathParamSerializerInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase 4 | { 5 | /// 6 | /// Encapsulates extra information provides to 7 | /// 8 | /// 9 | /// This is broken out as a separate structure so that extra properties can be added without breaking backwards compatibility 10 | /// 11 | public readonly struct RequestPathParamSerializerInfo 12 | { 13 | /// 14 | /// Gets information about the request 15 | /// 16 | public IRequestInfo RequestInfo { get; } 17 | 18 | /// 19 | /// Gets the format string specified using 20 | /// 21 | public string? Format { get; } 22 | 23 | /// 24 | /// Gets the format provider. If this is null, the default will be used. 25 | /// Specified by the user on 26 | /// 27 | public IFormatProvider? FormatProvider { get; } 28 | 29 | /// 30 | /// Initialises a new instance of the structure 31 | /// 32 | /// Information about the request 33 | /// Format string specified using 34 | /// Format provider to use 35 | public RequestPathParamSerializerInfo(IRequestInfo requestInfo, string? format, IFormatProvider? formatProvider) 36 | { 37 | this.RequestInfo = requestInfo; 38 | this.Format = format; 39 | this.FormatProvider = formatProvider; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/RestEase/RequestQueryParamSerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace RestEase 5 | { 6 | #pragma warning disable CS0618 // Type or member is obsolete 7 | /// 8 | /// Helper which knows how to serialize query parmaeters 9 | /// 10 | public abstract class RequestQueryParamSerializer : IRequestQueryParamSerializer 11 | #pragma warning restore CS0618 // Type or member is obsolete 12 | { 13 | /// 14 | /// Serialize a query parameter whose value is a collection, into a collection of name -> value pairs 15 | /// 16 | /// 17 | /// Most of the time, you will return a single KeyValuePair for each value in the collection, and all will have 18 | /// the same key. However this is not required: you can return whatever you want. 19 | /// 20 | /// Type of the value to serialize 21 | /// Name of the query parameter 22 | /// Values of the query parmaeter 23 | /// Extra info which may be useful to the serializer 24 | /// A colletion of name -> value pairs to use as query parameters 25 | public virtual IEnumerable> SerializeQueryCollectionParam(string name, IEnumerable values, RequestQueryParamSerializerInfo info) 26 | { 27 | throw new NotImplementedException($"You must override and implement SerializeQueryCollectionParam(string name, IEnumerable values, RequestQueryParamSerializerInfo info) in {this.GetType().Name}"); 28 | } 29 | 30 | /// 31 | /// Serialize a query parameter whose value is scalar (not a collection), into a collection of name -> value pairs 32 | /// 33 | /// 34 | /// Most of the time, you will only return a single KeyValuePair from this method. However, you are given the flexibility, 35 | /// to return multiple KeyValuePairs if you wish. Duplicate keys are allowed: they will be serialized as separate query parameters. 36 | /// 37 | /// Type of the value to serialize 38 | /// Name of the query parameter 39 | /// Value of the query parameter 40 | /// Extra info which may be useful to the serializer 41 | /// A colletion of name -> value pairs to use as query parameters 42 | public virtual IEnumerable> SerializeQueryParam(string name, T value, RequestQueryParamSerializerInfo info) 43 | { 44 | throw new NotImplementedException($"You must override and implement SerializeQueryParam(string name, T value, RequestQueryParamSerializerInfo info) in {this.GetType().Name}"); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/RestEase/RequestQueryParamSerializerInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestEase 4 | { 5 | /// 6 | /// Encapsulates extra information provides to 7 | /// 8 | /// 9 | /// This is broken out as a separate structure so that extra properties can be added without breaking backwards compatibility 10 | /// 11 | public readonly struct RequestQueryParamSerializerInfo 12 | { 13 | /// 14 | /// Gets information about the request 15 | /// 16 | public IRequestInfo RequestInfo { get; } 17 | 18 | /// 19 | /// Gets the format string specified using 20 | /// 21 | public string? Format { get; } 22 | 23 | /// 24 | /// Gets the format provider. If this is null, the default will be used. 25 | /// Specified by the user on 26 | /// 27 | public IFormatProvider? FormatProvider { get; } 28 | 29 | /// 30 | /// Initialises a new instance of the structure 31 | /// 32 | /// Information about the request 33 | /// Format string specified using 34 | /// Format provider to use 35 | public RequestQueryParamSerializerInfo(IRequestInfo requestInfo, string? format, IFormatProvider? formatProvider) 36 | { 37 | this.RequestInfo = requestInfo; 38 | this.Format = format; 39 | this.FormatProvider = formatProvider; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/RestEase/Response.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | 4 | namespace RestEase 5 | { 6 | /// 7 | /// Response containing both the HttpResponseMessage and deserialized response 8 | /// 9 | /// Type of deserialized response 10 | public class Response : IDisposable 11 | { 12 | private readonly Func contentDeserializer; 13 | private bool contentDeserialized; 14 | private T deserializedContent = default!; 15 | 16 | /// 17 | /// Gets the raw HttpResponseMessage 18 | /// 19 | public HttpResponseMessage ResponseMessage { get; private set; } 20 | 21 | /// 22 | /// Gets the string content of the response, if there is a response 23 | /// 24 | public string? StringContent { get; private set; } 25 | 26 | /// 27 | /// Gets the deserialized response 28 | /// 29 | /// The deserialized content 30 | public T GetContent() 31 | { 32 | if (!this.contentDeserialized) 33 | { 34 | this.deserializedContent = this.contentDeserializer(); 35 | this.contentDeserialized = true; 36 | } 37 | 38 | return this.deserializedContent; 39 | } 40 | 41 | /// 42 | /// Initialises a new instance of the class 43 | /// 44 | /// String content read from the response 45 | /// HttpResponseMessage received 46 | /// Func which will deserialize the content into a T 47 | public Response(string? content, HttpResponseMessage response, Func contentDeserializer) 48 | { 49 | this.StringContent = content; 50 | this.ResponseMessage = response; 51 | this.contentDeserializer = contentDeserializer; 52 | } 53 | 54 | /// 55 | /// Disposes the underlying 56 | /// 57 | public void Dispose() 58 | { 59 | this.ResponseMessage.Dispose(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/RestEase/ResponseDeserializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | 4 | namespace RestEase 5 | { 6 | #pragma warning disable CS0618 // Type or member is obsolete 7 | /// 8 | /// Helper capable of deserializing a response, to return to the caller 9 | /// 10 | public abstract class ResponseDeserializer : IResponseDeserializer 11 | { 12 | /// 13 | /// Gets or sets a value indicating whether this deserializer can deserialize strings 14 | /// 15 | /// 16 | /// If true, interface methods which return Task{string} (or Task{Response{string}} 17 | /// result in the response being passed through this deserializer. If false, such methods result in 18 | /// the raw response being returned, and not passed through this deserializer. 19 | /// 20 | /// The default value is false. 21 | /// 22 | public bool HandlesStrings { get; set; } = false; 23 | 24 | [Obsolete("Override Deserialize(string content, HttpResponseMessage response, ResponseDeserializerInfo info) instead", error: true)] 25 | T IResponseDeserializer.Deserialize(string? content, HttpResponseMessage response) 26 | { 27 | // This exists only so that we can assign instances of ResponseDeserializer to the IResponseDeserializer in RestClient 28 | throw new InvalidOperationException("This should never be called"); 29 | } 30 | 31 | /// 32 | /// Read the response string from the response, deserialize, and return a deserialized object 33 | /// 34 | /// Type of object to deserialize into 35 | /// String content read from the response 36 | /// HttpResponseMessage. Consider calling response.Content.ReadAsStringAsync() to retrieve a string 37 | /// Extra information about the response 38 | /// Deserialized response 39 | public virtual T Deserialize(string? content, HttpResponseMessage response, ResponseDeserializerInfo info) 40 | { 41 | throw new NotImplementedException($"You must override and implement T Deserialize(string content, HttpResponseMessage response, ResponseDeserializerInfo info) in {this.GetType().Name}"); 42 | } 43 | } 44 | #pragma warning restore CS0618 // Type or member is obsolete 45 | } 46 | -------------------------------------------------------------------------------- /src/RestEase/ResponseDeserializerInfo.cs: -------------------------------------------------------------------------------- 1 | namespace RestEase 2 | { 3 | /// 4 | /// Encapsulates extra information provides to 5 | /// 6 | /// 7 | /// This is broken out as a separate structure so that extra properties can be added without breaking backwards compatibility 8 | /// 9 | public readonly struct ResponseDeserializerInfo 10 | { 11 | /// 12 | /// Gets information about the request 13 | /// 14 | public IRequestInfo RequestInfo { get; } 15 | 16 | /// 17 | /// Initialises a new instance of the structure 18 | /// 19 | /// Information about the request 20 | public ResponseDeserializerInfo(IRequestInfo requestInfo) 21 | { 22 | this.RequestInfo = requestInfo; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/RestEase/RestEase.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0;netstandard2.1;netstandard2.0;netstandard1.1;net452 5 | false 6 | 10.0 7 | enable 8 | true 9 | 10 | 1.5.5 11 | 12 | 0.0.0 13 | ../../NuGet 14 | RestEase 15 | REST;JSON 16 | Copyright © Antony Male 2015-2022 17 | README.md 18 | icon.png 19 | https://github.com/canton7/RestEase 20 | MIT 21 | git 22 | https://github.com/canton7/RestEase 23 | Antony Male 24 | 25 | Easy-to-use typesafe REST API client library, which is simple and customisable. 26 | 27 | Write a C# interface which describes your API, and RestEase generates an implementation you can call into. 28 | 29 | Source Generators are here! Reference the RestEase.SourceGenerator NuGet package. 30 | 31 | 32 | 33 | 34 | true 35 | snupkg 36 | true 37 | portable 38 | 39 | 40 | false 41 | 1.6.1 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | <_Parameter1>RestEase.UnitTests 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/RestEase/StringEnumRequestPathParamSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Reflection; 3 | using RestEase.Implementation; 4 | using System.Linq; 5 | using RestEase.Platform; 6 | 7 | #if !NETSTANDARD1_1 8 | using System.ComponentModel; 9 | using System.Runtime.Serialization; 10 | #endif 11 | 12 | namespace RestEase 13 | { 14 | /// 15 | /// 16 | /// A serializer that handles enum values specially, serializing them into their display value 17 | /// as defined by a EnumMember, DisplayName, or Display attribute (in that order). 18 | /// 19 | public class StringEnumRequestPathParamSerializer : RequestPathParamSerializer 20 | { 21 | private static readonly ConcurrentDictionary cache = new(); 22 | 23 | /// 24 | /// 25 | /// Serialize a path parameter whose value is scalar (not a collection), into a string value 26 | /// 27 | /// Type of the value to serialize 28 | /// Value of the path parameter 29 | /// Extra info which may be useful to the serializer 30 | /// A string value to use as path parameter 31 | /// 32 | /// If the value is an enum value, the serializer will check if it has an EnumMember, DisplayName or Display 33 | /// attribute, and if so return the value of that instead (in that order of preference). 34 | /// 35 | public override string? SerializePathParam(T value, RequestPathParamSerializerInfo info) 36 | { 37 | if (value == null) 38 | { 39 | return null; 40 | } 41 | 42 | var typeInfo = typeof(T).GetTypeInfo(); 43 | 44 | if (!typeInfo.IsEnum) 45 | { 46 | return ToStringHelper.ToString(value, info.Format, info.FormatProvider); 47 | } 48 | 49 | if (cache.TryGetValue(value, out string? stringValue)) 50 | { 51 | return stringValue; 52 | } 53 | 54 | stringValue = value.ToString()!; 55 | 56 | var fieldInfo = typeInfo.GetField(stringValue); 57 | 58 | if (fieldInfo == null) 59 | { 60 | return CacheAdd(value, stringValue); 61 | } 62 | 63 | #if !NETSTANDARD1_1 64 | var enumMemberAttribute = fieldInfo.GetCustomAttribute(); 65 | 66 | if (enumMemberAttribute?.Value != null) 67 | { 68 | return CacheAdd(value, enumMemberAttribute.Value); 69 | } 70 | 71 | var displayNameAttribute = fieldInfo.GetCustomAttribute(); 72 | 73 | if (displayNameAttribute != null) 74 | { 75 | return CacheAdd(value, displayNameAttribute.DisplayName); 76 | } 77 | #endif 78 | 79 | // netstandard can get this by referencing System.ComponentModel.DataAnnotations, (and framework 80 | // can get this by referencing the assembly). However we don't want a dependency on this nuget package, 81 | // for something so niche, so do a reflection-only load 82 | var displayAttribute = fieldInfo.CustomAttributes 83 | .FirstOrDefault(x => x.AttributeType.FullName == "System.ComponentModel.DataAnnotations.DisplayAttribute"); 84 | if (displayAttribute != null) 85 | { 86 | object? name = displayAttribute.NamedArguments.FirstOrDefault(x => x.MemberName == "Name").TypedValue.Value; 87 | if (name != null) 88 | { 89 | return CacheAdd(value, (string)name); 90 | } 91 | } 92 | 93 | return CacheAdd(value, stringValue); 94 | } 95 | 96 | private static string CacheAdd(object key, string value) 97 | { 98 | cache.TryAdd(key, value); 99 | return value; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/SourceGeneratorSandbox/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using RestEase; 5 | 6 | namespace SourceGeneratorSandbox 7 | { 8 | public class Program 9 | { 10 | public static void Main() 11 | { 12 | var impl = RestClient.For("https://api.example.com"); 13 | impl.FooAsync("test", "test").Wait(); 14 | } 15 | 16 | public interface ISomeApi 17 | { 18 | [Get("foo/{bar}")] 19 | Task FooAsync([Path] string bar, string baz); 20 | } 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/SourceGeneratorSandbox/SourceGeneratorSandbox.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net6.0 6 | preview 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | --------------------------------------------------------------------------------