1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.schemagen.json.impl;
7   
8   import com.fasterxml.jackson.databind.node.ArrayNode;
9   import com.fasterxml.jackson.databind.node.ObjectNode;
10  
11  import java.util.List;
12  import java.util.Set;
13  import java.util.stream.Collectors;
14  import java.util.stream.Stream;
15  
16  import dev.metaschema.core.model.IModule;
17  import dev.metaschema.core.util.CollectionUtil;
18  import dev.metaschema.core.util.ObjectUtils;
19  import dev.metaschema.schemagen.SchemaGenerationException;
20  import edu.umd.cs.findbugs.annotations.NonNull;
21  
22  /**
23   * Represents a JSON schema for a Metaschema-based module.
24   */
25  public class JsonSchemaModule
26      implements IJsonSchema {
27    private final IModule module;
28    private final List<IJsonSchemaDefinitionAssembly> roots;
29  
30    /**
31     * Construct a new JSON schema definition.
32     *
33     * @param module
34     *          the associated Metaschema-based module
35     * @param state
36     *          the JSON generation state
37     */
38    public JsonSchemaModule(
39        @NonNull IModule module,
40        @NonNull IJsonGenerationState state) {
41      this.module = module;
42      this.roots = module.getExportedRootAssemblyDefinitions().stream()
43          .map(root -> state.getAssemblyDefinition(ObjectUtils.notNull(root), null))
44          .collect(Collectors.toUnmodifiableList());
45    }
46  
47    @Override
48    public boolean isInline(IJsonGenerationState state) {
49      // always
50      return true;
51    }
52  
53    /**
54     * Get the schemas for referenced definitions for model objects and data types
55     * used within this module schema.
56     *
57     * @param state
58     *          the JSON generation state used for context
59     * @return a stream containing the referenced definitions
60     */
61    @NonNull
62    public Stream<IJsonSchemaDefinable> collectDefinitions(@NonNull IJsonGenerationState state) {
63      return ObjectUtils.notNull(roots.stream()
64          .flatMap(root -> root.collectDefinitions(CollectionUtil.emptySet(), state)));
65    }
66  
67    @Override
68    public void generateInlineJsonSchema(ObjectNode node, IJsonGenerationState state) {
69      node.put("$schema", "http://json-schema.org/draft-07/schema#");
70      node.put("$id",
71          String.format("%s/%s-%s-schema.json",
72              module.getXmlNamespace(),
73              module.getShortName(),
74              module.getVersion()));
75      node.put("$comment", module.getName().toMarkdown());
76      node.put("type", "object");
77  
78      if (roots.isEmpty()) {
79        throw new SchemaGenerationException("No root definitions found");
80      }
81  
82      node.set("definitions", generateDefinitions(state));
83  
84      if (roots.size() == 1) {
85        generateRoot(node, ObjectUtils.notNull(roots.iterator().next()), state);
86      } else {
87        ArrayNode oneOfNode = node.putArray("oneOf");
88        roots.forEach(root -> {
89          assert root != null;
90          ObjectNode rootNode = ObjectUtils.notNull(oneOfNode.addObject());
91          assert rootNode != null;
92  
93          generateRoot(rootNode, root, state);
94        });
95      }
96    }
97  
98    /**
99     * Generate the referenced JSON schema definitions used in this JSON schema.
100    *
101    * @param state
102    *          the JSON generation state used for context
103    * @return the definitions JSON schema node
104    */
105   private ObjectNode generateDefinitions(@NonNull IJsonGenerationState state) {
106 
107     // ensure all definitions are recorded
108     Set<IJsonSchemaDefinable> usedDefinitions = ObjectUtils.notNull(collectDefinitions(state)
109         .collect(Collectors.toUnmodifiableSet()));
110 
111     ObjectNode definitionsNode = ObjectUtils.notNull(state.getJsonNodeFactory().objectNode());
112 
113     usedDefinitions.stream()
114         .filter(definition -> !definition.isInline(state))
115         .distinct()
116         .sorted(JsonSchemaHelper.DEFINABLE_NAME_COMPARATOR)
117         .forEach(definition -> {
118           ObjectNode definitionNode = definitionsNode.putObject(definition.getDefinitionName());
119           assert definitionNode != null;
120           definition.generateDefinitionJsonSchema(definitionNode, state);
121         });
122 
123     state.generateDataTypeDefinitions(definitionsNode);
124 
125     return definitionsNode;
126   }
127 
128   private static void generateRoot(
129       @NonNull ObjectNode node,
130       @NonNull IJsonSchemaDefinitionAssembly schema,
131       @NonNull IJsonGenerationState state) {
132     ObjectNode propertiesObj = node.putObject("properties");
133 
134     propertiesObj.putObject("$schema")
135         .put("type", "string")
136         .put("format", "uri-reference");
137 
138     String name = schema.getDefinition().getRootJsonName();
139 
140     schema.generateJsonSchemaOrDefinitionRef(ObjectUtils.notNull(propertiesObj.putObject(name)), state);
141 
142     node.putArray("required")
143         .add(name);
144     node.put("additionalProperties", false);
145   }
146 }