Skip to content

Commit

Permalink
Merge branch 'v12/dev' into contrib
Browse files Browse the repository at this point in the history
  • Loading branch information
nul800sebastiaan committed Jun 22, 2023
2 parents 4b0e971 + 8b1e4fa commit 62f692e
Show file tree
Hide file tree
Showing 133 changed files with 2,669 additions and 1,423 deletions.
7 changes: 2 additions & 5 deletions .github/workflows/pr-first-response.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ jobs:
issues: write
pull-requests: write
steps:
- name: Install dependencies
run: |
npm install node-fetch@2
- name: Fetch random comment 🗣️ and add it to the PR
uses: actions/github-script@v6
with:
Expand Down Expand Up @@ -44,13 +41,13 @@ jobs:
});
} else {
console.log("Returned data not indicate success.");
if(response.status !== 200) {
console.log("Status code:", response.status)
}
console.log("Returned data:", data);
if(data === '') {
console.log("An empty response usually indicates that either no comment was found or the actor user was not eligible for getting an automated response (HQ users are not getting auto-responses).")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,25 @@
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Extensions;
using OperationIdRegexes = Umbraco.Cms.Api.Common.OpenApi.OperationIdRegexes;

namespace Umbraco.Cms.Api.Common.Configuration;

public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IOptions<ApiVersioningOptions> _apiVersioningOptions;
private readonly IOperationIdSelector _operationIdSelector;
private readonly ISchemaIdSelector _schemaIdSelector;

public ConfigureUmbracoSwaggerGenOptions(IOptions<ApiVersioningOptions> apiVersioningOptions)
=> _apiVersioningOptions = apiVersioningOptions;
public ConfigureUmbracoSwaggerGenOptions(
IOptions<ApiVersioningOptions> apiVersioningOptions,
IOperationIdSelector operationIdSelector,
ISchemaIdSelector schemaIdSelector)
{
_apiVersioningOptions = apiVersioningOptions;
_operationIdSelector = operationIdSelector;
_schemaIdSelector = schemaIdSelector;
}

public void Configure(SwaggerGenOptions swaggerGenOptions)
{
Expand All @@ -30,9 +37,7 @@ public void Configure(SwaggerGenOptions swaggerGenOptions)
Description = "All endpoints not defined under specific APIs"
});

swaggerGenOptions.CustomOperationIds(description =>
CustomOperationId(description, _apiVersioningOptions.Value));

swaggerGenOptions.CustomOperationIds(description => _operationIdSelector.OperationId(description, _apiVersioningOptions.Value));
swaggerGenOptions.DocInclusionPredicate((name, api) =>
{
if (string.IsNullOrWhiteSpace(api.GroupName))
Expand All @@ -43,97 +48,18 @@ public void Configure(SwaggerGenOptions swaggerGenOptions)
if (api.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
{
return controllerActionDescriptor.MethodInfo.HasMapToApiAttribute(name);
}
return false;
});
swaggerGenOptions.TagActionsBy(api => new[] { api.GroupName });
swaggerGenOptions.OrderActionsBy(ActionOrderBy);
swaggerGenOptions.DocumentFilter<MimeTypeDocumentFilter>();
swaggerGenOptions.SchemaFilter<EnumSchemaFilter>();
swaggerGenOptions.CustomSchemaIds(SchemaIdGenerator.Generate);
swaggerGenOptions.CustomSchemaIds(_schemaIdSelector.SchemaId);
swaggerGenOptions.SupportNonNullableReferenceTypes();
swaggerGenOptions.UseOneOfForPolymorphism();
swaggerGenOptions.UseAllOfForInheritance();
var cachedApiElementNamespace = typeof(ApiElement).Namespace ?? string.Empty;
swaggerGenOptions.SelectDiscriminatorNameUsing(type =>
{
if (type.Namespace != null && type.Namespace.StartsWith(cachedApiElementNamespace))
{
// We do not show type on delivery, as it is read only.
return null;
}
if (type.GetInterfaces().Any())
{
return "$type";
}
return null;
});
swaggerGenOptions.SelectDiscriminatorValueUsing(x => x.Name);
}

private static string CustomOperationId(ApiDescription api, ApiVersioningOptions apiVersioningOptions)
{
ApiVersion defaultVersion = apiVersioningOptions.DefaultApiVersion;
var httpMethod = api.HttpMethod?.ToLower().ToFirstUpper() ?? "Get";

// if the route info "Name" is supplied we'll use this explicitly as the operation ID
// - usage example: [HttpGet("my-api/route}", Name = "MyCustomRoute")]
if (string.IsNullOrWhiteSpace(api.ActionDescriptor.AttributeRouteInfo?.Name) == false)
{
var explicitOperationId = api.ActionDescriptor.AttributeRouteInfo!.Name;
return explicitOperationId.InvariantStartsWith(httpMethod)
? explicitOperationId
: $"{httpMethod}{explicitOperationId}";
}

var relativePath = api.RelativePath;

if (string.IsNullOrWhiteSpace(relativePath))
{
throw new Exception(
$"There is no relative path for controller action {api.ActionDescriptor.RouteValues["controller"]}");
}

// Remove the prefixed base path with version, e.g. /umbraco/management/api/v1/tracked-reference/{id} => tracked-reference/{id}
var unprefixedRelativePath = OperationIdRegexes
.VersionPrefixRegex()
.Replace(relativePath, string.Empty);

// Remove template placeholders, e.g. tracked-reference/{id} => tracked-reference/Id
var formattedOperationId = OperationIdRegexes
.TemplatePlaceholdersRegex()
.Replace(unprefixedRelativePath, m => $"By{m.Groups[1].Value.ToFirstUpper()}");

// Remove dashes (-) and slashes (/) and convert the following letter to uppercase with
// the word "By" in front, e.g. tracked-reference/Id => TrackedReferenceById
formattedOperationId = OperationIdRegexes
.ToCamelCaseRegex()
.Replace(formattedOperationId, m => m.Groups[1].Value.ToUpper());

//Get map to version attribute
string? version = null;

if (api.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
{
var versionAttributeValue = controllerActionDescriptor.MethodInfo.GetMapToApiVersionAttributeValue();

// We only wanna add a version, if it is not the default one.
if (string.Equals(versionAttributeValue, defaultVersion.ToString()) == false)
{
version = versionAttributeValue;
}
}

// Return the operation ID with the formatted http method verb in front, e.g. GetTrackedReferenceById
return $"{httpMethod}{formattedOperationId.ToFirstUpper()}{version}";
}

// see https://github.com/domaindrivendev/Swashbuckle.AspNetCore#change-operation-sort-order-eg-for-ui-sorting
private static string ActionOrderBy(ApiDescription apiDesc)
=> $"{apiDesc.GroupName}_{apiDesc.ActionDescriptor.AttributeRouteInfo?.Template ?? apiDesc.ActionDescriptor.RouteValues["controller"]}_{apiDesc.ActionDescriptor.RouteValues["action"]}_{apiDesc.HttpMethod}";

}
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.Configuration;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Api.Common.Serialization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
using Umbraco.Extensions;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;

namespace Umbraco.Cms.Api.Common.DependencyInjection;

Expand All @@ -25,50 +14,9 @@ public static IUmbracoBuilder AddUmbracoApiOpenApiUI(this IUmbracoBuilder builde
builder.Services.AddSwaggerGen();
builder.Services.ConfigureOptions<ConfigureUmbracoSwaggerGenOptions>();
builder.Services.AddSingleton<IUmbracoJsonTypeInfoResolver, UmbracoJsonTypeInfoResolver>();

builder.Services.Configure<UmbracoPipelineOptions>(options =>
{
options.AddFilter(new UmbracoPipelineFilter(
"UmbracoApiCommon",
applicationBuilder =>
{
},
applicationBuilder =>
{
IServiceProvider provider = applicationBuilder.ApplicationServices;
IWebHostEnvironment webHostEnvironment = provider.GetRequiredService<IWebHostEnvironment>();
IOptions<SwaggerGenOptions> swaggerGenOptions = provider.GetRequiredService<IOptions<SwaggerGenOptions>>();
if (!webHostEnvironment.IsProduction())
{
GlobalSettings? settings = provider.GetRequiredService<IOptions<GlobalSettings>>().Value;
IHostingEnvironment hostingEnvironment = provider.GetRequiredService<IHostingEnvironment>();
var umbracoPath = settings.GetBackOfficePath(hostingEnvironment);
applicationBuilder.UseSwagger(swaggerOptions =>
{
swaggerOptions.RouteTemplate =
$"{umbracoPath.TrimStart(Constants.CharArrays.ForwardSlash)}/swagger/{{documentName}}/swagger.json";
});
applicationBuilder.UseSwaggerUI(
swaggerUiOptions =>
{
swaggerUiOptions.RoutePrefix = $"{umbracoPath.TrimStart(Constants.CharArrays.ForwardSlash)}/swagger";
foreach ((var name, OpenApiInfo? apiInfo) in swaggerGenOptions.Value.SwaggerGeneratorOptions.SwaggerDocs.OrderBy(x=>x.Value.Title))
{
swaggerUiOptions.SwaggerEndpoint($"{name}/swagger.json", $"{apiInfo.Title}");
}
});
}
},
applicationBuilder =>
{
}));
});
builder.Services.AddSingleton<IOperationIdSelector, OperationIdSelector>();
builder.Services.AddSingleton<ISchemaIdSelector, SchemaIdSelector>();
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new SwaggerRouteTemplatePipelineFilter("UmbracoApiCommon")));

