Re-implement paths for ChildNodes 36/109036/24
authorlubos-cicut <lubos.cicut@pantheon.tech>
Mon, 20 Nov 2023 17:17:27 +0000 (18:17 +0100)
committerIvan Hrasko <ivan.hrasko@pantheon.tech>
Fri, 8 Dec 2023 13:52:11 +0000 (13:52 +0000)
Re-implement logic for generating paths for child nodes
with new input streams approach.

JIRA: NETCONF-938
Change-Id: I38bda3346e8ec49751a2ac511ad8bd790a6d27cd
Signed-off-by: lubos-cicut <lubos.cicut@pantheon.tech>
12 files changed:
restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/impl/OpenApiInputStream.java
restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/impl/PathsSteam.java [deleted file]
restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/impl/PathsStream.java [new file with mode: 0644]
restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/DeleteEntity.java [new file with mode: 0644]
restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/GetEntity.java [new file with mode: 0644]
restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/OperationEntity.java
restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/ParameterEntity.java [new file with mode: 0644]
restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/ParameterSchemaEntity.java [new file with mode: 0644]
restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PatchEntity.java [new file with mode: 0644]
restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PathEntity.java
restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PostEntity.java
restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PutEntity.java [new file with mode: 0644]

index 7d7eea749278ba1f5220e00283f031e10af4eca4..910f8a4c5d53412ed9e92cf76ea9bad4670d8825 100644 (file)
@@ -40,13 +40,15 @@ public final class OpenApiInputStream extends InputStream {
 
     public OpenApiInputStream(final EffectiveModelContext context, final String openApiVersion, final Info info,
             final List<Server> servers, final List<Map<String, List<String>>> security, final String deviceName,
-            final String urlPrefix) throws IOException {
+            final String urlPrefix, final boolean isForSingleModule, final boolean includeDataStore)
+            throws IOException {
         final OpenApiBodyWriter writer = new OpenApiBodyWriter(generator, stream);
         stack.add(new OpenApiVersionStream(new OpenApiVersionEntity(), writer));
         stack.add(new InfoStream(new InfoEntity(info.version(), info.title(), info.description()), writer));
         stack.add(new ServersStream(new ServersEntity(
             List.of(new ServerEntity(servers.iterator().next().url()))), writer));
-        stack.add(new PathsSteam(context, writer, generator, stream, deviceName, urlPrefix));
+        stack.add(new PathsStream(context, writer, generator, stream, deviceName, urlPrefix, isForSingleModule,
+            includeDataStore));
         stack.add(new SchemasStream(context, writer, generator, stream));
         stack.add(new SecurityStream(writer));
     }
diff --git a/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/impl/PathsSteam.java b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/impl/PathsSteam.java
deleted file mode 100644 (file)
index 058a148..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License v1.0 which accompanies this distribution,
- * and is available at http://www.eclipse.org/legal/epl-v10.html
- */
-package org.opendaylight.restconf.openapi.impl;
-
-import com.fasterxml.jackson.core.JsonGenerator;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayDeque;
-import java.util.Deque;
-import java.util.Iterator;
-import org.opendaylight.restconf.openapi.jaxrs.OpenApiBodyWriter;
-import org.opendaylight.restconf.openapi.model.PathEntity;
-import org.opendaylight.restconf.openapi.model.PostEntity;
-import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
-import org.opendaylight.yangtools.yang.model.api.Module;
-
-public final class PathsSteam extends InputStream {
-    private final Iterator<? extends Module> iterator;
-    private final JsonGenerator generator;
-    private final OpenApiBodyWriter writer;
-    private final ByteArrayOutputStream stream;
-    private final String deviceName;
-    private final String urlPrefix;
-
-    private Reader reader;
-    private boolean eof;
-
-    public PathsSteam(final EffectiveModelContext context, final OpenApiBodyWriter writer,
-            final JsonGenerator generator, final ByteArrayOutputStream stream, final String deviceName,
-            final String urlPrefix) {
-        iterator = context.getModules().iterator();
-        this.generator = generator;
-        this.writer = writer;
-        this.stream = stream;
-        this.deviceName = deviceName;
-        this.urlPrefix = urlPrefix;
-    }
-
-    @Override
-    public int read() throws IOException {
-        if (eof) {
-            return -1;
-        }
-        if (reader == null) {
-            generator.writeObjectFieldStart("paths");
-            generator.flush();
-            reader = new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8);
-            stream.reset();
-        }
-
-        var read = reader.read();
-        while (read == -1) {
-            if (iterator.hasNext()) {
-                reader = new InputStreamReader(new PathStream(toPaths(iterator.next()), writer),
-                    StandardCharsets.UTF_8);
-                read = reader.read();
-                continue;
-            }
-            generator.writeEndObject();
-            generator.flush();
-            reader = new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8);
-            stream.reset();
-            eof = true;
-            return reader.read();
-        }
-
-        return read;
-    }
-
-    @Override
-    public int read(final byte[] array, final int off, final int len) throws IOException {
-        return super.read(array, off, len);
-    }
-
-    private Deque<PathEntity> toPaths(final Module module) {
-        final var result = new ArrayDeque<PathEntity>();
-        // RPC operations (via post) - RPCs have their own path
-        for (final var rpc : module.getRpcs()) {
-            // TODO connect path with payload
-            final var post = new PostEntity(rpc, deviceName, module.getName());
-            final String resolvedPath = "/rests/operations" + urlPrefix + "/" + module.getName() + ":"
-                + rpc.getQName().getLocalName();
-            final var entity = new PathEntity(resolvedPath, post);
-            result.add(entity);
-        }
-
-        /**
-         * TODO
-         * for (final var container : module.getChildNodes()) {
-         *             // get
-         *             // post
-         *             // put
-         *             // patch
-         *             // delete
-         *
-         *             // add path into deque
-         *         }
-         */
-        return result;
-    }
-}
diff --git a/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/impl/PathsStream.java b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/impl/PathsStream.java
new file mode 100644 (file)
index 0000000..adca136
--- /dev/null
@@ -0,0 +1,335 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.openapi.impl;
+
+import static org.opendaylight.restconf.openapi.util.RestDocgenUtil.resolveFullNameFromNode;
+import static org.opendaylight.restconf.openapi.util.RestDocgenUtil.resolvePathArgumentsName;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.restconf.openapi.jaxrs.OpenApiBodyWriter;
+import org.opendaylight.restconf.openapi.model.DeleteEntity;
+import org.opendaylight.restconf.openapi.model.GetEntity;
+import org.opendaylight.restconf.openapi.model.ParameterEntity;
+import org.opendaylight.restconf.openapi.model.ParameterSchemaEntity;
+import org.opendaylight.restconf.openapi.model.PatchEntity;
+import org.opendaylight.restconf.openapi.model.PathEntity;
+import org.opendaylight.restconf.openapi.model.PostEntity;
+import org.opendaylight.restconf.openapi.model.PutEntity;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
+import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
+import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.Module;
+import org.opendaylight.yangtools.yang.model.api.SchemaNode;
+import org.opendaylight.yangtools.yang.model.api.type.BooleanTypeDefinition;
+import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
+import org.opendaylight.yangtools.yang.model.api.type.Int16TypeDefinition;
+import org.opendaylight.yangtools.yang.model.api.type.Int32TypeDefinition;
+import org.opendaylight.yangtools.yang.model.api.type.Int64TypeDefinition;
+import org.opendaylight.yangtools.yang.model.api.type.Int8TypeDefinition;
+import org.opendaylight.yangtools.yang.model.api.type.Uint16TypeDefinition;
+import org.opendaylight.yangtools.yang.model.api.type.Uint32TypeDefinition;
+import org.opendaylight.yangtools.yang.model.api.type.Uint64TypeDefinition;
+import org.opendaylight.yangtools.yang.model.api.type.Uint8TypeDefinition;
+
+public final class PathsStream extends InputStream {
+    private final Iterator<? extends Module> iterator;
+    private final JsonGenerator generator;
+    private final OpenApiBodyWriter writer;
+    private final ByteArrayOutputStream stream;
+    private final EffectiveModelContext schemaContext;
+    private final String deviceName;
+    private final String urlPrefix;
+    private final boolean isForSingleModule;
+    private final boolean includeDataStore;
+
+    private static final String OPERATIONS = "/rests/operations";
+    private static final String DATA = "/rests/data";
+
+    private boolean hasRootPostLink;
+    private boolean hasAddedDataStore;
+
+    private Reader reader;
+    private boolean eof;
+
+    public PathsStream(final EffectiveModelContext schemaContext, final OpenApiBodyWriter writer,
+            final JsonGenerator generator, final ByteArrayOutputStream stream, final String deviceName,
+            final String urlPrefix, final boolean isForSingleModule, final boolean includeDataStore) {
+        iterator = schemaContext.getModules().iterator();
+        this.generator = generator;
+        this.writer = writer;
+        this.stream = stream;
+        this.schemaContext = schemaContext;
+        this.isForSingleModule = isForSingleModule;
+        this.deviceName = deviceName;
+        this.urlPrefix = urlPrefix;
+        this.includeDataStore = includeDataStore;
+        hasRootPostLink = false;
+        hasAddedDataStore = false;
+    }
+
+    @Override
+    public int read() throws IOException {
+        if (eof) {
+            return -1;
+        }
+        if (reader == null) {
+            generator.writeObjectFieldStart("paths");
+            generator.flush();
+            reader = new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8);
+            stream.reset();
+        }
+
+        var read = reader.read();
+        while (read == -1) {
+            if (iterator.hasNext()) {
+                reader = new InputStreamReader(new PathStream(toPaths(iterator.next()), writer),
+                    StandardCharsets.UTF_8);
+                read = reader.read();
+                continue;
+            }
+            generator.writeEndObject();
+            generator.flush();
+            reader = new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8);
+            stream.reset();
+            eof = true;
+            return reader.read();
+        }
+
+        return read;
+    }
+
+    @Override
+    public int read(final byte @NonNull [] array, final int off, final int len) throws IOException {
+        return super.read(array, off, len);
+    }
+
+    private Deque<PathEntity> toPaths(final Module module) {
+        final var result = new ArrayDeque<PathEntity>();
+        if (includeDataStore && !hasAddedDataStore) {
+            final var dataPath = DATA + urlPrefix;
+            result.add(new PathEntity(dataPath, null, null, null,
+                new GetEntity(null, deviceName, "data", null, null, false),
+                null));
+            final var operationsPath = OPERATIONS + urlPrefix;
+            result.add(new PathEntity(operationsPath, null, null, null,
+                new GetEntity(null, deviceName, "operations", null, null, false),
+                null));
+            hasAddedDataStore = true;
+        }
+        // RPC operations (via post) - RPCs have their own path
+        for (final var rpc : module.getRpcs()) {
+            // TODO connect path with payload
+            final var localName = rpc.getQName().getLocalName();
+            final var post = new PostEntity(rpc, deviceName, module.getName(), new ArrayList<>(), localName, null);
+            final var resolvedPath = OPERATIONS + urlPrefix + "/" + module.getName() + ":" + localName;
+            final var entity = new PathEntity(resolvedPath, post, null, null, null, null);
+            result.add(entity);
+        }
+        for (final var node : module.getChildNodes()) {
+            final var moduleName = module.getName();
+            final boolean isConfig = node.isConfiguration();
+            final var nodeLocalName = node.getQName().getLocalName();
+
+            if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
+                if (isConfig && !hasRootPostLink && isForSingleModule) {
+                    final var resolvedPath = DATA + urlPrefix;
+                    result.add(new PathEntity(resolvedPath, new PostEntity(node, deviceName, moduleName,
+                        new ArrayList<>(), nodeLocalName, module), null, null, null, null));
+                    hasRootPostLink = true;
+                }
+                //process first node
+                final var pathParams = new ArrayList<ParameterEntity>();
+                final var localName = moduleName + ":" + nodeLocalName;
+                final var path = urlPrefix + "/" + processPath(node, pathParams, localName);
+                processChildNode(node, pathParams, moduleName, result, path, nodeLocalName, isConfig, schemaContext,
+                    deviceName);
+            }
+        }
+        return result;
+    }
+
+    private static void processChildNode(final DataSchemaNode node, final List<ParameterEntity> pathParams,
+            final String moduleName, final Deque<PathEntity> result, final String path, final String refPath,
+            final boolean isConfig, final EffectiveModelContext schemaContext, final String deviceName) {
+        final var resourcePath = DATA + path;
+        final var fullName = resolveFullNameFromNode(node.getQName(), schemaContext);
+        final var firstChild = getListOrContainerChildNode((DataNodeContainer) node);
+        if (firstChild != null && node instanceof ContainerSchemaNode) {
+            result.add(processTopPathEntity(node, resourcePath, pathParams, moduleName, refPath, isConfig,
+                fullName, firstChild, deviceName));
+        } else {
+            result.add(processDataPathEntity(node, resourcePath, pathParams, moduleName, refPath,
+                isConfig, fullName, deviceName));
+        }
+        final var childNodes = ((DataNodeContainer) node).getChildNodes();
+        if (node instanceof ActionNodeContainer actionContainer) {
+            final var actionParams = new ArrayList<>(pathParams);
+            actionContainer.getActions().forEach(actionDef -> {
+                final var resourceActionPath = path + "/" + resolvePathArgumentsName(actionDef.getQName(),
+                    node.getQName(), schemaContext);
+                final var childPath = OPERATIONS + resourceActionPath;
+                result.add(processRootAndActionPathEntity(actionDef, childPath, actionParams, moduleName,
+                    refPath, deviceName));
+            });
+        }
+        for (final var childNode : childNodes) {
+            final var childParams = new ArrayList<>(pathParams);
+            final var newRefPath = refPath + "_" + childNode.getQName().getLocalName();
+            if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
+                final var localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
+                final var resourceDataPath = path + "/" + processPath(childNode, childParams, localName);
+                final var newConfig = isConfig && childNode.isConfiguration();
+                processChildNode(childNode, childParams, moduleName, result, resourceDataPath, newRefPath, newConfig,
+                    schemaContext, deviceName);
+            }
+        }
+    }
+
+    private static <T extends DataNodeContainer> DataSchemaNode getListOrContainerChildNode(final T node) {
+        return node.getChildNodes().stream()
+            .filter(n -> n instanceof ListSchemaNode || n instanceof ContainerSchemaNode)
+            .findFirst().orElse(null);
+    }
+
+    private static PathEntity processDataPathEntity(final SchemaNode node, final String resourcePath,
+            final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
+            final boolean isConfig, final String fullName, final String deviceName) {
+        if (isConfig) {
+            return new PathEntity(resourcePath, null,
+                new PatchEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
+                new PutEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
+                new GetEntity(node, deviceName, moduleName, pathParams, refPath, true),
+                new DeleteEntity(node, deviceName, moduleName, pathParams, refPath));
+        } else {
+            return new PathEntity(resourcePath, null, null, null,
+                new GetEntity(node, deviceName, moduleName, pathParams, refPath, false), null);
+        }
+    }
+
+    private static PathEntity processTopPathEntity(final SchemaNode node, final String resourcePath,
+            final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
+            final boolean isConfig, final String fullName, final SchemaNode childNode, final String deviceName) {
+        if (isConfig) {
+            final var childNodeRefPath = refPath + "_" + childNode.getQName().getLocalName();
+            var post = new PostEntity(childNode, deviceName, moduleName, pathParams, childNodeRefPath, node);
+            if (!((DataSchemaNode) childNode).isConfiguration()) {
+                post = new PostEntity(node, deviceName, moduleName, pathParams, refPath, null);
+            }
+            return new PathEntity(resourcePath, post,
+                new PatchEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
+                new PutEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
+                new GetEntity(node, deviceName, moduleName, pathParams, refPath, true),
+                new DeleteEntity(node, deviceName, moduleName, pathParams, refPath));
+        } else {
+            return new PathEntity(resourcePath, null, null, null,
+                new GetEntity(node, deviceName, moduleName, pathParams, refPath, false), null);
+        }
+    }
+
+    private static PathEntity processRootAndActionPathEntity(final SchemaNode node, final String resourcePath,
+            final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
+            final String deviceName) {
+        return new PathEntity(resourcePath,
+            new PostEntity(node, deviceName, moduleName, pathParams, refPath, null),
+            null, null, null, null);
+    }
+
+    private static String processPath(final DataSchemaNode node, final List<ParameterEntity> pathParams,
+            final String localName) {
+        final var path = new StringBuilder();
+        path.append(localName);
+        final var parameters = pathParams.stream()
+            .map(ParameterEntity::name)
+            .collect(Collectors.toSet());
+
+        if (node instanceof ListSchemaNode listSchemaNode) {
+            var prefix = "=";
+            var discriminator = 1;
+            for (final var listKey : listSchemaNode.getKeyDefinition()) {
+                final var keyName = listKey.getLocalName();
+                var paramName = keyName;
+                while (!parameters.add(paramName)) {
+                    paramName = keyName + discriminator;
+                    discriminator++;
+                }
+
+                final var pathParamIdentifier = prefix + "{" + paramName + "}";
+                prefix = ",";
+                path.append(pathParamIdentifier);
+
+                final var description = listSchemaNode.findDataChildByName(listKey)
+                    .flatMap(DataSchemaNode::getDescription).orElse(null);
+
+                pathParams.add(new ParameterEntity(paramName, "path", true,
+                    new ParameterSchemaEntity(getAllowedType(listSchemaNode, listKey), null), description));
+            }
+        }
+        return path.toString();
+    }
+
+    private static String getAllowedType(final ListSchemaNode list, final QName key) {
+        final var keyType = ((LeafSchemaNode) list.getDataChildByName(key)).getType();
+
+        // see: https://datatracker.ietf.org/doc/html/rfc7950#section-4.2.4
+        // see: https://swagger.io/docs/specification/data-models/data-types/
+        // TODO: Java 21 use pattern matching for switch
+        if (keyType instanceof Int8TypeDefinition) {
+            return "integer";
+        }
+        if (keyType instanceof Int16TypeDefinition) {
+            return "integer";
+        }
+        if (keyType instanceof Int32TypeDefinition) {
+            return "integer";
+        }
+        if (keyType instanceof Int64TypeDefinition) {
+            return "integer";
+        }
+        if (keyType instanceof Uint8TypeDefinition) {
+            return "integer";
+        }
+        if (keyType instanceof Uint16TypeDefinition) {
+            return "integer";
+        }
+        if (keyType instanceof Uint32TypeDefinition) {
+            return "integer";
+        }
+        if (keyType instanceof Uint64TypeDefinition) {
+            return "integer";
+        }
+
+        if (keyType instanceof DecimalTypeDefinition) {
+            return "number";
+        }
+
+        if (keyType instanceof BooleanTypeDefinition) {
+            return "boolean";
+        }
+
+        return "string";
+    }
+}
diff --git a/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/DeleteEntity.java b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/DeleteEntity.java
new file mode 100644 (file)
index 0000000..709ce1d
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.openapi.model;
+
+import static javax.ws.rs.core.Response.Status.NO_CONTENT;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import java.io.IOException;
+import java.util.List;
+import javax.ws.rs.HttpMethod;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.yang.model.api.SchemaNode;
+
+public final class DeleteEntity extends OperationEntity {
+
+    public DeleteEntity(final SchemaNode schema, final String deviceName, final String moduleName,
+            final List<ParameterEntity> parameters, final String refPath) {
+        super(schema, deviceName, moduleName, parameters, refPath);
+    }
+
+    @Override
+    protected String operation() {
+        return "delete";
+    }
+
+    @Override
+    @Nullable String summary() {
+        return SUMMARY_TEMPLATE.formatted(HttpMethod.DELETE, deviceName(), moduleName(), nodeName());
+    }
+
+    @Override
+    void generateResponses(final @NonNull JsonGenerator generator) throws IOException {
+        generator.writeObjectFieldStart(RESPONSES);
+        generator.writeObjectFieldStart(String.valueOf(NO_CONTENT.getStatusCode()));
+        generator.writeStringField(DESCRIPTION, "Deleted");
+        generator.writeEndObject();
+        generator.writeEndObject();
+    }
+}
diff --git a/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/GetEntity.java b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/GetEntity.java
new file mode 100644 (file)
index 0000000..7273a1e
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.openapi.model;
+
+import static javax.ws.rs.core.Response.Status.OK;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import java.io.IOException;
+import java.util.List;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.core.MediaType;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.SchemaNode;
+
+public final class GetEntity extends OperationEntity {
+
+    private final boolean isConfig;
+
+    public GetEntity(final SchemaNode schema, final String deviceName, final String moduleName,
+            final List<ParameterEntity> parameters, final String refPath, final boolean isConfig) {
+        super(schema, deviceName, moduleName, parameters, refPath);
+        this.isConfig = isConfig;
+    }
+
+    @Override
+    protected String operation() {
+        return "get";
+    }
+
+    @Override
+    @Nullable String summary() {
+        return SUMMARY_TEMPLATE.formatted(HttpMethod.GET, deviceName(), moduleName(), nodeName());
+    }
+
+    @Override
+    void generateResponses(final @NonNull JsonGenerator generator) throws IOException {
+        final var ref = COMPONENTS_PREFIX + moduleName() + "_" + refPath();
+        generator.writeObjectFieldStart(RESPONSES);
+        generator.writeObjectFieldStart(String.valueOf(OK.getStatusCode()));
+        generator.writeStringField(DESCRIPTION, String.valueOf(OK.getStatusCode()));
+        generator.writeObjectFieldStart(CONTENT);
+        generateMediaTypeSchemaRef(generator, MediaType.APPLICATION_XML, ref);
+        generator.writeObjectFieldStart(MediaType.APPLICATION_JSON);
+        generator.writeObjectFieldStart(SCHEMA);
+        generator.writeObjectFieldStart(PROPERTIES);
+        generator.writeObjectFieldStart(nodeName());
+        if (schema() instanceof ListSchemaNode) {
+            generator.writeStringField(TYPE, ARRAY);
+            generator.writeObjectFieldStart(ITEMS);
+            generator.writeStringField(REF, ref);
+            generator.writeStringField(TYPE, OBJECT);
+            generator.writeEndObject(); //end of items
+        } else {
+            generator.writeStringField(REF, ref);
+            generator.writeStringField(TYPE, OBJECT);
+        }
+        generator.writeEndObject(); //end of nodeName
+        generator.writeEndObject(); //end of props
+        generator.writeEndObject(); //end of schema
+        generator.writeEndObject(); //end of json
+        generator.writeEndObject(); //end of content
+        generator.writeEndObject(); //end of 200
+        generator.writeEndObject(); //end of responses
+    }
+
+    @Override
+    void generateParams(@NonNull JsonGenerator generator) throws IOException {
+        final var contentParam = new ParameterEntity(CONTENT, "query", !isConfig,
+            new ParameterSchemaEntity("string", List.of("config", "nonconfig", "all")), null);
+        parameters().add(contentParam);
+        generator.writeArrayFieldStart(PARAMETERS);
+        for (final var parameter : parameters()) {
+            final var schemaEnum = parameter.schema().schemaEnum();
+            generator.writeStartObject();
+            generator.writeStringField(NAME, parameter.name());
+            generator.writeStringField(IN, parameter.in());
+            generator.writeBooleanField(REQUIRED, parameter.required());
+            generator.writeObjectFieldStart(SCHEMA);
+            if (schemaEnum != null) {
+                generator.writeArrayFieldStart("enum");
+                for (final var enumCase : schemaEnum) {
+                    generator.writeString(enumCase);
+                }
+                generator.writeEndArray(); //end of enum
+            }
+            generator.writeStringField(TYPE, parameter.schema().type());
+            generator.writeEndObject(); //end of schema
+            if (parameter.description() != null) {
+                generator.writeStringField(DESCRIPTION, parameter.description());
+            }
+            generator.writeEndObject(); //end of parameter
+        }
+        generator.writeEndArray(); //end of params
+        parameters().remove(contentParam);
+    }
+}
index a263be0aed2774692d1477beb36cca7ad4ab5eed..fa7d0abde7ba602a97cf640d5df42c18ef11fc03 100644 (file)
@@ -7,21 +7,47 @@
  */
 package org.opendaylight.restconf.openapi.model;
 
