diff --git a/README.md b/README.md index 6c62153..52c057f 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,14 @@ Having the definition you are able to build live documentation services, and gen Our tool supports: * rpc - which are translated into POST operations + * actions - which are translated into POST operations on instance-specific RESTCONF operation paths, analogously to rpc * containers and lists - which are represented in RESTCONF data space URI and Swagger modules. * leafs and leaf lists - that are translated into Swagger models' attributes. Generator handles enums as well. * leafrefs - which are represented as model attributes with typesUsageTreeBuilder of the referred leafs * groupings - which, depending on strategy, are either unpacked into models that use these groupings or optimized model inheritance structures * augmentations - which, depending on strategy, are either unpacked into models that use these groupings or optimized model inheritance structures * YANG modules documentation - which is added to generated swagger API specification + * mount-point - for which types can be provided during swagger generation In this project we use YANG parser from [OpenDaylight](https://www.opendaylight.org/) (ODL) yang-tools project. The generated Swagger specification is available as Java object or serialized either to YAML or JSON file. @@ -76,6 +78,26 @@ module ... : List of YANG module names to generate defaults to current directory. Multiple dirs might be separated by system path separator (default: ) + -mount-point-mappings : Mount-point mappings as a JSON string. + Assigns mount-point labels (from YANG + files) to lists of content types + (can be passed as a specific type or + a whole module) that should be used + when generating Swagger definitions. + Expected format: + '{"mount-label": ["module:grouping", ...]}' + or + '{"mount-label": ["module", ...]}' + Example: + '{"mount-point-name": [ + "mounted-module-name:top-level-container", + "mounted-module-name:top-level-container" + ]}' + -mount-point-mappings-json-file file : JSON file containing mount-point + mappings in the same format as + -mount-point-mappings. Useful when the + mapping is too large to pass directly + on the command line. ``` For example: diff --git a/cli/pom.xml b/cli/pom.xml index 183575e..256089d 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -15,7 +15,7 @@ yangtools com.mrv.yangtools - 2.1.0 + 2.2.0 4.0.0 diff --git a/cli/src/main/java/com/mrv/yangtools/codegen/main/Main.java b/cli/src/main/java/com/mrv/yangtools/codegen/main/Main.java index b0cb20e..c7f8cdd 100644 --- a/cli/src/main/java/com/mrv/yangtools/codegen/main/Main.java +++ b/cli/src/main/java/com/mrv/yangtools/codegen/main/Main.java @@ -9,6 +9,8 @@ package com.mrv.yangtools.codegen.main; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mrv.yangtools.codegen.MountPointMappings; import com.mrv.yangtools.codegen.SwaggerGenerator; import com.mrv.yangtools.codegen.impl.path.AbstractPathHandlerBuilder; import com.mrv.yangtools.codegen.impl.path.odl.ODLPathHandlerBuilder; @@ -29,12 +31,12 @@ import java.io.*; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.nio.file.FileSystems; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; -import java.util.Arrays; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -87,6 +89,12 @@ public class Main { @Option(name = "-basepath", usage="") public String basePath = "localhost:1234"; + @Option(name = "-mount-point-mappings", usage = "Mount point mappings as JSON string, e.g. '{\"mount-point-name\": [\"mounted-module-name\", \"mounted-module-name\"]}'", metaVar = "json") + public String mountPointMappings = ""; + + @Option(name = "-mount-point-mappings-json-file", usage = "JSON file containing mount point mappings, same format as -mount-point-mappings", metaVar = "file") + public String mountPointMappingsJson = ""; + public enum ElementType { DATA, RPC, DATA_AND_RPC } @@ -119,6 +127,10 @@ void init() throws FileNotFoundException { if (output != null && !output.trim().isEmpty()) { out = new FileOutputStream(output); } + + if (isOptionSet(mountPointMappingsJson) && isOptionSet(mountPointMappings)) { + throw new IllegalArgumentException("mount-point-mappings & mount-point-mappings-json cannot be set at the same time"); + } } void generate() throws IOException, ReactorException { @@ -160,7 +172,7 @@ void generate() throws IOException, ReactorException { .pathHandler(pathHandler) .elements(map(elementType)); - + setYangmntMappings(generator); if(AuthenticationMechanism.BASIC.equals(authenticationMechanism)) { generator.appendPostProcessor(new AddSecurityDefinitions().withSecurityDefinition("api_sec", new BasicAuthDefinition())); @@ -184,6 +196,58 @@ void generate() throws IOException, ReactorException { generator.generate(new OutputStreamWriter(out)); } + private void setYangmntMappings(SwaggerGenerator generator) { + + if (isOptionSet(mountPointMappingsJson)) { + MountPointMappings mapping = parseMountPointMappingJson(); + log.debug("Parsed mount-point-mappings from file '{}': {}", mountPointMappingsJson, mapping); + if (!mapping.isEmpty()) { + generator.yangmntMappings(mapping); + } + return; + } + + if (isOptionSet(mountPointMappings)) { + log.debug("Raw mount-point-mappings arg: {}", mountPointMappings); + MountPointMappings mapping = parseMountPointMappings(mountPointMappings); + log.debug("Parsed mount-point-mappings: {}", mapping); + if (mapping != null && !mapping.isEmpty()) { + generator.yangmntMappings(mapping); + } + } + } + + private MountPointMappings parseMountPointMappingJson() { + if (!isOptionSet(mountPointMappingsJson)) { + throw new IllegalArgumentException("mount-point-mappings-json-file is empty"); + } + + Path jsonPath = FileSystems.getDefault().getPath(mountPointMappingsJson); + try { + String raw = new String(Files.readAllBytes(jsonPath), StandardCharsets.UTF_8); + return parseMountPointMappings(raw); + } catch (IOException e) { + log.error("Cannot read mount-point mappings from file: {}", mountPointMappingsJson, e); + throw new IllegalArgumentException("Cannot read mount-point mappings JSON file: " + mountPointMappingsJson, e); + } + } + + private MountPointMappings parseMountPointMappings(String raw) { + ObjectMapper mapper = new ObjectMapper(); + try { + return mapper.readValue(raw, MountPointMappings.class); + } catch (IllegalArgumentException iae) { + throw iae; + } catch (Exception e) { + log.error("Invalid mount-point-mappings format: {}", raw, e); + throw new IllegalArgumentException("Invalid mount-point-mappings format", e); + } + } + + private boolean isOptionSet(String optionName) { + return optionName != null && !optionName.trim().isEmpty(); + } + private void validate(String basePath) { URI.create(basePath); } diff --git a/common/pom.xml b/common/pom.xml index 0aae240..0e13ddd 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -16,7 +16,7 @@ yangtools com.mrv.yangtools - 2.1.0 + 2.2.0 4.0.0 diff --git a/examples/build-standalone/pom.xml b/examples/build-standalone/pom.xml index 7f9af2c..4859ae0 100644 --- a/examples/build-standalone/pom.xml +++ b/examples/build-standalone/pom.xml @@ -15,7 +15,7 @@ examples com.mrv.yangtools - 2.1.0 + 2.2.0 4.0.0 diff --git a/examples/pom.xml b/examples/pom.xml index fce7da0..414f8c5 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -16,7 +16,7 @@ yangtools com.mrv.yangtools - 2.1.0 + 2.2.0 4.0.0 pom diff --git a/pom.xml b/pom.xml index 700fcbd..146b547 100644 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,7 @@ com.mrv.yangtools yangtools - 2.1.0 + 2.2.0 swagger-generator common diff --git a/swagger-codegen-jaxrs/pom.xml b/swagger-codegen-jaxrs/pom.xml index 14fc0f0..9dad739 100644 --- a/swagger-codegen-jaxrs/pom.xml +++ b/swagger-codegen-jaxrs/pom.xml @@ -15,7 +15,7 @@ yangtools com.mrv.yangtools - 2.1.0 + 2.2.0 4.0.0 diff --git a/swagger-generator/pom.xml b/swagger-generator/pom.xml index 78906e5..04c8217 100644 --- a/swagger-generator/pom.xml +++ b/swagger-generator/pom.xml @@ -15,7 +15,7 @@ yangtools com.mrv.yangtools - 2.1.0 + 2.2.0 4.0.0 diff --git a/swagger-generator/src/main/java/com/mrv/yangtools/codegen/IoCSwaggerGenerator.java b/swagger-generator/src/main/java/com/mrv/yangtools/codegen/IoCSwaggerGenerator.java index 006f9e5..a8a3ceb 100644 --- a/swagger-generator/src/main/java/com/mrv/yangtools/codegen/IoCSwaggerGenerator.java +++ b/swagger-generator/src/main/java/com/mrv/yangtools/codegen/IoCSwaggerGenerator.java @@ -19,6 +19,8 @@ import com.mrv.yangtools.codegen.impl.ModuleUtils; import com.mrv.yangtools.codegen.impl.OptimizingDataObjectBuilder; import com.mrv.yangtools.codegen.impl.UnpackingDataObjectsBuilder; +import com.mrv.yangtools.codegen.impl.path.AbstractPathHandlerBuilder; +import com.mrv.yangtools.codegen.impl.postprocessor.MountPointPostProcessor; import com.mrv.yangtools.codegen.impl.postprocessor.ReplaceEmptyWithParent; import io.swagger.models.Info; import io.swagger.models.Swagger; @@ -57,6 +59,7 @@ public class IoCSwaggerGenerator { private final Set moduleNames; private final ModuleUtils moduleUtils; private Consumer postprocessor; + private Consumer mountPointPostProcessor; private DataObjectBuilder dataObjectsBuilder; private ObjectMapper mapper; private int maxDepth = Integer.MAX_VALUE; @@ -114,6 +117,15 @@ public IoCSwaggerGenerator(@Assisted EffectiveModelContext ctx, @Assisted Set targets. + *

+ * Safe to call more than once — replaces any previously registered + * {@link MountPointPostProcessor} instead of appending a second one. + */ + public IoCSwaggerGenerator yangmntMappings(MountPointMappings mappings) { + if(mappings != null && !mappings.isEmpty()) { + this.mountPointPostProcessor = new MountPointPostProcessor(mappings, ctx, moduleUtils, dataObjectsBuilder); + } else { + this.mountPointPostProcessor = null; + } + return this; + } /** * Run Swagger generation for configured modules. Write result to target. The file format @@ -295,6 +322,15 @@ public Swagger generate() { }); //initialize plugable path handler + if(pathHandlerBuilder == null) { + try { + AbstractPathHandlerBuilder defaultBuilder = new com.mrv.yangtools.codegen.impl.path.rfc8040.PathHandlerBuilder(); + defaultBuilder.useModuleName(); + pathHandlerBuilder = defaultBuilder; + } catch (Throwable t) { + throw new IllegalStateException("No PathHandlerBuilder configured and default builder could not be instantiated", t); + } + } pathHandlerBuilder.configure(ctx, target, dataObjectsBuilder); modules.forEach(m -> new ModuleGenerator(m).generate()); @@ -313,7 +349,8 @@ public Swagger generate() { /** * Replace empty definitions with their parents. - * Sort models (ref models first) + * Sort models (ref models first). + * Run mount-point post-processor if configured. * @param target to work on */ protected void postProcessSwagger(Swagger target) { @@ -322,6 +359,9 @@ protected void postProcessSwagger(Swagger target) { return; } postprocessor.accept(target); + if(mountPointPostProcessor != null) { + mountPointPostProcessor.accept(target); + } } private class ModuleGenerator { @@ -361,6 +401,20 @@ private void generate(RpcDefinition rpc) { pathCtx = pathCtx.drop(); } + private void generateActions(ActionNodeContainer node) { + if(!toGenerate.contains(Elements.RPC)) return; + + node.getActions().forEach(action -> { + pathCtx = new PathSegment(pathCtx) + .withName(action.getQName().getLocalName()) + .withModule(moduleUtils.toModuleName(action)); + + handler.path(action, pathCtx); + + pathCtx = pathCtx.drop(); + }); + } + private void generate(DataSchemaNode node, final int depth) { if(depth == 0) { log.debug("Maxmium depth level reached, skipping {} and it's childs", node.getPath()); @@ -382,6 +436,7 @@ private void generate(DataSchemaNode node, final int depth) { .asReadOnly(!cN.isConfiguration()); handler.path(cN, pathCtx); + generateActions(cN); cN.getChildNodes().forEach(n -> generate(n, depth-1)); dataObjectsBuilder.addModel(cN); @@ -397,6 +452,7 @@ private void generate(DataSchemaNode node, final int depth) { .withListNode(lN); handler.path(lN, pathCtx); + generateActions(lN); lN.getChildNodes().forEach(n -> generate(n, depth-1)); dataObjectsBuilder.addModel(lN); diff --git a/swagger-generator/src/main/java/com/mrv/yangtools/codegen/MountPointMappings.java b/swagger-generator/src/main/java/com/mrv/yangtools/codegen/MountPointMappings.java new file mode 100644 index 0000000..7fe4070 --- /dev/null +++ b/swagger-generator/src/main/java/com/mrv/yangtools/codegen/MountPointMappings.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2026. MRV Communications, Inc. 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 + * Contributors: + * Christopher Murch + * Bartosz Michalik + */ + +package com.mrv.yangtools.codegen; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Concrete binding for mount-point mappings. + */ +public class MountPointMappings { + private final Map> mappings; + + @JsonCreator + public static MountPointMappings fromMap(Map input) { + if (input == null) return new MountPointMappings(Collections.emptyMap()); + Map> result = new LinkedHashMap<>(); + for (Map.Entry entry : input.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + if (value instanceof List) { + List targets = new ArrayList<>(); + for (Object item : (List) value) { + targets.add(MountPointTarget.fromJson(item)); + } + result.put(key, targets); + } else if (value != null) { + result.put(key, Collections.singletonList(MountPointTarget.fromJson(value))); + } + } + return new MountPointMappings(result); + } + + public MountPointMappings(Map> mappings) { + if (mappings == null) { + this.mappings = Collections.emptyMap(); + } else { + Map> copy = new LinkedHashMap<>(); + for (Map.Entry> entry : mappings.entrySet()) { + copy.put(entry.getKey(), Collections.unmodifiableList(new ArrayList<>(entry.getValue()))); + } + this.mappings = Collections.unmodifiableMap(copy); + } + } + + @JsonValue + public Map> asMap() { + return mappings; + } + + public List getTargets(String label) { + return mappings.getOrDefault(label, Collections.emptyList()); + } + + public boolean isEmpty() { + return mappings.isEmpty(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MountPointMappings that = (MountPointMappings) o; + return Objects.equals(mappings, that.mappings); + } + + @Override + public int hashCode() { + return Objects.hash(mappings); + } + + @Override + public String toString() { + return mappings.toString(); + } +} diff --git a/swagger-generator/src/main/java/com/mrv/yangtools/codegen/MountPointTarget.java b/swagger-generator/src/main/java/com/mrv/yangtools/codegen/MountPointTarget.java new file mode 100644 index 0000000..318dfa7 --- /dev/null +++ b/swagger-generator/src/main/java/com/mrv/yangtools/codegen/MountPointTarget.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2026. MRV Communications, Inc. 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 + * Contributors: + * Christopher Murch + * Bartosz Michalik + */ + +package com.mrv.yangtools.codegen; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.Map; +import java.util.Objects; + +/** + * Represents a target for a mount-point: a module and a name (grouping, container or list). + */ +public class MountPointTarget { + private final String module; + private final String name; + + public MountPointTarget(String module, String name) { + this.module = module; + this.name = Objects.requireNonNull(name, "name is required"); + } + + public String getModule() { + return module; + } + + public String getName() { + return name; + } + + @JsonCreator + public static MountPointTarget fromJson(Object value) { + if (value instanceof String) { + return parse((String) value); + } else if (value instanceof Map) { + Map map = (Map) value; + Object moduleObj = map.get("module"); + Object nameObj = map.get("name"); + if (nameObj == null) { + throw new IllegalArgumentException("Mapping target name is required"); + } + String module = moduleObj != null ? moduleObj.toString().trim() : null; + String name = nameObj.toString().trim(); + if (name.isEmpty()) { + throw new IllegalArgumentException("Mapping target name cannot be empty"); + } + return new MountPointTarget(module != null && module.isEmpty() ? null : module, name); + } + throw new IllegalArgumentException("Invalid mapping target value: " + value); + } + + public static MountPointTarget parse(String value) { + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException("Mapping value cannot be empty"); + } + String[] parts = value.split(":", 2); + if (parts.length == 2) { + String module = parts[0].trim(); + String name = parts[1].trim(); + if (name.isEmpty()) { + throw new IllegalArgumentException("Mapping target name cannot be empty"); + } + return new MountPointTarget(module.isEmpty() ? null : module, name); + } else { + String name = parts[0].trim(); + if (name.isEmpty()) { + throw new IllegalArgumentException("Mapping target name cannot be empty"); + } + return new MountPointTarget(null, name); + } + } + + @Override + @JsonValue + public String toString() { + return (module != null ? module + ":" : "") + name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MountPointTarget that = (MountPointTarget) o; + return Objects.equals(module, that.module) && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(module, name); + } +} diff --git a/swagger-generator/src/main/java/com/mrv/yangtools/codegen/PathHandler.java b/swagger-generator/src/main/java/com/mrv/yangtools/codegen/PathHandler.java index 37a4216..3592d2d 100644 --- a/swagger-generator/src/main/java/com/mrv/yangtools/codegen/PathHandler.java +++ b/swagger-generator/src/main/java/com/mrv/yangtools/codegen/PathHandler.java @@ -13,6 +13,7 @@ import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode; import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ActionDefinition; import org.opendaylight.yangtools.yang.model.api.RpcDefinition; /** @@ -22,4 +23,5 @@ public interface PathHandler { void path(ContainerSchemaNode node, PathSegment path); void path(ListSchemaNode node, PathSegment path); void path(RpcDefinition rpc, PathSegment path); + void path(ActionDefinition action, PathSegment path); } diff --git a/swagger-generator/src/main/java/com/mrv/yangtools/codegen/SwaggerGenerator.java b/swagger-generator/src/main/java/com/mrv/yangtools/codegen/SwaggerGenerator.java index a9704f6..493a666 100644 --- a/swagger-generator/src/main/java/com/mrv/yangtools/codegen/SwaggerGenerator.java +++ b/swagger-generator/src/main/java/com/mrv/yangtools/codegen/SwaggerGenerator.java @@ -17,6 +17,8 @@ import com.mrv.yangtools.codegen.impl.ModuleUtils; import com.mrv.yangtools.codegen.impl.OptimizingDataObjectBuilder; import com.mrv.yangtools.codegen.impl.UnpackingDataObjectsBuilder; +import com.mrv.yangtools.codegen.impl.path.AbstractPathHandlerBuilder; +import com.mrv.yangtools.codegen.impl.postprocessor.MountPointPostProcessor; import com.mrv.yangtools.codegen.impl.postprocessor.ReplaceEmptyWithParent; import com.mrv.yangtools.codegen.impl.postprocessor.SortComplexModels; import com.mrv.yangtools.common.SwaggerUtils; @@ -58,6 +60,7 @@ public class SwaggerGenerator { private final Set moduleNames; private final ModuleUtils moduleUtils; private Consumer postprocessor; + private Consumer mountPointPostProcessor; private DataObjectBuilder dataObjectsBuilder; private ObjectMapper mapper; private int maxDepth = Integer.MAX_VALUE; @@ -66,6 +69,7 @@ public class SwaggerGenerator { private Set toGenerate; private final AnnotatingTypeConverter converter; private PathHandlerBuilder pathHandlerBuilder; + private MountPointMappings yangmntMappings; public SwaggerGenerator defaultConfig() { //setting defaults @@ -126,10 +130,17 @@ public SwaggerGenerator(EffectiveModelContext ctx, Collection targets. + *

+ * Safe to call more than once — replaces any previously registered + * {@link MountPointPostProcessor} instead of appending a second one. + */ + public SwaggerGenerator yangmntMappings(MountPointMappings mappings) { + this.yangmntMappings = mappings; + if(yangmntMappings != null && !yangmntMappings.isEmpty()) { + this.mountPointPostProcessor = new MountPointPostProcessor(yangmntMappings, ctx, moduleUtils, dataObjectsBuilder); + } else { + this.mountPointPostProcessor = null; + } + return this; + } + + /** + * Provide mappings for mount-point extension: label -> targets (deprecated, use MountPointMappings) + */ + @Deprecated + public SwaggerGenerator yangmntMappings(Map> mappings) { + return yangmntMappings(new MountPointMappings(mappings)); + } /** * Run Swagger generation for configured modules. Write result to target. The file format @@ -307,6 +342,14 @@ public Swagger generate() { }); //initialize plugable path handler + if(pathHandlerBuilder == null) { + try { + AbstractPathHandlerBuilder defaultBuilder = new com.mrv.yangtools.codegen.impl.path.rfc8040.PathHandlerBuilder(); + pathHandlerBuilder = defaultBuilder; + } catch (Throwable t) { + throw new IllegalStateException("No PathHandlerBuilder configured and default builder could not be instantiated", t); + } + } pathHandlerBuilder.configure(ctx, target, dataObjectsBuilder); modules.forEach(m -> new ModuleGenerator(m).generate()); @@ -329,7 +372,8 @@ public Swagger generate() { /** * Replace empty definitions with their parents. - * Sort models (ref models first) + * Sort models (ref models first). + * Run mount-point post-processor if configured. * @param target to work on */ protected void postProcessSwagger(Swagger target) { @@ -338,6 +382,13 @@ protected void postProcessSwagger(Swagger target) { return; } postprocessor.accept(target); + if(mountPointPostProcessor != null) { + mountPointPostProcessor.accept(target); + // Re-run removal of unused definitions after mount-point processing, + // because mount-point expansion may replace raw grouping refs with *Wrapper + // refs, leaving the original grouping definitions orphaned. + new com.mrv.yangtools.codegen.impl.postprocessor.RemoveUnusedDefinitions().accept(target); + } } private class ModuleGenerator { @@ -377,6 +428,20 @@ private void generate(RpcDefinition rpc) { pathCtx = pathCtx.drop(); } + private void generateActions(ActionNodeContainer node) { + if(!toGenerate.contains(Elements.RPC)) return; + + node.getActions().forEach(action -> { + pathCtx = new PathSegment(pathCtx) + .withName(action.getQName().getLocalName()) + .withModule(moduleUtils.toModuleName(action)); + + handler.path(action, pathCtx); + + pathCtx = pathCtx.drop(); + }); + } + private void generate(DataSchemaNode node, final int depth) { if(depth == 0) { log.debug("Maximum depth level reached, skipping {} and it's childs", node.getPath()); @@ -398,6 +463,7 @@ private void generate(DataSchemaNode node, final int depth) { .asReadOnly(!cN.isConfiguration()); handler.path(cN, pathCtx); + generateActions(cN); cN.getChildNodes().forEach(n -> generate(n, depth-1)); dataObjectsBuilder.addModel(cN); @@ -413,6 +479,7 @@ private void generate(DataSchemaNode node, final int depth) { .withListNode(lN); handler.path(lN, pathCtx); + generateActions(lN); lN.getChildNodes().forEach(n -> generate(n, depth-1)); dataObjectsBuilder.addModel(lN); diff --git a/swagger-generator/src/main/java/com/mrv/yangtools/codegen/impl/path/AbstractPathHandler.java b/swagger-generator/src/main/java/com/mrv/yangtools/codegen/impl/path/AbstractPathHandler.java index 8597abb..10114ee 100644 --- a/swagger-generator/src/main/java/com/mrv/yangtools/codegen/impl/path/AbstractPathHandler.java +++ b/swagger-generator/src/main/java/com/mrv/yangtools/codegen/impl/path/AbstractPathHandler.java @@ -54,15 +54,22 @@ public PathHandler useModuleName(boolean use) { @Override public void path(RpcDefinition rpc, PathSegment pathCtx) { - InputSchemaNode input = rpc.getInput(); - OutputSchemaNode output = rpc.getOutput(); - ContainerLike root = ContainerSchemaNodes.forRPC(rpc); - + generateOperation(rpc, pathCtx, dataObjectBuilder.getName(ContainerSchemaNodes.forRPC(rpc))); + } + + @Override + public void path(ActionDefinition action, PathSegment pathCtx) { + generateOperation(action, pathCtx, action.getQName().getLocalName()); + } + + private void generateOperation(OperationDefinition operationDef, PathSegment pathCtx, String operationName) { + InputSchemaNode input = operationDef.getInput(); + OutputSchemaNode output = operationDef.getOutput(); + input = input.getChildNodes().isEmpty() ? null : input; output = output.getChildNodes().isEmpty() ? null : output; - - PathPrinter printer = getPrinter(pathCtx); + PathPrinter printer = getPrinter(pathCtx); Operation post = defaultOperation(pathCtx); post.tag(module.getName()); @@ -72,8 +79,8 @@ public void path(RpcDefinition rpc, PathSegment pathCtx) { ModelImpl inputModel = new ModelImpl().type(ModelImpl.OBJECT); inputModel.addProperty("input", new RefProperty(dataObjectBuilder.getDefinitionRef(input))); - post.summary("operates on " + dataObjectBuilder.getName(root)); - post.description("operates on " + dataObjectBuilder.getName(root)); + post.summary("operates on " + operationName); + post.description("operates on " + operationName); post.parameter(new BodyParameter() .name(dataObjectBuilder.getName(input) + ".body-param") .schema(inputModel) diff --git a/swagger-generator/src/main/java/com/mrv/yangtools/codegen/impl/postprocessor/EffectiveStatementReflectionHelper.java b/swagger-generator/src/main/java/com/mrv/yangtools/codegen/impl/postprocessor/EffectiveStatementReflectionHelper.java new file mode 100644 index 0000000..d09d8f8 --- /dev/null +++ b/swagger-generator/src/main/java/com/mrv/yangtools/codegen/impl/postprocessor/EffectiveStatementReflectionHelper.java @@ -0,0 +1,241 @@ +package com.mrv.yangtools.codegen.impl.postprocessor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Package-private helper that encapsulates all reflection-based logic for discovering + * mount-point extension labels from ODL effective-statement objects. + * + *

ODL's YANG model APIs are partially sealed / non-public, so this class probes + * the effective-statement tree through a series of fallbacks:

+ *
    + *
  1. Resolve the effective statement via {@code asEffectiveStatement()} or + * {@code getEffectiveStatement()} (public, then declared).
  2. + *
  3. Collect all Collection/Map/array-typed members from the effective statement + * (public methods → declared methods → declared fields).
  4. + *
  5. For each item whose {@code toString()} contains a mount-point token, extract + * the label via: + *
      + *
    • Public getter ({@code getArgument}, {@code getName}, …)
    • + *
    • Declared getter (same names, with {@code setAccessible})
    • + *
    • Regex parse of the toString representation
    • + *
    + *
  6. + *
+ * + *

Extracting this logic into its own class makes it possible to unit-test the + * reflection chain in isolation — without standing up a full + * {@code EffectiveModelContext} — by passing plain stub objects.

+ */ +class EffectiveStatementReflectionHelper { + + private static final Logger log = LoggerFactory.getLogger(EffectiveStatementReflectionHelper.class); + + static final String[] EFFECTIVE_STATEMENT_METHODS = {"asEffectiveStatement", "getEffectiveStatement"}; + static final String[] ARG_METHODS = {"getArgument", "getArg", "getValue", "getArgumentValue", "getLabel", "getName"}; + static final Pattern MOUNT_POINT_PATTERN = Pattern.compile("mount[-_]point\\s+([a-zA-Z0-9_-]+)"); + + private boolean declaredAccessRestricted = false; + + /** + * Attempts to discover the mount-point extension label for the given schema node + * by reflectively inspecting its effective statement. + * + * @param node a YANG schema node (e.g. {@code DataSchemaNode}, {@code GroupingDefinition}) + * @return the mount-point label, or {@code null} if none found + */ + String findMountPointLabel(Object node) { + if (node == null) return null; + try { + Object eff = resolveEffectiveStatement(node); + if (eff == null) return null; + + List> candidateCols = new ArrayList<>(); + collectCollectionLikeMembersFromMethods(eff, candidateCols, false); + collectCollectionLikeMembersFromMethods(eff, candidateCols, true); + collectCollectionLikeMembersFromFields(eff, candidateCols); + + // iterate collected sub-statement collections and look for mount-point tokens + for (Collection col : candidateCols) { + if (col == null) continue; + for (Object item : col) { + if (item == null) continue; + String text = item.toString().toLowerCase(); + if (text.contains("mount-point") || text.contains("yangmnt:mount-point") || text.contains("yangmnt:mount_point")) { + // try to get argument via public methods on item + String arg = tryGetStringProperty(item, ARG_METHODS); + if (arg != null && !arg.isEmpty()) return arg; + + // try declared methods as fallback for non-public item types + String declaredArg = tryGetStringPropertyDeclared(item, ARG_METHODS); + if (declaredArg != null && !declaredArg.isEmpty()) return declaredArg; + + // fallback: try to parse token after 'mount-point' in toString + int idx = text.indexOf("mount-point"); + if (idx >= 0) { + String after = text.substring(idx); + Matcher mm = MOUNT_POINT_PATTERN.matcher(after); + if (mm.find()) return mm.group(1); + } + } + } + } + } catch (java.lang.reflect.InaccessibleObjectException iae) { + log.trace("Reflective access to effective statement blocked for node class {}: {}", node.getClass(), iae.toString()); + declaredAccessRestricted = true; + return null; + } catch (Exception e) { + log.debug("Error while inspecting node for mount-point: {}", e.toString()); + } + return null; + } + + Object resolveEffectiveStatement(Object node) { + for (String methodName : EFFECTIVE_STATEMENT_METHODS) { + Object eff = invokePublicNoArg(node, methodName); + if (eff != null) return eff; + } + if (declaredAccessRestricted) return null; + for (String methodName : EFFECTIVE_STATEMENT_METHODS) { + Object eff = invokeDeclaredNoArg(node, methodName); + if (eff != null) return eff; + if (declaredAccessRestricted) break; + } + return null; + } + + Object invokePublicNoArg(Object target, String methodName) { + try { + Method m = target.getClass().getMethod(methodName); + return m.invoke(target); + } catch (NoSuchMethodException e) { + return null; + } catch (IllegalAccessException | InvocationTargetException | RuntimeException e) { + log.trace("Cannot invoke public method {} on {}: {}", methodName, target.getClass(), e.toString()); + return null; + } + } + + Object invokeDeclaredNoArg(Object target, String methodName) { + try { + Method method = target.getClass().getDeclaredMethod(methodName); + method.setAccessible(true); + return method.invoke(target); + } catch (NoSuchMethodException e) { + return null; + } catch (IllegalAccessException | InvocationTargetException | RuntimeException e) { + log.trace("Declared access to {} blocked for {}: {}", methodName, target.getClass(), e.toString()); + declaredAccessRestricted = true; + return null; + } + } + + void collectCollectionLikeMembersFromMethods(Object eff, List> candidateCols, boolean declared) { + if (declared && declaredAccessRestricted) return; + Method[] methods = declared ? eff.getClass().getDeclaredMethods() : eff.getClass().getMethods(); + for (Method method : methods) { + try { + Class rt = method.getReturnType(); + if (!(Collection.class.isAssignableFrom(rt) || Map.class.isAssignableFrom(rt) || rt.isArray())) { + continue; + } + method.setAccessible(true); + Object res = method.invoke(eff); + addPossibleCollection(candidateCols, res); + } catch (IllegalAccessException | InvocationTargetException e) { + if (declared) { + log.trace("Declared method invocation blocked for {}#{}: {}", eff.getClass(), method.getName(), e.toString()); + declaredAccessRestricted = true; + break; + } + log.trace("Public method invocation blocked for {}#{}: {}", eff.getClass(), method.getName(), e.toString()); + declaredAccessRestricted = true; + } catch (Exception e) { + // ignore this method + } + } + } + + void collectCollectionLikeMembersFromFields(Object eff, List> candidateCols) { + if (declaredAccessRestricted) return; + for (Field f : eff.getClass().getDeclaredFields()) { + try { + Class ft = f.getType(); + if (!(Collection.class.isAssignableFrom(ft) || Map.class.isAssignableFrom(ft) || ft.isArray())) { + continue; + } + f.setAccessible(true); + Object res = f.get(eff); + addPossibleCollection(candidateCols, res); + } catch (IllegalAccessException e) { + log.trace("Declared field access blocked for {}#{}: {}", eff.getClass(), f.getName(), e.toString()); + declaredAccessRestricted = true; + break; + } catch (Exception e) { + // ignore + } + } + } + + void addPossibleCollection(List> candidateCols, Object obj) { + if (obj == null) return; + if (obj instanceof Collection) { + candidateCols.add((Collection) obj); + } else if (obj instanceof Map) { + Map map = (Map) obj; + for (Object v : map.values()) { + if (v instanceof Collection) { + candidateCols.add((Collection) v); + } else { + candidateCols.add(Collections.singletonList(v)); + } + } + } else if (obj.getClass().isArray()) { + Object[] arr = (Object[]) obj; + candidateCols.add(Arrays.asList(arr)); + } else { + candidateCols.add(Collections.singletonList(obj)); + } + } + + String tryGetStringProperty(Object obj, String[] candidates) { + for (String name : candidates) { + try { + Method m = obj.getClass().getMethod(name); + Object v = m.invoke(obj); + if (v != null) return v.toString(); + } catch (Exception e) { + // ignore + } + } + return null; + } + + String tryGetStringPropertyDeclared(Object obj, String[] candidates) { + if (declaredAccessRestricted) return null; + for (String name : candidates) { + try { + Method m = obj.getClass().getDeclaredMethod(name); + m.setAccessible(true); + Object v = m.invoke(obj); + if (v != null) return v.toString(); + } catch (IllegalAccessException e) { + log.trace("Declared access blocked for item method {} of {}: {}", name, obj.getClass(), e.toString()); + declaredAccessRestricted = true; + return null; + } catch (Exception e) { + // ignore + } + } + return null; + } +} + diff --git a/swagger-generator/src/main/java/com/mrv/yangtools/codegen/impl/postprocessor/MountPointPostProcessor.java b/swagger-generator/src/main/java/com/mrv/yangtools/codegen/impl/postprocessor/MountPointPostProcessor.java new file mode 100644 index 0000000..f91af69 --- /dev/null +++ b/swagger-generator/src/main/java/com/mrv/yangtools/codegen/impl/postprocessor/MountPointPostProcessor.java @@ -0,0 +1,946 @@ +package com.mrv.yangtools.codegen.impl.postprocessor; + +import io.swagger.models.Model; +import io.swagger.models.ModelImpl; +import io.swagger.models.ComposedModel; +import io.swagger.models.RefModel; +import io.swagger.models.Swagger; +import io.swagger.models.Path; +import io.swagger.models.Response; +import io.swagger.models.Operation; +import io.swagger.models.parameters.BodyParameter; +import io.swagger.models.parameters.Parameter; +import io.swagger.models.parameters.PathParameter; +import io.swagger.models.properties.Property; +import io.swagger.models.properties.RefProperty; +import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; +import com.mrv.yangtools.codegen.impl.ModuleUtils; +import com.mrv.yangtools.codegen.impl.DataNodeHelper; +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; +import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; + +import java.util.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.mrv.yangtools.codegen.DataObjectRepo; +import com.mrv.yangtools.codegen.DataObjectBuilder; +import com.mrv.yangtools.codegen.SwaggerGenerator; +import com.mrv.yangtools.codegen.MountPointTarget; +import com.mrv.yangtools.codegen.MountPointMappings; +import org.opendaylight.yangtools.yang.model.api.GroupingDefinition; +import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.Module; +import org.opendaylight.yangtools.yang.model.api.RpcDefinition; +import org.opendaylight.yangtools.yang.model.api.InputSchemaNode; +import org.opendaylight.yangtools.yang.model.api.OutputSchemaNode; +import org.opendaylight.yangtools.yang.data.util.ContainerSchemaNodes; + +/** + * Lightweight postprocessor that attaches mount definitions to target models. + */ +public class MountPointPostProcessor implements java.util.function.Consumer { + private static final Logger log = LoggerFactory.getLogger(MountPointPostProcessor.class); + + /** Encapsulates all reflection-based probing of ODL effective-statement internals. */ + private final EffectiveStatementReflectionHelper reflectionHelper = new EffectiveStatementReflectionHelper(); + + private final MountPointMappings mappings; + private final EffectiveModelContext ctx; + private final ModuleUtils moduleUtils; + private final DataObjectRepo dataRepo; + private final Map moduleSwaggerCache = new HashMap<>(); + + public MountPointPostProcessor(MountPointMappings mappings, EffectiveModelContext ctx, ModuleUtils moduleUtils, DataObjectRepo dataRepo) { + this.mappings = mappings == null ? new MountPointMappings(Collections.emptyMap()) : mappings; + this.ctx = ctx; + this.moduleUtils = moduleUtils; + this.dataRepo = dataRepo; + } + + // Collect mount-point labels across all modules and return mapping label -> list of DataNodeContainer nodes + private Map> getMountPointFromModules() { + Map> nodesByLabel = new HashMap<>(); + ctx.getModules() + .forEach(module -> collectMountPointsFromContainer(module, nodesByLabel)); + return nodesByLabel; + } + + // Scan provided container (module/container/list) for nodes and try to find mount-point label on each + private void collectMountPointsFromContainer(DataNodeContainer container, Map> nodesByLabel) { + log.debug("Scanning container for mount-points: {}", container); + + // DataNodeHelper.stream(container) yields schema nodes (recursively); check each for mount-point extension + DataNodeHelper.stream(container) + .forEach(node -> { + String label = findMountPointLabel(node); + if (label != null && !label.isEmpty()) { + log.info("Found mount-point label '{}' in node {}", label, node); + // ensure we store the container node (only Container/List/Module/Grouping are DataNodeContainer) + if (node instanceof DataNodeContainer) { + nodesByLabel.computeIfAbsent(label, k -> new ArrayList<>()).add((DataNodeContainer) node); + } else { + // if not a container, associate with the parent container passed to this method + nodesByLabel.computeIfAbsent(label, k -> new ArrayList<>()).add(container); + } + } + }); + } + + @Override + public void accept(Swagger swagger) { + if(swagger.getDefinitions() == null || swagger.getDefinitions().isEmpty()) return; + + // build a set of candidate refs based on provided module:grouping strings + Map candidateDefs = buildCandidateDefinitions(swagger); + + // collect nodes by mount-point label + Map> nodesByLabel = getMountPointFromModules(); + + if(nodesByLabel.isEmpty()) { + log.info("No mount-point extensions found in model"); + return; + } + + // for each found mount label, look up CLI mapping and attach definitions to corresponding swagger models + for(Map.Entry> entry : nodesByLabel.entrySet()) { + String label = entry.getKey(); + List targets = mappings.getTargets(label); + if(targets.isEmpty()) continue; + + // resolve mapped module:grouping entries to definition refs using context and dataRepo + List refModels = new ArrayList<>(); + for(MountPointTarget target : targets) { + // module-only case: module is null, check if name resolves to a known module + if(target.getModule() == null) { + Optional mod = findModuleByName(target.getName()); + if(mod.isPresent()) { + // Attach data paths first so that the standalone swagger (with Wrapper types) + // is generated and merged into the main swagger before we resolve grouping refs. + attachModuleRpcsToMount(mod.get(), entry.getValue(), swagger); + attachModuleDataPathsToMount(mod.get(), entry.getValue(), swagger); + // gather refs from the whole module (Wrapper types now exist in swagger) + List moduleRefs = resolveModuleMappings(mod.get().getName(), swagger); + addUniqueRefModels(refModels, moduleRefs); + continue; + } else { + throw new IllegalArgumentException(target.getName() + " does not exist, check Your configuration & spelling"); + } + } + + String modulePart = target.getModule() != null ? target.getModule() : ""; + String namePart = target.getName(); + + // If mapping explicitly references a module (module:...), ensure RPCs from that module are attached to this mount + if(!modulePart.isEmpty()) { + Optional moduleRef = findModuleByName(modulePart); + if(moduleRef.isPresent()) { + try { + attachModuleRpcsToMount(moduleRef.get(), entry.getValue(), swagger); + attachModuleDataPathsToMount(moduleRef.get(), entry.getValue(), swagger); + } catch (Exception e) { + log.debug("Attaching RPCs for module {} failed: {}", modulePart, e.toString()); + } + } + } + + String defRef = null; + Object resolvedNode = null; + // Three-tier fallback resolution order: + // 1) Exact match on grouping QName local name (and optional module). + // 2) Exact match on top-level container/list QName local name (and optional module). + // 3) Fuzzy substring match against all existing swagger definition keys. + // WARNING: tier 3 uses a substring (contains) check, so a target named e.g. + // "config" would also match "specific-config". This may silently resolve + // to the wrong definition. If you see unexpected mount-point attachments, + // check the WARN log emitted when this heuristic fires. + + // 1) try groupings - exact QName match + for(GroupingDefinition g : ctx.getGroupings()) { + String gLocal = g.getQName().getLocalName(); + String gModule = moduleUtils.toModuleName(g); + if(gLocal.equals(namePart) && (modulePart.isEmpty() || gModule.equals(modulePart))) { + try { defRef = dataRepo.getDefinitionRef(g); resolvedNode = g; } catch (Exception e) { defRef = null; resolvedNode = null; } + if(defRef != null) break; + } + } + // 2) try containers/lists - exact QName match + if(defRef == null) { + Iterator it = DataNodeHelper.stream(ctx) + .filter(n -> n instanceof ContainerSchemaNode || n instanceof ListSchemaNode) + .iterator(); + while(it.hasNext()) { + org.opendaylight.yangtools.yang.model.api.SchemaNode candidate = it.next(); + if(!(candidate instanceof DataSchemaNode)) continue; + DataSchemaNode ds = (DataSchemaNode) candidate; + String local = ds.getQName().getLocalName(); + String candModule = moduleUtils.toModuleName(ds); + if(local.equals(namePart) && (modulePart.isEmpty() || candModule.equals(modulePart))) { + try { defRef = resolveDefinition((DataNodeContainer)ds); resolvedNode = ds; } catch (Exception e) { defRef = null; resolvedNode = null; } + if(defRef != null) break; + } + } + } + // 3) fallback: fuzzy substring heuristic against swagger definitions (may match wrong definition - see comment above) + if(defRef == null && !candidateDefs.isEmpty()) { + String lower = namePart.toLowerCase(); + Optional match = candidateDefs.keySet().stream().filter(k -> k.contains(lower) && (modulePart.isEmpty() || k.contains(modulePart.toLowerCase()))).findFirst(); + if(match.isPresent()) { + defRef = "#/definitions/" + candidateDefs.get(match.get()); + log.warn("Mount-point target '{}{}' resolved via fuzzy substring match to definition '{}'. " + + "This heuristic uses contains() and may have matched the wrong definition. " + + "Consider using an exact module:grouping mapping to avoid ambiguity.", + modulePart.isEmpty() ? "" : modulePart + ":", namePart, candidateDefs.get(match.get())); + } + } + + if(defRef != null) { + // ensure simple ref (strip prefix) + String simple = toSimpleDefinitionRef(defRef); + + // If definition missing in swagger definitions, try to create it using data object builder + if(!swagger.getDefinitions().containsKey(simple) && resolvedNode != null && dataRepo instanceof DataObjectBuilder) { + try { + DataObjectBuilder builder = (DataObjectBuilder) dataRepo; + String name = null; + try { + name = getNameUnchecked(resolvedNode); + } catch (Exception e) { + // ignore + } + if(name != null) { + try { + addModelUnchecked(builder, resolvedNode, name); + } catch (ClassCastException cce) { + try { + addModelUnchecked(builder, resolvedNode); + } catch (Exception ex) { + log.warn("Cannot create definition for mapping {}: {}", target, ex.toString()); + } + } catch (Exception ex) { + log.warn("Cannot create definition for mapping {}: {}", target, ex.toString()); + } + } else { + try { + addModelUnchecked(builder, resolvedNode); + } catch (Exception ex) { + log.warn("Cannot create definition for mapping {}: {}", target, ex.toString()); + } + } + + // Additionally, ensure nested container/list models inside the resolved node are created + try { + if(resolvedNode instanceof GroupingDefinition) { + createModelsForGrouping((GroupingDefinition) resolvedNode, builder); + } else if(resolvedNode instanceof DataNodeContainer) { + createModelsForContainer((DataNodeContainer) resolvedNode, builder); + } + } catch (Exception ex) { + log.debug("Creating nested definitions for mapping {} failed: {}", target, ex.toString()); + } + + } catch (Exception e) { + log.warn("Creating definition for mapping {} failed: {}", target, e.toString()); + } + } + + List wrappers = findWrapperDefsForGrouping(swagger, simple, modulePart); + if (!wrappers.isEmpty()) { + for (String w : wrappers) refModels.add(new RefModel("#/definitions/" + w)); + } else { + refModels.add(new RefModel("#/definitions/" + simple)); + } + } else { + throw new IllegalArgumentException(target + " does not exist, check Your configuration & spelling"); + } + } + + if(refModels.isEmpty()) continue; + + // attach to each node's generated definition (resolve via DataObjectRepo) + for(DataNodeContainer node : entry.getValue()) { + String defRef; + try { + defRef = resolveDefinition(node); + } catch (Exception e) { + log.warn("Cannot resolve definition ref for node {}: {}", getNodeId(node), e.toString()); + continue; + } + if(defRef == null || defRef.isEmpty()) { + log.warn("No swagger definition found for node {} to attach mount refs", getNodeId(node)); + continue; + } + // get original model if present + String simpleRef = toSimpleDefinitionRef(defRef); + Model original = swagger.getDefinitions().get(simpleRef); + + ComposedModel cm = new ComposedModel(); + cm.setInterfaces(refModels); + + // set parent interface if available + if(!refModels.isEmpty()) cm.parent(refModels.get(0)); + + if(original instanceof ModelImpl) { + cm.child(copyModelImpl((ModelImpl) original)); + } else if (original instanceof ComposedModel) { + // preserve original allOf entries where possible to avoid dropping existing components + ComposedModel origCm = (ComposedModel) original; + List origAll = origCm.getAllOf(); + if(origAll != null && !origAll.isEmpty()) { + List targetAll = cm.getAllOf(); + if(targetAll == null) { + targetAll = new ArrayList<>(); + cm.setAllOf(targetAll); + } + // copy items and avoid exact duplicates (by string representation) + Set seen = new HashSet<>(); + for(Model o : targetAll) if(o != null) seen.add(o.toString()); + for(Model o : origAll) { + if(o == null) continue; + if(!seen.contains(o.toString())) { + targetAll.add(o); + seen.add(o.toString()); + } + } + } + } else { + cm.parent(new RefModel(defRef)); + } + + // Prune inline empty 'object' entries from allOf to avoid redundant object entries in composed models + try { + List allOf = cm.getAllOf(); + if(allOf != null) { + allOf.removeIf(m -> { + if(m == null) return true; + if(m instanceof ModelImpl) { + ModelImpl mm = (ModelImpl) m; + String t = mm.getType(); + java.util.Map props = mm.getProperties(); + boolean isEmptyObject = "object".equals(t) && (props == null || props.isEmpty()); + return isEmptyObject; + } + return false; + }); + } + } catch (Exception e) { + log.debug("Error while pruning composed model allOf entries: {}", e.toString()); + } + + // put back using simple name key + swagger.getDefinitions().put(simpleRef, cm); + log.info("Attached mount refs to definition {} for mount label {}", simpleRef, label); + } + } + } + + private Map buildCandidateDefinitions(Swagger swagger) { + Map candidateDefs = new HashMap<>(); + if(swagger.getDefinitions() == null) return candidateDefs; + for(String s : swagger.getDefinitions().keySet()) { + candidateDefs.put(s.toLowerCase(), s); + } + return candidateDefs; + } + + private void addUniqueRefModels(List target, List source) { + for(RefModel candidate : source) { + boolean exists = target.stream().anyMatch(r -> r.getSimpleRef().equals(candidate.getSimpleRef())); + if(!exists) { + target.add(candidate); + } + } + } + + private String toSimpleDefinitionRef(String defRef) { + return defRef != null && defRef.startsWith("#/definitions/") + ? defRef.substring("#/definitions/".length()) + : defRef; + } + + private RefModel toDefinitionRefModel(String defRef) { + return new RefModel("#/definitions/" + toSimpleDefinitionRef(defRef)); + } + + private ModelImpl copyModelImpl(ModelImpl source) { + ModelImpl copy = new ModelImpl(); + copy.setType(source.getType()); + copy.setProperties(source.getProperties()); + copy.setDescription(source.getDescription()); + return copy; + } + + /** + * Try to discover mount-point extension argument for a node (DataSchemaNode or GroupingDefinition etc.) + * using reflection on effective statement. + *

+ * Delegates to {@link EffectiveStatementReflectionHelper} which encapsulates all + * reflection-based probing so it can be unit-tested in isolation. + */ + private String findMountPointLabel(Object node) { + return reflectionHelper.findMountPointLabel(node); + } + + private String getNodeId(DataNodeContainer node) { + if(node instanceof org.opendaylight.yangtools.yang.model.api.SchemaNode) { + try { + return ((org.opendaylight.yangtools.yang.model.api.SchemaNode)node).getQName().getLocalName(); + } catch (Exception e) { /* ignore */ } + } + if(node instanceof org.opendaylight.yangtools.yang.model.api.Module) { + return ((org.opendaylight.yangtools.yang.model.api.Module)node).getName(); + } + return node.toString(); + } + + @SuppressWarnings("unchecked") + private String resolveDefinition(DataNodeContainer node) { + // DataObjectRepo#getDefinitionRef expects a type that is both SchemaNode and DataNodeContainer. + if(node == null) return null; + if(!(node instanceof org.opendaylight.yangtools.yang.model.api.SchemaNode)) return null; + try { + // unchecked cast to intersection generic expected by DataObjectRepo + return dataRepo.getDefinitionRef((T) node); + } catch (ClassCastException e) { + // if the node isn't the exact expected shape, return null + return null; + } + } + + @SuppressWarnings("unchecked") + private + String getNameUnchecked(Object o) { + return dataRepo.getName((T) o); + } + + @SuppressWarnings("unchecked") + private + void addModelUnchecked(DataObjectBuilder builder, Object o, String name) { + builder.addModel((T) o, name); + } + + @SuppressWarnings("unchecked") + private + void addModelUnchecked(DataObjectBuilder builder, Object o) { + builder.addModel((T) o); + } + + // New helper: find module by name in context + private Optional findModuleByName(String moduleName) { + if(moduleName == null || moduleName.isEmpty()) return Optional.empty(); + // ensure the Optional type matches Module (ctx.getModules() may return ? extends Module) + return ctx.getModules().stream().map(m -> (Module) m).filter(m -> moduleName.equals(m.getName())).findFirst(); + } + + // New helper: resolve all groupings and top-level containers/lists from a module into RefModel list + private List resolveModuleMappings(String moduleName, Swagger swagger) { + List refs = new ArrayList<>(); + Optional modOpt = findModuleByName(moduleName); + if(!modOpt.isPresent()) return refs; + Module mod = modOpt.get(); + + Set seen = new HashSet<>(); + + // 1) groupings in context that belong to this module + for(GroupingDefinition g : ctx.getGroupings()) { + String gModule = moduleUtils.toModuleName(g); + if(!moduleName.equals(gModule)) continue; + try { + String defRef = dataRepo.getDefinitionRef(g); + if(defRef != null) { + String simple = toSimpleDefinitionRef(defRef); + // ensure model exists in swagger definitions; create if missing + if((swagger.getDefinitions() == null || !swagger.getDefinitions().containsKey(simple)) && dataRepo instanceof DataObjectBuilder) { + try { + DataObjectBuilder builder = (DataObjectBuilder) dataRepo; + try { + String gname = null; + try { gname = getNameUnchecked(g); } catch (Exception e) { /* ignore */ } + if(gname != null) addModelUnchecked(builder, g, gname); + else addModelUnchecked(builder, g); + } catch (Exception ex) { /* ignore */ } + try { createModelsForGrouping(g, builder); } catch (Exception ex2) { /* ignore */ } + } catch (Exception exx) { /* ignore */ } + } + if(seen.add(simple)) { + List wrappers = findWrapperDefsForGrouping(swagger, simple, moduleName); + if (!wrappers.isEmpty()) { + for (String w : wrappers) refs.add(toDefinitionRefModel(w)); + } else { + refs.add(toDefinitionRefModel(simple)); + } + } + } + } catch (Exception e) { + // try to create using builder if available + if(dataRepo instanceof DataObjectBuilder) { + try { + DataObjectBuilder builder = (DataObjectBuilder) dataRepo; + try { addModelUnchecked(builder, g); } catch (Exception ex) { /* ignore */ } + try { String defRef = dataRepo.getDefinitionRef(g); if(defRef != null) { String simple = toSimpleDefinitionRef(defRef); if(seen.add(simple)) { List wrappers = findWrapperDefsForGrouping(swagger, simple, moduleName); if (!wrappers.isEmpty()) { for (String w : wrappers) refs.add(toDefinitionRefModel(w)); } else { refs.add(toDefinitionRefModel(simple)); } } } } catch (Exception ex2) { /* ignore */ } + + // ensure nested container/list models inside grouping are created as well + try { createModelsForGrouping(g, builder); } catch (Exception ex3) { /* ignore */ } + } catch (Exception ex) { /* ignore */ } + } + } + } + + // 2) include top-level containers and lists from the module as well (to bring nested types) + if(dataRepo instanceof DataObjectBuilder) { + DataObjectBuilder builder = (DataObjectBuilder) dataRepo; + for(org.opendaylight.yangtools.yang.model.api.DataSchemaNode child : mod.getChildNodes()) { + if(child instanceof ContainerSchemaNode || child instanceof ListSchemaNode) { + try { + // Try to ensure model exists but DO NOT add its RefModel to the returned refs list. + String defRef = resolveDefinition((DataNodeContainer) child); + if(defRef == null) { + // try to create model + try { addModelUnchecked(builder, child); } catch (Exception ex) { /* ignore */ } + // after creating, attempt to resolve again (but we won't add to refs) + try { String defRef2 = resolveDefinition((DataNodeContainer) child); if(defRef2 != null) { String simple = toSimpleDefinitionRef(defRef2); /* ensure uniqueness in swagger but do not add to refs */ } } catch (Exception ex2) { /* ignore */ } + + // recursively create nested models for children so nested types exist in definitions + try { createModelsForContainer((DataNodeContainer) child, builder); } catch (Exception ex3) { /* ignore */ } + } else { + // definition already present; still ensure nested definitions exist + try { createModelsForContainer((DataNodeContainer) child, builder); } catch (Exception ex3) { /* ignore */ } + } + } catch (Exception e) { + // ignore individual child + } + } + } + } + + // Note: For module-only mappings we intentionally include groupings and top-level containers/lists from the module + // to provide nested definitions required by mounted models. + + return refs; + } + + // recursively create models for grouping's inner containers/lists + private void createModelsForGrouping(GroupingDefinition grouping, DataObjectBuilder builder) { + if(grouping == null || builder == null) return; + // GroupingDefinition may contain DataSchemaNode children inside its body + DataNodeHelper.stream(grouping) + .filter(n -> n instanceof ContainerSchemaNode || n instanceof ListSchemaNode) + .map(n -> (DataNodeContainer) n) + .forEach(c -> { + try { + addModelUnchecked(builder, c); + } catch (Exception e) { + // ignore + } + try { createModelsForContainer(c, builder); } catch (Exception e) { /* ignore */ } + }); + } + + // recursively create models for container/list and its nested containers/lists + private void createModelsForContainer(DataNodeContainer container, DataObjectBuilder builder) { + if(container == null || builder == null) return; + // for each child that is a container or list, ensure model exists and recurse + for(Object childObj : ((org.opendaylight.yangtools.yang.model.api.DataNodeContainer)container).getChildNodes()) { + if(!(childObj instanceof org.opendaylight.yangtools.yang.model.api.DataSchemaNode)) continue; + org.opendaylight.yangtools.yang.model.api.DataSchemaNode child = (org.opendaylight.yangtools.yang.model.api.DataSchemaNode) childObj; + if(child instanceof ContainerSchemaNode || child instanceof ListSchemaNode) { + DataNodeContainer dc = (DataNodeContainer) child; + try { + addModelUnchecked(builder, dc); + } catch (Exception e) { + // ignore + } + // recurse + try { createModelsForContainer(dc, builder); } catch (Exception e) { /* ignore */ } + } + } + } + + // attach RPCs from module as operations under each mount node + private void attachModuleRpcsToMount(Module module, List mountNodes, Swagger swagger) { + if(module == null || mountNodes == null || mountNodes.isEmpty()) return; + log.debug("attachModuleRpcsToMount invoked for module {} with {} mount nodes", module.getName(), mountNodes.size()); + if(!(dataRepo instanceof DataObjectBuilder)) { + log.info("No DataObjectBuilder available - skipping attaching RPC models for module {}", module.getName()); + return; + } + DataObjectBuilder builder = (DataObjectBuilder) dataRepo; + + for(RpcDefinition rpc : module.getRpcs()) { + try { + log.debug("Processing RPC {} in module {}", rpc.getQName().getLocalName(), module.getName()); + InputSchemaNode input = rpc.getInput(); + OutputSchemaNode output = rpc.getOutput(); + input = input.getChildNodes().isEmpty() ? null : input; + output = output.getChildNodes().isEmpty() ? null : output; + + // create base operation (for global /operations paths) tagged with RPC module + Operation baseOp = new Operation(); + baseOp.response(400, new Response().description("Internal error")); + baseOp.setParameters(new ArrayList<>()); + baseOp.tag(module.getName()); + + if(input != null) { + builder.addModel(input); + ModelImpl inputModel = new ModelImpl().type(ModelImpl.OBJECT); + inputModel.addProperty("input", new RefProperty(builder.getDefinitionRef(input))); + baseOp.summary("operates on " + builder.getName(ContainerSchemaNodes.forRPC(rpc))); + baseOp.description("operates on " + builder.getName(ContainerSchemaNodes.forRPC(rpc))); + baseOp.parameter(new BodyParameter() + .name(builder.getName(input) + ".body-param") + .schema(inputModel) + .description(input.getDescription().orElse(null)) + ); + } + + if(output != null) { + ModelImpl model = new ModelImpl().type(ModelImpl.OBJECT); + model.addProperty("output", new RefProperty(builder.getDefinitionRef(output))); + builder.addModel(output); + baseOp.response(200, new Response() + .responseSchema(model) + .description(output.getDescription().orElse("Correct response"))); + } + + baseOp.response(201, new Response().description("No response")); + + // attach to each mount node as a path (operations root) + for(DataNodeContainer mount : mountNodes) { + // Only create mounted RPC under data path; do not create operations-root entries here + try { + String dataPath = findDataPathForMount(mount, swagger); + if(dataPath != null) { + Operation mountedOp = copyOperation(baseOp); + List inheritedParams = extractPathParameters(dataPath, swagger); + if (!inheritedParams.isEmpty()) { + List merged = new ArrayList<>(inheritedParams); + if (mountedOp.getParameters() != null) merged.addAll(mountedOp.getParameters()); + mountedOp.setParameters(merged); + } + String mountModuleName = null; + try { + if(mount instanceof org.opendaylight.yangtools.yang.model.api.SchemaNode) { + mountModuleName = moduleUtils.toModuleName((org.opendaylight.yangtools.yang.model.api.SchemaNode) mount); + } else if(mount instanceof Module) { + mountModuleName = ((Module) mount).getName(); + } + } catch (Exception e) { + mountModuleName = module.getName(); + } + + mountedOp.tag(module.getName()); + String mountedRpcKey = dataPath + "/" + module.getName() + ":" + rpc.getQName().getLocalName(); + if(swagger.getPaths() != null && swagger.getPaths().containsKey(mountedRpcKey)) { + log.warn("Mounted RPC path {} already exists in swagger, skipping", mountedRpcKey); + } else { + if(swagger.getPaths() == null) swagger.setPaths(new java.util.LinkedHashMap<>()); + swagger.path(mountedRpcKey, new Path().post(mountedOp)); + log.info("Attached mounted RPC {} under data path {} for mount node {}", rpc.getQName().getLocalName(), mountedRpcKey, getNodeId(mount)); + + // keep global operations/* entries (generated by the main path handlers); + // this postprocessor only adds mounted data-path operations and must not remove globals + } + } else { + log.debug("Could not find data path for mount node {}, skipping mounted RPC creation for {}", getNodeId(mount), rpc.getQName().getLocalName()); + } + } catch (Exception e) { + log.warn("Failed to attach mounted RPC {} for mount node {}: {}", rpc.getQName().getLocalName(), getNodeId(mount), e.toString()); + } + } + + } catch (Exception e) { + log.warn("Failed to attach RPC {} from module {}: {}", rpc.getQName().getLocalName(), module.getName(), e.toString()); + } + } + } + + // Generate mounted data API for module under each mount data path. + private void attachModuleDataPathsToMount(Module module, List mountNodes, Swagger swagger) { + if(module == null || mountNodes == null || mountNodes.isEmpty() || swagger == null) return; + + Map sourceDataPaths = resolveModuleDataPaths(module, swagger); + if(sourceDataPaths.isEmpty()) { + log.debug("No source data paths found for module {}, skipping mounted data path generation", module.getName()); + return; + } + + for(DataNodeContainer mount : mountNodes) { + String mountDataPath = findDataPathForMount(mount, swagger); + if(mountDataPath == null) { + log.debug("Could not find data path for mount node {}, skipping mounted data paths for module {}", getNodeId(mount), module.getName()); + continue; + } + + for(Map.Entry source : sourceDataPaths.entrySet()) { + String mountedPath = mountDataPath + source.getKey().substring("/data".length()); + if(swagger.getPaths() != null && swagger.getPaths().containsKey(mountedPath)) continue; + + if(swagger.getPaths() == null) swagger.setPaths(new LinkedHashMap<>()); + Path copiedPath = copyPathWithModuleTag(source.getValue(), module.getName()); + List inheritedParams = extractPathParameters(mountDataPath, swagger); + if (!inheritedParams.isEmpty()) { + injectPathParameters(copiedPath, inheritedParams); + } + swagger.path(mountedPath, copiedPath); + log.info("Attached mounted data path {} for module {} under mount node {}", mountedPath, module.getName(), getNodeId(mount)); + } + } + } + + private Map resolveModuleDataPaths(Module module, Swagger swagger) { + Map sourceDataPaths = new LinkedHashMap<>(); + String modulePrefix = "/data/" + module.getName() + ":"; + + if(swagger.getPaths() != null) { + for(Map.Entry entry : swagger.getPaths().entrySet()) { + String pathKey = entry.getKey(); + if(pathKey != null && pathKey.startsWith(modulePrefix) && entry.getValue() != null) { + sourceDataPaths.put(pathKey, entry.getValue()); + } + } + } + + if(!sourceDataPaths.isEmpty()) return sourceDataPaths; + + // Module was not part of modulesToGenerate - generate its data API in isolation and reuse it as source. + Swagger moduleSwagger = moduleSwaggerCache.computeIfAbsent(module.getName(), ignored -> generateModuleDataSwagger(module)); + if(moduleSwagger == null || moduleSwagger.getPaths() == null) return sourceDataPaths; + + for(Map.Entry entry : moduleSwagger.getPaths().entrySet()) { + String pathKey = entry.getKey(); + if(pathKey != null && pathKey.startsWith(modulePrefix) && entry.getValue() != null) { + sourceDataPaths.put(pathKey, entry.getValue()); + } + } + + if(moduleSwagger.getDefinitions() != null) { + if(swagger.getDefinitions() == null) swagger.setDefinitions(new LinkedHashMap<>()); + moduleSwagger.getDefinitions().forEach((name, model) -> swagger.getDefinitions().putIfAbsent(name, model)); + } + + return sourceDataPaths; + } + + private Swagger generateModuleDataSwagger(Module module) { + try { + SwaggerGenerator generator = new SwaggerGenerator(ctx, Collections.singletonList(module)).defaultConfig() + .elements(SwaggerGenerator.Elements.DATA) + .pathHandler(new com.mrv.yangtools.codegen.impl.path.rfc8040.PathHandlerBuilder().useModuleName()); + // Ensure *Wrapper definitions are generated for this module's containers/lists, + // matching what Rfc4080PayloadWrapper produces for normally-generated modules. + generator.appendPostProcessor(new Rfc4080PayloadWrapper()); + return generator.generate(); + } catch (Exception e) { + log.warn("Failed to generate standalone data API for module {}: {}", module.getName(), e.toString()); + return null; + } + } + + // New helper: try to discover the data path key in swagger for a given mount node + private String findDataPathForMount(DataNodeContainer mount, Swagger swagger) { + if(mount == null || swagger == null || swagger.getPaths() == null) return null; + String nodeId = getNodeId(mount); + if(nodeId == null) return null; + + // candidate keys that start with /data/ and contain the node id as a segment + List candidates = new ArrayList<>(); + List terminalCandidates = new ArrayList<>(); + for(String key : swagger.getPaths().keySet()) { + if(!key.startsWith("/data/")) continue; + // split into segments, ignore leading empty + String[] segs = key.split("/"); + for(int i = 0; i < segs.length; i++) { + String s = segs[i]; + if(s == null || s.isEmpty()) continue; + // compare with nodeId or with module:nodeId form + if(s.equals(nodeId) || s.endsWith(":" + nodeId) || s.startsWith(nodeId + "=") || s.contains(":" + nodeId + "=") || s.contains(":" + nodeId)) { + candidates.add(key); + if(i == segs.length - 1) { + terminalCandidates.add(key); + } + break; + } + } + } + + if(candidates.isEmpty()) return null; + if(!terminalCandidates.isEmpty()) { + terminalCandidates.sort(Comparator.comparingInt(String::length).reversed()); + return terminalCandidates.get(0); + } + // prefer the longest (most specific) path + candidates.sort(Comparator.comparingInt(String::length).reversed()); + return candidates.get(0); + } + + private Path copyPathWithModuleTag(Path src, String moduleName) { + Path dst = new Path(); + if(src == null) return dst; + + if(src.getGet() != null) dst.setGet(copyOperationWithTag(src.getGet(), moduleName)); + if(src.getPut() != null) dst.setPut(copyOperationWithTag(src.getPut(), moduleName)); + if(src.getPost() != null) dst.setPost(copyOperationWithTag(src.getPost(), moduleName)); + if(src.getDelete() != null) dst.setDelete(copyOperationWithTag(src.getDelete(), moduleName)); + if(src.getPatch() != null) dst.setPatch(copyOperationWithTag(src.getPatch(), moduleName)); + if(src.getHead() != null) dst.setHead(copyOperationWithTag(src.getHead(), moduleName)); + if(src.getOptions() != null) dst.setOptions(copyOperationWithTag(src.getOptions(), moduleName)); + + try { + if(src.getParameters() != null) dst.setParameters(new ArrayList<>(src.getParameters())); + } catch (Exception e) { + // ignore + } + + return dst; + } + + private Operation copyOperationWithTag(Operation src, String moduleName) { + Operation dst = copyOperation(src); + dst.setTags(new ArrayList<>(Collections.singletonList(moduleName))); + return dst; + } + + // New helper: shallow copy of an Operation (parameters, responses, summary, description) without tags + private Operation copyOperation(Operation src) { + Operation dst = new Operation(); + try { + dst.setParameters(src.getParameters() == null ? null : new ArrayList<>(src.getParameters())); + } catch (Exception e) { + // ignore + } + try { + if(src.getResponses() != null) { + Map copy = new LinkedHashMap<>(); + copy.putAll(src.getResponses()); + dst.setResponses(copy); + } + } catch (Exception e) { + // ignore + } + try { dst.setSummary(src.getSummary()); } catch (Exception e) {} + try { dst.setDescription(src.getDescription()); } catch (Exception e) {} + try { dst.setOperationId(src.getOperationId()); } catch (Exception e) {} + try { dst.setConsumes(src.getConsumes() == null ? null : new ArrayList<>(src.getConsumes())); } catch (Exception e) {} + try { dst.setProduces(src.getProduces() == null ? null : new ArrayList<>(src.getProduces())); } catch (Exception e) {} + try { dst.setSchemes(src.getSchemes() == null ? null : new ArrayList<>(src.getSchemes())); } catch (Exception e) {} + try { dst.setDeprecated(src.isDeprecated()); } catch (Exception e) {} + try { dst.setSecurity(src.getSecurity() == null ? null : new ArrayList<>(src.getSecurity())); } catch (Exception e) {} + try { dst.setExternalDocs(src.getExternalDocs()); } catch (Exception e) {} + // do not copy tags - caller should set appropriate tag + return dst; + } + + /** + * Extract path parameters (e.g. {@code {name}}) declared on {@code path} by looking them up + * in the existing swagger operation definitions. Falls back to a synthetic string parameter + * when a declaration cannot be found. + */ + private List extractPathParameters(String path, Swagger swagger) { + if (path == null || path.isEmpty() || swagger == null || swagger.getPaths() == null) { + return Collections.emptyList(); + } + List paramNames = new ArrayList<>(); + int start = path.indexOf('{'); + while (start >= 0) { + int end = path.indexOf('}', start); + if (end < 0) break; + paramNames.add(path.substring(start + 1, end)); + start = path.indexOf('{', end); + } + if (paramNames.isEmpty()) return Collections.emptyList(); + + List result = new ArrayList<>(); + Set resolved = new HashSet<>(); + + for (String paramName : paramNames) { + boolean found = false; + for (Map.Entry entry : swagger.getPaths().entrySet()) { + if (!entry.getKey().contains("{" + paramName + "}")) continue; + for (Operation op : collectOperations(entry.getValue())) { + if (op == null || op.getParameters() == null) continue; + for (Parameter p : op.getParameters()) { + if ("path".equals(p.getIn()) && paramName.equals(p.getName()) && resolved.add(paramName)) { + result.add(p); + found = true; + break; + } + } + if (found) break; + } + if (found) break; + } + if (!found && resolved.add(paramName)) { + PathParameter pp = new PathParameter(); + pp.setName(paramName); + pp.setRequired(true); + pp.setType("string"); + result.add(pp); + } + } + return result; + } + + private List collectOperations(Path path) { + List ops = new ArrayList<>(); + if (path.getGet() != null) ops.add(path.getGet()); + if (path.getPut() != null) ops.add(path.getPut()); + if (path.getPost() != null) ops.add(path.getPost()); + if (path.getDelete() != null) ops.add(path.getDelete()); + if (path.getPatch() != null) ops.add(path.getPatch()); + return ops; + } + + /** + * For a grouping definition (e.g. {@code entry.type._2.Content}) finds or creates + * {@code *Wrapper} definitions for each container property ({@link RefProperty}), + * prefixing the property key with {@code moduleName} for RESTCONF namespace qualification. + * Returns the list of wrapper definition names, or an empty list when none can be resolved. + */ + private List findWrapperDefsForGrouping(Swagger swagger, String defName, String moduleName) { + List wrappers = new ArrayList<>(); + if (swagger.getDefinitions() == null) return wrappers; + Model def = swagger.getDefinitions().get(defName); + if (def == null || def.getProperties() == null) return wrappers; + + for (Map.Entry propEntry : def.getProperties().entrySet()) { + String propKey = propEntry.getKey(); + Property prop = propEntry.getValue(); + if (!(prop instanceof RefProperty)) continue; + + RefProperty refProp = (RefProperty) prop; + String refDefName = refProp.getSimpleRef(); + if (refDefName == null || !swagger.getDefinitions().containsKey(refDefName)) continue; + + String wrapperName = refDefName + "Wrapper"; + if (!swagger.getDefinitions().containsKey(wrapperName)) { + String nsKey = propKey.contains(":") ? propKey : moduleName + ":" + propKey; + ModelImpl wrapper = new ModelImpl(); + wrapper.addProperty(nsKey, new RefProperty("#/definitions/" + refDefName)); + swagger.getDefinitions().put(wrapperName, wrapper); + log.debug("Created Wrapper definition {} with key {} for module {}", wrapperName, nsKey, moduleName); + } + wrappers.add(wrapperName); + } + return wrappers; + } + + /** Prepend {@code params} to every operation on {@code path}, skipping any already declared. */ + private void injectPathParameters(Path path, List params) { + if (params.isEmpty()) return; + for (Operation op : collectOperations(path)) { + if (op == null) continue; + List existing = op.getParameters() == null ? new ArrayList<>() : new ArrayList<>(op.getParameters()); + Set existingPathParamNames = new HashSet<>(); + for (Parameter p : existing) { + if ("path".equals(p.getIn())) existingPathParamNames.add(p.getName()); + } + List merged = new ArrayList<>(); + for (Parameter p : params) { + if (!existingPathParamNames.contains(p.getName())) merged.add(p); + } + merged.addAll(existing); + op.setParameters(merged); + } + } +} diff --git a/swagger-generator/src/test/java/com/mrv/yangtools/codegen/MountPointMappingsTest.java b/swagger-generator/src/test/java/com/mrv/yangtools/codegen/MountPointMappingsTest.java new file mode 100644 index 0000000..8a58a70 --- /dev/null +++ b/swagger-generator/src/test/java/com/mrv/yangtools/codegen/MountPointMappingsTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026. MRV Communications, Inc. 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 + * Contributors: + * Christopher Murch + * Bartosz Michalik + */ + +package com.mrv.yangtools.codegen; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; +import static org.junit.Assert.*; + +import java.util.List; +import java.util.Map; + +public class MountPointMappingsTest { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Test + public void testDeserializationFromString() throws Exception { + String json = "{\"label1\": [\"module1:name1\", \"module2:name2\"]}"; + MountPointMappings mappings = mapper.readValue(json, MountPointMappings.class); + + assertEquals(1, mappings.asMap().size()); + List targets = mappings.getTargets("label1"); + assertEquals(2, targets.size()); + assertEquals("module1", targets.get(0).getModule()); + assertEquals("name1", targets.get(0).getName()); + assertEquals("module2", targets.get(1).getModule()); + assertEquals("name2", targets.get(1).getName()); + } + + @Test + public void testDeserializationFromObject() throws Exception { + String json = "{\"label1\": [{\"module\": \"module1\", \"name\": \"name1\"}]}"; + MountPointMappings mappings = mapper.readValue(json, MountPointMappings.class); + + assertEquals(1, mappings.asMap().size()); + List targets = mappings.getTargets("label1"); + assertEquals(1, targets.size()); + assertEquals("module1", targets.get(0).getModule()); + assertEquals("name1", targets.get(0).getName()); + } + + @Test + public void testDeserializationMixed() throws Exception { + String json = "{\"label1\": [\"module1:name1\", {\"module\": \"module2\", \"name\": \"name2\"}]}"; + MountPointMappings mappings = mapper.readValue(json, MountPointMappings.class); + + assertEquals(1, mappings.asMap().size()); + List targets = mappings.getTargets("label1"); + assertEquals(2, targets.size()); + assertEquals("name1", targets.get(0).getName()); + assertEquals("name2", targets.get(1).getName()); + } + + @Test + public void testDeserializationSingleItem() throws Exception { + String json = "{\"label1\": \"module1:name1\"}"; + MountPointMappings mappings = mapper.readValue(json, MountPointMappings.class); + + assertEquals(1, mappings.asMap().size()); + List targets = mappings.getTargets("label1"); + assertEquals(1, targets.size()); + assertEquals("name1", targets.get(0).getName()); + } + + @Test(expected = Exception.class) + public void testInvalidFormat() throws Exception { + String json = "{\"label1\": 123}"; + mapper.readValue(json, MountPointMappings.class); + } +} diff --git a/swagger-generator/src/test/java/com/mrv/yangtools/codegen/impl/postprocessor/MountPointPostProcessorReflectionHelpersTest.java b/swagger-generator/src/test/java/com/mrv/yangtools/codegen/impl/postprocessor/MountPointPostProcessorReflectionHelpersTest.java new file mode 100644 index 0000000..51fe1ab --- /dev/null +++ b/swagger-generator/src/test/java/com/mrv/yangtools/codegen/impl/postprocessor/MountPointPostProcessorReflectionHelpersTest.java @@ -0,0 +1,137 @@ +package com.mrv.yangtools.codegen.impl.postprocessor; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; + +public class MountPointPostProcessorReflectionHelpersTest { + + private EffectiveStatementReflectionHelper helper; + + @Before + public void setUp() { + helper = new EffectiveStatementReflectionHelper(); + } + + @Test + public void tryGetStringPropertyReturnsFirstMatch() { + String value = helper.tryGetStringProperty( + new PropertySource(), + new String[]{"missing", "getName"}); + + Assert.assertEquals("expected-name", value); + } + + @Test + public void tryGetStringPropertyReturnsNullWhenNoMethodMatches() { + String value = helper.tryGetStringProperty( + new PropertySource(), + new String[]{"missingA", "missingB"}); + + Assert.assertNull(value); + } + + @Test + public void findMountPointLabelReadsArgumentMethod() { + String value = helper.findMountPointLabel( + new NodeWithEffective( + new EffectiveWithCollection( + new ItemWithArgument("entry-type-specific-data")))); + + Assert.assertEquals("entry-type-specific-data", value); + } + + @Test + public void findMountPointLabelFallsBackToStringParsing() { + String value = helper.findMountPointLabel( + new NodeWithEffective( + new EffectiveWithCollection( + new ItemWithText("yangmnt:mount-point parsed-label")))); + + Assert.assertEquals("parsed-label", value); + } + + @Test + public void findMountPointLabelReturnsNullWithoutMountPoint() { + String value = helper.findMountPointLabel( + new NodeWithEffective( + new EffectiveWithCollection( + new ItemWithText("no-extension")))); + + Assert.assertNull(value); + } + + @Test + public void findMountPointLabelReturnsNullForNullNode() { + String value = helper.findMountPointLabel(null); + + Assert.assertNull(value); + } + + private static final class PropertySource { + public String getName() { + return "expected-name"; + } + } + + private static final class NodeWithEffective { + private final Object effective; + + private NodeWithEffective(Object effective) { + this.effective = effective; + } + + public Object asEffectiveStatement() { + return effective; + } + } + + private static final class EffectiveWithCollection { + private final Object item; + + private EffectiveWithCollection(Object item) { + this.item = item; + } + + public java.util.Collection getItems() { + return Arrays.asList(item); + } + + public java.util.Map getMapItems() { + return Collections.emptyMap(); + } + } + + private static final class ItemWithArgument { + private final String label; + + private ItemWithArgument(String label) { + this.label = label; + } + + public String getArgument() { + return label; + } + + @Override + public String toString() { + return "yangmnt:mount-point"; + } + } + + private static final class ItemWithText { + private final String text; + + private ItemWithText(String text) { + this.text = text; + } + + @Override + public String toString() { + return text; + } + } +} diff --git a/swagger-generator/src/test/java/com/mrv/yangtools/codegen/issues/Issue45.java b/swagger-generator/src/test/java/com/mrv/yangtools/codegen/issues/Issue45.java new file mode 100644 index 0000000..c6dacaa --- /dev/null +++ b/swagger-generator/src/test/java/com/mrv/yangtools/codegen/issues/Issue45.java @@ -0,0 +1,162 @@ +package com.mrv.yangtools.codegen.issues; + +import com.mrv.yangtools.codegen.AbstractItTest; +import com.mrv.yangtools.codegen.MountPointMappings; +import com.mrv.yangtools.codegen.MountPointTarget; +import com.mrv.yangtools.codegen.SwaggerGenerator; +import com.mrv.yangtools.codegen.impl.path.AbstractPathHandlerBuilder; +import com.mrv.yangtools.codegen.impl.postprocessor.Rfc4080PayloadWrapper; +import com.mrv.yangtools.common.ContextHelper; +import io.swagger.models.ComposedModel; +import io.swagger.models.Model; +import io.swagger.models.ModelImpl; +import io.swagger.models.RefModel; +import io.swagger.models.parameters.BodyParameter; +import io.swagger.models.parameters.Parameter; +import io.swagger.models.properties.Property; +import io.swagger.models.properties.RefProperty; +import org.junit.Assert; +import org.junit.Test; +import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; +import org.opendaylight.yangtools.yang.model.api.Module; +import org.opendaylight.yangtools.yang.parser.spi.meta.ReactorException; + +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.Assert.assertTrue; + +public class Issue45 extends AbstractItTest { + + @Test + public void testSpecificTypeMounting() throws IOException, ReactorException { + runSwaggerGeneratorWithMountMappings(Arrays.asList("entry-type-1:content", "entry-type-2:content")); + + validateMountPointMapping(); + validateActionMapping(); + } + + @Test + public void testModuleMounting() throws IOException, ReactorException { + + runSwaggerGeneratorWithMountMappings(Arrays.asList("entry-type-1", "entry-type-2")); + + validateMountPointMapping(); + validateActionMapping(); + } + + @Test + public void testInvalidModuleMounting() throws IOException, ReactorException { + + try { + runSwaggerGeneratorWithMountMappings(Arrays.asList("invalid-module")); + Assert.fail("Expected IllegalArgumentException for invalid module mapping"); + } catch (IllegalArgumentException iae) { + assertTrue(iae.getMessage().contains("invalid-module does not exist")); + } + } + + private void validateActionMapping() { + String actionPath = "/operations/list-manager:list-entry={name}/list-entry-action"; + assertTrue("Missing action path in swagger: " + actionPath, swagger.getPaths().containsKey(actionPath)); + Assert.assertNotNull("Action should be exposed as POST", swagger.getPaths().get(actionPath).getPost()); + } + + private void validateMountPointMapping() { + ComposedModel specificConfig = (ComposedModel) swagger.getDefinitions().get("list.manager.listentry.SpecificConfig"); + List refs = specificConfig.getAllOf().stream() + .map(m -> ((RefModel) m).getSimpleRef()) + .collect(Collectors.toList()); + assertTrue(refs.contains("entry.type._1.content.Type1Wrapper")); + assertTrue(refs.contains("entry.type._2.content.Type2Wrapper")); + Assert.assertFalse("entry.type._1.Content should be removed as unused after mount-point processing", + swagger.getDefinitions().containsKey("entry.type._1.Content")); + Assert.assertFalse("entry.type._2.Content should be removed as unused after mount-point processing", + swagger.getDefinitions().containsKey("entry.type._2.Content")); + + String rpcPath = "/data/list-manager:list-entry={name}/specific-config/entry-type-1:entry-type-1-operation"; + assertTrue("Missing RPC mountpoint path: " + rpcPath, swagger.getPaths().containsKey(rpcPath)); + Assert.assertNotNull("RPC operations should be a POST: " + rpcPath, swagger.getPaths().get(rpcPath).getPost()); + + assertTrue("Mounted operation should have entry-type-1 tag assigned", swagger.getPaths().get(rpcPath).getPost().getTags().contains("entry-type-1")); + List parameters = swagger.getPaths().get(rpcPath).getPost().getParameters(); + Assert.assertNotNull("Mounted RPC should define POST parameters: " + rpcPath, parameters); + + BodyParameter bodyParameter = parameters.stream() + .filter(p -> p instanceof BodyParameter && "body".equals(p.getIn())) + .map(p -> (BodyParameter) p) + .findFirst() + .orElse(null); + + Assert.assertNotNull("Mounted RPC should define body payload parameter: " + rpcPath, bodyParameter); + Assert.assertEquals("body", bodyParameter.getIn()); + Assert.assertEquals("entry.type._1.entrytype1operation.Input.body-param", bodyParameter.getName()); + Assert.assertFalse("Body payload parameter should be optional", Boolean.TRUE.equals(bodyParameter.getRequired())); + + Model bodySchema = bodyParameter.getSchema(); + Assert.assertNotNull("Body parameter should expose schema", bodySchema); + Assert.assertTrue("Body schema should be an object model", bodySchema instanceof ModelImpl); + Assert.assertEquals("object", ((ModelImpl) bodySchema).getType()); + Assert.assertNotNull("Body schema should expose properties", bodySchema.getProperties()); + + Property inputProperty = bodySchema.getProperties().get("input"); + Assert.assertNotNull("Body schema should expose 'input' property", inputProperty); + Assert.assertTrue("Body schema input property should be a ref", inputProperty instanceof RefProperty); + + RefProperty inputRef = (RefProperty) inputProperty; + Assert.assertEquals("#/definitions/entry.type._1.entrytype1operation.Input", inputRef.get$ref()); + Assert.assertEquals("#/definitions/entry.type._1.entrytype1operation.Input", inputRef.getOriginalRef()); + + String mountedType2Path = "/data/list-manager:list-entry={name}/specific-config/entry-type-2:type-2"; + assertTrue("Missing mounted data path for entry-type-2: " + mountedType2Path, swagger.getPaths().containsKey(mountedType2Path)); + Assert.assertNotNull("Mounted entry-type-2 should expose GET", swagger.getPaths().get(mountedType2Path).getGet()); + Assert.assertNotNull("Mounted entry-type-2 should expose POST", swagger.getPaths().get(mountedType2Path).getPost()); + Assert.assertNotNull("Mounted entry-type-2 should expose PUT", swagger.getPaths().get(mountedType2Path).getPut()); + Assert.assertNotNull("Mounted entry-type-2 should expose DELETE", swagger.getPaths().get(mountedType2Path).getDelete()); + assertTrue("Mounted entry-type-2 should keep module tag", swagger.getPaths().get(mountedType2Path).getGet().getTags().contains("entry-type-2")); + + String globalType2Path = "/data/entry-type-2:type-2"; + Assert.assertFalse("entry-type-2 should not be generated globally when excluded from modulesToGenerate", swagger.getPaths().containsKey(globalType2Path)); + } + + private void runSwaggerGeneratorWithMountMappings(List listEntryData) throws ReactorException { + final PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:*.yang"); + final EffectiveModelContext context = buildEffectiveModelContext( + Paths.get("src","test","resources","bug_45").toString(), + p -> matcher.matches(p.getFileName())); + + Collection modulesToGenerate = context.getModules().stream() + .filter(module -> module.getName().equals("list-manager") + || module.getName().equals("entry-type-1")) + .collect(Collectors.toList()); + + AbstractPathHandlerBuilder pathHandler = new com.mrv.yangtools.codegen.impl.path.rfc8040.PathHandlerBuilder(); + pathHandler.useModuleName(); + + SwaggerGenerator generator = new SwaggerGenerator(context, modulesToGenerate).defaultConfig() + .pathHandler(pathHandler); + Map> mappingMap = new HashMap<>(); + mappingMap.put("entry-type-specific-data", listEntryData.stream() + .map(MountPointTarget::parse) + .collect(Collectors.toList())); + generator.appendPostProcessor(new Rfc4080PayloadWrapper()); + generator.yangmntMappings(new MountPointMappings(mappingMap)); + swagger = generator.generate(); + } + + private EffectiveModelContext buildEffectiveModelContext(String dir, Predicate accept) + throws ReactorException { + return ContextHelper.getFromDir(Stream.of(FileSystems.getDefault().getPath(dir)), accept); + } +} diff --git a/swagger-generator/src/test/resources/bug_45/entry-type-1.yang b/swagger-generator/src/test/resources/bug_45/entry-type-1.yang new file mode 100644 index 0000000..237ed04 --- /dev/null +++ b/swagger-generator/src/test/resources/bug_45/entry-type-1.yang @@ -0,0 +1,37 @@ +module entry-type-1 { + namespace "urn:demo:entry-type-1"; + prefix "et1"; + yang-version 1.1; + import list-manager {prefix lm;} + revision "2022-03-12" { + description "First cut"; + } + grouping content { + container type-1 { + leaf x { + type string; + } + leaf y { + type string; + } + leaf z { + type string; + } + } + } + + rpc entry-type-1-operation { // Should get translated as restconf operation (inside the mount point) + input { + leaf entry-type-1-operation-input { + type string; + } + } + output { + leaf entry-type-1-operation-output { + type string; + } + } + } + + uses content; +} diff --git a/swagger-generator/src/test/resources/bug_45/entry-type-2.yang b/swagger-generator/src/test/resources/bug_45/entry-type-2.yang new file mode 100644 index 0000000..bc79601 --- /dev/null +++ b/swagger-generator/src/test/resources/bug_45/entry-type-2.yang @@ -0,0 +1,23 @@ +module entry-type-2 { + namespace "urn:demo:entry-type-2"; + prefix "et2"; + yang-version 1.1; + import list-manager {prefix lm;} + revision "2022-03-12" { + description "First cut"; + } + grouping content { + container type-2 { + leaf a { + type string; + } + leaf b { + type string; + } + leaf c { + type string; + } + } + } + uses content; +} diff --git a/swagger-generator/src/test/resources/bug_45/ietf-datastores.yang b/swagger-generator/src/test/resources/bug_45/ietf-datastores.yang new file mode 100644 index 0000000..9458a04 --- /dev/null +++ b/swagger-generator/src/test/resources/bug_45/ietf-datastores.yang @@ -0,0 +1,90 @@ +module ietf-datastores { + yang-version 1.1; + namespace + "urn:ietf:params:xml:ns:yang:ietf-datastores"; + + prefix ds; + + organization + "IETF Network Modeling (NETMOD) Working Group"; + + contact + "WG Web: WG List: + Author: Martin Bjorklund Author: Juergen + Schoenwaelder Author: + Phil Shafer Author: Kent Watsen + Author: Rob Wilton "; + + description + "This YANG module defines a set of identities for identifying + datastores. Copyright (c) 2018 IETF Trust and the persons identified + as authors of the code. All rights reserved. Redistribution and + use in source and binary forms, with or without modification, + is permitted pursuant to, and subject to the license terms contained + in, the Simplified BSD License set forth in Section 4.c of the + IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info). + This version of this YANG module is part of RFC 8342 (https://www.rfc-editor.org/info/rfc8342); + see the RFC itself for full legal notices."; + + revision "2018-02-14" { + description "Initial revision."; + reference + "RFC 8342: Network Management Datastore Architecture (NMDA)"; + } + + identity datastore { + description + "Abstract base identity for datastore identities."; + } + + identity conventional { + base datastore; + description + "Abstract base identity for conventional configuration datastores."; + } + + identity running { + base conventional; + description + "The running configuration datastore."; + } + + identity candidate { + base conventional; + description + "The candidate configuration datastore."; + } + + identity startup { + base conventional; + description + "The startup configuration datastore."; + } + + identity intended { + base conventional; + description + "The intended configuration datastore."; + } + + identity dynamic { + base datastore; + description + "Abstract base identity for dynamic configuration datastores."; + } + + identity operational { + base datastore; + description + "The operational state datastore."; + } + + typedef datastore-ref { + type identityref { + base datastore; + } + description + "A datastore identity reference."; + } +} +// module ietf-datastores \ No newline at end of file diff --git a/swagger-generator/src/test/resources/bug_45/ietf-inet-types.yang b/swagger-generator/src/test/resources/bug_45/ietf-inet-types.yang new file mode 100644 index 0000000..6775614 --- /dev/null +++ b/swagger-generator/src/test/resources/bug_45/ietf-inet-types.yang @@ -0,0 +1,454 @@ + module ietf-inet-types { + + yang-version 1; + + namespace + "urn:ietf:params:xml:ns:yang:ietf-inet-types"; + + prefix inet; + + organization + "IETF NETMOD (NETCONF Data Modeling Language) Working Group"; + + contact + "WG Web: + WG List: + + WG Chair: David Kessens + + + WG Chair: Juergen Schoenwaelder + + + Editor: Juergen Schoenwaelder + "; + + description + "This module contains a collection of generally useful derived + YANG data types for Internet addresses and related things. + + Copyright (c) 2013 IETF Trust and the persons identified as + authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject + to the license terms contained in, the Simplified BSD License + set forth in Section 4.c of the IETF Trust's Legal Provisions + Relating to IETF Documents + (http://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC 6991; see + the RFC itself for full legal notices."; + + revision "2013-07-15" { + description + "This revision adds the following new data types: + - ip-address-no-zone + - ipv4-address-no-zone + - ipv6-address-no-zone"; + reference + "RFC 6991: Common YANG Data Types"; + + } + + revision "2010-09-24" { + description "Initial revision."; + reference + "RFC 6021: Common YANG Data Types"; + + } + + + typedef ip-version { + type enumeration { + enum "unknown" { + value 0; + description + "An unknown or unspecified version of the Internet + protocol."; + } + enum "ipv4" { + value 1; + description + "The IPv4 protocol as defined in RFC 791."; + } + enum "ipv6" { + value 2; + description + "The IPv6 protocol as defined in RFC 2460."; + } + } + description + "This value represents the version of the IP protocol. + + In the value set and its semantics, this type is equivalent + to the InetVersion textual convention of the SMIv2."; + reference + "RFC 791: Internet Protocol + RFC 2460: Internet Protocol, Version 6 (IPv6) Specification + RFC 4001: Textual Conventions for Internet Network Addresses"; + + } + + typedef dscp { + type uint8 { + range "0..63"; + } + description + "The dscp type represents a Differentiated Services Code Point + that may be used for marking packets in a traffic stream. + In the value set and its semantics, this type is equivalent + to the Dscp textual convention of the SMIv2."; + reference + "RFC 3289: Management Information Base for the Differentiated + Services Architecture + RFC 2474: Definition of the Differentiated Services Field + (DS Field) in the IPv4 and IPv6 Headers + RFC 2780: IANA Allocation Guidelines For Values In + the Internet Protocol and Related Headers"; + + } + + typedef ipv6-flow-label { + type uint32 { + range "0..1048575"; + } + description + "The ipv6-flow-label type represents the flow identifier or Flow + Label in an IPv6 packet header that may be used to + discriminate traffic flows. + + In the value set and its semantics, this type is equivalent + to the IPv6FlowLabel textual convention of the SMIv2."; + reference + "RFC 3595: Textual Conventions for IPv6 Flow Label + RFC 2460: Internet Protocol, Version 6 (IPv6) Specification"; + + } + + typedef port-number { + type uint16 { + range "0..65535"; + } + description + "The port-number type represents a 16-bit port number of an + Internet transport-layer protocol such as UDP, TCP, DCCP, or + SCTP. Port numbers are assigned by IANA. A current list of + all assignments is available from . + + Note that the port number value zero is reserved by IANA. In + situations where the value zero does not make sense, it can + be excluded by subtyping the port-number type. + In the value set and its semantics, this type is equivalent + to the InetPortNumber textual convention of the SMIv2."; + reference + "RFC 768: User Datagram Protocol + RFC 793: Transmission Control Protocol + RFC 4960: Stream Control Transmission Protocol + RFC 4340: Datagram Congestion Control Protocol (DCCP) + RFC 4001: Textual Conventions for Internet Network Addresses"; + + } + + typedef as-number { + type uint32; + description + "The as-number type represents autonomous system numbers + which identify an Autonomous System (AS). An AS is a set + of routers under a single technical administration, using + an interior gateway protocol and common metrics to route + packets within the AS, and using an exterior gateway + protocol to route packets to other ASes. IANA maintains + the AS number space and has delegated large parts to the + regional registries. + + Autonomous system numbers were originally limited to 16 + bits. BGP extensions have enlarged the autonomous system + number space to 32 bits. This type therefore uses an uint32 + base type without a range restriction in order to support + a larger autonomous system number space. + + In the value set and its semantics, this type is equivalent + to the InetAutonomousSystemNumber textual convention of + the SMIv2."; + reference + "RFC 1930: Guidelines for creation, selection, and registration + of an Autonomous System (AS) + RFC 4271: A Border Gateway Protocol 4 (BGP-4) + RFC 4001: Textual Conventions for Internet Network Addresses + RFC 6793: BGP Support for Four-Octet Autonomous System (AS) + Number Space"; + + } + + typedef ip-address { + type union { + type ipv4-address; + type ipv6-address; + } + description + "The ip-address type represents an IP address and is IP + version neutral. The format of the textual representation + implies the IP version. This type supports scoped addresses + by allowing zone identifiers in the address format."; + reference + "RFC 4007: IPv6 Scoped Address Architecture"; + + } + + typedef ipv4-address { + type string { + pattern + '(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(%[\p{N}\p{L}]+)?'; + } + description + "The ipv4-address type represents an IPv4 address in + dotted-quad notation. The IPv4 address may include a zone + index, separated by a % sign. + + The zone index is used to disambiguate identical address + values. For link-local addresses, the zone index will + typically be the interface index number or the name of an + interface. If the zone index is not present, the default + zone of the device will be used. + + The canonical format for the zone index is the numerical + format"; + } + + typedef ipv6-address { + type string { + pattern + '((:|[0-9a-fA-F]{0,4}):)([0-9a-fA-F]{0,4}:){0,5}((([0-9a-fA-F]{0,4}:)?(:|[0-9a-fA-F]{0,4}))|(((25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])))(%[\p{N}\p{L}]+)?'; + pattern + '(([^:]+:){6}(([^:]+:[^:]+)|(.*\..*)))|((([^:]+:)*[^:]+)?::(([^:]+:)*[^:]+)?)(%.+)?'; + } + description + "The ipv6-address type represents an IPv6 address in full, + mixed, shortened, and shortened-mixed notation. The IPv6 + address may include a zone index, separated by a % sign. + + The zone index is used to disambiguate identical address + values. For link-local addresses, the zone index will + typically be the interface index number or the name of an + interface. If the zone index is not present, the default + zone of the device will be used. + + + + The canonical format of IPv6 addresses uses the textual + representation defined in Section 4 of RFC 5952. The + canonical format for the zone index is the numerical + format as described in Section 11.2 of RFC 4007."; + reference + "RFC 4291: IP Version 6 Addressing Architecture + RFC 4007: IPv6 Scoped Address Architecture + RFC 5952: A Recommendation for IPv6 Address Text + Representation"; + + } + + typedef ip-address-no-zone { + type union { + type ipv4-address-no-zone; + type ipv6-address-no-zone; + } + description + "The ip-address-no-zone type represents an IP address and is + IP version neutral. The format of the textual representation + implies the IP version. This type does not support scoped + addresses since it does not allow zone identifiers in the + address format."; + reference + "RFC 4007: IPv6 Scoped Address Architecture"; + + } + + typedef ipv4-address-no-zone { + type ipv4-address { + pattern '[0-9\.]*'; + } + description + "An IPv4 address without a zone index. This type, derived from + ipv4-address, may be used in situations where the zone is + known from the context and hence no zone index is needed."; + } + + typedef ipv6-address-no-zone { + type ipv6-address { + pattern '[0-9a-fA-F:\.]*'; + } + description + "An IPv6 address without a zone index. This type, derived from + ipv6-address, may be used in situations where the zone is + known from the context and hence no zone index is needed."; + reference + "RFC 4291: IP Version 6 Addressing Architecture + RFC 4007: IPv6 Scoped Address Architecture + RFC 5952: A Recommendation for IPv6 Address Text + Representation"; + + } + + typedef ip-prefix { + type union { + type ipv4-prefix; + type ipv6-prefix; + } + description + "The ip-prefix type represents an IP prefix and is IP + version neutral. The format of the textual representations + implies the IP version."; + } + + typedef ipv4-prefix { + type string { + pattern + '(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])/(([0-9])|([1-2][0-9])|(3[0-2]))'; + } + description + "The ipv4-prefix type represents an IPv4 address prefix. + The prefix length is given by the number following the + slash character and must be less than or equal to 32. + + A prefix length value of n corresponds to an IP address + mask that has n contiguous 1-bits from the most + significant bit (MSB) and all other bits set to 0. + + The canonical format of an IPv4 prefix has all bits of + the IPv4 address set to zero that are not part of the + IPv4 prefix."; + } + + typedef ipv6-prefix { + type string { + pattern + '((:|[0-9a-fA-F]{0,4}):)([0-9a-fA-F]{0,4}:){0,5}((([0-9a-fA-F]{0,4}:)?(:|[0-9a-fA-F]{0,4}))|(((25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])))(/(([0-9])|([0-9]{2})|(1[0-1][0-9])|(12[0-8])))'; + pattern + '(([^:]+:){6}(([^:]+:[^:]+)|(.*\..*)))|((([^:]+:)*[^:]+)?::(([^:]+:)*[^:]+)?)(/.+)'; + } + description + "The ipv6-prefix type represents an IPv6 address prefix. + The prefix length is given by the number following the + slash character and must be less than or equal to 128. + + A prefix length value of n corresponds to an IP address + mask that has n contiguous 1-bits from the most + significant bit (MSB) and all other bits set to 0. + + The IPv6 address should have all bits that do not belong + to the prefix set to zero. + + The canonical format of an IPv6 prefix has all bits of + the IPv6 address set to zero that are not part of the + IPv6 prefix. Furthermore, the IPv6 address is represented + as defined in Section 4 of RFC 5952."; + reference + "RFC 5952: A Recommendation for IPv6 Address Text + Representation"; + + } + + typedef domain-name { + type string { + length "1..253"; + pattern + '((([a-zA-Z0-9_]([a-zA-Z0-9\-_]){0,61})?[a-zA-Z0-9]\.)*([a-zA-Z0-9_]([a-zA-Z0-9\-_]){0,61})?[a-zA-Z0-9]\.?)|\.'; + } + description + "The domain-name type represents a DNS domain name. The + name SHOULD be fully qualified whenever possible. + + Internet domain names are only loosely specified. Section + 3.5 of RFC 1034 recommends a syntax (modified in Section + 2.1 of RFC 1123). The pattern above is intended to allow + for current practice in domain name use, and some possible + future expansion. It is designed to hold various types of + domain names, including names used for A or AAAA records + (host names) and other records, such as SRV records. Note + that Internet host names have a stricter syntax (described + in RFC 952) than the DNS recommendations in RFCs 1034 and + 1123, and that systems that want to store host names in + schema nodes using the domain-name type are recommended to + adhere to this stricter standard to ensure interoperability. + + The encoding of DNS names in the DNS protocol is limited + to 255 characters. Since the encoding consists of labels + prefixed by a length bytes and there is a trailing NULL + byte, only 253 characters can appear in the textual dotted + notation. + + The description clause of schema nodes using the domain-name + type MUST describe when and how these names are resolved to + IP addresses. Note that the resolution of a domain-name value + may require to query multiple DNS records (e.g., A for IPv4 + and AAAA for IPv6). The order of the resolution process and + which DNS record takes precedence can either be defined + explicitly or may depend on the configuration of the + resolver. + + Domain-name values use the US-ASCII encoding. Their canonical + format uses lowercase US-ASCII characters. Internationalized + domain names MUST be A-labels as per RFC 5890."; + reference + "RFC 952: DoD Internet Host Table Specification + RFC 1034: Domain Names - Concepts and Facilities + RFC 1123: Requirements for Internet Hosts -- Application + and Support + RFC 2782: A DNS RR for specifying the location of services + (DNS SRV) + RFC 5890: Internationalized Domain Names in Applications + (IDNA): Definitions and Document Framework"; + + } + + typedef host { + type union { + type ip-address; + type domain-name; + } + description + "The host type represents either an IP address or a DNS + domain name."; + } + + typedef uri { + type string; + description + "The uri type represents a Uniform Resource Identifier + (URI) as defined by STD 66. + + Objects using the uri type MUST be in US-ASCII encoding, + and MUST be normalized as described by RFC 3986 Sections + 6.2.1, 6.2.2.1, and 6.2.2.2. All unnecessary + percent-encoding is removed, and all case-insensitive + characters are set to lowercase except for hexadecimal + digits, which are normalized to uppercase as described in + Section 6.2.2.1. + + The purpose of this normalization is to help provide + unique URIs. Note that this normalization is not + sufficient to provide uniqueness. Two URIs that are + textually distinct after this normalization may still be + equivalent. + + Objects using the uri type may restrict the schemes that + they permit. For example, 'data:' and 'urn:' schemes + might not be appropriate. + + A zero-length URI is not a valid URI. This can be used to + express 'URI absent' where required. + + In the value set and its semantics, this type is equivalent + to the Uri SMIv2 textual convention defined in RFC 5017."; + reference + "RFC 3986: Uniform Resource Identifier (URI): Generic Syntax + RFC 3305: Report from the Joint W3C/IETF URI Planning Interest + Group: Uniform Resource Identifiers (URIs), URLs, + and Uniform Resource Names (URNs): Clarifications + and Recommendations + RFC 5017: MIB Textual Conventions for Uniform Resource + Identifiers (URIs)"; + + } + } // module ietf-inet-types \ No newline at end of file diff --git a/swagger-generator/src/test/resources/bug_45/ietf-yang-library.yang b/swagger-generator/src/test/resources/bug_45/ietf-yang-library.yang new file mode 100644 index 0000000..b4b4741 --- /dev/null +++ b/swagger-generator/src/test/resources/bug_45/ietf-yang-library.yang @@ -0,0 +1,548 @@ +module ietf-yang-library { + yang-version 1.1; + namespace "urn:ietf:params:xml:ns:yang:ietf-yang-library"; + prefix yanglib; + + import ietf-yang-types { + prefix yang; + reference + "RFC 6991: Common YANG Data Types"; + } + import ietf-inet-types { + prefix inet; + reference + "RFC 6991: Common YANG Data Types"; + } + import ietf-datastores { + prefix ds; + reference + "RFC 8342: Network Management Datastore Architecture + (NMDA)"; + } + + organization + "IETF NETCONF (Network Configuration) Working Group"; + contact + "WG Web: + WG List: + + Author: Andy Bierman + + + Author: Martin Bjorklund + + + Author: Juergen Schoenwaelder + + + Author: Kent Watsen + + + Author: Robert Wilton + "; + description + "This module provides information about the YANG modules, + datastores, and datastore schemas used by a network + management server. + The key words 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL', 'SHALL + NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED', 'NOT RECOMMENDED', + 'MAY', and 'OPTIONAL' in this document are to be interpreted as + described in BCP 14 (RFC 2119) (RFC 8174) when, and only when, + they appear in all capitals, as shown here. + + Copyright (c) 2019 IETF Trust and the persons identified as + authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject + to the license terms contained in, the Simplified BSD License + set forth in Section 4.c of the IETF Trust's Legal Provisions + Relating to IETF Documents + (https://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC 8525; see + the RFC itself for full legal notices."; + + revision 2019-01-04 { + description + "Added support for multiple datastores according to the + Network Management Datastore Architecture (NMDA)."; + reference + "RFC 8525: YANG Library"; + } + revision 2016-04-09 { + description + "Initial revision."; + reference + "RFC 7895: YANG Module Library"; + } + + /* + * Typedefs + */ + + typedef revision-identifier { + type string { + pattern '\d{4}-\d{2}-\d{2}'; + } + description + "Represents a specific date in YYYY-MM-DD format."; + } + + /* + * Groupings + */ + grouping module-identification-leafs { + description + "Parameters for identifying YANG modules and submodules."; + leaf name { + type yang:yang-identifier; + mandatory true; + description + "The YANG module or submodule name."; + } + leaf revision { + type revision-identifier; + description + "The YANG module or submodule revision date. If no revision + statement is present in the YANG module or submodule, this + leaf is not instantiated."; + } + } + + grouping location-leaf-list { + description + "Common leaf-list parameter for the locations of modules and + submodules."; + leaf-list location { + type inet:uri; + description + "Contains a URL that represents the YANG schema + resource for this module or submodule. + + This leaf will only be present if there is a URL + available for retrieval of the schema for this entry."; + } + } + + grouping module-implementation-parameters { + description + "Parameters for describing the implementation of a module."; + leaf-list feature { + type yang:yang-identifier; + description + "List of all YANG feature names from this module that are + supported by the server, regardless whether they are defined + in the module or any included submodule."; + } + leaf-list deviation { + type leafref { + path "../../module/name"; + } + + description + "List of all YANG deviation modules used by this server to + modify the conformance of the module associated with this + entry. Note that the same module can be used for deviations + for multiple modules, so the same entry MAY appear within + multiple 'module' entries. + + This reference MUST NOT (directly or indirectly) + refer to the module being deviated. + + Robust clients may want to make sure that they handle a + situation where a module deviates itself (directly or + indirectly) gracefully."; + } + } + + grouping module-set-parameters { + description + "A set of parameters that describe a module set."; + leaf name { + type string; + description + "An arbitrary name of the module set."; + } + list module { + key "name"; + description + "An entry in this list represents a module implemented by the + server, as per Section 5.6.5 of RFC 7950, with a particular + set of supported features and deviations."; + reference + "RFC 7950: The YANG 1.1 Data Modeling Language"; + uses module-identification-leafs; + leaf namespace { + type inet:uri; + mandatory true; + description + "The XML namespace identifier for this module."; + } + uses location-leaf-list; + list submodule { + key "name"; + description + "Each entry represents one submodule within the + parent module."; + uses module-identification-leafs; + uses location-leaf-list; + } + + uses module-implementation-parameters; + } + list import-only-module { + key "name revision"; + description + "An entry in this list indicates that the server imports + reusable definitions from the specified revision of the + module but does not implement any protocol-accessible + objects from this revision. + + Multiple entries for the same module name MAY exist. This + can occur if multiple modules import the same module but + specify different revision dates in the import statements."; + leaf name { + type yang:yang-identifier; + description + "The YANG module name."; + } + leaf revision { + type union { + type revision-identifier; + type string { + length "0"; + } + } + description + "The YANG module revision date. + A zero-length string is used if no revision statement + is present in the YANG module."; + } + leaf namespace { + type inet:uri; + mandatory true; + description + "The XML namespace identifier for this module."; + } + uses location-leaf-list; + list submodule { + key "name"; + description + "Each entry represents one submodule within the + parent module."; + uses module-identification-leafs; + uses location-leaf-list; + } + } + } + + grouping yang-library-parameters { + description + "The YANG library data structure is represented as a grouping + so it can be reused in configuration or another monitoring + data structure."; + list module-set { + key "name"; + description + "A set of modules that may be used by one or more schemas. + + A module set does not have to be referentially complete, + i.e., it may define modules that contain import statements + for other modules not included in the module set."; + uses module-set-parameters; + } + list schema { + key "name"; + description + "A datastore schema that may be used by one or more + datastores. + + The schema must be valid and referentially complete, i.e., + it must contain modules to satisfy all used import + statements for all modules specified in the schema."; + leaf name { + type string; + description + "An arbitrary name of the schema."; + } + leaf-list module-set { + type leafref { + path "../../module-set/name"; + } + description + "A set of module-sets that are included in this schema. + If a non-import-only module appears in multiple module + sets, then the module revision and the associated features + and deviations must be identical."; + } + } + list datastore { + key "name"; + description + "A datastore supported by this server. + + Each datastore indicates which schema it supports. + + The server MUST instantiate one entry in this list per + specific datastore it supports. + Each datastore entry with the same datastore schema SHOULD + reference the same schema."; + leaf name { + type ds:datastore-ref; + description + "The identity of the datastore."; + } + leaf schema { + type leafref { + path "../../schema/name"; + } + mandatory true; + description + "A reference to the schema supported by this datastore. + All non-import-only modules of the schema are implemented + with their associated features and deviations."; + } + } + } + + /* + * Top-level container + */ + + container yang-library { + config false; + description + "Container holding the entire YANG library of this server."; + uses yang-library-parameters; + leaf content-id { + type string; + mandatory true; + description + "A server-generated identifier of the contents of the + '/yang-library' tree. The server MUST change the value of + this leaf if the information represented by the + '/yang-library' tree, except '/yang-library/content-id', has + changed."; + } + } + + /* + * Notifications + */ + + notification yang-library-update { + description + "Generated when any YANG library information on the + server has changed."; + leaf content-id { + type leafref { + path "/yanglib:yang-library/yanglib:content-id"; + } + mandatory true; + description + "Contains the YANG library content identifier for the updated + YANG library at the time the notification is generated."; + } + } + + /* + * Legacy groupings + */ + + grouping module-list { + status deprecated; + description + "The module data structure is represented as a grouping + so it can be reused in configuration or another monitoring + data structure."; + + grouping common-leafs { + status deprecated; + description + "Common parameters for YANG modules and submodules."; + leaf name { + type yang:yang-identifier; + status deprecated; + description + "The YANG module or submodule name."; + } + leaf revision { + type union { + type revision-identifier; + type string { + length "0"; + } + } + status deprecated; + description + "The YANG module or submodule revision date. + A zero-length string is used if no revision statement + is present in the YANG module or submodule."; + } + + } + + grouping schema-leaf { + status deprecated; + description + "Common schema leaf parameter for modules and submodules."; + leaf schema { + type inet:uri; + description + "Contains a URL that represents the YANG schema + resource for this module or submodule. + + This leaf will only be present if there is a URL + available for retrieval of the schema for this entry."; + } + } + list module { + key "name revision"; + status deprecated; + description + "Each entry represents one revision of one module + currently supported by the server."; + uses common-leafs { + status deprecated; + } + uses schema-leaf { + status deprecated; + } + leaf namespace { + type inet:uri; + mandatory true; + status deprecated; + description + "The XML namespace identifier for this module."; + } + leaf-list feature { + type yang:yang-identifier; + status deprecated; + description + "List of YANG feature names from this module that are + supported by the server, regardless of whether they are + defined in the module or any included submodule."; + } + list deviation { + key "name revision"; + status deprecated; + + description + "List of YANG deviation module names and revisions + used by this server to modify the conformance of + the module associated with this entry. Note that + the same module can be used for deviations for + multiple modules, so the same entry MAY appear + within multiple 'module' entries. + + The deviation module MUST be present in the 'module' + list, with the same name and revision values. + The 'conformance-type' value will be 'implement' for + the deviation module."; + uses common-leafs { + status deprecated; + } + } + leaf conformance-type { + type enumeration { + enum implement { + description + "Indicates that the server implements one or more + protocol-accessible objects defined in the YANG module + identified in this entry. This includes deviation + statements defined in the module. + + For YANG version 1.1 modules, there is at most one + 'module' entry with conformance type 'implement' for a + particular module name, since YANG 1.1 requires that + at most one revision of a module is implemented. + + For YANG version 1 modules, there SHOULD NOT be more + than one 'module' entry for a particular module + name."; + } + enum import { + description + "Indicates that the server imports reusable definitions + from the specified revision of the module but does + not implement any protocol-accessible objects from + this revision. + + Multiple 'module' entries for the same module name MAY + exist. This can occur if multiple modules import the + same module but specify different revision dates in + the import statements."; + } + } + mandatory true; + + status deprecated; + description + "Indicates the type of conformance the server is claiming + for the YANG module identified by this entry."; + } + list submodule { + key "name revision"; + status deprecated; + description + "Each entry represents one submodule within the + parent module."; + uses common-leafs { + status deprecated; + } + uses schema-leaf { + status deprecated; + } + } + } + } + + /* + * Legacy operational state data nodes + */ + + container modules-state { + config false; + status deprecated; + description + "Contains YANG module monitoring information."; + leaf module-set-id { + type string; + mandatory true; + status deprecated; + description + "Contains a server-specific identifier representing + the current set of modules and submodules. The + server MUST change the value of this leaf if the + information represented by the 'module' list instances + has changed."; + } + uses module-list { + status deprecated; + } + } + + /* + * Legacy notifications + */ + + notification yang-library-change { + status deprecated; + description + "Generated when the set of modules and submodules supported + by the server has changed."; + leaf module-set-id { + type leafref { + path "/yanglib:modules-state/yanglib:module-set-id"; + } + mandatory true; + status deprecated; + description + "Contains the module-set-id value representing the + set of modules and submodules supported at the server + at the time the notification is generated."; + } + } + } + diff --git a/swagger-generator/src/test/resources/bug_45/ietf-yang-schema-mount.yang b/swagger-generator/src/test/resources/bug_45/ietf-yang-schema-mount.yang new file mode 100644 index 0000000..abc1e48 --- /dev/null +++ b/swagger-generator/src/test/resources/bug_45/ietf-yang-schema-mount.yang @@ -0,0 +1,256 @@ +module ietf-yang-schema-mount { + yang-version 1.1; + namespace "urn:ietf:params:xml:ns:yang:ietf-yang-schema-mount"; + prefix yangmnt; + + import ietf-inet-types { + prefix inet; + reference + "RFC 6991: Common YANG Data Types"; + } + + import ietf-yang-types { + prefix yang; + reference + "RFC 6991: Common YANG Data Types"; + } + + import ietf-yang-library { + prefix yanglib; + reference + "RFC 7895: YANG Module Library"; + } + + organization + "IETF NETMOD (NETCONF Data Modeling Language) Working Group"; + + contact + "WG Web: + WG List: + + Editor: Martin Bjorklund + + + Editor: Ladislav Lhotka + "; + + description + "This module defines a YANG extension statement that can be used + to incorporate data models defined in other YANG modules in a + module. It also defines operational state data that specify the + overall structure of the data model. + + Copyright (c) 2017 IETF Trust and the persons identified as + authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject to + the license terms contained in, the Simplified BSD License set + forth in Section 4.c of the IETF Trust's Legal Provisions + Relating to IETF Documents + (https://trustee.ietf.org/license-info). + + The key words 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL', 'SHALL + NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED', 'MAY', and + 'OPTIONAL' in the module text are to be interpreted as described + in RFC 2119 (https://tools.ietf.org/html/rfc2119). + + This version of this YANG module is part of RFC XXXX + (https://tools.ietf.org/html/rfcXXXX); see the RFC itself for + full legal notices."; + + revision 2017-10-09 { + description + "Initial revision."; + reference + "RFC XXXX: YANG Schema Mount"; + } + + /* + * Extensions + */ + + extension mount-point { + argument label; + description + "The argument 'label' is a YANG identifier, i.e., it is of the + type 'yang:yang-identifier'. + + The 'mount-point' statement MUST NOT be used in a YANG + version 1 module, neither explicitly nor via a 'uses' + statement. + + The 'mount-point' statement MAY be present as a substatement + of 'container' and 'list', and MUST NOT be present elsewhere. + There MUST NOT be more than one 'mount-point' statement in a + given 'container' or 'list' statement. + + If a mount point is defined within a grouping, its label is + bound to the module where the grouping is used. + + A mount point defines a place in the node hierarchy where + other data models may be attached. A server that implements a + module with a mount point populates the + /schema-mounts/mount-point list with detailed information on + which data models are mounted at each mount point. + + Note that the 'mount-point' statement does not define a new + data node."; + } + + /* + * Groupings + */ + + grouping mount-point-list { + description + "This grouping is used inside the 'schema-mounts' container and + inside the 'schema' list."; + list mount-point { + key "module label"; + description + "Each entry of this list specifies a schema for a particular + mount point. + + Each mount point MUST be defined using the 'mount-point' + extension in one of the modules listed in the corresponding + YANG library instance with conformance type 'implement'. The + corresponding YANG library instance is: + + - standard YANG library state data as defined in RFC 7895, + if the 'mount-point' list is a child of 'schema-mounts', + + - the contents of the sibling 'yanglib:modules-state' + container, if the 'mount-point' list is a child of + 'schema'."; + leaf module { + type yang:yang-identifier; + description + "Name of a module containing the mount point."; + } + leaf label { + type yang:yang-identifier; + description + "Label of the mount point defined using the 'mount-point' + extension."; + } + leaf config { + type boolean; + default "true"; + description + "If this leaf is set to 'false', then all data nodes in the + mounted schema are read-only (config false), regardless of + their 'config' property."; + } + choice schema-ref { + mandatory true; + description + "Alternatives for specifying the schema."; + leaf inline { + type empty; + description + "This leaf indicates that the server has mounted + 'ietf-yang-library' and 'ietf-schema-mount' at the mount + point, and their instantiation (i.e., state data + containers 'yanglib:modules-state' and 'schema-mounts') + provides the information about the mounted schema."; + } + list use-schema { + key "name"; + min-elements 1; + description + "Each entry of this list contains a reference to a schema + defined in the /schema-mounts/schema list."; + leaf name { + type leafref { + path "/schema-mounts/schema/name"; + } + description + "Name of the referenced schema."; + } + leaf-list parent-reference { + type yang:xpath1.0; + description + "Entries of this leaf-list are XPath 1.0 expressions + that are evaluated in the following context: + + - The context node is the node in the parent data tree + where the mount-point is defined. + + - The accessible tree is the parent data tree + *without* any nodes defined in modules that are + mounted inside the parent schema. + + - The context position and context size are both equal + to 1. + + - The set of variable bindings is empty. + + - The function library is the core function library + defined in [XPath] and the functions defined in + Section 10 of [RFC7950]. + + - The set of namespace declarations is defined by the + 'namespace' list under 'schema-mounts'. + + Each XPath expression MUST evaluate to a nodeset + (possibly empty). For the purposes of evaluating XPath + expressions whose context nodes are defined in the + mounted schema, the union of all these nodesets + together with ancestor nodes are added to the + accessible data tree."; + } + } + } + } + } + + /* + * State data nodes + */ + + container schema-mounts { + config false; + description + "Contains information about the structure of the overall + mounted data model implemented in the server."; + list namespace { + key "prefix"; + description + "This list provides a mapping of namespace prefixes that are + used in XPath expressions of 'parent-reference' leafs to the + corresponding namespace URI references."; + leaf prefix { + type yang:yang-identifier; + description + "Namespace prefix."; + } + leaf uri { + type inet:uri; + description + "Namespace URI reference."; + } + } + uses mount-point-list; + list schema { + key "name"; + description + "Each entry specifies a schema that can be mounted at a mount + point. The schema information consists of two parts: + + - an instance of YANG library that defines YANG modules used + in the schema, + + - mount-point list with content identical to the top-level + mount-point list (this makes the schema structure + recursive)."; + leaf name { + type string; + description + "Arbitrary name of the schema entry."; + } + uses yanglib:module-list; + uses mount-point-list; + } + } +} \ No newline at end of file diff --git a/swagger-generator/src/test/resources/bug_45/ietf-yang-types.yang b/swagger-generator/src/test/resources/bug_45/ietf-yang-types.yang new file mode 100644 index 0000000..b3d5bfd --- /dev/null +++ b/swagger-generator/src/test/resources/bug_45/ietf-yang-types.yang @@ -0,0 +1,489 @@ +module ietf-yang-types { + + yang-version 1; + + namespace + "urn:ietf:params:xml:ns:yang:ietf-yang-types"; + + prefix yang; + + organization + "IETF NETMOD (NETCONF Data Modeling Language) Working Group"; + + contact + "WG Web: + WG List: + + WG Chair: David Kessens + + + WG Chair: Juergen Schoenwaelder + + + Editor: Juergen Schoenwaelder + "; + + description + "This module contains a collection of generally useful derived + YANG data types. + + Copyright (c) 2013 IETF Trust and the persons identified as + authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject + to the license terms contained in, the Simplified BSD License + set forth in Section 4.c of the IETF Trust's Legal Provisions + Relating to IETF Documents + (http://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC 6991; see + the RFC itself for full legal notices."; + + revision "2013-07-15" { + description + "This revision adds the following new data types: + - yang-identifier + - hex-string + - uuid + - dotted-quad"; + reference + "RFC 6991: Common YANG Data Types"; + + } + + revision "2010-09-24" { + description "Initial revision."; + reference + "RFC 6021: Common YANG Data Types"; + + } + + + typedef counter32 { + type uint32; + description + "The counter32 type represents a non-negative integer + that monotonically increases until it reaches a + maximum value of 2^32-1 (4294967295 decimal), when it + wraps around and starts increasing again from zero. + + Counters have no defined 'initial' value, and thus, a + single value of a counter has (in general) no information + content. Discontinuities in the monotonically increasing + value normally occur at re-initialization of the + management system, and at other times as specified in the + description of a schema node using this type. If such + other times can occur, for example, the creation of + a schema node of type counter32 at times other than + re-initialization, then a corresponding schema node + should be defined, with an appropriate type, to indicate + the last discontinuity. + + The counter32 type should not be used for configuration + schema nodes. A default statement SHOULD NOT be used in + combination with the type counter32. + + In the value set and its semantics, this type is equivalent + to the Counter32 type of the SMIv2."; + reference + "RFC 2578: Structure of Management Information Version 2 + (SMIv2)"; + + } + + typedef zero-based-counter32 { + type counter32; + default "0"; + description + "The zero-based-counter32 type represents a counter32 + that has the defined 'initial' value zero. + + A schema node of this type will be set to zero (0) on creation + and will thereafter increase monotonically until it reaches + a maximum value of 2^32-1 (4294967295 decimal), when it + wraps around and starts increasing again from zero. + + Provided that an application discovers a new schema node + of this type within the minimum time to wrap, it can use the + 'initial' value as a delta. It is important for a management + station to be aware of this minimum time and the actual time + between polls, and to discard data if the actual time is too + long or there is no defined minimum time. + + In the value set and its semantics, this type is equivalent + to the ZeroBasedCounter32 textual convention of the SMIv2."; + reference + "RFC 4502: Remote Network Monitoring Management Information + Base Version 2"; + + } + + typedef counter64 { + type uint64; + description + "The counter64 type represents a non-negative integer + that monotonically increases until it reaches a + maximum value of 2^64-1 (18446744073709551615 decimal), + when it wraps around and starts increasing again from zero. + + Counters have no defined 'initial' value, and thus, a + single value of a counter has (in general) no information + content. Discontinuities in the monotonically increasing + value normally occur at re-initialization of the + management system, and at other times as specified in the + description of a schema node using this type. If such + other times can occur, for example, the creation of + a schema node of type counter64 at times other than + re-initialization, then a corresponding schema node + should be defined, with an appropriate type, to indicate + the last discontinuity. + + The counter64 type should not be used for configuration + schema nodes. A default statement SHOULD NOT be used in + combination with the type counter64. + + In the value set and its semantics, this type is equivalent + to the Counter64 type of the SMIv2."; + reference + "RFC 2578: Structure of Management Information Version 2 + (SMIv2)"; + + } + + typedef zero-based-counter64 { + type counter64; + default "0"; + description + "The zero-based-counter64 type represents a counter64 that + has the defined 'initial' value zero. + + + + + A schema node of this type will be set to zero (0) on creation + and will thereafter increase monotonically until it reaches + a maximum value of 2^64-1 (18446744073709551615 decimal), + when it wraps around and starts increasing again from zero. + + Provided that an application discovers a new schema node + of this type within the minimum time to wrap, it can use the + 'initial' value as a delta. It is important for a management + station to be aware of this minimum time and the actual time + between polls, and to discard data if the actual time is too + long or there is no defined minimum time. + + In the value set and its semantics, this type is equivalent + to the ZeroBasedCounter64 textual convention of the SMIv2."; + reference + "RFC 2856: Textual Conventions for Additional High Capacity + Data Types"; + + } + + typedef gauge32 { + type uint32; + description + "The gauge32 type represents a non-negative integer, which + may increase or decrease, but shall never exceed a maximum + value, nor fall below a minimum value. The maximum value + cannot be greater than 2^32-1 (4294967295 decimal), and + the minimum value cannot be smaller than 0. The value of + a gauge32 has its maximum value whenever the information + being modeled is greater than or equal to its maximum + value, and has its minimum value whenever the information + being modeled is smaller than or equal to its minimum value. + If the information being modeled subsequently decreases + below (increases above) the maximum (minimum) value, the + gauge32 also decreases (increases). + + In the value set and its semantics, this type is equivalent + to the Gauge32 type of the SMIv2."; + reference + "RFC 2578: Structure of Management Information Version 2 + (SMIv2)"; + + } + + typedef gauge64 { + type uint64; + description + "The gauge64 type represents a non-negative integer, which + may increase or decrease, but shall never exceed a maximum + value, nor fall below a minimum value. The maximum value + cannot be greater than 2^64-1 (18446744073709551615), and + the minimum value cannot be smaller than 0. The value of + a gauge64 has its maximum value whenever the information + being modeled is greater than or equal to its maximum + value, and has its minimum value whenever the information + being modeled is smaller than or equal to its minimum value. + If the information being modeled subsequently decreases + below (increases above) the maximum (minimum) value, the + gauge64 also decreases (increases). + + In the value set and its semantics, this type is equivalent + to the CounterBasedGauge64 SMIv2 textual convention defined + in RFC 2856"; + reference + "RFC 2856: Textual Conventions for Additional High Capacity + Data Types"; + + } + + typedef object-identifier { + type string { + pattern + '(([0-1](\.[1-3]?[0-9]))|(2\.(0|([1-9]\d*))))(\.(0|([1-9]\d*)))*'; + } + description + "The object-identifier type represents administratively + assigned names in a registration-hierarchical-name tree. + + Values of this type are denoted as a sequence of numerical + non-negative sub-identifier values. Each sub-identifier + value MUST NOT exceed 2^32-1 (4294967295). Sub-identifiers + are separated by single dots and without any intermediate + whitespace. + + The ASN.1 standard restricts the value space of the first + sub-identifier to 0, 1, or 2. Furthermore, the value space + of the second sub-identifier is restricted to the range + 0 to 39 if the first sub-identifier is 0 or 1. Finally, + the ASN.1 standard requires that an object identifier + has always at least two sub-identifiers. The pattern + captures these restrictions. + + Although the number of sub-identifiers is not limited, + module designers should realize that there may be + implementations that stick with the SMIv2 limit of 128 + sub-identifiers. + + This type is a superset of the SMIv2 OBJECT IDENTIFIER type + since it is not restricted to 128 sub-identifiers. Hence, + this type SHOULD NOT be used to represent the SMIv2 OBJECT + IDENTIFIER type; the object-identifier-128 type SHOULD be + used instead."; + reference + "ISO9834-1: Information technology -- Open Systems + Interconnection -- Procedures for the operation of OSI + Registration Authorities: General procedures and top + arcs of the ASN.1 Object Identifier tree"; + + } + + typedef object-identifier-128 { + type object-identifier { + pattern '\d*(\.\d*){1,127}'; + } + description + "This type represents object-identifiers restricted to 128 + sub-identifiers. + + In the value set and its semantics, this type is equivalent + to the OBJECT IDENTIFIER type of the SMIv2."; + reference + "RFC 2578: Structure of Management Information Version 2 + (SMIv2)"; + + } + + typedef yang-identifier { + type string { + length "1..max"; + pattern '[a-zA-Z_][a-zA-Z0-9\-_.]*'; + pattern + '.|..|[^xX].*|.[^mM].*|..[^lL].*'; + } + description + "A YANG identifier string as defined by the 'identifier' + rule in Section 12 of RFC 6020. An identifier must + start with an alphabetic character or an underscore + followed by an arbitrary sequence of alphabetic or + numeric characters, underscores, hyphens, or dots. + + A YANG identifier MUST NOT start with any possible + combination of the lowercase or uppercase character + sequence 'xml'."; + reference + "RFC 6020: YANG - A Data Modeling Language for the Network + Configuration Protocol (NETCONF)"; + + } + + typedef date-and-time { + type string { + pattern + '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[\+\-]\d{2}:\d{2})'; + } + description + "The date-and-time type is a profile of the ISO 8601 + standard for representation of dates and times using the + Gregorian calendar. The profile is defined by the + date-time production in Section 5.6 of RFC 3339. + + The date-and-time type is compatible with the dateTime XML + schema type with the following notable exceptions: + + (a) The date-and-time type does not allow negative years. + + (b) The date-and-time time-offset -00:00 indicates an unknown + time zone (see RFC 3339) while -00:00 and +00:00 and Z + all represent the same time zone in dateTime. + + (c) The canonical format (see below) of data-and-time values + differs from the canonical format used by the dateTime XML + schema type, which requires all times to be in UTC using + the time-offset 'Z'. + + This type is not equivalent to the DateAndTime textual + convention of the SMIv2 since RFC 3339 uses a different + separator between full-date and full-time and provides + higher resolution of time-secfrac. + + The canonical format for date-and-time values with a known time + zone uses a numeric time zone offset that is calculated using + the device's configured known offset to UTC time. A change of + the device's offset to UTC time will cause date-and-time values + to change accordingly. Such changes might happen periodically + in case a server follows automatically daylight saving time + (DST) time zone offset changes. The canonical format for + date-and-time values with an unknown time zone (usually + referring to the notion of local time) uses the time-offset + -00:00."; + reference + "RFC 3339: Date and Time on the Internet: Timestamps + RFC 2579: Textual Conventions for SMIv2 + XSD-TYPES: XML Schema Part 2: Datatypes Second Edition"; + + } + + typedef timeticks { + type uint32; + description + "The timeticks type represents a non-negative integer that + represents the time, modulo 2^32 (4294967296 decimal), in + hundredths of a second between two epochs. When a schema + node is defined that uses this type, the description of + the schema node identifies both of the reference epochs. + + In the value set and its semantics, this type is equivalent + to the TimeTicks type of the SMIv2."; + reference + "RFC 2578: Structure of Management Information Version 2 + (SMIv2)"; + + } + + typedef timestamp { + type timeticks; + description + "The timestamp type represents the value of an associated + timeticks schema node at which a specific occurrence + happened. The specific occurrence must be defined in the + description of any schema node defined using this type. When + the specific occurrence occurred prior to the last time the + associated timeticks attribute was zero, then the timestamp + value is zero. Note that this requires all timestamp values + to be reset to zero when the value of the associated timeticks + attribute reaches 497+ days and wraps around to zero. + + The associated timeticks schema node must be specified + in the description of any schema node using this type. + + In the value set and its semantics, this type is equivalent + to the TimeStamp textual convention of the SMIv2."; + reference + "RFC 2579: Textual Conventions for SMIv2"; + + } + + typedef phys-address { + type string { + pattern + '([0-9a-fA-F]{2}(:[0-9a-fA-F]{2})*)?'; + } + description + "Represents media- or physical-level addresses represented + as a sequence octets, each octet represented by two hexadecimal + numbers. Octets are separated by colons. The canonical + representation uses lowercase characters. + + In the value set and its semantics, this type is equivalent + to the PhysAddress textual convention of the SMIv2."; + reference + "RFC 2579: Textual Conventions for SMIv2"; + + } + + typedef mac-address { + type string { + pattern + '[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}'; + } + description + "The mac-address type represents an IEEE 802 MAC address. + The canonical representation uses lowercase characters. + + In the value set and its semantics, this type is equivalent + to the MacAddress textual convention of the SMIv2."; + reference + "IEEE 802: IEEE Standard for Local and Metropolitan Area + Networks: Overview and Architecture + RFC 2579: Textual Conventions for SMIv2"; + + } + + typedef xpath1.0 { + type string; + description + "This type represents an XPATH 1.0 expression. + + When a schema node is defined that uses this type, the + description of the schema node MUST specify the XPath + context in which the XPath expression is evaluated."; + reference + "XPATH: XML Path Language (XPath) Version 1.0"; + + } + + typedef hex-string { + type string { + pattern + '([0-9a-fA-F]{2}(:[0-9a-fA-F]{2})*)?'; + } + description + "A hexadecimal string with octets represented as hex digits + separated by colons. The canonical representation uses + lowercase characters."; + } + + typedef uuid { + type string { + pattern + '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'; + } + description + "A Universally Unique IDentifier in the string representation + defined in RFC 4122. The canonical representation uses + lowercase characters. + + The following is an example of a UUID in string representation: + f81d4fae-7dec-11d0-a765-00a0c91e6bf6 + "; + reference + "RFC 4122: A Universally Unique IDentifier (UUID) URN + Namespace"; + + } + + typedef dotted-quad { + type string { + pattern + '(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])'; + } + description + "An unsigned 32-bit number expressed in the dotted-quad + notation, i.e., four octets written as decimal numbers + and separated with the '.' (full stop) character."; + } + } // module ietf-yang-types diff --git a/swagger-generator/src/test/resources/bug_45/list-manager.yang b/swagger-generator/src/test/resources/bug_45/list-manager.yang new file mode 100644 index 0000000..eacbca4 --- /dev/null +++ b/swagger-generator/src/test/resources/bug_45/list-manager.yang @@ -0,0 +1,70 @@ +module list-manager { + namespace "urn:demo:list-manager"; + prefix "lm"; + yang-version 1.1; + import ietf-yang-types {prefix yang;} + import ietf-yang-schema-mount { prefix "yangmnt"; } + + revision "2022-03-12" { + description "First cut"; + } + typedef list-entry-status { + type enumeration { + enum active; + enum disabled; + } + } + grouping list-config-parameters { + leaf name { + type string; + } + leaf status { + type list-entry-status; + } + } + grouping list-state-attributes { + leaf in-use { + config false; + type boolean; + } + leaf created-at { + config false; + type yang:date-and-time; + } + } + list list-entry { + key name; + uses list-config-parameters; + uses list-state-attributes; + leaf entry-type { + type string; + } + container specific-config { + yangmnt:mount-point entry-type-specific-data; + } + action list-entry-action { + input { + leaf entry-action-input { + type string; + } + } + output { + leaf entry-action-output { + type string; + } + } + } + } + rpc list-manager-operation { + input { + leaf rpc-operation-input { + type string; + } + } + output { + leaf rpc-operation-output { + type string; + } + } + } +} \ No newline at end of file diff --git a/swagger-maven-plugin/pom.xml b/swagger-maven-plugin/pom.xml index 68bc673..5e67db9 100644 --- a/swagger-maven-plugin/pom.xml +++ b/swagger-maven-plugin/pom.xml @@ -15,7 +15,7 @@ yangtools com.mrv.yangtools - 2.1.0 + 2.2.0 4.0.0