From a8f71679156c10e506923b2ae3ed81c17aaa76bb Mon Sep 17 00:00:00 2001 From: lubos-cicut Date: Mon, 20 Nov 2023 18:17:27 +0100 Subject: [PATCH] Re-implement paths for ChildNodes 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 --- .../openapi/impl/OpenApiInputStream.java | 6 +- .../restconf/openapi/impl/PathsSteam.java | 111 ------ .../restconf/openapi/impl/PathsStream.java | 335 ++++++++++++++++++ .../restconf/openapi/model/DeleteEntity.java | 45 +++ .../restconf/openapi/model/GetEntity.java | 103 ++++++ .../openapi/model/OperationEntity.java | 132 ++++++- .../openapi/model/ParameterEntity.java | 23 ++ .../openapi/model/ParameterSchemaEntity.java | 19 + .../restconf/openapi/model/PatchEntity.java | 82 +++++ .../restconf/openapi/model/PathEntity.java | 33 +- .../restconf/openapi/model/PostEntity.java | 130 +++++-- .../restconf/openapi/model/PutEntity.java | 84 +++++ 12 files changed, 940 insertions(+), 163 deletions(-) delete mode 100644 restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/impl/PathsSteam.java create mode 100644 restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/impl/PathsStream.java create mode 100644 restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/DeleteEntity.java create mode 100644 restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/GetEntity.java create mode 100644 restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/ParameterEntity.java create mode 100644 restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/ParameterSchemaEntity.java create mode 100644 restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PatchEntity.java create mode 100644 restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PutEntity.java diff --git a/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/impl/OpenApiInputStream.java b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/impl/OpenApiInputStream.java index 7d7eea7492..910f8a4c5d 100644 --- a/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/impl/OpenApiInputStream.java +++ b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/impl/OpenApiInputStream.java @@ -40,13 +40,15 @@ public final class OpenApiInputStream extends InputStream { public OpenApiInputStream(final EffectiveModelContext context, final String openApiVersion, final Info info, final List servers, final List>> 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 index 058a14801f..0000000000 --- a/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/impl/PathsSteam.java +++ /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 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 toPaths(final Module module) { - final var result = new ArrayDeque(); - // 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 index 0000000000..adca13602e --- /dev/null +++ b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/impl/PathsStream.java @@ -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 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 toPaths(final Module module) { + final var result = new ArrayDeque(); + 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(); + 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 pathParams, + final String moduleName, final Deque 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 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 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 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 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 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 index 0000000000..709ce1d54d --- /dev/null +++ b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/DeleteEntity.java @@ -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 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 index 0000000000..7273a1e44e --- /dev/null +++ b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/GetEntity.java @@ -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 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); + } +} diff --git a/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/OperationEntity.java b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/OperationEntity.java index a263be0aed..fa7d0abde7 100644 --- a/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/OperationEntity.java +++ b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/OperationEntity.java @@ -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 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 parameters() { + return parameters; + } + + protected String refPath() { + return refPath; + } + + public OperationEntity(final SchemaNode schema, final String deviceName, final String moduleName, + final List 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 index 0000000000..a6f8485ba0 --- /dev/null +++ b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/ParameterEntity.java @@ -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 index 0000000000..463ca0a512 --- /dev/null +++ b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/ParameterSchemaEntity.java @@ -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 schemaEnum) { + public ParameterSchemaEntity(final String type, final List 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 index 0000000000..7e260eb292 --- /dev/null +++ b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PatchEntity.java @@ -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 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 + } +} diff --git a/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PathEntity.java b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PathEntity.java index 2b0174734b..2d5c2ac9ff 100644 --- a/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PathEntity.java +++ b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PathEntity.java @@ -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; } } diff --git a/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PostEntity.java b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PostEntity.java index 40aa3f578c..ac1783e987 100644 --- a/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PostEntity.java +++ b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PostEntity.java @@ -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 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 index 0000000000..812509875e --- /dev/null +++ b/restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/model/PutEntity.java @@ -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 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 + } +} -- 2.36.6