Skip to content

Commit

Permalink
Merge pull request #107 from SedativeEffect/feature/C-SHARP-207-metho…
Browse files Browse the repository at this point in the history
…d-tags

feat(autodoc): method tags
  • Loading branch information
Rast1234 authored Jul 9, 2024
2 parents 18acc96 + 8b2d633 commit f9e3fe5
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 31 deletions.
4 changes: 3 additions & 1 deletion src/Tochka.JsonRpc.OpenRpc/Models/OpenRpcMethod.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Json.Schema;

namespace Tochka.JsonRpc.OpenRpc.Models;

Expand All @@ -18,8 +19,9 @@ public sealed record OpenRpcMethod(string Name)

/// <summary>
/// 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 <see cref="OpenRpcTag" /> Object.
/// </summary>
public List<OpenRpcTag>? Tags { get; set; }
public List<JsonSchema>? Tags { get; set; }

/// <summary>
/// A short summary of what the method does.
Expand Down
2 changes: 1 addition & 1 deletion src/Tochka.JsonRpc.OpenRpc/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.OpenRpcTag!>?
Tochka.JsonRpc.OpenRpc.Models.OpenRpcMethod.Tags.get -> System.Collections.Generic.List<Json.Schema.JsonSchema!>?
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
Expand Down
50 changes: 41 additions & 9 deletions src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcDocumentGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -44,16 +45,31 @@ public OpenRpcDocumentGenerator(IApiDescriptionGroupCollectionProvider apiDescri
}

/// <inheritdoc />
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<string, OpenRpcTag> 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));
}

// internal virtual for mocking in tests
internal virtual List<OpenRpcServer> GetServers(Uri host, string? route)
Expand All @@ -68,20 +84,22 @@ internal virtual List<OpenRpcServer> GetServers(Uri host, string? route)
}

// internal virtual for mocking in tests
internal virtual List<OpenRpcMethod> GetMethods(string documentName, Uri host) =>
internal virtual List<OpenRpcMethod> GetMethods(string documentName, Uri host, Dictionary<string, OpenRpcTag> 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<string, OpenRpcTag> 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<JsonRpcActionParametersMetadata>();
var serializerMetadata = apiDescription.ActionDescriptor.EndpointMetadata.Get<JsonRpcSerializerOptionsAttribute>();
var jsonSerializerOptionsProviderType = serializerMetadata?.ProviderType;
Expand All @@ -97,10 +115,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<JsonSchema>? GetMethodTags(string? controllerName, Dictionary<string, OpenRpcTag> tags)
{
if (string.IsNullOrEmpty(controllerName))
{
return null;
}

controllerName = serverOptions.DefaultDataJsonSerializerOptions.ConvertName(controllerName);
return tags.TryGetValue(controllerName, out _)
? new List<JsonSchema> { new JsonSchemaBuilder().Ref($"#/components/tags/{controllerName}").Build() }
: null;
}

// internal virtual for mocking in tests
internal virtual IEnumerable<OpenRpcContentDescriptor> GetMethodParams(ApiDescription apiDescription, string methodName, JsonRpcActionParametersMetadata? parametersMetadata, JsonSerializerOptions jsonSerializerOptions)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,19 @@ public void Generate_UseArgsAndInternalMethods()
var servers = new List<OpenRpcServer>();
var methods = new List<OpenRpcMethod>();
var schemas = new Dictionary<string, JsonSchema>();
var tags = new Dictionary<string, OpenRpcTag>();
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);

Expand All @@ -81,7 +85,8 @@ public void Generate_UseArgsAndInternalMethods()
Methods = methods,
Components = new()
{
Schemas = schemas
Schemas = schemas,
Tags = tags
}
};
result.Should().BeEquivalentTo(expected);
Expand Down Expand Up @@ -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<string, OpenRpcTag>();
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[]
{
Expand Down Expand Up @@ -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<string, OpenRpcTag>();
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[]
{
Expand Down Expand Up @@ -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<string, OpenRpcTag>();
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[]
{
Expand All @@ -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<string, OpenRpcTag>();
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[]
{
Expand All @@ -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<string, OpenRpcTag>();
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[]
{
Expand All @@ -292,6 +302,7 @@ public void GetMethod_UseInternalMethods()
var methodServers = new List<OpenRpcServer>();
var methodParamsStructure = new OpenRpcParamStructure();
var parametersMetadata = new JsonRpcActionParametersMetadata();
var tags = new Dictionary<string, OpenRpcTag>();
description.ActionDescriptor.EndpointMetadata.Add(parametersMetadata);
documentGeneratorMock.Setup(g => g.GetMethodParams(description, MethodName, parametersMetadata, serverOptions.DefaultDataJsonSerializerOptions))
.Returns(methodParams)
Expand All @@ -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)
{
Expand All @@ -332,6 +343,7 @@ public void GetMethod_ActionHasCustomSerializerOptions_UseSerializerOptionsFromA
var methodServers = new List<OpenRpcServer>();
var methodParamsStructure = new OpenRpcParamStructure();
var parametersMetadata = new JsonRpcActionParametersMetadata();
var tags = new Dictionary<string, OpenRpcTag>();
var serializerOptionsProvider = new SnakeCaseJsonSerializerOptionsProvider();
description.ActionDescriptor.EndpointMetadata.Add(parametersMetadata);
description.ActionDescriptor.EndpointMetadata.Add(new JsonRpcSerializerOptionsAttribute(serializerOptionsProvider.GetType()));
Expand All @@ -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)
{
Expand All @@ -375,6 +387,7 @@ public void GetMethod_MethodHasXmlDocs_UseSummaryAndRemarks()
var methodServers = new List<OpenRpcServer>();
var methodParamsStructure = new OpenRpcParamStructure();
var parametersMetadata = new JsonRpcActionParametersMetadata();
var tags = new Dictionary<string, OpenRpcTag>();
description.ActionDescriptor.EndpointMetadata.Add(parametersMetadata);
documentGeneratorMock.Setup(g => g.GetMethodParams(description, MethodName, parametersMetadata, serverOptions.DefaultDataJsonSerializerOptions))
.Returns(methodParams)
Expand All @@ -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)
{
Expand Down Expand Up @@ -417,6 +430,7 @@ public void GetMethod_MethodHasObsoleteAttribute_MarkAsDeprecated()
var methodServers = new List<OpenRpcServer>();
var methodParamsStructure = new OpenRpcParamStructure();
var parametersMetadata = new JsonRpcActionParametersMetadata();
var tags = new Dictionary<string, OpenRpcTag>();
description.ActionDescriptor.EndpointMetadata.Add(parametersMetadata);
documentGeneratorMock.Setup(g => g.GetMethodParams(description, MethodName, parametersMetadata, serverOptions.DefaultDataJsonSerializerOptions))
.Returns(methodParams)
Expand All @@ -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)
{
Expand Down Expand Up @@ -459,6 +473,7 @@ public void GetMethod_MethodFromTypeWithObsoleteAttribute_MarkAsDeprecated()
var methodServers = new List<OpenRpcServer>();
var methodParamsStructure = new OpenRpcParamStructure();
var parametersMetadata = new JsonRpcActionParametersMetadata();
var tags = new Dictionary<string, OpenRpcTag>();
description.ActionDescriptor.EndpointMetadata.Add(parametersMetadata);
documentGeneratorMock.Setup(g => g.GetMethodParams(description, MethodName, parametersMetadata, serverOptions.DefaultDataJsonSerializerOptions))
.Returns(methodParams)
Expand All @@ -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)
{
Expand Down

0 comments on commit f9e3fe5

Please sign in to comment.