+import static javax.ws.rs.core.Response.Status.OK;
+
 import com.fasterxml.jackson.core.JsonGenerator;
 import java.io.IOException;
+import java.util.List;
+import javax.ws.rs.HttpMethod;
 import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.Nullable;
-import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
+import org.opendaylight.yangtools.yang.model.api.SchemaNode;
 
 /**
  * Archetype for an Operation.
  */
-public abstract sealed class OperationEntity extends OpenApiEntity permits PostEntity {
-    private final OperationDefinition schema;
+public abstract sealed class OperationEntity extends OpenApiEntity permits DeleteEntity, GetEntity, PatchEntity,
+        PostEntity, PutEntity {
+    protected static final String SCHEMA = "schema";
+    protected static final String SUMMARY_TEMPLATE = "%s - %s - %s - %s";
+    protected static final String RESPONSES = "responses";
+    protected static final String DESCRIPTION = "description";
+    protected static final String OBJECT = "object";
+    protected static final String CONTENT = "content";
+    protected static final String COMPONENTS_PREFIX = "#/components/schemas/";
+    protected static final String PROPERTIES = "properties";
+    protected static final String TYPE = "type";
+    protected static final String ARRAY = "array";
+    protected static final String ITEMS = "items";
+    protected static final String REF = "$ref";
+    protected static final String PARAMETERS = "parameters";
+    protected static final String SUMMARY = "summary";
+    protected static final String NAME = "name";
+    protected static final String IN = "in";
+    protected static final String REQUIRED = "required";
+    protected static final String REQUEST_BODY = "requestBody";
+
+    private final SchemaNode schema;
     private final String deviceName;
     private final String moduleName;
+    private final String refPath;
+    private final List<ParameterEntity> parameters;
 
-    protected OperationDefinition schema() {
+    protected SchemaNode schema() {
         return schema;
     }
 
@@ -33,32 +59,47 @@ public abstract sealed class OperationEntity extends OpenApiEntity permits PostE
         return moduleName;
     }
 
-    public OperationEntity(final OperationDefinition schema, final String deviceName, final String moduleName) {
+    protected List<ParameterEntity> parameters() {
+        return parameters;
+    }
+
+    protected String refPath() {
+        return refPath;
+    }
+
+    public OperationEntity(final SchemaNode schema, final String deviceName, final String moduleName,
+            final List<ParameterEntity> parameters, final String refPath) {
         this.schema = schema;
         this.deviceName = deviceName;
         this.moduleName = moduleName;
+        this.parameters = parameters;
+        this.refPath = refPath;
     }
 
     @Override
     public void generate(@NonNull JsonGenerator generator) throws IOException {
-        generator.writeObjectFieldStart(operation());
-        final var deprecated = deprecated();
-        if (deprecated != null) {
-            generator.writeBooleanField("deprecated", deprecated);
+        if (schema() == null) {
+            generateGetRoot(generator, moduleName());
+        } else {
+            generator.writeObjectFieldStart(operation());
+            generateBasics(generator);
+            generateRequestBody(generator);
+            generateResponses(generator);
+            generateTags(generator);
+            generateParams(generator);
+            generator.writeEndObject();
         }
+    }
+
+    public void generateBasics(@NonNull JsonGenerator generator) throws IOException {
         final var description = description();
         if (description != null) {
-            generator.writeStringField("description", description);
+            generator.writeStringField(DESCRIPTION, description);
         }
         final var summary = summary();
         if (summary != null) {
-            generator.writeStringField("summary", summary);
+            generator.writeStringField(SUMMARY, summary);
         }
-        generateRequestBody(generator);
-        generateResponses(generator);
-        generateTags(generator);
-        generateParams(generator);
-        generator.writeEndObject();
     }
 
     protected abstract String operation();
@@ -71,6 +112,13 @@ public abstract sealed class OperationEntity extends OpenApiEntity permits PostE
         return schema.getDescription().orElse("");
     }
 
+    @Nullable String nodeName() {
+        if (schema() != null) {
+            return schema().getQName().getLocalName();
+        }
+        return null;
+    }
+
     @Nullable abstract String summary();
 
     void generateRequestBody(final @NonNull JsonGenerator generator) throws IOException {
@@ -88,8 +136,54 @@ public abstract sealed class OperationEntity extends OpenApiEntity permits PostE
     }
 
     void generateParams(final @NonNull JsonGenerator generator) throws IOException {
-        generator.writeArrayFieldStart("parameters");
-        // need empty array here
-        generator.writeEndArray();
+        generator.writeArrayFieldStart(PARAMETERS);
+        if (!parameters().isEmpty()) {
+            for (final var parameter : parameters()) {
+                generator.writeStartObject();
+                generator.writeStringField(NAME, parameter.name());
+                generator.writeStringField(IN, parameter.in());
+                generator.writeBooleanField(REQUIRED, parameter.required());
+                generator.writeObjectFieldStart(SCHEMA);
+                generator.writeStringField(TYPE, parameter.schema().type());
+                generator.writeEndObject(); //end of schema
+                if (parameter.description() != null) {
+                    generator.writeStringField(DESCRIPTION, parameter.description());
+                }
+                generator.writeEndObject(); //end of parameter
+            }
+        }
+        generator.writeEndArray(); //end of params
+    }
+
+
+    protected static void generateMediaTypeSchemaRef(final JsonGenerator generator, final String mediaType,
+            final String ref) throws IOException {
+        generator.writeObjectFieldStart(mediaType);
+        generator.writeObjectFieldStart(SCHEMA);
+        generator.writeStringField(REF, ref);
+        generator.writeEndObject();
+        generator.writeEndObject();
+    }
+
+    void generateGetRoot(final JsonGenerator generator, final String resourceType)
+            throws IOException {
+        generator.writeObjectFieldStart("get");
+        if (resourceType.equals("data")) {
+            generator.writeStringField(DESCRIPTION, "Queries the config (startup) datastore on the mounted hosted.");
+        } else if (resourceType.equals("operations")) {
+            generator.writeStringField(DESCRIPTION,
+                "Queries the available operations (RPC calls) on the mounted hosted.");
+        }
+        generator.writeObjectFieldStart(RESPONSES);
+        generator.writeObjectFieldStart(String.valueOf(OK.getStatusCode()));
+        generator.writeStringField(DESCRIPTION, "OK");
+        generator.writeEndObject(); //end of 200
+        generator.writeEndObject(); // end of responses
+        final var summary = HttpMethod.GET + " - " + deviceName() + " - datastore - " + resourceType;
+        generator.writeStringField(SUMMARY, summary);
+        generator.writeArrayFieldStart("tags");
+        generator.writeString(deviceName + " GET root");
+        generator.writeEndArray(); //end of tags
+        generator.writeEndObject(); //end of get
     }
 }
diff --git a/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/ParameterEntity.java b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/ParameterEntity.java
new file mode 100644 (file)
index 0000000..a6f8485
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech s.r.o and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.openapi.model;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+
+public record ParameterEntity(@NonNull String name, @NonNull String in, boolean required,
+        @NonNull ParameterSchemaEntity schema, @Nullable String description) {
+    public ParameterEntity(final String name, final String in, final boolean required,
+            final ParameterSchemaEntity schema, final String description) {
+        this.name = name;
+        this.in = in;
+        this.required = required;
+        this.schema = schema;
+        this.description = description;
+    }
+}
diff --git a/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/ParameterSchemaEntity.java b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/ParameterSchemaEntity.java
new file mode 100644 (file)
index 0000000..463ca0a
--- /dev/null
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech s.r.o and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.openapi.model;
+
+import java.util.List;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+
+public record ParameterSchemaEntity(@NonNull String type, @Nullable List<String> schemaEnum) {
+    public ParameterSchemaEntity(final String type, final List<String> schemaEnum) {
+        this.type = type;
+        this.schemaEnum = schemaEnum;
+    }
+}
diff --git a/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PatchEntity.java b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PatchEntity.java
new file mode 100644 (file)
index 0000000..7e260eb
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.openapi.model;
+
+import static javax.ws.rs.core.Response.Status.NO_CONTENT;
+import static javax.ws.rs.core.Response.Status.OK;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import java.io.IOException;
+import java.util.List;
+import javax.ws.rs.HttpMethod;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.SchemaNode;
+
+public final class PatchEntity extends OperationEntity {
+
+    private final String fullName;
+
+    public PatchEntity(final SchemaNode schema, final String deviceName, final String moduleName,
+            final List<ParameterEntity> parameters, final String refPath, final String fullName) {
+        super(schema, deviceName, moduleName, parameters, refPath);
+        this.fullName = fullName;
+    }
+
+    @Override
+    protected String operation() {
+        return "patch";
+    }
+
+    @Override
+    @Nullable String summary() {
+        return SUMMARY_TEMPLATE.formatted(HttpMethod.PATCH, moduleName(), deviceName(), nodeName());
+    }
+
+    @Override
+    void generateResponses(final @NonNull JsonGenerator generator) throws IOException {
+        generator.writeObjectFieldStart(RESPONSES);
+        generator.writeObjectFieldStart(String.valueOf(OK.getStatusCode()));
+        generator.writeStringField(DESCRIPTION, "OK");
+        generator.writeEndObject(); //end of 200
+        generator.writeObjectFieldStart(String.valueOf(NO_CONTENT.getStatusCode()));
+        generator.writeStringField(DESCRIPTION, "Updated");
+        generator.writeEndObject(); //end of 204
+        generator.writeEndObject();
+    }
+
+    @Override
+    void generateRequestBody(final @NonNull JsonGenerator generator) throws IOException {
+        final var ref = COMPONENTS_PREFIX + moduleName() + "_" + refPath();
+        generator.writeObjectFieldStart(REQUEST_BODY);
+        generator.writeStringField(DESCRIPTION, nodeName());
+        generator.writeObjectFieldStart(CONTENT);
+        generator.writeObjectFieldStart("application/yang-data+json");
+        generator.writeObjectFieldStart(SCHEMA);
+        generator.writeObjectFieldStart(PROPERTIES);
+        generator.writeObjectFieldStart(fullName);
+        if (schema() instanceof ListSchemaNode) {
+            generator.writeStringField(TYPE, ARRAY);
+            generator.writeObjectFieldStart(ITEMS);
+            generator.writeStringField(REF, ref);
+            generator.writeStringField(TYPE, OBJECT);
+            generator.writeEndObject(); //end of items
+        } else {
+            generator.writeStringField(REF, ref);
+            generator.writeStringField(TYPE, OBJECT);
+        }
+        generator.writeEndObject(); //end of nodeName
+        generator.writeEndObject(); //end of props
+        generator.writeEndObject(); //end of schema
+        generator.writeEndObject(); //end of json
+        generateMediaTypeSchemaRef(generator, "application/yang-data+xml", ref);
+        generator.writeEndObject(); //end of content
+        generator.writeEndObject(); //end of request body
+    }
+}
index 2b0174734baf11662cac8378b607bea2c2b2c17b..2d5c2ac9ffc47c14488cb04b51a4a105658fd590 100644 (file)
@@ -15,11 +15,20 @@ import org.eclipse.jdt.annotation.Nullable;
 
 public final class PathEntity extends OpenApiEntity {
     private final @NonNull String path;
-    private final @Nullable OperationEntity post;
+    private final @Nullable PostEntity post;
+    private final @Nullable PatchEntity patch;
+    private final @Nullable GetEntity get;
+    private final @Nullable PutEntity put;
+    private final @Nullable DeleteEntity delete;
 
-    public PathEntity(final String path, final OperationEntity post) {
+    public PathEntity(final String path, final PostEntity post, final PatchEntity patch,
+            final PutEntity put, final GetEntity get, final DeleteEntity delete) {
         this.path = Objects.requireNonNull(path);
         this.post = post;
+        this.patch = patch;
+        this.put = put;
+        this.delete = delete;
+        this.get = get;
     }
 
     @Override
@@ -49,6 +58,14 @@ public final class PathEntity extends OpenApiEntity {
         if (patchOperation != null) {
             patchOperation.generate(generator);
         }
+        final var deleteOperation = delete();
+        if (deleteOperation != null) {
+            deleteOperation.generate(generator);
+        }
+        final var getOperation = get();
+        if (getOperation != null) {
+            getOperation.generate(generator);
+        }
         generator.writeEndObject();
     }
 
@@ -69,10 +86,18 @@ public final class PathEntity extends OpenApiEntity {
     }
 
     @Nullable OperationEntity put() {
-        return null;
+        return put;
     }
 
     @Nullable OperationEntity patch() {
-        return null;
+        return patch;
+    }
+
+    @Nullable OperationEntity get() {
+        return get;
+    }
+
+    @Nullable OperationEntity delete() {
+        return delete;
     }
 }
index 40aa3f578c5d4101311ccd59386b342b47438ec0..ac1783e987a83a466fca3656a4d7cf3427ee932d 100644 (file)
@@ -7,31 +7,43 @@
  */
 package org.opendaylight.restconf.openapi.model;
 
+import static javax.ws.rs.core.Response.Status.CREATED;
 import static javax.ws.rs.core.Response.Status.NO_CONTENT;
 import static javax.ws.rs.core.Response.Status.OK;
 import static org.opendaylight.restconf.openapi.impl.DefinitionGenerator.OUTPUT_SUFFIX;
 
 import com.fasterxml.jackson.core.JsonGenerator;
 import java.io.IOException;
+import java.util.List;
 import javax.ws.rs.HttpMethod;
 import javax.ws.rs.core.MediaType;
 import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
+import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.DocumentedNode;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.Module;
 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
-import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
+import org.opendaylight.yangtools.yang.model.api.SchemaNode;
 
 public final class PostEntity extends OperationEntity {
-    private static final String SUMMARY_TEMPLATE = "%s - %s - %s - %s";
+
+    private final DocumentedNode parentNode;
     private static final String INPUT_SUFFIX = "_input";
     private static final String INPUT_KEY = "input";
-    private static final String OBJECT = "object";
-    private static final String SCHEMA = "schema";
-    private static final String TYPE = "type";
-    private static final String DESCRIPTION = "description";
-    private static final String COMPONENTS_PREFIX = "#/components/schemas/";
-
-    public PostEntity(final OperationDefinition schema, final String deviceName, final String moduleName) {
-        super(schema, deviceName, moduleName);
+    private static final String POST_DESCRIPTION = """
+        \n
+        Note:
+        In example payload, you can see only the first data node child of the resource to be created, following the
+        guidelines of RFC 8040, which allows us to create only one resource in POST request.
+        """;
+
+    public PostEntity(final SchemaNode schema, final String deviceName, final String moduleName,
+            final List<ParameterEntity> parameters, final String refPath, final DocumentedNode parentNode) {
+        super(schema, deviceName, moduleName, parameters, refPath);
+        this.parentNode = parentNode;
     }
 
     protected String operation() {
@@ -39,14 +51,20 @@ public final class PostEntity extends OperationEntity {
     }
 
     @Nullable String summary() {
-        final var operationName = schema().getQName().getLocalName() + INPUT_SUFFIX;
-        return SUMMARY_TEMPLATE.formatted(HttpMethod.POST, deviceName(), moduleName(), operationName);
+        if (parentNode instanceof Module) {
+            return SUMMARY_TEMPLATE.formatted(HttpMethod.POST, deviceName(), moduleName(), moduleName());
+        }
+        if (parentNode != null) {
+            return SUMMARY_TEMPLATE.formatted(HttpMethod.POST, deviceName(), moduleName(),
+                ((DataSchemaNode) parentNode).getQName().getLocalName());
+        }
+        return SUMMARY_TEMPLATE.formatted(HttpMethod.POST, deviceName(), moduleName(), nodeName());
     }
 
     @Override
     void generateResponses(final @NonNull JsonGenerator generator) throws IOException {
-        if (schema() instanceof RpcDefinition rpc) {
-            generator.writeObjectFieldStart("responses");
+        generator.writeObjectFieldStart(RESPONSES);
+        if (schema() instanceof OperationDefinition rpc) {
             final var output = rpc.getOutput();
             final var operationName = rpc.getQName().getLocalName();
             if (!output.getChildNodes().isEmpty()) {
@@ -55,7 +73,7 @@ public final class PostEntity extends OperationEntity {
                 generator.writeObjectFieldStart(String.valueOf(OK.getStatusCode()));
                 generator.writeStringField(DESCRIPTION, String.format("RPC %s success", operationName));
 
-                generator.writeObjectFieldStart("content");
+                generator.writeObjectFieldStart(CONTENT);
                 generateMediaTypeSchemaRef(generator, MediaType.APPLICATION_JSON, ref);
                 generateMediaTypeSchemaRef(generator, MediaType.APPLICATION_XML, ref);
                 generator.writeEndObject();
@@ -68,18 +86,22 @@ public final class PostEntity extends OperationEntity {
                 generator.writeEndObject();
 
             }
+        } else {
+            generator.writeObjectFieldStart(String.valueOf(CREATED.getStatusCode()));
+            generator.writeStringField(DESCRIPTION, "Created");
             generator.writeEndObject();
         }
+        generator.writeEndObject();
     }
 
     @Override
     void generateRequestBody(final @NonNull JsonGenerator generator) throws IOException {
-        if (schema() instanceof RpcDefinition rpc) {
-            generator.writeObjectFieldStart("requestBody");
+        generator.writeObjectFieldStart(REQUEST_BODY);
+        if (schema() instanceof OperationDefinition rpc) {
             final var input = rpc.getInput();
             final var operationName = rpc.getQName().getLocalName();
             generator.writeStringField(DESCRIPTION, operationName + INPUT_SUFFIX);
-            generator.writeObjectFieldStart("content");
+            generator.writeObjectFieldStart(CONTENT);
             if (!input.getChildNodes().isEmpty()) {
                 // TODO: add proper discriminator from DefinitionNames when schemas re-implementation is done
                 final var ref = COMPONENTS_PREFIX + moduleName() + "_" + operationName + INPUT_SUFFIX;
@@ -98,7 +120,7 @@ public final class PostEntity extends OperationEntity {
                 generator.writeObjectFieldStart(MediaType.APPLICATION_JSON);
                 generator.writeObjectFieldStart(SCHEMA);
 
-                generator.writeObjectFieldStart("properties");
+                generator.writeObjectFieldStart(PROPERTIES);
                 generator.writeObjectFieldStart(INPUT_KEY);
                 generator.writeStringField(TYPE, OBJECT);
                 generator.writeEndObject();
@@ -112,7 +134,7 @@ public final class PostEntity extends OperationEntity {
                 generator.writeObjectFieldStart(SCHEMA);
 
                 generator.writeObjectFieldStart("xml");
-                generator.writeStringField("name", INPUT_KEY);
+                generator.writeStringField(NAME, INPUT_KEY);
                 generator.writeStringField("namespace", input.getQName().getNamespace().toString());
                 generator.writeEndObject();
 
@@ -121,16 +143,70 @@ public final class PostEntity extends OperationEntity {
                 generator.writeEndObject();
             }
             generator.writeEndObject();
+        } else {
+            generator.writeStringField(DESCRIPTION, nodeName());
+            generator.writeObjectFieldStart(CONTENT);
+            final var ref = COMPONENTS_PREFIX + moduleName() + "_" + refPath();
+            //TODO: Remove if and fix this weird logic of top level nodes
+            var childConfig = true;
+            if (schema() instanceof DataNodeContainer dataSchema) {
+                final var child = dataSchema.getChildNodes().stream()
+                    .filter(n -> n instanceof ListSchemaNode || n instanceof ContainerSchemaNode)
+                    .findFirst().orElse(null);
+                if (child != null) {
+                    childConfig = child.isConfiguration();
+                }
+            }
+            if (parentNode == null && !childConfig) {
+                generateMediaTypeSchemaRef(generator, MediaType.APPLICATION_JSON, ref);
+            } else {
+                generator.writeObjectFieldStart(MediaType.APPLICATION_JSON);
+                generator.writeObjectFieldStart(SCHEMA);
+
+                generator.writeObjectFieldStart(PROPERTIES);
+                generator.writeObjectFieldStart(nodeName());
+                if (schema() instanceof ListSchemaNode) {
+                    generator.writeStringField(TYPE, ARRAY);
+                    generator.writeObjectFieldStart(ITEMS);
+                    generator.writeStringField(REF, ref);
+                    generator.writeStringField(TYPE, OBJECT);
+                    generator.writeEndObject();
+                } else {
+                    generator.writeStringField(REF, ref);
+                    generator.writeStringField(TYPE, OBJECT);
+                }
+                generator.writeEndObject();
+                generator.writeEndObject();
+                generator.writeEndObject();
+                generator.writeEndObject();
+            }
+            generateMediaTypeSchemaRef(generator, MediaType.APPLICATION_XML, ref);
             generator.writeEndObject();
         }
+        generator.writeEndObject();
     }
 
-    private static void generateMediaTypeSchemaRef(final JsonGenerator generator, final String mediaType,
-            final String ref) throws IOException {
-        generator.writeObjectFieldStart(mediaType);
-        generator.writeObjectFieldStart(SCHEMA);
-        generator.writeStringField("$ref", ref);
-        generator.writeEndObject();
-        generator.writeEndObject();
+
+    @Override
+    public void generateBasics(@NonNull JsonGenerator generator) throws IOException {
+        final var description = description();
+        if (schema() instanceof OperationDefinition) {
+            generator.writeStringField(DESCRIPTION, description);
+        } else {
+            generator.writeStringField(DESCRIPTION, description + POST_DESCRIPTION);
+        }
+        final var summary = summary();
+        if (summary != null) {
+            generator.writeStringField(SUMMARY, summary);
+        }
+    }
+
+    @Override
+    @Nullable String description() {
+        if (parentNode != null) {
+            return parentNode.getDescription().orElse("");
+        } else {
+            return super.description();
+        }
     }
 }
diff --git a/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PutEntity.java b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PutEntity.java
new file mode 100644 (file)
index 0000000..8125098
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.openapi.model;
+
+import static javax.ws.rs.core.Response.Status.CREATED;
+import static javax.ws.rs.core.Response.Status.NO_CONTENT;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import java.io.IOException;
+import java.util.List;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.core.MediaType;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.SchemaNode;
+
+public final class PutEntity extends OperationEntity {
+
+    private final String fullName;
+
+    public PutEntity(final SchemaNode schema, final String deviceName, final String moduleName,
+            final List<ParameterEntity> parameters, final String refPath, final String fullName) {
+        super(schema, deviceName, moduleName, parameters, refPath);
+        this.fullName = fullName;
+    }
+
+    @Override
+    protected String operation() {
+        return "put";
+    }
+
+    @Override
+    @Nullable String summary() {
+        return SUMMARY_TEMPLATE.formatted(HttpMethod.PUT, moduleName(), deviceName(), nodeName());
+    }
+
+    @Override
+    void generateResponses(final @NonNull JsonGenerator generator) throws IOException {
+        generator.writeObjectFieldStart(RESPONSES);
+        generator.writeObjectFieldStart(String.valueOf(CREATED.getStatusCode()));
+        generator.writeStringField(DESCRIPTION, "Created");
+        generator.writeEndObject(); //end of 201
+        generator.writeObjectFieldStart(String.valueOf(NO_CONTENT.getStatusCode()));
+        generator.writeStringField(DESCRIPTION, "Updated");
+        generator.writeEndObject(); //end of 204
+        generator.writeEndObject();
+    }
+
+    @Override
+    void generateRequestBody(final @NonNull JsonGenerator generator) throws IOException {
+        generator.writeObjectFieldStart(REQUEST_BODY);
+        generator.writeStringField(DESCRIPTION, nodeName());
+        generator.writeObjectFieldStart(CONTENT);
+        final var ref = COMPONENTS_PREFIX + moduleName() + "_" + refPath();
+        generator.writeObjectFieldStart(MediaType.APPLICATION_JSON);
+        generator.writeObjectFieldStart(SCHEMA);
+        generator.writeObjectFieldStart(PROPERTIES);
+        generator.writeObjectFieldStart(fullName);
+        if (schema() instanceof ListSchemaNode) {
+            generator.writeStringField(TYPE, ARRAY);
+            generator.writeObjectFieldStart(ITEMS);
+            generator.writeStringField(REF, ref);
+            generator.writeStringField(TYPE, OBJECT);
+            generator.writeEndObject(); //end of items
+        } else {
+            generator.writeStringField(REF, ref);
+            generator.writeStringField(TYPE, OBJECT);
+        }
+        generator.writeEndObject(); //end of moduleName
+        generator.writeEndObject(); //end of props
+        generator.writeEndObject(); //end of schema
+        generator.writeEndObject(); //end of json
+
+        generateMediaTypeSchemaRef(generator, MediaType.APPLICATION_XML, ref);
+        generator.writeEndObject(); //end of content
+        generator.writeEndObject(); //end of request body
+    }
+}