From 96849d343219d0864bc445dc1c5319a15ffbfb39 Mon Sep 17 00:00:00 2001 From: Denis Kozak Date: Tue, 25 Jun 2024 14:20:20 +0300 Subject: [PATCH 1/2] feat(autodoc): method tags --- .../Models/OpenRpcMethod.cs | 4 +- .../PublicAPI.Shipped.txt | 2 +- .../Services/OpenRpcDocumentGenerator.cs | 49 ++++++++++++++--- .../Services/OpenRpcDocumentGeneratorTests.cs | 55 ++++++++++++------- 4 files changed, 79 insertions(+), 31 deletions(-) diff --git a/src/Tochka.JsonRpc.OpenRpc/Models/OpenRpcMethod.cs b/src/Tochka.JsonRpc.OpenRpc/Models/OpenRpcMethod.cs index 330e6df7..5d90ab4e 100644 --- a/src/Tochka.JsonRpc.OpenRpc/Models/OpenRpcMethod.cs +++ b/src/Tochka.JsonRpc.OpenRpc/Models/OpenRpcMethod.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; +using Json.Schema; namespace Tochka.JsonRpc.OpenRpc.Models; @@ -18,8 +19,9 @@ public sealed record OpenRpcMethod(string Name) /// /// A list of tags for API documentation control. Tags can be used for logical grouping of methods by resources or any other qualifier. + /// The list can use the Reference Object to link to tags that are defined by the Object. /// - public List? Tags { get; set; } + public List? Tags { get; set; } /// /// A short summary of what the method does. diff --git a/src/Tochka.JsonRpc.OpenRpc/PublicAPI.Shipped.txt b/src/Tochka.JsonRpc.OpenRpc/PublicAPI.Shipped.txt index b1c53673..186f4806 100644 --- a/src/Tochka.JsonRpc.OpenRpc/PublicAPI.Shipped.txt +++ b/src/Tochka.JsonRpc.OpenRpc/PublicAPI.Shipped.txt @@ -156,7 +156,7 @@ Tochka.JsonRpc.OpenRpc.Models.OpenRpcMethod.Servers.get -> System.Collections.Ge Tochka.JsonRpc.OpenRpc.Models.OpenRpcMethod.Servers.set -> void Tochka.JsonRpc.OpenRpc.Models.OpenRpcMethod.Summary.get -> string? Tochka.JsonRpc.OpenRpc.Models.OpenRpcMethod.Summary.set -> void -Tochka.JsonRpc.OpenRpc.Models.OpenRpcMethod.Tags.get -> System.Collections.Generic.List? +Tochka.JsonRpc.OpenRpc.Models.OpenRpcMethod.Tags.get -> System.Collections.Generic.List? Tochka.JsonRpc.OpenRpc.Models.OpenRpcMethod.Tags.set -> void Tochka.JsonRpc.OpenRpc.Models.OpenRpcParamStructure Tochka.JsonRpc.OpenRpc.Models.OpenRpcParamStructure.ByName = 1 -> Tochka.JsonRpc.OpenRpc.Models.OpenRpcParamStructure diff --git a/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcDocumentGenerator.cs b/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcDocumentGenerator.cs index a0e175b3..e5562cf4 100644 --- a/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcDocumentGenerator.cs +++ b/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcDocumentGenerator.cs @@ -2,6 +2,7 @@ using System.Reflection; using System.Text.Json; using JetBrains.Annotations; +using Json.Schema; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -44,16 +45,30 @@ public OpenRpcDocumentGenerator(IApiDescriptionGroupCollectionProvider apiDescri } /// - public Models.OpenRpc Generate(OpenRpcInfo info, string documentName, Uri host) => - new(info) + public Models.OpenRpc Generate(OpenRpcInfo info, string documentName, Uri host) + { + var tags = GetControllersTags(); + return new(info) { Servers = GetServers(host, serverOptions.RoutePrefix.Value), - Methods = GetMethods(documentName, host), + Methods = GetMethods(documentName, host, tags), Components = new() { - Schemas = schemaGenerator.GetAllSchemas() + Schemas = schemaGenerator.GetAllSchemas(), + Tags = tags } }; + } + + internal virtual Dictionary GetControllersTags() + { + var tags = apiDescriptionsProvider.ApiDescriptionGroups.Items + .SelectMany(static g => g.Items) + .Select(x => serverOptions.DefaultDataJsonSerializerOptions.ConvertName((x.ActionDescriptor as ControllerActionDescriptor)!.ControllerName)) + .Distinct(); + + return tags.ToDictionary(static x => x, static x => new OpenRpcTag(x)); + } // internal virtual for mocking in tests internal virtual List GetServers(Uri host, string? route) @@ -68,20 +83,22 @@ internal virtual List GetServers(Uri host, string? route) } // internal virtual for mocking in tests - internal virtual List GetMethods(string documentName, Uri host) => + internal virtual List GetMethods(string documentName, Uri host, Dictionary tags) => apiDescriptionsProvider.ApiDescriptionGroups.Items .SelectMany(static g => g.Items) .Where(d => !openRpcOptions.IgnoreObsoleteActions || !IsObsoleteTransitive(d)) .Where(static d => d.ActionDescriptor.EndpointMetadata.Any(static m => m is JsonRpcControllerAttribute)) .Where(d => openRpcOptions.DocInclusionPredicate(documentName, d)) - .Select(d => GetMethod(d, host)) + .Select(d => GetMethod(d, host, tags)) .OrderBy(static m => m.Name) .ToList(); // internal virtual for mocking in tests - internal virtual OpenRpcMethod GetMethod(ApiDescription apiDescription, Uri host) + internal virtual OpenRpcMethod GetMethod(ApiDescription apiDescription, Uri host, Dictionary tags) { - var methodInfo = (apiDescription.ActionDescriptor as ControllerActionDescriptor)?.MethodInfo; + var actionDescriptor = apiDescription.ActionDescriptor as ControllerActionDescriptor; + var methodInfo = actionDescriptor?.MethodInfo; + var controllerName = actionDescriptor?.ControllerName; var parametersMetadata = apiDescription.ActionDescriptor.EndpointMetadata.Get(); var serializerMetadata = apiDescription.ActionDescriptor.EndpointMetadata.Get(); var jsonSerializerOptionsProviderType = serializerMetadata?.ProviderType; @@ -97,10 +114,24 @@ internal virtual OpenRpcMethod GetMethod(ApiDescription apiDescription, Uri host Result = GetResultContentDescriptor(apiDescription, methodName, jsonSerializerOptions), Deprecated = IsObsoleteTransitive(apiDescription), Servers = GetMethodServers(apiDescription, host), - ParamStructure = GetParamsStructure(parametersMetadata) + ParamStructure = GetParamsStructure(parametersMetadata), + Tags = GetMethodTags(controllerName, tags) }; } + internal virtual List? GetMethodTags(string? controllerName, Dictionary tags) + { + if (string.IsNullOrEmpty(controllerName)) + { + return null; + } + + controllerName = serverOptions.DefaultDataJsonSerializerOptions.ConvertName(controllerName); + return tags.TryGetValue(controllerName, out _) + ? new List { new JsonSchemaBuilder().Ref($"#/components/tags/{controllerName}").Build() } + : null; + } + // internal virtual for mocking in tests internal virtual IEnumerable GetMethodParams(ApiDescription apiDescription, string methodName, JsonRpcActionParametersMetadata? parametersMetadata, JsonSerializerOptions jsonSerializerOptions) { diff --git a/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcDocumentGeneratorTests.cs b/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcDocumentGeneratorTests.cs index 614dd28d..776f232a 100644 --- a/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcDocumentGeneratorTests.cs +++ b/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcDocumentGeneratorTests.cs @@ -63,15 +63,19 @@ public void Generate_UseArgsAndInternalMethods() var servers = new List(); var methods = new List(); var schemas = new Dictionary(); + var tags = new Dictionary(); documentGeneratorMock.Setup(g => g.GetServers(host, serverOptions.RoutePrefix)) .Returns(servers) .Verifiable(); - documentGeneratorMock.Setup(g => g.GetMethods(DocumentName, host)) + documentGeneratorMock.Setup(g => g.GetMethods(DocumentName, host, tags)) .Returns(methods) .Verifiable(); schemaGeneratorMock.Setup(static g => g.GetAllSchemas()) .Returns(schemas) .Verifiable(); + documentGeneratorMock.Setup(static g => g.GetControllersTags()) + .Returns(tags) + .Verifiable(); var result = documentGeneratorMock.Object.Generate(info, DocumentName, host); @@ -81,7 +85,8 @@ public void Generate_UseArgsAndInternalMethods() Methods = methods, Components = new() { - Schemas = schemas + Schemas = schemas, + Tags = tags } }; result.Should().BeEquivalentTo(expected); @@ -120,13 +125,14 @@ public void GetMethods_IgnoreObsoleteActionsTrueInOptions_ExcludeActionsWithObso 0)) .Verifiable(); var method = new OpenRpcMethod("name"); - documentGeneratorMock.Setup(g => g.GetMethod(apiDescription1, host)) + var tags = new Dictionary(); + documentGeneratorMock.Setup(g => g.GetMethod(apiDescription1, host, tags)) .Returns(method) .Verifiable(); openRpcOptions.DocInclusionPredicate = static (_, _) => true; openRpcOptions.IgnoreObsoleteActions = true; - var result = documentGeneratorMock.Object.GetMethods(DocumentName, host); + var result = documentGeneratorMock.Object.GetMethods(DocumentName, host, tags); var expected = new[] { @@ -158,19 +164,20 @@ public void GetMethods_IgnoreObsoleteActionsFalseInOptions_IncludeActionsWithObs var method1 = new OpenRpcMethod("name1"); var method2 = new OpenRpcMethod("name2"); var method3 = new OpenRpcMethod("name3"); - documentGeneratorMock.Setup(g => g.GetMethod(apiDescription1, host)) + var tags = new Dictionary(); + documentGeneratorMock.Setup(g => g.GetMethod(apiDescription1, host, tags)) .Returns(method1) .Verifiable(); - documentGeneratorMock.Setup(g => g.GetMethod(apiDescription2, host)) + documentGeneratorMock.Setup(g => g.GetMethod(apiDescription2, host, tags)) .Returns(method2) .Verifiable(); - documentGeneratorMock.Setup(g => g.GetMethod(apiDescription3, host)) + documentGeneratorMock.Setup(g => g.GetMethod(apiDescription3, host, tags)) .Returns(method3) .Verifiable(); openRpcOptions.DocInclusionPredicate = static (_, _) => true; openRpcOptions.IgnoreObsoleteActions = false; - var result = documentGeneratorMock.Object.GetMethods(DocumentName, host); + var result = documentGeneratorMock.Object.GetMethods(DocumentName, host, tags); var expected = new[] { @@ -199,12 +206,13 @@ public void GetMethods_MethodNotFromJsonRpcController_ExcludeActions() 0)) .Verifiable(); var method = new OpenRpcMethod("name"); - documentGeneratorMock.Setup(g => g.GetMethod(apiDescription1, host)) + var tags = new Dictionary(); + documentGeneratorMock.Setup(g => g.GetMethod(apiDescription1, host, tags)) .Returns(method) .Verifiable(); openRpcOptions.DocInclusionPredicate = static (_, _) => true; - var result = documentGeneratorMock.Object.GetMethods(DocumentName, host); + var result = documentGeneratorMock.Object.GetMethods(DocumentName, host, tags); var expected = new[] { @@ -230,12 +238,13 @@ public void GetMethods_DocInclusionPredicateReturnsFalse_ExcludeActions() 0)) .Verifiable(); var method = new OpenRpcMethod("name"); - documentGeneratorMock.Setup(g => g.GetMethod(apiDescription1, host)) + var tags = new Dictionary(); + documentGeneratorMock.Setup(g => g.GetMethod(apiDescription1, host, tags)) .Returns(method) .Verifiable(); openRpcOptions.DocInclusionPredicate = (_, d) => d == apiDescription1; - var result = documentGeneratorMock.Object.GetMethods(DocumentName, host); + var result = documentGeneratorMock.Object.GetMethods(DocumentName, host, tags); var expected = new[] { @@ -262,15 +271,16 @@ public void GetMethods_OrderMethodsByName() .Verifiable(); var method1 = new OpenRpcMethod("a"); var method2 = new OpenRpcMethod("b"); - documentGeneratorMock.Setup(g => g.GetMethod(apiDescription1, host)) + var tags = new Dictionary(); + documentGeneratorMock.Setup(g => g.GetMethod(apiDescription1, host, tags)) .Returns(method1) .Verifiable(); - documentGeneratorMock.Setup(g => g.GetMethod(apiDescription2, host)) + documentGeneratorMock.Setup(g => g.GetMethod(apiDescription2, host, tags)) .Returns(method2) .Verifiable(); openRpcOptions.DocInclusionPredicate = static (_, _) => true; - var result = documentGeneratorMock.Object.GetMethods(DocumentName, host); + var result = documentGeneratorMock.Object.GetMethods(DocumentName, host, tags); var expected = new[] { @@ -292,6 +302,7 @@ public void GetMethod_UseInternalMethods() var methodServers = new List(); var methodParamsStructure = new OpenRpcParamStructure(); var parametersMetadata = new JsonRpcActionParametersMetadata(); + var tags = new Dictionary(); description.ActionDescriptor.EndpointMetadata.Add(parametersMetadata); documentGeneratorMock.Setup(g => g.GetMethodParams(description, MethodName, parametersMetadata, serverOptions.DefaultDataJsonSerializerOptions)) .Returns(methodParams) @@ -306,7 +317,7 @@ public void GetMethod_UseInternalMethods() .Returns(methodParamsStructure) .Verifiable(); - var result = documentGeneratorMock.Object.GetMethod(description, host); + var result = documentGeneratorMock.Object.GetMethod(description, host, tags); var expected = new OpenRpcMethod(MethodName) { @@ -332,6 +343,7 @@ public void GetMethod_ActionHasCustomSerializerOptions_UseSerializerOptionsFromA var methodServers = new List(); var methodParamsStructure = new OpenRpcParamStructure(); var parametersMetadata = new JsonRpcActionParametersMetadata(); + var tags = new Dictionary(); var serializerOptionsProvider = new SnakeCaseJsonSerializerOptionsProvider(); description.ActionDescriptor.EndpointMetadata.Add(parametersMetadata); description.ActionDescriptor.EndpointMetadata.Add(new JsonRpcSerializerOptionsAttribute(serializerOptionsProvider.GetType())); @@ -349,7 +361,7 @@ public void GetMethod_ActionHasCustomSerializerOptions_UseSerializerOptionsFromA .Returns(methodParamsStructure) .Verifiable(); - var result = documentGeneratorMock.Object.GetMethod(description, host); + var result = documentGeneratorMock.Object.GetMethod(description, host, tags); var expected = new OpenRpcMethod(MethodName) { @@ -375,6 +387,7 @@ public void GetMethod_MethodHasXmlDocs_UseSummaryAndRemarks() var methodServers = new List(); var methodParamsStructure = new OpenRpcParamStructure(); var parametersMetadata = new JsonRpcActionParametersMetadata(); + var tags = new Dictionary(); description.ActionDescriptor.EndpointMetadata.Add(parametersMetadata); documentGeneratorMock.Setup(g => g.GetMethodParams(description, MethodName, parametersMetadata, serverOptions.DefaultDataJsonSerializerOptions)) .Returns(methodParams) @@ -389,7 +402,7 @@ public void GetMethod_MethodHasXmlDocs_UseSummaryAndRemarks() .Returns(methodParamsStructure) .Verifiable(); - var result = documentGeneratorMock.Object.GetMethod(description, host); + var result = documentGeneratorMock.Object.GetMethod(description, host, tags); var expected = new OpenRpcMethod(MethodName) { @@ -417,6 +430,7 @@ public void GetMethod_MethodHasObsoleteAttribute_MarkAsDeprecated() var methodServers = new List(); var methodParamsStructure = new OpenRpcParamStructure(); var parametersMetadata = new JsonRpcActionParametersMetadata(); + var tags = new Dictionary(); description.ActionDescriptor.EndpointMetadata.Add(parametersMetadata); documentGeneratorMock.Setup(g => g.GetMethodParams(description, MethodName, parametersMetadata, serverOptions.DefaultDataJsonSerializerOptions)) .Returns(methodParams) @@ -431,7 +445,7 @@ public void GetMethod_MethodHasObsoleteAttribute_MarkAsDeprecated() .Returns(methodParamsStructure) .Verifiable(); - var result = documentGeneratorMock.Object.GetMethod(description, host); + var result = documentGeneratorMock.Object.GetMethod(description, host, tags); var expected = new OpenRpcMethod(MethodName) { @@ -459,6 +473,7 @@ public void GetMethod_MethodFromTypeWithObsoleteAttribute_MarkAsDeprecated() var methodServers = new List(); var methodParamsStructure = new OpenRpcParamStructure(); var parametersMetadata = new JsonRpcActionParametersMetadata(); + var tags = new Dictionary(); description.ActionDescriptor.EndpointMetadata.Add(parametersMetadata); documentGeneratorMock.Setup(g => g.GetMethodParams(description, MethodName, parametersMetadata, serverOptions.DefaultDataJsonSerializerOptions)) .Returns(methodParams) @@ -473,7 +488,7 @@ public void GetMethod_MethodFromTypeWithObsoleteAttribute_MarkAsDeprecated() .Returns(methodParamsStructure) .Verifiable(); - var result = documentGeneratorMock.Object.GetMethod(description, host); + var result = documentGeneratorMock.Object.GetMethod(description, host, tags); var expected = new OpenRpcMethod(MethodName) { From 8b2d6330204aac55058262d4b908682070af4e3e Mon Sep 17 00:00:00 2001 From: Denis Kozak Date: Tue, 25 Jun 2024 16:21:49 +0300 Subject: [PATCH 2/2] fix: remove empty tag for controller without prefix --- src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcDocumentGenerator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcDocumentGenerator.cs b/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcDocumentGenerator.cs index e5562cf4..29479723 100644 --- a/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcDocumentGenerator.cs +++ b/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcDocumentGenerator.cs @@ -65,6 +65,7 @@ internal virtual Dictionary GetControllersTags() var tags = apiDescriptionsProvider.ApiDescriptionGroups.Items .SelectMany(static g => g.Items) .Select(x => serverOptions.DefaultDataJsonSerializerOptions.ConvertName((x.ActionDescriptor as ControllerActionDescriptor)!.ControllerName)) + .Where(x => !string.IsNullOrEmpty(x)) .Distinct(); return tags.ToDictionary(static x => x, static x => new OpenRpcTag(x));