return builder;
}
Expand Down
9 changes: 9 additions & 0 deletions src/Umbraco.Cms.Api.Common/OpenApi/IOperationIdSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.ApiExplorer;

namespace Umbraco.Cms.Api.Common.OpenApi;

public interface IOperationIdSelector
{
string? OperationId(ApiDescription apiDescription, ApiVersioningOptions apiVersioningOptions);
}
6 changes: 6 additions & 0 deletions src/Umbraco.Cms.Api.Common/OpenApi/ISchemaIdSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Common.OpenApi;

public interface ISchemaIdSelector
{
string SchemaId(Type type);
}
11 changes: 10 additions & 1 deletion src/Umbraco.Cms.Api.Common/OpenApi/MimeTypeDocumentFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,21 @@
namespace Umbraco.Cms.Api.Common.OpenApi;

/// <summary>
/// This filter explicitly removes all other mime types than application/json from the produced OpenAPI document when application/json is accepted.
/// This filter explicitly removes all other mime types than application/json from a named OpenAPI document when application/json is accepted.
/// </summary>
public class MimeTypeDocumentFilter : IDocumentFilter
{
private readonly string _documentName;

public MimeTypeDocumentFilter(string documentName) => _documentName = documentName;

public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
if (context.DocumentName != _documentName)
{
return;
}

OpenApiOperation[] operations = swaggerDoc.Paths
.SelectMany(path => path.Value.Operations.Values)
.ToArray();
Expand Down
79 changes: 79 additions & 0 deletions src/Umbraco.Cms.Api.Common/OpenApi/OperationIdSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Umbraco.Extensions;

namespace Umbraco.Cms.Api.Common.OpenApi;

public class OperationIdSelector : IOperationIdSelector
{
public virtual string? OperationId(ApiDescription apiDescription, ApiVersioningOptions apiVersioningOptions)
{
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor
|| controllerActionDescriptor.ControllerTypeInfo.Namespace?.StartsWith("Umbraco.Cms.Api") is not true)
{
return null;
}

return UmbracoOperationId(apiDescription, apiVersioningOptions);
}

protected string? UmbracoOperationId(ApiDescription apiDescription, ApiVersioningOptions apiVersioningOptions)
{
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
{
return null;
}

ApiVersion defaultVersion = apiVersioningOptions.DefaultApiVersion;
var httpMethod = apiDescription.HttpMethod?.ToLower().ToFirstUpper() ?? "Get";

// if the route info "Name" is supplied we'll use this explicitly as the operation ID
// - usage example: [HttpGet("my-api/route}", Name = "MyCustomRoute")]
if (string.IsNullOrWhiteSpace(apiDescription.ActionDescriptor.AttributeRouteInfo?.Name) == false)
{
var explicitOperationId = apiDescription.ActionDescriptor.AttributeRouteInfo!.Name;
return explicitOperationId.InvariantStartsWith(httpMethod)
? explicitOperationId
: $"{httpMethod}{explicitOperationId}";
}

var relativePath = apiDescription.RelativePath;

if (string.IsNullOrWhiteSpace(relativePath))
{
throw new Exception(
$"There is no relative path for controller action {apiDescription.ActionDescriptor.RouteValues["controller"]}");
}

// Remove the prefixed base path with version, e.g. /umbraco/management/api/v1/tracked-reference/{id} => tracked-reference/{id}
var unprefixedRelativePath = OperationIdRegexes
.VersionPrefixRegex()
.Replace(relativePath, string.Empty);

// Remove template placeholders, e.g. tracked-reference/{id} => tracked-reference/Id
var formattedOperationId = OperationIdRegexes
.TemplatePlaceholdersRegex()
.Replace(unprefixedRelativePath, m => $"By{m.Groups[1].Value.ToFirstUpper()}");

// Remove dashes (-) and slashes (/) and convert the following letter to uppercase with
// the word "By" in front, e.g. tracked-reference/Id => TrackedReferenceById
formattedOperationId = OperationIdRegexes
.ToCamelCaseRegex()
.Replace(formattedOperationId, m => m.Groups[1].Value.ToUpper());

//Get map to version attribute
string? version = null;

var versionAttributeValue = controllerActionDescriptor.MethodInfo.GetMapToApiVersionAttributeValue();

// We only wanna add a version, if it is not the default one.
if (string.Equals(versionAttributeValue, defaultVersion.ToString()) == false)
{
version = versionAttributeValue;
}

// Return the operation ID with the formatted http method verb in front, e.g. GetTrackedReferenceById
return $"{httpMethod}{formattedOperationId.ToFirstUpper()}{version}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

namespace Umbraco.Cms.Api.Common.OpenApi;

internal static class SchemaIdGenerator
public class SchemaIdSelector : ISchemaIdSelector
{
public static string Generate(Type type)
public virtual string SchemaId(Type type)
=> type.Namespace?.StartsWith("Umbraco.Cms") is true ? UmbracoSchemaId(type) : type.Name;

protected string UmbracoSchemaId(Type type)
{
string SanitizedTypeName(Type t) => t.Name
// first grab the "non generic" part of any generic type name (i.e. "PagedViewModel`1" becomes "PagedViewModel")
Expand Down

0 comments on commit 62f692e

Please sign in to comment.