diff --git a/.claude/rules/development-workflow.md b/.claude/rules/development-workflow.md
index 0f1a9188a..9eadca992 100644
--- a/.claude/rules/development-workflow.md
+++ b/.claude/rules/development-workflow.md
@@ -1,5 +1,7 @@
# Development Workflow
+> **Scope:** This document describes development workflows for AI agents (Claude + superpowers plugin) and automated code review. Human developers should adapt these TDD, parallel-execution, and review principles to their tooling and IDE.
+
## Skill Usage Protocol (MANDATORY)
Before responding to ANY task, complete this checklist:
@@ -53,9 +55,17 @@ User instructions describe WHAT to do, not HOW. "Add X" or "Fix Y" does NOT mean
---
-## Test-Driven Development (MANDATORY)
+## Test-Driven Development (MANDATORY - BLOCKING)
+
+**ALL code changes MUST follow TDD. No exceptions. This is BLOCKING.**
+
+### The Iron Law of TDD
+
+```text
+TESTS MUST BE WRITTEN AND FAIL BEFORE ANY IMPLEMENTATION CODE EXISTS
+```
-**ALL code changes MUST follow TDD. No exceptions.**
+This is non-negotiable. Implementation code written before tests is a violation.
### The TDD Cycle
@@ -65,15 +75,40 @@ User instructions describe WHAT to do, not HOW. "Add X" or "Fix Y" does NOT mean
4. **Refactor** - Clean up while keeping tests green
5. **Repeat** - For each new behavior
+### Enforcement Gate
+
+**Before writing ANY implementation code, you MUST have:**
+- [ ] A test file created for the new functionality
+- [ ] At least one failing test that exercises the code path
+- [ ] Verification that the test fails for the expected reason (not a syntax error)
+
+**If you haven't completed these steps, STOP. Go back and write tests first.**
+
+### What's Allowed with TDD
+
+Multiple test-writing agents **CAN run in parallel** with each other. Only implementation agents must wait for all test agents to complete. See "TDD with Parallel Agents" section below.
+
### Red Flags (You're Skipping TDD)
-If you catch yourself doing ANY of these, STOP:
+If you catch yourself doing ANY of these, STOP IMMEDIATELY:
- Writing implementation code before tests
- "I'll add tests after I get it working"
- "This is too simple to need tests"
- "Let me just make this small change first"
+- Dispatching implementation agents before test agents complete
+- Dispatching tests and implementation in the same parallel batch
- Modifying code without first verifying existing test coverage
+### Common Rationalizations That Violate TDD
+
+| Rationalization | Why It's Wrong |
+|-----------------|----------------|
+| "Tests slow me down" | Tests written after are harder to write and less effective |
+| "I'll write tests once I know the design" | TDD helps you discover the design |
+| "Implementation is straightforward" | Straightforward code still needs tests first |
+| "Tests and impl in parallel for efficiency" | Tests MUST complete before implementation starts |
+| "The code already works" | Prove it works by writing the test first |
+
### When Tests Already Exist
When modifying existing code:
@@ -86,6 +121,7 @@ When modifying existing code:
- Use `superpowers:test-driven-development` skill for the full workflow
- The skill will guide you through RED-GREEN-REFACTOR
- Never skip the "watch it fail" step - it proves your test works
+- **Agents dispatched for implementation MUST have tests written first**
---
@@ -131,6 +167,7 @@ PRDs/[date]-[name]/
- **Do NOT proceed to development until PRD is approved**
### Phase 3: Development
+
Execute the plan using these skills:
- `superpowers:executing-plans` - Execute tasks in batches with review checkpoints
- `superpowers:test-driven-development` - Write tests first, then implementation
@@ -145,6 +182,64 @@ Execute the plan using these skills:
- Update PR status in implementation-plan.md
- Add notes about deviations from plan
+#### Parallel Agent Usage
+
+**Use multiple agents in parallel whenever tasks are independent.**
+
+Dispatch multiple agents in a single message when:
+- Tasks operate on different files with no dependencies
+- Tasks can be clearly scoped with complete context
+- Each task can succeed or fail independently
+- Waiting for sequential completion would waste time
+
+| Scenario | Parallel Approach |
+|----------|-------------------|
+| Implementing 3 new classes | One agent per class |
+| Updating interface + implementation | Agent for each file |
+| Writing tests for multiple components | Agent per test class |
+| Code review + linting | Separate review agents |
+| Reading multiple files for context | Agent per exploration area |
+
+**How to dispatch:** Use a SINGLE message with multiple Task tool calls. Do not dispatch agents sequentially when they can run in parallel.
+
+**Dispatch granularity:** When deciding agent scope, prefer finer-grained agents (one per file/class) over coarse-grained agents (one for entire feature). Smaller scopes:
+- Fail independently without blocking other work
+- Produce clearer error messages
+- Enable better parallelism
+
+**Detecting dependencies:** Tasks have dependencies when:
+- One task's output is another's input (e.g., interface before implementation)
+- Shared state must be modified in sequence
+- Compilation order matters (e.g., base class before subclass)
+
+Tasks are independent when they touch different files with no shared interfaces.
+
+**Red flags (you're not using parallel agents):**
+- Dispatching one agent, waiting for result, then dispatching another
+- "I'll run this agent first to see what happens"
+- "These tasks might have dependencies" (when they clearly don't)
+- Running sequential agents for independent file changes
+
+#### TDD with Parallel Agents
+
+Tests MUST be written before implementation, but test-writing agents CAN run in parallel:
+- Dispatch multiple test-writing agents in parallel (one per test class)
+- Wait for all test agents to complete
+- Verify tests fail for the correct reasons
+- THEN dispatch implementation agents in parallel
+
+**Correct order:**
+1. Parallel agents write tests → all complete
+2. Run the full test suite and verify new tests fail correctly:
+ - Failures should be assertion failures (e.g., "expected X but was Y"), not compilation errors
+ - Review failure messages to confirm they match test intent, not "class not found" or similar
+3. Parallel agents write implementation → all complete
+4. Verify tests pass
+
+**Wrong order:**
+- Tests and implementation agents in the same parallel batch
+- Implementation agents before test agents complete
+
### Phase 4: Code Review Cycle
Perform iterative code review until all issues are resolved:
@@ -161,7 +256,7 @@ Perform iterative code review until all issues are resolved:
5. **Repeat** until all issues are resolved and changes are accepted
-```
+```text
Code Complete
↓
Code Review Agents (parallel) → Consolidated Issues Report
@@ -173,6 +268,24 @@ Work Issues → Re-review
No Issues → Proceed to Verification
```
+#### Handling Optional Nitpicks
+
+When reviewers (human or automated) mark feedback as "nitpick" or "optional":
+
+**Address nitpicks when they:**
+- Improve code quality in files already being changed
+- Reduce duplication or improve readability
+- Add low-risk enhancements (e.g., additional test cases)
+- Can be implemented without significant new changes
+
+**Defer nitpicks when they:**
+- Require changes outside the PR's scope
+- Introduce significant new code or risk
+- Would substantially increase PR size
+- Conflict with the PR's focused purpose
+
+**When deferring:** Note the suggestion in a comment or issue for future consideration.
+
### Phase 5: Verification & PR
- `superpowers:verification-before-completion` - Confirm all tests pass
- Verify all code review issues are resolved
@@ -181,6 +294,12 @@ No Issues → Proceed to Verification
#### Build Verification Summary Format
+**When:** After running a full build with quality checks (e.g., `mvn clean install -PCI -Prelease`).
+
+**Who:** Developer or AI agent. Manual compilation from tool output is acceptable; automation is preferred.
+
+**Where:** Include in the conversation output or PR description before proceeding to merge.
+
After running builds with quality checks, provide a scannable summary:
```text
@@ -210,20 +329,21 @@ Build failed:
```
### Workflow Summary
-```
+```text
GitHub issue/prompt
↓
brainstorming → prd-construction → PRDs/[date]-[name]/
↓
User Approves PRD
↓
-executing-plans + TDD + subagents
+Phase 3: Development (TDD with Parallel Agents)
+ ├─ Parallel test agents → verify failures (MUST complete first)
+ ├─ Parallel implementation agents → verify passes (only after tests pass)
+ └─ Update implementation-plan.md with progress [x]
↓
-Update implementation-plan.md with progress [x]
+Phase 4: Code review cycle (parallel review agents)
↓
-verification-before-completion
- ↓
-PR to develop
+Phase 5: verification-before-completion → PR to develop
```
---
@@ -268,7 +388,7 @@ Once root cause is confirmed:
1. **Check for existing test** - Does a test cover this scenario?
2. **If no test exists** - Use `superpowers:test-driven-development` to:
- Write a failing test that reproduces the bug
- - Confirm the test fails for the right reason
+ - Confirm the test fails for the intended reason
- Do NOT proceed to fix until test fails correctly
### Step 3: Implement Fix
diff --git a/databind-metaschema/pom-bootstrap.xml b/databind-metaschema/pom-bootstrap.xml
new file mode 100644
index 000000000..dc4e2c0d6
--- /dev/null
+++ b/databind-metaschema/pom-bootstrap.xml
@@ -0,0 +1,93 @@
+
+
+
+ * This implementation wraps a + * {@link MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding.ChoiceGroupBinding} + * instance from the binding configuration and provides the configuration + * interface methods. + * + *
+ * The class stores: + *
+ * Choice group bindings enable fine-grained control over code generation for + * choice groups, particularly for specifying custom collection types with + * type-safe item bounds. + */ +public interface IChoiceGroupBindingConfiguration { + + /** + * Get the name of the choice group to match. + * + *
+ * This name corresponds to the {@code group-as} name specified in the + * Metaschema module for the choice group. + * + * @return the choice group name + */ + @NonNull + String getGroupAsName(); + + /** + * Get the fully qualified Java type name to use for collection items. + * + *
+ * When specified, the generated field and getter will use this type instead of + * {@link Object} for the collection item type. This allows for type-safe + * collections when all choice alternatives share a common supertype. + * + * @return the fully qualified Java type name, or {@code null} if not specified + */ + @Nullable + String getItemTypeName(); + + /** + * Determine whether to use a wildcard bounded type for the collection. + * + *
+ * When {@code true}, generates {@code List extends Type>} instead of
+ * {@code List
+ * Defaults to {@code true} if an item type is specified.
+ *
+ * @return {@code true} to use wildcard bounds, {@code false} otherwise
+ */
+ boolean isUseWildcard();
+}
diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/IDefinitionBindingConfiguration.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/IDefinitionBindingConfiguration.java
index 511bf12f0..b8b59229f 100644
--- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/IDefinitionBindingConfiguration.java
+++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/IDefinitionBindingConfiguration.java
@@ -6,6 +6,7 @@
package gov.nist.secauto.metaschema.databind.codegen.config;
import java.util.List;
+import java.util.Map;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
@@ -36,4 +37,16 @@ public interface IDefinitionBindingConfiguration {
*/
@NonNull
List
+ * Choice group bindings provide configuration for choice groups within this
+ * assembly definition, keyed by the choice group's {@code group-as} name.
+ *
+ * @return a map of group-as name to choice group binding configuration
+ */
+ @NonNull
+ Map
+ * Returns a collection type ({@link List} or {@link Map}) when maxOccurs allows
+ * multiple items, or the item type directly for single-valued instances. When
+ * binding configuration specifies a custom item type with wildcard usage
+ * enabled, generates bounded wildcard types (e.g.,
+ * {@code List extends Type>}).
+ *
+ * @return the Java field type for code generation
+ */
+ @NonNull
+ @Override
+ public TypeName getJavaFieldType() {
+ TypeName item = getJavaItemType();
+
+ @NonNull
+ TypeName retval;
+ IChoiceGroupInstance instance = getInstance();
+ int maxOccurrence = instance.getMaxOccurs();
+ if (maxOccurrence == -1 || maxOccurrence > 1) {
+ // Check if we should use wildcard types
+ TypeName collectionItemType = item;
+ IAssemblyDefinition parent = instance.getContainingDefinition();
+ ITypeResolver resolver = getParentTypeInfo().getTypeResolver();
+ IBindingConfiguration bindingConfig = resolver.getBindingConfiguration();
+ IDefinitionBindingConfiguration defConfig = bindingConfig.getBindingConfigurationForDefinition(parent);
+ if (defConfig != null) {
+ IChoiceGroupBindingConfiguration choiceConfig = defConfig.getChoiceGroupBindings()
+ .get(instance.getGroupAsName());
+ if (choiceConfig != null && choiceConfig.getItemTypeName() != null && choiceConfig.isUseWildcard()) {
+ // Use wildcard type for flexibility
+ collectionItemType = WildcardTypeName.subtypeOf(item);
+ }
+ }
+
+ if (JsonGroupAsBehavior.KEYED.equals(instance.getJsonGroupAsBehavior())) {
+ retval = ObjectUtils.notNull(
+ ParameterizedTypeName.get(ClassName.get(Map.class), ClassName.get(String.class), collectionItemType));
+ } else {
+ retval = ObjectUtils.notNull(ParameterizedTypeName.get(ClassName.get(List.class), collectionItemType));
+ }
+ } else {
+ retval = item;
+ }
+ return retval;
+ }
+
@Override
protected AnnotationSpec.Builder newBindingAnnotation() {
return ObjectUtils.notNull(AnnotationSpec.builder(BoundChoiceGroup.class));
diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/DefaultTypeResolver.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/DefaultTypeResolver.java
index 37e665e41..d6f18d8e7 100644
--- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/DefaultTypeResolver.java
+++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/DefaultTypeResolver.java
@@ -19,6 +19,8 @@
import gov.nist.secauto.metaschema.core.util.ObjectUtils;
import gov.nist.secauto.metaschema.databind.codegen.ClassUtils;
import gov.nist.secauto.metaschema.databind.codegen.config.IBindingConfiguration;
+import gov.nist.secauto.metaschema.databind.codegen.config.IChoiceGroupBindingConfiguration;
+import gov.nist.secauto.metaschema.databind.codegen.config.IDefinitionBindingConfiguration;
import gov.nist.secauto.metaschema.databind.codegen.typeinfo.def.IAssemblyDefinitionTypeInfo;
import gov.nist.secauto.metaschema.databind.codegen.typeinfo.def.IDefinitionTypeInfo;
import gov.nist.secauto.metaschema.databind.codegen.typeinfo.def.IFieldDefinitionTypeInfo;
@@ -65,7 +67,8 @@ public DefaultTypeResolver(@NonNull IBindingConfiguration bindingConfiguration)
this.bindingConfiguration = bindingConfiguration;
}
- protected IBindingConfiguration getBindingConfiguration() {
+ @Override
+ public IBindingConfiguration getBindingConfiguration() {
return bindingConfiguration;
}
@@ -179,7 +182,14 @@ public ClassName getClassName(@NonNull INamedModelInstanceTypeInfo typeInfo) {
@Override
public ClassName getClassName(IChoiceGroupInstance instance) {
- // TODO: Support some form of binding override for a common interface type
+ IAssemblyDefinition parent = instance.getContainingDefinition();
+ IDefinitionBindingConfiguration config = getBindingConfiguration().getBindingConfigurationForDefinition(parent);
+ if (config != null) {
+ IChoiceGroupBindingConfiguration choiceConfig = config.getChoiceGroupBindings().get(instance.getGroupAsName());
+ if (choiceConfig != null && choiceConfig.getItemTypeName() != null) {
+ return ClassName.bestGuess(choiceConfig.getItemTypeName());
+ }
+ }
return ObjectUtils.notNull(ClassName.get(Object.class));
}
diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/ITypeResolver.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/ITypeResolver.java
index 02f55ac99..b9936b8a5 100644
--- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/ITypeResolver.java
+++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/ITypeResolver.java
@@ -41,6 +41,14 @@ static ITypeResolver newTypeResolver(@NonNull IBindingConfiguration bindingConfi
return new DefaultTypeResolver(bindingConfiguration);
}
+ /**
+ * Get the binding configuration used by this type resolver.
+ *
+ * @return the binding configuration
+ */
+ @NonNull
+ IBindingConfiguration getBindingConfiguration();
+
/**
* Get type information for the provided {@code instance}.
*
diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/config/binding/MetaschemaBindings.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/config/binding/MetaschemaBindings.java
index 83908c464..96c82fa7a 100644
--- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/config/binding/MetaschemaBindings.java
+++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/config/binding/MetaschemaBindings.java
@@ -9,6 +9,7 @@
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
+import gov.nist.secauto.metaschema.core.datatype.adapter.BooleanAdapter;
import gov.nist.secauto.metaschema.core.datatype.adapter.TokenAdapter;
import gov.nist.secauto.metaschema.core.datatype.adapter.UriAdapter;
import gov.nist.secauto.metaschema.core.datatype.adapter.UriReferenceAdapter;
@@ -18,9 +19,11 @@
import gov.nist.secauto.metaschema.core.util.ObjectUtils;
import gov.nist.secauto.metaschema.databind.model.annotations.BoundAssembly;
import gov.nist.secauto.metaschema.databind.model.annotations.BoundField;
+import gov.nist.secauto.metaschema.databind.model.annotations.BoundFieldValue;
import gov.nist.secauto.metaschema.databind.model.annotations.BoundFlag;
import gov.nist.secauto.metaschema.databind.model.annotations.GroupAs;
import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly;
+import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaField;
import java.net.URI;
import java.util.LinkedList;
import java.util.List;
@@ -674,6 +677,17 @@ public static class DefineAssemblyBinding implements IBoundObject {
groupAs = @GroupAs(name = "property-bindings", inJson = JsonGroupAsBehavior.LIST))
private List
+ * Provides binding configuration for a choice group within the parent assembly.
+ *
+ * @return the choice-group-binding value
+ */
+ @NonNull
+ public List
+ * Provides binding configuration for a choice group within the parent assembly.
+ *
+ * @param value
+ * the choice-group-binding value to set
+ */
+ public void setChoiceGroupBindings(@NonNull List
+ * The name of the choice group (matches the group-as name in the metaschema).
+ *
+ * @return the name value
+ */
+ @NonNull
+ public String getName() {
+ return _name;
+ }
+
+ /**
+ * Set the name.
+ *
+ *
+ * The name of the choice group (matches the group-as name in the metaschema).
+ *
+ * @param value
+ * the name value to set
+ */
+ public void setName(@NonNull String value) {
+ _name = value;
+ }
+
+ /**
+ * Get the item Type.
+ *
+ *
+ * A fully qualified Java type for collection items. When specified, the
+ * generated field and getter will use this type instead of Object.
+ *
+ * @return the item-type value, or {@code null} if not set
+ */
+ @Nullable
+ public ItemType getItemType() {
+ return _itemType;
+ }
+
+ /**
+ * Set the item Type.
+ *
+ *
+ * A fully qualified Java type for collection items. When specified, the
+ * generated field and getter will use this type instead of Object.
+ *
+ * @param value
+ * the item-type value to set
+ */
+ public void setItemType(@Nullable ItemType value) {
+ _itemType = value;
+ }
+
+ @Override
+ public String toString() {
+ return new ReflectionToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE).toString();
+ }
+
+ /**
+ * A fully qualified Java type for collection items. When specified, the
+ * generated field and getter will use this type instead of Object.
+ */
+ @MetaschemaField(
+ formalName = "Item Type",
+ description = "A fully qualified Java type for collection items. When specified, the generated field and getter will use this type instead of Object.",
+ name = "item-type",
+ moduleClass = MetaschemaBindingsModule.class)
+ public static class ItemType implements IBoundObject {
+ private final IMetaschemaData __metaschemaData;
+
+ /**
+ * Whether to use a wildcard bounded type (List<? extends Type>). Defaults
+ * to true.
+ */
+ @BoundFlag(
+ formalName = "Use Wildcard",
+ description = "Whether to use a wildcard bounded type (List extends Type>). Defaults to true.",
+ name = "use-wildcard",
+ defaultValue = "true",
+ typeAdapter = BooleanAdapter.class)
+ private Boolean _useWildcard;
+
+ @BoundFieldValue(
+ valueKeyName = "STRVALUE",
+ typeAdapter = TokenAdapter.class)
+ private String _value;
+
+ /**
+ * Constructs a new
+ * {@code gov.nist.secauto.metaschema.databind.config.binding.MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding.ChoiceGroupBinding.ItemType}
+ * instance with no metadata.
+ */
+ public ItemType() {
+ this(null);
+ }
+
+ /**
+ * Constructs a new
+ * {@code gov.nist.secauto.metaschema.databind.config.binding.MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding.ChoiceGroupBinding.ItemType}
+ * instance with the specified metadata.
+ *
+ * @param data
+ * the metaschema data, or {@code null} if none
+ */
+ public ItemType(IMetaschemaData data) {
+ this.__metaschemaData = data;
+ }
+
+ @Override
+ public IMetaschemaData getMetaschemaData() {
+ return __metaschemaData;
+ }
+
+ /**
+ * Get the use Wildcard.
+ *
+ *
+ * Whether to use a wildcard bounded type (List<? extends Type>). Defaults
+ * to true.
+ *
+ * @return the use-wildcard value, or {@code null} if not set
+ */
+ @Nullable
+ public Boolean getUseWildcard() {
+ return _useWildcard;
+ }
+
+ /**
+ * Set the use Wildcard.
+ *
+ *
+ * Whether to use a wildcard bounded type (List<? extends Type>). Defaults
+ * to true.
+ *
+ * @param value
+ * the use-wildcard value to set
+ */
+ public void setUseWildcard(@Nullable Boolean value) {
+ _useWildcard = value;
+ }
+
+ @Nullable
+ public String getValue() {
+ return _value;
+ }
+
+ public void setValue(@Nullable String value) {
+ _value = value;
+ }
+
+ @Override
+ public String toString() {
+ return new ReflectionToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE).toString();
+ }
+ }
+ }
}
/**
diff --git a/databind/src/main/metaschema/metaschema-bindings.yaml b/databind/src/main/metaschema/metaschema-bindings.yaml
index 711375be7..ef2db30b2 100644
--- a/databind/src/main/metaschema/metaschema-bindings.yaml
+++ b/databind/src/main/metaschema/metaschema-bindings.yaml
@@ -140,6 +140,35 @@ METASCHEMA:
formal-name: Collection Class
description: A fully qualified Java collection class name to use instead of the default.
as-type: token
+ - object-type: assembly
+ name: choice-group-binding
+ formal-name: Choice Group Binding
+ description: Provides binding configuration for a choice group within the parent assembly.
+ max-occurs: unbounded
+ group-as:
+ name: choice-group-bindings
+ in-json: ARRAY
+ flags:
+ - object-type: flag
+ name: name
+ formal-name: Name
+ description: The name of the choice group (matches the group-as name in the metaschema).
+ as-type: token
+ required: "yes"
+ model:
+ instances:
+ - object-type: field
+ name: item-type
+ formal-name: Item Type
+ description: A fully qualified Java type for collection items. When specified, the generated field and getter will use this type instead of Object.
+ as-type: token
+ flags:
+ - object-type: flag
+ name: use-wildcard
+ formal-name: Use Wildcard
+ description: Whether to use a wildcard bounded type (List extends Type>). Defaults to true.
+ as-type: boolean
+ default: "true"
- object-type: assembly
name: define-field-binding
formal-name: Define Field Binding
diff --git a/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultBindingConfigurationTest.java b/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultBindingConfigurationTest.java
index 60e37f50d..e4c52fa62 100644
--- a/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultBindingConfigurationTest.java
+++ b/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultBindingConfigurationTest.java
@@ -294,4 +294,109 @@ void testDuplicatePropertyBindingLastWins() throws IOException {
"Duplicate property bindings should use last-wins semantics");
}
+ @Test
+ void testChoiceGroupBindingParsing() throws IOException {
+ // Test that choice-group-binding elements are parsed from XML config
+ File bindingConfigFile = new File("src/test/resources/metaschema/binding-config-with-choice-groups.xml");
+ URI assemblyMetaschemaLocation = new File("src/test/resources/metaschema/assembly/metaschema.xml")
+ .getAbsoluteFile().toURI();
+
+ DefaultBindingConfiguration config = new DefaultBindingConfiguration();
+ config.load(bindingConfigFile);
+
+ IAssemblyDefinition testAssemblyDefinition = context.mock(IAssemblyDefinition.class, "testAssembly");
+ IModule testModule = context.mock(IModule.class, "testModule");
+
+ context.checking(new Expectations() {
+ {
+ allowing(testModule).getLocation();
+ will(returnValue(assemblyMetaschemaLocation));
+ allowing(testAssemblyDefinition).getContainingModule();
+ will(returnValue(testModule));
+ allowing(testAssemblyDefinition).getModelType();
+ will(returnValue(ModelType.ASSEMBLY));
+ allowing(testAssemblyDefinition).getName();
+ will(returnValue("test-assembly"));
+ }
+ });
+
+ // Get the definition binding configuration
+ IDefinitionBindingConfiguration defConfig = config.getBindingConfigurationForDefinition(
+ ObjectUtils.notNull(testAssemblyDefinition));
+
+ assertNotNull(defConfig, "Definition binding configuration should exist");
+
+ // Verify choice group bindings are accessible
+ assertNotNull(defConfig.getChoiceGroupBindings(),
+ "Choice group bindings map should not be null");
+ assertEquals(3, defConfig.getChoiceGroupBindings().size(),
+ "Should have 3 choice group bindings");
+
+ // Verify "mixed-content" choice group binding
+ IChoiceGroupBindingConfiguration mixedContentConfig
+ = defConfig.getChoiceGroupBindings().get("mixed-content");
+ assertNotNull(mixedContentConfig, "mixed-content choice group binding should exist");
+ assertEquals("mixed-content", mixedContentConfig.getGroupAsName());
+ assertEquals("gov.nist.secauto.metaschema.core.model.IModelElement",
+ mixedContentConfig.getItemTypeName());
+ assertEquals(true, mixedContentConfig.isUseWildcard(),
+ "mixed-content should use wildcard");
+
+ // Verify "typed-items" choice group binding
+ IChoiceGroupBindingConfiguration typedItemsConfig
+ = defConfig.getChoiceGroupBindings().get("typed-items");
+ assertNotNull(typedItemsConfig, "typed-items choice group binding should exist");
+ assertEquals("typed-items", typedItemsConfig.getGroupAsName());
+ assertEquals("java.lang.String", typedItemsConfig.getItemTypeName());
+ assertEquals(false, typedItemsConfig.isUseWildcard(),
+ "typed-items should not use wildcard");
+
+ // Verify "untyped-items" choice group binding
+ IChoiceGroupBindingConfiguration untypedItemsConfig
+ = defConfig.getChoiceGroupBindings().get("untyped-items");
+ assertNotNull(untypedItemsConfig, "untyped-items choice group binding should exist");
+ assertEquals("untyped-items", untypedItemsConfig.getGroupAsName());
+ assertNull(untypedItemsConfig.getItemTypeName(),
+ "untyped-items should not have an item type");
+ }
+
+ @Test
+ void testEmptyChoiceGroupBindings() throws IOException {
+ // Test that empty choice group bindings map is returned when none configured
+ File bindingConfigFile = new File("src/test/resources/metaschema/binding-config-with-choice-groups.xml");
+ URI assemblyMetaschemaLocation = new File("src/test/resources/metaschema/assembly/metaschema.xml")
+ .getAbsoluteFile().toURI();
+
+ DefaultBindingConfiguration config = new DefaultBindingConfiguration();
+ config.load(bindingConfigFile);
+
+ IAssemblyDefinition simpleAssemblyDefinition = context.mock(IAssemblyDefinition.class, "simpleAssembly");
+ IModule simpleModule = context.mock(IModule.class, "simpleModule");
+
+ context.checking(new Expectations() {
+ {
+ allowing(simpleModule).getLocation();
+ will(returnValue(assemblyMetaschemaLocation));
+ allowing(simpleAssemblyDefinition).getContainingModule();
+ will(returnValue(simpleModule));
+ allowing(simpleAssemblyDefinition).getModelType();
+ will(returnValue(ModelType.ASSEMBLY));
+ allowing(simpleAssemblyDefinition).getName();
+ will(returnValue("simple-assembly"));
+ }
+ });
+
+ // Get the definition binding configuration
+ IDefinitionBindingConfiguration defConfig = config.getBindingConfigurationForDefinition(
+ ObjectUtils.notNull(simpleAssemblyDefinition));
+
+ assertNotNull(defConfig, "Definition binding configuration should exist");
+
+ // Verify choice group bindings map is empty but not null
+ assertNotNull(defConfig.getChoiceGroupBindings(),
+ "Choice group bindings map should not be null");
+ assertEquals(0, defConfig.getChoiceGroupBindings().size(),
+ "Choice group bindings map should be empty");
+ }
+
}
diff --git a/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultChoiceGroupBindingConfigurationTest.java b/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultChoiceGroupBindingConfigurationTest.java
new file mode 100644
index 000000000..89f4ca9ba
--- /dev/null
+++ b/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultChoiceGroupBindingConfigurationTest.java
@@ -0,0 +1,205 @@
+/*
+ * SPDX-FileCopyrightText: none
+ * SPDX-License-Identifier: CC0-1.0
+ */
+
+package gov.nist.secauto.metaschema.databind.codegen.config;
+
+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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition;
+import gov.nist.secauto.metaschema.core.model.IModule;
+import gov.nist.secauto.metaschema.core.model.ModelType;
+import gov.nist.secauto.metaschema.core.util.ObjectUtils;
+
+import org.jmock.Expectations;
+import org.jmock.junit5.JUnit5Mockery;
+import org.jmock.lib.concurrent.Synchroniser;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Tests for {@link DefaultChoiceGroupBindingConfiguration}.
+ *
+ * These tests use XML binding configuration files to verify parsing and
+ * behavior of choice group binding configurations.
+ */
+class DefaultChoiceGroupBindingConfigurationTest {
+
+ private static final File BINDING_CONFIG_FILE
+ = new File("src/test/resources/metaschema/binding-config-with-choice-groups.xml");
+ private static final URI ASSEMBLY_METASCHEMA_LOCATION
+ = new File("src/test/resources/metaschema/assembly/metaschema.xml").getAbsoluteFile().toURI();
+
+ @RegisterExtension
+ JUnit5Mockery context = new JUnit5Mockery() {
+ {
+ setThreadingPolicy(new Synchroniser());
+ }
+ };
+
+ private final AtomicInteger mockCounter = new AtomicInteger(0);
+ private DefaultBindingConfiguration bindingConfig;
+
+ @BeforeEach
+ void setUp() throws IOException {
+ bindingConfig = new DefaultBindingConfiguration();
+ bindingConfig.load(BINDING_CONFIG_FILE);
+ }
+
+ /**
+ * Creates a mock assembly definition with standard expectations and retrieves
+ * the binding configuration for it.
+ *
+ * @return the definition binding configuration for the mock assembly
+ */
+ private IDefinitionBindingConfiguration getDefinitionConfig() {
+ int id = mockCounter.incrementAndGet();
+ IAssemblyDefinition testAssemblyDefinition = context.mock(IAssemblyDefinition.class, "testAssembly" + id);
+ IModule testModule = context.mock(IModule.class, "testModule" + id);
+
+ context.checking(new Expectations() {
+ {
+ allowing(testModule).getLocation();
+ will(returnValue(ASSEMBLY_METASCHEMA_LOCATION));
+ allowing(testAssemblyDefinition).getContainingModule();
+ will(returnValue(testModule));
+ allowing(testAssemblyDefinition).getModelType();
+ will(returnValue(ModelType.ASSEMBLY));
+ allowing(testAssemblyDefinition).getName();
+ will(returnValue("test-assembly"));
+ }
+ });
+
+ return bindingConfig.getBindingConfigurationForDefinition(ObjectUtils.notNull(testAssemblyDefinition));
+ }
+
+ @Test
+ void testGetGroupAsName() {
+ IDefinitionBindingConfiguration defConfig = getDefinitionConfig();
+
+ IChoiceGroupBindingConfiguration choiceGroupConfig
+ = defConfig.getChoiceGroupBindings().get("mixed-content");
+ assertNotNull(choiceGroupConfig);
+ assertEquals("mixed-content", choiceGroupConfig.getGroupAsName());
+ }
+
+ @Test
+ void testGetItemTypeNameWhenSpecified() {
+ IDefinitionBindingConfiguration defConfig = getDefinitionConfig();
+
+ IChoiceGroupBindingConfiguration typedItemsConfig
+ = defConfig.getChoiceGroupBindings().get("typed-items");
+ assertNotNull(typedItemsConfig);
+ assertEquals("java.lang.String", typedItemsConfig.getItemTypeName());
+ }
+
+ @Test
+ void testGetItemTypeNameWhenNotSpecified() {
+ IDefinitionBindingConfiguration defConfig = getDefinitionConfig();
+
+ IChoiceGroupBindingConfiguration untypedItemsConfig
+ = defConfig.getChoiceGroupBindings().get("untyped-items");
+ assertNotNull(untypedItemsConfig);
+ assertNull(untypedItemsConfig.getItemTypeName(),
+ "Item type should be null when not specified");
+ }
+
+ @Test
+ void testIsUseWildcardDefaultsToTrue() {
+ IDefinitionBindingConfiguration defConfig = getDefinitionConfig();
+
+ // "mixed-content" has use-wildcard="true" explicitly set
+ IChoiceGroupBindingConfiguration mixedContentConfig
+ = defConfig.getChoiceGroupBindings().get("mixed-content");
+ assertNotNull(mixedContentConfig);
+ assertTrue(mixedContentConfig.isUseWildcard(),
+ "useWildcard should be true when explicitly set");
+ }
+
+ @Test
+ void testIsUseWildcardWhenExplicitlyFalse() {
+ IDefinitionBindingConfiguration defConfig = getDefinitionConfig();
+
+ // "typed-items" has use-wildcard="false"
+ IChoiceGroupBindingConfiguration typedItemsConfig
+ = defConfig.getChoiceGroupBindings().get("typed-items");
+ assertNotNull(typedItemsConfig);
+ assertFalse(typedItemsConfig.isUseWildcard(),
+ "useWildcard should be false when explicitly set to false");
+ }
+
+ @Test
+ void testIsUseWildcardWhenNoItemType() {
+ IDefinitionBindingConfiguration defConfig = getDefinitionConfig();
+
+ // "untyped-items" has no item-type element
+ IChoiceGroupBindingConfiguration untypedItemsConfig
+ = defConfig.getChoiceGroupBindings().get("untyped-items");
+ assertNotNull(untypedItemsConfig);
+ assertTrue(untypedItemsConfig.isUseWildcard(),
+ "useWildcard should default to true when no item type specified");
+ }
+
+ // --- Negative Test Cases ---
+
+ @Test
+ void testNonExistentGroupNameReturnsNull() {
+ IDefinitionBindingConfiguration defConfig = getDefinitionConfig();
+
+ IChoiceGroupBindingConfiguration result
+ = defConfig.getChoiceGroupBindings().get("non-existent-group");
+ assertNull(result, "Non-existent group name should return null");
+ }
+
+ @Test
+ void testMissingResourceFileThrowsException() {
+ File nonExistentFile = new File("src/test/resources/metaschema/does-not-exist.xml");
+ DefaultBindingConfiguration config = new DefaultBindingConfiguration();
+
+ assertThrows(IOException.class, () -> config.load(nonExistentFile),
+ "Loading non-existent file should throw IOException");
+ }
+
+ @Test
+ void testEmptyChoiceGroupBindingsMap() {
+ // Test definition that has no choice group bindings configured
+ int id = mockCounter.incrementAndGet();
+ IAssemblyDefinition unconfiguredAssembly = context.mock(IAssemblyDefinition.class, "unconfiguredAssembly" + id);
+ IModule unconfiguredModule = context.mock(IModule.class, "unconfiguredModule" + id);
+
+ // Use a different metaschema location that won't match any binding config
+ URI differentLocation = new File("src/test/resources/metaschema/other/metaschema.xml")
+ .getAbsoluteFile().toURI();
+
+ context.checking(new Expectations() {
+ {
+ allowing(unconfiguredModule).getLocation();
+ will(returnValue(differentLocation));
+ allowing(unconfiguredAssembly).getContainingModule();
+ will(returnValue(unconfiguredModule));
+ allowing(unconfiguredAssembly).getModelType();
+ will(returnValue(ModelType.ASSEMBLY));
+ allowing(unconfiguredAssembly).getName();
+ will(returnValue("unconfigured-assembly"));
+ }
+ });
+
+ IDefinitionBindingConfiguration defConfig = bindingConfig.getBindingConfigurationForDefinition(
+ ObjectUtils.notNull(unconfiguredAssembly));
+
+ // When no binding config matches, should return null
+ assertNull(defConfig, "Unconfigured assembly should have no binding configuration");
+ }
+}
diff --git a/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/ChoiceGroupTypeInfoImplTest.java b/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/ChoiceGroupTypeInfoImplTest.java
new file mode 100644
index 000000000..958cad26c
--- /dev/null
+++ b/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/ChoiceGroupTypeInfoImplTest.java
@@ -0,0 +1,392 @@
+/*
+ * SPDX-FileCopyrightText: none
+ * SPDX-License-Identifier: CC0-1.0
+ */
+
+package gov.nist.secauto.metaschema.databind.codegen.typeinfo;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.ParameterizedTypeName;
+import com.squareup.javapoet.TypeName;
+import com.squareup.javapoet.WildcardTypeName;
+
+import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition;
+import gov.nist.secauto.metaschema.core.model.IChoiceGroupInstance;
+import gov.nist.secauto.metaschema.core.model.JsonGroupAsBehavior;
+import gov.nist.secauto.metaschema.databind.codegen.config.IBindingConfiguration;
+import gov.nist.secauto.metaschema.databind.codegen.config.IChoiceGroupBindingConfiguration;
+import gov.nist.secauto.metaschema.databind.codegen.config.IDefinitionBindingConfiguration;
+import gov.nist.secauto.metaschema.databind.codegen.typeinfo.def.IAssemblyDefinitionTypeInfo;
+
+import org.jmock.Expectations;
+import org.jmock.junit5.JUnit5Mockery;
+import org.jmock.lib.concurrent.Synchroniser;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Unit tests for {@link ChoiceGroupTypeInfoImpl} field type generation.
+ */
+class ChoiceGroupTypeInfoImplTest {
+
+ @RegisterExtension
+ JUnit5Mockery context = new JUnit5Mockery() {
+ {
+ setThreadingPolicy(new Synchroniser());
+ }
+ };
+
+ private final IChoiceGroupInstance choiceGroupInstance = context.mock(IChoiceGroupInstance.class);
+ private final IAssemblyDefinition assemblyDefinition = context.mock(IAssemblyDefinition.class);
+ private final IAssemblyDefinitionTypeInfo parentTypeInfo = context.mock(IAssemblyDefinitionTypeInfo.class);
+
+ /**
+ * Test that getJavaFieldType returns List with wildcard type when useWildcard
+ * is true.
+ */
+ @Test
+ void testGetJavaFieldTypeReturnsWildcardTypeWhenConfigured() {
+ IBindingConfiguration bindingConfig = context.mock(IBindingConfiguration.class);
+ IDefinitionBindingConfiguration defConfig = context.mock(IDefinitionBindingConfiguration.class);
+ IChoiceGroupBindingConfiguration choiceConfig = context.mock(IChoiceGroupBindingConfiguration.class);
+
+ String groupAsName = "test-choices";
+ String itemTypeName = "com.example.ITestInterface";
+ ClassName itemClass = ClassName.bestGuess(itemTypeName);
+
+ Map