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}