From 9c97c04bda6255612c3fbdf8b06adb9b3dc11d04 Mon Sep 17 00:00:00 2001 From: Romain Manni-Bucau Date: Thu, 25 Jan 2024 19:59:26 +0100 Subject: [PATCH] [jsonrpc] add openapi converter --- .../documentation/BaseOpenRPCConverter.java | 67 ++++ .../fusion/documentation/OpenRPC2Adoc.java | 41 +-- .../fusion/documentation/OpenRPC2OpenAPI.java | 312 ++++++++++++++++++ .../content/fusion/documentation.adoc | 67 ++++ .../minisite/content/fusion/examples.adoc | 2 + .../fusion/documentation/OpenRPC2ApiTest.java | 227 +++++++++++++ 6 files changed, 682 insertions(+), 34 deletions(-) create mode 100644 fusion-documentation/src/main/java/io/yupiik/fusion/documentation/BaseOpenRPCConverter.java create mode 100644 fusion-documentation/src/main/java/io/yupiik/fusion/documentation/OpenRPC2OpenAPI.java create mode 100644 fusion-documentation/src/main/minisite/content/fusion/documentation.adoc create mode 100644 fusion-documentation/src/test/java/io/yupiik/fusion/documentation/OpenRPC2ApiTest.java diff --git a/fusion-documentation/src/main/java/io/yupiik/fusion/documentation/BaseOpenRPCConverter.java b/fusion-documentation/src/main/java/io/yupiik/fusion/documentation/BaseOpenRPCConverter.java new file mode 100644 index 00000000..f995dd28 --- /dev/null +++ b/fusion-documentation/src/main/java/io/yupiik/fusion/documentation/BaseOpenRPCConverter.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.fusion.documentation; + +import io.yupiik.fusion.json.JsonMapper; +import io.yupiik.fusion.json.internal.JsonMapperImpl; +import io.yupiik.fusion.json.pretty.PrettyJsonMapper; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import static java.util.Objects.requireNonNull; +import static java.util.Optional.empty; + +public abstract class BaseOpenRPCConverter implements Runnable { + protected final Map configuration; + + public BaseOpenRPCConverter(final Map configuration) { + this.configuration = configuration; + } + + protected abstract String convert(Map openrpc, JsonMapper mapper); + + @Override + public void run() { + final var input = Path.of(requireNonNull(configuration.get("input"), "No 'input'")); + if (Files.notExists(input)) { + throw new IllegalArgumentException("Input does not exist '" + input + "'"); + } + final var output = Path.of(requireNonNull(configuration.get("output"), "No 'output'")); + try (final var mapper = new PrettyJsonMapper(new JsonMapperImpl(List.of(), c -> empty()))) { + final var openrpc = asObject(mapper.fromString(Object.class, preProcessInput(Files.readString(input)))); + final var adoc = convert(openrpc, mapper); + if (output.getParent() != null) { + Files.createDirectories(output.getParent()); + } + Files.writeString(output, adoc); + } catch (final IOException ioe) { + throw new IllegalStateException(ioe); + } + } + + protected String preProcessInput(final String input) { + return input; + } + + @SuppressWarnings("unchecked") + protected Map asObject(final Object o) { + return (Map) o; + } +} diff --git a/fusion-documentation/src/main/java/io/yupiik/fusion/documentation/OpenRPC2Adoc.java b/fusion-documentation/src/main/java/io/yupiik/fusion/documentation/OpenRPC2Adoc.java index 6e679124..4d6ab24e 100644 --- a/fusion-documentation/src/main/java/io/yupiik/fusion/documentation/OpenRPC2Adoc.java +++ b/fusion-documentation/src/main/java/io/yupiik/fusion/documentation/OpenRPC2Adoc.java @@ -15,49 +15,27 @@ */ package io.yupiik.fusion.documentation; -import io.yupiik.fusion.json.internal.JsonMapperImpl; +import io.yupiik.fusion.json.JsonMapper; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; import static java.util.Map.entry; -import static java.util.Objects.requireNonNull; -import static java.util.Optional.empty; import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.joining; -public class OpenRPC2Adoc implements Runnable { - private final Map configuration; - +/** + * Converts a partial fusion openrpc to an asciidoctor content. + */ +public class OpenRPC2Adoc extends BaseOpenRPCConverter { public OpenRPC2Adoc(final Map configuration) { - this.configuration = configuration; + super(configuration); } @Override - public void run() { - final var input = Path.of(requireNonNull(configuration.get("input"), "No 'input'")); - final var output = Path.of(requireNonNull(configuration.get("output"), "No 'output'")); - if (Files.notExists(input)) { - throw new IllegalArgumentException("Input does not exist '" + input + "'"); - } - try (final var mapper = new JsonMapperImpl(List.of(), c -> empty())) { - final var openrpc = asObject(mapper.fromString(Object.class, Files.readString(input))); - final var adoc = toAdoc(openrpc); - if (output.getParent() != null) { - Files.createDirectories(output.getParent()); - } - Files.writeString(output, adoc); - } catch (final IOException ioe) { - throw new IllegalStateException(ioe); - } - } - - private String toAdoc(final Map openrpc) { + public String convert(final Map openrpc, final JsonMapper ignored) { final var methods = openrpc.get("methods"); if (!(methods instanceof Map mtd)) { return ""; @@ -139,9 +117,4 @@ private String type(final Map schemas, final Map default -> type; }; } - - @SuppressWarnings("unchecked") - private Map asObject(final Object o) { - return (Map) o; - } } diff --git a/fusion-documentation/src/main/java/io/yupiik/fusion/documentation/OpenRPC2OpenAPI.java b/fusion-documentation/src/main/java/io/yupiik/fusion/documentation/OpenRPC2OpenAPI.java new file mode 100644 index 00000000..71113fab --- /dev/null +++ b/fusion-documentation/src/main/java/io/yupiik/fusion/documentation/OpenRPC2OpenAPI.java @@ -0,0 +1,312 @@ +/* + * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.fusion.documentation; + +import io.yupiik.fusion.json.JsonMapper; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collector; + +import static java.util.Comparator.comparing; +import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.toMap; + +/** + * Converts a partial fusion openrpc to an openapi content which can be used with swagger ui + * (just hacking with a request interceptor to use the server url instead of the request url since path are actually methods). + * + *
+ *     
+ *  requestInterceptor: function (request) {
+ *    if (request.loadSpec) {
+ *      return request;
+ *    }
+ *    var method = request.url.substring(request.url.lastIndexOf('/') + 1);
+ *    return Object.assign(request, {
+ *      url: spec.servers.filter(function (server) { return request.url.indexOf(server.url) === 0; })[0].url,
+ *      body: JSON.stringify({ jsonrpc: '2.0', method: method, params: JSON.parse(request.body) }, undefined, 2)
+ *    });
+ *  }
+ *     
+ * 
+ */ +public class OpenRPC2OpenAPI extends BaseOpenRPCConverter { + public OpenRPC2OpenAPI(final Map configuration) { + super(configuration); + } + + // enables to add tags for examples with some custom logic + protected Map process(final Map out) { + return out; + } + + @Override + protected String preProcessInput(final String input) { + return input.replace("\"#/schemas/", "\"#/components/schemas/"); + } + + @Override + public String convert(final Map openrpc, final JsonMapper mapper) { + // suffix .url/.description to any prefix for servers + final var servers = configuration.keySet().stream() + .filter(it -> it.endsWith(".url")) + .map(it -> { + final var prefix = it.substring(0, it.length() - ".url".length()); + final var out = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + out.put("url", configuration.get(it)); + out.put("description", configuration.getOrDefault(prefix + ".description", prefix)); + return out; + }) + .sorted(comparing(m -> m.get("url").toString())) + .toList(); + + final var out = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + out.put("openapi", configuration.getOrDefault("openApiVersion", "3.0.3" /* swagger ui does not like 3.1.0 for ex */)); + out.put("servers", servers.isEmpty() ? List.of(Map.of("url", "http://localhost:8080/jsonrpc")) : servers); + + // extract anything starting with info.x and set it as x key + // common/required keys are title, description and versions + out.put("info", configuration.keySet().stream() + .filter(it -> it.startsWith("info.")) + .collect(toMap( + i -> i.substring("info.".length()), configuration::get, + (a, b) -> a, () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)))); + + out.put("paths", createPaths(openrpc)); + out.put("components", createComponents(openrpc)); + + return mapper.toString(process(out)); + } + + protected Map createPaths(final Map openRpc) { + final var methods = openRpc.get("methods"); + if (!(methods instanceof Map mtd)) { + return Map.of(); + } + final var voidSchema = Map.of("type", "object"); + return mtd.entrySet().stream() + .collect(Collector.of( + () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER), + (a, i) -> a.put( + '/' + i.getKey().toString(), + Map.of("post", toMethod(voidSchema, asObject(i.getValue())))), + (a, b) -> { + a.putAll(b); + return a; + })); + } + + protected Map toMethod(final Map voidSchema, + final Map method) { + final var out = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + ofNullable(method.get("name")) + .map(Object::toString) + .ifPresent(value -> out.put("operationId", value)); + ofNullable(method.get("description")) + .map(Object::toString) + .ifPresent(value -> out.put("summary", value)); + out.put("requestBody", createRequestBody(method)); + out.put("responses", createResponses(voidSchema, method)); + return out; + } + + @SuppressWarnings("unchecked") + protected Map createRequestBody(final Map method) { + final var params = toJsonSchema((List>) method.get("params")); + return Map.of("content", Map.of("application/json", Map.of("schema", wrapParams(params, method)))); + } + + protected Map wrapParams(final Map params, final Map method) { + final var out = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + out.put("type", "object"); + out.put("required", List.of("jsonrpc", "method")); + + final var name = method.get("name"); + + final var methodValue = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + methodValue.put("type", "string"); + methodValue.put("default", name == null ? "" : name); + methodValue.put("description", "The JSON-RPC method name, should always be '" + name + "'"); + + final var jsonrpc = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + jsonrpc.put("type", "string"); + jsonrpc.put("default", "2.0"); + jsonrpc.put("description", "JSON-RPC version, should always be '2.0'."); + + final var properties = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + properties.put("jsonrpc", jsonrpc); + properties.put("method", methodValue); + properties.put("params", params); + out.put("properties", properties); + return out; + } + + @SuppressWarnings("unchecked") + protected Map createResponses(final Map voidSchema, + final Map method) { + final var result = method.get("result"); + final var resultSchema = result == null ? + voidSchema : + stripId((Map) ((Map) result).get("schema")); + final var ok = create200Response(asObject(resultSchema)); + final var base = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + base.put("200", ok); + + final var errors = method.get("errors"); + if (!(errors instanceof Collection errorList)) { + return base; + } + + errorList.stream() + .map(this::asObject) + .forEach(it -> base.put("x-jsonrpc-code=" + it.get("code"), createErrorResponse(it))); + + return base; + } + + protected Map createErrorResponse(final Map error) { + final var code = error.get("code"); + final var data = error.get("data"); + final var message = error.get("message"); + final var schema = data == null ? Map.of("type", "object") : stripId(asObject(data)); + final var errorCode = "Error code=" + code; + + final var out = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + out.put("description", ofNullable(message).map(it -> it + " (" + errorCode + ")").orElse(errorCode)); + out.put("content", Map.of("application/json", + Map.of("schema", wrapError(((Number) code).intValue(), message == null ? "" : message.toString(), asObject(schema))))); + return out; + } + + protected Map create200Response(final Map resultSchema) { + final var out = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + out.put("description", "OK"); + out.put("content", Map.of("application/json", Map.of("schema", wrapResult(resultSchema)))); + return out; + } + + protected Map wrapError(final int code, final String message, final Map schema) { + final var out = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + out.put("type", "object"); + out.put("required", List.of("jsonrpc", "error")); + + final var codeSchema = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + codeSchema.put("type", "integer"); + codeSchema.put("default", code); + codeSchema.put("description", "A Number that indicates the error type that occurred. This MUST be an integer."); + + final var messageSchema = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + messageSchema.put("type", "string"); + messageSchema.put("default", message); + messageSchema.put("description", "A String providing a short description of the error. The message SHOULD be limited to a concise single sentence."); + + final var errorProperties = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + errorProperties.put("code", codeSchema); + errorProperties.put("message", messageSchema); + errorProperties.put("data", schema); + + final var error = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + error.put("type", "object"); + error.put("required", List.of("code", "message")); + error.put("properties", errorProperties); + + final var jsonrpc = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + jsonrpc.put("type", "string"); + jsonrpc.put("default", "2.0"); + jsonrpc.put("description", "JSON-RPC version, should always be '2.0'."); + + final var properties = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + properties.put("jsonrpc", jsonrpc); + properties.put("error", error); + out.put("properties", properties); + + return out; + } + + protected Map wrapResult(final Map result) { + final var jsonrpc = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + jsonrpc.put("type", "string"); + jsonrpc.put("default", "2.0"); + jsonrpc.put("description", "JSON-RPC version, should always be '2.0'."); + + final var properties = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + properties.put("jsonrpc", jsonrpc); + properties.put("result", result); + + final var out = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + out.put("type", "object"); + out.put("required", List.of("jsonrpc", "result")); + out.put("properties", properties); + return out; + } + + protected Map createComponents(final Map openRpc) { + return Map.of("schemas", createSchemas(openRpc)); + } + + protected Map createSchemas(final Map openRpc) { + final var out = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + out.putAll(asObject(openRpc.get("schemas")).entrySet().stream() + .collect(toMap(Map.Entry::getKey, e -> stripId(asObject(e.getValue()))))); + return out; + } + + protected Map stripId(final Map jsonObject) { + final var out = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + out.putAll(jsonObject.entrySet().stream() + .filter(it -> !"$id".equals(it.getKey())) + .collect(toMap(Map.Entry::getKey, entry -> { + if (entry.getValue() instanceof Collection list) { + return list.stream() + .map(i -> i instanceof Map map ? stripId(asObject(map)) : i) + .toList(); + } else if (entry.getValue() instanceof Map map) { + return stripId(asObject(map)); + } + return entry.getValue(); + }))); + return out; + } + + protected Map toJsonSchema(final Collection> params) { + final var required = new ArrayList(); + final var base = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + base.put("type", "object"); + base.put("properties", params.stream() + .peek(it -> { + if (it.get("required") instanceof Boolean r && r) { + required.add(it.get("name").toString()); + } + }) + .collect(Collector.of( + () -> new LinkedHashMap(), + (a, i) -> a.put(i.get("name").toString(), stripId(asObject(i.get("schema")))), + (m1, m2) -> { + m1.putAll(m2); + return m1; + }))); + if (required.isEmpty()) { + return base; + } + base.put("required", required); + return base; + } +} diff --git a/fusion-documentation/src/main/minisite/content/fusion/documentation.adoc b/fusion-documentation/src/main/minisite/content/fusion/documentation.adoc new file mode 100644 index 00000000..0cdf0ac0 --- /dev/null +++ b/fusion-documentation/src/main/minisite/content/fusion/documentation.adoc @@ -0,0 +1,67 @@ += Documentation + +Documentation module provides some tasks you can integrate with Yupiik minisite - or any other living documentation. +It aims at consuming the JSON metadata generated by Fusion to convert it to a documentation content. + +== Dependency + +[source,xml] +---- + + io.yupiik.fusion + fusion-documentation + ${fusion.version} + +---- + +== JSON-RPC/OpenRPC to Asciidoc + +The class `io.yupiik.fusion.documentation.OpenRPC2Adoc` enables to convert the partial OpenRPC metadata (`META-INF/fusion/jsonrpc/openrpc.json`) to Asciidoc format for an easier integration in documentation. + +Configuration: + +[opts="header",cols="^,^"] +|=== +| Name | Description +| input | the location of the input file (json), often it will be `target/classes/META-INF/fusion/jsonrpc/openrpc.json`. +| output | the location of the output file (asciidoc). +|=== + + +== JSON-RPC/OpenRPC to OpenAPI + +OpenAPI is the OpenRPC for plain old RESTful API. +However it has some great tooling - SwaggerUI to not cite it. + +The class `io.yupiik.fusion.documentation.OpenRPC2OpenAPI` enables to convert the partial OpenRPC metadata (`META-INF/fusion/jsonrpc/openrpc.json`) to OpenAPI format for an easier integration in documentation and in particular SwaggerUI. + +Configuration: + +[opts="header",cols="^,^"] +|=== +| Name | Description +| input | the location of the input file (json), often it will be `target/classes/META-INF/fusion/jsonrpc/openrpc.json`. +| output | the location of the output file (asciidoc). +| *.url | define a server URL, `*` is a prefix used to match the description (you can define multiple servers while prefix is unique). +| *.description | define a server description, `*` is a prefix used to match the url. +| info.* | define `info` block of OpenAPI. Ensure to define `info.title`, `info.version` and `info.description` at least. +|=== + +To integrate it with SwaggerUI you have to register the SwaggerUI `requestInterceptor` which will convert the request url to the server url (which must be JSON-RPC endpoint) since the converter moves methods as path to comply to OpenAPI model: + +[source,javascript] +---- +SwaggerUIBundle({ + ..., + requestInterceptor: function (request) { + if (request.loadSpec) { + return request; + } + var method = request.url.substring(request.url.lastIndexOf('/') + 1); + return Object.assign(request, { + url: spec.servers.filter(function (server) { return request.url.indexOf(server.url) === 0; })[0].url, + body: JSON.stringify({ jsonrpc: '2.0', method: method, params: JSON.parse(request.body) }, undefined, 2) + }); + }, +}); +---- diff --git a/fusion-documentation/src/main/minisite/content/fusion/examples.adoc b/fusion-documentation/src/main/minisite/content/fusion/examples.adoc index 0420826b..b57a3aa4 100644 --- a/fusion-documentation/src/main/minisite/content/fusion/examples.adoc +++ b/fusion-documentation/src/main/minisite/content/fusion/examples.adoc @@ -285,6 +285,8 @@ public class Endpoints { TIP: you can use the configuration entry `fusion.jsonrpc.binding` to change the `/jsonrpc` default binding. You can also set `fusion.jsonrpc.forceInputStreamUsage` to `true` to force the input to be reactive instead of using default request `Reader`. +NOTE: you can review xref:documentation.adoc[documentation] page to see how to render OpenRPC as asciidoc or OpenAPI content. + == Define a "reactive" JSON-RPC endpoint [source,java] diff --git a/fusion-documentation/src/test/java/io/yupiik/fusion/documentation/OpenRPC2ApiTest.java b/fusion-documentation/src/test/java/io/yupiik/fusion/documentation/OpenRPC2ApiTest.java new file mode 100644 index 00000000..25cadeef --- /dev/null +++ b/fusion-documentation/src/test/java/io/yupiik/fusion/documentation/OpenRPC2ApiTest.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.fusion.documentation; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class OpenRPC2ApiTest { + @Test + void render(@TempDir final Path work) throws IOException { + final var spec = Files.writeString(work.resolve("openrpc.json"), """ + { + "schemas": { + "org.example.application.jsonrpc.Greeting": { + "title": "Greeting", + "type": "object", + "properties": { + "message": { + "nullable": true, + "type": "string" + } + }, + "$id": "org.example.application.jsonrpc.Greeting" + } + }, + "methods": { + "greet": { + "description": "Returns some greeting.", + "errors": [ + { + "code": 400, + "message": "Invalid incoming data." + } + ], + "name": "greet", + "paramStructure": "either", + "params": [ + { + "name": "name", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "result": { + "name": "result", + "schema": { + "$ref": "#/schemas/org.example.application.jsonrpc.Greeting" + } + }, + "summary": "Returns some greeting." + } + } + }"""); + final var out = work.resolve("out.adoc"); + new OpenRPC2OpenAPI(Map.of( + "input", spec.toString(), + "output", out.toString(), + "info.title", "The API", + "info.version", "1.2.3", + "info.description", "A super API.", + "servers.main.url", "https://api.company.com/jsonrpc", + "servers.main.description", "The main server" + )).run(); + assertEquals(""" + { + "components": { + "schemas": { + "org.example.application.jsonrpc.Greeting": { + "properties": { + "message": { + "nullable": true, + "type": "string" + } + }, + "title": "Greeting", + "type": "object" + } + } + }, + "info": { + "description": "A super API.", + "title": "The API", + "version": "1.2.3" + }, + "openapi": "3.0.3", + "paths": { + "/greet": { + "post": { + "operationId": "greet", + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "jsonrpc": { + "default": "2.0", + "description": "JSON-RPC version, should always be '2.0'.", + "type": "string" + }, + "method": { + "default": "greet", + "description": "The JSON-RPC method name, should always be 'greet'", + "type": "string" + }, + "params": { + "properties": { + "name": { + "nullable": true, + "type": "string" + } + }, + "type": "object" + } + }, + "required": [ + "jsonrpc", + "method" + ], + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "jsonrpc": { + "default": "2.0", + "description": "JSON-RPC version, should always be '2.0'.", + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/org.example.application.jsonrpc.Greeting" + } + }, + "required": [ + "jsonrpc", + "result" + ], + "type": "object" + } + } + }, + "description": "OK" + }, + "x-jsonrpc-code=400": { + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "properties": { + "code": { + "default": 400, + "description": "A Number that indicates the error type that occurred. This MUST be an integer.", + "type": "integer" + }, + "data": { + "type": "object" + }, + "message": { + "default": "Invalid incoming data.", + "description": "A String providing a short description of the error. The message SHOULD be limited to a concise single sentence.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "jsonrpc": { + "default": "2.0", + "description": "JSON-RPC version, should always be '2.0'.", + "type": "string" + } + }, + "required": [ + "jsonrpc", + "error" + ], + "type": "object" + } + } + }, + "description": "Invalid incoming data. (Error code=400)" + } + }, + "summary": "Returns some greeting." + } + } + }, + "servers": [ + { + "description": "The main server", + "url": "https://api.company.com/jsonrpc" + } + ] + }""", Files.readString(out)); + } +}