Fix incorrect schema reference in root POST link 19/107419/15
authorŠimon Ukuš <simon.ukus@pantheon.tech>
Thu, 24 Aug 2023 09:36:09 +0000 (11:36 +0200)
committerIvan Hrasko <ivan.hrasko@pantheon.tech>
Fri, 22 Sep 2023 07:49:17 +0000 (09:49 +0200)
We are creating a root POST link, when requesting swagger document
for single module. We were passing empty string as module name
when building the POST operation, which resulted in our schema ref
beginning with underscore (like this: "_toaster_module").
The schema is created, but without the leading underscore.

This patch fixes it by passing null value and adding logic
that performs the null checking to construct correct schema reference.

JIRA: NETCONF-1133
Change-Id: I9019dea1874133706f8825b0159e6c571d7b9558
Signed-off-by: Šimon Ukuš <simon.ukus@pantheon.tech>
restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/impl/BaseYangOpenApiGenerator.java
restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/builder/OperationBuilder.java
restconf/restconf-openapi/src/test/java/org/opendaylight/restconf/openapi/impl/OpenApiGeneratorRFC8040Test.java

index 0a2397765f87c155f15fc821cb9db7b6e9e55ade..25004f534cbcc4e4ce409d3164e2fca4e269beb5 100644 (file)
@@ -242,7 +242,7 @@ public abstract class BaseYangOpenApiGenerator {
             final String moduleName = module.getName();
             final String name = moduleName + MODULE_NAME_SUFFIX;
             paths.put(resourcePath, new Path.Builder()
-                .post(buildPost(null, "", name, "", moduleName, deviceName,
+                .post(buildPost(null, null, name, "", moduleName, deviceName,
                     module.getDescription().orElse(""), pathParams))
                 .build());
         }
index 68b6065c969a128ffcbb5cdd85b5ea7f50ea115b..8a5203bbfb47a5d314e53ac1374a904a6dfd0d12 100644 (file)
@@ -56,8 +56,7 @@ public final class OperationBuilder {
         // Hidden on purpose
     }
 
-    public static Operation buildPost(final DataSchemaNode node, final String parentName,
-        final String nodeName,
+    public static Operation buildPost(final DataSchemaNode node, final String parentName, final String nodeName,
             final String discriminator, final String moduleName, final @NonNull String deviceName,
             final String description, final List<Parameter> pathParams) {
         final var summary = SUMMARY_TEMPLATE.formatted(HttpMethod.POST, deviceName, moduleName, nodeName);
@@ -65,13 +64,20 @@ public final class OperationBuilder {
         final List<Parameter> parameters = new ArrayList<>(pathParams);
         final ObjectNode requestBody;
         final DataSchemaNode childNode = node == null ? null : getListOrContainerChildNode(node);
+        final List<String> nameElements = new ArrayList<>();
+        if (parentName != null) {
+            nameElements.add(parentName);
+        }
         if (childNode != null && childNode.isConfiguration()) {
             final String childNodeName = childNode.getQName().getLocalName();
-            final String childDefName = parentName + "_" + nodeName + "_" + childNodeName + discriminator;
+            nameElements.add(nodeName);
+            nameElements.add(childNodeName + discriminator);
+            final String childDefName = String.join("_", nameElements);
             requestBody = createRequestBodyParameter(childDefName, childNodeName, childNode instanceof ListSchemaNode,
                 summary, childNodeName);
         } else {
-            final String defName = parentName + "_" + nodeName + discriminator;
+            nameElements.add(nodeName + discriminator);
+            final String defName = String.join("_", nameElements);
             requestBody = createPostDataRequestBodyParameter(defName, nodeName);
         }
         final ObjectNode responses = JsonNodeFactory.instance.objectNode();
index 62c869282fde7ea2eda0a55c5afcb237a074641c..b4222956af345c6cbb25032024fa5d5273c459ee 100644 (file)
@@ -17,6 +17,7 @@ import static org.mockito.Mockito.when;
 import static org.opendaylight.restconf.openapi.OpenApiTestUtils.getPathGetParameters;
 import static org.opendaylight.restconf.openapi.OpenApiTestUtils.getPathPostParameters;
 import static org.opendaylight.restconf.openapi.impl.BaseYangOpenApiGenerator.BASIC_AUTH_NAME;
+import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.COMPONENTS_PREFIX;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import java.util.ArrayList;
@@ -142,6 +143,23 @@ public final class OpenApiGeneratorRFC8040Test {
         assertNotNull(configLst11);
     }
 
+    /**
+     * Test that reference to schema in each path is valid (all referenced schemas exist).
+     */
+    @Test
+    public void testSchemasExistenceSingleModule() {
+        final var document = generator.getApiDeclaration(TOASTER_2, REVISION_DATE, uriInfo);
+        assertNotNull(document);
+        final var referencedSchemas = new HashSet<String>();
+        for (final var path : document.paths().values()) {
+            referencedSchemas.addAll(extractSchemaRefFromPath(path));
+        }
+        final var schemaNames = document.components().schemas().keySet();
+        for (final var ref : referencedSchemas) {
+            assertTrue("Referenced schema " + ref + " does not exist", schemaNames.contains(ref));
+        }
+    }
+
     /**
      * Test that generated document contains RPC schemas for "make-toast" with correct input.
      */
@@ -445,4 +463,65 @@ public final class OpenApiGeneratorRFC8040Test {
         assertNotNull(required);
         assertEquals(expected, required);
     }
+
+    private static Set<String> extractSchemaRefFromPath(final Path path) {
+        if (path == null) {
+            return Set.of();
+        }
+        final var references = new HashSet<String>();
+        final var get = path.get();
+        if (get != null) {
+            references.addAll(schemaRefFromContent(get.responses().path("200").path("content")));
+        }
+        final var post = path.post();
+        if (post != null) {
+            references.addAll(schemaRefFromContent(post.requestBody().path("content")));
+        }
+        final var put = path.put();
+        if (put != null) {
+            references.addAll(schemaRefFromContent(put.requestBody().path("content")));
+        }
+        final var patch = path.patch();
+        if (patch != null) {
+            references.addAll(schemaRefFromContent(patch.requestBody().path("content")));
+        }
+        return references;
+    }
+
+    /**
+     * The schema node does not have 1 specific structure and the "$ref" child is not always the first child after
+     * schema. Possible schema structures include:
+     * <ul>
+     *   <li>schema/$ref/{reference}</li>
+     *   <li>schema/properties/{nodeName}/$ref/{reference}</li>
+     *   <li>schema/properties/{nodeName}/items/$ref/{reference}</li>
+     * </ul>
+     * @param content the element identified with key "content"
+     * @return the set of referenced schemas
+     */
+    private static Set<String> schemaRefFromContent(final JsonNode content) {
+        final HashSet<String> refs = new HashSet<>();
+        content.fieldNames().forEachRemaining(mediaType -> {
+            final JsonNode schema = content.path(mediaType).path("schema");
+            final JsonNode props = schema.path("properties");
+            final JsonNode nameNode = props.isMissingNode() ? props : props.elements().next().path("items");
+            final JsonNode ref;
+            if (props.isMissingNode()) {
+                // either there is no node with the key "properties", try to find immediate child of schema
+                ref = schema.path("$ref");
+            } else if (nameNode.path("items").isMissingNode()) {
+                // or the "properties" is defined and under that we didn't find the "items" node
+                // try to get "$ref" as immediate child under nameNode
+                ref = nameNode.path("$ref");
+            } else {
+                // or the "items" node is defined, in which case we try to get the "$ref" from this node
+                ref = nameNode.path("items").path("$ref");
+            }
+
+            if (ref != null && !ref.isMissingNode()) {
+                refs.add(ref.asText().replaceFirst(COMPONENTS_PREFIX, ""));
+            }
+        });
+        return refs;
+    }
 }