From e77ea77bae76599587902461f66bccef8397b7a4 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 8 Jul 2017 11:14:29 +0200 Subject: [PATCH] Refactor Swagger generator. --- .../StringExtensions.cs | 5 + .../Contents/Builders/ContentSchemaBuilder.cs | 57 +++ .../Contents/Builders/EdmModelBuilder.cs | 1 + src/Squidex.Read/Users/UserExtensions.cs | 2 +- .../Users/UserManagerExtensions.cs | 2 +- src/Squidex/Config/Swagger/SwaggerServices.cs | 2 +- .../Swagger/XmlResponseTypesProcessor.cs | 24 +- .../Generator/SchemaSwaggerGenerator.cs | 227 +++++++++++ .../Generator/SchemasSwaggerGenerator.cs | 352 ++---------------- src/Squidex/Pipeline/Swagger/SwaggerHelper.cs | 65 +++- tests/Benchmarks/Tests/HandleEvents.cs | 1 - 11 files changed, 372 insertions(+), 366 deletions(-) create mode 100644 src/Squidex.Read/Contents/Builders/ContentSchemaBuilder.cs create mode 100644 src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs diff --git a/src/Squidex.Infrastructure/StringExtensions.cs b/src/Squidex.Infrastructure/StringExtensions.cs index 1560c8c07..a44942c1b 100644 --- a/src/Squidex.Infrastructure/StringExtensions.cs +++ b/src/Squidex.Infrastructure/StringExtensions.cs @@ -36,5 +36,10 @@ namespace Squidex.Infrastructure { return string.Concat(value.Split(new[] { '-', '_', ' ' }, StringSplitOptions.RemoveEmptyEntries).Select(c => char.ToUpper(c[0]) + c.Substring(1))); } + + public static string WithFallback(this string value, string fallback) + { + return !string.IsNullOrWhiteSpace(value) ? value.Trim() : fallback; + } } } diff --git a/src/Squidex.Read/Contents/Builders/ContentSchemaBuilder.cs b/src/Squidex.Read/Contents/Builders/ContentSchemaBuilder.cs new file mode 100644 index 000000000..22f440d61 --- /dev/null +++ b/src/Squidex.Read/Contents/Builders/ContentSchemaBuilder.cs @@ -0,0 +1,57 @@ +// ========================================================================== +// ContentSchemaBuilder.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using NJsonSchema; +using Squidex.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Read.Contents.Builders +{ + public sealed class ContentSchemaBuilder + { + public JsonSchema4 CreateContentSchema(Schema schema, JsonSchema4 dataSchema) + { + Guard.NotNull(schema, nameof(schema)); + Guard.NotNull(dataSchema, nameof(dataSchema)); + + var schemaName = schema.Properties.Label.WithFallback(schema.Name); + + var contentSchema = new JsonSchema4 + { + Properties = + { + ["id"] = CreateProperty($"The id of the {schemaName} content."), + ["data"] = CreateProperty($"The data of the {schemaName}.", dataSchema), + ["version"] = CreateProperty($"The version of the {schemaName}.", JsonObjectType.Number), + ["created"] = CreateProperty($"The date and time when the {schemaName} content has been created.", "date-time"), + ["createdBy"] = CreateProperty($"The user that has created the {schemaName} content."), + ["lastModified"] = CreateProperty($"The date and time when the {schemaName} content has been modified last.", "date-time"), + ["lastModifiedBy"] = CreateProperty($"The user that has updated the {schemaName} content last.") + }, + Type = JsonObjectType.Object + }; + + return contentSchema; + } + + private static JsonProperty CreateProperty(string description, JsonSchema4 dataSchema) + { + return new JsonProperty { Description = description, IsRequired = true, Type = JsonObjectType.Object, SchemaReference = dataSchema }; + } + + private static JsonProperty CreateProperty(string description, JsonObjectType type) + { + return new JsonProperty { Description = description, IsRequired = true, Type = type }; + } + + private static JsonProperty CreateProperty(string description, string format = null) + { + return new JsonProperty { Description = description, Format = format, IsRequired = true, Type = JsonObjectType.String }; + } + } +} diff --git a/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs b/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs index 58d5a16fc..02098f409 100644 --- a/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs +++ b/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs @@ -57,6 +57,7 @@ namespace Squidex.Read.Contents.Builders var entityType = new EdmEntityType("Squidex", schema.Name); entityType.AddStructuralProperty("data", new EdmComplexTypeReference(schemaType, false)); + entityType.AddStructuralProperty("version", EdmPrimitiveTypeKind.Int32); entityType.AddStructuralProperty("created", EdmPrimitiveTypeKind.DateTimeOffset); entityType.AddStructuralProperty("createdBy", EdmPrimitiveTypeKind.String); entityType.AddStructuralProperty("lastModified", EdmPrimitiveTypeKind.DateTimeOffset); diff --git a/src/Squidex.Read/Users/UserExtensions.cs b/src/Squidex.Read/Users/UserExtensions.cs index 03ba924bb..cfcdba5cb 100644 --- a/src/Squidex.Read/Users/UserExtensions.cs +++ b/src/Squidex.Read/Users/UserExtensions.cs @@ -56,7 +56,7 @@ namespace Squidex.Read.Users { var url = user.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.SquidexPictureUrl)?.Value; - if (!string.IsNullOrWhiteSpace(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute) && url.Contains("gravatar")) + if (url != null && !string.IsNullOrWhiteSpace(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute) && url.Contains("gravatar")) { if (url.Contains("?")) { diff --git a/src/Squidex.Read/Users/UserManagerExtensions.cs b/src/Squidex.Read/Users/UserManagerExtensions.cs index bd2583c19..cdb2638ec 100644 --- a/src/Squidex.Read/Users/UserManagerExtensions.cs +++ b/src/Squidex.Read/Users/UserManagerExtensions.cs @@ -39,7 +39,7 @@ namespace Squidex.Read.Users { var result = userManager.Users; - if (!string.IsNullOrWhiteSpace(email)) + if (email != null && !string.IsNullOrWhiteSpace(email)) { var upperEmail = email.ToUpperInvariant(); diff --git a/src/Squidex/Config/Swagger/SwaggerServices.cs b/src/Squidex/Config/Swagger/SwaggerServices.cs index 5efde2a63..e2b02792b 100644 --- a/src/Squidex/Config/Swagger/SwaggerServices.cs +++ b/src/Squidex/Config/Swagger/SwaggerServices.cs @@ -87,7 +87,7 @@ namespace Squidex.Config.Swagger settings.DocumentProcessors.Add(new XmlTagProcessor()); settings.OperationProcessors.Add(new XmlTagProcessor()); - settings.OperationProcessors.Add(new XmlResponseTypesProcessor(settings)); + settings.OperationProcessors.Add(new XmlResponseTypesProcessor()); return settings; } diff --git a/src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs b/src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs index 86d4c71a0..04e0f15b6 100644 --- a/src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs +++ b/src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs @@ -6,16 +6,13 @@ // All rights reserved. // ========================================================================== -using System; using System.Text.RegularExpressions; using System.Threading.Tasks; -using NJsonSchema.Generation; using NJsonSchema.Infrastructure; using NSwag; -using NSwag.AspNetCore; using NSwag.SwaggerGeneration.Processors; using NSwag.SwaggerGeneration.Processors.Contexts; -using Squidex.Controllers.Api; +using Squidex.Pipeline.Swagger; // ReSharper disable UseObjectOrCollectionInitializer @@ -25,13 +22,6 @@ namespace Squidex.Config.Swagger { private static readonly Regex ResponseRegex = new Regex("(?[0-9]{3}) => (?.*)", RegexOptions.Compiled); - private readonly SwaggerSettings swaggerSettings; - - public XmlResponseTypesProcessor(SwaggerSettings swaggerSettings) - { - this.swaggerSettings = swaggerSettings; - } - public async Task ProcessAsync(OperationProcessorContext context) { var hasOkResponse = false; @@ -69,22 +59,14 @@ namespace Squidex.Config.Swagger return true; } - private async Task AddInternalErrorResponseAsync(OperationProcessorContext context, SwaggerOperation operation) + private static async Task AddInternalErrorResponseAsync(OperationProcessorContext context, SwaggerOperation operation) { if (operation.Responses.ContainsKey("500")) { return; } - var errorType = typeof(ErrorDto); - var errorContract = swaggerSettings.ActualContractResolver.ResolveContract(errorType); - var errorSchema = JsonObjectTypeDescription.FromType(errorType, errorContract, new Attribute[0], swaggerSettings.DefaultEnumHandling); - - var response = new SwaggerResponse { Description = "Operation failed." }; - - response.Schema = await context.SwaggerGenerator.GenerateAndAppendSchemaFromTypeAsync(errorType, errorSchema.IsNullable, null); - - operation.Responses.Add("500", response); + operation.AddResponse("500", "Operation failed", await context.SwaggerGenerator.GetErrorDtoSchemaAsync()); } private static void RemoveOkResponse(SwaggerOperation operation) diff --git a/src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs b/src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs new file mode 100644 index 000000000..8b480bf41 --- /dev/null +++ b/src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs @@ -0,0 +1,227 @@ +// ========================================================================== +// SchemaSwaggerGenerator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using NJsonSchema; +using NSwag; +using Squidex.Core; +using Squidex.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Pipeline.Swagger; +using Squidex.Read.Contents.Builders; + +// ReSharper disable InvertIf + +namespace Squidex.Controllers.ContentApi.Generator +{ + public sealed class SchemaSwaggerGenerator + { + private static readonly string schemaQueryDescription; + private static readonly string schemaBodyDescription; + private readonly ContentSchemaBuilder schemaBuilder = new ContentSchemaBuilder(); + private readonly SwaggerDocument document; + private readonly JsonSchema4 contentSchema; + private readonly JsonSchema4 dataSchema; + private readonly string schemaPath; + private readonly string schemaName; + private readonly string schemaKey; + private readonly string appPath; + + static SchemaSwaggerGenerator() + { + schemaBodyDescription = SwaggerHelper.LoadDocs("schemabody"); + schemaQueryDescription = SwaggerHelper.LoadDocs("schemaquery"); + } + + public SchemaSwaggerGenerator(SwaggerDocument document, string path, Schema schema, Func schemaResolver, PartitionResolver partitionResolver) + { + this.document = document; + + appPath = path; + + schemaPath = schema.Name; + schemaName = schema.Properties.Label.WithFallback(schema.Name); + schemaKey = schema.Name.ToPascalCase(); + + dataSchema = schemaResolver($"{schemaKey}Dto", schema.BuildJsonSchema(partitionResolver, schemaResolver)); + + contentSchema = schemaResolver($"{schemaKey}ContentDto", schemaBuilder.CreateContentSchema(schema, dataSchema)); + } + + public void GenerateSchemaOperations() + { + document.Tags.Add( + new SwaggerTag + { + Name = schemaName, Description = $"API to managed {schemaName} contents." + }); + + var schemaOperations = new List + { + GenerateSchemaQueryOperation(), + GenerateSchemaCreateOperation(), + GenerateSchemaGetOperation(), + GenerateSchemaUpdateOperation(), + GenerateSchemaPatchOperation(), + GenerateSchemaPublishOperation(), + GenerateSchemaUnpublishOperation(), + GenerateSchemaDeleteOperation() + }; + + foreach (var operation in schemaOperations.SelectMany(x => x.Values).Distinct()) + { + operation.Tags = new List { schemaName }; + } + } + + private SwaggerOperations GenerateSchemaQueryOperation() + { + return AddOperation(SwaggerOperationMethod.Get, null, $"{appPath}/{schemaPath}", operation => + { + operation.OperationId = $"Query{schemaKey}Contents"; + operation.Summary = $"Queries {schemaName} contents."; + + operation.Description = schemaQueryDescription; + + operation.AddQueryParameter("$top", JsonObjectType.Number, "Optional number of contents to take."); + operation.AddQueryParameter("$skip", JsonObjectType.Number, "Optional number of contents to skip."); + operation.AddQueryParameter("$filter", JsonObjectType.String, "Optional OData filter."); + operation.AddQueryParameter("$search", JsonObjectType.String, "Optional OData full text search."); + operation.AddQueryParameter("orderby", JsonObjectType.String, "Optional OData order definition."); + + operation.AddResponse("200", $"{schemaName} content retrieved.", CreateContentsSchema(schemaName, contentSchema)); + }); + } + + private SwaggerOperations GenerateSchemaGetOperation() + { + return AddOperation(SwaggerOperationMethod.Get, schemaName, $"{appPath}/{schemaPath}/{{id}}", operation => + { + operation.OperationId = $"Get{schemaKey}Content"; + operation.Summary = $"Get a {schemaName} content."; + + operation.AddResponse("200", $"{schemaName} content found.", contentSchema); + }); + } + + private SwaggerOperations GenerateSchemaCreateOperation() + { + return AddOperation(SwaggerOperationMethod.Post, null, $"{appPath}/{schemaPath}", operation => + { + operation.OperationId = $"Create{schemaKey}Content"; + operation.Summary = $"Create a {schemaName} content."; + + operation.AddBodyParameter("data", dataSchema, schemaBodyDescription); + operation.AddQueryParameter("publish", JsonObjectType.Boolean, "Set to true to autopublish content."); + + operation.AddResponse("201", $"{schemaName} created.", contentSchema); + }); + } + + private SwaggerOperations GenerateSchemaUpdateOperation() + { + return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}", operation => + { + operation.OperationId = $"Update{schemaKey}Content"; + operation.Summary = $"Update a {schemaName} content."; + + operation.AddBodyParameter("data", dataSchema, schemaBodyDescription); + + operation.AddResponse("204", $"{schemaName} element updated."); + }); + } + + private SwaggerOperations GenerateSchemaPatchOperation() + { + return AddOperation(SwaggerOperationMethod.Patch, schemaName, $"{appPath}/{schemaPath}/{{id}}", operation => + { + operation.OperationId = $"Path{schemaKey}Content"; + operation.Summary = $"Patchs a {schemaName} content."; + + operation.AddBodyParameter("data", contentSchema, schemaBodyDescription); + + operation.AddResponse("204", $"{schemaName} element updated."); + }); + } + + private SwaggerOperations GenerateSchemaPublishOperation() + { + return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/publish", operation => + { + operation.OperationId = $"Publish{schemaKey}Content"; + operation.Summary = $"Publish a {schemaName} content."; + + operation.AddResponse("204", $"{schemaName} element published."); + }); + } + + private SwaggerOperations GenerateSchemaUnpublishOperation() + { + return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/unpublish", operation => + { + operation.OperationId = $"Unpublish{schemaKey}Content"; + operation.Summary = $"Unpublish a {schemaName} content."; + + operation.AddResponse("204", $"{schemaName} element unpublished."); + }); + } + + private SwaggerOperations GenerateSchemaDeleteOperation() + { + return AddOperation(SwaggerOperationMethod.Delete, schemaName, $"{appPath}/{schemaPath}/{{id}}/", operation => + { + operation.OperationId = $"Delete{schemaKey}Content"; + operation.Summary = $"Delete a {schemaName} content."; + + operation.AddResponse("204", $"{schemaName} content deleted."); + }); + } + + private SwaggerOperations AddOperation(SwaggerOperationMethod method, string entityName, string path, Action updater) + { + var operations = document.Paths.GetOrAdd(path, k => new SwaggerOperations()); + var operation = new SwaggerOperation(); + + updater(operation); + + operations[method] = operation; + + if (entityName != null) + { + operation.AddPathParameter("id", JsonObjectType.String, $"The id of the {entityName} content (GUID)."); + + operation.AddResponse("404", $"App, schema or {entityName} content not found."); + } + + return operations; + } + + private static JsonSchema4 CreateContentsSchema(string schemaName, JsonSchema4 contentSchema) + { + var schema = new JsonSchema4 + { + Properties = + { + ["total"] = new JsonProperty + { + Type = JsonObjectType.Number, IsRequired = true, Description = $"The total number of {schemaName} contents." + }, + ["items"] = new JsonProperty + { + Type = JsonObjectType.Array, IsRequired = true, Item = contentSchema, Description = $"The {schemaName} contents." + } + }, + Type = JsonObjectType.Object + }; + + return schema; + } + } +} \ No newline at end of file diff --git a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs index b35af1fb6..7127d2376 100644 --- a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs +++ b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs @@ -6,21 +6,17 @@ // All rights reserved. // ========================================================================== -using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using NJsonSchema; -using NJsonSchema.Generation; using NSwag; using NSwag.AspNetCore; using NSwag.SwaggerGeneration; using Squidex.Config; -using Squidex.Controllers.Api; using Squidex.Core.Identity; -using Squidex.Core.Schemas; using Squidex.Infrastructure; using Squidex.Pipeline.Swagger; using Squidex.Read.Apps; @@ -34,108 +30,36 @@ namespace Squidex.Controllers.ContentApi.Generator { public sealed class SchemasSwaggerGenerator { - private readonly SwaggerJsonSchemaGenerator schemaGenerator; - private readonly SwaggerDocument document = new SwaggerDocument { Tags = new List() }; - private readonly SwaggerSettings swaggerSettings; private readonly HttpContext context; - private readonly JsonSchemaResolver schemaResolver; - private readonly SwaggerGenerator swaggerGenerator; + private readonly SwaggerSettings settings; private readonly MyUrlsOptions urlOptions; - private readonly string schemaQueryDescription; - private readonly string schemaBodyDescription; - private JsonSchema4 errorDtoSchema; - private string appBasePath; - private IAppEntity app; + private SwaggerJsonSchemaGenerator schemaGenerator; + private JsonSchemaResolver schemaResolver; + private SwaggerGenerator swaggerGenerator; + private SwaggerDocument document; public SchemasSwaggerGenerator(IHttpContextAccessor context, SwaggerSettings settings, IOptions urlOptions) { this.context = context.HttpContext; - + this.settings = settings; this.urlOptions = urlOptions.Value; + } + + public async Task Generate(IAppEntity app, IEnumerable schemas) + { + document = SwaggerHelper.CreateApiDocument(context, urlOptions, app.Name); schemaGenerator = new SwaggerJsonSchemaGenerator(settings); schemaResolver = new SwaggerSchemaResolver(document, settings); - swaggerSettings = settings; swaggerGenerator = new SwaggerGenerator(schemaGenerator, settings, schemaResolver); - schemaBodyDescription = SwaggerHelper.LoadDocs("schemabody"); - schemaQueryDescription = SwaggerHelper.LoadDocs("schemaquery"); - } - - public async Task Generate(IAppEntity targetApp, IEnumerable schemas) - { - app = targetApp; - - await GenerateBasicSchemas(); - - GenerateBasePath(); - GenerateTitle(); - GenerateRequestInfo(); - GenerateContentTypes(); - GenerateSchemes(); - GenerateSchemasOperations(schemas); - GenerateSecurityDefinitions(); + GenerateSchemasOperations(schemas, app); GenerateSecurityRequirements(); - GenerateDefaultErrors(); - GeneratePing(); - return document; - } - - private void GenerateBasePath() - { - appBasePath = $"/content/{app.Name}"; - } - - private void GenerateSchemes() - { - document.Schemes.Add(context.Request.Scheme == "http" ? SwaggerSchema.Http : SwaggerSchema.Https); - } - - private void GenerateTitle() - { - document.Host = context.Request.Host.Value ?? string.Empty; - document.BasePath = "/api"; - } - - private void GenerateRequestInfo() - { - document.Info = new SwaggerInfo - { - ExtensionData = new Dictionary - { - ["x-logo"] = new { url = urlOptions.BuildUrl("images/logo-white.png", false), backgroundColor = "#3f83df" } - }, - Title = $"Suidex API for {app.Name} App" - }; - } - - private void GenerateContentTypes() - { - document.Consumes = new List - { - "application/json" - }; - - document.Produces = new List - { - "application/json" - }; - } - - private void GenerateSecurityDefinitions() - { - document.SecurityDefinitions.Add("OAuth2", SwaggerHelper.CreateOAuthSchema(urlOptions)); - } + await GenerateDefaultErrorsAsync(); - private async Task GenerateBasicSchemas() - { - var errorType = typeof(ErrorDto); - var errorContract = swaggerSettings.ActualContractResolver.ResolveContract(errorType); - var errorSchema = JsonObjectTypeDescription.FromType(errorType, errorContract, new Attribute[0], swaggerSettings.DefaultEnumHandling); - - errorDtoSchema = await swaggerGenerator.GenerateAndAppendSchemaFromTypeAsync(errorType, errorSchema.IsNullable, null); + return document; } private void GenerateSecurityRequirements() @@ -154,258 +78,26 @@ namespace Squidex.Controllers.ContentApi.Generator } } - private void GenerateDefaultErrors() + private void GenerateSchemasOperations(IEnumerable schemas, IAppEntity app) { - foreach (var operation in document.Paths.Values.SelectMany(x => x.Values)) - { - operation.Responses.Add("500", new SwaggerResponse { Description = "Operation failed with internal server error.", Schema = errorDtoSchema }); - } - } + var appBasePath = $"/content/{app.Name}"; - private void GenerateSchemasOperations(IEnumerable schemas) - { foreach (var schema in schemas.Where(x => x.IsPublished).Select(x => x.Schema)) { - GenerateSchemaOperations(schema); - } - } - - private void GenerateSchemaOperations(Schema schema) - { - var schemaIdentifier = schema.Name.ToPascalCase(); - var schemaName = !string.IsNullOrWhiteSpace(schema.Properties.Label) ? schema.Properties.Label.Trim() : schema.Name; - - document.Tags.Add( - new SwaggerTag - { - Name = schemaName, - Description = $"API to managed {schemaName} contents." - }); - - var dataSchema = AppendSchema($"{schemaIdentifier}Dto", schema.BuildJsonSchema(app.PartitionResolver, AppendSchema)); - - var schemaOperations = new List - { - GenerateSchemaQueryOperation(schema, schemaName, schemaIdentifier, dataSchema), - GenerateSchemaCreateOperation(schema, schemaName, schemaIdentifier, dataSchema), - GenerateSchemaGetOperation(schema, schemaName, schemaIdentifier, dataSchema), - GenerateSchemaUpdateOperation(schema, schemaName, schemaIdentifier, dataSchema), - GenerateSchemaPatchOperation(schema, schemaName, schemaIdentifier, dataSchema), - GenerateSchemaPublishOperation(schema, schemaName, schemaIdentifier), - GenerateSchemaUnpublishOperation(schema, schemaName, schemaIdentifier), - GenerateSchemaDeleteOperation(schema, schemaName, schemaIdentifier) - }; - - foreach (var operation in schemaOperations.SelectMany(x => x.Values).Distinct()) - { - operation.Tags = new List { schemaName }; - } - } - - private void GeneratePing() - { - var swaggerOperation = AddOperation(SwaggerOperationMethod.Get, null, $"ping/{app.Name}", operation => - { - operation.OperationId = "MakePingTest"; - - operation.Description = "Make a simple request, e.g. to test credentials."; - - operation.Summary = "Make Test"; - - }); - - foreach (var operation in swaggerOperation.Values) - { - operation.Tags = new List { "PingTest" }; + new SchemaSwaggerGenerator(document, appBasePath, schema, AppendSchema, app.PartitionResolver).GenerateSchemaOperations(); } } - private SwaggerOperations GenerateSchemaQueryOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) - { - return AddOperation(SwaggerOperationMethod.Get, null, $"{appBasePath}/{schema.Name}", operation => - { - operation.OperationId = $"Query{schemaIdentifier}Contents"; - - operation.Summary = $"Queries {schemaName} contents."; - - operation.Description = schemaQueryDescription; - - operation.AddQueryParameter("$top", JsonObjectType.Number, "Optional number of contents to take."); - operation.AddQueryParameter("$skip", JsonObjectType.Number, "Optional number of contents to skip."); - operation.AddQueryParameter("$filter", JsonObjectType.String, "Optional OData filter."); - operation.AddQueryParameter("$search", JsonObjectType.String, "Optional OData full text search."); - operation.AddQueryParameter("orderby", JsonObjectType.String, "Optional OData order definition."); - - var responseSchema = CreateContentsSchema(schemaName, schema.Name, dataSchema); - - operation.AddResponse("200", $"{schemaName} content retrieved.", responseSchema); - }); - } - - private SwaggerOperations GenerateSchemaGetOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) + private async Task GenerateDefaultErrorsAsync() { - return AddOperation(SwaggerOperationMethod.Get, schemaName, $"{appBasePath}/{schema.Name}/{{id}}", operation => - { - operation.OperationId = $"Get{schemaIdentifier}Content"; - - operation.Summary = $"Get a {schemaName} content."; + const string errorDescription = "Operation failed with internal server error."; - var responseSchema = CreateContentSchema(schemaName, schemaIdentifier, dataSchema); - - operation.AddResponse("200", $"{schemaName} content found.", responseSchema); - }); - } - - private SwaggerOperations GenerateSchemaCreateOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) - { - return AddOperation(SwaggerOperationMethod.Post, null, $"{appBasePath}/{schema.Name}", operation => - { - operation.OperationId = $"Create{schemaIdentifier}Content"; + var errorDtoSchema = await swaggerGenerator.GetErrorDtoSchemaAsync(); - operation.Summary = $"Create a {schemaName} content."; - - var responseSchema = CreateContentSchema(schemaName, schemaIdentifier, dataSchema); - - operation.AddBodyParameter(dataSchema, "data", schemaBodyDescription); - operation.AddQueryParameter("publish", JsonObjectType.Boolean, "Set to true to autopublish content."); - operation.AddResponse("201", $"{schemaName} created.", responseSchema); - }); - } - - private SwaggerOperations GenerateSchemaUpdateOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) - { - return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appBasePath}/{schema.Name}/{{id}}", operation => - { - operation.OperationId = $"Update{schemaIdentifier}Content"; - - operation.Summary = $"Update a {schemaName} content."; - - operation.AddBodyParameter(dataSchema, "data", schemaBodyDescription); - operation.AddResponse("204", $"{schemaName} element updated."); - }); - } - - private SwaggerOperations GenerateSchemaPatchOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) - { - return AddOperation(SwaggerOperationMethod.Patch, schemaName, $"{appBasePath}/{schema.Name}/{{id}}", operation => - { - operation.OperationId = $"Path{schemaIdentifier}Content"; - - operation.Summary = $"Patchs a {schemaName} content."; - - operation.AddBodyParameter(dataSchema, "data", schemaBodyDescription); - operation.AddResponse("204", $"{schemaName} element updated."); - }); - } - - private SwaggerOperations GenerateSchemaPublishOperation(Schema schema, string schemaName, string schemaIdentifier) - { - return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appBasePath}/{schema.Name}/{{id}}/publish", operation => - { - operation.OperationId = $"Publish{schemaIdentifier}Content"; - - operation.Summary = $"Publish a {schemaName} content."; - - operation.AddResponse("204", $"{schemaName} element published."); - }); - } - - private SwaggerOperations GenerateSchemaUnpublishOperation(Schema schema, string schemaName, string schemaIdentifier) - { - return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appBasePath}/{schema.Name}/{{id}}/unpublish", operation => - { - operation.OperationId = $"Unpublish{schemaIdentifier}Content"; - - operation.Summary = $"Unpublish a {schemaName} content."; - - operation.AddResponse("204", $"{schemaName} element unpublished."); - }); - } - - private SwaggerOperations GenerateSchemaDeleteOperation(Schema schema, string schemaName, string schemaIdentifier) - { - return AddOperation(SwaggerOperationMethod.Delete, schemaName, $"{appBasePath}/{schema.Name}/{{id}}/", operation => - { - operation.OperationId = $"Delete{schemaIdentifier}Content"; - - operation.Summary = $"Delete a {schemaName} content."; - - operation.AddResponse("204", $"{schemaName} content deleted."); - }); - } - - private SwaggerOperations AddOperation(SwaggerOperationMethod method, string entityName, string path, Action updater) - { - var operations = document.Paths.GetOrAdd(path, k => new SwaggerOperations()); - var operation = new SwaggerOperation(); - - updater(operation); - - operations[method] = operation; - - if (entityName != null) + foreach (var operation in document.Paths.Values.SelectMany(x => x.Values)) { - operation.AddPathParameter("id", JsonObjectType.String, $"The id of the {entityName} content (GUID)."); - - operation.AddResponse("404", $"App, schema or {entityName} content not found."); + operation.Responses.Add("500", new SwaggerResponse { Description = errorDescription, Schema = errorDtoSchema }); } - - return operations; - } - - private JsonSchema4 CreateContentsSchema(string schemaName, string id, JsonSchema4 dataSchema) - { - var contentSchema = CreateContentSchema(schemaName, id, dataSchema); - - var schema = new JsonSchema4 - { - Properties = - { - ["total"] = new JsonProperty - { - Type = JsonObjectType.Number, IsRequired = true, Description = $"The total number of {schemaName} contents." - }, - ["items"] = new JsonProperty - { - Type = JsonObjectType.Array, IsRequired = true, Item = contentSchema, Description = $"The {schemaName} contents." - } - }, - Type = JsonObjectType.Object - }; - - return schema; - } - - private JsonSchema4 CreateContentSchema(string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) - { - var dataProperty = new JsonProperty { Description = schemaBodyDescription, Type = JsonObjectType.Object, IsRequired = true, SchemaReference = dataSchema }; - - var schema = new JsonSchema4 - { - Properties = - { - ["id"] = CreateProperty($"The id of the {schemaName} content."), - ["data"] = dataProperty, - ["version"] = CreateProperty($"The version of the {schemaName}", JsonObjectType.Number), - ["created"] = CreateProperty($"The date and time when the {schemaName} content has been created.", "date-time"), - ["createdBy"] = CreateProperty($"The user that has created the {schemaName} content."), - ["lastModified"] = CreateProperty($"The date and time when the {schemaName} content has been modified last.", "date-time"), - ["lastModifiedBy"] = CreateProperty($"The user that has updated the {schemaName} content last.") - }, - Type = JsonObjectType.Object - }; - - return AppendSchema($"{schemaIdentifier}ContentDto", schema); - } - - private static JsonProperty CreateProperty(string description, JsonObjectType type) - { - return new JsonProperty { Description = description, IsRequired = true, Type = type }; - } - - private static JsonProperty CreateProperty(string description, string format = null) - { - return new JsonProperty { Description = description, Format = format, IsRequired = true, Type = JsonObjectType.String }; } private JsonSchema4 AppendSchema(string name, JsonSchema4 schema) diff --git a/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs b/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs index 5ab298c48..bae8e6066 100644 --- a/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs +++ b/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs @@ -6,34 +6,70 @@ // All rights reserved. // ========================================================================== -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using NJsonSchema; using NSwag; using Squidex.Config; using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using NSwag.SwaggerGeneration; +using Squidex.Controllers.Api; using Squidex.Core.Identity; namespace Squidex.Pipeline.Swagger { public static class SwaggerHelper { - private static readonly ConcurrentDictionary Docs = new ConcurrentDictionary(); - public static string LoadDocs(string name) { - return Docs.GetOrAdd(name, x => + var assembly = typeof(SwaggerHelper).GetTypeInfo().Assembly; + + using (var resourceStream = assembly.GetManifestResourceStream($"Squidex.Docs.{name}.md")) { - var assembly = typeof(SwaggerHelper).GetTypeInfo().Assembly; + var streamReader = new StreamReader(resourceStream); + + return streamReader.ReadToEnd(); + } + } - using (var resourceStream = assembly.GetManifestResourceStream($"Squidex.Docs.{name}.md")) + public static SwaggerDocument CreateApiDocument(HttpContext context, MyUrlsOptions urlOptions, string appName) + { + var document = new SwaggerDocument + { + Tags = new List(), + Schemes = new List + { + context.Request.Scheme == "http" ? SwaggerSchema.Http : SwaggerSchema.Https + }, + Consumes = new List + { + "application/json" + }, + Produces = new List + { + "application/json" + }, + Info = new SwaggerInfo { - var streamReader = new StreamReader(resourceStream); + ExtensionData = new Dictionary + { + ["x-logo"] = new { url = urlOptions.BuildUrl("images/logo-white.png", false), backgroundColor = "#3f83df" } + }, + Title = $"Suidex API for {appName} App" + }, + BasePath = "/api" + }; + + if (!string.IsNullOrWhiteSpace(context.Request.Host.Value)) + { + document.Host = context.Request.Host.Value; + } - return streamReader.ReadToEnd(); - } - }); + document.SecurityDefinitions.Add("OAuth2", CreateOAuthSchema(urlOptions)); + + return document; } public static SwaggerSecurityScheme CreateOAuthSchema(MyUrlsOptions urlOptions) @@ -62,6 +98,13 @@ namespace Squidex.Pipeline.Swagger return result; } + public static async Task GetErrorDtoSchemaAsync(this SwaggerGenerator swaggerGenerator) + { + var errorType = typeof(ErrorDto); + + return await swaggerGenerator.GenerateAndAppendSchemaFromTypeAsync(errorType, false, null); + } + public static void AddQueryParameter(this SwaggerOperation operation, string name, JsonObjectType type, string description = null) { var parameter = new SwaggerParameter { Type = type, Name = name, Kind = SwaggerParameterKind.Query }; @@ -89,7 +132,7 @@ namespace Squidex.Pipeline.Swagger operation.Parameters.Add(parameter); } - public static void AddBodyParameter(this SwaggerOperation operation, JsonSchema4 schema, string name, string description) + public static void AddBodyParameter(this SwaggerOperation operation, string name, JsonSchema4 schema, string description) { var parameter = new SwaggerParameter { Schema = schema, Name = name, Kind = SwaggerParameterKind.Body }; diff --git a/tests/Benchmarks/Tests/HandleEvents.cs b/tests/Benchmarks/Tests/HandleEvents.cs index 7f27eaa73..6c5289571 100644 --- a/tests/Benchmarks/Tests/HandleEvents.cs +++ b/tests/Benchmarks/Tests/HandleEvents.cs @@ -9,7 +9,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using MongoDB.Bson; using MongoDB.Driver; using Newtonsoft.Json; using Squidex.Infrastructure;