1
2
3
4
5
6 package dev.metaschema.schemagen.json.impl;
7
8 import com.fasterxml.jackson.databind.JsonNode;
9 import com.fasterxml.jackson.databind.node.ArrayNode;
10 import com.fasterxml.jackson.databind.node.BigIntegerNode;
11 import com.fasterxml.jackson.databind.node.BooleanNode;
12 import com.fasterxml.jackson.databind.node.DecimalNode;
13 import com.fasterxml.jackson.databind.node.DoubleNode;
14 import com.fasterxml.jackson.databind.node.IntNode;
15 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
16 import com.fasterxml.jackson.databind.node.LongNode;
17 import com.fasterxml.jackson.databind.node.ObjectNode;
18 import com.fasterxml.jackson.databind.node.TextNode;
19
20 import java.math.BigDecimal;
21 import java.math.BigInteger;
22 import java.util.ArrayList;
23 import java.util.Collection;
24 import java.util.Comparator;
25 import java.util.LinkedList;
26 import java.util.List;
27 import java.util.Set;
28 import java.util.stream.Collectors;
29 import java.util.stream.Stream;
30
31 import dev.metaschema.core.datatype.IDataTypeAdapter;
32 import dev.metaschema.core.datatype.markup.MarkupLine;
33 import dev.metaschema.core.datatype.markup.MarkupMultiline;
34 import dev.metaschema.core.model.IAnyInstance;
35 import dev.metaschema.core.model.IChoiceInstance;
36 import dev.metaschema.core.model.IContainerModelAbsolute;
37 import dev.metaschema.core.model.IFlagInstance;
38 import dev.metaschema.core.model.IModelDefinition;
39 import dev.metaschema.core.model.IModelElement;
40 import dev.metaschema.core.model.INamedModelElement;
41 import dev.metaschema.core.model.IValuedDefinition;
42 import dev.metaschema.core.model.IValuedInstance;
43 import dev.metaschema.core.qname.IEnhancedQName;
44 import dev.metaschema.core.util.CollectionUtil;
45 import dev.metaschema.core.util.ObjectUtils;
46 import edu.umd.cs.findbugs.annotations.NonNull;
47 import edu.umd.cs.findbugs.annotations.Nullable;
48
49
50
51
52
53
54
55
56 public final class JsonSchemaHelper {
57
58
59
60 public static final Comparator<IJsonSchemaPropertyNamed> INSTANCE_NAME_COMPARATOR
61 = Comparator.comparing(IJsonSchemaPropertyNamed::getName);
62
63
64
65 public static final Comparator<IJsonSchemaDefinable> DEFINABLE_NAME_COMPARATOR
66 = Comparator.comparing(IJsonSchemaDefinable::getDefinitionName);
67
68
69
70
71
72
73
74
75
76 public static void generateTitle(
77 @NonNull INamedModelElement named,
78 @NonNull ObjectNode obj) {
79 String formalName = named.getEffectiveFormalName();
80 if (formalName != null) {
81 obj.put("title", formalName);
82 }
83 }
84
85
86
87
88
89
90
91
92
93
94
95
96 public static <NAMED extends INamedModelElement & IModelElement> void generateDescription(
97 @NonNull NAMED named,
98 @NonNull ObjectNode obj) {
99 MarkupLine description = named.getEffectiveDescription();
100
101 StringBuilder retval = null;
102 if (description != null) {
103 retval = new StringBuilder().append(description.toMarkdown());
104 }
105
106 MarkupMultiline remarks = named.getRemarks();
107 if (remarks != null) {
108 if (retval == null) {
109 retval = new StringBuilder();
110 } else {
111 retval.append("\n\n");
112 }
113 retval.append(remarks.toMarkdown());
114 }
115 if (retval != null) {
116 obj.put("description", retval.toString());
117 }
118 }
119
120
121
122
123
124
125
126
127
128
129 public static void generateDefault(
130 @NonNull IValuedInstance instance,
131 @NonNull ObjectNode obj) {
132 Object defaultValue = instance.getEffectiveDefaultValue();
133 if (defaultValue != null) {
134 IValuedDefinition definition = instance.getDefinition();
135 IDataTypeAdapter<?> adapter = definition.getJavaTypeAdapter();
136 obj.set("default", toJsonValue(defaultValue, adapter));
137 }
138 }
139
140 private static JsonNode toJsonValue(
141 @Nullable Object defaultValue,
142 @NonNull IDataTypeAdapter<?> adapter) {
143 JsonNode retval = null;
144 switch (adapter.getJsonRawType()) {
145 case BOOLEAN:
146 if (defaultValue instanceof Boolean) {
147 retval = BooleanNode.valueOf((Boolean) defaultValue);
148 }
149 break;
150 case INTEGER:
151 if (defaultValue instanceof BigInteger) {
152 retval = BigIntegerNode.valueOf((BigInteger) defaultValue);
153 } else if (defaultValue instanceof Integer) {
154 retval = IntNode.valueOf((Integer) defaultValue);
155 } else if (defaultValue instanceof Long) {
156 retval = LongNode.valueOf((Long) defaultValue);
157 }
158 break;
159 case NUMBER:
160 if (defaultValue instanceof BigDecimal) {
161 retval = DecimalNode.valueOf((BigDecimal) defaultValue);
162 } else if (defaultValue instanceof Double) {
163 retval = DoubleNode.valueOf((Double) defaultValue);
164 }
165 break;
166 case ANY:
167 case ARRAY:
168 case OBJECT:
169 case NULL:
170 throw new UnsupportedOperationException("Invalid type: " + adapter.getClass());
171 case STRING:
172
173 break;
174 }
175
176 if (retval == null && defaultValue != null) {
177 retval = TextNode.valueOf(adapter.asString(defaultValue));
178 }
179 return retval;
180 }
181
182
183
184
185
186
187
188
189
190 @NonNull
191 public static String generateDefinitionJsonPointer(@NonNull IJsonSchemaDefinable schema) {
192 return ObjectUtils.notNull(new StringBuilder()
193 .append("#/definitions/")
194 .append(schema.getDefinitionName())
195 .toString());
196 }
197
198
199
200
201
202
203
204
205
206
207
208 public static void generateProperties(
209 @NonNull Collection<IJsonSchemaPropertyNamed> properties,
210 @NonNull ObjectNode node,
211 @NonNull IJsonGenerationState state) {
212
213 if (!properties.isEmpty()) {
214 ObjectNode propertiesNode = node.putObject("properties");
215
216 List<String> required = new LinkedList<>();
217 properties.forEach(property -> {
218 ObjectNode propertyNode = ObjectUtils.notNull(state.getJsonNodeFactory().objectNode());
219 property.generate(propertyNode, state);
220
221 String name = property.getName();
222 propertiesNode.set(name, propertyNode);
223
224 if (property.isRequired()) {
225 required.add(name);
226 }
227 });
228
229 if (!required.isEmpty()) {
230 ArrayNode requiredNode = node.putArray("required");
231 required.forEach(requiredNode::add);
232 }
233 }
234 }
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252 @NonNull
253 public static List<IJsonSchemaPropertyFlag> buildFlagProperties(
254 @NonNull IModelDefinition definition,
255 @Nullable IEnhancedQName jsonKeyFlagName,
256 @NonNull IJsonGenerationState state) {
257
258 Stream<? extends IFlagInstance> flagStream = definition.getFlagInstances().stream();
259
260
261 if (jsonKeyFlagName != null) {
262 IFlagInstance jsonKeyFlag = definition.getFlagInstanceByName(jsonKeyFlagName.getIndexPosition());
263 if (jsonKeyFlag == null) {
264 throw new IllegalArgumentException(
265 String.format("The referenced json-key flag-name '%s' does not exist on definition '%s'.",
266 jsonKeyFlagName,
267 definition.getName()));
268 }
269 flagStream = flagStream.filter(instance -> !jsonKeyFlag.equals(instance));
270 }
271
272 return ObjectUtils.notNull(flagStream
273 .map(instance -> state.getJsonSchemaPropertyFlag(ObjectUtils.requireNonNull(instance)))
274 .collect(Collectors.toUnmodifiableList()));
275 }
276
277
278
279
280
281
282
283
284
285
286
287
288
289 @NonNull
290 public static List<IJsonSchemaPropertyNamed> buildModelProperties(
291 @NonNull IContainerModelAbsolute definition,
292 @NonNull IJsonGenerationState state) {
293 return ObjectUtils.notNull(definition.getModelInstances().stream()
294
295 .filter(instance -> !(instance instanceof IChoiceInstance))
296 .filter(instance -> !(instance instanceof IAnyInstance))
297 .map(instance -> state.getJsonSchemaPropertyModel(ObjectUtils.notNull(instance)))
298 .collect(Collectors.toUnmodifiableList()));
299 }
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315 public static void generateFieldBody(
316 @NonNull IJsonSchemaDefinitionField field,
317 @NonNull ObjectNode node,
318 @NonNull IJsonGenerationState state) {
319 if (field.getNonValueProperties().isEmpty()) {
320
321 field.getFieldValue().generateJsonSchemaOrDefinitionRef(node, state);
322 } else {
323 generateComplexFieldBody(field, node, state);
324 }
325 }
326
327 private static void generateComplexFieldBody(
328 @NonNull IJsonSchemaDefinitionField field,
329 @NonNull ObjectNode node,
330 @NonNull IJsonGenerationState state) {
331 List<? extends IJsonSchemaPropertyNamed> nonValueProperties = field.getNonValueProperties();
332 node.put("type", "object");
333
334 IFlagInstance jsonValueKeyFlag = field.getDefinition().getJsonValueKeyFlagInstance();
335
336 Stream<? extends IJsonSchemaPropertyNamed> propertiesStream = nonValueProperties.stream();
337
338 if (jsonValueKeyFlag == null) {
339
340 propertiesStream = Stream.concat(propertiesStream, Stream.of(new FieldValueProperty(field)));
341 } else {
342 propertiesStream = propertiesStream.filter(property -> !(property instanceof IJsonSchemaPropertyFlag)
343 || !jsonValueKeyFlag.equals(((IJsonSchemaPropertyFlag) property).getInstance()));
344 }
345
346 List<IJsonSchemaPropertyNamed> properties = ObjectUtils.notNull(propertiesStream
347 .sorted(INSTANCE_NAME_COMPARATOR)
348 .collect(Collectors.toUnmodifiableList()));
349 generateProperties(
350 properties,
351 node,
352 state);
353
354 if (jsonValueKeyFlag == null) {
355 node.put("additionalProperties", false);
356 } else {
357 ObjectNode additionalPropertiesTypeNode;
358
359 additionalPropertiesTypeNode = ObjectUtils.notNull(JsonNodeFactory.instance.objectNode());
360
361 field.getFieldValue().generateJsonSchemaOrDefinitionRef(additionalPropertiesTypeNode, state);
362
363 ObjectNode additionalPropertiesNode = ObjectUtils.notNull(JsonNodeFactory.instance.objectNode());
364 ArrayNode allOf = additionalPropertiesNode.putArray("allOf");
365 allOf.add(additionalPropertiesTypeNode);
366 allOf.addObject()
367 .put(
368 "minProperties",
369
370 properties.stream()
371 .filter(IJsonSchemaPropertyNamed::isRequired)
372 .count() + 1)
373 .put("maxProperties", properties.size() + 1);
374
375 node.set("additionalProperties", additionalPropertiesNode);
376 }
377 }
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392 public static void generateAssemblyBody(
393 @NonNull IJsonSchemaDefinitionAssembly assembly,
394 @NonNull ObjectNode node,
395 @NonNull IJsonGenerationState state) {
396 node.put("type", "object");
397
398
399 boolean hasAny = assembly.getDefinition().getAnyInstance() != null;
400
401 List<JsonSchemaHelper.Choice> availableChoices = assembly.getChoices();
402
403 if (availableChoices.size() == 1) {
404 generateProperties(
405 availableChoices.iterator().next().getCombinations(),
406 node,
407 state);
408 node.put("additionalProperties", hasAny);
409 } else if (availableChoices.size() > 1) {
410 ArrayNode oneOf = node.putArray("anyOf");
411 availableChoices.forEach(choice -> {
412 ObjectNode schemaNode = ObjectUtils.notNull(oneOf.addObject());
413
414 generateProperties(
415 choice.getCombinations(),
416 schemaNode,
417 state);
418 schemaNode.put("additionalProperties", hasAny);
419 });
420 }
421 }
422
423 private static class FieldValueProperty implements IJsonSchemaPropertyNamed {
424 private final IJsonSchemaDefinitionField field;
425
426 public FieldValueProperty(IJsonSchemaDefinitionField field) {
427 this.field = field;
428 }
429
430 @Override
431 public String getName() {
432 return field.getDefinition().getEffectiveJsonValueKeyName();
433 }
434
435 @Override
436 public Stream<IJsonSchemaDefinable> collectDefinitions(
437 Set<IJsonSchemaDefinitionAssembly> visited,
438 IJsonGenerationState state) {
439 return ObjectUtils.notNull(Stream.empty());
440 }
441
442 @Override
443 public void generate(ObjectNode node, IJsonGenerationState state) {
444 field.getFieldValue().generateJsonSchemaOrDefinitionRef(node, state);
445 }
446
447 @Override
448 public boolean isRequired() {
449 return true;
450 }
451 }
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467 @NonNull
468 public static Stream<Choice> explodeChoices(
469 @NonNull Choice baseChoice,
470 @NonNull List<? extends IChoiceInstance> choiceInstances,
471 @NonNull IJsonGenerationState state) {
472 Stream<Choice> retval = Stream.of(baseChoice);
473 for (IChoiceInstance choice : choiceInstances) {
474 List<IJsonSchemaPropertyNamed> newChoices = buildModelProperties(ObjectUtils.notNull(choice), state);
475 retval = retval.flatMap(oldChoice -> oldChoice.explode(newChoices));
476 }
477 return ObjectUtils.notNull(retval);
478 }
479
480
481
482
483
484
485
486 public static final class Choice {
487 @NonNull
488 private final List<IJsonSchemaPropertyNamed> combinations;
489
490
491
492
493
494
495
496 public Choice(@NonNull List<IJsonSchemaPropertyNamed> combinations) {
497 this.combinations = combinations;
498 }
499
500
501
502
503
504
505 @NonNull
506 public List<IJsonSchemaPropertyNamed> getCombinations() {
507 return combinations;
508 }
509
510
511
512
513
514
515
516
517
518
519
520 @NonNull
521 public Stream<Choice> explode(@NonNull List<IJsonSchemaPropertyNamed> newChoices) {
522 return ObjectUtils.notNull(newChoices.isEmpty()
523 ? Stream.of(this)
524 : newChoices.stream()
525 .map(next -> {
526 List<IJsonSchemaPropertyNamed> retval = new ArrayList<>(combinations.size() + 1);
527 retval.addAll(combinations);
528 retval.add(next);
529 return new Choice(CollectionUtil.unmodifiableList(retval));
530 }));
531 }
532 }
533
534 private JsonSchemaHelper() {
535
536 }
537 }