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 @@
yangtoolscom.mrv.yangtools
- 2.1.0
+ 2.2.04.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 @@
yangtoolscom.mrv.yangtools
- 2.1.0
+ 2.2.04.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 @@
examplescom.mrv.yangtools
- 2.1.0
+ 2.2.04.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 @@
yangtoolscom.mrv.yangtools
- 2.1.0
+ 2.2.04.0.0pom
diff --git a/pom.xml b/pom.xml
index 700fcbd..146b547 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,7 +16,7 @@
com.mrv.yangtoolsyangtools
- 2.1.0
+ 2.2.0swagger-generatorcommon
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 @@
yangtoolscom.mrv.yangtools
- 2.1.0
+ 2.2.04.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 @@
yangtoolscom.mrv.yangtools
- 2.1.0
+ 2.2.04.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 extends org.open
//assign default strategy
strategy(Strategy.optimizing);
- //no exposed swagger API
- target.info(new Info());
+ // set default path handler builder (avoid NPE if caller doesn't set one)
+ try {
+ AbstractPathHandlerBuilder defaultBuilder = new com.mrv.yangtools.codegen.impl.path.rfc8040.PathHandlerBuilder();
+ this.pathHandlerBuilder = defaultBuilder;
+ } catch (Throwable t) {
+ // fallback: leave null and allow caller to set pathHandler explicitly
+ this.pathHandlerBuilder = null;
+ }
- pathHandlerBuilder = new com.mrv.yangtools.codegen.impl.path.rfc8040.PathHandlerBuilder();
+ // no exposed swagger API
+ target.info(new Info());
//default postprocessors
postprocessor = new ReplaceEmptyWithParent();
}
@@ -262,7 +273,31 @@ public SwaggerGenerator produces(String produces) {
public SwaggerGenerator maxDepth(int maxDepth) {
this.maxDepth = maxDepth;
return this;
- }
+ }
+
+ /**
+ * Provide mappings for mount-point extension: label -> 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:
+ *
+ *
Resolve the effective statement via {@code asEffectiveStatement()} or
+ * {@code getEffectiveStatement()} (public, then declared).
+ *
Collect all Collection/Map/array-typed members from the effective statement
+ * (public methods → declared methods → declared fields).
+ *
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
+ *
+ *
+ *
+ *
+ *
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