f973b1f9d73a97be197e5a20596bfb9a775e0743
[netconf.git] / restconf / restconf-openapi / src / main / java / org / opendaylight / restconf / openapi / model / PropertyEntity.java
1 /*
2  * Copyright (c) 2023 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.model;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.fasterxml.jackson.core.JsonGenerator;
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.regex.Pattern;
25 import java.util.stream.Collectors;
26 import org.eclipse.jdt.annotation.NonNull;
27 import org.opendaylight.restconf.openapi.impl.DefinitionNames;
28 import org.opendaylight.yangtools.yang.common.AbstractQName;
29 import org.opendaylight.yangtools.yang.common.Decimal64;
30 import org.opendaylight.yangtools.yang.common.XMLNamespace;
31 import org.opendaylight.yangtools.yang.model.api.AnydataSchemaNode;
32 import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode;
33 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
34 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
35 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
36 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
37 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
38 import org.opendaylight.yangtools.yang.model.api.ElementCountConstraint;
39 import org.opendaylight.yangtools.yang.model.api.ElementCountConstraintAware;
40 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
41 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.MandatoryAware;
45 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
46 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
47 import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
48 import org.opendaylight.yangtools.yang.model.api.meta.ModelStatement;
49 import org.opendaylight.yangtools.yang.model.api.type.BinaryTypeDefinition;
50 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition;
51 import org.opendaylight.yangtools.yang.model.api.type.BooleanTypeDefinition;
52 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
53 import org.opendaylight.yangtools.yang.model.api.type.EmptyTypeDefinition;
54 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition;
55 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
56 import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition;
57 import org.opendaylight.yangtools.yang.model.api.type.Int16TypeDefinition;
58 import org.opendaylight.yangtools.yang.model.api.type.Int32TypeDefinition;
59 import org.opendaylight.yangtools.yang.model.api.type.Int64TypeDefinition;
60 import org.opendaylight.yangtools.yang.model.api.type.Int8TypeDefinition;
61 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
62 import org.opendaylight.yangtools.yang.model.api.type.PatternConstraint;
63 import org.opendaylight.yangtools.yang.model.api.type.RangeConstraint;
64 import org.opendaylight.yangtools.yang.model.api.type.RangeRestrictedTypeDefinition;
65 import org.opendaylight.yangtools.yang.model.api.type.StringTypeDefinition;
66 import org.opendaylight.yangtools.yang.model.api.type.Uint16TypeDefinition;
67 import org.opendaylight.yangtools.yang.model.api.type.Uint32TypeDefinition;
68 import org.opendaylight.yangtools.yang.model.api.type.Uint64TypeDefinition;
69 import org.opendaylight.yangtools.yang.model.api.type.Uint8TypeDefinition;
70 import org.opendaylight.yangtools.yang.model.api.type.UnionTypeDefinition;
71 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
72 import org.slf4j.Logger;
73 import org.slf4j.LoggerFactory;
74
75 public class PropertyEntity {
76     private static final Logger LOG = LoggerFactory.getLogger(PropertyEntity.class);
77     private static final String COMPONENTS_PREFIX = "#/components/schemas/";
78     private static final String ARRAY_TYPE = "array";
79     private static final String DESCRIPTION = "description";
80     private static final String DEFAULT = "default";
81     private static final String ENUM = "enum";
82     private static final String EXAMPLE = "example";
83     private static final String FORMAT = "format";
84     private static final String ITEMS = "items";
85     private static final String STRING_TYPE = "string";
86     private static final String OBJECT_TYPE = "object";
87     private static final String NUMBER_TYPE = "number";
88     private static final String TYPE = "type";
89     private static final Pattern AUTOMATON_SPECIAL_CHARACTERS = Pattern.compile("[@&\"<>#~]");
90     // Adaptation from YANG regex to Automaton regex
91     // See https://github.com/mifmif/Generex/blob/master/src/main/java/com/mifmif/common/regex/Generex.java
92     private static final Map<String, String> PREDEFINED_CHARACTER_CLASSES = Map.of("\\\\d", "[0-9]",
93         "\\\\D", "[^0-9]", "\\\\s", "[ \t\n\f\r]", "\\\\S", "[^ \t\n\f\r]",
94         "\\\\w", "[a-zA-Z_0-9]", "\\\\W", "[^a-zA-Z_0-9]");
95
96     private final @NonNull DataSchemaNode node;
97     private final @NonNull JsonGenerator generator;
98     private final @NonNull List<String> required;
99     private final @NonNull String parentName;
100     private final @NonNull DefinitionNames definitionNames;
101
102     public PropertyEntity(final @NonNull DataSchemaNode node, final @NonNull JsonGenerator generator,
103             final @NonNull SchemaInferenceStack stack, final @NonNull List<String> required,
104             final @NonNull String parentName, final boolean isParentConfig,
105             final @NonNull DefinitionNames definitionNames) throws IOException {
106         this.node = requireNonNull(node);
107         this.generator = requireNonNull(generator);
108         this.required = requireNonNull(required);
109         this.parentName = requireNonNull(parentName);
110         this.definitionNames = requireNonNull(definitionNames);
111         generate(stack, isParentConfig);
112     }
113
114     private void generate(final SchemaInferenceStack stack, final boolean isParentConfig) throws IOException {
115         if (node instanceof ChoiceSchemaNode choice) {
116             stack.enterSchemaTree(node.getQName());
117             final var isConfig = isParentConfig && node.isConfiguration();
118             processChoiceNodeRecursively(isConfig, stack, choice);
119             stack.exit();
120         } else {
121             generator.writeObjectFieldStart(node.getQName().getLocalName());
122             processChildNode(node, stack, isParentConfig);
123             generator.writeEndObject();
124         }
125     }
126
127     private void processChoiceNodeRecursively(final boolean isConfig, final SchemaInferenceStack stack,
128             final ChoiceSchemaNode choice) throws IOException {
129         if (!choice.getCases().isEmpty()) {
130             final var caseSchemaNode = choice.getDefaultCase().orElse(
131                 choice.getCases().stream().findFirst().orElseThrow());
132             stack.enterSchemaTree(caseSchemaNode.getQName());
133             for (final var childNode : caseSchemaNode.getChildNodes()) {
134                 if (childNode instanceof ChoiceSchemaNode childChoice) {
135                     final var isChildConfig = isConfig && childNode.isConfiguration();
136                     stack.enterSchemaTree(childNode.getQName());
137                     processChoiceNodeRecursively(isChildConfig, stack, childChoice);
138                     stack.exit();
139                 } else if (!isConfig || childNode.isConfiguration()) {
140                     generator.writeObjectFieldStart(childNode.getQName().getLocalName());
141                     processChildNode(childNode, stack, isConfig);
142                     generator.writeEndObject();
143                 }
144             }
145             stack.exit();
146         }
147     }
148
149     private void processChildNode(final DataSchemaNode schemaNode, final SchemaInferenceStack stack,
150             final boolean isParentConfig) throws IOException {
151         final var parentNamespace = stack.toSchemaNodeIdentifier().lastNodeIdentifier().getNamespace();
152         stack.enterSchemaTree(schemaNode.getQName());
153         final var name = schemaNode.getQName().getLocalName();
154         final var shouldBeAddedAsChild = !isParentConfig || schemaNode.isConfiguration();
155         if (schemaNode instanceof ListSchemaNode || schemaNode instanceof ContainerSchemaNode) {
156             processDataNodeContainer((DataNodeContainer) schemaNode);
157             if (shouldBeAddedAsChild && isSchemaNodeMandatory(schemaNode)) {
158                 required.add(name);
159             }
160         } else if (shouldBeAddedAsChild) {
161             if (schemaNode instanceof LeafSchemaNode leaf) {
162                 processLeafNode(leaf, name, stack, parentNamespace);
163             } else if (schemaNode instanceof AnyxmlSchemaNode || schemaNode instanceof AnydataSchemaNode) {
164                 processUnknownDataSchemaNode(schemaNode, name, parentNamespace);
165             } else if (schemaNode instanceof LeafListSchemaNode leafList) {
166                 if (isSchemaNodeMandatory(schemaNode)) {
167                     required.add(name);
168                 }
169                 processLeafListNode(leafList, stack);
170             } else {
171                 throw new IllegalArgumentException("Unknown DataSchemaNode type: " + schemaNode.getClass());
172             }
173         }
174         stack.exit();
175     }
176
177     private void processDataNodeContainer(final DataNodeContainer dataNode) throws IOException {
178         final var schemaNode = (SchemaNode) dataNode;
179         final var localName = schemaNode.getQName().getLocalName();
180         final var nodeName = parentName + "_" + localName;
181
182         final String discriminator;
183         if (!definitionNames.isListedNode(schemaNode)) {
184             final var parentNameConfigLocalName = parentName + "_" + localName;
185             final var names = List.of(parentNameConfigLocalName);
186             discriminator = definitionNames.pickDiscriminator(schemaNode, names);
187         } else {
188             discriminator = definitionNames.getDiscriminator(schemaNode);
189         }
190
191         processRef(nodeName, schemaNode, discriminator);
192     }
193
194     private void processRef(final String name, final SchemaNode schemaNode, String discriminator) throws IOException {
195         final var ref = COMPONENTS_PREFIX + name + discriminator;
196         if (schemaNode instanceof ListSchemaNode listNode) {
197             generator.writeStringField(TYPE, ARRAY_TYPE);
198             generator.writeObjectFieldStart(ITEMS);
199             generator.writeStringField("$ref", ref);
200             generator.writeEndObject();
201             generator.writeStringField(DESCRIPTION, schemaNode.getDescription().orElse(""));
202
203             if (listNode.getElementCountConstraint().isPresent()) {
204                 final var minElements = listNode.getElementCountConstraint().orElseThrow().getMinElements();
205                 final var maxElements = listNode.getElementCountConstraint().orElseThrow().getMaxElements();
206                 if (minElements != null) {
207                     createExamples(listNode, minElements);
208                     generator.writeNumberField("minItems", minElements);
209                 }
210                 if (maxElements != null) {
211                     generator.writeNumberField("maxItems", maxElements);
212                 }
213             }
214         } else {
215              /*
216                 Description can't be added, because nothing allowed alongside $ref.
217                 allOf is not an option, because ServiceNow can't parse it.
218               */
219             generator.writeStringField("$ref", ref);
220         }
221     }
222
223     private void createExamples(final ListSchemaNode schemaNode,
224         @NonNull final Integer minElements) throws IOException {
225         final var firstExampleMap = prepareFirstListExample(schemaNode);
226         final var examples = new ArrayList<Map<String, Object>>();
227         examples.add(firstExampleMap);
228
229         final var unqiueContraintsNameSet = schemaNode.getUniqueConstraints().stream()
230             .map(ModelStatement::argument)
231             .flatMap(uniqueSt -> uniqueSt.stream()
232                 .map(schemaNI -> schemaNI.lastNodeIdentifier().getLocalName()))
233             .collect(Collectors.toSet());
234         final var keysNameSet = schemaNode.getKeyDefinition().stream()
235             .map(AbstractQName::getLocalName)
236             .collect(Collectors.toSet());
237         for (int i = 1; i < minElements; i++) {
238             final var exampleMap = new HashMap<String, Object>();
239             for (final var example : firstExampleMap.entrySet()) {
240                 final Object exampleValue;
241                 if (keysNameSet.contains(example.getKey()) || unqiueContraintsNameSet.contains(example.getKey())) {
242                     exampleValue = editExample(example.getValue(), i);
243                 } else {
244                     exampleValue = example.getValue();
245                 }
246                 exampleMap.put(example.getKey(), exampleValue);
247             }
248             examples.add(exampleMap);
249         }
250         generator.writeArrayFieldStart(EXAMPLE);
251         for (final var example : examples) {
252             generator.writeStartObject();
253             for (final var elem : example.entrySet()) {
254                 writeValue(elem.getKey(), elem.getValue());
255             }
256             generator.writeEndObject();
257         }
258         generator.writeEndArray();
259     }
260
261     private HashMap<String, Object> prepareFirstListExample(final ListSchemaNode schemaNode) {
262         final var childNodes = schemaNode.getChildNodes();
263         final var firstExampleMap = new HashMap<String, Object>();
264         // Cycle for each child node
265         for (final var childNode : childNodes) {
266             if (childNode instanceof TypedDataSchemaNode leafSchemaNode) {
267                 final var def = new TypeDef();
268                 processTypeDef(leafSchemaNode.getType(), leafSchemaNode, null, def);
269                 if (def.hasExample()) {
270                     firstExampleMap.put(leafSchemaNode.getQName().getLocalName(), def.getExample());
271                 }
272             }
273         }
274         return firstExampleMap;
275     }
276
277     private Object editExample(final Object exampleValue, final int edit) {
278         if (exampleValue instanceof String string) {
279             return string + "_" + edit;
280         } else if (exampleValue instanceof Integer number) {
281             return number + edit;
282         } else if (exampleValue instanceof Long number) {
283             return number + edit;
284         } else if (exampleValue instanceof Decimal64 number) {
285             return Decimal64.valueOf(BigDecimal.valueOf(number.intValue() + edit));
286         }
287         return exampleValue;
288     }
289
290     private void processUnknownDataSchemaNode(final DataSchemaNode leafNode, final String name,
291             final XMLNamespace parentNamespace) throws IOException {
292         assert (leafNode instanceof AnydataSchemaNode || leafNode instanceof AnyxmlSchemaNode);
293
294         final var leafDescription = leafNode.getDescription().orElse("");
295         generator.writeStringField(DESCRIPTION, leafDescription);
296
297         final var localName = leafNode.getQName().getLocalName();
298         generator.writeStringField(EXAMPLE, String.format("<%s> ... </%s>", localName, localName));
299         generator.writeStringField(TYPE, STRING_TYPE);
300         if (!leafNode.getQName().getNamespace().equals(parentNamespace)) {
301             // If the parent is not from the same model, define the child XML namespace.
302             buildXmlParameter(leafNode);
303         }
304         processMandatory((MandatoryAware) leafNode, name, required);
305     }
306
307     private void processLeafListNode(final LeafListSchemaNode listNode, final SchemaInferenceStack stack)
308             throws IOException {
309         generator.writeStringField(TYPE, ARRAY_TYPE);
310
311         Integer minElements = null;
312         final var optConstraint = listNode.getElementCountConstraint();
313         if (optConstraint.isPresent()) {
314             minElements = optConstraint.orElseThrow().getMinElements();
315             if (minElements != null) {
316                 generator.writeNumberField("minItems", minElements);
317             }
318             final var maxElements = optConstraint.orElseThrow().getMaxElements();
319             if (maxElements != null) {
320                 generator.writeNumberField("maxItems", maxElements);
321             }
322         }
323         final var def = new TypeDef();
324         processTypeDef(listNode.getType(), listNode, stack, def);
325
326         generator.writeObjectFieldStart(ITEMS);
327         processTypeDef(listNode.getType(), listNode, stack);
328         generator.writeEndObject();
329         generator.writeStringField(DESCRIPTION, listNode.getDescription().orElse(""));
330
331         if (def.hasExample() && minElements != null) {
332             final var listOfExamples = new ArrayList<>();
333             for (int i = 0; i < minElements; i++) {
334                 listOfExamples.add(def.getExample());
335             }
336             generator.writeArrayFieldStart(EXAMPLE);
337             for (final var example : listOfExamples) {
338                 generator.writeString(example.toString());
339             }
340             generator.writeEndArray();
341         }
342     }
343
344     private void processLeafNode(final LeafSchemaNode leafNode, final String jsonLeafName,
345             final SchemaInferenceStack stack, final XMLNamespace parentNamespace) throws IOException {
346         final var leafDescription = leafNode.getDescription().orElse("");
347         generator.writeStringField(DESCRIPTION, leafDescription);
348         processTypeDef(leafNode.getType(), leafNode, stack);
349         if (!leafNode.getQName().getNamespace().equals(parentNamespace)) {
350             // If the parent is not from the same model, define the child XML namespace.
351             buildXmlParameter(leafNode);
352         }
353         processMandatory(leafNode, jsonLeafName, required);
354     }
355
356     private static void processMandatory(final MandatoryAware node, final String nodeName,
357             final List<String> required) {
358         if (node.isMandatory()) {
359             required.add(nodeName);
360         }
361     }
362
363     private void buildXmlParameter(final SchemaNode schemaNode) throws IOException {
364         generator.writeObjectFieldStart("xml");
365         generator.writeStringField("name", schemaNode.getQName().getLocalName());
366         generator.writeStringField("namespace", schemaNode.getQName().getNamespace().toString());
367         generator.writeEndObject();
368     }
369
370     protected static boolean isSchemaNodeMandatory(final DataSchemaNode schemaNode) {
371         if (schemaNode instanceof ContainerSchemaNode containerNode) {
372             if (containerNode.isPresenceContainer()) {
373                 return false;
374             }
375             for (final var childNode : containerNode.getChildNodes()) {
376                 if (childNode instanceof MandatoryAware mandatoryAware && mandatoryAware.isMandatory()) {
377                     return true;
378                 }
379             }
380         }
381         //  A list or leaf-list node with a "min-elements" statement with a value greater than zero.
382         return schemaNode instanceof ElementCountConstraintAware constraintAware
383             && constraintAware.getElementCountConstraint()
384             .map(ElementCountConstraint::getMinElements)
385             .orElse(0) > 0;
386     }
387
388     private void processTypeDef(final TypeDefinition<?> leafTypeDef, final DataSchemaNode schemaNode,
389         final SchemaInferenceStack stack) throws IOException {
390         final var def = new TypeDef();
391
392         final var jsonType = processTypeDef(leafTypeDef, schemaNode, stack, def);
393
394         generator.writeStringField(TYPE, jsonType);
395         generateTypeDef(def);
396     }
397
398     private String processTypeDef(final TypeDefinition<?> leafTypeDef, final DataSchemaNode schemaNode,
399             final SchemaInferenceStack stack, final TypeDef def) {
400         final String jsonType;
401         if (leafTypeDef instanceof BinaryTypeDefinition binaryType) {
402             jsonType = processBinaryType(binaryType, def);
403         } else if (leafTypeDef instanceof BitsTypeDefinition bitsType) {
404             jsonType = processBitsType(bitsType, def);
405         } else if (leafTypeDef instanceof EnumTypeDefinition enumType) {
406             jsonType = processEnumType(enumType, def);
407         } else if (leafTypeDef instanceof IdentityrefTypeDefinition identityrefType) {
408             jsonType = processIdentityRefType(identityrefType, stack.modelContext(), def);
409         } else if (leafTypeDef instanceof StringTypeDefinition stringType) {
410             jsonType = processStringType(stringType, schemaNode.getQName().getLocalName(), def);
411         } else if (leafTypeDef instanceof UnionTypeDefinition unionType) {
412             jsonType = processTypeDef(unionType.getTypes().iterator().next(), schemaNode, stack, def);
413         } else if (leafTypeDef instanceof EmptyTypeDefinition) {
414             jsonType = OBJECT_TYPE;
415         } else if (leafTypeDef instanceof LeafrefTypeDefinition leafrefType) {
416             jsonType = processTypeDef(stack.resolveLeafref(leafrefType), schemaNode, stack, def);
417         } else if (leafTypeDef instanceof BooleanTypeDefinition) {
418             jsonType = "boolean";
419             if (leafTypeDef.getDefaultValue().isPresent()) {
420                 def.setDefaultValue(Boolean.parseBoolean((String) leafTypeDef.getDefaultValue().orElseThrow()));
421             }
422             def.setExample(true);
423         } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition<?, ?> rangeRestrictedType) {
424             jsonType = processNumberType(rangeRestrictedType, def);
425         } else if (leafTypeDef instanceof InstanceIdentifierTypeDefinition instanceIdentifierType) {
426             jsonType = processInstanceIdentifierType(instanceIdentifierType, schemaNode,
427                 stack.modelContext(), def);
428         } else {
429             jsonType = STRING_TYPE;
430         }
431
432         if (leafTypeDef.getDefaultValue().isPresent()) {
433             final var defaultValue = leafTypeDef.getDefaultValue().orElseThrow();
434             if (defaultValue instanceof String stringDefaultValue) {
435                 if (leafTypeDef instanceof BooleanTypeDefinition) {
436                     def.setDefaultValue(Boolean.valueOf(stringDefaultValue));
437                 } else if (leafTypeDef instanceof DecimalTypeDefinition
438                     || leafTypeDef instanceof Uint64TypeDefinition) {
439                     def.setDefaultValue(new BigDecimal(stringDefaultValue));
440                 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition<?, ?> rangeRestrictedType) {
441                     //uint8,16,32 int8,16,32,64
442                     if (isHexadecimalOrOctal(rangeRestrictedType)) {
443                         def.setDefaultValue(stringDefaultValue);
444                     } else {
445                         def.setDefaultValue(Long.valueOf(stringDefaultValue));
446                     }
447                 } else {
448                     def.setDefaultValue(stringDefaultValue);
449                 }
450             }
451         }
452         return jsonType;
453     }
454
455     private void writeValue(final String field, final Object value) throws IOException {
456         if (value instanceof Number) {
457             if (value instanceof Integer intValue) {
458                 generator.writeNumberField(field, intValue);
459             } else if (value instanceof Long longValue) {
460                 generator.writeNumberField(field, longValue);
461             } else if (value instanceof BigDecimal decimalValue) {
462                 generator.writeNumberField(field, decimalValue);
463             } else if (value instanceof BigInteger bigIntValue) {
464                 generator.writeNumberField(field, bigIntValue);
465             }
466         } else if (value instanceof Boolean bool) {
467             generator.writeBooleanField(field, bool);
468         } else {
469             generator.writeStringField(field, (String) value);
470         }
471     }
472
473     private void generateTypeDef(final TypeDef def) throws IOException {
474         if (def.hasEnums()) {
475             generator.writeArrayFieldStart(ENUM);
476             for (final var enumElem : def.getEnums()) {
477                 generator.writeString(enumElem);
478             }
479             generator.writeEndArray();
480         }
481         if (def.hasFormat()) {
482             generator.writeStringField(FORMAT, def.getFormat());
483         }
484         if (def.hasMinItems()) {
485             generator.writeNumberField("minItems", def.getMinItems());
486         }
487         if (def.hasDefaultValue()) {
488             writeValue(DEFAULT, def.getDefaultValue());
489         }
490         if (def.hasExample()) {
491             writeValue(EXAMPLE, def.getExample());
492         }
493         if (def.hasUniqueItems()) {
494             generator.writeBooleanField("uniqueItems", def.getUniqueItems());
495         }
496         if (def.hasMinLength()) {
497             generator.writeNumberField("minLength", def.getMinLength());
498         }
499         if (def.hasMaxLength()) {
500             generator.writeNumberField("maxLength", def.getMaxLength());
501         }
502     }
503
504     private String processBinaryType(final BinaryTypeDefinition definition, final TypeDef def) {
505         if (definition.getDefaultValue().isPresent()) {
506             def.setDefaultValue(definition.getDefaultValue().toString());
507         }
508         def.setFormat("byte");
509         return STRING_TYPE;
510     }
511
512     private String processBitsType(final BitsTypeDefinition bitsType, final TypeDef def) {
513         def.setMinItems(0);
514         def.setUniqueItems(true);
515         final var bits = bitsType.getBits();
516         final var enumNames = bits.stream()
517             .map(BitsTypeDefinition.Bit::getName)
518             .toList();
519
520         def.setEnums(enumNames);
521
522         def.setDefaultValue(bitsType.getDefaultValue().isPresent()
523             ? bitsType.getDefaultValue().orElseThrow().toString() :
524             (enumNames.iterator().next() + " " + enumNames.get(enumNames.size() - 1)));
525         return STRING_TYPE;
526     }
527
528     private String processEnumType(final EnumTypeDefinition enumLeafType, final TypeDef def) {
529         final var enumPairs = enumLeafType.getValues();
530         final var enumNames = enumPairs.stream()
531             .map(EnumTypeDefinition.EnumPair::getName)
532             .toList();
533
534         def.setEnums(enumNames);
535
536         if (enumLeafType.getDefaultValue().isPresent()) {
537             final var defaultValue = enumLeafType.getDefaultValue().orElseThrow().toString();
538             def.setDefaultValue(defaultValue);
539         }
540         def.setExample(enumLeafType.getValues().iterator().next().getName());
541         return STRING_TYPE;
542     }
543
544     private String processIdentityRefType(final IdentityrefTypeDefinition leafTypeDef,
545             final EffectiveModelContext schemaContext, final TypeDef def) {
546         final var schemaNode = leafTypeDef.getIdentities().iterator().next();
547         def.setExample(schemaNode.getQName().getLocalName());
548
549         final var derivedIds = schemaContext.getDerivedIdentities(schemaNode);
550         final var enumPayload = new ArrayList<String>();
551         enumPayload.add(schemaNode.getQName().getLocalName());
552         populateEnumWithDerived(derivedIds, enumPayload, schemaContext);
553         final var schemaEnum = new ArrayList<>(enumPayload);
554
555         def.setEnums(schemaEnum);
556
557         return STRING_TYPE;
558     }
559
560     private void populateEnumWithDerived(final Collection<? extends IdentitySchemaNode> derivedIds,
561             final List<String> enumPayload, final EffectiveModelContext context) {
562         for (final var derivedId : derivedIds) {
563             enumPayload.add(derivedId.getQName().getLocalName());
564             populateEnumWithDerived(context.getDerivedIdentities(derivedId), enumPayload, context);
565         }
566     }
567
568     private String processStringType(final StringTypeDefinition stringType, final String nodeName, final TypeDef def) {
569         var type = stringType;
570         while (type.getLengthConstraint().isEmpty() && type.getBaseType() != null) {
571             type = type.getBaseType();
572         }
573
574         if (type.getLengthConstraint().isPresent()) {
575             final var range = type.getLengthConstraint().orElseThrow().getAllowedRanges().span();
576             def.setMinLength(range.lowerEndpoint());
577             def.setMaxLength(range.upperEndpoint());
578         }
579
580         if (type.getPatternConstraints().iterator().hasNext()) {
581             final PatternConstraint pattern = type.getPatternConstraints().iterator().next();
582             var regex = pattern.getRegularExpressionString();
583             // Escape special characters to prevent issues inside Automaton.
584             regex = AUTOMATON_SPECIAL_CHARACTERS.matcher(regex).replaceAll("\\\\$0");
585             for (final var charClass : PREDEFINED_CHARACTER_CLASSES.entrySet()) {
586                 regex = regex.replaceAll(charClass.getKey(), charClass.getValue());
587             }
588             var defaultValue = "";
589             try {
590                 final var regExp = new RegExp(regex);
591                 defaultValue = regExp.toAutomaton().getShortestExample(true);
592             } catch (IllegalArgumentException ex) {
593                 LOG.warn("Cannot create example string for type: {} with regex: {}.", stringType.getQName(), regex);
594             }
595             def.setExample(defaultValue);
596         } else {
597             def.setExample("Some " + nodeName);
598         }
599
600         if (stringType.getDefaultValue().isPresent()) {
601             def.setDefaultValue(stringType.getDefaultValue().orElseThrow().toString());
602         }
603         return STRING_TYPE;
604     }
605
606     private String processNumberType(final RangeRestrictedTypeDefinition<?, ?> leafTypeDef,final TypeDef def) {
607         final var maybeLower = leafTypeDef.getRangeConstraint()
608             .map(RangeConstraint::getAllowedRanges).map(RangeSet::span).map(Range::lowerEndpoint);
609
610         if (isHexadecimalOrOctal(leafTypeDef)) {
611             return STRING_TYPE;
612         }
613
614         if (leafTypeDef instanceof DecimalTypeDefinition) {
615             if (leafTypeDef.getDefaultValue().isPresent()) {
616                 def.setDefaultValue(Decimal64.valueOf(
617                     leafTypeDef.getDefaultValue().orElseThrow().toString()).decimalValue());
618             }
619             if (maybeLower.isPresent()) {
620                 def.setExample(((Decimal64) maybeLower.orElseThrow()).decimalValue());
621             }
622             return NUMBER_TYPE;
623         }
624         if (leafTypeDef instanceof Uint8TypeDefinition
625             || leafTypeDef instanceof Uint16TypeDefinition
626             || leafTypeDef instanceof Int8TypeDefinition
627             || leafTypeDef instanceof Int16TypeDefinition
628             || leafTypeDef instanceof Int32TypeDefinition) {
629
630             def.setFormat("int32");
631             if (leafTypeDef.getDefaultValue().isPresent()) {
632                 def.setDefaultValue(Integer.parseInt(leafTypeDef.getDefaultValue().orElseThrow().toString()));
633             }
634             if (maybeLower.isPresent()) {
635                 def.setExample(Integer.parseInt(maybeLower.orElseThrow().toString()));
636             }
637         } else if (leafTypeDef instanceof Uint32TypeDefinition
638             || leafTypeDef instanceof Int64TypeDefinition) {
639             def.setFormat("int64");
640             if (leafTypeDef.getDefaultValue().isPresent()) {
641                 def.setDefaultValue(Long.parseLong(leafTypeDef.getDefaultValue().orElseThrow().toString()));
642             }
643             if (maybeLower.isPresent()) {
644                 def.setExample(Long.parseLong(maybeLower.orElseThrow().toString()));
645             }
646         } else {
647             //uint64
648             if (leafTypeDef.getDefaultValue().isPresent()) {
649                 def.setDefaultValue(new BigInteger(leafTypeDef.getDefaultValue().orElseThrow().toString()));
650             }
651             def.setExample(0);
652         }
653         return "integer";
654     }
655
656     private static boolean isHexadecimalOrOctal(final RangeRestrictedTypeDefinition<?, ?> typeDef) {
657         final var optDefaultValue = typeDef.getDefaultValue();
658         if (optDefaultValue.isPresent()) {
659             final var defaultValue = (String) optDefaultValue.orElseThrow();
660             return defaultValue.startsWith("0") || defaultValue.startsWith("-0");
661         }
662         return false;
663     }
664
665     private String processInstanceIdentifierType(final InstanceIdentifierTypeDefinition iidType,
666             final DataSchemaNode schemaNode, final EffectiveModelContext schemaContext,final TypeDef def) {
667         // create example instance-identifier to the first container of node's module if exists or leave it empty
668         final var module = schemaContext.findModule(schemaNode.getQName().getModule());
669         if (module.isPresent()) {
670             final var container = module.orElseThrow().getChildNodes().stream()
671                 .filter(n -> n instanceof ContainerSchemaNode)
672                 .findFirst();
673             if (container.isPresent()) {
674                 def.setExample(String.format("/%s:%s", module.orElseThrow().getPrefix(),
675                     container.orElseThrow().getQName().getLocalName()));
676             }
677         }
678         // set default value
679         if (iidType.getDefaultValue().isPresent()) {
680             def.setDefaultValue(iidType.getDefaultValue().orElseThrow().toString());
681         }
682         return STRING_TYPE;
683     }
684 }