001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.schemagen;
007
008import org.eclipse.jdt.annotation.NotOwning;
009
010import java.util.ArrayList;
011import java.util.LinkedList;
012import java.util.List;
013
014import dev.metaschema.core.configuration.IConfiguration;
015import dev.metaschema.core.metapath.IMetapathExpression;
016import dev.metaschema.core.model.IDefinition;
017import dev.metaschema.core.model.IModule;
018import dev.metaschema.core.model.INamedInstance;
019import dev.metaschema.core.model.IValuedDefinition;
020import dev.metaschema.core.model.constraint.IAllowedValue;
021import dev.metaschema.core.model.constraint.IAllowedValuesConstraint;
022import dev.metaschema.core.util.CollectionUtil;
023import dev.metaschema.core.util.ObjectUtils;
024import dev.metaschema.schemagen.datatype.IDatatypeManager;
025import edu.umd.cs.findbugs.annotations.NonNull;
026import edu.umd.cs.findbugs.annotations.Nullable;
027
028/**
029 * Provides a common base implementation for schema generation state management.
030 * <p>
031 * This abstract class maintains the context required during schema generation,
032 * including the module being processed, the output writer, datatype management,
033 * and inlining strategy.
034 *
035 * @param <WRITER>
036 *          the type of writer used for schema output
037 * @param <DATATYPE_MANAGER>
038 *          the type of datatype manager used for type name resolution
039 */
040public abstract class AbstractGenerationState<WRITER, DATATYPE_MANAGER extends IDatatypeManager>
041    implements IGenerationState<WRITER> {
042  @NonNull
043  private final IModule module;
044  @NonNull
045  private final WRITER writer;
046  @NonNull
047  private final DATATYPE_MANAGER datatypeManager;
048  @NonNull
049  private final IInlineStrategy inlineStrategy;
050
051  @NonNull
052  private final ModuleIndex moduleIndex;
053
054  /**
055   * Construct a new generation state instance.
056   *
057   * @param module
058   *          the Metaschema module to generate a schema for
059   * @param writer
060   *          the output writer for the generated schema
061   * @param configuration
062   *          the configuration controlling schema generation behavior
063   * @param datatypeManager
064   *          the manager for handling datatype name resolution
065   */
066  public AbstractGenerationState(
067      @NonNull IModule module,
068      @NonNull WRITER writer,
069      @NonNull IConfiguration<SchemaGenerationFeature<?>> configuration,
070      @NonNull DATATYPE_MANAGER datatypeManager) {
071    this.module = module;
072    this.writer = writer;
073    this.datatypeManager = datatypeManager;
074    this.inlineStrategy = IInlineStrategy.newInlineStrategy(configuration);
075    this.moduleIndex = ModuleIndex.indexDefinitions(module, this.inlineStrategy);
076  }
077
078  @Override
079  public IModule getModule() {
080    return module;
081  }
082
083  @Override
084  @NotOwning
085  public WRITER getWriter() {
086    return writer;
087  }
088
089  /**
090   * Get the datatype manager used for type name resolution.
091   *
092   * @return the datatype manager
093   */
094  @NonNull
095  protected DATATYPE_MANAGER getDatatypeManager() {
096    return datatypeManager;
097  }
098
099  /**
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}