1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.schemagen;
7   
8   import org.eclipse.jdt.annotation.NotOwning;
9   
10  import java.util.ArrayList;
11  import java.util.LinkedList;
12  import java.util.List;
13  
14  import dev.metaschema.core.configuration.IConfiguration;
15  import dev.metaschema.core.metapath.IMetapathExpression;
16  import dev.metaschema.core.model.IDefinition;
17  import dev.metaschema.core.model.IModule;
18  import dev.metaschema.core.model.INamedInstance;
19  import dev.metaschema.core.model.IValuedDefinition;
20  import dev.metaschema.core.model.constraint.IAllowedValue;
21  import dev.metaschema.core.model.constraint.IAllowedValuesConstraint;
22  import dev.metaschema.core.util.CollectionUtil;
23  import dev.metaschema.core.util.ObjectUtils;
24  import dev.metaschema.schemagen.datatype.IDatatypeManager;
25  import edu.umd.cs.findbugs.annotations.NonNull;
26  import edu.umd.cs.findbugs.annotations.Nullable;
27  
28  /**
29   * Provides a common base implementation for schema generation state management.
30   * <p>
31   * This abstract class maintains the context required during schema generation,
32   * including the module being processed, the output writer, datatype management,
33   * and inlining strategy.
34   *
35   * @param <WRITER>
36   *          the type of writer used for schema output
37   * @param <DATATYPE_MANAGER>
38   *          the type of datatype manager used for type name resolution
39   */
40  public abstract class AbstractGenerationState<WRITER, DATATYPE_MANAGER extends IDatatypeManager>
41      implements IGenerationState<WRITER> {
42    @NonNull
43    private final IModule module;
44    @NonNull
45    private final WRITER writer;
46    @NonNull
47    private final DATATYPE_MANAGER datatypeManager;
48    @NonNull
49    private final IInlineStrategy inlineStrategy;
50  
51    @NonNull
52    private final ModuleIndex moduleIndex;
53  
54    /**
55     * Construct a new generation state instance.
56     *
57     * @param module
58     *          the Metaschema module to generate a schema for
59     * @param writer
60     *          the output writer for the generated schema
61     * @param configuration
62     *          the configuration controlling schema generation behavior
63     * @param datatypeManager
64     *          the manager for handling datatype name resolution
65     */
66    public AbstractGenerationState(
67        @NonNull IModule module,
68        @NonNull WRITER writer,
69        @NonNull IConfiguration<SchemaGenerationFeature<?>> configuration,
70        @NonNull DATATYPE_MANAGER datatypeManager) {
71      this.module = module;
72      this.writer = writer;
73      this.datatypeManager = datatypeManager;
74      this.inlineStrategy = IInlineStrategy.newInlineStrategy(configuration);
75      this.moduleIndex = ModuleIndex.indexDefinitions(module, this.inlineStrategy);
76    }
77  
78    @Override
79    public IModule getModule() {
80      return module;
81    }
82  
83    @Override
84    @NotOwning
85    public WRITER getWriter() {
86      return writer;
87    }
88  
89    /**
90     * Get the datatype manager used for type name resolution.
91     *
92     * @return the datatype manager
93     */
94    @NonNull
95    protected DATATYPE_MANAGER getDatatypeManager() {
96      return datatypeManager;
97    }
98  
99    /**
100    * Get the module index containing indexed definitions from the module.
101    *
102    * @return the module index
103    */
104   @NonNull
105   public ModuleIndex getMetaschemaIndex() {
106     return moduleIndex;
107   }
108 
109   @Override
110   public boolean isInline(@NonNull IDefinition definition) {
111     return inlineStrategy.isInline(definition, getMetaschemaIndex());
112   }
113 
114   /**
115    * Retrieve any allowed values that are context independent, meaning they always
116    * apply regardless of the location of the node in the larger graph.
117    *
118    * @param definition
119    *          the definition to get allowed values for
120    * @return the list of allowed values or an empty list
121    */
122   @NonNull
123   protected static AllowedValueCollection getContextIndependentEnumeratedValues(
124       @NonNull IValuedDefinition definition) {
125     List<IAllowedValue> values = new LinkedList<>();
126     boolean closed = false;
127     for (IAllowedValuesConstraint constraint : definition.getAllowedValuesConstraints()) {
128       assert constraint != null;
129       if (!constraint.isAllowedOther()) {
130         closed = true;
131       }
132 
133       // FIXME: Should this compare the actual compiled expression?
134       if (!IMetapathExpression.contextNode().getPath().equals(constraint.getTarget().getPath())) {
135         values = CollectionUtil.emptyList();
136         break;
137       }
138 
139       values.addAll(constraint.getAllowedValues().values());
140     }
141     return new AllowedValueCollection(closed, values);
142   }
143 
144   /**
145    * Get the name of the definition (and any parent instances/definition) to
146    * ensure an inline type is unique.
147    *
148    * @param definition
149    *          the definition to generate a type name for
150    * @param childModule
151    *          the module of the left node
152    * @return the unique type name
153    */
154   private CharSequence getTypeContext(
155       @NonNull IDefinition definition,
156       @NonNull IModule childModule) {
157     StringBuilder builder = new StringBuilder();
158     if (definition.isInline()) {
159       INamedInstance inlineInstance = definition.getInlineInstance();
160       IDefinition parentDefinition = inlineInstance.getContainingDefinition();
161 
162       builder
163           .append(getTypeContext(parentDefinition, childModule))
164           .append(IGenerationState.toCamelCase(inlineInstance.getEffectiveName()));
165     } else {
166       builder.append(IGenerationState.toCamelCase(definition.getName()));
167     }
168     return builder;
169   }
170 
171   @Override
172   @NonNull
173   public String getTypeNameForDefinition(@NonNull IDefinition definition, @Nullable String suffix) {
174     StringBuilder builder = new StringBuilder()
175         .append(IGenerationState.toCamelCase(definition.getModelType().name()))
176         .append(IGenerationState.toCamelCase(definition.getContainingModule().getShortName()));
177 
178     if (isInline(definition)) {
179       builder.append(IGenerationState.toCamelCase(definition.getEffectiveName()));
180     } else {
181       // need to append the parent name(s) to disambiguate this type name
182       builder.append(getTypeContext(definition, definition.getContainingModule()));
183     }
184     if (suffix != null && !suffix.isBlank()) {
185       builder.append(suffix);
186     }
187     builder.append("Type");
188 
189     return ObjectUtils.notNull(builder.toString());
190   }
191 
192   /**
193    * Represents a collection of allowed values with a flag indicating whether the
194    * value set is closed (no other values allowed) or open.
195    */
196   public static class AllowedValueCollection {
197     private final boolean closed;
198     @NonNull
199     private final List<IAllowedValue> values;
200 
201     /**
202      * Construct a new allowed value collection.
203      *
204      * @param closed
205      *          {@code true} if only the specified values are allowed, {@code false}
206      *          if other values are also permitted
207      * @param values
208      *          the list of allowed values
209      */
210     public AllowedValueCollection(boolean closed, @NonNull List<IAllowedValue> values) {
211       this.closed = closed;
212       this.values = CollectionUtil.unmodifiableList(new ArrayList<>(values));
213     }
214 
215     /**
216      * Determine if the allowed value set is closed.
217      *
218      * @return {@code true} if only the specified values are allowed, {@code false}
219      *         if other values are also permitted
220      */
221     public boolean isClosed() {
222       return closed;
223     }
224 
225     /**
226      * Get the list of allowed values.
227      *
228      * @return an unmodifiable list of allowed values
229      */
230     @NonNull
231     public List<IAllowedValue> getValues() {
232       return values;
233     }
234   }
235 }