diff --git a/changelogs/internal/newsfragments/1787.clarification b/changelogs/internal/newsfragments/1787.clarification new file mode 100644 index 00000000..5d2d1378 --- /dev/null +++ b/changelogs/internal/newsfragments/1787.clarification @@ -0,0 +1 @@ +Solve `allOf` recursively in OpenAPI and JSON Schemas. diff --git a/layouts/partials/json-schema/resolve-additional-types.html b/layouts/partials/json-schema/resolve-additional-types.html index a24eddf4..157045b5 100644 --- a/layouts/partials/json-schema/resolve-additional-types.html +++ b/layouts/partials/json-schema/resolve-additional-types.html @@ -20,10 +20,6 @@ * The returned entries are based on the JSON schema definitions found by * recursing through the input `schema`, with the following differences: * - * * `allOf` references are expanded. (Although this partial requires that - * `resolve-allof` is called on the top-level `schema` beforehand, - * `resolve-allof` doesn't recurse down to subschemas). - * * * If `anchor_base` is set, each object with a `title` and `properties` * is given an `anchor`, which is a string suitable for using as an html * anchor for that object schema. @@ -210,12 +206,7 @@ {{ errorf "Invalid call to partials/get-additional-objects: %s is not a map" $name .this_object }} {{ end }} - /* Although we expect resolve-allof to be called on the input, resolve-allof does not recurse into - * nested schemas, so we have to call it again. - */ - {{ $this_object := partial "json-schema/resolve-allof" .this_object }} - - {{ $res := partial "resolve-additional-types-inner" (dict "schema" $this_object "anchor_base" .anchor_base "name" $name) }} + {{ $res := partial "resolve-additional-types-inner" (dict "schema" .this_object "anchor_base" .anchor_base "name" $name) }} {{ range $res.objects }} {{ $all_objects = $all_objects | append (partial "clean-object" .) }} {{ end }} diff --git a/layouts/partials/json-schema/resolve-allof.html b/layouts/partials/json-schema/resolve-allof.html index db8fc13a..b8b81e7d 100644 --- a/layouts/partials/json-schema/resolve-allof.html +++ b/layouts/partials/json-schema/resolve-allof.html @@ -1,7 +1,7 @@ {{/* - Resolves the `allOf` keyword (https://spec.openapis.org/oas/v3.1.0#composition-and-inheritance-polymorphism), - given a JSON schema object. + Resolves the `allOf` keyword (https://spec.openapis.org/oas/v3.1.0#composition-and-inheritance-polymorphism) + recursively, given a JSON schema object. `allOf` is used to support a kind of inheritance for JSON schema objects. @@ -11,92 +11,71 @@ Of course the parent can itself inherit from *its* parent, so we recurse to handle that. - - Note that `allOf` is only resolved at the top level of the schema object. For - example, if you call this on an API definition which defines a `parameter` - which has an allOf schema, it will not be resolved. To handle this, the - openapi templates call resolve-allof for every schema object that they - process. */}} {{ $ret := . }} {{ $original := . }} -{{/* - We special-case 'required', and accumulate the values from all the 'allOf' - entries (rather than simply overriding them). Start the accumulation here. -*/}} - -{{ $required := .required }} -{{ if not $required }} - {{ $required := slice }} -{{ end }} +{{ if reflect.IsSlice $original }} + {{/* + If it's a slice, just recurse. + */}} + {{ $ret = slice }} -{{ with $ret.allOf }} + {{ range $original }} + {{ $resolved := partial "json-schema/resolve-allof" . }} + {{ $ret = $ret | append $resolved }} + {{ end }} +{{ else if reflect.IsMap $original }} + {{ $ret = dict }} {{/* - construct a new dict, with each of the allOf entries merged into it in - turn. + We special-case 'required', and accumulate the values from all the 'allOf' + entries (rather than simply overriding them). Start the accumulation here. */}} - {{ $all_of_values := dict }} - {{ range . }} - {{ with .required }} - {{ $required = union $required . }} - {{ end }} + {{ $required := slice }} + {{ with $original.required }} + {{ $required = . }} + {{ end }} + {{ with $original.allOf }} {{/* - With merge, values from the second argument override those from the first argument. - So this order will accumulate values from allOf items, allowing later ones to override earlier - - Note also that `merge` does a *deep* merge - nested maps are also - merged. (Slices are replaced though.) + Merge each of the allOf entries. */}} - {{ $all_of_values = merge $all_of_values . }} + {{ range . }} + {{/* + First, resolve allOf in child. + */}} + {{ $resolved := partial "json-schema/resolve-allof" . }} + + {{ with $resolved.required }} + {{ $required = union $required . }} + {{ end }} + + {{/* + With merge, values from the second argument override those from the first argument. + So this order will accumulate values from allOf items, allowing later ones to override earlier + + Note also that `merge` does a *deep* merge - nested maps are also + merged. (Slices are replaced though.) + */}} + {{ $ret = merge $ret $resolved }} + {{ end }} {{ end }} {{/* Finally, merge in the original, allowing the original to override allOf. */}} - {{ $ret = merge $all_of_values $ret }} - - {{/* - Except that if allOf *itself* contains allOf (ie, the parent also - inherits from a grandparent), then we replace allOf in the original - with that in the parent. Below, we see that this has happened, and - recurse. - - TODO: surely it would be better to simply do the recursion as we iterate - though the allOf list above - not least because we might have multiple - parents with different grandparents, and by doing this we only get one - set of grandparents. - */}} - {{ with $all_of_values.allOf }} - {{ $ret = merge $ret (dict "allOf" . ) }} + {{ range $key, $value := $original }} + {{ if and (ne $key "allOf") (ne $key "required") }} + {{ $resolved := partial "json-schema/resolve-allof" $value }} + {{ $ret = merge $ret (dict $key $resolved) }} + {{ end }} {{ end }} - {{/* - special-case 'required': replace it with the union of all the - 'required' arrays from the original and allOf values. - - XXX: but first we merge in the original 'required', again? why - do we do that? it should already have been done at the start. - */}} - {{ with $ret.required }} - {{ $required = union $required $ret.required }} + {{ with $required }} + {{ $ret = merge $ret (dict "required" .) }} {{ end }} - - {{ $ret = merge $ret (dict "required" $required) }} -{{ end }} - -{{/* - If we replaced the 'allOf' dict with one from a grandparent, we now - need to recurse. -*/}} -{{ if ne $ret.allOf $original.allOf }} - - {{ $resolved := partial "json-schema/resolve-allof" $ret }} - {{ $ret = merge $ret $resolved }} - {{ end }} {{ return $ret }} diff --git a/layouts/partials/json-schema/resolve-example.html b/layouts/partials/json-schema/resolve-example.html index ef626afd..09b4254e 100644 --- a/layouts/partials/json-schema/resolve-example.html +++ b/layouts/partials/json-schema/resolve-example.html @@ -9,7 +9,7 @@ */}} -{{ $this_object := partial "json-schema/resolve-allof" . }} +{{ $this_object := . }} {{ $example := $this_object.example }} diff --git a/layouts/partials/openapi/render-object-table.html b/layouts/partials/openapi/render-object-table.html index 7c0be431..030978b1 100644 --- a/layouts/partials/openapi/render-object-table.html +++ b/layouts/partials/openapi/render-object-table.html @@ -38,9 +38,6 @@ {{ range $property_name, $property := $properties }} - - {{ $property := partial "json-schema/resolve-allof" $property }} - {{/* Handle two ways of indicating "required", one for simple parameters, the other for request and response body objects. @@ -67,7 +64,7 @@ Description - {{ $property := partial "json-schema/resolve-allof" . }} + {{ $property := . }} {{ partial "partials/property-type" $property }} @@ -111,9 +108,6 @@ like `[type]`. */}} {{ $items := .items }} - {{ if .items }} - {{ $items = partial "json-schema/resolve-allof" .items }} - {{ end }} {{ $inner_type := partial "property-type" $items }} {{ $type = delimit (slice "[" $inner_type "]") "" }} {{ else if or (reflect.IsSlice .type) .oneOf }} @@ -180,8 +174,7 @@ If the property uses `additionalProperties` to describe its internal structure, handle this with a bit of recursion */}} - {{ $additionalProperties := partial "json-schema/resolve-allof" .additionalProperties }} - {{ $type = delimit (slice "{string: " (partial "property-type" $additionalProperties) "}" ) "" }} + {{ $type = delimit (slice "{string: " (partial "property-type" .additionalProperties) "}" ) "" }} {{ else if reflect.IsMap .patternProperties }} {{/* If the property uses `patternProperties` to describe its @@ -193,7 +186,6 @@ {{ $types := slice }} {{ range $pattern, $schema := .patternProperties}} - {{ $schema = partial "json-schema/resolve-allof" $schema }} {{ $types = $types | append (partial "property-type" $schema) }} {{ end }} diff --git a/layouts/partials/openapi/render-request.html b/layouts/partials/openapi/render-request.html index d922b64e..5fa0c255 100644 --- a/layouts/partials/openapi/render-request.html +++ b/layouts/partials/openapi/render-request.html @@ -40,7 +40,7 @@ {{/* Display the JSON schemas */}} - {{ $schema := partial "json-schema/resolve-allof" $json_body.schema }} + {{ $schema := $json_body.schema }} {{ $additional_types := partial "json-schema/resolve-additional-types" (dict "schema" $schema "anchor_base" $anchor_base) }} {{ range $additional_types }} @@ -67,9 +67,7 @@ {{ $example := dict }} {{ if $body.schema }} - {{ $schema := partial "json-schema/resolve-allof" $body.schema }} - - {{ $example = partial "json-schema/resolve-example" $schema }} + {{ $example = partial "json-schema/resolve-example" $body.schema }} {{ end }} {{ if and (eq ($example | len) 0) $body.example }} diff --git a/layouts/partials/openapi/render-responses.html b/layouts/partials/openapi/render-responses.html index 437e811a..07ebaebd 100644 --- a/layouts/partials/openapi/render-responses.html +++ b/layouts/partials/openapi/render-responses.html @@ -47,7 +47,7 @@ Display the JSON schemas */}} - {{ $schema := partial "json-schema/resolve-allof" $json_body.schema }} + {{ $schema := $json_body.schema }} {{/* All this is to work out how to express the content of the response diff --git a/layouts/shortcodes/http-api.html b/layouts/shortcodes/http-api.html index 28e76400..489385de 100644 --- a/layouts/shortcodes/http-api.html +++ b/layouts/shortcodes/http-api.html @@ -24,5 +24,6 @@ {{ $path := delimit (slice "api" $spec $api) "/" }} {{ $api_data = partial "json-schema/resolve-refs" (dict "schema" $api_data "path" $path) }} +{{ $api_data = partial "json-schema/resolve-allof" $api_data }} {{ partial "openapi/render-api" (dict "api_data" $api_data "base_url" $base_url) }}