diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..f9bdcf4
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,4 @@
+[*.{cs,vb}]
+
+# IDE0037: Use inferred member name
+dotnet_diagnostic.IDE0037.severity = silent
diff --git a/.gitignore b/.gitignore
index 954c945..c57855c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -397,4 +397,5 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
-/Graeae.Models/OpenApi.Models.xml
+*.xml
+
diff --git a/Graeae.AspNet.Analyzer/AdditionalOperationsAnalyzer.cs b/Graeae.AspNet.Analyzer/AdditionalOperationsAnalyzer.cs
new file mode 100644
index 0000000..09f25c4
--- /dev/null
+++ b/Graeae.AspNet.Analyzer/AdditionalOperationsAnalyzer.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using Graeae.Models;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Yaml2JsonNode;
+
+namespace Graeae.AspNet.Analyzer;
+
+///
+/// Outputs diagnostics for handlers that handle routes or operations that are not listed in the OAI description.
+///
+[Generator(LanguageNames.CSharp)]
+internal class AdditionalOperationsAnalyzer : IIncrementalGenerator
+{
+ private static OpenApiDocument[]? OpenApiDocs { get; set; }
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ OpenApiDocs = null;
+
+ var handlerClasses = context.SyntaxProvider.CreateSyntaxProvider(HandlerClassPredicate, HandlerClassTransform)
+ .Where(x => x is not null);
+
+ var files = context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("openapi.yaml"));
+ var namesAndContents = files.Select((f, ct) => (Name: Path.GetFileNameWithoutExtension(f.Path), Content: f.GetText(ct)?.ToString(), Path: f.Path));
+
+ context.RegisterSourceOutput(handlerClasses.Combine(namesAndContents.Collect()), AddDiagnostics);
+ }
+
+ private static bool HandlerClassPredicate(SyntaxNode node, CancellationToken token)
+ {
+ return node is ClassDeclarationSyntax { AttributeLists.Count: > 0 };
+ }
+
+ private static (string, ClassDeclarationSyntax)? HandlerClassTransform(GeneratorSyntaxContext context, CancellationToken token)
+ {
+ var classDeclaration = (ClassDeclarationSyntax)context.Node;
+ var symbol = context.SemanticModel.GetDeclaredSymbol(context.Node);
+
+ if (symbol is INamedTypeSymbol &&
+ classDeclaration.TryGetAttribute("Graeae.AspNet.RequestHandlerAttribute", context.SemanticModel, token, out var attribute) &&
+ attribute!.TryGetStringParameter(out var route))
+ {
+ return (route!, classDeclaration);
+ }
+
+ return null;
+ }
+
+ private static void AddDiagnostics(SourceProductionContext context, ((string Route, ClassDeclarationSyntax Type)? Handler, ImmutableArray<(string Name, string? Content, string Path)> Files) source)
+ {
+ try
+ {
+ OpenApiDocs ??= source.Files.Select(file =>
+ {
+ if (file.Content == null)
+ throw new Exception("Failed to read file \"" + file.Path + "\"");
+ var doc = YamlSerializer.Deserialize(file.Content);
+ doc!.Initialize().Wait();
+
+ return doc;
+ }).ToArray();
+
+ var handler = source.Handler!.Value;
+
+ var allPaths = OpenApiDocs.SelectMany(x => x.Paths).ToList();
+ var path = allPaths.FirstOrDefault(x => x.Key.ToString() == handler.Route);
+ if (path.Key is null)
+ {
+ context.ReportDiagnostic(Diagnostics.AdditionalRouteHandler(handler.Route));
+ return;
+ }
+
+ var route = path.Key;
+ var pathItem = path.Value;
+
+ var methods = handler.Type.Members.OfType().ToArray();
+
+ foreach (var method in methods)
+ {
+ var (op, name) = GetMatchingOperation(method, pathItem);
+ if (!OperationExists(route, op, method))
+ context.ReportDiagnostic(Diagnostics.AdditionalRouteOperationHandler(route.ToString(), name!));
+ }
+ }
+ catch (Exception e)
+ {
+ //Debug.Break();
+ var errorMessage = $"Error: {e.Message}\n\nStack trace: {e.StackTrace}\n\nStack trace: {e.InnerException?.StackTrace}";
+ context.ReportDiagnostic(Diagnostics.OperationalError(errorMessage));
+ }
+ }
+
+ private static (Operation? Op, string? Name) GetMatchingOperation(MethodDeclarationSyntax method, PathItem pathItem) =>
+ method.Identifier.ValueText.ToUpperInvariant() switch
+ {
+ "GET" => (pathItem.Get, nameof(pathItem.Get)),
+ "POST" => (pathItem.Post, nameof(pathItem.Post)),
+ "PUT" => (pathItem.Put, nameof(pathItem.Put)),
+ "DELETE" => (pathItem.Delete, nameof(pathItem.Delete)),
+ "TRACE" => (pathItem.Trace, nameof(pathItem.Trace)),
+ "OPTIONS" => (pathItem.Options, nameof(pathItem.Options)),
+ "HEAD" => (pathItem.Head, nameof(pathItem.Head)),
+ _ => (null, null)
+ };
+
+ private static bool OperationExists(PathTemplate route, Operation? op, MethodDeclarationSyntax method)
+ {
+ if (op is null) return false;
+
+ // parameters can be implicitly or explicitly bound
+ //
+ // - path
+ // - implicitly bound by name
+ // - explicitly bound with [FromRoute(Name = "name")]
+ // - query
+ // - implicitly bound by name
+ // - explicitly bound with [FromQuery(Name = "name")]
+ // - header
+ // - explicitly bound with [FromHeader(Name = "name")]
+ // - body
+ // - implicitly bound by model
+
+ var implicitOpenApiParameters = route.Segments.Select(x =>
+ {
+ var match = PathHelpers.TemplatedSegmentPattern.Match(x);
+ return match.Success
+ ? new Parameter(match.Groups["param"].Value, ParameterLocation.Path)
+ : null;
+ }).Where(x => x is not null);
+ if (op.RequestBody is not null)
+ implicitOpenApiParameters = implicitOpenApiParameters.Append(Parameter.Body);
+ var explicitOpenapiParameters = op.Parameters?.Select(x => new Parameter(x.Name, x.In)) ?? [];
+ var openApiParameters = implicitOpenApiParameters.Union(explicitOpenapiParameters).ToArray();
+ var methodParameterList = method.ParameterList.Parameters.SelectMany(AnalysisExtensions.GetParameters);
+
+ return openApiParameters.All(x => methodParameterList.Contains(x));
+ }
+}
\ No newline at end of file
diff --git a/Graeae.AspNet.Analyzer/AnalysisExtensions.cs b/Graeae.AspNet.Analyzer/AnalysisExtensions.cs
new file mode 100644
index 0000000..4367436
--- /dev/null
+++ b/Graeae.AspNet.Analyzer/AnalysisExtensions.cs
@@ -0,0 +1,145 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using Graeae.Models;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Graeae.AspNet.Analyzer;
+
+internal static class AnalysisExtensions
+{
+ public static bool TryGetAttribute(this ClassDeclarationSyntax candidate, string attributeName, SemanticModel semanticModel, CancellationToken cancellationToken, out AttributeSyntax? value)
+ {
+ foreach (var attributeList in candidate.AttributeLists)
+ {
+ foreach (var attribute in attributeList.Attributes)
+ {
+ var info = semanticModel.GetSymbolInfo(attribute, cancellationToken);
+ var symbol = info.Symbol;
+
+ if (symbol is IMethodSymbol method
+ && method.ContainingType.ToDisplayString().Equals(attributeName, StringComparison.Ordinal))
+ {
+ value = attribute;
+ return true;
+ }
+ }
+ }
+
+ value = null;
+ return false;
+ }
+
+ public static bool TryGetStringParameter(this AttributeSyntax attribute, out string? value)
+ {
+ if (attribute.ArgumentList is
+ {
+ Arguments.Count: 1,
+ } argumentList)
+ {
+ var argument = argumentList.Arguments[0];
+
+ if (argument.Expression is LiteralExpressionSyntax literal)
+ {
+ value = literal.Token.Value?.ToString();
+ return true;
+ }
+ }
+
+ value = null;
+ return false;
+ }
+
+ public static IEnumerable GetParameters(this ParameterSyntax parameter)
+ {
+ if (TryGetAttribute(parameter.AttributeLists, "FromRoute", out var attribute) &&
+ TryGetStringParameter(attribute!, out var name))
+ yield return new Parameter(name!, ParameterLocation.Path);
+ else if (TryGetAttribute(parameter.AttributeLists, "FromQuery", out attribute) &&
+ TryGetStringParameter(attribute!, out name))
+ yield return new Parameter(name!, ParameterLocation.Query);
+ else if (TryGetAttribute(parameter.AttributeLists, "FromHeader", out attribute) &&
+ TryGetStringParameter(attribute!, out name))
+ yield return new Parameter(name!, ParameterLocation.Header);
+ else if (TryGetAttribute(parameter.AttributeLists, "FromBody", out _))
+ yield return Parameter.Body;
+ else if (TryGetAttribute(parameter.AttributeLists, "FromServices", out _))
+ {
+ }
+ else
+ {
+ // if no attributes are found then consider all implicit options
+ yield return new Parameter(parameter.Identifier.ValueText, ParameterLocation.Path);
+ yield return new Parameter(parameter.Identifier.ValueText, ParameterLocation.Query);
+ // TODO: this is catching services and the http context
+ //yield return Parameter.Body;
+ }
+ }
+
+ private static bool TryGetAttribute(SyntaxList attributeLists, string attributeName, out AttributeSyntax? attribute)
+ {
+ foreach (var attributeList in attributeLists)
+ {
+ foreach (var att in attributeList.Attributes)
+ {
+ if (att.Name.ToString() == attributeName)
+ {
+ attribute = att;
+ return true;
+ }
+ }
+ }
+
+ attribute = null;
+ return false;
+ }
+
+ ///
+ /// determine the namespace the class/enum/struct is declared in, if any
+ ///
+ public static string GetNamespace(this BaseTypeDeclarationSyntax syntax)
+ {
+ // If we don't have a namespace at all we'll return an empty string
+ // This accounts for the "default namespace" case
+ string nameSpace = string.Empty;
+
+ // Get the containing syntax node for the type declaration
+ // (could be a nested type, for example)
+ SyntaxNode? potentialNamespaceParent = syntax.Parent;
+
+ // Keep moving "out" of nested classes etc until we get to a namespace
+ // or until we run out of parents
+ while (potentialNamespaceParent != null &&
+ potentialNamespaceParent is not NamespaceDeclarationSyntax
+ && potentialNamespaceParent is not FileScopedNamespaceDeclarationSyntax)
+ {
+ potentialNamespaceParent = potentialNamespaceParent.Parent;
+ }
+
+ // Build up the final namespace by looping until we no longer have a namespace declaration
+ if (potentialNamespaceParent is BaseNamespaceDeclarationSyntax namespaceParent)
+ {
+ // We have a namespace. Use that as the type
+ nameSpace = namespaceParent.Name.ToString();
+
+ // Keep moving "out" of the namespace declarations until we
+ // run out of nested namespace declarations
+ while (true)
+ {
+ if (namespaceParent.Parent is not NamespaceDeclarationSyntax parent)
+ {
+ break;
+ }
+
+ // Add the outer namespace as a prefix to the final namespace
+ nameSpace = $"{namespaceParent.Name}.{nameSpace}";
+ namespaceParent = parent;
+ }
+ }
+
+ // return the final namespace
+ return nameSpace;
+ }
+}
\ No newline at end of file
diff --git a/Graeae.AspNet.Analyzer/Debug.cs b/Graeae.AspNet.Analyzer/Debug.cs
new file mode 100644
index 0000000..f3e564a
--- /dev/null
+++ b/Graeae.AspNet.Analyzer/Debug.cs
@@ -0,0 +1,12 @@
+using System.Diagnostics;
+
+namespace Graeae.AspNet.Analyzer;
+
+internal static class Debug
+{
+ [Conditional("DEBUG")]
+ public static void Inject()
+ {
+ if (!Debugger.IsAttached) Debugger.Launch();
+ }
+}
\ No newline at end of file
diff --git a/Graeae.AspNet.Analyzer/Diagnostics.cs b/Graeae.AspNet.Analyzer/Diagnostics.cs
new file mode 100644
index 0000000..009b5fe
--- /dev/null
+++ b/Graeae.AspNet.Analyzer/Diagnostics.cs
@@ -0,0 +1,30 @@
+using Microsoft.CodeAnalysis;
+
+namespace Graeae.AspNet.Analyzer;
+
+internal static class Diagnostics
+{
+ public static Diagnostic OperationalError(string message) =>
+ Diagnostic.Create(new("GR0001", "Operational error", message, "Operation", DiagnosticSeverity.Error, true), Location.None, DiagnosticSeverity.Error);
+
+ public static Diagnostic NoPaths(string openApiFilePath) =>
+ Diagnostic.Create(new("GR0002", "No paths", $"No paths are defined in '{openApiFilePath}'", "Path coverage", DiagnosticSeverity.Info, true), Location.None, DiagnosticSeverity.Info);
+
+ public static Diagnostic MissingRouteHandler(string route) =>
+ Diagnostic.Create(new("GR0003", "Route not handled", $"Found no handler type for route '{route}'", "Path coverage", DiagnosticSeverity.Warning, true), Location.None, DiagnosticSeverity.Warning);
+
+ public static Diagnostic MissingRouteOperationHandler(string route, string op) =>
+ Diagnostic.Create(new("GR0004", "Route not handled", $"Found no handler for '{op.ToUpperInvariant()} {route}'", "Path coverage", DiagnosticSeverity.Warning, true), Location.None, DiagnosticSeverity.Warning);
+
+ public static Diagnostic AdditionalRouteHandler(string route) =>
+ Diagnostic.Create(new("GR0005", "Route not published", $"Found handler type for route '{route}' but it does not appear in the OpenAPI definition", "Path coverage", DiagnosticSeverity.Warning, true), Location.None, DiagnosticSeverity.Warning);
+
+ public static Diagnostic AdditionalRouteOperationHandler(string route, string op) =>
+ Diagnostic.Create(new("GR0006", "Route not published", $"Found handler for '{op.ToUpperInvariant()} {route}' but it does not appear in the OpenAPI definition", "Path coverage", DiagnosticSeverity.Warning, true), Location.None, DiagnosticSeverity.Warning);
+
+ public static Diagnostic ExternalFileAdded(string filePath) =>
+ Diagnostic.Create(new("GR0007", "Document load success", $"File {filePath} added to document resolver", "OpenAPI docs", DiagnosticSeverity.Info, true), null, filePath);
+
+ public static Diagnostic ExternalFileNotAdded(string filePath) =>
+ Diagnostic.Create(new DiagnosticDescriptor("GR0008", "Document load failure", $"File {filePath} could not be added to document resolver", "OpenAPI docs", DiagnosticSeverity.Warning, true), null, filePath);
+}
\ No newline at end of file
diff --git a/Graeae.AspNet.Analyzer/Directory.Build.props b/Graeae.AspNet.Analyzer/Directory.Build.props
new file mode 100644
index 0000000..512ee4c
--- /dev/null
+++ b/Graeae.AspNet.Analyzer/Directory.Build.props
@@ -0,0 +1,239 @@
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(GetTargetPathDependsOn);GetDependencyTargetPaths
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Graeae.AspNet.Analyzer/Graeae.AspNet.Analyzer.csproj b/Graeae.AspNet.Analyzer/Graeae.AspNet.Analyzer.csproj
new file mode 100644
index 0000000..81b213e
--- /dev/null
+++ b/Graeae.AspNet.Analyzer/Graeae.AspNet.Analyzer.csproj
@@ -0,0 +1,68 @@
+
+
+
+ netstandard2.0
+ latest
+ enable
+
+
+
+ true
+ true
+ false
+ true
+ true
+ true
+ true
+
+ Greg Dennis
+ Analyzer for Graeae.AspNet
+ LICENSE
+ https://github.com/gregsdennis/Graeae
+ openapi.png
+ https://github.com/gregsdennis/Graeae
+ openapi json schema aspnet webapi minimalapi api roslyn analyzer
+ Release notes can be found at https://github.com/gregsdennis/Graeae
+ true
+ Graeae.AspNet.xml
+ 0.1.0-preview1
+ 0.1.0
+ 0.1.0.0
+ false
+ true
+ true
+ true
+ ../openapi.snk
+ README.md
+
+ true
+
+
+
+
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+
+ True
+ \
+
+
+
+
+
+
+
+
+
diff --git a/Graeae.AspNet.Analyzer/MissingOperationsAnalyzer.cs b/Graeae.AspNet.Analyzer/MissingOperationsAnalyzer.cs
new file mode 100644
index 0000000..c2afad8
--- /dev/null
+++ b/Graeae.AspNet.Analyzer/MissingOperationsAnalyzer.cs
@@ -0,0 +1,138 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using Graeae.Models;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Yaml2JsonNode;
+
+namespace Graeae.AspNet.Analyzer;
+
+///
+/// Outputs diagnostics when the OAI description defines routes and operations that aren't implemented.
+///
+[Generator(LanguageNames.CSharp)]
+internal class MissingOperationsAnalyzer : IIncrementalGenerator
+{
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var handlerClasses = context.SyntaxProvider.CreateSyntaxProvider(HandlerClassPredicate, HandlerClassTransform)
+ .Where(x => x is not null);
+
+ var files = context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("openapi.yaml"));
+ var namesAndContents = files.Select((f, ct) => (Name: Path.GetFileNameWithoutExtension(f.Path), Content: f.GetText(ct)?.ToString(), Path: f.Path));
+
+ context.RegisterSourceOutput(namesAndContents.Combine(handlerClasses.Collect()), AddDiagnostics);
+ }
+
+ private static bool HandlerClassPredicate(SyntaxNode node, CancellationToken token)
+ {
+ return node is ClassDeclarationSyntax { AttributeLists.Count: > 0 };
+ }
+
+ private static (string, ClassDeclarationSyntax)? HandlerClassTransform(GeneratorSyntaxContext context, CancellationToken token)
+ {
+ var classDeclaration = (ClassDeclarationSyntax)context.Node;
+ var symbol = context.SemanticModel.GetDeclaredSymbol(context.Node);
+
+ if (symbol is INamedTypeSymbol &&
+ classDeclaration.TryGetAttribute("Graeae.AspNet.RequestHandlerAttribute", context.SemanticModel, token, out var attribute) &&
+ attribute!.TryGetStringParameter(out var route))
+ {
+ return (route!, classDeclaration);
+ }
+
+ return null;
+ }
+
+ private static void AddDiagnostics(SourceProductionContext context, ((string Name, string? Content, string Path) File, ImmutableArray<(string Route, ClassDeclarationSyntax Type)?> Handlers) source)
+ {
+ try
+ {
+ var file = source.File;
+ if (file.Content == null)
+ throw new Exception("Failed to read file \"" + file.Path + "\"");
+
+ var doc = YamlSerializer.Deserialize(file.Content);
+ doc!.Initialize().Wait();
+
+ if (doc.Paths == null)
+ {
+ context.ReportDiagnostic(Diagnostics.NoPaths(file.Path));
+ return;
+ }
+
+ foreach (var entry in doc.Paths)
+ {
+ var route = entry.Key.ToString();
+ var handlerType = source.Handlers.FirstOrDefault(x => x?.Route == route)?.Type;
+
+ if (handlerType is null)
+ {
+ context.ReportDiagnostic(Diagnostics.MissingRouteHandler(route));
+ continue;
+ }
+
+ var methods = handlerType.Members.OfType().ToArray();
+
+ if (!MethodExists(entry.Key, entry.Value.Get, nameof(PathItem.Get), methods))
+ context.ReportDiagnostic(Diagnostics.MissingRouteOperationHandler(route, nameof(PathItem.Get)));
+ if (!MethodExists(entry.Key, entry.Value.Post, nameof(PathItem.Post), methods))
+ context.ReportDiagnostic(Diagnostics.MissingRouteOperationHandler(route, nameof(PathItem.Post)));
+ if (!MethodExists(entry.Key, entry.Value.Put, nameof(PathItem.Put), methods))
+ context.ReportDiagnostic(Diagnostics.MissingRouteOperationHandler(route, nameof(PathItem.Put)));
+ if (!MethodExists(entry.Key, entry.Value.Delete, nameof(PathItem.Delete), methods))
+ context.ReportDiagnostic(Diagnostics.MissingRouteOperationHandler(route, nameof(PathItem.Delete)));
+ if (!MethodExists(entry.Key, entry.Value.Trace, nameof(PathItem.Trace), methods))
+ context.ReportDiagnostic(Diagnostics.MissingRouteOperationHandler(route, nameof(PathItem.Trace)));
+ if (!MethodExists(entry.Key, entry.Value.Options, nameof(PathItem.Options), methods))
+ context.ReportDiagnostic(Diagnostics.MissingRouteOperationHandler(route, nameof(PathItem.Options)));
+ if (!MethodExists(entry.Key, entry.Value.Head, nameof(PathItem.Head), methods))
+ context.ReportDiagnostic(Diagnostics.MissingRouteOperationHandler(route, nameof(PathItem.Head)));
+ }
+ }
+ catch (Exception e)
+ {
+ //Debug.Break();
+ var errorMessage = $"Error: {e.Message}\n\nStack trace: {e.StackTrace}\n\nStack trace: {e.InnerException?.StackTrace}";
+ context.ReportDiagnostic(Diagnostics.OperationalError(errorMessage));
+ }
+ }
+
+ private static bool MethodExists(PathTemplate route, Operation? op, string opName, IEnumerable methods)
+ {
+ if (op is null) return true;
+
+ // parameters can be implicitly or explicitly bound
+ //
+ // - path
+ // - implicitly bound by name
+ // - explicitly bound with [FromRoute(Name = "name")]
+ // - query
+ // - implicitly bound by name
+ // - explicitly bound with [FromQuery(Name = "name")]
+ // - header
+ // - explicitly bound with [FromHeader(Name = "name")]
+ // - body
+ // - implicitly bound by model
+
+ var implicitOpenApiParameters = route.Segments.Select(x =>
+ {
+ var match = PathHelpers.TemplatedSegmentPattern.Match(x);
+ return match.Success
+ ? new Parameter(match.Groups["param"].Value, ParameterLocation.Path)
+ : null;
+ }).Where(x => x is not null);
+ if (op.RequestBody is not null)
+ implicitOpenApiParameters = implicitOpenApiParameters.Append(Parameter.Body);
+ var explicitOpenapiParameters = op.Parameters?.Select(x => new Parameter(x.Name, x.In)) ?? [];
+ var openApiParameters = implicitOpenApiParameters.Union(explicitOpenapiParameters).ToArray();
+ var methodParameterLists = methods.Where(x => string.Equals(x.Identifier.ValueText, opName, StringComparison.InvariantCultureIgnoreCase))
+ .Select(x => x.ParameterList.Parameters.SelectMany(AnalysisExtensions.GetParameters));
+
+ return methodParameterLists.Any(methodParameterList => openApiParameters.All(methodParameterList.Contains));
+ }
+}
\ No newline at end of file
diff --git a/Graeae.AspNet.Analyzer/ModelGenerationAnalyzer.cs b/Graeae.AspNet.Analyzer/ModelGenerationAnalyzer.cs
new file mode 100644
index 0000000..3255b13
--- /dev/null
+++ b/Graeae.AspNet.Analyzer/ModelGenerationAnalyzer.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Threading;
+using Corvus.Json;
+using Corvus.Json.CodeGeneration;
+using Corvus.Json.CodeGeneration.CSharp;
+using Graeae.Models;
+using Json.Schema;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+using Yaml2JsonNode;
+using Encoding = System.Text.Encoding;
+using VocabularyRegistry = Corvus.Json.CodeGeneration.VocabularyRegistry;
+
+namespace Graeae.AspNet.Analyzer;
+
+[Generator(LanguageNames.CSharp)]
+internal class ModelGenerationAnalyzer : IIncrementalGenerator
+{
+ private static string _namespace = null!;
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // need to identify the namespace to generate models in
+ var handlerClasses = context.SyntaxProvider.CreateSyntaxProvider(HandlerClassPredicate, HandlerClassTransform)
+ .Where(x => x is not null);
+ context.RegisterSourceOutput(handlerClasses.Collect(), CacheNamespace);
+
+ // generate the models
+ var files = context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("openapi.yaml"));
+ var namesAndContents = files.Select((f, ct) => (Name: Path.GetFileNameWithoutExtension(f.Path), Content: f.GetText(ct)?.ToString(), Path: f.Path));
+ context.RegisterSourceOutput(namesAndContents.Collect(), AddDiagnostics);
+ }
+
+ private static bool HandlerClassPredicate(SyntaxNode node, CancellationToken token)
+ {
+ return node is ClassDeclarationSyntax { AttributeLists.Count: > 0 };
+ }
+
+ private static string? HandlerClassTransform(GeneratorSyntaxContext context, CancellationToken token)
+ {
+ var classDeclaration = (ClassDeclarationSyntax)context.Node;
+ return classDeclaration.GetNamespace();
+ }
+
+ private static void CacheNamespace(SourceProductionContext context, ImmutableArray namespaces)
+ {
+ _namespace = (namespaces.Distinct().FirstOrDefault() ?? "Graeae.AspNet") + ".Models";
+ }
+
+ private static void AddDiagnostics(SourceProductionContext context, ImmutableArray<(string Name, string? Content, string Path)> files)
+ {
+ try
+ {
+ var documentResolver = new PrepopulatedDocumentResolver();
+ var references = new List();
+
+ foreach (var file in files)
+ {
+ if (file.Content == null)
+ throw new Exception("Failed to read file \"" + file.Path + "\"");
+
+ var yaml = YamlSerializer.Parse(file.Content);
+ var node = yaml.ToJsonNode().FirstOrDefault();
+ if (node is null) continue;
+
+ /*
+ if (!PathHelpers.TryNormalizeSchemaReference(file.Path, out var normalizedUri))
+ {
+ context.ReportDiagnostic(Diagnostics.OperationalError($"Could not normalize path for file '{file.Path}'"));
+ continue;
+ }
+ /*/
+ var normalizedUri = PathHelpers.Normalize(file.Path);
+ //*/
+
+ var doc = JsonDocument.Parse(node.ToString());
+ documentResolver.AddDocument(normalizedUri, doc);
+
+ var openapiDoc = node.Deserialize()!;
+ var schemaLocations = openapiDoc.FindSchemaLocations(normalizedUri);
+ foreach (var schemaLocation in schemaLocations)
+ {
+ references.Add(schemaLocation);
+ }
+ }
+
+ RegisterMetaSchemas(documentResolver);
+ var typeBuilder = new JsonSchemaTypeBuilder(documentResolver, RegisterVocabularies(documentResolver));
+ var typeDeclarations = references.Select(r => typeBuilder.AddTypeDeclarations(r, Corvus.Json.CodeGeneration.Draft202012.VocabularyAnalyser.DefaultVocabulary));
+ var languageProvider = CSharpLanguageProvider.DefaultWithOptions(new CSharpLanguageProvider.Options(_namespace));
+ var generatedCode = typeBuilder.GenerateCodeUsing(languageProvider, CancellationToken.None, typeDeclarations);
+
+ foreach (var codeFile in generatedCode)
+ {
+ context.AddSource(codeFile.FileName, SourceText.From(codeFile.FileContent, Encoding.UTF8));
+ }
+ }
+ catch (Exception e)
+ {
+ Debug.Inject();
+ var errorMessage = $"Error: {e.Message}\n\nStack trace: {e.StackTrace}\n\nStack trace: {e.InnerException?.StackTrace}";
+ context.ReportDiagnostic(Diagnostics.OperationalError(errorMessage));
+ }
+ }
+
+ private static void RegisterMetaSchemas(IDocumentResolver documentResolver)
+ {
+ void RegisterMetaSchema(string uri, JsonSchema schema)
+ {
+ var doc = JsonSerializer.SerializeToDocument(schema);
+ documentResolver.AddDocument(uri, doc);
+ }
+
+ RegisterMetaSchema(MetaSchemas.Draft6Id.ToString(), MetaSchemas.Draft6);
+ RegisterMetaSchema(MetaSchemas.Draft7Id.ToString(), MetaSchemas.Draft7);
+ RegisterMetaSchema(MetaSchemas.Draft201909Id.ToString(), MetaSchemas.Draft201909);
+ RegisterMetaSchema(MetaSchemas.Draft202012Id.ToString(), MetaSchemas.Draft202012);
+ RegisterMetaSchema(Json.Schema.OpenApi.MetaSchemas.OpenApiMetaId.ToString(), Json.Schema.OpenApi.MetaSchemas.OpenApiMeta);
+
+ }
+
+ private static VocabularyRegistry RegisterVocabularies(IDocumentResolver documentResolver)
+ {
+ VocabularyRegistry vocabularyRegistry = new();
+
+ // Add support for the vocabularies we are interested in.
+ Corvus.Json.CodeGeneration.Draft6.VocabularyAnalyser.RegisterAnalyser(vocabularyRegistry);
+ Corvus.Json.CodeGeneration.Draft7.VocabularyAnalyser.RegisterAnalyser(vocabularyRegistry);
+ Corvus.Json.CodeGeneration.Draft201909.VocabularyAnalyser.RegisterAnalyser(documentResolver, vocabularyRegistry);
+ Corvus.Json.CodeGeneration.Draft202012.VocabularyAnalyser.RegisterAnalyser(documentResolver, vocabularyRegistry);
+ Corvus.Json.CodeGeneration.OpenApi30.VocabularyAnalyser.RegisterAnalyser(vocabularyRegistry);
+
+ // And register the custom vocabulary for Corvus extensions.
+ vocabularyRegistry.RegisterVocabularies(Corvus.Json.CodeGeneration.CorvusVocabulary.SchemaVocabulary.DefaultInstance);
+ return vocabularyRegistry;
+ }
+}
\ No newline at end of file
diff --git a/Graeae.AspNet.Analyzer/OpenApiDocumentExtensions.cs b/Graeae.AspNet.Analyzer/OpenApiDocumentExtensions.cs
new file mode 100644
index 0000000..d9e6bb5
--- /dev/null
+++ b/Graeae.AspNet.Analyzer/OpenApiDocumentExtensions.cs
@@ -0,0 +1,133 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using Corvus.Json;
+using Graeae.Models;
+using Json.Schema;
+using JsonPointer = Json.Pointer.JsonPointer;
+
+namespace Graeae.AspNet.Analyzer;
+
+internal static class OpenApiDocumentExtensions
+{
+ public static IEnumerable FindSchemaLocations(this OpenApiDocument openApiDocument, string documentPath)
+ {
+ return GetSchemas(JsonPointer.Create("components"), openApiDocument.Components)
+ .Concat(GetSchemas(JsonPointer.Create("paths"), openApiDocument.Paths))
+ .Concat(GetSchemas(JsonPointer.Create("webhooks"), openApiDocument.Webhooks))
+ .Select(x => new JsonReference(new Uri(documentPath).ToString(), $"#{x}"));
+ }
+
+ private static IEnumerable GetSchemas(JsonPointer baseRoute, ComponentCollection? components)
+ {
+ if (components is null) return [];
+
+ return GetSchemas(baseRoute.Combine("schemas"), components.Schemas)
+ .Concat(GetSchemas(baseRoute.Combine("responses"), components.Responses))
+ .Concat(GetSchemas(baseRoute.Combine("parameters"), components.Parameters))
+ .Concat(GetSchemas(baseRoute.Combine("requestBodies"), components.RequestBodies))
+ .Concat(GetSchemas(baseRoute.Combine("headers"), components.Headers))
+ .Concat(GetSchemas(baseRoute.Combine("callbacks"), components.Callbacks))
+ .Concat(GetSchemas(baseRoute.Combine("pathItems"), components.PathItems));
+ }
+
+ private static IEnumerable GetSchemas(JsonPointer baseRoute, IDictionary? schemas)
+ {
+ if (schemas is null) return [];
+
+ return schemas.Select(x => baseRoute.Combine(x.Key));
+ }
+
+ private static IEnumerable GetSchemas(JsonPointer baseRoute, IDictionary? responses)
+ {
+ if (responses is null) return [];
+
+ return responses.SelectMany(x =>
+ {
+ var b = baseRoute.Combine(x.Key.GetKeyString());
+ return GetSchemas(b.Combine("headers"), x.Value.Headers)
+ .Concat(GetSchemas(b.Combine("content"), x.Value.Content));
+ });
+ }
+
+ private static IEnumerable GetSchemas(JsonPointer baseRoute, IDictionary? parameters)
+ {
+ if (parameters is null) return [];
+
+ return parameters.SelectMany(x => GetSchemas(baseRoute.Combine(x.Key, "content"), x.Value.Content));
+ }
+
+ private static IEnumerable GetSchemas(JsonPointer baseRoute, IReadOnlyList? parameters)
+ {
+ if (parameters is null) return [];
+
+ return parameters.SelectMany((x, i) => GetSchemas(baseRoute.Combine(i, "content"), x.Content));
+ }
+
+ private static IEnumerable GetSchemas(JsonPointer baseRoute, IDictionary? requestBodies)
+ {
+ if (requestBodies is null) return [];
+
+ return requestBodies.SelectMany(x => GetSchemas(baseRoute.Combine(x.Key, "content"), x.Value.Content));
+ }
+
+ private static IEnumerable GetSchemas(JsonPointer baseRoute, IDictionary? headers)
+ {
+ if (headers is null) return [];
+
+ return headers.SelectMany(x => GetSchemas(baseRoute.Combine(x.Key, "content"), x.Value.Content));
+ }
+
+ private static IEnumerable GetSchemas(JsonPointer baseRoute, IDictionary? callbacks)
+ {
+ if (callbacks is null) return [];
+
+ return callbacks.SelectMany(x => GetSchemas(baseRoute.Combine(x.Key), x.Value));
+ }
+
+ private static IEnumerable GetSchemas(JsonPointer baseRoute, IDictionary? mediaTypes)
+ {
+ if (mediaTypes is null) return [];
+
+ return mediaTypes.Select(x => baseRoute.Combine(x.Key, "schema"));
+ }
+
+ private static IEnumerable GetSchemas(JsonPointer baseRoute, IDictionary? pathItems)
+ {
+ if (pathItems is null) return [];
+
+ return pathItems.SelectMany(x =>
+ {
+ var b = baseRoute.Combine(x.Key.GetKeyString());
+ return GetSchemas(b.Combine("get"), x.Value.Get)
+ .Concat(GetSchemas(b.Combine("put"), x.Value.Put))
+ .Concat(GetSchemas(b.Combine("post"), x.Value.Post))
+ .Concat(GetSchemas(b.Combine("delete"), x.Value.Delete))
+ .Concat(GetSchemas(b.Combine("options"), x.Value.Options))
+ .Concat(GetSchemas(b.Combine("head"), x.Value.Head))
+ .Concat(GetSchemas(b.Combine("patch"), x.Value.Patch))
+ .Concat(GetSchemas(b.Combine("trace"), x.Value.Trace))
+ .Concat(GetSchemas(b.Combine("parameters"), x.Value.Parameters));
+ });
+ }
+
+ private static IEnumerable GetSchemas(JsonPointer baseRoute, Operation? operation)
+ {
+ if (operation is null) return [];
+
+ return GetSchemas(baseRoute.Combine("parameters"), operation.Parameters)
+ .Concat(GetSchemas(baseRoute.Combine("requestBody", "content"), operation.RequestBody?.Content))
+ .Concat(GetSchemas(baseRoute.Combine("responses"), operation.Responses))
+ .Concat(GetSchemas(baseRoute.Combine("callbacks"), operation.Callbacks));
+ }
+
+ private static string GetKeyString(this T? value)
+ {
+ var keyString = typeof(T).IsEnum
+ ? Convert.ToInt32(value).ToString()
+ : value!.ToString();
+
+ return keyString;
+ }
+}
\ No newline at end of file
diff --git a/Graeae.AspNet.Analyzer/Parameter.cs b/Graeae.AspNet.Analyzer/Parameter.cs
new file mode 100644
index 0000000..16743e1
--- /dev/null
+++ b/Graeae.AspNet.Analyzer/Parameter.cs
@@ -0,0 +1,17 @@
+using Graeae.Models;
+
+namespace Graeae.AspNet.Analyzer;
+
+internal record Parameter
+{
+ public static readonly Parameter Body = new(string.Empty, ParameterLocation.Unspecified);
+
+ public string Name { get; }
+ public ParameterLocation In { get; }
+
+ public Parameter(string name, ParameterLocation @in)
+ {
+ Name = @in == ParameterLocation.Header ? name.ToLowerInvariant() : name;
+ In = @in;
+ }
+}
\ No newline at end of file
diff --git a/Graeae.AspNet.Analyzer/PathHelpers.cs b/Graeae.AspNet.Analyzer/PathHelpers.cs
new file mode 100644
index 0000000..fac134f
--- /dev/null
+++ b/Graeae.AspNet.Analyzer/PathHelpers.cs
@@ -0,0 +1,15 @@
+using Corvus.Json;
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Text.RegularExpressions;
+
+namespace Graeae.AspNet.Analyzer;
+
+internal static class PathHelpers
+{
+ public static readonly Regex TemplatedSegmentPattern = new(@"^\{(?.*)\}$", RegexOptions.Compiled | RegexOptions.ECMAScript);
+
+ //public static string Normalize(string path) => path.Replace("\\", "/");
+ public static string Normalize(string path) => new Uri(path).ToString().Replace("file:///C:", "https://graeae.net");
+}
\ No newline at end of file
diff --git a/Graeae.AspNet.Tests.Host/Graeae.AspNet.Tests.Host.csproj b/Graeae.AspNet.Tests.Host/Graeae.AspNet.Tests.Host.csproj
new file mode 100644
index 0000000..16227cf
--- /dev/null
+++ b/Graeae.AspNet.Tests.Host/Graeae.AspNet.Tests.Host.csproj
@@ -0,0 +1,29 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+ true
+ $(BaseIntermediateOutputPath)Generated
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Graeae.AspNet.Tests.Host/Program.cs b/Graeae.AspNet.Tests.Host/Program.cs
new file mode 100644
index 0000000..af7642e
--- /dev/null
+++ b/Graeae.AspNet.Tests.Host/Program.cs
@@ -0,0 +1,12 @@
+using Graeae.AspNet;
+
+var builder = WebApplication.CreateBuilder();
+
+var app = builder.Build();
+
+await app.MapOpenApi("openapi.yaml", new OpenApiOptions
+{
+ IgnoreUnhandledPaths = true
+});
+
+app.Run();
\ No newline at end of file
diff --git a/Graeae.AspNet.Tests.Host/Properties/launchSettings.json b/Graeae.AspNet.Tests.Host/Properties/launchSettings.json
new file mode 100644
index 0000000..271e4e5
--- /dev/null
+++ b/Graeae.AspNet.Tests.Host/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "profiles": {
+ "Graeae.AspNet.Tests.Host": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:57341;http://localhost:57342"
+ }
+ }
+}
\ No newline at end of file
diff --git a/Graeae.AspNet.Tests.Host/RequestHandlers/GoodbyeHandler.cs b/Graeae.AspNet.Tests.Host/RequestHandlers/GoodbyeHandler.cs
new file mode 100644
index 0000000..28e711b
--- /dev/null
+++ b/Graeae.AspNet.Tests.Host/RequestHandlers/GoodbyeHandler.cs
@@ -0,0 +1,13 @@
+using Microsoft.AspNetCore.Mvc;
+using Graeae.AspNet.Tests.Host.RequestHandlers.Models;
+
+namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+[RequestHandler("/goodbye")]
+public static class GoodbyeHandler
+{
+ public static IResult Get(HttpContext context, [FromBody] Person? person)
+ {
+ return TypedResults.Ok($"Hello, {person?.Name ?? "World"}");
+ }
+}
diff --git a/Graeae.AspNet.Tests.Host/RequestHandlers/HelloHandler.cs b/Graeae.AspNet.Tests.Host/RequestHandlers/HelloHandler.cs
new file mode 100644
index 0000000..349f277
--- /dev/null
+++ b/Graeae.AspNet.Tests.Host/RequestHandlers/HelloHandler.cs
@@ -0,0 +1,12 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+[RequestHandler("/hello")]
+public static class HelloHandler
+{
+ public static IResult Get(HttpContext context, [FromQuery] string? name)
+ {
+ return TypedResults.Ok($"Hello, {name ?? "World"}");
+ }
+}
\ No newline at end of file
diff --git a/Graeae.AspNet.Tests.Host/RequestHandlers/HelloNameHandler.cs b/Graeae.AspNet.Tests.Host/RequestHandlers/HelloNameHandler.cs
new file mode 100644
index 0000000..2654c6a
--- /dev/null
+++ b/Graeae.AspNet.Tests.Host/RequestHandlers/HelloNameHandler.cs
@@ -0,0 +1,12 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+[RequestHandler("/hello/{name}")]
+public static class HelloNameHandler
+{
+ public static IResult Get(HttpContext context, [FromRoute] string? name)
+ {
+ return TypedResults.Ok($"Hello, {name ?? "World"}");
+ }
+}
\ No newline at end of file
diff --git a/Graeae.AspNet.Tests.Host/openapi.yaml b/Graeae.AspNet.Tests.Host/openapi.yaml
new file mode 100644
index 0000000..e14673c
--- /dev/null
+++ b/Graeae.AspNet.Tests.Host/openapi.yaml
@@ -0,0 +1,68 @@
+openapi: 3.1.0
+info:
+ title: Graeae Generation Test Host
+ version: 1.0.0
+paths:
+ /hello:
+ get:
+ description: hello world
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+ parameters:
+ - name: name
+ in: query
+ required: false
+ schema:
+ type: string
+ post:
+ description: hello world
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+ parameters:
+ - name: name
+ in: query
+ required: false
+ schema:
+ $ref: '#/components/schemas/Person'
+ /goodbye:
+ get:
+ description: goodbye world
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+components:
+ schemas:
+ Person:
+ type: object
+ properties:
+ name:
+ type: string
+ age:
+ type: integer
+ required:
+ - name
+ - age
diff --git a/Graeae.AspNet.Tests/Analyzer/MissingOperationsAnalyzerTests.cs b/Graeae.AspNet.Tests/Analyzer/MissingOperationsAnalyzerTests.cs
new file mode 100644
index 0000000..fd8db86
--- /dev/null
+++ b/Graeae.AspNet.Tests/Analyzer/MissingOperationsAnalyzerTests.cs
@@ -0,0 +1,922 @@
+using Graeae.AspNet.Analyzer;
+using Microsoft.CodeAnalysis.Testing;
+using VerifyCS = Graeae.AspNet.Tests.Analyzer.Verifiers.CSharpSourceGeneratorVerifier;
+
+namespace Graeae.AspNet.Tests.Analyzer;
+
+public class MissingOperationsAnalyzerTests
+{
+ private const string AttributeContent = @"using System;
+
+namespace Graeae.AspNet;
+
+[AttributeUsage(AttributeTargets.Class)]
+public class RequestHandlerAttribute : Attribute
+{
+ public string Path { get; }
+
+ public RequestHandlerAttribute(string path)
+ {
+ Path = path;
+ }
+}";
+
+ [Test]
+ public async Task FoundGetHelloWarnGoodbye()
+ {
+ var openapiContent = @"openapi: 3.1.0
+info:
+ title: Graeae Generation Test Host
+ version: 1.0.0
+paths:
+ /hello:
+ get:
+ description: hello world
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+ /goodbye:
+ get:
+ description: goodbye world
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+";
+
+ var handler = @"using System.Threading.Tasks;
+using Graeae.AspNet;
+using Microsoft.AspNetCore.Http;
+
+namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+[RequestHandler(""/hello"")]
+public static class HelloHandler
+{
+ public static Task Get(HttpContext context)
+ {
+ return Task.FromResult(""hello world"");
+ }
+}";
+
+ await new VerifyCS.Test
+ {
+ TestState =
+ {
+ Sources = { handler, AttributeContent },
+ AdditionalFiles = { ("openapi.yaml", openapiContent) },
+ ReferenceAssemblies = PackageHelper.AspNetWeb,
+ ExpectedDiagnostics =
+ {
+ new DiagnosticResult(Diagnostics.MissingRouteHandler("/goodbye").Descriptor)
+ }
+ }
+ }.RunAsync();
+ }
+
+ [Test]
+ public async Task MethodExistsWrongParams_Body()
+ {
+ var openapiContent = @"openapi: 3.1.0
+info:
+ title: Graeae Generation Test Host
+ version: 1.0.0
+paths:
+ /hello:
+ post:
+ description: goodbye world
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+";
+ var handler = @"using System.Threading.Tasks;
+using Graeae.AspNet;
+using Microsoft.AspNetCore.Http;
+
+namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+[RequestHandler(""/hello"")]
+public static class HelloHandler
+{
+ public static Task Post(HttpContext context)
+ {
+ return Task.FromResult(""hello world"");
+ }
+}";
+
+ await new VerifyCS.Test
+ {
+ TestState =
+ {
+ Sources = { handler, AttributeContent },
+ AdditionalFiles = { ("openapi.yaml", openapiContent) },
+ ReferenceAssemblies = PackageHelper.AspNetWeb,
+ ExpectedDiagnostics =
+ {
+ new DiagnosticResult(Diagnostics.MissingRouteOperationHandler("/hello", "Post").Descriptor)
+ }
+ }
+ }.RunAsync();
+ }
+
+ [Test]
+ public async Task FoundGetHello_ImplicitBody()
+ {
+ var openapiContent = @"openapi: 3.1.0
+info:
+ title: Graeae Generation Test Host
+ version: 1.0.0
+paths:
+ /hello:
+ post:
+ description: goodbye world
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+";
+ var handler = @"using System.Threading.Tasks;
+using Graeae.AspNet;
+using Microsoft.AspNetCore.Http;
+
+namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+[RequestHandler(""/hello"")]
+public static class HelloHandler
+{
+ public static Task Post(HttpContext context, HelloPostBodyModel model)
+ {
+ return Task.FromResult($""hello {model.Name}"");
+ }
+}";
+ var model = @"namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+public class HelloPostBodyModel
+{
+ public string Name { get; set; }
+}
+";
+
+ await new VerifyCS.Test
+ {
+ TestState =
+ {
+ Sources = { handler, AttributeContent, model },
+ AdditionalFiles = { ("openapi.yaml", openapiContent) },
+ ReferenceAssemblies = PackageHelper.AspNetWeb,
+ }
+ }.RunAsync();
+ }
+
+ [Test]
+ public async Task FoundGetHello_ExplicitBody()
+ {
+ var openapiContent = @"openapi: 3.1.0
+info:
+ title: Graeae Generation Test Host
+ version: 1.0.0
+paths:
+ /hello:
+ post:
+ description: goodbye world
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+";
+ var handler = @"using System.Threading.Tasks;
+using Graeae.AspNet;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+[RequestHandler(""/hello"")]
+public static class HelloHandler
+{
+ public static Task Post(HttpContext context, [FromBody] HelloPostBodyModel model)
+ {
+ return Task.FromResult($""hello {model.Name}"");
+ }
+}";
+ var model = @"namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+public class HelloPostBodyModel
+{
+ public string Name { get; set; }
+}
+";
+
+ await new VerifyCS.Test
+ {
+ TestState =
+ {
+ Sources = { handler, AttributeContent, model },
+ AdditionalFiles = { ("openapi.yaml", openapiContent) },
+ ReferenceAssemblies = PackageHelper.AspNetWeb,
+ }
+ }.RunAsync();
+ }
+
+ [Test]
+ public async Task MethodExistsWrongParams_Query()
+ {
+ var openapiContent = @"openapi: 3.1.0
+info:
+ title: Graeae Generation Test Host
+ version: 1.0.0
+paths:
+ /hello:
+ get:
+ description: hello world
+ parameters:
+ - name: name
+ in: query
+ required: false
+ schema:
+ type: string
+ format: int32
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+";
+ var handler = @"using System.Threading.Tasks;
+using Graeae.AspNet;
+using Microsoft.AspNetCore.Http;
+
+namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+[RequestHandler(""/hello"")]
+public static class HelloHandler
+{
+ public static Task Get(HttpContext context)
+ {
+ return Task.FromResult($""hello world"");
+ }
+}";
+
+ await new VerifyCS.Test
+ {
+ TestState =
+ {
+ Sources = { handler, AttributeContent },
+ AdditionalFiles = { ("openapi.yaml", openapiContent) },
+ ReferenceAssemblies = PackageHelper.AspNetWeb,
+ ExpectedDiagnostics =
+ {
+ new DiagnosticResult(Diagnostics.MissingRouteOperationHandler("/hello", "Get").Descriptor)
+ }
+ }
+ }.RunAsync();
+ }
+
+ [Test]
+ public async Task FoundGetHello_ImplicitQuery()
+ {
+ var openapiContent = @"openapi: 3.1.0
+info:
+ title: Graeae Generation Test Host
+ version: 1.0.0
+paths:
+ /hello:
+ get:
+ description: hello world
+ parameters:
+ - name: name
+ in: query
+ required: false
+ schema:
+ type: string
+ format: int32
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+";
+ var handler = @"using System.Threading.Tasks;
+using Graeae.AspNet;
+using Microsoft.AspNetCore.Http;
+
+namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+[RequestHandler(""/hello"")]
+public static class HelloHandler
+{
+ public static Task Get(HttpContext context, string name)
+ {
+ return Task.FromResult($""hello {name ?? ""world""}"");
+ }
+}";
+
+ await new VerifyCS.Test
+ {
+ TestState =
+ {
+ Sources = { handler, AttributeContent },
+ AdditionalFiles = { ("openapi.yaml", openapiContent) },
+ ReferenceAssemblies = PackageHelper.AspNetWeb,
+ }
+ }.RunAsync();
+ }
+
+ [Test]
+ public async Task FoundGetHello_ExplicitQuery()
+ {
+ var openapiContent = @"openapi: 3.1.0
+info:
+ title: Graeae Generation Test Host
+ version: 1.0.0
+paths:
+ /hello:
+ get:
+ description: hello world
+ parameters:
+ - name: n
+ in: query
+ required: false
+ schema:
+ type: string
+ format: int32
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+";
+ var handler = @"using System.Threading.Tasks;
+using Graeae.AspNet;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+[RequestHandler(""/hello"")]
+public static class HelloHandler
+{
+ public static Task Get(HttpContext context, [FromQuery(Name = ""n"")] string name)
+ {
+ return Task.FromResult($""hello {name ?? ""world""}"");
+ }
+}";
+
+ await new VerifyCS.Test
+ {
+ TestState =
+ {
+ Sources = { handler, AttributeContent },
+ AdditionalFiles = { ("openapi.yaml", openapiContent) },
+ ReferenceAssemblies = PackageHelper.AspNetWeb,
+ }
+ }.RunAsync();
+ }
+
+ [Test]
+ public async Task FoundGetHello_ExplicitQuery_UnmatchedName()
+ {
+ var openapiContent = @"openapi: 3.1.0
+info:
+ title: Graeae Generation Test Host
+ version: 1.0.0
+paths:
+ /hello:
+ get:
+ description: hello world
+ parameters:
+ - name: name
+ in: query
+ required: false
+ schema:
+ type: string
+ format: int32
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+";
+ var handler = @"using System.Threading.Tasks;
+using Graeae.AspNet;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+[RequestHandler(""/hello"")]
+public static class HelloHandler
+{
+ public static Task Get(HttpContext context, [FromQuery(Name = ""n"")] string name)
+ {
+ return Task.FromResult($""hello {name ?? ""world""}"");
+ }
+}";
+
+ await new VerifyCS.Test
+ {
+ TestState =
+ {
+ Sources = { handler, AttributeContent },
+ AdditionalFiles = { ("openapi.yaml", openapiContent) },
+ ReferenceAssemblies = PackageHelper.AspNetWeb,
+ ExpectedDiagnostics =
+ {
+ new DiagnosticResult(Diagnostics.MissingRouteOperationHandler("/hello", "Get").Descriptor)
+ }
+ }
+ }.RunAsync();
+ }
+
+ [Test]
+ public async Task MethodExistsWrongParams_Path()
+ {
+ var openapiContent = @"openapi: 3.1.0
+info:
+ title: Graeae Generation Test Host
+ version: 1.0.0
+paths:
+ /hello/{name}:
+ get:
+ description: hello world
+ parameters:
+ - name: name
+ in: path
+ required: false
+ schema:
+ type: string
+ format: int32
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+";
+ var handler = @"using System.Threading.Tasks;
+using Graeae.AspNet;
+using Microsoft.AspNetCore.Http;
+
+namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+[RequestHandler(""/hello/{name}"")]
+public static class HelloHandler
+{
+ public static Task Get(HttpContext context)
+ {
+ return Task.FromResult($""hello world"");
+ }
+}";
+
+ await new VerifyCS.Test
+ {
+ TestState =
+ {
+ Sources = { handler, AttributeContent },
+ AdditionalFiles = { ("openapi.yaml", openapiContent) },
+ ReferenceAssemblies = PackageHelper.AspNetWeb,
+ ExpectedDiagnostics =
+ {
+ new DiagnosticResult(Diagnostics.MissingRouteOperationHandler("/hello/{name}", "Get").Descriptor)
+ }
+ }
+ }.RunAsync();
+ }
+
+ [Test]
+ public async Task FoundGetHello_ImplicitPath()
+ {
+ var openapiContent = @"openapi: 3.1.0
+info:
+ title: Graeae Generation Test Host
+ version: 1.0.0
+paths:
+ /hello/{name}:
+ get:
+ description: hello world
+ parameters:
+ - name: name
+ in: path
+ required: false
+ schema:
+ type: string
+ format: int32
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+";
+ var handler = @"using System.Threading.Tasks;
+using Graeae.AspNet;
+using Microsoft.AspNetCore.Http;
+
+namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+[RequestHandler(""/hello/{name}"")]
+public static class HelloHandler
+{
+ public static Task Get(HttpContext context, string name)
+ {
+ return Task.FromResult($""hello {name ?? ""world""}"");
+ }
+}";
+
+ await new VerifyCS.Test
+ {
+ TestState =
+ {
+ Sources = { handler, AttributeContent },
+ AdditionalFiles = { ("openapi.yaml", openapiContent) },
+ ReferenceAssemblies = PackageHelper.AspNetWeb,
+ }
+ }.RunAsync();
+ }
+
+ [Test]
+ public async Task FoundGetHello_ExplicitPath()
+ {
+ var openapiContent = @"openapi: 3.1.0
+info:
+ title: Graeae Generation Test Host
+ version: 1.0.0
+paths:
+ /hello/{name}:
+ get:
+ description: hello world
+ parameters:
+ - name: name
+ in: path
+ required: false
+ schema:
+ type: string
+ format: int32
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+";
+ var handler = @"using System.Threading.Tasks;
+using Graeae.AspNet;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+[RequestHandler(""/hello/{name}"")]
+public static class HelloHandler
+{
+ public static Task Get(HttpContext context, [FromRoute(Name = ""name"")] string foo)
+ {
+ return Task.FromResult($""hello {foo ?? ""world""}"");
+ }
+}";
+
+ await new VerifyCS.Test
+ {
+ TestState =
+ {
+ Sources = { handler, AttributeContent },
+ AdditionalFiles = { ("openapi.yaml", openapiContent) },
+ ReferenceAssemblies = PackageHelper.AspNetWeb,
+ }
+ }.RunAsync();
+ }
+
+ [Test]
+ public async Task FoundGetHello_ExplicitPath_UnmatchedName()
+ {
+ var openapiContent = @"openapi: 3.1.0
+info:
+ title: Graeae Generation Test Host
+ version: 1.0.0
+paths:
+ /hello/{name}:
+ get:
+ description: hello world
+ parameters:
+ - name: name
+ in: path
+ required: false
+ schema:
+ type: string
+ format: int32
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+";
+ var handler = @"using System.Threading.Tasks;
+using Graeae.AspNet;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+[RequestHandler(""/hello/{name}"")]
+public static class HelloHandler
+{
+ public static Task Get(HttpContext context, [FromRoute(Name = ""n"")] string name)
+ {
+ return Task.FromResult($""hello {name ?? ""world""}"");
+ }
+}";
+
+ await new VerifyCS.Test
+ {
+ TestState =
+ {
+ Sources = { handler, AttributeContent },
+ AdditionalFiles = { ("openapi.yaml", openapiContent) },
+ ReferenceAssemblies = PackageHelper.AspNetWeb,
+ ExpectedDiagnostics =
+ {
+ new DiagnosticResult(Diagnostics.MissingRouteOperationHandler("/hello/{name}", "Get").Descriptor)
+ }
+ }
+ }.RunAsync();
+ }
+
+ [Test]
+ public async Task MethodExistsWrongParams_Header()
+ {
+ var openapiContent = @"openapi: 3.1.0
+info:
+ title: Graeae Generation Test Host
+ version: 1.0.0
+paths:
+ /hello:
+ get:
+ description: hello world
+ parameters:
+ - name: name
+ in: header
+ required: false
+ schema:
+ type: string
+ format: int32
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+";
+ var handler = @"using System.Threading.Tasks;
+using Graeae.AspNet;
+using Microsoft.AspNetCore.Http;
+
+namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+[RequestHandler(""/hello"")]
+public static class HelloHandler
+{
+ public static Task Get(HttpContext context)
+ {
+ return Task.FromResult($""hello world"");
+ }
+}";
+
+ await new VerifyCS.Test
+ {
+ TestState =
+ {
+ Sources = { handler, AttributeContent },
+ AdditionalFiles = { ("openapi.yaml", openapiContent) },
+ ReferenceAssemblies = PackageHelper.AspNetWeb,
+ ExpectedDiagnostics =
+ {
+ new DiagnosticResult(Diagnostics.MissingRouteOperationHandler("/hello", "Get").Descriptor)
+ }
+ }
+ }.RunAsync();
+ }
+
+ [Test]
+ public async Task FoundGetHello_Header()
+ {
+ var openapiContent = @"openapi: 3.1.0
+info:
+ title: Graeae Generation Test Host
+ version: 1.0.0
+paths:
+ /hello:
+ get:
+ description: hello world
+ parameters:
+ - name: x-name
+ in: header
+ required: false
+ schema:
+ type: string
+ format: int32
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+";
+ var handler = @"using System.Threading.Tasks;
+using Graeae.AspNet;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+[RequestHandler(""/hello"")]
+public static class HelloHandler
+{
+ public static Task Get(HttpContext context, [FromHeader(Name = ""x-name"")] string name)
+ {
+ return Task.FromResult($""hello {name ?? ""world""}"");
+ }
+}";
+
+ await new VerifyCS.Test
+ {
+ TestState =
+ {
+ Sources = { handler, AttributeContent },
+ AdditionalFiles = { ("openapi.yaml", openapiContent) },
+ ReferenceAssemblies = PackageHelper.AspNetWeb,
+ }
+ }.RunAsync();
+ }
+
+ [Test]
+ public async Task FoundGetHello_Header_DifferentCasing()
+ {
+ var openapiContent = @"openapi: 3.1.0
+info:
+ title: Graeae Generation Test Host
+ version: 1.0.0
+paths:
+ /hello:
+ get:
+ description: hello world
+ parameters:
+ - name: X-Name
+ in: header
+ required: false
+ schema:
+ type: string
+ format: int32
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+";
+ var handler = @"using System.Threading.Tasks;
+using Graeae.AspNet;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+[RequestHandler(""/hello"")]
+public static class HelloHandler
+{
+ public static Task Get(HttpContext context, [FromHeader(Name = ""x-name"")] string name)
+ {
+ return Task.FromResult($""hello {name ?? ""world""}"");
+ }
+}";
+
+ await new VerifyCS.Test
+ {
+ TestState =
+ {
+ Sources = { handler, AttributeContent },
+ AdditionalFiles = { ("openapi.yaml", openapiContent) },
+ ReferenceAssemblies = PackageHelper.AspNetWeb,
+ }
+ }.RunAsync();
+ }
+
+ [Test]
+ public async Task FoundGetHello_Header_UnmatchedName()
+ {
+ var openapiContent = @"openapi: 3.1.0
+info:
+ title: Graeae Generation Test Host
+ version: 1.0.0
+paths:
+ /hello:
+ get:
+ description: hello world
+ parameters:
+ - name: name
+ in: header
+ required: false
+ schema:
+ type: string
+ format: int32
+ responses:
+ '200':
+ description: okay
+ content:
+ application/json:
+ schema:
+ type: string
+";
+ var handler = @"using System.Threading.Tasks;
+using Graeae.AspNet;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Graeae.AspNet.Tests.Host.RequestHandlers;
+
+[RequestHandler(""/hello"")]
+public static class HelloHandler
+{
+ public static Task Get(HttpContext context, [FromHeader(Name = ""x-custom"")] string name)
+ {
+ return Task.FromResult($""hello {name ?? ""world""}"");
+ }
+}";
+
+ await new VerifyCS.Test
+ {
+ TestState =
+ {
+ Sources = { handler, AttributeContent },
+ AdditionalFiles = { ("openapi.yaml", openapiContent) },
+ ReferenceAssemblies = PackageHelper.AspNetWeb,
+ ExpectedDiagnostics =
+ {
+ new DiagnosticResult(Diagnostics.MissingRouteOperationHandler("/hello", "Get").Descriptor)
+ }
+ }
+ }.RunAsync();
+ }
+}
\ No newline at end of file
diff --git a/Graeae.AspNet.Tests/Analyzer/PackageHelper.cs b/Graeae.AspNet.Tests/Analyzer/PackageHelper.cs
new file mode 100644
index 0000000..5352485
--- /dev/null
+++ b/Graeae.AspNet.Tests/Analyzer/PackageHelper.cs
@@ -0,0 +1,17 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis.Testing;
+
+namespace Graeae.AspNet.Tests.Analyzer;
+
+internal static class PackageHelper
+{
+ public static readonly ReferenceAssemblies AspNetWeb = ReferenceAssemblies.Net.Net60.AddPackages(
+ ("Microsoft.AspNetCore.App.Ref", "6.0.23")
+ );
+
+ public static ReferenceAssemblies AddPackages(this ReferenceAssemblies start, params (string name, string version)[] packages) =>
+ start.AddPackages(packages
+ .Select(x => new PackageIdentity(x.name, x.version))
+ .ToImmutableArray()
+ );
+}
\ No newline at end of file
diff --git a/Graeae.AspNet.Tests/Analyzer/Verifiers/CSharpSourceGeneratorVerifier.Test.cs b/Graeae.AspNet.Tests/Analyzer/Verifiers/CSharpSourceGeneratorVerifier.Test.cs
new file mode 100644
index 0000000..c301ff5
--- /dev/null
+++ b/Graeae.AspNet.Tests/Analyzer/Verifiers/CSharpSourceGeneratorVerifier.Test.cs
@@ -0,0 +1,38 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.CodeAnalysis.Testing.Verifiers;
+#pragma warning disable CS0618 // Type or member is obsolete
+
+namespace Graeae.AspNet.Tests.Analyzer.Verifiers;
+
+internal static partial class CSharpSourceGeneratorVerifier
+ where TSourceGenerator : IIncrementalGenerator, new()
+{
+ public sealed class Test : CSharpSourceGeneratorTest
+ {
+ public Test()
+ {
+ }
+
+ public LanguageVersion LanguageVersion { get; set; } = LanguageVersion.Default;
+
+ protected override IEnumerable GetSourceGenerators()
+ {
+ return new[] { typeof(TSourceGenerator) };
+ }
+
+ protected override CompilationOptions CreateCompilationOptions()
+ {
+ var compilationOptions = base.CreateCompilationOptions();
+ return compilationOptions.WithSpecificDiagnosticOptions(
+ compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings));
+ }
+
+ protected override ParseOptions CreateParseOptions()
+ {
+ return ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(LanguageVersion);
+ }
+ }
+}
diff --git a/Graeae.AspNet.Tests/Analyzer/Verifiers/CSharpSourceGeneratorVerifier.cs b/Graeae.AspNet.Tests/Analyzer/Verifiers/CSharpSourceGeneratorVerifier.cs
new file mode 100644
index 0000000..c7f85af
--- /dev/null
+++ b/Graeae.AspNet.Tests/Analyzer/Verifiers/CSharpSourceGeneratorVerifier.cs
@@ -0,0 +1,63 @@
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Graeae.AspNet.Tests.Analyzer.Verifiers;
+
+internal static partial class CSharpSourceGeneratorVerifier
+ where TSourceGenerator : IIncrementalGenerator, new()
+{
+ internal static readonly (string filename, string content)[] EmptyGeneratedSources = Array.Empty<(string filename, string content)>();
+
+ public static DiagnosticResult Diagnostic()
+ => new DiagnosticResult();
+
+ public static DiagnosticResult Diagnostic(string id, DiagnosticSeverity severity)
+ => new DiagnosticResult(id, severity);
+
+ public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor)
+ => new DiagnosticResult(descriptor);
+
+ public static async Task VerifyGeneratorAsync(string source, (string filename, string content) generatedSource)
+ => await VerifyGeneratorAsync(source, DiagnosticResult.EmptyDiagnosticResults, new[] { generatedSource });
+
+ public static async Task VerifyGeneratorAsync(string source, params (string filename, string content)[] generatedSources)
+ => await VerifyGeneratorAsync(source, DiagnosticResult.EmptyDiagnosticResults, generatedSources);
+
+ public static async Task VerifyGeneratorAsync(string source, DiagnosticResult diagnostic)
+ => await VerifyGeneratorAsync(source, new[] { diagnostic }, EmptyGeneratedSources);
+
+ public static async Task VerifyGeneratorAsync(string source, params DiagnosticResult[] diagnostics)
+ => await VerifyGeneratorAsync(source, diagnostics, EmptyGeneratedSources);
+
+ public static async Task VerifyGeneratorAsync(string source, DiagnosticResult diagnostic, (string filename, string content) generatedSource)
+ => await VerifyGeneratorAsync(source, new[] { diagnostic }, new[] { generatedSource });
+
+ public static async Task VerifyGeneratorAsync(string source, DiagnosticResult[] diagnostics, (string filename, string content) generatedSource)
+ => await VerifyGeneratorAsync(source, diagnostics, new[] { generatedSource });
+
+ public static async Task VerifyGeneratorAsync(string source, DiagnosticResult diagnostic, params (string filename, string content)[] generatedSources)
+ => await VerifyGeneratorAsync(source, new[] { diagnostic }, generatedSources);
+
+ public static async Task VerifyGeneratorAsync(string source, DiagnosticResult[] diagnostics, params (string filename, string content)[] generatedSources)
+ {
+ CSharpSourceGeneratorVerifier.Test test = new()
+ {
+ TestState =
+ {
+ Sources = { source },
+ },
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net60,
+ };
+
+ foreach ((string filename, string content) in generatedSources)
+ {
+ test.TestState.GeneratedSources.Add((typeof(TSourceGenerator), filename, SourceText.From(content, Encoding.UTF8)));
+ }
+
+ test.ExpectedDiagnostics.AddRange(diagnostics);
+
+ await test.RunAsync(CancellationToken.None);
+ }
+}
diff --git a/Graeae.AspNet.Tests/Analyzer/Verifiers/CSharpVerifierHelper.cs b/Graeae.AspNet.Tests/Analyzer/Verifiers/CSharpVerifierHelper.cs
new file mode 100644
index 0000000..c823832
--- /dev/null
+++ b/Graeae.AspNet.Tests/Analyzer/Verifiers/CSharpVerifierHelper.cs
@@ -0,0 +1,19 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace Graeae.AspNet.Tests.Analyzer.Verifiers;
+
+internal static class CSharpVerifierHelper
+{
+ internal static ImmutableDictionary NullableWarnings { get; } = GetNullableWarningsFromCompiler();
+
+ private static ImmutableDictionary GetNullableWarningsFromCompiler()
+ {
+ string[] args = { "/warnaserror:nullable" };
+ CSharpCommandLineArguments commandLineArguments = CSharpCommandLineParser.Default.Parse(args, Environment.CurrentDirectory, Environment.CurrentDirectory);
+ ImmutableDictionary nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions;
+
+ return nullableWarnings;
+ }
+}
diff --git a/Graeae.AspNet.Tests/Graeae.AspNet.Tests.csproj b/Graeae.AspNet.Tests/Graeae.AspNet.Tests.csproj
new file mode 100644
index 0000000..eab72c0
--- /dev/null
+++ b/Graeae.AspNet.Tests/Graeae.AspNet.Tests.csproj
@@ -0,0 +1,47 @@
+
+
+
+ net8.0
+ enable
+ enable
+ $(NoWarn);CA1707;NUnit2005
+
+ false
+
+ latest
+
+ true
+ ../openapi.snk
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/Graeae.AspNet.Tests/RuntimeExpressionResolutionTests.cs b/Graeae.AspNet.Tests/RuntimeExpressionResolutionTests.cs
new file mode 100644
index 0000000..5f08fd4
--- /dev/null
+++ b/Graeae.AspNet.Tests/RuntimeExpressionResolutionTests.cs
@@ -0,0 +1,85 @@
+using System.Net;
+using System.Text.Json.Nodes;
+using Graeae.Models;
+using Json.More;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Primitives;
+using Encoding = System.Text.Encoding;
+
+namespace Graeae.AspNet.Tests;
+
+public class RuntimeExpressionResolutionTests
+{
+ private HttpContext _context;
+ private PathTemplate _pathTemplate;
+
+ [OneTimeSetUp]
+ public void Setup()
+ {
+ var requestContent = new JsonObject
+ {
+ ["id"] = 1,
+ ["name"] = "item 1",
+ }.AsJsonString();
+ var requestBody = new MemoryStream();
+ var requestWriter = new StreamWriter(requestBody, Encoding.UTF8);
+ requestWriter.Write(requestContent);
+ requestWriter.Flush();
+
+ var responseContent = new JsonObject
+ {
+ ["id"] = 1,
+ ["name"] = "item 1",
+ ["price"] = 3.14
+ }.AsJsonString();
+ var responseBody = new MemoryStream();
+ var responseWriter = new StreamWriter(responseBody, Encoding.UTF8);
+ responseWriter.Write(responseContent);
+ responseWriter.Flush();
+
+ _context = new DefaultHttpContext
+ {
+ Request =
+ {
+ Method = "PUT",
+ IsHttps = true,
+ Protocol = "https",
+ ContentType = "application/json", // .net copies to headers
+ Host = new HostString("graeae.net"),
+ Path = "/items/1",
+ Body = requestBody,
+ Query = new QueryCollection(new Dictionary { ["param"] = "value" }),
+ Headers =
+ {
+ ["x-Authorization"] = "Bearer random_key"
+ }
+ },
+ Response =
+ {
+ Body = responseBody,
+ StatusCode = (int)HttpStatusCode.OK,
+ ContentType = "application/json"
+ }
+ };
+
+ _pathTemplate = PathTemplate.Parse("/items/{item}");
+ }
+
+ [TestCase("$url", "https://graeae.net/items/1?param=value")]
+ [TestCase("$method", "PUT")]
+ [TestCase("$statusCode", "200")]
+ [TestCase("$request.header.Content-Type", "application/json")]
+ [TestCase("$request.header.x-Authorization", "Bearer random_key")]
+ [TestCase("$request.path.item", "1")]
+ [TestCase("$request.query.param", "value")]
+ [TestCase("$request.body#/name", "item 1")]
+ [TestCase("$response.body#/price", "3.14")]
+ [TestCase("$response.header.Content-Type", "application/json")]
+ public void Test1(string runtimeExpression, string expected)
+ {
+ var expr = RuntimeExpression.Parse(runtimeExpression);
+ var actual = expr.Resolve(_context, _pathTemplate);
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+}
\ No newline at end of file
diff --git a/Graeae.AspNet.Tests/Usings.cs b/Graeae.AspNet.Tests/Usings.cs
new file mode 100644
index 0000000..cefced4
--- /dev/null
+++ b/Graeae.AspNet.Tests/Usings.cs
@@ -0,0 +1 @@
+global using NUnit.Framework;
\ No newline at end of file
diff --git a/Graeae.AspNet/Evaluator.cs b/Graeae.AspNet/Evaluator.cs
new file mode 100644
index 0000000..3f17830
--- /dev/null
+++ b/Graeae.AspNet/Evaluator.cs
@@ -0,0 +1,122 @@
+using System.Text;
+using System.Text.Json.Nodes;
+using Graeae.Models;
+using Json.More;
+using Json.Pointer;
+using Json.Schema;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
+
+namespace Graeae.AspNet;
+
+///
+/// Extensions to evaluate runtime expressions against an HTTP call.
+///
+public static class Evaluator
+{
+ ///
+ /// Resolves a callback key expression.
+ ///
+ /// The callback key expression
+ /// The HTTP context
+ /// (optional) A path template
+ /// The resolved expression as a URI
+ public static Uri Resolve(this CallbackKeyExpression expr, HttpContext context, PathTemplate? pathTemplate = null)
+ {
+ var sb = new StringBuilder(expr.ToString());
+
+ foreach (var parameter in expr.Parameters)
+ {
+ var value = Resolve(parameter, context, pathTemplate);
+ sb.Replace(parameter.ToString(), value);
+ }
+
+ return new Uri(sb.ToString());
+ }
+
+ ///
+ /// Resolves a runtime expression.
+ ///
+ /// The runtime expression
+ /// The HTTP context
+ /// (optional) A path template
+ /// The resolved expression
+ public static string? Resolve(this RuntimeExpression expr, HttpContext context, PathTemplate? pathTemplate = null)
+ {
+ return expr.ExpressionType switch
+ {
+ RuntimeExpressionType.Url => context.Request.GetEncodedUrl(),
+ RuntimeExpressionType.Method => context.Request.Method,
+ RuntimeExpressionType.StatusCode => context.Response.StatusCode.ToString(),
+ RuntimeExpressionType.Request => GetFromRequest(expr, context.Request, pathTemplate),
+ RuntimeExpressionType.Response => GetFromResponse(expr, context.Response),
+ _ => throw new ArgumentOutOfRangeException(nameof(expr))
+ };
+ }
+
+ private static string? GetFromRequest(RuntimeExpression expr, HttpRequest request, PathTemplate? pathTemplate)
+ {
+ switch (expr.SourceType)
+ {
+ case RuntimeExpressionSourceType.Header:
+ request.Headers.TryGetValue(expr.Token!, out var header);
+ return header;
+ case RuntimeExpressionSourceType.Query:
+ request.Query.TryGetValue(expr.Name!, out var queryParam);
+ return queryParam;
+ case RuntimeExpressionSourceType.Path:
+ ArgumentNullException.ThrowIfNull(pathTemplate);
+ var actualPath = PathTemplate.Parse(request.Path.Value!);
+ var matched = pathTemplate.Segments.Zip(actualPath.Segments, (x, y) => (Template: x, Actual: y));
+ var target = $"{{{expr.Name}}}";
+ return matched.FirstOrDefault(x => x.Template == target).Actual;
+ case RuntimeExpressionSourceType.Body:
+ return GetFromStream(expr.JsonPointer!, request.Body);
+ case null:
+ case RuntimeExpressionSourceType.Unspecified:
+ default:
+ throw new ArgumentOutOfRangeException(nameof(expr));
+ }
+ }
+
+ private static string GetFromResponse(RuntimeExpression expr, HttpResponse response)
+ {
+ switch (expr.SourceType)
+ {
+ case RuntimeExpressionSourceType.Header:
+ response.Headers.TryGetValue(expr.Token!, out var header);
+ return header!;
+ case RuntimeExpressionSourceType.Query:
+ throw new NotSupportedException("$response.query is not supported");
+ case RuntimeExpressionSourceType.Path:
+ throw new NotSupportedException("$response.path is not supported");
+ case RuntimeExpressionSourceType.Body:
+ return GetFromStream(expr.JsonPointer!, response.Body);
+ case null:
+ case RuntimeExpressionSourceType.Unspecified:
+ default:
+ throw new ArgumentOutOfRangeException(nameof(expr));
+ }
+ }
+
+ private static string GetFromStream(JsonPointer pointer, Stream stream)
+ {
+ using var newStream = new MemoryStream();
+
+ var position = stream.Position;
+ stream.Position = 0;
+ stream.CopyTo(newStream);
+ stream.Position = position;
+ newStream.Position = 0;
+
+ using var reader = new StreamReader(newStream);
+ var content = reader.ReadToEnd();
+
+ var json = JsonNode.Parse(content);
+
+ pointer.TryEvaluate(json, out var target);
+ return target.GetSchemaValueType() == SchemaValueType.String
+ ? target!.GetValue()
+ : target.AsJsonString();
+ }
+}
\ No newline at end of file
diff --git a/Graeae.AspNet/Graeae.AspNet.csproj b/Graeae.AspNet/Graeae.AspNet.csproj
new file mode 100644
index 0000000..96a68b1
--- /dev/null
+++ b/Graeae.AspNet/Graeae.AspNet.csproj
@@ -0,0 +1,56 @@
+
+
+
+ net8.0
+ enable
+ enable
+ Graeae.AspNet
+ latest
+
+ Greg Dennis
+ Asp.Net operational functionality using Graeae.Models
+ LICENSE
+ https://github.com/gregsdennis/Graeae
+ openapi.png
+ https://github.com/gregsdennis/Graeae
+ openapi json schema aspnet webapi minimalapi api
+ Release notes can be found at https://github.com/gregsdennis/Graeae
+ true
+ Graeae.AspNet.xml
+ 0.1.0-preview1
+ 0.1.0
+ 0.1.0.0
+ true
+ snupkg
+ true
+ true
+ true
+ ../openapi.snk
+ README.md
+
+
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+
+ True
+ \
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Graeae.AspNet/OpenApiOptions.cs b/Graeae.AspNet/OpenApiOptions.cs
new file mode 100644
index 0000000..4c96bae
--- /dev/null
+++ b/Graeae.AspNet/OpenApiOptions.cs
@@ -0,0 +1,17 @@
+namespace Graeae.AspNet;
+
+///
+/// Defines options for the extension.
+///
+public class OpenApiOptions
+{
+ ///
+ /// Provides a default set of options.
+ ///
+ public static OpenApiOptions Default { get; } = new();
+
+ ///
+ /// Ignores paths/routes that do not have handlers assigned.
+ ///
+ public bool IgnoreUnhandledPaths { get; set; }
+}
\ No newline at end of file
diff --git a/Graeae.AspNet/RequestHandlerAttribute.cs b/Graeae.AspNet/RequestHandlerAttribute.cs
new file mode 100644
index 0000000..614b6d5
--- /dev/null
+++ b/Graeae.AspNet/RequestHandlerAttribute.cs
@@ -0,0 +1,28 @@
+using Graeae.Models;
+
+namespace Graeae.AspNet;
+
+///
+/// Indicates that the attributed class contains handler methods for the indicated route.
+///
+[AttributeUsage(AttributeTargets.Class)]
+public class RequestHandlerAttribute : Attribute
+{
+ ///
+ /// The route to handle.
+ ///
+ public string Path { get; }
+
+ ///
+ /// Creates a new
+ ///
+ /// The route to handle
+ /// Thrown when the route is not a valid path template
+ public RequestHandlerAttribute(string path)
+ {
+ if (!PathTemplate.TryParse(path, out _))
+ throw new ArgumentException($"'{path}' is not a valid path template.");
+
+ Path = path;
+ }
+}
\ No newline at end of file
diff --git a/Graeae.AspNet/WebApplicationExtensions.cs b/Graeae.AspNet/WebApplicationExtensions.cs
new file mode 100644
index 0000000..8da15f8
--- /dev/null
+++ b/Graeae.AspNet/WebApplicationExtensions.cs
@@ -0,0 +1,101 @@
+using System.Linq.Expressions;
+using System.Reflection;
+using Graeae.Models;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Routing;
+using Yaml2JsonNode;
+
+namespace Graeae.AspNet;
+
+///
+/// Extends the app builder to scan an Open API document and automatically register methods.
+///
+public static class WebApplicationExtensions
+{
+ ///
+ /// Maps request handlers (see ) contained in the current assembly.
+ ///
+ /// The application builder
+ /// The file name of the Open API document
+ ///
+ /// The application builder.
+ public static async Task MapOpenApi(this IEndpointRouteBuilder app, string openApiFileName, OpenApiOptions? options = null)
+ {
+ options ??= OpenApiOptions.Default;
+
+ var openApiText = await File.ReadAllTextAsync(openApiFileName);
+ var openApiDocument = YamlSerializer.Deserialize(openApiText)!;
+
+ await openApiDocument.Initialize();
+
+ if (openApiDocument.Paths != null)
+ {
+ foreach (var (pathTemplate, pathItem) in openApiDocument.Paths)
+ {
+ MapPath(app, pathTemplate, pathItem, options);
+ }
+ }
+
+ return app;
+ }
+
+ private static Type[]? _allEntryTypes;
+ private static Type[] AllEntryTypes => _allEntryTypes ??= Assembly.GetEntryAssembly()!.GetTypes();
+
+ private static void MapPath(IEndpointRouteBuilder app, PathTemplate pathTemplate, PathItem pathItem, OpenApiOptions options)
+ {
+ var path = pathTemplate.ToString();
+ var type = AllEntryTypes.SingleOrDefault(x => x.GetCustomAttribute()?.Path == path);
+ if (type == null)
+ {
+ if (!options.IgnoreUnhandledPaths)
+ throw new NotImplementedException($"A handler for '{path}' was not found.");
+
+ return;
+ }
+
+ if (pathItem.Get is not null)
+ MapOperation(app, nameof(pathItem.Get), path, type);
+ if (pathItem.Post is not null)
+ MapOperation(app, nameof(pathItem.Post), path, type);
+ if (pathItem.Put is not null)
+ MapOperation(app, nameof(pathItem.Put), path, type);
+ if (pathItem.Delete is not null)
+ MapOperation(app, nameof(pathItem.Delete), path, type);
+ if (pathItem.Trace is not null)
+ MapOperation(app, nameof(pathItem.Trace), path, type);
+ if (pathItem.Head is not null)
+ MapOperation(app, nameof(pathItem.Head), path, type);
+ if (pathItem.Options is not null)
+ MapOperation(app, nameof(pathItem.Options), path, type);
+ if (pathItem.Patch is not null)
+ MapOperation(app, nameof(pathItem.Patch), path, type);
+ }
+
+ private static void MapOperation(IEndpointRouteBuilder app, string action, string path, Type handlerType)
+ {
+ var handlerMethods = handlerType.GetMethods(BindingFlags.Public | BindingFlags.Static)
+ .Where(x => x.Name == action);
+ foreach (var handlerMethod in handlerMethods)
+ {
+ var handlerDelegate = CreateDelegate(handlerMethod);
+ app.MapMethods(path, [action], handlerDelegate);
+ }
+ }
+
+ private static Delegate CreateDelegate(this MethodInfo methodInfo)
+ {
+ Func getType;
+ var types = methodInfo.GetParameters().Select(p => p.ParameterType);
+
+ if (methodInfo.ReturnType == typeof(void))
+ getType = Expression.GetActionType;
+ else
+ {
+ getType = Expression.GetFuncType;
+ types = types.Append(methodInfo.ReturnType);
+ }
+
+ return Delegate.CreateDelegate(getType(types.ToArray()), methodInfo);
+ }
+}
\ No newline at end of file
diff --git a/Graeae.Models.Tests/CallbackTests.cs b/Graeae.Models.Tests/CallbackTests.cs
index 3df9911..11fdc8d 100644
--- a/Graeae.Models.Tests/CallbackTests.cs
+++ b/Graeae.Models.Tests/CallbackTests.cs
@@ -8,8 +8,8 @@ public void CallbackExpressionParse()
// example from https://spec.openapis.org/oas/v3.1.0#callback-object-examples
var expr = (CallbackKeyExpression) "http://notificationServer.com?transactionId={$request.body#/id}&email={$request.body#/email}";
- Assert.AreEqual(2, expr.Parameters.Length);
- Assert.AreEqual("$request.body#/id", expr.Parameters[0]);
- Assert.AreEqual("$request.body#/email", expr.Parameters[1]);
+ Assert.That(expr.Parameters.Length, Is.EqualTo(2));
+ Assert.That(expr.Parameters[0], Is.EqualTo("$request.body#/id"));
+ Assert.That(expr.Parameters[1], Is.EqualTo("$request.body#/email"));
}
}
\ No newline at end of file
diff --git a/Graeae.Models.Tests/Graeae.Models.Tests.csproj b/Graeae.Models.Tests/Graeae.Models.Tests.csproj
index eb02367..684d0f6 100644
--- a/Graeae.Models.Tests/Graeae.Models.Tests.csproj
+++ b/Graeae.Models.Tests/Graeae.Models.Tests.csproj
@@ -12,11 +12,17 @@
-
-
-
-
-
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/Graeae.Models.Tests/PayloadValidationTests.cs b/Graeae.Models.Tests/PayloadValidationTests.cs
index 92dff8d..5ceb1ae 100644
--- a/Graeae.Models.Tests/PayloadValidationTests.cs
+++ b/Graeae.Models.Tests/PayloadValidationTests.cs
@@ -12,7 +12,7 @@ public class PayloadValidationTests
public async Task ReferencesValid(string fileName)
{
var schemaFileName = GetFile("schema-components.json");
- var fileText = File.ReadAllText(schemaFileName);
+ var fileText = await File.ReadAllTextAsync(schemaFileName);
var openApiDoc = JsonSerializer.Deserialize(fileText, TestSerializerContext.Default.OpenApiDocument);
var options = new EvaluationOptions
@@ -23,11 +23,11 @@ public async Task ReferencesValid(string fileName)
var componentRef = JsonPointer.Parse("#/components/schemas/outer");
var fullFileName = GetFile(fileName);
- var payloadJson = File.ReadAllText(fullFileName);
+ var payloadJson = await File.ReadAllTextAsync(fullFileName);
var document = JsonDocument.Parse(payloadJson);
var results = openApiDoc.EvaluatePayload(document, componentRef, options);
- Assert.True(results!.IsValid);
+ Assert.That(results!.IsValid, Is.True);
}
@@ -39,7 +39,7 @@ public async Task ReferencesInvalid(string fileName)
{
var schemaFileName = GetFile("schema-components.json");
- var fileText = File.ReadAllText(schemaFileName);
+ var fileText = await File.ReadAllTextAsync(schemaFileName);
var openApiDoc = JsonSerializer.Deserialize(fileText, TestSerializerContext.Default.OpenApiDocument);
var options = new EvaluationOptions
@@ -51,10 +51,10 @@ public async Task ReferencesInvalid(string fileName)
var componentRef = JsonPointer.Parse("#/components/schemas/outer");
var fullFileName = GetFile(fileName);
- var payloadJson = File.ReadAllText(fullFileName);
+ var payloadJson = await File.ReadAllTextAsync(fullFileName);
var document = JsonDocument.Parse(payloadJson);
var results = openApiDoc.EvaluatePayload(document, componentRef, options);
- Assert.False(results!.IsValid);
+ Assert.That(results!.IsValid, Is.False);
}
}
\ No newline at end of file
diff --git a/Graeae.Models.Tests/RefResolutionTests.cs b/Graeae.Models.Tests/RefResolutionTests.cs
index adbd0d9..7cec653 100644
--- a/Graeae.Models.Tests/RefResolutionTests.cs
+++ b/Graeae.Models.Tests/RefResolutionTests.cs
@@ -64,7 +64,7 @@ public async Task SchemaRefResolvesToAnotherPartOfOpenApiDoc()
var validation = start!.Evaluate(instance, options);
- Assert.IsTrue(validation.IsValid);
+ Assert.That(validation.IsValid, Is.True);
}
[Test]
diff --git a/Graeae.Models.Tests/RuntimeExpressionTests.cs b/Graeae.Models.Tests/RuntimeExpressionTests.cs
index 69b54df..376030e 100644
--- a/Graeae.Models.Tests/RuntimeExpressionTests.cs
+++ b/Graeae.Models.Tests/RuntimeExpressionTests.cs
@@ -18,7 +18,7 @@ public void ParseTests_Success(string source)
Console.WriteLine(backToString);
#pragma warning disable NUnit2005
- Assert.AreEqual(source, backToString);
+ Assert.That(backToString, Is.EqualTo(source));
#pragma warning restore NUnit2005
}
diff --git a/Graeae.Models.Tests/ValidationTests.cs b/Graeae.Models.Tests/ValidationTests.cs
index 5c01345..4d2ae85 100644
--- a/Graeae.Models.Tests/ValidationTests.cs
+++ b/Graeae.Models.Tests/ValidationTests.cs
@@ -1,5 +1,4 @@
using System.Text.Json;
-using System.Text.Json.Nodes;
using Json.Schema;
using Yaml2JsonNode;
@@ -31,7 +30,7 @@ public void ValidateOpenApiDoc_3_0(string fileName)
Console.WriteLine(JsonSerializer.Serialize(results, TestEnvironment.TestOutputSerializerOptions));
- Assert.IsTrue(results.IsValid);
+ Assert.That(results.IsValid, Is.True);
}
[Test]
@@ -52,6 +51,6 @@ public void ValidateOpenApiDoc_3_1(string fileName)
Console.WriteLine(JsonSerializer.Serialize(results, TestEnvironment.TestOutputSerializerOptions));
- Assert.IsTrue(results.IsValid);
+ Assert.That(results.IsValid, Is.True);
}
}
\ No newline at end of file
diff --git a/Graeae.Models/Callback.cs b/Graeae.Models/Callback.cs
index 7740920..56e1a79 100644
--- a/Graeae.Models/Callback.cs
+++ b/Graeae.Models/Callback.cs
@@ -44,10 +44,10 @@ private protected void Import(JsonObject obj, JsonSerializerOptions? options)
{
ExtensionData = ExtensionData.FromNode(obj);
- foreach (var (key, value) in obj)
+ foreach (var kvp in obj)
{
- if (key.StartsWith("x-")) continue;
- Add(CallbackKeyExpression.Parse(key), PathItem.FromNode(value, options));
+ if (kvp.Key.StartsWith("x-")) continue;
+ Add(CallbackKeyExpression.Parse(kvp.Key), PathItem.FromNode(kvp.Value, options));
}
}
@@ -65,9 +65,9 @@ private protected void Import(JsonObject obj, JsonSerializerOptions? options)
}
else
{
- foreach (var (key, value) in callback)
+ foreach (var kvp in callback)
{
- obj.Add(key.ToString(), PathItem.ToNode(value, options));
+ obj.Add(kvp.Key.ToString(), PathItem.ToNode(kvp.Value, options));
}
obj.AddExtensions(callback.ExtensionData);
}
@@ -79,7 +79,7 @@ private protected void Import(JsonObject obj, JsonSerializerOptions? options)
{
if (keys.Length == 0) return null;
- return this.GetFromMap(keys[0])?.Resolve(keys[1..]) ??
+ return this.GetFromMap(keys[0])?.Resolve(keys.Slice(1)) ??
ExtensionData?.Resolve(keys);
}
@@ -158,9 +158,9 @@ bool import(JsonNode? node)
void copy(Callback other)
{
ExtensionData = other.ExtensionData;
- foreach (var (key, value) in other)
+ foreach (var kvp in other)
{
- this[key] = value;
+ this[kvp.Key] = kvp.Value;
}
}
diff --git a/Graeae.Models/CallbackKeyExpression.cs b/Graeae.Models/CallbackKeyExpression.cs
index c76c7c9..775f963 100644
--- a/Graeae.Models/CallbackKeyExpression.cs
+++ b/Graeae.Models/CallbackKeyExpression.cs
@@ -1,4 +1,5 @@
-using System.Text.RegularExpressions;
+using System.Linq;
+using System.Text.RegularExpressions;
namespace Graeae.Models;
@@ -8,7 +9,7 @@ namespace Graeae.Models;
public class CallbackKeyExpression : IEquatable
{
private static readonly Regex TemplateVarsIdentifier = new(@"^([^{]*)(\{(?[^}]+)\}([^{])*)*$");
-
+
private readonly string _source;
///
@@ -25,7 +26,7 @@ private CallbackKeyExpression(string source, IEnumerable para
internal static CallbackKeyExpression Parse(string source)
{
var matches = TemplateVarsIdentifier.Matches(source);
- var parameters = matches.SelectMany(x => x.Groups["runtimeExpr"].Captures.Select(c => c.Value))
+ var parameters = matches.Cast().SelectMany(x => x.Groups["runtimeExpr"].Captures.Cast().Select(c => c.Value))
.Select(RuntimeExpression.Parse);
return new CallbackKeyExpression(source, parameters);
diff --git a/Graeae.Models/ComponentCollection.cs b/Graeae.Models/ComponentCollection.cs
index 45b15da..f66e411 100644
--- a/Graeae.Models/ComponentCollection.cs
+++ b/Graeae.Models/ComponentCollection.cs
@@ -1,4 +1,5 @@
-using System.Text.Json;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Json.Schema;
@@ -201,6 +202,8 @@ internal IEnumerable FindRefs()
internal class ComponentCollectionJsonConverter : JsonConverter
{
+ [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Deserialize(ref Utf8JsonReader, JsonSerializerOptions)")]
+ [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")]
public override ComponentCollection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var obj = JsonSerializer.Deserialize(ref reader, options) ??
@@ -209,6 +212,8 @@ public override ComponentCollection Read(ref Utf8JsonReader reader, Type typeToC
return ComponentCollection.FromNode(obj, options);
}
+ [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Deserialize(ref Utf8JsonReader, JsonSerializerOptions)")]
+ [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")]
public override void Write(Utf8JsonWriter writer, ComponentCollection value, JsonSerializerOptions options)
{
var json = ComponentCollection.ToNode(value, options);
diff --git a/Graeae.Models/Encoding.cs b/Graeae.Models/Encoding.cs
index bbf01f8..15c2e6a 100644
--- a/Graeae.Models/Encoding.cs
+++ b/Graeae.Models/Encoding.cs
@@ -88,7 +88,7 @@ internal static Encoding FromNode(JsonNode? node, JsonSerializerOptions? options
if (keys[0] == "headers")
{
if (keys.Length == 1) return null;
- return Headers.GetFromMap(keys[1])?.Resolve(keys[2..]);
+ return Headers.GetFromMap(keys[1])?.Resolve(keys.Slice(2));
}
return ExtensionData?.Resolve(keys);
diff --git a/Graeae.Models/Example.cs b/Graeae.Models/Example.cs
index ff131a7..ea6c6e3 100644
--- a/Graeae.Models/Example.cs
+++ b/Graeae.Models/Example.cs
@@ -1,7 +1,7 @@
-using System.Text.Json;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
-using Json.More;
namespace Graeae.Models;
@@ -12,12 +12,12 @@ namespace Graeae.Models;
public class Example : IRefTargetContainer
{
private static readonly string[] KnownKeys =
- {
+ [
"summary",
"description",
"value",
"externalValue"
- };
+ ];
///
/// Gets or sets the summary.
@@ -189,6 +189,8 @@ void copy(Example other)
internal class ExampleJsonConverter : JsonConverter
{
+ [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Deserialize(ref Utf8JsonReader, JsonSerializerOptions)")]
+ [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")]
public override Example Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var obj = JsonSerializer.Deserialize(ref reader, options) ??
@@ -197,6 +199,8 @@ public override Example Read(ref Utf8JsonReader reader, Type typeToConvert, Json
return Example.FromNode(obj);
}
+ [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Deserialize(ref Utf8JsonReader, JsonSerializerOptions)")]
+ [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")]
public override void Write(Utf8JsonWriter writer, Example value, JsonSerializerOptions options)
{
var json = Example.ToNode(value);
diff --git a/Graeae.Models/ExtensionData.cs b/Graeae.Models/ExtensionData.cs
index 91bee23..ab09d44 100644
--- a/Graeae.Models/ExtensionData.cs
+++ b/Graeae.Models/ExtensionData.cs
@@ -10,9 +10,9 @@ public class ExtensionData : Dictionary, IRefTargetContainer
internal static ExtensionData? FromNode(JsonObject obj)
{
var data = new ExtensionData();
- foreach (var (key, value) in obj.Where(x => x.Key.StartsWith("x-")))
+ foreach (var kvp in obj.Where(x => x.Key.StartsWith("x-")))
{
- data.Add(key, value);
+ data.Add(kvp.Key, kvp.Value);
}
return data.Any() ? data : null;
@@ -26,7 +26,7 @@ public class ExtensionData : Dictionary, IRefTargetContainer
if (!TryGetValue(keys[0], out var jn)) return null;
if (keys.Length == 1) return jn;
- keys[1..].ToPointer().TryEvaluate(jn, out var result);
+ keys.Slice(1).ToPointer().TryEvaluate(jn, out var result);
return result;
}
diff --git a/Graeae.Models/Graeae.Models.csproj b/Graeae.Models/Graeae.Models.csproj
index 58459ed..376de15 100644
--- a/Graeae.Models/Graeae.Models.csproj
+++ b/Graeae.Models/Graeae.Models.csproj
@@ -6,7 +6,7 @@
latest
enable
Graeae.Models
- IDE0290
+ IDE0290,CS1591,CS1735,IL3050,IL2026,IL2046
true
Greg Dennis
@@ -29,6 +29,8 @@
true
../openapi.snk
README.md
+
+ true
@@ -48,9 +50,10 @@
-
-
+
-
+
+
+
diff --git a/Graeae.Models/Graeae.Models.xml b/Graeae.Models/Graeae.Models.xml
deleted file mode 100644
index 1a3c155..0000000
--- a/Graeae.Models/Graeae.Models.xml
+++ /dev/null
@@ -1,2378 +0,0 @@
-
-
-
- Graeae.Models
-
-
-
-
- Models a callback.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Models a `$ref` to a callback.
-
-
-
-
- The URI for the reference.
-
-
-
-
- Gets or sets the summary.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets whether the reference has been resolved.
-
-
-
-
- Creates a new
-
- The reference URI
-
-
-
- Creates a new
-
- The reference URI
-
-
-
- Models a callback key expression.
-
-
-
-
- Gets the parameters that exist in the key expression.
-
-
-
-
- (not yet implemented) Resolves the callback expression.
-
- Throws not implemented.
- It's not implemented.
-
- In order to implement this, an HttpRequest or HttpResponse is required, which
- means adding a reference to ASP.net. As a result, it may make more sense for
- resolution functionality to exist in a secondary package.
-
-
-
- Indicates whether the current object is equal to another object of the same type.
- An object to compare with this object.
-
- if the current object is equal to the parameter; otherwise, .
-
-
- Returns a string that represents the current object.
- A string that represents the current object.
-
-
-
- Implicitly converts a string to a via parsing.
-
- A
-
-
-
- Models the `components` collection.
-
-
-
-
- Gets or sets the schema components.
-
-
-
-
- Gets or sets the response components.
-
-
-
-
- Gets or sets the parameter components.
-
-
-
-
- Gets or sets the example components.
-
-
-
-
- Gets or sets the request body components.
-
-
-
-
- Gets or sets the header components.
-
-
-
-
- Gets or sets the security scheme components.
-
-
-
-
- Gets or sets the link components.
-
-
-
-
- Gets or sets the callback components.
-
-
-
-
- Gets or sets the path item components.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Models the contact information.
-
-
-
-
- Gets or sets the contact name.
-
-
-
-
- Gets or sets the contact URL.
-
-
-
-
- Gets or sets the contact email.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Models an encoding object.
-
-
-
-
- Gets or sets the encoding content type.
-
-
-
-
- Gets or sets headers.
-
-
-
-
- Gets or sets the encoding parameter style.
-
-
-
-
- Gets or sets whether this will be exploded into multiple parameters.
-
-
-
-
- Gets or sets whether the parameter value SHOULD allow reserved characters.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Models an example.
-
-
-
-
- Gets or sets the summary.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets or sets the example value.
-
-
-
-
- Gets or sets a URI that points to the literal example.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Models a `$ref` to an example.
-
-
-
-
- The URI for the reference.
-
-
-
-
- Gets or sets the summary.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets whether the reference has been resolved.
-
-
-
-
- Creates a new
-
- The reference URI
-
-
-
- Creates a new
-
- The reference URI
-
-
-
- Supports extension data for all types.
-
-
-
-
- Models external documentation.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets the URL for the target documentation.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Creates a new
-
- The URL for the target documentation.
-
-
-
- Creates a new
-
- The URL for the target documentation.
-
-
-
- Provides extended JSON Schema formats.
-
-
-
-
- Validates that a number is a valid 32-bit integer.
-
-
-
-
- Validates that a number is a valid 64-bit integer.
-
-
-
-
- Validates that a number is a valid single-precision floating point value.
-
-
-
-
- Validates that a number is a valid double-precision floating point value.
-
-
-
-
- Validates that a string is a password.
-
-
-
-
- Defines the source generated JSON serialization contract metadata for a given type.
-
-
-
-
- Defines the source generated JSON serialization contract metadata for a given type.
-
-
-
-
- Defines the source generated JSON serialization contract metadata for a given type.
-
-
-
-
- Defines the source generated JSON serialization contract metadata for a given type.
-
-
-
-
- Defines the source generated JSON serialization contract metadata for a given type.
-
-
-
-
- Defines the source generated JSON serialization contract metadata for a given type.
-
-
-
-
- Defines the source generated JSON serialization contract metadata for a given type.
-
-
-
-
- Defines the source generated JSON serialization contract metadata for a given type.
-
-
-
-
- Defines the source generated JSON serialization contract metadata for a given type.
-
-
-
-
- Defines the source generated JSON serialization contract metadata for a given type.
-
-
-
-
- Defines the source generated JSON serialization contract metadata for a given type.
-
-
-
-
- Defines the source generated JSON serialization contract metadata for a given type.
-
-
-
-
- Defines the source generated JSON serialization contract metadata for a given type.
-
-
-
-
- Defines the source generated JSON serialization contract metadata for a given type.
-
-
-
-
- Defines the source generated JSON serialization contract metadata for a given type.
-
-
-
-
- Defines the source generated JSON serialization contract metadata for a given type.
-
-
-
-
- Defines the source generated JSON serialization contract metadata for a given type.
-
-
-
-
- The default associated with a default instance.
-
-
-
-
- The source-generated options associated with this context.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Models a header.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets or sets whether the header is required.
-
-
-
-
- Gets or sets whether the header is deprecated.
-
-
-
-
- Gets or sets whether the header can be present with an empty value.
-
-
-
-
- Gets or sets how the header value will be serialized.
-
-
-
-
- Gets or sets whether this will be exploded into multiple parameters.
-
-
-
-
- Gets or sets whether the parameter value should allow reserved characters.
-
-
-
-
- Gets or sets a schema for the content.
-
-
-
-
- Gets or sets an example.
-
-
-
-
- Gets or sets a collection of examples.
-
-
-
-
- Gets or sets a collection of content.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Models a `$ref` to a header.
-
-
-
-
- The URI for the reference.
-
-
-
-
- Gets or sets the summary.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets whether the reference has been resolved.
-
-
-
-
- Creates a new
-
- The reference URI
-
-
-
- Creates a new
-
- The reference URI
-
-
-
- Indicates that the item is a reference to another object rather than being the object itself.
-
-
-
-
- The URI for the reference.
-
-
-
-
- Gets or sets the summary.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets whether the reference has been resolved.
-
-
-
-
- Resolves the reference.
-
- The document root.
- Serializer options
- A task.
-
-
-
- Models the license information.
-
-
-
-
- Gets license name used for the API.
-
-
-
-
- Gets or sets an SPDX license expression for the API.
-
-
-
-
- Gets or sets URL to the license used for the API.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Creates a new
-
- The license name used for the API.
-
-
-
- Models a link object.
-
-
-
-
- Gets or sets a relative or absolute URI reference to an OAS operation.
-
-
-
-
- Gets or sets the name of the operation.
-
-
-
-
- Gets or sets the parameter collection.
-
-
-
-
- Gets or sets the request body for the target operation.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets or sets the server for the target operation.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Models a `$ref` to a link.
-
-
-
-
- The URI for the reference.
-
-
-
-
- Gets or sets the summary.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets whether the reference has been resolved.
-
-
-
-
- Creates a new
-
- The reference URI
-
-
-
- Creates a new
-
- The reference URI
-
-
-
- Models a media type object.
-
-
-
-
- Gets or sets a schema for the meta type.
-
-
-
-
- Gets or sets an example.
-
-
-
-
- Gets or sets a collection of examples.
-
-
-
-
- Gets or sets a collection of encodings.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Models an OAuth flow.
-
-
-
-
- Gets the authorization URL.
-
-
-
-
- Gets the token URL.
-
-
-
-
- Gets or sets the refresh token URL.
-
-
-
-
- Gets the scopes.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Creates a new
-
- The authorization URL
- The token URL
- The scopes
-
-
-
- Models the OAuth flow collection.
-
-
-
-
- Gets or sets the implicit flow.
-
-
-
-
- Gets or sets the password flow.
-
-
-
-
- Gets or sets the client-credentials flow.
-
-
-
-
- Gets or sets the authorization-code flow.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Models the OpenAPI document.
-
-
-
-
- Gets the OpenAPI document version.
-
-
-
-
- Gets the API information.
-
-
-
-
- Gets or sets the default JSON Schema dialect.
-
-
-
-
- Gets or sets the server collection.
-
-
-
-
- Gets or sets the paths collection.
-
-
-
-
- Gets or sets the webhooks collection.
-
-
-
-
- Gets or sets the components collection.
-
-
-
-
- Gets or sets the security requirements collection.
-
-
-
-
- Gets or sets the tags.
-
-
-
-
- Gets or sets external documentation.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Creates a new
-
- The OpenAPI version
- The API information
-
-
-
- Initializes the document model.
-
- (optional) A schema registry.
- (optional) Serializer options
- Thrown if a reference cannot be resolved.
-
-
-
- Finds and retrieves an object within the document at a specified location.
-
- The type of object
- The expected location
- The object, if an object of that type exists at that location; otherwise null.
-
-
-
- Provides extensions on various OpenAPI types.
-
-
-
-
- Validates a payload against the schema at the indicated location.
-
- The OpenAPI document.
- The payload to validate.
- The location within the document where the schema can be found.
- (optional) The evaluation options. This should be the same options object used to initialize the OpenAPI document.
- The evaluation options if the schema was found; otherwise null.
-
-
-
- Validates a payload against the schema at the indicated location.
-
- The OpenAPI document.
- The payload to validate.
- The location within the document where the schema can be found.
- (optional) The evaluation options. This should be the same options object used to initialize the OpenAPI document.
- The evaluation options if the schema was found; otherwise null.
-
-
-
- Validates a payload against the schema at the indicated location.
-
- The OpenAPI document.
- The payload to validate.
- The location within the document where the schema can be found.
- (optional) The evaluation options. This should be the same options object used to initialize the OpenAPI document.
- The evaluation options if the schema was found; otherwise null.
-
-
-
- Models the info object.
-
-
-
-
- Gets the title.
-
-
-
-
- Gets or sets the summary.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets or sets the link to the terms of service.
-
-
-
-
- Gets or sets the contact information.
-
-
-
-
- Gets or sets the license information.
-
-
-
-
- Gets or sets the API version.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Creates a new
-
- The title
- The API version
-
-
-
- Models an operation.
-
-
-
-
- Gets or sets the tags.
-
-
-
-
- Gets or sets the summary.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets or sets external documentation.
-
-
-
-
- Gets or sets the operation ID.
-
-
-
-
- Gets or sets the parameters.
-
-
-
-
- Gets or sets the request body.
-
-
-
-
- Gets or sets the response collection.
-
-
-
-
- Gets or sets the callbacks collection.
-
-
-
-
- Gets or sets whether the operation is deprecated.
-
-
-
-
- Gets or sets the security requirements.
-
-
-
-
- Gets or sets the server collection.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Models a parameter.
-
-
-
-
- Gets the name.
-
-
-
-
- Gets the parameter location.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets or sets whether the parameter is required.
-
-
-
-
- Gets or sets whether the parameter is deprecated.
-
-
-
-
- Gets or sets whether the parameter is allowed to be present with an empty value.
-
-
-
-
- Gets or sets how the parameter value will be serialized.
-
-
-
-
- Gets or sets whether this will be exploded into multiple parameters.
-
-
-
-
- Gets or sets whether the parameter value should allow reserved characters.
-
-
-
-
- Gets or sets a schema for the content.
-
-
-
-
- Gets or sets an example.
-
-
-
-
- Gets or sets a collection of examples.
-
-
-
-
- Gets or sets a collection of content.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Creates a new
-
- The name
- The parameter location
-
-
-
- Models a `$ref` to a parameter.
-
-
-
-
- The URI for the reference.
-
-
-
-
- Gets or sets the summary.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets whether the reference has been resolved.
-
-
-
-
- Creates a new
-
- The reference URI
-
-
-
- Creates a new
-
- The reference URI
-
-
-
- Defines the different parameter locations.
-
-
-
-
- Indicates the location is unknown.
-
-
-
-
- Indicates the parameter is in the query string.
-
-
-
-
- Indicates the parameter is in a header.
-
-
-
-
- Indicates the parameter is in the path.
-
-
-
-
- Indicates the parameter is in a cookie.
-
-
-
-
- Defines the different parameter styles.
-
-
-
-
- Indicates the parameter type is unknown.
-
-
-
-
- Path-style parameters defined by RFC6570
-
-
-
-
- Label style parameters defined by RFC6570
-
-
-
-
- Form style parameters defined by RFC6570
-
-
-
-
- Simple style parameters defined by RFC6570
-
-
-
-
- Space separated array or object values
-
-
-
-
- Pipe separated array or object values
-
-
-
-
- Provides a simple way of rendering nested objects using form parameters
-
-
-
-
- Models a path collection.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Models an individual path.
-
-
-
-
- Gets or sets the summary.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets or sets the GET operation.
-
-
-
-
- Gets or sets the PUT operation.
-
-
-
-
- Gets or sets the POST operation.
-
-
-
-
- Gets or sets the DELETE operation.
-
-
-
-
- Gets or sets the OPTIONS operation.
-
-
-
-
- Gets or sets the HEAD operation.
-
-
-
-
- Gets or sets the PATCH operation.
-
-
-
-
- Gets or sets the TRACE operation.
-
-
-
-
- Gets or sets the collection of servers.
-
-
-
-
- Gets or sets the collection of parameters.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Models a `$ref` to a path item.
-
-
-
-
- The URI for the reference.
-
-
-
-
- Gets or sets the summary.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets whether the reference has been resolved.
-
-
-
-
- Creates a new
-
- The reference URI
-
-
-
- Creates a new
-
- The reference URI
-
-
-
- Models a templated path URI.
-
-
-
-
- Gets the segments of the path.
-
-
-
-
- Parses a new path template from a string.
-
- The string source
- A path template.
-
-
-
- Attempts to parse a new path template from a string.
-
- The string source
- The path template if the parse succeeded, otherwise null
- True if the parse succeeded, otherwise false.
-
-
- Returns a string that represents the current object.
- A string that represents the current object.
-
-
- Indicates whether the current object is equal to another object of the same type.
- An object to compare with this object.
-
- if the current object is equal to the parameter; otherwise, .
-
-
- Indicates whether the current object is equal to another object of the same type.
- An object to compare with this object.
-
- if the current object is equal to the parameter; otherwise, .
-
-
- Determines whether the specified object is equal to the current object.
- The object to compare with the current object.
-
- if the specified object is equal to the current object; otherwise, .
-
-
- Serves as the default hash function.
- A hash code for the current object.
-
-
-
- Implicitly converts a string to a path template via parsing.
-
- The string source.
-
-
-
- Allows customization of `$ref` resolutions.
-
-
-
-
- Provides factory methods for creating references to objects in the components collection.
-
-
-
-
- Creates a reference to a callback.
-
- The key that identifies the callback.
- The reference.
-
-
-
- Creates a reference to a example.
-
- The key that identifies the example.
- The reference.
-
-
-
- Creates a reference to a header.
-
- The key that identifies the header.
- The reference.
-
-
-
- Creates a reference to a link.
-
- The key that identifies the link.
- The reference.
-
-
-
- Creates a reference to a parameter.
-
- The key that identifies the parameter.
- The reference.
-
-
-
- Creates a reference to a path item.
-
- The key that identifies the path item.
- The reference.
-
-
-
- Creates a reference to a request body.
-
- The key that identifies the request body.
- The reference.
-
-
-
- Creates a reference to a response.
-
- The key that identifies the response.
- The reference.
-
-
-
- Creates a reference to a schema.
-
- The key that identifies the schema.
- The reference.
-
-
-
- Creates a reference to a security scheme.
-
- The key that identifies the security scheme.
- The reference.
-
-
-
- Gets or sets the `$ref` fetching function.
-
-
-
-
- Defines a default basic fetching function that uses an
- and supports YAML and JSON content.
-
- The resource URI
- The JSON content as a `JsonNode`
-
-
-
- Thrown when a `$ref` cannot be resolved.
-
-
-
-
- Creates a new
-
- The exception message.
-
-
-
- Models a request body.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets the content collection.
-
-
-
-
- Gets or sets whether the request body is required.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Creates a new
-
-
-
-
-
- Models a `$ref` to a request body.
-
-
-
-
- The URI for the reference.
-
-
-
-
- Gets or sets the summary.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets whether the reference has been resolved.
-
-
-
-
- Creates a new
-
- The reference URI
-
-
-
- Creates a new
-
- The reference URI
-
-
-
- Models a response.
-
-
-
-
- Gets the description.
-
-
-
-
- Gets or sets the header collection.
-
-
-
-
- Gets or sets the content collection.
-
-
-
-
- Gets or sets the link collection.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Creates a new
-
- The description
-
-
-
- Models a `$ref` to a response.
-
-
-
-
- The URI for the reference.
-
-
-
-
- Gets or sets the summary.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets whether the reference has been resolved.
-
-
-
-
- Creates a new
-
- The reference URI
-
-
-
- Creates a new
-
- The reference URI
-
-
-
- Models a response collection.
-
-
-
-
- Gets or sets the default response for the collection.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Models an OpenAPI runtime expression.
-
-
-
-
- A `$url` runtime expression.
-
-
-
-
- A `$method` runtime expression.
-
-
-
-
- A `$statusCode` runtime expression.
-
-
-
-
- Gets the expression type.
-
-
-
-
- Gets the source type.
-
-
-
-
- Gets the token.
-
-
-
-
- Gets the name.
-
-
-
-
- Gets the JSON Pointer.
-
-
-
-
- Parses a runtime expression from a string.
-
- The string source
- A runtime expression
- Throw when the parse fails
-
-
- Returns a string that represents the current object.
- A string that represents the current object.
-
-
- Indicates whether the current object is equal to another object of the same type.
- An object to compare with this object.
-
- if the current object is equal to the parameter; otherwise, .
-
-
- Indicates whether the current object is equal to another object of the same type.
- An object to compare with this object.
-
- if the current object is equal to the parameter; otherwise, .
-
-
- Determines whether the specified object is equal to the current object.
- The object to compare with the current object.
-
- if the specified object is equal to the current object; otherwise, .
-
-
- Serves as the default hash function.
- A hash code for the current object.
-
-
-
- Defines the different runtime expression sources.
-
-
-
-
- Indicates the expression source type is unknown.
-
-
-
-
- Indicates the expression source is the header.
-
-
-
-
- Indicates the expression source is the query string.
-
-
-
-
- Indicates the expression source is the path.
-
-
-
-
- Indicates the expression source is the body.
-
-
-
-
- Defines the different types of runtime expressions.
-
-
-
-
- Indicates the expression type is unknown.
-
-
-
-
- Indicates a `$url` expression.
-
-
-
-
- Indicates a `$method` expression.
-
-
-
-
- Indicates a `$statusCode` expression.
-
-
-
-
- Indicates a `$request` expression.
-
-
-
-
- Indicates a `$response` expression.
-
-
-
-
- Overrides the JSON Schema to support draft 4 boolean values.
-
-
-
-
- The name of the keyword.
-
-
-
-
- The boolean value, if it's a boolean.
-
-
-
-
- The number value, if it's a number.
-
-
-
-
- Creates a new .
-
- Whether the `minimum` value should be considered exclusive.
-
-
-
- Creates a new .
-
- The minimum value.
-
-
- Builds a constraint object for a keyword.
- The for the schema object that houses this keyword.
-
- The set of other s that have been processed prior to this one.
- Will contain the constraints for keyword dependencies.
-
- The .
- A constraint object.
-
-
-
- JSON converter for
-
-
-
- Reads and converts the JSON to type .
- The reader.
- The type to convert.
- An object that specifies serialization options to use.
- The converted value.
-
-
- Writes a specified value as JSON.
- The writer to write to.
- The value to convert to JSON.
- An object that specifies serialization options to use.
-
-
-
- Overrides the JSON Schema to support draft 4 boolean values.
-
-
-
-
- The name of the keyword.
-
-
-
-
- The boolean value, if it's a boolean.
-
-
-
-
- The number value, if it's a number.
-
-
-
-
- Creates a new .
-
- Whether the `minimum` value should be considered exclusive.
-
-
-
- Creates a new .
-
- The minimum value.
-
-
- Builds a constraint object for a keyword.
- The for the schema object that houses this keyword.
-
- The set of other s that have been processed prior to this one.
- Will contain the constraints for keyword dependencies.
-
- The .
- A constraint object.
-
-
-
- JSON converter for
-
-
-
- Reads and converts the JSON to type .
- The reader.
- The type to convert.
- An object that specifies serialization options to use.
- The converted value.
-
-
- Writes a specified value as JSON.
- The writer to write to.
- The value to convert to JSON.
- An object that specifies serialization options to use.
-
-
-
- Represents the JSON Schema draft 4 `id` keyword.
-
-
-
-
- The name of the keyword.
-
-
-
-
- The ID.
-
-
-
-
- Creates a new .
-
- The ID.
-
-
- Builds a constraint object for a keyword.
- The for the schema object that houses this keyword.
-
- The set of other s that have been processed prior to this one.
- Will contain the constraints for keyword dependencies.
-
- The .
- A constraint object.
-
-
-
- JSON converter for
-
-
-
- Reads and converts the JSON to type .
- The reader.
- The type to convert.
- An object that specifies serialization options to use.
- The converted value.
-
-
- Writes a specified value as JSON.
- The writer to write to.
- The value to convert to JSON.
- An object that specifies serialization options to use.
-
-
-
- Provides additional functionality for JSON Schema draft 4 support.
-
-
-
-
- Defines a JSON Schema draft 4 spec version.
-
-
-
-
- Defines the OpenAPI / JSON Schema draft 4 `file` type.
-
-
-
-
- Defines the JSON Schema draft 4 meta-schema URI.
-
-
-
-
- Defines the JSON Schema draft 4 meta-schema.
-
-
-
-
- Enables support for OpenAPI v3.0 and JSON Schema draft 4.
-
-
-
-
- Overrides the JSON Schema to support draft 4.
-
-
-
-
- The name of the keyword.
-
-
-
-
- The ID.
-
-
-
-
- Creates a new .
-
- The instance type that is allowed.
-
-
- Builds a constraint object for a keyword.
- The for the schema object that houses this keyword.
-
- The set of other s that have been processed prior to this one.
- Will contain the constraints for keyword dependencies.
-
- The .
- A constraint object.
-
-
-
- JSON converter for
-
-
-
- Reads and converts the JSON to type .
- The reader.
- The type to convert.
- An object that specifies serialization options to use.
- The converted value.
-
-
- Writes a specified value as JSON.
- The writer to write to.
- The value to convert to JSON.
- An object that specifies serialization options to use.
-
-
-
- Provides extension methods for explicitly building draft 4 schemas.
-
-
-
-
- Adds the draft 4 `exclusiveMaximum` override.
-
- The builder
- The value
- The builder
-
-
-
- Adds the draft 4 `exclusiveMinimum` override.
-
- The builder
- The value
- The builder
-
-
-
- Adds the draft 4 `id` override.
-
- The builder
- The id
- The builder
-
-
-
- Adds the draft 4 `id` override.
-
- The builder
- The id
- The builder
-
-
-
- Adds the draft 4 `type` override.
-
- The builder
- The type
- The builder
-
-
-
- Adds the `nullable` keyword.
-
- The builder
- The value
- The builder
-
-
-
- Provides the OpenAPI `nullable` keyword.
-
-
-
-
- The name of the keyword.
-
-
-
-
- The ID.
-
-
-
-
- Creates a new .
-
- Whether the `minimum` value should be considered exclusive.
-
-
- Builds a constraint object for a keyword.
- The for the schema object that houses this keyword.
-
- The set of other s that have been processed prior to this one.
- Will contain the constraints for keyword dependencies.
-
- The .
- A constraint object.
-
-
-
- JSON converter for
-
-
-
- Reads and converts the JSON to type .
- The reader.
- The type to convert.
- An object that specifies serialization options to use.
- The converted value.
-
-
- Writes a specified value as JSON.
- The writer to write to.
- The value to convert to JSON.
- An object that specifies serialization options to use.
-
-
-
- Models a security requirement.
-
-
-
-
- Models a security scheme.
-
-
-
-
- Gets the type of security scheme.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets or sets the name.
-
-
-
-
- Gets or sets the location of the API key.
-
-
-
-
- Gets or sets the scheme.
-
-
-
-
- Gets or sets the bearer token format.
-
-
-
-
- Gets or sets the collection of OAuth flows.
-
-
-
-
- Gets the OpenID Connect URL.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Creates a new
-
- The security scheme type
-
-
-
- Models a `$ref` to a security scheme.
-
-
-
-
- The URI for the reference.
-
-
-
-
- Gets or sets the summary.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets whether the reference has been resolved.
-
-
-
-
- Creates a new
-
- The reference URI
-
-
-
- Creates a new
-
- The reference URI
-
-
-
- Defines the different security scheme locations.
-
-
-
-
- Indicates the location is unknown.
-
-
-
-
- Indicates the API key is located in the query.
-
-
-
-
- Indicates the API key is located in a header.
-
-
-
-
- Indicates the API key is located in a cookie.
-
-
-
-
- Models a server.
-
-
-
-
- Gets the URL of the server.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets or sets the variable map.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Creates a new
-
- The server URL
-
-
-
- Models a server variable.
-
-
-
-
- Gets or sets an enumeration of string values to be used if the substitution options are from a limited set.
-
-
-
-
- Gets the default value to use for substitution.
-
-
-
-
- Gets or sets the description.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Creates a new
-
- The default value
-
-
-
- Models a tag.
-
-
-
-
- Gets the tag name.
-
-
-
-
- Gets or sets the tag description.
-
-
-
-
- Gets or sets external documentation.
-
-
-
-
- Gets or set extension data.
-
-
-
-
- Creates a new
-
- The tag name
-
-
-
- Creates a new from a .
-
- The `JsonNode`.
- The model.
- Thrown when the JSON does not accurately represent the model.
-
-
-
diff --git a/Graeae.Models/Header.cs b/Graeae.Models/Header.cs
index a4d471b..331a11e 100644
--- a/Graeae.Models/Header.cs
+++ b/Graeae.Models/Header.cs
@@ -164,7 +164,7 @@ private protected void Import(JsonObject obj, JsonSerializerOptions? options)
// TODO: consider some other kind of value being buried in a schema
throw new NotImplementedException();
case "example":
- return Example?.GetFromNode(keys[1..]);
+ return Example?.GetFromNode(keys.Slice(1));
case "examples":
if (keys.Length == 1) return null;
keysConsumed++;
@@ -178,7 +178,7 @@ private protected void Import(JsonObject obj, JsonSerializerOptions? options)
}
return target != null
- ? target.Resolve(keys[keysConsumed..])
+ ? target.Resolve(keys.Slice(keysConsumed))
: ExtensionData?.Resolve(keys);
}
diff --git a/Graeae.Models/Link.cs b/Graeae.Models/Link.cs
index 592b96b..c9df08a 100644
--- a/Graeae.Models/Link.cs
+++ b/Graeae.Models/Link.cs
@@ -119,7 +119,7 @@ private protected void Import(JsonObject obj)
if (keys[0] == "server")
{
if (keys.Length == 1) return Server;
- return Server?.Resolve(keys[1..]);
+ return Server?.Resolve(keys.Slice(1));
}
return ExtensionData?.Resolve(keys);
diff --git a/Graeae.Models/MediaType.cs b/Graeae.Models/MediaType.cs
index 392de33..45b1243 100644
--- a/Graeae.Models/MediaType.cs
+++ b/Graeae.Models/MediaType.cs
@@ -89,7 +89,7 @@ internal static MediaType FromNode(JsonNode? node, JsonSerializerOptions? option
// TODO: consider some other kind of value being buried in a schema
throw new NotImplementedException();
case "example":
- return Example?.GetFromNode(keys[1..]);
+ return Example?.GetFromNode(keys.Slice(1));
case "examples":
if (keys.Length == 1) return null;
keysConsumed++;
@@ -103,7 +103,7 @@ internal static MediaType FromNode(JsonNode? node, JsonSerializerOptions? option
}
return target != null
- ? target.Resolve(keys[keysConsumed..])
+ ? target.Resolve(keys.Slice(keysConsumed))
: ExtensionData?.Resolve(keys);
}
diff --git a/Graeae.Models/OAuthFlow.cs b/Graeae.Models/OAuthFlow.cs
index 9fd2692..256a9d9 100644
--- a/Graeae.Models/OAuthFlow.cs
+++ b/Graeae.Models/OAuthFlow.cs
@@ -82,9 +82,9 @@ internal static OAuthFlow FromNode(JsonNode? node)
};
var scopes = new JsonObject();
- foreach (var (key, value) in flow.Scopes)
+ foreach (var kvp in flow.Scopes)
{
- scopes.Add(key, value);
+ scopes.Add(kvp.Key, kvp.Value);
}
obj.Add("scopes", scopes);
diff --git a/Graeae.Models/OAuthFlowCollection.cs b/Graeae.Models/OAuthFlowCollection.cs
index 2e14e48..e6597e1 100644
--- a/Graeae.Models/OAuthFlowCollection.cs
+++ b/Graeae.Models/OAuthFlowCollection.cs
@@ -96,7 +96,7 @@ internal static OAuthFlowCollection FromNode(JsonNode? node)
}
return target != null
- ? target.Resolve(keys[keysConsumed..])
+ ? target.Resolve(keys.Slice(keysConsumed))
: ExtensionData?.Resolve(keys);
}
}
diff --git a/Graeae.Models/OpenApiDocument.cs b/Graeae.Models/OpenApiDocument.cs
index 2316109..681faa7 100644
--- a/Graeae.Models/OpenApiDocument.cs
+++ b/Graeae.Models/OpenApiDocument.cs
@@ -84,7 +84,7 @@ public class OpenApiDocument : IBaseDocument
Uri IBaseDocument.BaseUri { get; } = GenerateBaseUri();
- private static Uri GenerateBaseUri() => new($"openapi:stj.openapi.models:{Guid.NewGuid().ToString("N")[..10]}");
+ private static Uri GenerateBaseUri() => new($"graeae:models:{Guid.NewGuid().ToString("N").AsSpan(0, 10).ToString()}");
static OpenApiDocument()
{
@@ -276,7 +276,7 @@ private async Task TryResolveRefs(JsonSerializerOptions? options)
}
return target != null
- ? target.Resolve(keys[keysConsumed..])
+ ? target.Resolve(keys.Slice(keysConsumed))
: ExtensionData?.Resolve(keys);
}
}
diff --git a/Graeae.Models/OpenApiInfo.cs b/Graeae.Models/OpenApiInfo.cs
index b58709a..1b25ec1 100644
--- a/Graeae.Models/OpenApiInfo.cs
+++ b/Graeae.Models/OpenApiInfo.cs
@@ -111,20 +111,15 @@ internal static OpenApiInfo FromNode(JsonNode? node)
{
if (keys.Length == 0) return this;
- int keysConsumed = 1;
- IRefTargetContainer? target = null;
- switch (keys[0])
+ IRefTargetContainer? target = keys[0] switch
{
- case "contact":
- target = Contact;
- break;
- case "license":
- target = License;
- break;
- }
+ "contact" => Contact,
+ "license" => License,
+ _ => null
+ };
return target != null
- ? target.Resolve(keys[keysConsumed..])
+ ? target.Resolve(keys[1..])
: ExtensionData?.Resolve(keys);
}
}
diff --git a/Graeae.Models/Operation.cs b/Graeae.Models/Operation.cs
index e5cdaff..a18e4ba 100644
--- a/Graeae.Models/Operation.cs
+++ b/Graeae.Models/Operation.cs
@@ -165,7 +165,7 @@ internal static Operation FromNode(JsonNode? node, JsonSerializerOptions? option
}
return target != null
- ? target.Resolve(keys[keysConsumed..])
+ ? target.Resolve(keys.Slice(keysConsumed))
: ExtensionData?.Resolve(keys);
}
diff --git a/Graeae.Models/Parameter.cs b/Graeae.Models/Parameter.cs
index f14e916..66defda 100644
--- a/Graeae.Models/Parameter.cs
+++ b/Graeae.Models/Parameter.cs
@@ -191,7 +191,7 @@ private protected void Import(JsonObject obj, JsonSerializerOptions? options)
// TODO: consider some other kind of value being buried in a schema
throw new NotImplementedException();
case "example":
- return Example?.GetFromNode(keys[1..]);
+ return Example?.GetFromNode(keys.Slice(1));
case "examples":
if (keys.Length == 1) return null;
keysConsumed++;
@@ -205,7 +205,7 @@ private protected void Import(JsonObject obj, JsonSerializerOptions? options)
}
return target != null
- ? target.Resolve(keys[keysConsumed..])
+ ? target.Resolve(keys.Slice(keysConsumed))
: ExtensionData?.Resolve(keys);
}
diff --git a/Graeae.Models/ParsingHelper.cs b/Graeae.Models/ParsingHelper.cs
index 5547f71..a9437bc 100644
--- a/Graeae.Models/ParsingHelper.cs
+++ b/Graeae.Models/ParsingHelper.cs
@@ -4,7 +4,7 @@ internal static class ParsingHelper
{
public static string Expect(this string source, ref int i, params string[] options)
{
- var text = source[i..];
+ var text = source.Substring(i);
foreach (var option in options)
{
if (text.StartsWith(option))
diff --git a/Graeae.Models/PathCollection.cs b/Graeae.Models/PathCollection.cs
index 225ac76..ad427d9 100644
--- a/Graeae.Models/PathCollection.cs
+++ b/Graeae.Models/PathCollection.cs
@@ -26,13 +26,13 @@ internal static PathCollection FromNode(JsonNode? node, JsonSerializerOptions? o
ExtensionData = ExtensionData.FromNode(obj)
};
- foreach (var (key, value) in obj)
+ foreach (var kvp in obj)
{
- if (key.StartsWith("x-")) continue;
- if (!PathTemplate.TryParse(key, out var template))
- throw new JsonException($"`{key}` is not a valid path template");
+ if (kvp.Key.StartsWith("x-")) continue;
+ if (!PathTemplate.TryParse(kvp.Key, out var template))
+ throw new JsonException($"`{kvp.Key}` is not a valid path template");
- collection.Add(template, PathItem.FromNode(value, options));
+ collection.Add(template, PathItem.FromNode(kvp.Value, options));
}
// Validating extra keys is done in the loop.
@@ -46,9 +46,9 @@ internal static PathCollection FromNode(JsonNode? node, JsonSerializerOptions? o
var obj = new JsonObject();
- foreach (var (key, value) in paths)
+ foreach (var kvp in paths)
{
- obj.Add(key.ToString(), PathItem.ToNode(value, options));
+ obj.Add(kvp.Key.ToString(), PathItem.ToNode(kvp.Value, options));
}
obj.AddExtensions(paths.ExtensionData);
@@ -60,7 +60,7 @@ internal static PathCollection FromNode(JsonNode? node, JsonSerializerOptions? o
{
if (keys.Length == 0) return null;
- return this.GetFromMap(keys[0])?.Resolve(keys[1..]) ??
+ return this.GetFromMap(keys[0])?.Resolve(keys.Slice(1)) ??
ExtensionData?.Resolve(keys);
}
diff --git a/Graeae.Models/PathItem.cs b/Graeae.Models/PathItem.cs
index fb4d741..5994ec7 100644
--- a/Graeae.Models/PathItem.cs
+++ b/Graeae.Models/PathItem.cs
@@ -204,7 +204,7 @@ private protected void Import(JsonObject obj, JsonSerializerOptions? options)
}
return target != null
- ? target.Resolve(keys[keysConsumed..])
+ ? target.Resolve(keys.Slice(keysConsumed))
: ExtensionData?.Resolve(keys);
}
diff --git a/Graeae.Models/PathTemplate.cs b/Graeae.Models/PathTemplate.cs
index a9bb194..6ed73fb 100644
--- a/Graeae.Models/PathTemplate.cs
+++ b/Graeae.Models/PathTemplate.cs
@@ -68,7 +68,7 @@ public bool Equals(PathTemplate? other)
if (Segments.Length != other.Segments.Length) return false;
- var zipped = Segments.Zip(other.Segments);
+ var zipped = Segments.Zip(other.Segments, (x,y) => (First: x, Second: y));
return zipped.All(x => x.First == x.Second ||
TemplatedSegmentPattern.IsMatch(x.First) && TemplatedSegmentPattern.IsMatch(x.Second));
diff --git a/Graeae.Models/RequestBody.cs b/Graeae.Models/RequestBody.cs
index 9bfb513..6df861d 100644
--- a/Graeae.Models/RequestBody.cs
+++ b/Graeae.Models/RequestBody.cs
@@ -111,7 +111,7 @@ private protected void Import(JsonObject obj)
{
if (keys.Length == 1) return null;
var target = Content.GetFromMap(keys[1]);
- return target?.Resolve(keys[2..]);
+ return target?.Resolve(keys.Slice(2));
}
return ExtensionData?.Resolve(keys);
diff --git a/Graeae.Models/Response.cs b/Graeae.Models/Response.cs
index 8b12efd..3e5000f 100644
--- a/Graeae.Models/Response.cs
+++ b/Graeae.Models/Response.cs
@@ -136,7 +136,7 @@ private protected void Import(JsonObject obj, JsonSerializerOptions? options)
}
return target != null
- ? target.Resolve(keys[keysConsumed..])
+ ? target.Resolve(keys.Slice(keysConsumed))
: ExtensionData?.Resolve(keys);
}
diff --git a/Graeae.Models/ResponseCollection.cs b/Graeae.Models/ResponseCollection.cs
index badb1d1..fbf25c7 100644
--- a/Graeae.Models/ResponseCollection.cs
+++ b/Graeae.Models/ResponseCollection.cs
@@ -32,16 +32,16 @@ internal static ResponseCollection FromNode(JsonNode? node, JsonSerializerOption
ExtensionData = ExtensionData.FromNode(obj)
};
- foreach (var (key, value) in obj)
+ foreach (var kvp in obj)
{
- if (key == "default") continue;
- if (key.StartsWith("x-")) continue;
- if (!short.TryParse(key, out var code))
- throw new JsonException($"`{key}` is not a valid status code");
+ if (kvp.Key == "default") continue;
+ if (kvp.Key.StartsWith("x-")) continue;
+ if (!short.TryParse(kvp.Key, out var code))
+ throw new JsonException($"`{kvp.Key}` is not a valid status code");
if (Enum.GetName(typeof(HttpStatusCode), code) == null)
- throw new JsonException($"`{key}` is not a known status code");
+ throw new JsonException($"`{kvp.Key}` is not a known status code");
- collection.Add((HttpStatusCode)code, Response.FromNode(value, options));
+ collection.Add((HttpStatusCode)code, Response.FromNode(kvp.Value, options));
}
// Validating extra keys is done in the loop.
@@ -55,9 +55,9 @@ internal static ResponseCollection FromNode(JsonNode? node, JsonSerializerOption
var obj = new JsonObject();
- foreach (var (key, value) in responses)
+ foreach (var kvp in responses)
{
- obj.Add(((int)key).ToString(), Response.ToNode(value, options));
+ obj.Add(((int)kvp.Key).ToString(), Response.ToNode(kvp.Value, options));
}
obj.MaybeAdd("default", Response.ToNode(responses.Default, options));
@@ -72,7 +72,7 @@ internal static ResponseCollection FromNode(JsonNode? node, JsonSerializerOption
if (keys.Length == 0) return null;
var first = keys[0];
- return this.FirstOrDefault(x => ((int)x.Key).ToString() == first).Value?.Resolve(keys[1..]) ??
+ return this.FirstOrDefault(x => ((int)x.Key).ToString() == first).Value?.Resolve(keys.Slice(1)) ??
ExtensionData?.Resolve(keys);
}
diff --git a/Graeae.Models/RuntimeExpression.cs b/Graeae.Models/RuntimeExpression.cs
index cb4838b..831e463 100644
--- a/Graeae.Models/RuntimeExpression.cs
+++ b/Graeae.Models/RuntimeExpression.cs
@@ -122,24 +122,24 @@ public static RuntimeExpression Parse(string source)
{
j++;
}
- expr.Token = source[i..j];
+ expr.Token = source.Substring(i, j-i);
break;
case "query":
expr.SourceType = RuntimeExpressionSourceType.Query;
source.Expect(ref i, ".");
- expr.Name = source[i..];
+ expr.Name = source.Substring(i);
break;
case "path":
expr.SourceType = RuntimeExpressionSourceType.Path;
source.Expect(ref i, ".");
- expr.Name = source[i..];
+ expr.Name = source.Substring(i);
break;
case "body":
expr.SourceType = RuntimeExpressionSourceType.Body;
source.Expect(ref i, "#");
if (i < source.Length)
{
- if (JsonPointer.TryParse(source[i..], out var jp))
+ if (JsonPointer.TryParse(source.Substring(i), out var jp))
expr.JsonPointer = jp;
else
throw new JsonException("Text after `#` must be a valid JSON Pointer");
diff --git a/Graeae.Models/SecurityRequirement.cs b/Graeae.Models/SecurityRequirement.cs
index d4d5184..56b5f3e 100644
--- a/Graeae.Models/SecurityRequirement.cs
+++ b/Graeae.Models/SecurityRequirement.cs
@@ -18,13 +18,13 @@ internal static SecurityRequirement FromNode(JsonNode? node)
var callback = new SecurityRequirement();
- foreach (var (key, value) in obj)
+ foreach (var kvp in obj)
{
- if (value is not JsonArray array)
+ if (kvp.Value is not JsonArray array)
throw new JsonException("security requirements must be string arrays");
- callback.Add(key, array.Select(x => x is JsonValue v && v.TryGetValue(out string? s) ? s : throw new JsonException("security requirement values must be strings")));
+ callback.Add(kvp.Key, array.Select(x => x is JsonValue v && v.TryGetValue(out string? s) ? s : throw new JsonException("security requirement values must be strings")));
}
// Validating extra keys is done in the loop.
@@ -38,9 +38,9 @@ internal static SecurityRequirement FromNode(JsonNode? node)
var obj = new JsonObject();
- foreach (var (key, value) in requirement)
+ foreach (var kvp in requirement)
{
- obj.Add(key, value.Select(x => (JsonNode?)x).ToJsonArray());
+ obj.Add(kvp.Key, kvp.Value.Select(x => (JsonNode?)x).ToJsonArray());
}
return obj;
diff --git a/Graeae.Models/SecurityScheme.cs b/Graeae.Models/SecurityScheme.cs
index 56f1a94..cc89d92 100644
--- a/Graeae.Models/SecurityScheme.cs
+++ b/Graeae.Models/SecurityScheme.cs
@@ -144,7 +144,7 @@ private protected void Import(JsonObject obj, JsonSerializerOptions? options)
if (keys[0] == "flows")
{
if (keys.Length == 1) return Flows;
- return Flows?.Resolve(keys[1..]);
+ return Flows?.Resolve(keys.Slice(1));
}
return ExtensionData?.Resolve(keys);
diff --git a/Graeae.Models/SerializationExtensions.cs b/Graeae.Models/SerializationExtensions.cs
index d412682..380a07c 100644
--- a/Graeae.Models/SerializationExtensions.cs
+++ b/Graeae.Models/SerializationExtensions.cs
@@ -54,10 +54,10 @@ public static Dictionary ExpectMap(this JsonObject obj, string pro
var deserialized = new Dictionary();
- foreach (var (key, value) in map)
+ foreach (var kvp in map)
{
- var item = factory(value);
- deserialized.Add(key, item);
+ var item = factory(kvp.Value);
+ deserialized.Add(kvp.Key, item);
}
return deserialized;
@@ -71,10 +71,10 @@ public static Dictionary ExpectMap(this JsonObject obj, string pro
var deserialized = new Dictionary();
- foreach (var (key, value) in map)
+ foreach (var kvp in map)
{
- var item = factory(value);
- deserialized.Add(key, item);
+ var item = factory(kvp.Value);
+ deserialized.Add(kvp.Key, item);
}
return deserialized;
@@ -179,9 +179,9 @@ public static void AddExtensions(this JsonObject obj, ExtensionData? extensionDa
{
if (extensionData == null) return;
- foreach (var (key, value) in extensionData)
+ foreach (var kvp in extensionData)
{
- obj.Add(key, value?.DeepClone());
+ obj.Add(kvp.Key, kvp.Value?.DeepClone());
}
}
@@ -198,10 +198,10 @@ public static void MaybeAddMap(this JsonObject obj, string propertyName, Dict
// We do this manually here because .ToDictionary() allocates an intermediate dictionary
var newObj = new JsonObject();
- foreach (var (key, value) in values)
+ foreach (var kvp in values)
{
- var node = convert(value);
- newObj.Add(key, node);
+ var node = convert(kvp.Value);
+ newObj.Add(kvp.Key, node);
}
obj.Add(propertyName, newObj);
diff --git a/Graeae.Models/Server.cs b/Graeae.Models/Server.cs
index f269c72..e12dd21 100644
--- a/Graeae.Models/Server.cs
+++ b/Graeae.Models/Server.cs
@@ -83,7 +83,7 @@ internal static Server FromNode(JsonNode? node)
if (keys[0] == "variables")
{
if (keys.Length == 1) return null;
- return Variables.GetFromMap(keys[1])?.Resolve(keys[2..]);
+ return Variables.GetFromMap(keys[1])?.Resolve(keys.Slice(2));
}
return ExtensionData?.Resolve(keys);
diff --git a/Graeae.Models/Tag.cs b/Graeae.Models/Tag.cs
index f3ad2e1..5dfaeab 100644
--- a/Graeae.Models/Tag.cs
+++ b/Graeae.Models/Tag.cs
@@ -89,7 +89,7 @@ internal static Tag FromNode(JsonNode? node)
if (keys[0] == "externalDocs")
{
if (keys.Length == 1) return ExternalDocs;
- return ExternalDocs?.Resolve(keys[1..]);
+ return ExternalDocs?.Resolve(keys.Slice(1));
}
return ExtensionData?.Resolve(keys);
diff --git a/Graeae.sln b/Graeae.sln
index 9b421b7..7fd7184 100644
--- a/Graeae.sln
+++ b/Graeae.sln
@@ -7,6 +7,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Graeae.Models", "Graeae.Mod
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Graeae.Models.Tests", "Graeae.Models.Tests\Graeae.Models.Tests.csproj", "{943DABF3-96AB-47AE-88C6-9FCEBCA82920}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Graeae.AspNet", "Graeae.AspNet\Graeae.AspNet.csproj", "{B24340A3-0F0B-4E1A-A998-0C0C49718E20}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Graeae.AspNet.Tests.Host", "Graeae.AspNet.Tests.Host\Graeae.AspNet.Tests.Host.csproj", "{1C13A90F-1A7E-4FE4-BA9F-95A35A3F97A3}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Graeae.AspNet.Analyzer", "Graeae.AspNet.Analyzer\Graeae.AspNet.Analyzer.csproj", "{BDED533C-D538-4E96-B37B-A4C65FA4D504}"
+ ProjectSection(ProjectDependencies) = postProject
+ {B24340A3-0F0B-4E1A-A998-0C0C49718E20} = {B24340A3-0F0B-4E1A-A998-0C0C49718E20}
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -26,6 +35,24 @@ Global
{943DABF3-96AB-47AE-88C6-9FCEBCA82920}.Localization|Any CPU.Build.0 = Release|Any CPU
{943DABF3-96AB-47AE-88C6-9FCEBCA82920}.Release|Any CPU.ActiveCfg = Release|Any CPU
{943DABF3-96AB-47AE-88C6-9FCEBCA82920}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B24340A3-0F0B-4E1A-A998-0C0C49718E20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B24340A3-0F0B-4E1A-A998-0C0C49718E20}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B24340A3-0F0B-4E1A-A998-0C0C49718E20}.Localization|Any CPU.ActiveCfg = Debug|Any CPU
+ {B24340A3-0F0B-4E1A-A998-0C0C49718E20}.Localization|Any CPU.Build.0 = Debug|Any CPU
+ {B24340A3-0F0B-4E1A-A998-0C0C49718E20}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B24340A3-0F0B-4E1A-A998-0C0C49718E20}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1C13A90F-1A7E-4FE4-BA9F-95A35A3F97A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1C13A90F-1A7E-4FE4-BA9F-95A35A3F97A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1C13A90F-1A7E-4FE4-BA9F-95A35A3F97A3}.Localization|Any CPU.ActiveCfg = Debug|Any CPU
+ {1C13A90F-1A7E-4FE4-BA9F-95A35A3F97A3}.Localization|Any CPU.Build.0 = Debug|Any CPU
+ {1C13A90F-1A7E-4FE4-BA9F-95A35A3F97A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1C13A90F-1A7E-4FE4-BA9F-95A35A3F97A3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BDED533C-D538-4E96-B37B-A4C65FA4D504}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BDED533C-D538-4E96-B37B-A4C65FA4D504}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BDED533C-D538-4E96-B37B-A4C65FA4D504}.Localization|Any CPU.ActiveCfg = Debug|Any CPU
+ {BDED533C-D538-4E96-B37B-A4C65FA4D504}.Localization|Any CPU.Build.0 = Debug|Any CPU
+ {BDED533C-D538-4E96-B37B-A4C65FA4D504}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BDED533C-D538-4E96-B37B-A4C65FA4D504}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE