Browse Source

Open API improvements (#667)

* Open API improvements
pull/668/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
ffa2ab140c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 64
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs
  2. 53
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs
  3. 42
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/SchemaBuilder.cs
  4. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  5. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Schemas/ContentJsonSchemaBuilder.cs
  6. 51
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/Builder.cs
  7. 2
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationsBuilder.cs
  8. 22
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs
  9. 1
      backend/src/Squidex/Config/Domain/CommandsServices.cs
  10. 46
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs
  11. 1
      backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs

64
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs

@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
{
public static class JsonSchemaExtensions
{
public static JsonSchema BuildFlatJsonSchema(this Schema schema, SchemaResolver schemaResolver, bool withHidden = false)
public static JsonSchema BuildFlatJsonSchema(this Schema schema, SchemaResolver schemaResolver)
{
Guard.NotNull(schemaResolver, nameof(schemaResolver));
@ -22,9 +22,9 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
var jsonSchema = SchemaBuilder.Object();
foreach (var field in schema.Fields.ForApi(withHidden))
foreach (var field in schema.Fields.ForApi())
{
var property = JsonTypeVisitor.BuildProperty(field, schemaResolver, withHidden);
var property = JsonTypeVisitor.BuildProperty(field, null, false);
if (property != null)
{
@ -37,42 +37,62 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
return jsonSchema;
}
public static JsonSchema BuildJsonSchema(this Schema schema, PartitionResolver partitionResolver, SchemaResolver schemaResolver, bool withHidden = false)
public static JsonSchema BuildDynamicJsonSchema(this Schema schema, SchemaResolver schemaResolver, bool withHidden = false)
{
Guard.NotNull(schemaResolver, nameof(schemaResolver));
Guard.NotNull(partitionResolver, nameof(partitionResolver));
var schemaName = schema.TypeName();
var jsonSchema = SchemaBuilder.Object();
foreach (var field in schema.Fields.ForApi(withHidden))
{
var propertyItem = JsonTypeVisitor.BuildProperty(field, null, withHidden);
if (propertyItem != null)
{
var property =
SchemaBuilder.ObjectProperty(propertyItem)
.SetDescription(field)
.SetRequired(field.RawProperties.IsRequired);
jsonSchema.Properties.Add(field.Name, property);
}
}
return jsonSchema;
}
public static JsonSchema BuildJsonSchema(this Schema schema, PartitionResolver partitionResolver, bool withHidden = false)
{
Guard.NotNull(partitionResolver, nameof(partitionResolver));
var jsonSchema = SchemaBuilder.Object();
foreach (var field in schema.Fields.ForApi(withHidden))
{
var partitionObject = SchemaBuilder.Object();
var propertyObject = SchemaBuilder.Object();
var partitioning = partitionResolver(field.Partitioning);
foreach (var partitionKey in partitioning.AllKeys)
{
var partitionItemProperty = JsonTypeVisitor.BuildProperty(field, schemaResolver, withHidden);
var propertyItem = JsonTypeVisitor.BuildProperty(field, null, withHidden);
if (partitionItemProperty != null)
if (propertyItem != null)
{
var isOptional = partitioning.IsOptional(partitionKey);
var name = partitioning.GetName(partitionKey);
partitionItemProperty.Description = name;
partitionItemProperty.SetRequired(field.RawProperties.IsRequired && !isOptional);
propertyItem.SetDescription(name);
propertyItem.SetRequired(field.RawProperties.IsRequired && !isOptional);
partitionObject.Properties.Add(partitionKey, partitionItemProperty);
propertyObject.Properties.Add(partitionKey, propertyItem);
}
}
if (partitionObject.Properties.Count > 0)
if (propertyObject.Properties.Count > 0)
{
var propertyReference = schemaResolver($"{schemaName}{field.Name.ToPascalCase()}PropertyDto", () => partitionObject);
jsonSchema.Properties.Add(field.Name, CreateProperty(field, propertyReference));
jsonSchema.Properties.Add(field.Name, CreateProperty(field, propertyObject));
}
}
@ -81,8 +101,16 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
public static JsonSchemaProperty CreateProperty(IField field, JsonSchema reference)
{
var jsonProperty = SchemaBuilder.ObjectProperty(reference);
var jsonProperty =
SchemaBuilder.ReferenceProperty(reference)
.SetDescription(field)
.SetRequired(field.RawProperties.IsRequired);
return jsonProperty;
}
private static JsonSchemaProperty SetDescription(this JsonSchemaProperty jsonProperty, IField field)
{
if (!string.IsNullOrWhiteSpace(field.RawProperties.Hints))
{
jsonProperty.Description = $"{field.Name} ({field.RawProperties.Hints})";
@ -92,8 +120,6 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
jsonProperty.Description = field.Name;
}
jsonProperty.SetRequired(field.RawProperties.IsRequired);
return jsonProperty;
}
}

53
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs

@ -21,11 +21,11 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
public readonly struct Args
{
public readonly SchemaResolver SchemaResolver;
public readonly SchemaResolver? SchemaResolver;
public readonly bool WithHiddenFields;
public Args(SchemaResolver schemaResolver, bool withHiddenFields)
public Args(SchemaResolver? schemaResolver, bool withHiddenFields)
{
SchemaResolver = schemaResolver;
@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
{
}
public static JsonSchemaProperty? BuildProperty(IField field, SchemaResolver schemaResolver, bool withHiddenFields)
public static JsonSchemaProperty? BuildProperty(IField field, SchemaResolver? schemaResolver, bool withHiddenFields)
{
var args = new Args(schemaResolver, withHiddenFields);
@ -81,30 +81,41 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
public JsonSchemaProperty? Visit(IField<GeolocationFieldProperties> field, Args args)
{
var reference = args.SchemaResolver("GeolocationDto", () =>
if (args.SchemaResolver != null)
{
var geolocationSchema = SchemaBuilder.Object();
var reference = args.SchemaResolver("GeolocationDto", () =>
{
var geolocationSchema = SchemaBuilder.Object();
geolocationSchema.Format = GeoJson.Format;
geolocationSchema.Format = GeoJson.Format;
geolocationSchema.Properties.Add("latitude", new JsonSchemaProperty
{
Type = JsonObjectType.Number,
Maximum = 90,
Minimum = -90
}.SetRequired(true));
geolocationSchema.Properties.Add("latitude", new JsonSchemaProperty
{
Type = JsonObjectType.Number,
Maximum = 90,
Minimum = -90
}.SetRequired(false));
geolocationSchema.Properties.Add("longitude", new JsonSchemaProperty
{
Type = JsonObjectType.Number,
Maximum = 180,
Minimum = -180
}.SetRequired(true));
geolocationSchema.Properties.Add("longitude", new JsonSchemaProperty
{
Type = JsonObjectType.Number,
Maximum = 180,
Minimum = -180
}.SetRequired(false));
return geolocationSchema;
});
return geolocationSchema;
});
return SchemaBuilder.ObjectProperty(reference);
}
else
{
var property = SchemaBuilder.ObjectProperty();
return SchemaBuilder.ObjectProperty(reference);
property.Format = GeoJson.Format;
return property;
}
}
public JsonSchemaProperty? Visit(IField<JsonFieldProperties> field, Args args)

42
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/SchemaBuilder.cs

@ -23,48 +23,68 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
public static JsonSchemaProperty ArrayProperty(JsonSchema item, string? description = null, bool isRequired = false)
{
return Enrich(new JsonSchemaProperty { Type = JsonObjectType.Array, Item = item }, description, isRequired);
return new JsonSchemaProperty { Type = JsonObjectType.Array, Item = item }
.SetDescription(description)
.SetRequired(isRequired);
}
public static JsonSchemaProperty BooleanProperty(string? description = null, bool isRequired = false)
{
return Enrich(new JsonSchemaProperty { Type = JsonObjectType.Boolean }, description, isRequired);
return new JsonSchemaProperty { Type = JsonObjectType.Boolean }
.SetDescription(description)
.SetRequired(isRequired);
}
public static JsonSchemaProperty DateTimeProperty(string? description = null, bool isRequired = false)
{
return Enrich(new JsonSchemaProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime }, description, isRequired);
return new JsonSchemaProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime }
.SetDescription(description)
.SetRequired(isRequired);
}
public static JsonSchemaProperty NumberProperty(string? description = null, bool isRequired = false)
{
return Enrich(new JsonSchemaProperty { Type = JsonObjectType.Number }, description, isRequired);
return new JsonSchemaProperty { Type = JsonObjectType.Number }
.SetDescription(description)
.SetRequired(isRequired);
}
public static JsonSchemaProperty StringProperty(string? description = null, bool isRequired = false)
{
return Enrich(new JsonSchemaProperty { Type = JsonObjectType.String }, description, isRequired);
return new JsonSchemaProperty { Type = JsonObjectType.String }
.SetDescription(description)
.SetRequired(isRequired);
}
public static JsonSchemaProperty ObjectProperty(JsonSchema reference, string? description = null, bool isRequired = false)
public static JsonSchemaProperty ReferenceProperty(JsonSchema reference, string? description = null, bool isRequired = false)
{
return Enrich(new JsonSchemaProperty { Reference = reference }, description, isRequired);
return new JsonSchemaProperty { Reference = reference }
.SetDescription(description)
.SetRequired(isRequired);
}
public static JsonSchemaProperty ObjectProperty(JsonSchema? value = null, string? description = null, bool isRequired = false)
{
return new JsonSchemaProperty { Type = JsonObjectType.Object, AdditionalPropertiesSchema = value }
.SetDescription(description)
.SetRequired(isRequired);
}
public static JsonSchemaProperty JsonProperty(string? description = null, bool isRequired = false)
{
return Enrich(new JsonSchemaProperty(), description, isRequired);
return new JsonSchemaProperty()
.SetDescription(description)
.SetRequired(isRequired);
}
private static JsonSchemaProperty Enrich(JsonSchemaProperty property, string? description = null, bool isRequired = false)
public static T SetDescription<T>(this T property, string? description = null) where T : JsonSchemaProperty
{
property.Description = description;
property.SetRequired(isRequired);
return property;
}
public static JsonSchemaProperty SetRequired(this JsonSchemaProperty property, bool isRequired)
public static T SetRequired<T>(this T property, bool isRequired) where T : JsonSchemaProperty
{
property.IsRequired = isRequired;
property.IsNullableRaw = !isRequired;

2
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs

@ -235,7 +235,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
private static JsonSchema BuildJsonSchema(Schema schema, IAppEntity app, bool withHiddenFields)
{
var dataSchema = schema.BuildJsonSchema(app.PartitionResolver(), (n, action) => action(), withHiddenFields);
var dataSchema = schema.BuildJsonSchema(app.PartitionResolver(), withHiddenFields);
return ContentJsonSchemaBuilder.BuildSchema(schema.DisplayName(), dataSchema);
}

4
backend/src/Squidex.Domain.Apps.Entities/Contents/Schemas/ContentJsonSchemaBuilder.cs

@ -38,8 +38,8 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
if (dataSchema != null)
{
jsonSchema.Properties["data"] = SchemaBuilder.ObjectProperty(dataSchema, $"The data of the {name}.", true);
jsonSchema.Properties["dataDraft"] = SchemaBuilder.ObjectProperty(dataSchema, $"The draft data of the {name}.");
jsonSchema.Properties["data"] = SchemaBuilder.ReferenceProperty(dataSchema, $"The data of the {name}.", true);
jsonSchema.Properties["dataDraft"] = SchemaBuilder.ReferenceProperty(dataSchema, $"The draft data of the {name}.");
}
return jsonSchema;

51
backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/Builder.cs

@ -11,7 +11,6 @@ using NJsonSchema;
using NSwag;
using NSwag.Generation;
using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.GenerateJsonSchema;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps;
@ -21,7 +20,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
internal sealed class Builder
{
private readonly PartitionResolver partitionResolver;
private const string ResultTotal = "total";
private const string ResultItems = "items";
public string AppName { get; }
@ -34,8 +34,6 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
OpenApiSchemaResolver schemaResolver,
OpenApiSchemaGenerator schemaGenerator)
{
this.partitionResolver = app.PartitionResolver();
Document = document;
AppName = app.Name;
@ -50,16 +48,22 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
return JsonSchema.CreateAnySchema();
});
var contentSchema = ResolveSchema($"ContentDto", () =>
var contentSchema = ResolveSchema("ContentDto", () =>
{
return ContentJsonSchemaBuilder.BuildSchema("Shared", dataSchema, true);
});
var contentsSchema = ResolveSchema("ContentResultDto", () =>
{
return BuildResult(contentSchema);
});
var path = $"/content/{AppName}";
var builder = new OperationsBuilder
{
ContentSchema = contentSchema,
ContentsSchema = contentsSchema,
DataSchema = dataSchema,
Path = path,
Parent = this,
@ -83,19 +87,27 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
var dataSchema = ResolveSchema($"{typeName}DataDto", () =>
{
return schema.BuildJsonSchema(partitionResolver, ResolveSchema);
return schema.BuildDynamicJsonSchema(ResolveSchema);
});
var dataFlatSchema = ResolveSchema($"{typeName}FlatDataDto", () =>
var contentSchema = ResolveSchema($"{typeName}ContentDto", () =>
{
return schema.BuildFlatJsonSchema(ResolveSchema);
var contentDataSchema = dataSchema;
if (flat)
{
contentDataSchema = ResolveSchema($"{typeName}FlatDataDto", () =>
{
return schema.BuildFlatJsonSchema(ResolveSchema);
});
}
return ContentJsonSchemaBuilder.BuildSchema(displayName, contentDataSchema, true);
});
var contentSchema = ResolveSchema($"{typeName}ContentDto", () =>
var contentsSchema = ResolveSchema($"{typeName}ContentResultDto", () =>
{
var data = flat ? dataFlatSchema : dataSchema;
return ContentJsonSchemaBuilder.BuildSchema(displayName, data, true);
return BuildResult(contentSchema);
});
var path = $"/content/{AppName}/{schema.Name}";
@ -103,6 +115,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
var builder = new OperationsBuilder
{
ContentSchema = contentSchema,
ContentsSchema = contentsSchema,
DataSchema = dataSchema,
Path = path,
Parent = this,
@ -127,5 +140,19 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
Reference = Document.Definitions.GetOrAdd(name, x => factory())
};
}
private static JsonSchema BuildResult(JsonSchema contentSchema)
{
return new JsonSchema
{
AllowAdditionalProperties = false,
Properties =
{
[ResultTotal] = SchemaBuilder.NumberProperty("The total number of content items.", true),
[ResultItems] = SchemaBuilder.ArrayProperty(contentSchema, "The content items.", true)
},
Type = JsonObjectType.Object
};
}
}
}

2
backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationsBuilder.cs

@ -27,6 +27,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
public JsonSchema ContentSchema { get; init; }
public JsonSchema ContentsSchema { get; init; }
public JsonSchema DataSchema { get; init; }
public string FormatText(string text)

22
backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs

@ -13,7 +13,6 @@ using NJsonSchema;
using NSwag;
using NSwag.Generation;
using NSwag.Generation.Processors.Contexts;
using Squidex.Domain.Apps.Core.GenerateJsonSchema;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Hosting;
@ -82,28 +81,24 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
private static void GenerateSharedOperations(OperationsBuilder builder)
{
var contentsSchema = BuildResults(builder);
builder.AddOperation(OpenApiOperationMethod.Get, "/")
.RequirePermission(Permissions.AppContentsReadOwn)
.Operation("Query")
.OperationSummary("Query contents across all schemas.")
.HasQuery("ids", JsonObjectType.String, "Comma-separated list of content IDs.")
.Responds(200, "Content items retrieved.", contentsSchema)
.Responds(200, "Content items retrieved.", builder.ContentsSchema)
.Responds(400, "Query not valid.");
}
private static void GenerateSchemaOperations(OperationsBuilder builder)
{
var contentsSchema = BuildResults(builder);
builder.AddOperation(OpenApiOperationMethod.Get, "/")
.RequirePermission(Permissions.AppContentsReadOwn)
.Operation("Query")
.OperationSummary("Query schema contents items.")
.Describe(Properties.Resources.OpenApiSchemaQuery)
.HasQueryOptions(true)
.Responds(200, "Content items retrieved.", contentsSchema)
.Responds(200, "Content items retrieved.", builder.ContentsSchema)
.Responds(400, "Query not valid.");
builder.AddOperation(OpenApiOperationMethod.Get, "/{id}")
@ -192,19 +187,6 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
.Responds(204, "Content item deleted");
}
private static JsonSchema BuildResults(OperationsBuilder builder)
{
return new JsonSchema
{
Properties =
{
["total"] = SchemaBuilder.NumberProperty("The total number of content items.", true),
["items"] = SchemaBuilder.ArrayProperty(builder.ContentSchema, "The content items.", true)
},
Type = JsonObjectType.Object
};
}
private OpenApiDocument CreateApiDocument(HttpContext context, IAppEntity app)
{
var appName = app.Name;

1
backend/src/Squidex/Config/Domain/CommandsServices.cs

@ -23,7 +23,6 @@ using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Domain.Apps.Entities.Schemas.DomainObject;
using Squidex.Domain.Apps.Entities.Schemas.Indexes;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Web.CommandMiddlewares;
namespace Squidex.Config.Domain

46
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using NJsonSchema;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.GenerateJsonSchema;
@ -25,38 +26,19 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema
{
var languagesConfig = LanguagesConfig.English.Set(Language.DE);
var schemaResolver = new SchemaResolver((name, action) =>
{
return action();
});
var jsonSchema = schema.BuildJsonSchema(languagesConfig.ToResolver());
var jsonSchema = schema.BuildJsonSchema(languagesConfig.ToResolver(), schemaResolver);
var jsonProperties = AllPropertyNames(jsonSchema);
CheckFields(jsonSchema);
}
void CheckField(IField field)
{
if (!field.IsForApi())
{
Assert.DoesNotContain(field.Name, jsonProperties);
}
else
{
Assert.Contains(field.Name, jsonProperties);
}
[Fact]
public void Should_build_json_schema_with_resolver()
{
var schemaResolver = new SchemaResolver((name, action) => action());
if (field is IArrayField array)
{
foreach (var nested in array.Fields)
{
CheckField(nested);
}
}
}
var jsonSchema = schema.BuildDynamicJsonSchema(schemaResolver);
foreach (var field in schema.Fields)
{
CheckField(field);
}
CheckFields(jsonSchema);
}
[Fact]
@ -70,6 +52,12 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema
});
var jsonSchema = schema.BuildFlatJsonSchema(schemaResolver);
CheckFields(jsonSchema);
}
private void CheckFields(JsonSchema jsonSchema)
{
var jsonProperties = AllPropertyNames(jsonSchema);
void CheckField(IField field)
@ -118,6 +106,8 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema
AddProperties(current.Item);
AddProperties(current.Reference);
AddProperties(current.AdditionalItemsSchema);
AddProperties(current.AdditionalPropertiesSchema);
}
}

1
backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs

@ -7,7 +7,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Infrastructure.EventSourcing;

Loading…
Cancel
Save