import com.google.common.base.Preconditions;
import java.io.IOException;
import java.time.format.DateTimeParseException;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
import javax.ws.rs.core.UriInfo;
import org.eclipse.jdt.annotation.NonNull;
import org.opendaylight.mdsal.dom.api.DOMSchemaService;
import org.opendaylight.yangtools.yang.common.Revision;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.model.api.Module;
public abstract class BaseYangOpenApiGenerator {
private static final String CONTROLLER_RESOURCE_NAME = "Controller";
final var title = "Controller modules of RESTCONF";
final var url = schema + "://" + host + "/";
final var basePath = getBasePath();
- final var modules = context.getModules();
+ final var modules = getModulesWithoutDuplications(context);
return new OpenApiInputStream(context, title, url, SECURITY, CONTROLLER_RESOURCE_NAME, "",false, false,
modules, basePath);
}
}
public abstract String getBasePath();
+
+ public static Set<Module> getModulesWithoutDuplications(final @NonNull EffectiveModelContext schemaContext) {
+ return new LinkedHashSet<>(schemaContext.getModules()
+ .stream()
+ .collect(Collectors.toMap(
+ Module::getName,
+ Function.identity(),
+ (module1, module2) -> Revision.compare(
+ module1.getRevision(), module2.getRevision()) > 0 ? module1 : module2,
+ LinkedHashMap::new))
+ .values());
+ }
}
import org.opendaylight.restconf.openapi.impl.OpenApiInputStream;
import org.opendaylight.yangtools.concepts.Registration;
import org.opendaylight.yangtools.yang.common.QName;
-import org.opendaylight.yangtools.yang.common.Revision;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
}
boolean includeDataStore = true;
- var modules = context.getModules();
+ var modules = BaseYangOpenApiGenerator.getModulesWithoutDuplications(context);
if (strPageNum != null) {
final var pageNum = Integer.parseInt(strPageNum);
final var end = DEFAULT_PAGESIZE * pageNum - 1;
} else {
includeDataStore = false;
}
- modules = filterByRange(context, start);
+ modules = filterByRange(modules, start);
}
final var schema = openApiGenerator.createSchemaFromUriInfo(uriInfo);
final var host = openApiGenerator.createHostFromUriInfo(uriInfo);
final var url = schema + "://" + host + "/";
final var basePath = openApiGenerator.getBasePath();
- final var modules = modelContext.getModules();
+ final var modules = BaseYangOpenApiGenerator.getModulesWithoutDuplications(modelContext);
return new OpenApiInputStream(modelContext, urlPrefix, url, SECURITY, deviceName, urlPrefix, true, false,
modules, basePath);
}
longIdToInstanceId.remove(id);
}
- private static Set<Module> filterByRange(final EffectiveModelContext schemaContext, final Integer start) {
- final var sortedModules = new TreeSet<Module>((module1, module2) -> {
- int result = module1.getName().compareTo(module2.getName());
- if (result == 0) {
- result = Revision.compare(module1.getRevision(), module2.getRevision());
- }
- if (result == 0) {
- result = module1.getNamespace().compareTo(module2.getNamespace());
- }
- return result;
- });
- sortedModules.addAll(schemaContext.getModules());
-
+ private static Set<Module> filterByRange(final Set<Module> modulesWithoutDuplications, final Integer start) {
+ final var sortedModules = new TreeSet<>(modulesWithoutDuplications);
final int end = start + DEFAULT_PAGESIZE - 1;
-
var firstModule = sortedModules.first();
- final var iterator = sortedModules.iterator();
+ final var iterator = modulesWithoutDuplications.iterator();
int counter = 0;
while (iterator.hasNext() && counter < end) {
final var module = iterator.next();
*/
package org.opendaylight.restconf.openapi.impl;
+import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import org.skyscreamer.jsonassert.JSONAssert;
public class ToasterDocumentTest extends AbstractDocumentTest {
+ /**
+ * Model toaster@2009-11-20 is used for test correct generating of complex openapi object.
+ */
private static final String TOASTER = "toaster";
private static final String TOASTER_REV = "2009-11-20";
+ /**
+ * Model toaster@2009-11-19 is used for test correct generating of openapi with models with same name and another
+ * revision date. We want to test that the same model is not duplicated and loaded just the newest version.
+ */
+ private static final String TOASTER_OLD_REV = "2009-11-19";
@BeforeAll
public static void beforeClass() {
}
/**
- * Tests the swagger document that is result of the call to the '/toaster(2009-11-20)' endpoint.
- *
- * <p>
- * Model toaster is used for test correct generating of complex openapi object.
+ * Tests the swagger document that is result of the call to the '/toaster@revision' endpoint.
*/
- @Test
- public void getDocByModuleTest() throws Exception {
- final var jsonControllerDoc = getDocByModule(TOASTER, TOASTER_REV);
- final var expectedJson = getExpectedDoc("toaster-document/controller-toaster.json");
- JSONAssert.assertEquals(expectedJson, jsonControllerDoc, IGNORE_ORDER);
+ @ParameterizedTest
+ @MethodSource("getToasterRevisions")
+ public void getDocByModuleTest(final String revision, final String jsonPath) throws Exception {
+ final var expectedJson = getExpectedDoc("toaster-document/" + jsonPath);
+ final var moduleDoc = getDocByModule(TOASTER, revision);
+ JSONAssert.assertEquals(expectedJson, moduleDoc, IGNORE_ORDER);
+ }
+
+ static Stream<Arguments> getToasterRevisions() {
+ // moduleName, revision, jsonPath
+ return Stream.of(
+ Arguments.of(TOASTER_REV, "controller-toaster.json"),
+ Arguments.of(TOASTER_OLD_REV, "controller-toaster-old.json")
+ );
}
/**
}
/**
- * Tests the swagger document that is result of the call to the '/mounts/1/toaster(2009-11-20)' endpoint.
- *
- * <p>
- * Model toaster is used for test correct generating of complex openapi object.
+ * Tests the swagger document that is result of the call to the '/toaster@revision' endpoint.
*/
- @Test
- public void getMountDocByModuleTest() throws Exception {
- final var jsonDeviceDoc = getMountDocByModule(TOASTER, TOASTER_REV);
- final var expectedJson = getExpectedDoc("toaster-document/device-toaster.json");
- JSONAssert.assertEquals(expectedJson, jsonDeviceDoc, IGNORE_ORDER);
+ @ParameterizedTest
+ @MethodSource("getMountToasterRevisions")
+ public void getMountDocByModuleTest(final String revision, final String jsonPath) throws Exception {
+ final var expectedJson = getExpectedDoc("toaster-document/" + jsonPath);
+ final var moduleDoc = getMountDocByModule(TOASTER, revision);
+ JSONAssert.assertEquals(expectedJson, moduleDoc, IGNORE_ORDER);
+ }
+
+ static Stream<Arguments> getMountToasterRevisions() {
+ // moduleName, revision, jsonPath
+ return Stream.of(
+ Arguments.of(TOASTER_REV, "device-toaster.json"),
+ Arguments.of(TOASTER_OLD_REV, "device-toaster-old.json")
+ );
}
}
--- /dev/null
+{
+ "openapi": "3.0.3",
+ "info": {
+ "version": "1.0.0",
+ "title": "toaster",
+ "description": "We are providing full API for configurational data which can be edited (by POST, PUT, PATCH and DELETE).\nFor operational data we only provide GET API.\n\nFor majority of request you can see only config data in examples. That's because we can show only one example\nper request. The exception when you can see operational data in example is when data are representing\noperational (config false) container with no config data in it."
+ },
+ "servers": [
+ {
+ "url": "http://localhost:8181/"
+ }
+ ],
+ "paths": {
+ "/rests/operations/toaster:cancel-toast": {
+ "post": {
+ "description": "Stop making toast, if any is being made.\nA 'resource-denied' error will be returned\nif the toaster service is disabled.",
+ "summary": "POST - Controller - toaster - cancel-toast",
+ "requestBody": {
+ "description": "cancel-toast_input",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "input": {
+ "type": "object"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "xml": {
+ "name": "input",
+ "namespace": "http://netconfcentral.org/ns/toaster"
+ },
+ "type": "object"
+ }
+ }
+ }
+ },
+ "responses": {
+ "204": {
+ "description": "RPC cancel-toast success"
+ }
+ },
+ "tags": [
+ "Controller toaster"
+ ],
+ "parameters": []
+ }
+ },
+ "/rests/operations/toaster:restock-toaster": {
+ "post": {
+ "description": "Restocks the toaster with the amount of bread specified.",
+ "summary": "POST - Controller - toaster - restock-toaster",
+ "requestBody": {
+ "description": "restock-toaster_input",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "input": {
+ "$ref": "#/components/schemas/toaster_restock-toaster_input",
+ "type": "object"
+ }
+ }
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/toaster_restock-toaster_input"
+ }
+ }
+ }
+ },
+ "responses": {
+ "204": {
+ "description": "RPC restock-toaster success"
+ }
+ },
+ "tags": [
+ "Controller toaster"
+ ],
+ "parameters": []
+ }
+ },
+ "/rests/data": {
+ "post": {
+ "description": "YANG version of the TOASTER-MIB.\n\nNote:\nIn example payload, you can see only the first data node child of the resource to be created, following the\nguidelines of RFC 8040, which allows us to create only one resource in POST request.\n",
+ "summary": "POST - Controller - toaster - toaster",
+ "requestBody": {
+ "description": "toaster",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "toaster": {
+ "$ref": "#/components/schemas/toaster_toaster",
+ "type": "object"
+ }
+ }
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/toaster_toaster"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Created"
+ }
+ },
+ "tags": [
+ "Controller toaster"
+ ],
+ "parameters": []
+ }
+ },
+ "/rests/data/toaster:toaster": {
+ "put": {
+ "description": "Top-level container for all toaster database objects.",
+ "summary": "PUT - toaster - Controller - toaster",
+ "requestBody": {
+ "description": "toaster",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "toaster:toaster": {
+ "$ref": "#/components/schemas/toaster_toaster",
+ "type": "object"
+ }
+ }
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/toaster_toaster"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Created"
+ },
+ "204": {
+ "description": "Updated"
+ }
+ },
+ "tags": [
+ "Controller toaster"
+ ],
+ "parameters": []
+ },
+ "patch": {
+ "description": "Top-level container for all toaster database objects.",
+ "summary": "PATCH - toaster - Controller - toaster",
+ "requestBody": {
+ "description": "toaster",
+ "content": {
+ "application/yang-data+json": {
+ "schema": {
+ "properties": {
+ "toaster:toaster": {
+ "$ref": "#/components/schemas/toaster_toaster",
+ "type": "object"
+ }
+ }
+ }
+ },
+ "application/yang-data+xml": {
+ "schema": {
+ "$ref": "#/components/schemas/toaster_toaster"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ },
+ "204": {
+ "description": "Updated"
+ }
+ },
+ "tags": [
+ "Controller toaster"
+ ],
+ "parameters": []
+ },
+ "delete": {
+ "description": "Top-level container for all toaster database objects.",
+ "summary": "DELETE - Controller - toaster - toaster",
+ "responses": {
+ "204": {
+ "description": "Deleted"
+ }
+ },
+ "tags": [
+ "Controller toaster"
+ ],
+ "parameters": []
+ },
+ "get": {
+ "description": "Top-level container for all toaster database objects.",
+ "summary": "GET - Controller - toaster - toaster",
+ "responses": {
+ "200": {
+ "description": "200",
+ "content": {
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/toaster_toaster"
+ }
+ },
+ "application/json": {
+ "schema": {
+ "properties": {
+ "toaster": {
+ "$ref": "#/components/schemas/toaster_toaster",
+ "type": "object"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "tags": [
+ "Controller toaster"
+ ],
+ "parameters": [
+ {
+ "name": "content",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "enum": [
+ "config",
+ "nonconfig",
+ "all"
+ ],
+ "type": "string"
+ }
+ }
+ ]
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "toaster_restock-toaster_input": {
+ "title": "toaster_restock-toaster_input",
+ "type": "object",
+ "properties": {
+ "amountOfBreadToStock": {
+ "description": "Indicates the amount of bread to re-stock",
+ "type": "integer",
+ "format": "int64",
+ "example": 0
+ }
+ },
+ "xml": {
+ "name": "input",
+ "namespace": "http://netconfcentral.org/ns/toaster"
+ }
+ },
+ "toaster_toaster": {
+ "title": "toaster_toaster",
+ "type": "object",
+ "description": "Top-level container for all toaster database objects.",
+ "properties": {
+ "darknessFactor": {
+ "description": "The darkness factor. Basically, the number of ms to multiple the doneness value by.",
+ "type": "integer",
+ "format": "int64",
+ "default": 1000,
+ "example": 0
+ }
+ },
+ "xml": {
+ "name": "toaster",
+ "namespace": "http://netconfcentral.org/ns/toaster"
+ }
+ }
+ },
+ "securitySchemes": {
+ "basicAuth": {
+ "scheme": "basic",
+ "type": "http"
+ }
+ }
+ },
+ "security": [
+ {
+ "basicAuth": []
+ }
+ ]
+}
\ No newline at end of file
--- /dev/null
+{
+ "openapi": "3.0.3",
+ "info": {
+ "version": "1.0.0",
+ "title": "toaster",
+ "description": "We are providing full API for configurational data which can be edited (by POST, PUT, PATCH and DELETE).\nFor operational data we only provide GET API.\n\nFor majority of request you can see only config data in examples. That's because we can show only one example\nper request. The exception when you can see operational data in example is when data are representing\noperational (config false) container with no config data in it."
+ },
+ "servers": [
+ {
+ "url": "http://localhost:8181/"
+ }
+ ],
+ "paths": {
+ "/rests/operations/nodes/node=123/yang-ext:mount/toaster:cancel-toast": {
+ "post": {
+ "description": "Stop making toast, if any is being made.\nA 'resource-denied' error will be returned\nif the toaster service is disabled.",
+ "summary": "POST - 123 - toaster - cancel-toast",
+ "requestBody": {
+ "description": "cancel-toast_input",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "input": {
+ "type": "object"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "xml": {
+ "name": "input",
+ "namespace": "http://netconfcentral.org/ns/toaster"
+ },
+ "type": "object"
+ }
+ }
+ }
+ },
+ "responses": {
+ "204": {
+ "description": "RPC cancel-toast success"
+ }
+ },
+ "tags": [
+ "123 toaster"
+ ],
+ "parameters": []
+ }
+ },
+ "/rests/operations/nodes/node=123/yang-ext:mount/toaster:restock-toaster": {
+ "post": {
+ "description": "Restocks the toaster with the amount of bread specified.",
+ "summary": "POST - 123 - toaster - restock-toaster",
+ "requestBody": {
+ "description": "restock-toaster_input",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "input": {
+ "$ref": "#/components/schemas/toaster_restock-toaster_input",
+ "type": "object"
+ }
+ }
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/toaster_restock-toaster_input"
+ }
+ }
+ }
+ },
+ "responses": {
+ "204": {
+ "description": "RPC restock-toaster success"
+ }
+ },
+ "tags": [
+ "123 toaster"
+ ],
+ "parameters": []
+ }
+ },
+ "/rests/data/nodes/node=123/yang-ext:mount": {
+ "post": {
+ "description": "YANG version of the TOASTER-MIB.\n\nNote:\nIn example payload, you can see only the first data node child of the resource to be created, following the\nguidelines of RFC 8040, which allows us to create only one resource in POST request.\n",
+ "summary": "POST - 123 - toaster - toaster",
+ "requestBody": {
+ "description": "toaster",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "toaster": {
+ "$ref": "#/components/schemas/toaster_toaster",
+ "type": "object"
+ }
+ }
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/toaster_toaster"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Created"
+ }
+ },
+ "tags": [
+ "123 toaster"
+ ],
+ "parameters": []
+ }
+ },
+ "/rests/data/nodes/node=123/yang-ext:mount/toaster:toaster": {
+ "put": {
+ "description": "Top-level container for all toaster database objects.",
+ "summary": "PUT - toaster - 123 - toaster",
+ "requestBody": {
+ "description": "toaster",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "toaster:toaster": {
+ "$ref": "#/components/schemas/toaster_toaster",
+ "type": "object"
+ }
+ }
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/toaster_toaster"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Created"
+ },
+ "204": {
+ "description": "Updated"
+ }
+ },
+ "tags": [
+ "123 toaster"
+ ],
+ "parameters": []
+ },
+ "patch": {
+ "description": "Top-level container for all toaster database objects.",
+ "summary": "PATCH - toaster - 123 - toaster",
+ "requestBody": {
+ "description": "toaster",
+ "content": {
+ "application/yang-data+json": {
+ "schema": {
+ "properties": {
+ "toaster:toaster": {
+ "$ref": "#/components/schemas/toaster_toaster",
+ "type": "object"
+ }
+ }
+ }
+ },
+ "application/yang-data+xml": {
+ "schema": {
+ "$ref": "#/components/schemas/toaster_toaster"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ },
+ "204": {
+ "description": "Updated"
+ }
+ },
+ "tags": [
+ "123 toaster"
+ ],
+ "parameters": []
+ },
+ "delete": {
+ "description": "Top-level container for all toaster database objects.",
+ "summary": "DELETE - 123 - toaster - toaster",
+ "responses": {
+ "204": {
+ "description": "Deleted"
+ }
+ },
+ "tags": [
+ "123 toaster"
+ ],
+ "parameters": []
+ },
+ "get": {
+ "description": "Top-level container for all toaster database objects.",
+ "summary": "GET - 123 - toaster - toaster",
+ "responses": {
+ "200": {
+ "description": "200",
+ "content": {
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/toaster_toaster"
+ }
+ },
+ "application/json": {
+ "schema": {
+ "properties": {
+ "toaster": {
+ "$ref": "#/components/schemas/toaster_toaster",
+ "type": "object"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "tags": [
+ "123 toaster"
+ ],
+ "parameters": [
+ {
+ "name": "content",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "enum": [
+ "config",
+ "nonconfig",
+ "all"
+ ],
+ "type": "string"
+ }
+ }
+ ]
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "toaster_restock-toaster_input": {
+ "title": "toaster_restock-toaster_input",
+ "type": "object",
+ "properties": {
+ "amountOfBreadToStock": {
+ "description": "Indicates the amount of bread to re-stock",
+ "type": "integer",
+ "format": "int64",
+ "example": 0
+ }
+ },
+ "xml": {
+ "name": "input",
+ "namespace": "http://netconfcentral.org/ns/toaster"
+ }
+ },
+ "toaster_toaster": {
+ "title": "toaster_toaster",
+ "type": "object",
+ "description": "Top-level container for all toaster database objects.",
+ "properties": {
+ "darknessFactor": {
+ "description": "The darkness factor. Basically, the number of ms to multiple the doneness value by.",
+ "type": "integer",
+ "format": "int64",
+ "default": 1000,
+ "example": 0
+ }
+ },
+ "xml": {
+ "name": "toaster",
+ "namespace": "http://netconfcentral.org/ns/toaster"
+ }
+ }
+ },
+ "securitySchemes": {
+ "basicAuth": {
+ "scheme": "basic",
+ "type": "http"
+ }
+ }
+ },
+ "security": [
+ {
+ "basicAuth": []
+ }
+ ]
+}
\ No newline at end of file
--- /dev/null
+module toaster {
+
+ yang-version 1;
+
+ namespace
+ "http://netconfcentral.org/ns/toaster";
+
+ prefix toast;
+
+ organization "Netconf Central";
+
+ contact
+ "Andy Bierman <andy@netconfcentral.org>";
+
+ description
+ "YANG version of the TOASTER-MIB.";
+
+ revision "2009-11-19" {
+ description
+ "Changes for testing same module name with another revision. We need to have loaded the newest version.";
+ }
+
+ container toaster {
+ presence
+ "Indicates the toaster service is available";
+ description
+ "Top-level container for all toaster database objects.";
+ leaf toasterManufacturer {
+ type string;
+ config false;
+ mandatory true;
+ description
+ "The name of the toaster's manufacturer. For instance,
+ Microsoft Toaster.";
+ }
+
+ leaf toasterModelNumber {
+ type string;
+ config false;
+ mandatory true;
+ description
+ "The name of the toaster's model. For instance,
+ Radiant Automatic.";
+ }
+
+ leaf toasterStatus {
+ type enumeration {
+ enum "up" {
+ value 1;
+ description
+ "The toaster knob position is up.
+ No toast is being made now.";
+ }
+ enum "down" {
+ value 2;
+ description
+ "The toaster knob position is down.
+ Toast is being made now.";
+ }
+ }
+ config false;
+ mandatory true;
+ description
+ "This variable indicates the current state of
+ the toaster.";
+ }
+
+ leaf darknessFactor {
+ type uint32;
+ config true;
+ default 1000;
+ description
+ "The darkness factor. Basically, the number of ms to multiple the doneness value by.";
+ }
+ } // container toaster
+
+ rpc cancel-toast {
+ description
+ "Stop making toast, if any is being made.
+ A 'resource-denied' error will be returned
+ if the toaster service is disabled.";
+ } // rpc cancel-toast
+
+ rpc restock-toaster {
+ description
+ "Restocks the toaster with the amount of bread specified.";
+
+ input {
+ leaf amountOfBreadToStock {
+ type uint32;
+ description
+ "Indicates the amount of bread to re-stock";
+ }
+ }
+ } // rpc restock-toaster
+
+} // module toaster