Fix OpenApi ignoring min-elements for list
[netconf.git] / restconf / restconf-openapi / src / main / java / org / opendaylight / restconf / openapi / impl / DefinitionGenerator.java
1 /*
2  * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.restconf.openapi.impl;
9
10 import static org.opendaylight.restconf.openapi.impl.BaseYangOpenApiGenerator.MODULE_NAME_SUFFIX;
11 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.COMPONENTS_PREFIX;
12
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;
23 import java.util.Map;
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;
86
87 /**
88  * Generates JSON Schema for data defined in YANG. This class is not thread-safe.
89  */
90 public final class DefinitionGenerator {
91
92     private static final Logger LOG = LoggerFactory.getLogger(DefinitionGenerator.class);
93
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]");
115
116     private DefinitionGenerator() {
117         // Hidden on purpose
118     }
119
120     /**
121      * Creates Json definitions from provided module according to openapi spec.
122      *
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
128      */
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);
133
134         return definitions;
135     }
136
137     public static Map<String, Schema> convertToSchemas(final Module module, final EffectiveModelContext schemaContext,
138             final DefinitionNames definitionNames, final boolean isForSingleModule)
139             throws IOException {
140         final Map<String, Schema> definitions = new HashMap<>();
141         if (isForSingleModule) {
142             definitionNames.addUnlinkedName(module.getName() + MODULE_NAME_SUFFIX);
143         }
144         return convertToSchemas(module, schemaContext, definitions, definitionNames);
145     }
146
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()) {
153                 return false;
154             }
155             for (final DataSchemaNode childNode : containerNode.getChildNodes()) {
156                 if (childNode instanceof MandatoryAware mandatoryAware && mandatoryAware.isMandatory()) {
157                     return true;
158                 }
159             }
160         }
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)
165                 .orElse(0)
166                 > 0;
167     }
168
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);
180             }
181             stack.exit();
182         }
183     }
184
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);
191             stack.exit();
192         }
193     }
194
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);
202             stack.exit();
203         }
204     }
205
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);
214     }
215
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()
224                 .title(filename)
225                 .type(OBJECT_TYPE)
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());
232         }
233         stack.exit();
234     }
235
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;
241
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));
253                 }
254             }
255         } else {
256              /*
257                 Description can't be added, because nothing allowed alongside $ref.
258                 allOf is not an option, because ServiceNow can't parse it.
259               */
260             dataNodeProperties.ref(ref);
261         }
262
263         return dataNodeProperties.build();
264     }
265
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);
271
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);
286                 } else {
287                     exampleValue = example.getValue();
288                 }
289                 exampleMap.put(example.getKey(), exampleValue);
290             }
291             examples.add(exampleMap);
292         }
293         return examples;
294     }
295
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);
307                 }
308             }
309         }
310         return firstExampleMap;
311     }
312
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));
322         }
323         return exampleValue;
324     }
325
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()
334             .type(OBJECT_TYPE)
335             .title(nodeName)
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));
340
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);
346         } else {
347             discriminator = definitionNames.getDiscriminator(schemaNode);
348         }
349
350         final String defName = nodeName + discriminator;
351         childSchemaBuilder.xml(buildXmlParameter(schemaNode));
352         definitions.put(defName, childSchemaBuilder.build());
353
354         return processRef(nodeName, discriminator, schemaNode);
355     }
356
357     /**
358      * Processes the nodes.
359      */
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);
373                 stack.exit();
374             } else {
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);
379                 }
380             }
381         }
382         parentNodeBuilder.properties(properties).required(required.size() > 0 ? required : null);
383         return properties;
384     }
385
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);
402                     stack.exit();
403                 } else {
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);
408                     }
409                 }
410             }
411             stack.exit();
412             return properties;
413         }
414         return Map.of();
415     }
416
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());
423         /*
424             Add module name prefix to property name, when needed, when ServiceNow can process colons,
425             use RestDocGenUtil#resolveNodesName for creating property name
426          */
427         final String name = node.getQName().getLocalName();
428         /*
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.
433         */
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)) {
441                     required.add(name);
442                 }
443                 property = dataNodeContainer;
444             }
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)) {
453                     required.add(name);
454                 }
455                 property = processLeafListNode(leafList, stack);
456             } else {
457                 throw new IllegalArgumentException("Unknown DataSchemaNode type: " + node.getClass());
458             }
459         }
460         stack.exit();
461         return property;
462     }
463
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);
468
469         final Property.Builder itemsVal = new Property.Builder();
470         final Optional<ElementCountConstraint> optConstraint = listNode.getElementCountConstraint();
471         optConstraint.ifPresent(elementCountConstraint -> processElementCount(elementCountConstraint, props));
472
473         processTypeDef(listNode.getType(), listNode, itemsVal, stack);
474
475         final Property itemsValue = itemsVal.build();
476         final Property propsValue = props.build();
477         props.items(itemsValue);
478
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());
483             }
484             props.example(listOfExamples);
485         }
486         props.description(listNode.getDescription().orElse(""));
487
488         return props.build();
489     }
490
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);
495         }
496         final Integer maxElements = constraint.getMaxElements();
497         if (maxElements != null) {
498             props.maxItems(maxElements);
499         }
500     }
501
502     private static void processMandatory(final MandatoryAware node, final String nodeName,
503             final List<String> required) {
504         if (node.isMandatory()) {
505             required.add(nodeName);
506         }
507     }
508
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();
512
513         final String leafDescription = leafNode.getDescription().orElse("");
514         property.description(leafDescription);
515
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));
520         }
521         processMandatory(leafNode, jsonLeafName, required);
522         return property.build();
523     }
524
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);
528
529         final Property.Builder property = new Property.Builder();
530
531         final String leafDescription = leafNode.getDescription().orElse("");
532         property.description(leafDescription);
533
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));
540         }
541         processMandatory((MandatoryAware) leafNode, name, required);
542         return property.build();
543     }
544
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());
573         } else {
574             jsonType = STRING_TYPE;
575         }
576         if (TYPE_KEY != null && jsonType != null) {
577             property.type(jsonType);
578         }
579
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);
592                     } else {
593                         property.defaultValue(Long.valueOf(stringDefaultValue));
594                     }
595                 } else {
596                     property.defaultValue(stringDefaultValue);
597                 }
598             } else {
599                 //we should never get here. getDefaultValue always gives us string
600                 property.defaultValue(defaultValue.toString());
601             }
602         }
603         return jsonType;
604     }
605
606     private static String processBinaryType(final BinaryTypeDefinition definition, final Property.Builder property) {
607         definition.getDefaultValue().ifPresent(property::defaultValue);
608         property.format("byte");
609         return STRING_TYPE;
610     }
611
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)
616             .toList();
617
618         property.enums(enumNames);
619         enumLeafType.getDefaultValue().ifPresent(property::defaultValue);
620         property.example(enumLeafType.getValues().iterator().next().getName());
621         return STRING_TYPE;
622     }
623
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);
634         return STRING_TYPE;
635     }
636
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);
642         }
643     }
644
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()
650             .map(Bit::getName)
651             .toList();
652         property.enums(enumNames);
653         property.defaultValue(enumNames.iterator().next() + " " + enumNames.get(enumNames.size() - 1));
654         bitsType.getDefaultValue().ifPresent(property::defaultValue);
655         return STRING_TYPE;
656     }
657
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();
663         }
664
665         type.getLengthConstraint().ifPresent(constraint -> {
666             final Range<Integer> range = constraint.getAllowedRanges().span();
667             property.minLength(range.lowerEndpoint());
668             property.maxLength(range.upperEndpoint());
669         });
670
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());
678             }
679             String defaultValue = "";
680             try {
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);
685             }
686             property.example(defaultValue);
687         } else {
688             property.example("Some " + nodeName);
689         }
690
691         stringType.getDefaultValue().ifPresent(property::defaultValue);
692         return STRING_TYPE;
693     }
694
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);
699
700         if (isHexadecimalOrOctal(leafTypeDef)) {
701             return STRING_TYPE;
702         }
703
704         if (leafTypeDef instanceof DecimalTypeDefinition) {
705             leafTypeDef.getDefaultValue().ifPresent(number -> property.defaultValue(Decimal64.valueOf((String) number)
706                 .decimalValue()));
707             maybeLower.ifPresent(number -> property.example(((Decimal64) number).decimalValue()));
708             return NUMBER_TYPE;
709         }
710         if (leafTypeDef instanceof Uint8TypeDefinition
711                 || leafTypeDef instanceof Uint16TypeDefinition
712                 || leafTypeDef instanceof Int8TypeDefinition
713                 || leafTypeDef instanceof Int16TypeDefinition
714                 || leafTypeDef instanceof Int32TypeDefinition) {
715
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) {
721
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())));
725         } else {
726             //uint64
727             leafTypeDef.getDefaultValue().ifPresent(number -> property.defaultValue(new BigInteger((String) number)));
728             property.example(0);
729         }
730         return INTEGER_TYPE;
731     }
732
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");
738         }
739         return false;
740     }
741
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)
749                     .findFirst();
750             container.ifPresent(c -> property.example(String.format("/%s:%s", module.orElseThrow().getPrefix(),
751                 c.getQName().getLocalName())));
752         }
753         // set default value
754         iidType.getDefaultValue().ifPresent(property::defaultValue);
755         return STRING_TYPE;
756     }
757
758     private static Xml buildXmlParameter(final SchemaNode node) {
759         final QName qName = node.getQName();
760         return new Xml(qName.getLocalName(), qName.getNamespace().toString(), null);
761     }
762 }