2 * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.restconf.openapi.impl;
10 import static org.opendaylight.restconf.openapi.impl.BaseYangOpenApiGenerator.MODULE_NAME_SUFFIX;
11 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.COMPONENTS_PREFIX;
13 import com.google.common.collect.Range;
14 import com.google.common.collect.RangeSet;
15 import dk.brics.automaton.RegExp;
16 import java.io.IOException;
17 import java.math.BigDecimal;
18 import java.math.BigInteger;
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.HashMap;
22 import java.util.List;
24 import java.util.Optional;
25 import java.util.regex.Pattern;
26 import java.util.stream.Collectors;
27 import org.eclipse.jdt.annotation.NonNull;
28 import org.opendaylight.restconf.openapi.model.Property;
29 import org.opendaylight.restconf.openapi.model.Schema;
30 import org.opendaylight.restconf.openapi.model.Xml;
31 import org.opendaylight.yangtools.yang.common.AbstractQName;
32 import org.opendaylight.yangtools.yang.common.Decimal64;
33 import org.opendaylight.yangtools.yang.common.QName;
34 import org.opendaylight.yangtools.yang.common.XMLNamespace;
35 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
36 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
37 import org.opendaylight.yangtools.yang.model.api.AnydataSchemaNode;
38 import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode;
39 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
40 import org.opendaylight.yangtools.yang.model.api.ContainerLike;
41 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
43 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
45 import org.opendaylight.yangtools.yang.model.api.ElementCountConstraint;
46 import org.opendaylight.yangtools.yang.model.api.ElementCountConstraintAware;
47 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
48 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
49 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
50 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
51 import org.opendaylight.yangtools.yang.model.api.MandatoryAware;
52 import org.opendaylight.yangtools.yang.model.api.Module;
53 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
54 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
55 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
56 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
57 import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
58 import org.opendaylight.yangtools.yang.model.api.meta.ModelStatement;
59 import org.opendaylight.yangtools.yang.model.api.type.BinaryTypeDefinition;
60 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition;
61 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition.Bit;
62 import org.opendaylight.yangtools.yang.model.api.type.BooleanTypeDefinition;
63 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
64 import org.opendaylight.yangtools.yang.model.api.type.EmptyTypeDefinition;
65 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition;
66 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition.EnumPair;
67 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
68 import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition;
69 import org.opendaylight.yangtools.yang.model.api.type.Int16TypeDefinition;
70 import org.opendaylight.yangtools.yang.model.api.type.Int32TypeDefinition;
71 import org.opendaylight.yangtools.yang.model.api.type.Int64TypeDefinition;
72 import org.opendaylight.yangtools.yang.model.api.type.Int8TypeDefinition;
73 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
74 import org.opendaylight.yangtools.yang.model.api.type.PatternConstraint;
75 import org.opendaylight.yangtools.yang.model.api.type.RangeConstraint;
76 import org.opendaylight.yangtools.yang.model.api.type.RangeRestrictedTypeDefinition;
77 import org.opendaylight.yangtools.yang.model.api.type.StringTypeDefinition;
78 import org.opendaylight.yangtools.yang.model.api.type.Uint16TypeDefinition;
79 import org.opendaylight.yangtools.yang.model.api.type.Uint32TypeDefinition;
80 import org.opendaylight.yangtools.yang.model.api.type.Uint64TypeDefinition;
81 import org.opendaylight.yangtools.yang.model.api.type.Uint8TypeDefinition;
82 import org.opendaylight.yangtools.yang.model.api.type.UnionTypeDefinition;
83 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
84 import org.slf4j.Logger;
85 import org.slf4j.LoggerFactory;
88 * Generates JSON Schema for data defined in YANG. This class is not thread-safe.
90 public final class DefinitionGenerator {
92 private static final Logger LOG = LoggerFactory.getLogger(DefinitionGenerator.class);
94 private static final String TYPE_KEY = "type";
95 private static final String ARRAY_TYPE = "array";
96 public static final String INPUT = "input";
97 public static final String INPUT_SUFFIX = "_input";
98 public static final String OUTPUT = "output";
99 public static final String OUTPUT_SUFFIX = "_output";
100 private static final String STRING_TYPE = "string";
101 private static final String OBJECT_TYPE = "object";
102 private static final String NUMBER_TYPE = "number";
103 private static final String INTEGER_TYPE = "integer";
104 private static final String INT32_FORMAT = "int32";
105 private static final String INT64_FORMAT = "int64";
106 private static final String BOOLEAN_TYPE = "boolean";
107 // Special characters used in Automaton.
108 // See https://www.brics.dk/automaton/doc/dk/brics/automaton/RegExp.html
109 private static final Pattern AUTOMATON_SPECIAL_CHARACTERS = Pattern.compile("[@&\"<>#~]");
110 // Adaptation from YANG regex to Automaton regex
111 // See https://github.com/mifmif/Generex/blob/master/src/main/java/com/mifmif/common/regex/Generex.java
112 private static final Map<String, String> PREDEFINED_CHARACTER_CLASSES = Map.of("\\\\d", "[0-9]",
113 "\\\\D", "[^0-9]", "\\\\s", "[ \t\n\f\r]", "\\\\S", "[^ \t\n\f\r]",
114 "\\\\w", "[a-zA-Z_0-9]", "\\\\W", "[^a-zA-Z_0-9]");
116 private DefinitionGenerator() {
121 * Creates Json definitions from provided module according to openapi spec.
123 * @param module - Yang module to be converted
124 * @param schemaContext - SchemaContext of all Yang files used by Api Doc
125 * @param definitionNames - Store for definition names
126 * @return {@link Map} containing data used for creating examples and definitions in OpenAPI documentation
127 * @throws IOException if I/O operation fails
129 private static Map<String, Schema> convertToSchemas(final Module module, final EffectiveModelContext schemaContext,
130 final Map<String, Schema> definitions, final DefinitionNames definitionNames) throws IOException {
131 processContainersAndLists(module, definitions, definitionNames, schemaContext);
132 processRPCs(module, definitions, definitionNames, schemaContext);
137 public static Map<String, Schema> convertToSchemas(final Module module, final EffectiveModelContext schemaContext,
138 final DefinitionNames definitionNames, final boolean isForSingleModule)
140 final Map<String, Schema> definitions = new HashMap<>();
141 if (isForSingleModule) {
142 definitionNames.addUnlinkedName(module.getName() + MODULE_NAME_SUFFIX);
144 return convertToSchemas(module, schemaContext, definitions, definitionNames);
147 private static boolean isSchemaNodeMandatory(final DataSchemaNode node) {
148 // https://www.rfc-editor.org/rfc/rfc7950#page-14
149 // mandatory node: A mandatory node is one of:
150 if (node instanceof ContainerSchemaNode containerNode) {
151 // A container node without a "presence" statement and that has at least one mandatory node as a child.
152 if (containerNode.isPresenceContainer()) {
155 for (final DataSchemaNode childNode : containerNode.getChildNodes()) {
156 if (childNode instanceof MandatoryAware mandatoryAware && mandatoryAware.isMandatory()) {
161 // A list or leaf-list node with a "min-elements" statement with a value greater than zero.
162 return node instanceof ElementCountConstraintAware constraintAware
163 && constraintAware.getElementCountConstraint()
164 .map(ElementCountConstraint::getMinElements)
169 private static void processContainersAndLists(final Module module, final Map<String, Schema> definitions,
170 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext) throws IOException {
171 final String moduleName = module.getName();
172 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
173 for (final DataSchemaNode childNode : module.getChildNodes()) {
174 stack.enterSchemaTree(childNode.getQName());
175 // For every container and list in the module
176 if (childNode instanceof ContainerSchemaNode || childNode instanceof ListSchemaNode) {
177 processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
178 stack, module, true);
179 processActionNodeContainer(childNode, moduleName, definitions, definitionNames, stack, module);
185 private static void processActionNodeContainer(final DataSchemaNode childNode, final String moduleName,
186 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
187 final SchemaInferenceStack stack, final Module module) throws IOException {
188 for (final ActionDefinition actionDef : ((ActionNodeContainer) childNode).getActions()) {
189 stack.enterSchemaTree(actionDef.getQName());
190 processOperations(actionDef, moduleName, definitions, definitionNames, stack, module);
195 private static void processRPCs(final Module module, final Map<String, Schema> definitions,
196 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext) throws IOException {
197 final String moduleName = module.getName();
198 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
199 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
200 stack.enterSchemaTree(rpcDefinition.getQName());
201 processOperations(rpcDefinition, moduleName, definitions, definitionNames, stack, module);
206 private static void processOperations(final OperationDefinition operationDef, final String parentName,
207 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
208 final SchemaInferenceStack stack, final Module module) throws IOException {
209 final String operationName = operationDef.getQName().getLocalName();
210 processOperationInputOutput(operationDef.getInput(), operationName, parentName, true, definitions,
211 definitionNames, stack, module);
212 processOperationInputOutput(operationDef.getOutput(), operationName, parentName, false, definitions,
213 definitionNames, stack, module);
216 private static void processOperationInputOutput(final ContainerLike container, final String operationName,
217 final String parentName, final boolean isInput, final Map<String, Schema> definitions,
218 final DefinitionNames definitionNames, final SchemaInferenceStack stack,
219 final Module module) throws IOException {
220 stack.enterSchemaTree(container.getQName());
221 if (!container.getChildNodes().isEmpty()) {
222 final String filename = parentName + "_" + operationName + (isInput ? INPUT_SUFFIX : OUTPUT_SUFFIX);
223 final Schema.Builder childSchemaBuilder = new Schema.Builder()
226 .xml(new Xml(isInput ? INPUT : OUTPUT, container.getQName().getNamespace().toString(), null));
227 processChildren(childSchemaBuilder, container.getChildNodes(), parentName, definitions, definitionNames,
228 stack, module, false);
229 final String discriminator =
230 definitionNames.pickDiscriminator(container, List.of(filename));
231 definitions.put(filename + discriminator, childSchemaBuilder.build());
236 private static Property processRef(final String filename, final String discriminator,
237 final SchemaNode schemaNode) {
238 final Property.Builder dataNodeProperties = new Property.Builder();
239 final String name = filename + discriminator;
240 final String ref = COMPONENTS_PREFIX + name;
242 if (schemaNode instanceof ListSchemaNode node) {
243 dataNodeProperties.type(ARRAY_TYPE);
244 final Property items = new Property.Builder().ref(ref).build();
245 dataNodeProperties.items(items);
246 dataNodeProperties.description(schemaNode.getDescription().orElse(""));
247 if (node.getElementCountConstraint().isPresent()) {
248 final var minElements = node.getElementCountConstraint().orElseThrow().getMinElements();
249 dataNodeProperties.minItems(minElements);
250 dataNodeProperties.maxItems(node.getElementCountConstraint().orElseThrow().getMaxElements());
251 if (minElements != null) {
252 dataNodeProperties.example(createExamples(node, minElements));
257 Description can't be added, because nothing allowed alongside $ref.
258 allOf is not an option, because ServiceNow can't parse it.
260 dataNodeProperties.ref(ref);
263 return dataNodeProperties.build();
266 private static List<Map<String, Object>> createExamples(final ListSchemaNode node,
267 @NonNull final Integer minElements) {
268 final var firstExampleMap = prepareFirstListExample(node);
269 final var examples = new ArrayList<Map<String, Object>>();
270 examples.add(firstExampleMap);
272 final var unqiueContraintsNameSet = node.getUniqueConstraints().stream()
273 .map(ModelStatement::argument)
274 .flatMap(uniqueSt -> uniqueSt.stream()
275 .map(schemaNI -> schemaNI.lastNodeIdentifier().getLocalName()))
276 .collect(Collectors.toSet());
277 final var keysNameSet = node.getKeyDefinition().stream()
278 .map(AbstractQName::getLocalName)
279 .collect(Collectors.toSet());
280 for (int i = 1; i < minElements; i++) {
281 final var exampleMap = new HashMap<String, Object>();
282 for (final var example : firstExampleMap.entrySet()) {
283 final Object exampleValue;
284 if (keysNameSet.contains(example.getKey()) || unqiueContraintsNameSet.contains(example.getKey())) {
285 exampleValue = editExample(example.getValue(), i);
287 exampleValue = example.getValue();
289 exampleMap.put(example.getKey(), exampleValue);
291 examples.add(exampleMap);
296 private static HashMap<String, Object> prepareFirstListExample(final ListSchemaNode node) {
297 final var childNodes = node.getChildNodes();
298 final var firstExampleMap = new HashMap<String, Object>();
299 // Cycle for each child node
300 for (final var childNode : childNodes) {
301 if (childNode instanceof TypedDataSchemaNode leafSchemaNode) {
302 final var property = new Property.Builder();
303 processTypeDef(leafSchemaNode.getType(), leafSchemaNode, property, null);
304 final var exampleValue = property.build().example();
305 if (exampleValue != null) {
306 firstExampleMap.put(leafSchemaNode.getQName().getLocalName(), exampleValue);
310 return firstExampleMap;
313 private static Object editExample(final Object exampleValue, final int edit) {
314 if (exampleValue instanceof String string) {
315 return string + "_" + edit;
316 } else if (exampleValue instanceof Integer number) {
317 return number + edit;
318 } else if (exampleValue instanceof Long number) {
319 return number + edit;
320 } else if (exampleValue instanceof Decimal64 number) {
321 return Decimal64.valueOf(BigDecimal.valueOf(number.intValue() + edit));
326 private static Property processDataNodeContainer(final DataNodeContainer dataNode, final String parentName,
327 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
328 final SchemaInferenceStack stack, final Module module, final boolean isParentConfig) throws IOException {
329 final Collection<? extends DataSchemaNode> containerChildren = dataNode.getChildNodes();
330 final SchemaNode schemaNode = (SchemaNode) dataNode;
331 final String localName = schemaNode.getQName().getLocalName();
332 final String nodeName = parentName + "_" + localName;
333 final Schema.Builder childSchemaBuilder = new Schema.Builder()
336 .description(schemaNode.getDescription().orElse(""));
337 final boolean isConfig = ((DataSchemaNode) dataNode).isConfiguration() && isParentConfig;
338 childSchemaBuilder.properties(processChildren(childSchemaBuilder, containerChildren,
339 parentName + "_" + localName, definitions, definitionNames, stack, module, isConfig));
341 final String discriminator;
342 if (!definitionNames.isListedNode(schemaNode)) {
343 final String parentNameConfigLocalName = parentName + "_" + localName;
344 final List<String> names = List.of(parentNameConfigLocalName);
345 discriminator = definitionNames.pickDiscriminator(schemaNode, names);
347 discriminator = definitionNames.getDiscriminator(schemaNode);
350 final String defName = nodeName + discriminator;
351 childSchemaBuilder.xml(buildXmlParameter(schemaNode));
352 definitions.put(defName, childSchemaBuilder.build());
354 return processRef(nodeName, discriminator, schemaNode);
358 * Processes the nodes.
360 private static Map<String, Property> processChildren(final Schema.Builder parentNodeBuilder,
361 final Collection<? extends DataSchemaNode> nodes, final String parentName,
362 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
363 final SchemaInferenceStack stack, final Module module, final boolean isParentConfig) throws IOException {
364 final Map<String, Property> properties = new HashMap<>();
365 final List<String> required = new ArrayList<>();
366 for (final DataSchemaNode node : nodes) {
367 if (node instanceof ChoiceSchemaNode choice) {
368 stack.enterSchemaTree(node.getQName());
369 final boolean isConfig = isParentConfig && node.isConfiguration();
370 final Map<String, Property> choiceProperties = processChoiceNodeRecursively(parentName,
371 definitions, definitionNames, isConfig, stack, required, choice, module);
372 properties.putAll(choiceProperties);
375 final Property property = processChildNode(node, parentName, definitions, definitionNames,
376 stack, required, module, isParentConfig);
377 if (property != null) {
378 properties.put(node.getQName().getLocalName(), property);
382 parentNodeBuilder.properties(properties).required(required.size() > 0 ? required : null);
386 private static Map<String, Property> processChoiceNodeRecursively(final String parentName,
387 final Map<String, Schema> definitions, final DefinitionNames definitionNames, final boolean isConfig,
388 final SchemaInferenceStack stack, final List<String> required, final ChoiceSchemaNode choice,
389 final Module module) throws IOException {
390 if (!choice.getCases().isEmpty()) {
391 final var properties = new HashMap<String, Property>();
392 final var caseSchemaNode = choice.getDefaultCase().orElse(choice.getCases().stream()
393 .findFirst().orElseThrow());
394 stack.enterSchemaTree(caseSchemaNode.getQName());
395 for (final var childNode : caseSchemaNode.getChildNodes()) {
396 if (childNode instanceof ChoiceSchemaNode childChoice) {
397 final var isChildConfig = isConfig && childNode.isConfiguration();
398 stack.enterSchemaTree(childNode.getQName());
399 final var childProperties = processChoiceNodeRecursively(parentName, definitions, definitionNames,
400 isChildConfig, stack, required, childChoice, module);
401 properties.putAll(childProperties);
404 final var property = processChildNode(childNode, parentName, definitions, definitionNames, stack,
405 required, module, isConfig);
406 if (property != null) {
407 properties.put(childNode.getQName().getLocalName(), property);
417 private static Property processChildNode(final DataSchemaNode node, final String parentName,
418 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
419 final SchemaInferenceStack stack, final List<String> required, final Module module,
420 final boolean isParentConfig) throws IOException {
421 final XMLNamespace parentNamespace = stack.toSchemaNodeIdentifier().lastNodeIdentifier().getNamespace();
422 stack.enterSchemaTree(node.getQName());
424 Add module name prefix to property name, when needed, when ServiceNow can process colons,
425 use RestDocGenUtil#resolveNodesName for creating property name
427 final String name = node.getQName().getLocalName();
429 If the parent is operational, then current node is also operational and should be added as a child
430 even if node.isConfiguration()==true.
431 If the parent is configuration, then current node should be added as a child only if
432 node.isConfiguration()==true.
434 final boolean shouldBeAddedAsChild = !isParentConfig || node.isConfiguration();
435 Property property = null;
436 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
437 final Property dataNodeContainer = processDataNodeContainer((DataNodeContainer) node, parentName,
438 definitions, definitionNames, stack, module, isParentConfig);
439 if (shouldBeAddedAsChild) {
440 if (isSchemaNodeMandatory(node)) {
443 property = dataNodeContainer;
445 processActionNodeContainer(node, parentName, definitions, definitionNames, stack, module);
446 } else if (shouldBeAddedAsChild) {
447 if (node instanceof LeafSchemaNode leaf) {
448 property = processLeafNode(leaf, name, required, stack, parentNamespace);
449 } else if (node instanceof AnyxmlSchemaNode || node instanceof AnydataSchemaNode) {
450 property = processUnknownDataSchemaNode(node, name, required, parentNamespace);
451 } else if (node instanceof LeafListSchemaNode leafList) {
452 if (isSchemaNodeMandatory(node)) {
455 property = processLeafListNode(leafList, stack);
457 throw new IllegalArgumentException("Unknown DataSchemaNode type: " + node.getClass());
464 private static Property processLeafListNode(final LeafListSchemaNode listNode,
465 final SchemaInferenceStack stack) {
466 final Property.Builder props = new Property.Builder();
467 props.type(ARRAY_TYPE);
469 final Property.Builder itemsVal = new Property.Builder();
470 final Optional<ElementCountConstraint> optConstraint = listNode.getElementCountConstraint();
471 optConstraint.ifPresent(elementCountConstraint -> processElementCount(elementCountConstraint, props));
473 processTypeDef(listNode.getType(), listNode, itemsVal, stack);
475 final Property itemsValue = itemsVal.build();
476 final Property propsValue = props.build();
477 props.items(itemsValue);
479 if (itemsValue.example() != null && propsValue.minItems() != null) {
480 final List<Object> listOfExamples = new ArrayList<>();
481 for (int i = 0; i < propsValue.minItems(); i++) {
482 listOfExamples.add(itemsValue.example());
484 props.example(listOfExamples);
486 props.description(listNode.getDescription().orElse(""));
488 return props.build();
491 private static void processElementCount(final ElementCountConstraint constraint, final Property.Builder props) {
492 final Integer minElements = constraint.getMinElements();
493 if (minElements != null) {
494 props.minItems(minElements);
496 final Integer maxElements = constraint.getMaxElements();
497 if (maxElements != null) {
498 props.maxItems(maxElements);
502 private static void processMandatory(final MandatoryAware node, final String nodeName,
503 final List<String> required) {
504 if (node.isMandatory()) {
505 required.add(nodeName);
509 private static Property processLeafNode(final LeafSchemaNode leafNode, final String jsonLeafName,
510 final List<String> required, final SchemaInferenceStack stack, final XMLNamespace parentNamespace) {
511 final Property.Builder property = new Property.Builder();
513 final String leafDescription = leafNode.getDescription().orElse("");
514 property.description(leafDescription);
516 processTypeDef(leafNode.getType(), leafNode, property, stack);
517 if (!leafNode.getQName().getNamespace().equals(parentNamespace)) {
518 // If the parent is not from the same model, define the child XML namespace.
519 property.xml(buildXmlParameter(leafNode));
521 processMandatory(leafNode, jsonLeafName, required);
522 return property.build();
525 private static Property processUnknownDataSchemaNode(final DataSchemaNode leafNode, final String name,
526 final List<String> required, final XMLNamespace parentNamespace) {
527 assert (leafNode instanceof AnydataSchemaNode || leafNode instanceof AnyxmlSchemaNode);
529 final Property.Builder property = new Property.Builder();
531 final String leafDescription = leafNode.getDescription().orElse("");
532 property.description(leafDescription);
534 final String localName = leafNode.getQName().getLocalName();
535 property.example(String.format("<%s> ... </%s>", localName, localName));
536 property.type(STRING_TYPE);
537 if (!leafNode.getQName().getNamespace().equals(parentNamespace)) {
538 // If the parent is not from the same model, define the child XML namespace.
539 property.xml(buildXmlParameter(leafNode));
541 processMandatory((MandatoryAware) leafNode, name, required);
542 return property.build();
545 private static String processTypeDef(final TypeDefinition<?> leafTypeDef, final DataSchemaNode node,
546 final Property.Builder property, final SchemaInferenceStack stack) {
547 final String jsonType;
548 if (leafTypeDef instanceof BinaryTypeDefinition binaryType) {
549 jsonType = processBinaryType(binaryType, property);
550 } else if (leafTypeDef instanceof BitsTypeDefinition bitsType) {
551 jsonType = processBitsType(bitsType, property);
552 } else if (leafTypeDef instanceof EnumTypeDefinition enumType) {
553 jsonType = processEnumType(enumType, property);
554 } else if (leafTypeDef instanceof IdentityrefTypeDefinition identityrefType) {
555 jsonType = processIdentityRefType(identityrefType, property, stack.getEffectiveModelContext());
556 } else if (leafTypeDef instanceof StringTypeDefinition stringType) {
557 jsonType = processStringType(stringType, property, node.getQName().getLocalName());
558 } else if (leafTypeDef instanceof UnionTypeDefinition unionType) {
559 jsonType = processTypeDef(unionType.getTypes().iterator().next(), node, property, stack);
560 } else if (leafTypeDef instanceof EmptyTypeDefinition) {
561 jsonType = OBJECT_TYPE;
562 } else if (leafTypeDef instanceof LeafrefTypeDefinition leafrefType) {
563 return processTypeDef(stack.resolveLeafref(leafrefType), node, property, stack);
564 } else if (leafTypeDef instanceof BooleanTypeDefinition) {
565 jsonType = BOOLEAN_TYPE;
566 leafTypeDef.getDefaultValue().ifPresent(v -> property.defaultValue(Boolean.valueOf((String) v)));
567 property.example(true);
568 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition<?, ?> rangeRestrictedType) {
569 jsonType = processNumberType(rangeRestrictedType, property);
570 } else if (leafTypeDef instanceof InstanceIdentifierTypeDefinition instanceIdentifierType) {
571 jsonType = processInstanceIdentifierType(instanceIdentifierType, node, property,
572 stack.getEffectiveModelContext());
574 jsonType = STRING_TYPE;
576 if (TYPE_KEY != null && jsonType != null) {
577 property.type(jsonType);
580 if (leafTypeDef.getDefaultValue().isPresent()) {
581 final Object defaultValue = leafTypeDef.getDefaultValue().orElseThrow();
582 if (defaultValue instanceof String stringDefaultValue) {
583 if (leafTypeDef instanceof BooleanTypeDefinition) {
584 property.defaultValue(Boolean.valueOf(stringDefaultValue));
585 } else if (leafTypeDef instanceof DecimalTypeDefinition
586 || leafTypeDef instanceof Uint64TypeDefinition) {
587 property.defaultValue(new BigDecimal(stringDefaultValue));
588 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition<?, ?> rangeRestrictedType) {
589 //uint8,16,32 int8,16,32,64
590 if (isHexadecimalOrOctal(rangeRestrictedType)) {
591 property.defaultValue(stringDefaultValue);
593 property.defaultValue(Long.valueOf(stringDefaultValue));
596 property.defaultValue(stringDefaultValue);
599 //we should never get here. getDefaultValue always gives us string
600 property.defaultValue(defaultValue.toString());
606 private static String processBinaryType(final BinaryTypeDefinition definition, final Property.Builder property) {
607 definition.getDefaultValue().ifPresent(property::defaultValue);
608 property.format("byte");
612 private static String processEnumType(final EnumTypeDefinition enumLeafType, final Property.Builder property) {
613 final List<EnumPair> enumPairs = enumLeafType.getValues();
614 final List<String> enumNames = enumPairs.stream()
615 .map(EnumPair::getName)
618 property.enums(enumNames);
619 enumLeafType.getDefaultValue().ifPresent(property::defaultValue);
620 property.example(enumLeafType.getValues().iterator().next().getName());
624 private static String processIdentityRefType(final IdentityrefTypeDefinition leafTypeDef,
625 final Property.Builder property, final EffectiveModelContext schemaContext) {
626 final IdentitySchemaNode node = leafTypeDef.getIdentities().iterator().next();
627 property.example(node.getQName().getLocalName());
628 final Collection<? extends IdentitySchemaNode> derivedIds = schemaContext.getDerivedIdentities(node);
629 final List<String> enumPayload = new ArrayList<>();
630 enumPayload.add(node.getQName().getLocalName());
631 populateEnumWithDerived(derivedIds, enumPayload, schemaContext);
632 final List<String> schemaEnum = new ArrayList<>(enumPayload);
633 property.enums(schemaEnum);
637 private static void populateEnumWithDerived(final Collection<? extends IdentitySchemaNode> derivedIds,
638 final List<String> enumPayload, final EffectiveModelContext context) {
639 for (final IdentitySchemaNode derivedId : derivedIds) {
640 enumPayload.add(derivedId.getQName().getLocalName());
641 populateEnumWithDerived(context.getDerivedIdentities(derivedId), enumPayload, context);
645 private static String processBitsType(final BitsTypeDefinition bitsType, final Property.Builder property) {
646 property.minItems(0);
647 property.uniqueItems(true);
648 final Collection<? extends Bit> bits = bitsType.getBits();
649 final List<String> enumNames = bits.stream()
652 property.enums(enumNames);
653 property.defaultValue(enumNames.iterator().next() + " " + enumNames.get(enumNames.size() - 1));
654 bitsType.getDefaultValue().ifPresent(property::defaultValue);
658 private static String processStringType(final StringTypeDefinition stringType, final Property.Builder property,
659 final String nodeName) {
660 var type = stringType;
661 while (type.getLengthConstraint().isEmpty() && type.getBaseType() != null) {
662 type = type.getBaseType();
665 type.getLengthConstraint().ifPresent(constraint -> {
666 final Range<Integer> range = constraint.getAllowedRanges().span();
667 property.minLength(range.lowerEndpoint());
668 property.maxLength(range.upperEndpoint());
671 if (type.getPatternConstraints().iterator().hasNext()) {
672 final PatternConstraint pattern = type.getPatternConstraints().iterator().next();
673 String regex = pattern.getRegularExpressionString();
674 // Escape special characters to prevent issues inside Automaton.
675 regex = AUTOMATON_SPECIAL_CHARACTERS.matcher(regex).replaceAll("\\\\$0");
676 for (final var charClass : PREDEFINED_CHARACTER_CLASSES.entrySet()) {
677 regex = regex.replaceAll(charClass.getKey(), charClass.getValue());
679 String defaultValue = "";
681 final RegExp regExp = new RegExp(regex);
682 defaultValue = regExp.toAutomaton().getShortestExample(true);
683 } catch (IllegalArgumentException ex) {
684 LOG.warn("Cannot create example string for type: {} with regex: {}.", stringType.getQName(), regex);
686 property.example(defaultValue);
688 property.example("Some " + nodeName);
691 stringType.getDefaultValue().ifPresent(property::defaultValue);
695 private static String processNumberType(final RangeRestrictedTypeDefinition<?, ?> leafTypeDef,
696 final Property.Builder property) {
697 final Optional<Number> maybeLower = leafTypeDef.getRangeConstraint()
698 .map(RangeConstraint::getAllowedRanges).map(RangeSet::span).map(Range::lowerEndpoint);
700 if (isHexadecimalOrOctal(leafTypeDef)) {
704 if (leafTypeDef instanceof DecimalTypeDefinition) {
705 leafTypeDef.getDefaultValue().ifPresent(number -> property.defaultValue(Decimal64.valueOf((String) number)
707 maybeLower.ifPresent(number -> property.example(((Decimal64) number).decimalValue()));
710 if (leafTypeDef instanceof Uint8TypeDefinition
711 || leafTypeDef instanceof Uint16TypeDefinition
712 || leafTypeDef instanceof Int8TypeDefinition
713 || leafTypeDef instanceof Int16TypeDefinition
714 || leafTypeDef instanceof Int32TypeDefinition) {
716 property.format(INT32_FORMAT);
717 leafTypeDef.getDefaultValue().ifPresent(number -> property.defaultValue(Integer.valueOf((String) number)));
718 maybeLower.ifPresent(number -> property.example(Integer.valueOf(number.toString())));
719 } else if (leafTypeDef instanceof Uint32TypeDefinition
720 || leafTypeDef instanceof Int64TypeDefinition) {
722 property.format(INT64_FORMAT);
723 leafTypeDef.getDefaultValue().ifPresent(number -> property.defaultValue(Long.valueOf((String) number)));
724 maybeLower.ifPresent(number -> property.example(Long.valueOf(number.toString())));
727 leafTypeDef.getDefaultValue().ifPresent(number -> property.defaultValue(new BigInteger((String) number)));
733 private static boolean isHexadecimalOrOctal(final RangeRestrictedTypeDefinition<?, ?> typeDef) {
734 final Optional<?> optDefaultValue = typeDef.getDefaultValue();
735 if (optDefaultValue.isPresent()) {
736 final String defaultValue = (String) optDefaultValue.orElseThrow();
737 return defaultValue.startsWith("0") || defaultValue.startsWith("-0");
742 private static String processInstanceIdentifierType(final InstanceIdentifierTypeDefinition iidType,
743 final DataSchemaNode node, final Property.Builder property, final EffectiveModelContext schemaContext) {
744 // create example instance-identifier to the first container of node's module if exists or leave it empty
745 final var module = schemaContext.findModule(node.getQName().getModule());
746 if (module.isPresent()) {
747 final var container = module.orElseThrow().getChildNodes().stream()
748 .filter(n -> n instanceof ContainerSchemaNode)
750 container.ifPresent(c -> property.example(String.format("/%s:%s", module.orElseThrow().getPrefix(),
751 c.getQName().getLocalName())));
754 iidType.getDefaultValue().ifPresent(property::defaultValue);
758 private static Xml buildXmlParameter(final SchemaNode node) {
759 final QName qName = node.getQName();
760 return new Xml(qName.getLocalName(), qName.getNamespace().toString(), null);