├── .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 |
--------------------------------------------------------------------------------
/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