From 3adbfa30da98f2cee9d46fd96cf3b0eff3a31c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= <76261501+zecakeh@users.noreply.github.com> Date: Wed, 5 Mar 2025 10:44:59 +0100 Subject: [PATCH] Look for examples in all possible locations (#2076) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We used to only look for examples in a few (sometimes arbitrary) places, and we didn't support showing several examples in most cases. This is intended to fix this. In the process we try to deduplicate code to make sure that we use the same logic everywhere. Signed-off-by: Kévin Commaille --- .../internal/newsfragments/2076.clarification | 1 + .../newsfragments/2076.clarification | 1 + data/api/server-server/invites-v1.yaml | 16 --- data/api/server-server/invites-v2.yaml | 19 ---- data/api/server-server/joins-v2.yaml | 1 - data/api/server-server/knocks.yaml | 1 - layouts/partials/events/example.html | 8 +- .../partials/json-schema/resolve-example.html | 46 -------- .../json-schema/resolve-examples.html | 71 ++++++++++++ .../openapi/render-media-type-objects.html | 104 ++++++++++++++++++ layouts/partials/openapi/render-request.html | 59 +--------- .../partials/openapi/render-responses.html | 68 +----------- layouts/partials/render-example.html | 40 +++++++ layouts/shortcodes/definition.html | 11 +- 14 files changed, 226 insertions(+), 220 deletions(-) create mode 100644 changelogs/internal/newsfragments/2076.clarification create mode 100644 changelogs/server_server/newsfragments/2076.clarification delete mode 100644 layouts/partials/json-schema/resolve-example.html create mode 100644 layouts/partials/json-schema/resolve-examples.html create mode 100644 layouts/partials/openapi/render-media-type-objects.html create mode 100644 layouts/partials/render-example.html diff --git a/changelogs/internal/newsfragments/2076.clarification b/changelogs/internal/newsfragments/2076.clarification new file mode 100644 index 00000000..3cf1fb94 --- /dev/null +++ b/changelogs/internal/newsfragments/2076.clarification @@ -0,0 +1 @@ +Support more locations for examples in OpenAPI definitions and JSON schemas. \ No newline at end of file diff --git a/changelogs/server_server/newsfragments/2076.clarification b/changelogs/server_server/newsfragments/2076.clarification new file mode 100644 index 00000000..b3cad0d2 --- /dev/null +++ b/changelogs/server_server/newsfragments/2076.clarification @@ -0,0 +1 @@ +Remove an erroneous `room_id` field in a few examples. diff --git a/data/api/server-server/invites-v1.yaml b/data/api/server-server/invites-v1.yaml index c328f05e..7d241c37 100644 --- a/data/api/server-server/invites-v1.yaml +++ b/data/api/server-server/invites-v1.yaml @@ -75,22 +75,6 @@ paths: example: $ref: ../../event-schemas/examples/invite_room_state.json type: object - example: { - "room_id": "!somewhere:example.org", - "type": "m.room.member", - "state_key": "@joe:elsewhere.com", - "origin": "example.org", - "origin_server_ts": 1549041175876, - "sender": "@someone:example.org", - "content": { - "membership": "invite" - }, - "signatures": { - "example.com": { - "ed25519:key_version": "SomeSignatureHere" - }, - } - } required: true responses: "200": diff --git a/data/api/server-server/invites-v2.yaml b/data/api/server-server/invites-v2.yaml index 8984b6d1..f42c8eea 100644 --- a/data/api/server-server/invites-v2.yaml +++ b/data/api/server-server/invites-v2.yaml @@ -78,25 +78,6 @@ paths: required: - room_version - event - example: { - "room_version": "2", - "event": { - "room_id": "!somewhere:example.org", - "type": "m.room.member", - "state_key": "@joe:elsewhere.com", - "origin": "example.org", - "origin_server_ts": 1549041175876, - "sender": "@someone:example.org", - "content": { - "membership": "invite" - }, - "signatures": { - "example.com": { - "ed25519:key_version": "SomeSignatureHere" - }, - } - } - } required: true responses: "200": diff --git a/data/api/server-server/joins-v2.yaml b/data/api/server-server/joins-v2.yaml index bb45b1be..91e6a83e 100644 --- a/data/api/server-server/joins-v2.yaml +++ b/data/api/server-server/joins-v2.yaml @@ -140,7 +140,6 @@ paths: - type - content example: { - "room_id": "!somewhere:example.org", "type": "m.room.member", "state_key": "@someone:example.org", "origin": "example.org", diff --git a/data/api/server-server/knocks.yaml b/data/api/server-server/knocks.yaml index cba9d32b..01bfa637 100644 --- a/data/api/server-server/knocks.yaml +++ b/data/api/server-server/knocks.yaml @@ -270,7 +270,6 @@ paths: - type - content example: { - "room_id": "!somewhere:example.org", "type": "m.room.member", "state_key": "@someone:example.org", "origin": "example.org", diff --git a/layouts/partials/events/example.html b/layouts/partials/events/example.html index 90752fbd..c24cd66b 100644 --- a/layouts/partials/events/example.html +++ b/layouts/partials/events/example.html @@ -13,10 +13,4 @@ {{ $path := delimit (slice "event-schemas/examples" .name) "/" }} {{ $example_content := partial "json-schema/resolve-refs" (dict "schema" .schema "path" $path) }} -{{ $example_json := jsonify (dict "indent" " ") $example_content }} -{{ $example_json = replace $example_json "\\u003c" "<" }} -{{ $example_json = replace $example_json "\\u003e" ">" | safeHTML }} - -```json -{{ $example_json }} -``` +{{ partial "render-example" (dict "example" $example_content) }} diff --git a/layouts/partials/json-schema/resolve-example.html b/layouts/partials/json-schema/resolve-example.html deleted file mode 100644 index 8fa98400..00000000 --- a/layouts/partials/json-schema/resolve-example.html +++ /dev/null @@ -1,46 +0,0 @@ -{{/* - - For complex objects, example content is sometimes attached to the - object's individual properties (and subproperties...), so to get - a complete example for the whole object we need to iterate through - its properties (and subproperties...) and assemble them. - - That's what this template does. - -*/}} - -{{ $this_object := . }} - -{{ $example := $this_object.example }} - -{{ if not $example }} - - {{ if eq $this_object.type "object" }} - {{ $example = dict }} - - {{ range $key, $property := $this_object.properties}} - {{ $this_property_example := partial "json-schema/resolve-example" $property }} - {{ if $this_property_example }} - {{ $example = merge (dict $key $this_property_example) $example }} - {{ end }} - {{ end }} - - {{ else if eq $this_object.type "array" }} - - {{/* the "items" within an array can either be an object (where we have a - list of items which match the schema), or a list (for tuple - validation, where each item has a different schema). - - TODO: support tuple validation here. - */}} - {{ if reflect.IsMap $this_object.items }} - {{ $items_example := partial "json-schema/resolve-example" $this_object.items }} - {{ if $items_example }} - {{ $example = slice $items_example }} - {{ end }} - {{ end }} - {{ end }} - -{{ end }} - -{{ return $example }} diff --git a/layouts/partials/json-schema/resolve-examples.html b/layouts/partials/json-schema/resolve-examples.html new file mode 100644 index 00000000..dd126540 --- /dev/null +++ b/layouts/partials/json-schema/resolve-examples.html @@ -0,0 +1,71 @@ +{{/* + + Find examples in the given JSON schema. + + Tries to find examples in the `examples` and `example` keys of the schema + first. + + If it doesn't succeed, iterates through the properties and subproperties to + collect their examples, to merge them into a complete example for the whole + object. + + Parameter: the JSON schema to extract the examples from. + +*/}} + +{{ $this_object := . }} +{{ $examples := slice }} + +{{ if $this_object.examples }} + {{/* This is an array of examples, we can just use it */}} + {{ $examples = $this_object.examples }} +{{ else if $this_object.example }} + {{ $examples = slice $this_object.example }} +{{ else }} + {{/* Resolve the nested examples */}} + {{ if eq $this_object.type "object" }} + {{ $example := dict }} + + {{ range $key, $property := $this_object.properties}} + {{ $this_property_examples := partial "json-schema/resolve-examples" $property }} + {{/* + It would be too complex to handle several nested examples, + just use the first one. + */}} + {{ with index $this_property_examples 0 }} + {{ $example = merge (dict $key .) $example }} + {{ end }} + {{ end }} + + {{/* + Add the assembled example to the list if either (a) the example is + non-empty, or (b) the object itself is meant to be empty (so an + empty example is correct). + */}} + {{ if (or $example (not $this_object.properties)) }} + {{ $examples = slice $example }} + {{ end }} + + {{ else if eq $this_object.type "array" }} + + {{/* the "items" within an array can either be an object (where we have a + list of items which match the schema), or a list (for tuple + validation, where each item has a different schema). + + TODO: support tuple validation here. + */}} + {{ if reflect.IsMap $this_object.items }} + {{ $items_examples := partial "json-schema/resolve-examples" $this_object.items }} + {{/* + It would be too complex to handle several nested examples, + just use the first one. + */}} + {{ with index $items_examples 0 }} + {{ $examples = slice (slice .) }} + {{ end }} + {{ end }} + {{ end }} + +{{ end }} + +{{ return $examples }} diff --git a/layouts/partials/openapi/render-media-type-objects.html b/layouts/partials/openapi/render-media-type-objects.html new file mode 100644 index 00000000..7c48320e --- /dev/null +++ b/layouts/partials/openapi/render-media-type-objects.html @@ -0,0 +1,104 @@ +{{/* + + Render a map of [Media Type Objects](https://spec.openapis.org/oas/v3.1.1#media-type-object). + This map can be found as the `content` field of requests and responses. + + Parameters: + + * `content`: A map of MIME type to Media Type Object + * `kind`: the kind of object that is rendered, either `request` or `response`. + * `anchor_base`: a prefix to add to the HTML anchors generated for each object + + This template renders: + + * Object tables if the map contains an `application/json` MIME type, or a + table with the MIME types and the corresponding description of the object. + * The examples of the Media Type Objects. + +*/}} + +{{/* + Request and response bodies can have several content types. If there is a + JSON type, we display that; otherwise we just show the content types and + descriptions. +*/}} +{{ $json_body := index .content "application/json" }} +{{ if $json_body }} + {{/* + Display the JSON schemas + */}} + + {{ $schema := $json_body.schema }} + + {{/* + All this is to work out how to express the content of the response + in the case where it is an array. + */}} + {{ if eq $schema.type "array" }} + {{ $type_of := "" }} + {{ if $schema.items.anyOf }} + {{ $types := slice }} + {{ range $schema.items.anyOf }} + {{ if .title }} + {{ $types = $types | append .title}} + {{ else }} + {{ $types = $types | append .type }} + {{ end }} + {{ end }} + {{ $type_of = delimit $types ", "}} + {{ else }} + {{ $type_of = $schema.items.title }} + {{ end }} +

Array of {{ $type_of }}.

+ {{ end }} + + {{/* + Render object tables for any objects referenced in the request/response. + + This will be a no-op for request/response body types which aren't + objects or arrays. + */}} + {{ $additional_types := partial "json-schema/resolve-additional-types" (dict "schema" $schema "anchor_base" .anchor_base) }} + {{ range $additional_types }} + {{ partial "openapi/render-object-table" . }} + {{ end }} +{{ else }} + {{/* + Show the content types and description. + */}} + {{ partial "openapi/render-content-type" (dict "content_types" .content) }} +{{ end }} + +{{/* + Show all the examples. +*/}} +{{ if eq .kind "request" }} +

Request body example

+{{ end }} + +{{ range $mime, $body := .content }} + {{ $examples := slice }} + + {{/* + Find the examples to display, with the following priority: + + 1. The `examples` field + 2. The `example` field + 3. The examples in the `schema` field + */}} + {{ if $body.examples }} + {{/* This is a map of string to Example Object */}} + {{ range $key, $example := $body.examples }} + {{/* The example is in the `value` field of the object */}} + {{ $examples = $examples | append (slice $example.value) }} + {{ end }} + {{ else if $body.example }} + {{ $examples = slice $body.example }} + {{ else if $body.schema }} + {{ $examples = partial "json-schema/resolve-examples" $body.schema }} + {{ end }} + + {{ range $examples }} + {{ partial "render-example" (dict "example" . "mime" $mime) }} + {{ end }} +{{ end }} diff --git a/layouts/partials/openapi/render-request.html b/layouts/partials/openapi/render-request.html index 80b352c6..88749cbf 100644 --- a/layouts/partials/openapi/render-request.html +++ b/layouts/partials/openapi/render-request.html @@ -33,65 +33,8 @@ {{ if $request_body }}

Request body

- {{/* - A request can have several content types. - */}} - {{ $json_body := index $request_body.content "application/json" }} - {{ if $json_body }} - {{/* - Display the JSON schemas - */}} - {{ $schema := $json_body.schema }} - {{ $additional_types := partial "json-schema/resolve-additional-types" (dict "schema" $schema "anchor_base" $anchor) }} - {{ range $additional_types }} - {{ partial "openapi/render-object-table" . }} - {{ end }} - {{ else }} - {{/* - Show the content types and description. - */}} - {{ partial "openapi/render-content-type" (dict "content_types" $request_body.content) }} - {{ end }} - -

Request body example

- {{/* - Show all the examples. - */}} - {{ range $mime, $body := $request_body.content }} - {{ $example := dict }} - - {{ if $body.schema }} - {{ $example = partial "json-schema/resolve-example" $body.schema }} - {{ end }} - - {{ if and (eq ($example | len) 0) $body.example }} - {{/* - If no example was generated from the schema, fallback to the - main example. - */}} - {{ $example = $body.example }} - {{ end }} - - {{ if eq $mime "application/json" }} - {{ $example_json := jsonify (dict "indent" " ") $example }} - {{ $example_json = replace $example_json "\\u003c" "<" }} - {{ $example_json = replace $example_json "\\u003e" ">" | safeHTML }} -```json -{{ $example_json }} -``` - {{ else }} - {{ $example = $example | safeHTML }} - {{/* - We need to set a language for the code otherwise the styling - is different than other examples. - */}} -```json -{{ $example }} -``` - {{ end }} - - {{ end }} + {{ partial "openapi/render-media-type-objects" (dict "content" $request_body.content "kind" "request" "anchor_base" $anchor) }} {{ end }} {{ else }} diff --git a/layouts/partials/openapi/render-responses.html b/layouts/partials/openapi/render-responses.html index 084e9481..0e7f1324 100644 --- a/layouts/partials/openapi/render-responses.html +++ b/layouts/partials/openapi/render-responses.html @@ -59,72 +59,6 @@ {{ partial "openapi/render-object-table" (dict "title" "Headers" "properties" $headers_dict) }} {{ end }} - {{/* - A response can have several content types. - */}} - {{ $json_body := index $response.content "application/json" }} - {{ if $json_body }} - {{/* - Display the JSON schemas - */}} - - {{ $schema := $json_body.schema }} - - {{/* - All this is to work out how to express the content of the response - in the case where it is an array. - */}} - {{ if eq $schema.type "array" }} - {{ $type_of := "" }} - {{ if $schema.items.anyOf }} - {{ $types := slice }} - {{ range $schema.items.anyOf }} - {{ if .title }} - {{ $types = $types | append .title}} - {{ else }} - {{ $types = $types | append .type }} - {{ end }} - {{ end }} - {{ $type_of = delimit $types ", "}} - {{ else }} - {{ $type_of = $schema.items.title }} - {{ end }} -

Array of {{ $type_of }}.

- {{ end }} - - - {{/* - render object tables for any objects referenced in the - response. (This will be a no-op for response types which aren't - objects or arrays.) - */}} - {{ $additional_types := partial "json-schema/resolve-additional-types" (dict "schema" $schema "anchor_base" $anchor) }} - {{ range $additional_types }} - {{ partial "openapi/render-object-table" . }} - {{ end }} - - {{/* - render an example. currently this is only supported for arrays and objects. - */}} - {{ if or (eq $schema.type "object") (eq $schema.type "array") }} - {{ $example := partial "json-schema/resolve-example" $schema }} - {{ if $json_body.examples }} - {{ $example = $json_body.examples.response.value }} - {{ end }} - - {{ $example_json := jsonify (dict "indent" " ") $example }} - {{ $example_json = replace $example_json "\\u003c" "<" }} - {{ $example_json = replace $example_json "\\u003e" ">" | safeHTML }} - -```json -{{ $example_json }} -``` - {{ end }} - {{ else }} - {{/* - Show the content types and description. - */}} - {{ partial "openapi/render-content-type" (dict "content_types" $response.content) }} - {{ end }} + {{ partial "openapi/render-media-type-objects" (dict "content" $response.content "kind" "response" "anchor_base" $anchor) }} {{ end }} {{ end }} diff --git a/layouts/partials/render-example.html b/layouts/partials/render-example.html new file mode 100644 index 00000000..1f6c18fb --- /dev/null +++ b/layouts/partials/render-example.html @@ -0,0 +1,40 @@ +{{/* + + Renders an example to be included in HTML, with support for pretty-printing + JSON. + + Parameters: + + * `example`: the example + * `mime`: the mime type of the example. Used to pretty-print JSON and for + syntax highlighting. If it is not provided, it is assumed to be + `application/json`. + +*/}} + +{{ $example := .example }} + +{{/* + We need to convert the mime type to a recognized language. + For simplicity we only support JSON, which is also the default. Other mime + types are not highlighted. +*/}} +{{ $language := "json" }} + +{{ if (and .mime (ne .mime "application/json")) }} + {{/* + `no-highlight` treats the value as plain text, but still styles the code + block like the ones with proper syntax highlighting. + */}} + {{ $language = "no-highlight" }} +{{ end }} + +{{ if eq $language "json" }} + {{ $example = jsonify (dict "indent" " ") $example }} + {{ $example = replace $example "\\u003c" "<" }} + {{ $example = replace $example "\\u003e" ">" }} +{{ end }} + +```{{ $language }} +{{ $example | safeHTML }} +``` diff --git a/layouts/shortcodes/definition.html b/layouts/shortcodes/definition.html index 10ad0e87..1a9643d9 100644 --- a/layouts/shortcodes/definition.html +++ b/layouts/shortcodes/definition.html @@ -58,13 +58,14 @@ {{ partial "openapi/render-object-table" . }} {{end}} +{{ $examples := partial "json-schema/resolve-examples" $definition }} +{{ if $examples }}

Examples

-{{ $example := partial "json-schema/resolve-example" $definition }} - -```json -{{ jsonify (dict "indent" " ") $example }} -``` + {{ range $examples }} + {{ partial "render-example" (dict "example" .) }} + {{ end }} +{{ end }}