getAllowedValueLocations() {
+ return nodeItemAnalysis.values();
+ }
+
+ /**
+ * Visit all definitions in the provided module to collect allowed-values
+ * constraints.
+ *
+ * This method creates a new {@link DynamicContext} configured with the module's
+ * default namespace and with predicate evaluation disabled.
+ *
+ * @param module
+ * the Metaschema module to visit
+ */
+ public void visit(@NonNull IModule module) {
+ DynamicContext context = new DynamicContext(
+ StaticContext.builder()
+ .defaultModelNamespace(module.getXmlNamespace())
+ .build());
+ context.disablePredicateEvaluation();
+
+ visit(INodeItemFactory.instance().newModuleNodeItem(module), context);
+ }
+
+ /**
+ * Visit all definitions in the provided module node item using the given
+ * dynamic context.
+ *
+ * @param module
+ * the module node item to visit
+ * @param context
+ * the dynamic context to use for constraint evaluation
+ */
+ public void visit(@NonNull IModuleNodeItem module, @NonNull DynamicContext context) {
+ visitMetaschema(module, context);
+ }
+
+ @SuppressWarnings("PMD.AvoidCatchingGenericException")
+ private void handleAllowedValuesAtLocation(
+ @NonNull IDefinitionNodeItem, ?> itemLocation,
+ @NonNull DynamicContext context) {
+ itemLocation.getDefinition().getAllowedValuesConstraints().stream()
+ .forEachOrdered(allowedValues -> {
+ ISequence> result = allowedValues.getTarget().evaluate(itemLocation, context);
+ result.stream().forEachOrdered(target -> {
+ assert target != null;
+ handleAllowedValues(allowedValues, itemLocation, (IDefinitionNodeItem, ?>) target);
+ });
+ });
+ }
+
+ private void handleAllowedValues(
+ @NonNull IAllowedValuesConstraint allowedValues,
+ @NonNull IDefinitionNodeItem, ?> location,
+ @NonNull IDefinitionNodeItem, ?> target) {
+ NodeItemRecord itemRecord = nodeItemAnalysis.get(target);
+ if (itemRecord == null) {
+ itemRecord = new NodeItemRecord(target);
+ nodeItemAnalysis.put(target, itemRecord);
+ }
+
+ AllowedValuesRecord allowedValuesRecord = new AllowedValuesRecord(allowedValues, location, target);
+ itemRecord.addAllowedValues(allowedValuesRecord);
+ }
+
+ @Override
+ public Void visitFlag(IFlagNodeItem item, DynamicContext context) {
+ assert context != null;
+ DynamicContext subContext = handleLetStatements(item, context);
+ handleAllowedValuesAtLocation(item, subContext);
+ return super.visitFlag(item, subContext);
+ }
+
+ @Override
+ public Void visitField(IFieldNodeItem item, DynamicContext context) {
+ assert context != null;
+ DynamicContext subContext = handleLetStatements(item, context);
+ handleAllowedValuesAtLocation(item, subContext);
+ return super.visitField(item, subContext);
+ }
+
+ @Override
+ public Void visitAssembly(IAssemblyNodeItem item, DynamicContext context) {
+ assert context != null;
+ DynamicContext subContext = handleLetStatements(item, context);
+ handleAllowedValuesAtLocation(item, subContext);
+ return super.visitAssembly(item, subContext);
+ }
+
+ @SuppressWarnings("PMD.AssignmentInOperand")
+ private DynamicContext handleLetStatements(IDefinitionNodeItem, ?> item, DynamicContext context) {
+ assert context != null;
+ DynamicContext subContext = context;
+ for (ILet let : item.getDefinition().getLetExpressions().values()) {
+ ISequence> result = let.getValueExpression().evaluate(item,
+ subContext).reusable();
+ subContext = subContext.bindVariableValue(let.getName(), result);
+ }
+ return subContext;
+ }
+
+ @Override
+ public Void visitAssembly(IAssemblyInstanceGroupedNodeItem item, DynamicContext context) {
+ return visitAssembly((IAssemblyNodeItem) item, context);
+ }
+
+ @Override
+ protected Void defaultResult() {
+ return null;
+ }
+
+ /**
+ * A record that associates a definition node item with all the allowed-values
+ * constraints that target it.
+ */
+ public static final class NodeItemRecord {
+ @NonNull
+ private final IDefinitionNodeItem, ?> item;
+ @NonNull
+ private final List allowedValues = new LinkedList<>();
+
+ private NodeItemRecord(@NonNull IDefinitionNodeItem, ?> item) {
+ this.item = item;
+ }
+
+ /**
+ * Get the definition node item that is targeted by the allowed-values
+ * constraints.
+ *
+ * @return the target node item
+ */
+ @NonNull
+ public IDefinitionNodeItem, ?> getItem() {
+ return item;
+ }
+
+ /**
+ * Get the list of allowed-values constraint records targeting this node item.
+ *
+ * @return the list of allowed-values records
+ */
+ @NonNull
+ public List getAllowedValues() {
+ return allowedValues;
+ }
+
+ /**
+ * Add an allowed-values constraint record to this node item.
+ *
+ * @param record
+ * the allowed-values record to add
+ */
+ public void addAllowedValues(@NonNull AllowedValuesRecord record) {
+ this.allowedValues.add(record);
+ }
+ }
+
+ /**
+ * A record capturing the relationship between an allowed-values constraint, the
+ * definition where it is declared, and the target node it applies to.
+ */
+ public static final class AllowedValuesRecord {
+ @NonNull
+ private final IAllowedValuesConstraint allowedValues;
+ @NonNull
+ private final IDefinitionNodeItem, ?> location;
+ @NonNull
+ private final IDefinitionNodeItem, ?> target;
+
+ /**
+ * Construct a new allowed-values record.
+ *
+ * @param allowedValues
+ * the allowed-values constraint
+ * @param location
+ * the definition node item where the constraint is declared
+ * @param target
+ * the definition node item that the constraint targets
+ */
+ public AllowedValuesRecord(
+ @NonNull IAllowedValuesConstraint allowedValues,
+ @NonNull IDefinitionNodeItem, ?> location,
+ @NonNull IDefinitionNodeItem, ?> target) {
+ this.allowedValues = allowedValues;
+ this.location = location;
+ this.target = target;
+ }
+
+ /**
+ * Get the allowed-values constraint.
+ *
+ * @return the allowed-values constraint
+ */
+ @NonNull
+ public IAllowedValuesConstraint getAllowedValues() {
+ return allowedValues;
+ }
+
+ /**
+ * Get the definition node item where the constraint is declared.
+ *
+ * @return the location node item
+ */
+ @NonNull
+ public IDefinitionNodeItem, ?> getLocation() {
+ return location;
+ }
+
+ /**
+ * Get the definition node item that the constraint targets.
+ *
+ * @return the target node item
+ */
+ @NonNull
+ public IDefinitionNodeItem, ?> getTarget() {
+ return target;
+ }
+ }
+}
diff --git a/core/src/test/java/dev/metaschema/core/metapath/item/node/AllowedValueCollectingNodeItemVisitorTest.java b/core/src/test/java/dev/metaschema/core/metapath/item/node/AllowedValueCollectingNodeItemVisitorTest.java
new file mode 100644
index 000000000..d2f991c04
--- /dev/null
+++ b/core/src/test/java/dev/metaschema/core/metapath/item/node/AllowedValueCollectingNodeItemVisitorTest.java
@@ -0,0 +1,147 @@
+/*
+ * SPDX-FileCopyrightText: none
+ * SPDX-License-Identifier: CC0-1.0
+ */
+
+package dev.metaschema.core.metapath.item.node;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+import java.util.Collection;
+import java.util.List;
+
+import dev.metaschema.core.datatype.markup.MarkupLine;
+import dev.metaschema.core.metapath.IMetapathExpression;
+import dev.metaschema.core.metapath.item.node.AllowedValueCollectingNodeItemVisitor.AllowedValuesRecord;
+import dev.metaschema.core.metapath.item.node.AllowedValueCollectingNodeItemVisitor.NodeItemRecord;
+import dev.metaschema.core.model.IAssemblyDefinition;
+import dev.metaschema.core.model.IModule;
+import dev.metaschema.core.model.ISource;
+import dev.metaschema.core.model.constraint.IAllowedValue;
+import dev.metaschema.core.model.constraint.IAllowedValuesConstraint;
+import dev.metaschema.core.model.constraint.IValueConstrained;
+import dev.metaschema.core.testsupport.MockedModelTestSupport;
+import dev.metaschema.core.testsupport.builder.IModuleBuilder;
+
+class AllowedValueCollectingNodeItemVisitorTest {
+
+ private static final String TEST_NAMESPACE = "http://example.com/ns/allowed-values-test";
+
+ @Test
+ void testVisitorFindsAllowedValuesConstraints() {
+ MockedModelTestSupport mocking = new MockedModelTestSupport();
+ ISource source = ISource.externalSource(URI.create(TEST_NAMESPACE));
+
+ IModule module = IModuleBuilder.builder()
+ .namespace(TEST_NAMESPACE)
+ .shortName("av-test")
+ .version("1.0.0")
+ .source(source)
+ .assembly(mocking.assembly()
+ .name("root")
+ .rootName("root")
+ .flags(List.of(mocking.flag().name("status"))))
+ .toModule();
+
+ // Add an allowed-values constraint to the assembly targeting the status flag
+ IAssemblyDefinition rootDef = module.getAssemblyDefinitions().iterator().next();
+ IValueConstrained constraintSupport = rootDef.getConstraintSupport();
+ constraintSupport.addConstraint(
+ IAllowedValuesConstraint.builder()
+ .source(source)
+ .target(IMetapathExpression.compile("@status"))
+ .allowedValue(IAllowedValue.of("active", MarkupLine.fromMarkdown("Active"), null))
+ .allowedValue(IAllowedValue.of("inactive", MarkupLine.fromMarkdown("Inactive"), null))
+ .build());
+
+ AllowedValueCollectingNodeItemVisitor visitor = new AllowedValueCollectingNodeItemVisitor();
+ visitor.visit(module);
+
+ Collection locations = visitor.getAllowedValueLocations();
+ assertFalse(locations.isEmpty(), "Visitor should find at least one allowed-values location");
+
+ NodeItemRecord record = locations.iterator().next();
+ assertNotNull(record.getItem(), "Node item should not be null");
+ assertFalse(record.getAllowedValues().isEmpty(), "Should have at least one allowed-values constraint");
+
+ AllowedValuesRecord avRecord = record.getAllowedValues().get(0);
+ assertNotNull(avRecord.getAllowedValues(), "AllowedValuesConstraint should not be null");
+ assertNotNull(avRecord.getLocation(), "Location should not be null");
+ assertNotNull(avRecord.getTarget(), "Target should not be null");
+ }
+
+ @Test
+ void testVisitorReturnsEmptyForModuleWithoutConstraints() {
+ MockedModelTestSupport mocking = new MockedModelTestSupport();
+ ISource source = ISource.externalSource(URI.create(TEST_NAMESPACE));
+
+ IModule module = IModuleBuilder.builder()
+ .namespace(TEST_NAMESPACE)
+ .shortName("no-constraints-test")
+ .version("1.0.0")
+ .source(source)
+ .assembly(mocking.assembly()
+ .name("root")
+ .rootName("root")
+ .flags(List.of(mocking.flag().name("id"))))
+ .toModule();
+
+ AllowedValueCollectingNodeItemVisitor visitor = new AllowedValueCollectingNodeItemVisitor();
+ visitor.visit(module);
+
+ Collection locations = visitor.getAllowedValueLocations();
+ assertTrue(locations.isEmpty(), "Visitor should return empty for module without allowed-values constraints");
+ }
+
+ @Test
+ void testVisitorHandlesMultipleConstraints() {
+ MockedModelTestSupport mocking = new MockedModelTestSupport();
+ ISource source = ISource.externalSource(URI.create(TEST_NAMESPACE));
+
+ IModule module = IModuleBuilder.builder()
+ .namespace(TEST_NAMESPACE)
+ .shortName("multi-constraints-test")
+ .version("1.0.0")
+ .source(source)
+ .assembly(mocking.assembly()
+ .name("root")
+ .rootName("root")
+ .flags(List.of(
+ mocking.flag().name("type"),
+ mocking.flag().name("status"))))
+ .toModule();
+
+ // Add two allowed-values constraints on the assembly targeting different flags
+ IAssemblyDefinition rootDef = module.getAssemblyDefinitions().iterator().next();
+ IValueConstrained constraintSupport = rootDef.getConstraintSupport();
+ constraintSupport.addConstraint(
+ IAllowedValuesConstraint.builder()
+ .source(source)
+ .target(IMetapathExpression.compile("@type"))
+ .allowedValue(IAllowedValue.of("typeA", MarkupLine.fromMarkdown("Type A"), null))
+ .build());
+ constraintSupport.addConstraint(
+ IAllowedValuesConstraint.builder()
+ .source(source)
+ .target(IMetapathExpression.compile("@status"))
+ .allowedValue(IAllowedValue.of("active", MarkupLine.fromMarkdown("Active"), null))
+ .build());
+
+ AllowedValueCollectingNodeItemVisitor visitor = new AllowedValueCollectingNodeItemVisitor();
+ visitor.visit(module);
+
+ Collection locations = visitor.getAllowedValueLocations();
+ assertFalse(locations.isEmpty(), "Visitor should find allowed-values locations");
+
+ int totalConstraints = locations.stream()
+ .mapToInt(loc -> loc.getAllowedValues().size())
+ .sum();
+ assertEquals(2, totalConstraints, "Should find exactly two allowed-values constraints");
+ }
+}
diff --git a/metaschema-cli/src/main/java/dev/metaschema/cli/commands/ListAllowedValuesCommand.java b/metaschema-cli/src/main/java/dev/metaschema/cli/commands/ListAllowedValuesCommand.java
new file mode 100644
index 000000000..b9bb6c5b9
--- /dev/null
+++ b/metaschema-cli/src/main/java/dev/metaschema/cli/commands/ListAllowedValuesCommand.java
@@ -0,0 +1,330 @@
+/*
+ * SPDX-FileCopyrightText: none
+ * SPDX-License-Identifier: CC0-1.0
+ */
+
+package dev.metaschema.cli.commands;
+
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactoryBuilder;
+import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Option;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import dev.metaschema.cli.processor.CallingContext;
+import dev.metaschema.cli.processor.ExitCode;
+import dev.metaschema.cli.processor.command.AbstractTerminalCommand;
+import dev.metaschema.cli.processor.command.CommandExecutionException;
+import dev.metaschema.cli.processor.command.ExtraArgument;
+import dev.metaschema.cli.processor.command.ICommandExecutor;
+import dev.metaschema.core.metapath.DynamicContext;
+import dev.metaschema.core.metapath.StaticContext;
+import dev.metaschema.core.metapath.item.node.AllowedValueCollectingNodeItemVisitor;
+import dev.metaschema.core.metapath.item.node.AllowedValueCollectingNodeItemVisitor.AllowedValuesRecord;
+import dev.metaschema.core.metapath.item.node.IDefinitionNodeItem;
+import dev.metaschema.core.metapath.item.node.IModuleNodeItem;
+import dev.metaschema.core.metapath.item.node.INodeItemFactory;
+import dev.metaschema.core.model.IModule;
+import dev.metaschema.core.model.constraint.IAllowedValue;
+import dev.metaschema.core.model.constraint.IAllowedValuesConstraint;
+import dev.metaschema.core.model.constraint.IConstraintSet;
+import dev.metaschema.core.util.ObjectUtils;
+import dev.metaschema.databind.IBindingContext;
+import edu.umd.cs.findbugs.annotations.NonNull;
+
+/**
+ * A CLI command that lists allowed-values constraints for a Metaschema module,
+ * organized by the target node they apply to.
+ *
+ * The output is produced in YAML format, showing each target location and the
+ * allowed-values constraints that apply to it, including constraint
+ * identifiers, allowed values, and source information.
+ */
+@SuppressWarnings("PMD.CouplingBetweenObjects")
+public class ListAllowedValuesCommand
+ extends AbstractTerminalCommand {
+ private static final Logger LOGGER = LogManager.getLogger(ListAllowedValuesCommand.class);
+
+ @NonNull
+ private static final String COMMAND = "list-allowed-values";
+ @NonNull
+ private static final List EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
+ ExtraArgument.newInstance("metaschema-module-file-or-URL", true),
+ ExtraArgument.newInstance("destination-file", false)));
+ @NonNull
+ private static final Option CONSTRAINTS_OPTION = ObjectUtils.notNull(
+ Option.builder("c")
+ .hasArgs()
+ .argName("URL")
+ .desc("additional constraint definitions")
+ .get());
+
+ @Override
+ public String getName() {
+ return COMMAND;
+ }
+
+ @Override
+ public String getDescription() {
+ return "List allowed values constraints for the provided Metaschema module";
+ }
+
+ @SuppressWarnings("null")
+ @Override
+ public Collection extends Option> gatherOptions() {
+ return List.of(
+ CONSTRAINTS_OPTION,
+ MetaschemaCommands.OVERWRITE_OPTION);
+ }
+
+ @Override
+ public List getExtraArguments() {
+ return EXTRA_ARGUMENTS;
+ }
+
+ @Override
+ public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
+ return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
+ }
+
+ /**
+ * Execute the list allowed values command.
+ *
+ * @param callingContext
+ * information about the calling context
+ * @param cmdLine
+ * the parsed command line details
+ * @throws CommandExecutionException
+ * if an error occurred while executing the command
+ */
+ @SuppressWarnings({
+ "PMD.OnlyOneReturn",
+ "PMD.AvoidCatchingGenericException",
+ "PMD.CognitiveComplexity"
+ })
+ protected void executeCommand(
+ @NonNull CallingContext callingContext,
+ @NonNull CommandLine cmdLine) throws CommandExecutionException {
+
+ List extraArgs = cmdLine.getArgList();
+
+ Path destination = null;
+ if (extraArgs.size() > 1) {
+ destination = MetaschemaCommands.handleDestination(ObjectUtils.requireNonNull(extraArgs.get(1)), cmdLine);
+ }
+
+ URI currentWorkingDirectory = ObjectUtils.notNull(getCurrentWorkingDirectory().toUri());
+ Set constraintSets = MetaschemaCommands.loadConstraintSets(
+ cmdLine,
+ CONSTRAINTS_OPTION,
+ currentWorkingDirectory);
+
+ IBindingContext bindingContext = MetaschemaCommands.newBindingContextWithDynamicCompilation(constraintSets);
+
+ URI moduleUri;
+ try {
+ moduleUri = resolveAgainstCWD(ObjectUtils.requireNonNull(extraArgs.get(0)));
+ } catch (URISyntaxException ex) {
+ throw new CommandExecutionException(
+ ExitCode.INVALID_ARGUMENTS,
+ String.format("Cannot load module as '%s' is not a valid file or URL. %s",
+ extraArgs.get(0),
+ ex.getLocalizedMessage()),
+ ex);
+ }
+ IModule module = MetaschemaCommands.loadModule(moduleUri, bindingContext);
+
+ try {
+ if (destination == null) {
+ Writer stringWriter = new StringWriter();
+ try (PrintWriter writer = new PrintWriter(stringWriter)) {
+ generateAllowedValuesList(module, writer);
+ }
+
+ if (LOGGER.isInfoEnabled()) {
+ LOGGER.info(stringWriter.toString());
+ }
+ } else {
+ try (Writer writer = Files.newBufferedWriter(
+ destination,
+ StandardCharsets.UTF_8,
+ StandardOpenOption.CREATE,
+ StandardOpenOption.WRITE,
+ StandardOpenOption.TRUNCATE_EXISTING)) {
+ try (PrintWriter printWriter = new PrintWriter(writer)) {
+ generateAllowedValuesList(module, printWriter);
+ }
+ }
+ }
+ } catch (IOException ex) {
+ throw new CommandExecutionException(ExitCode.IO_ERROR, ex);
+ } catch (RuntimeException ex) {
+ throw new CommandExecutionException(ExitCode.RUNTIME_ERROR, ex);
+ }
+ }
+
+ private static void generateAllowedValuesList(
+ @NonNull IModule module,
+ @NonNull PrintWriter writer) throws IOException {
+ AllowedValueCollectingNodeItemVisitor walker = new AllowedValueCollectingNodeItemVisitor();
+
+ StaticContext staticContext = StaticContext.builder()
+ .defaultModelNamespace(module.getXmlNamespace())
+ .build();
+
+ IModuleNodeItem moduleNodeItem = INodeItemFactory.instance().newModuleNodeItem(module);
+
+ DynamicContext dynamicContext = new DynamicContext(staticContext);
+ dynamicContext.disablePredicateEvaluation();
+
+ walker.visit(moduleNodeItem, dynamicContext);
+
+ Map,
+ List> allowedValuesByTarget
+ = ObjectUtils.notNull(walker.getAllowedValueLocations().stream()
+ .flatMap(location -> location.getAllowedValues().stream())
+ .collect(Collectors.groupingBy(AllowedValuesRecord::getTarget,
+ () -> new TreeMap<>(Comparator.comparing(IDefinitionNodeItem::getMetapath)),
+ Collectors.mapping(Function.identity(), Collectors.toUnmodifiableList()))));
+
+ generateYaml(allowedValuesByTarget, writer);
+ }
+
+ private static void generateYaml(
+ @NonNull Map, List> allowedValuesByTarget,
+ @NonNull PrintWriter writer) throws IOException {
+
+ YAMLFactoryBuilder builder = YAMLFactory.builder();
+ YAMLFactory factory = ObjectUtils.notNull(builder
+ .enable(YAMLGenerator.Feature.MINIMIZE_QUOTES)
+ .enable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)
+ .enable(YAMLGenerator.Feature.ALWAYS_QUOTE_NUMBERS_AS_STRINGS)
+ .disable(YAMLGenerator.Feature.SPLIT_LINES)
+ .build());
+
+ try (YAMLGenerator generator = factory.createGenerator(writer)) {
+
+ generator.writeStartObject();
+
+ writeLocations(allowedValuesByTarget, generator);
+
+ generator.writeEndObject();
+ }
+ }
+
+ private static void writeLocations(
+ @NonNull Map, List> allowedValuesByTarget,
+ @NonNull YAMLGenerator generator) throws IOException {
+ generator.writeFieldName("locations");
+ generator.writeStartObject();
+
+ for (Map.Entry, List> entry : allowedValuesByTarget.entrySet()) {
+ assert entry != null;
+ writeLocation(entry, generator);
+ }
+ generator.writeEndObject();
+ }
+
+ private static void writeLocation(
+ @NonNull Map.Entry, List> entry,
+ @NonNull YAMLGenerator generator) throws IOException {
+
+ IDefinitionNodeItem, ?> target = ObjectUtils.notNull(entry.getKey());
+
+ generator.writeFieldName(metapath(target));
+
+ generator.writeStartObject();
+
+ writeLocationConstraints(entry, generator);
+
+ generator.writeEndObject();
+ }
+
+ private static void writeLocationConstraints(
+ @NonNull Map.Entry, List> entry,
+ @NonNull YAMLGenerator generator) throws IOException {
+ IDefinitionNodeItem, ?> target = ObjectUtils.notNull(entry.getKey());
+
+ List allowedValues = entry.getValue();
+ if (allowedValues != null) {
+ generator.writeFieldName("constraints");
+
+ generator.writeStartArray();
+
+ for (AllowedValuesRecord record : allowedValues) {
+ assert target.equals(record.getTarget());
+
+ writeAllowedValue(record, generator);
+ }
+
+ generator.writeEndArray();
+ }
+ }
+
+ private static void writeAllowedValue(@NonNull AllowedValuesRecord record, @NonNull YAMLGenerator generator)
+ throws IOException {
+
+ generator.writeStartObject();
+
+ generator.writeStringField("type", "allowed-values");
+
+ IAllowedValuesConstraint constraint = record.getAllowedValues();
+ if (constraint.getId() != null) {
+ generator.writeStringField("identifier", constraint.getId());
+ }
+ generator.writeStringField("location", metapath(record.getLocation()));
+ generator.writeStringField("target", constraint.getTarget().getPath());
+
+ List values = constraint.getAllowedValues().values().stream()
+ .map(IAllowedValue::getValue)
+ .collect(Collectors.toList());
+ generator.writeFieldName("values");
+ if (values == null) {
+ generator.writeNull();
+ } else {
+ generator.writeStartArray();
+ for (String value : values) {
+ generator.writeString(value);
+ }
+ generator.writeEndArray();
+ }
+
+ generator.writeBooleanField("allow-other", constraint.isAllowedOther());
+
+ URI source = constraint.getSource().getSource();
+ generator.writeStringField("source", source == null ? "builtin" : source.toString());
+
+ generator.writeEndObject();
+ }
+
+ private static String metapath(@NonNull IDefinitionNodeItem, ?> item) {
+ return metapath(item.getMetapath());
+ }
+
+ private static String metapath(@NonNull String path) {
+ // remove position 1 predicates
+ return path.replace("[1]", "");
+ }
+}
diff --git a/metaschema-cli/src/main/java/dev/metaschema/cli/commands/MetaschemaCommands.java b/metaschema-cli/src/main/java/dev/metaschema/cli/commands/MetaschemaCommands.java
index 37de5611d..7d83ce5b6 100644
--- a/metaschema-cli/src/main/java/dev/metaschema/cli/commands/MetaschemaCommands.java
+++ b/metaschema-cli/src/main/java/dev/metaschema/cli/commands/MetaschemaCommands.java
@@ -63,6 +63,7 @@ public final class MetaschemaCommands {
new ValidateModuleCommand(),
new GenerateSchemaCommand(),
new GenerateDiagramCommand(),
+ new ListAllowedValuesCommand(),
new ValidateContentUsingModuleCommand(),
new ConvertContentUsingModuleCommand(),
new MetapathCommand()));
diff --git a/metaschema-cli/src/main/java/module-info.java b/metaschema-cli/src/main/java/module-info.java
index 7e348b101..b81ac3f1a 100644
--- a/metaschema-cli/src/main/java/module-info.java
+++ b/metaschema-cli/src/main/java/module-info.java
@@ -26,6 +26,7 @@
requires nl.talsmasoftware.lazy4j;
requires org.apache.commons.cli;
+ requires com.fasterxml.jackson.dataformat.yaml;
requires org.apache.logging.log4j;
requires org.apache.logging.log4j.core;
@@ -38,6 +39,7 @@
dev.metaschema.cli.commands.ValidateModuleCommand,
dev.metaschema.cli.commands.GenerateSchemaCommand,
dev.metaschema.cli.commands.GenerateDiagramCommand,
+ dev.metaschema.cli.commands.ListAllowedValuesCommand,
dev.metaschema.cli.commands.ValidateContentUsingModuleCommand,
dev.metaschema.cli.commands.ConvertContentUsingModuleCommand,
dev.metaschema.cli.commands.metapath.MetapathCommand;
diff --git a/metaschema-cli/src/test/java/dev/metaschema/cli/CLITest.java b/metaschema-cli/src/test/java/dev/metaschema/cli/CLITest.java
index 37c7fa3a9..cca212d2a 100644
--- a/metaschema-cli/src/test/java/dev/metaschema/cli/CLITest.java
+++ b/metaschema-cli/src/test/java/dev/metaschema/cli/CLITest.java
@@ -142,6 +142,13 @@ private static Stream providesValues() {
NO_EXCEPTION_CLASS));
add(Arguments.of(new String[] { "metapath", "eval", "--help" }, ExitCode.OK,
NO_EXCEPTION_CLASS));
+ add(Arguments.of(new String[] { "list-allowed-values", "--help" }, ExitCode.OK,
+ NO_EXCEPTION_CLASS));
+ add(Arguments.of(
+ new String[] { "list-allowed-values",
+ "src/test/resources/content/schema-validation-module.xml"
+ },
+ ExitCode.OK, NO_EXCEPTION_CLASS));
add(Arguments.of(
new String[] { "validate",
"../databind/src/test/resources/metaschema/fields_with_flags/metaschema.xml"
@@ -313,6 +320,28 @@ void testValidateContent() {
}
}
+ @Test
+ void testListAllowedValuesOutput() throws Exception {
+ java.nio.file.Path outputFile = java.nio.file.Path.of("target/test-list-allowed-values.yaml");
+ String[] cliArgs = { "list-allowed-values",
+ "src/test/resources/content/schema-validation-module.xml",
+ outputFile.toString(),
+ "--overwrite",
+ "--show-stack-trace"
+ };
+ ExitStatus status = CLI.runCli(NULL_STREAM, cliArgs);
+ evaluateResult(status, ExitCode.OK, cliArgs);
+
+ String content = java.nio.file.Files.readString(outputFile);
+ assertThat(content)
+ .contains("locations:")
+ .contains("/root/optional:")
+ .contains("type: allowed-values")
+ .contains("target: optional")
+ .contains("yes")
+ .contains("allow-other: false");
+ }
+
@Test
void testValidateConstraints() {
try (LogCaptor captor = LogCaptor.forRoot()) {