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 @@ + + + + 4.0.0 + + dev.metaschema.java + metaschema-framework + 3.0.0-SNAPSHOT + + metaschema-databind-modules-bootstrap + pom + Metaschema Modules - Binding Class Generator + Standalone POM for regenerating Metaschema model binding classes. + + + + ${project.build.directory}/generated-sources/metaschema + ${project.basedir}/../databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding + ${project.build.directory}/metaschema + ${project.basedir}/../core/metaschema/schema/metaschema + + + + + + + org.apache.maven.plugins + maven-clean-plugin + + + clean-stale-file + initialize + + clean + + + true + + + + ${stale.file.dir} + + metaschema-codegen-generateSourcesStaleFile + + + + + + + + + + ${project.groupId} + metaschema-maven-plugin + ${project.version} + + + metaschema-codegen + generate-sources + + generate-sources + + + ${metaschema.sources.dir} + ${output.dir} + + ${project.basedir}/src/main/metaschema-bindings/metaschema-model-bindings.xml + + + metaschema-module-metaschema.xml + + + ${metaschema.sources.dir}/metaschema-module-constraints.xml + + + + + + + + diff --git a/databind-metaschema/src/main/metaschema-bindings/metaschema-model-bindings.xml b/databind-metaschema/src/main/metaschema-bindings/metaschema-model-bindings.xml new file mode 100644 index 000000000..16fe7a56d --- /dev/null +++ b/databind-metaschema/src/main/metaschema-bindings/metaschema-model-bindings.xml @@ -0,0 +1,63 @@ + + + + + + gov.nist.secauto.metaschema.databind.model.metaschema.binding + + + + + + + GroupingAs + + + + + gov.nist.secauto.metaschema.databind.model.metaschema.IModelConstraintsBase + + + + + gov.nist.secauto.metaschema.databind.model.metaschema.IValueTargetedConstraintsBase + + + + + gov.nist.secauto.metaschema.databind.model.metaschema.IConstraintBase + + + + + gov.nist.secauto.metaschema.databind.model.metaschema.IValueConstraintsBase + + + + + gov.nist.secauto.metaschema.databind.model.metaschema.IConstraintBase + + + + + gov.nist.secauto.metaschema.databind.model.metaschema.IConstraintBase + + + + + gov.nist.secauto.metaschema.databind.model.metaschema.IConstraintBase + + + + + + gov.nist.secauto.metaschema.databind.model.metaschema.impl.AbstractAllowedValue + + + + diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultBindingConfiguration.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultBindingConfiguration.java index a357c27c4..2f8b06880 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultBindingConfiguration.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultBindingConfiguration.java @@ -335,6 +335,9 @@ private void processMetaschemaBindingConfig(URL configResource, MetaschemaBindin // Process property bindings for this assembly processAssemblyPropertyBindings(metaschemaConfig, name, assemblyBinding.getPropertyBindings()); + + // Process choice group bindings for this assembly + processChoiceGroupBindings(config, assemblyBinding.getChoiceGroupBindings()); } } } @@ -456,6 +459,33 @@ private static void processFieldPropertyBindings( MetaschemaBindings.MetaschemaBinding.DefineFieldBinding.PropertyBinding.Java::getCollectionClass); } + /** + * Process choice group bindings from a define-assembly-binding element. + * + * @param config + * the definition binding configuration to add choice group bindings to + * @param choiceGroupBindings + * the list of choice group bindings to process + */ + private static void processChoiceGroupBindings( + @NonNull IDefinitionBindingConfiguration config, + @Nullable List< + MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding.ChoiceGroupBinding> choiceGroupBindings) { + if (choiceGroupBindings == null || !(config instanceof DefaultDefinitionBindingConfiguration)) { + return; + } + + DefaultDefinitionBindingConfiguration mutableConfig = (DefaultDefinitionBindingConfiguration) config; + for (MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding.ChoiceGroupBinding choiceGroupBinding : choiceGroupBindings) { + String groupAsName = choiceGroupBinding.getName(); + if (groupAsName != null) { + IChoiceGroupBindingConfiguration choiceGroupConfig + = new DefaultChoiceGroupBindingConfiguration(choiceGroupBinding); + mutableConfig.addChoiceGroupBinding(groupAsName, choiceGroupConfig); + } + } + } + @NonNull private static IMutableDefinitionBindingConfiguration processDefinitionBindingConfiguration( @Nullable IDefinitionBindingConfiguration oldConfig, diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultChoiceGroupBindingConfiguration.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultChoiceGroupBindingConfiguration.java new file mode 100644 index 000000000..adecf115f --- /dev/null +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultChoiceGroupBindingConfiguration.java @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.databind.codegen.config; + +import gov.nist.secauto.metaschema.databind.config.binding.MetaschemaBindings; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Default implementation of {@link IChoiceGroupBindingConfiguration}. + * + *

+ * This implementation wraps a + * {@link MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding.ChoiceGroupBinding} + * instance from the binding configuration and provides the configuration + * interface methods. + * + *

+ * The class stores: + *

+ */ +public class DefaultChoiceGroupBindingConfiguration implements IChoiceGroupBindingConfiguration { + @NonNull + private final String groupAsName; + @Nullable + private final String itemTypeName; + private final boolean useWildcard; + + /** + * Constructs a new choice group binding configuration from a binding + * configuration object. + * + * @param binding + * the binding configuration object from the parsed binding + * configuration file + */ + public DefaultChoiceGroupBindingConfiguration( + @NonNull MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding.ChoiceGroupBinding binding) { + this.groupAsName = binding.getName(); + + MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding.ChoiceGroupBinding.ItemType itemType + = binding.getItemType(); + if (itemType != null) { + this.itemTypeName = itemType.getValue(); + // Default to true if not explicitly set + Boolean useWildcardFlag = itemType.getUseWildcard(); + this.useWildcard = useWildcardFlag == null || useWildcardFlag; + } else { + this.itemTypeName = null; + this.useWildcard = true; // default value + } + } + + @Override + @NonNull + public String getGroupAsName() { + return groupAsName; + } + + @Override + @Nullable + public String getItemTypeName() { + return itemTypeName; + } + + @Override + public boolean isUseWildcard() { + return useWildcard; + } +} diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultDefinitionBindingConfiguration.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultDefinitionBindingConfiguration.java index edb688e34..ffbe8c23b 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultDefinitionBindingConfiguration.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultDefinitionBindingConfiguration.java @@ -5,8 +5,11 @@ package gov.nist.secauto.metaschema.databind.codegen.config; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -18,6 +21,8 @@ public class DefaultDefinitionBindingConfiguration implements IMutableDefinition private String baseClassName; @NonNull private final List interfacesToImplement = new LinkedList<>(); + @NonNull + private final Map choiceGroupBindings = new LinkedHashMap<>(); /** * Create a new definition binding configuration. @@ -37,6 +42,7 @@ public DefaultDefinitionBindingConfiguration(@NonNull IDefinitionBindingConfigur this.className = config.getClassName(); this.baseClassName = config.getQualifiedBaseClassName(); this.interfacesToImplement.addAll(config.getInterfacesToImplement()); + this.choiceGroupBindings.putAll(config.getChoiceGroupBindings()); } @Override @@ -68,4 +74,21 @@ public List getInterfacesToImplement() { public void addInterfaceToImplement(String interfaceName) { this.interfacesToImplement.add(interfaceName); } + + @Override + public Map getChoiceGroupBindings() { + return Collections.unmodifiableMap(choiceGroupBindings); + } + + /** + * Add a choice group binding configuration. + * + * @param groupAsName + * the group-as name from the Metaschema module + * @param config + * the choice group binding configuration + */ + public void addChoiceGroupBinding(@NonNull String groupAsName, @NonNull IChoiceGroupBindingConfiguration config) { + this.choiceGroupBindings.put(groupAsName, config); + } } diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/IBindingConfiguration.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/IBindingConfiguration.java index 602b29970..791842aba 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/IBindingConfiguration.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/IBindingConfiguration.java @@ -67,4 +67,15 @@ public interface IBindingConfiguration { */ @NonNull List getQualifiedSuperinterfaceClassNames(@NonNull IModelDefinition definition); + + /** + * Retrieve the binding configuration for the provided definition. + * + * @param definition + * the definition to get the configuration for + * @return the binding configuration, or {@code null} if there is no + * configuration for this definition + */ + @Nullable + IDefinitionBindingConfiguration getBindingConfigurationForDefinition(@NonNull IModelDefinition definition); } diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/IChoiceGroupBindingConfiguration.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/IChoiceGroupBindingConfiguration.java new file mode 100644 index 000000000..cc367142e --- /dev/null +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/IChoiceGroupBindingConfiguration.java @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.databind.codegen.config; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Provides binding configuration for a choice group within an assembly + * definition. + * + *

+ * 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} instead of + * {@code List}. This provides additional flexibility when the exact item + * type may vary while still maintaining type safety. + * + *

+ * 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 getInterfacesToImplement(); + + /** + * Get the choice group binding configurations for this definition. + * + *

+ * 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 getChoiceGroupBindings(); } diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/ChoiceGroupTypeInfoImpl.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/ChoiceGroupTypeInfoImpl.java index 7190d1088..12a844612 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/ChoiceGroupTypeInfoImpl.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/ChoiceGroupTypeInfoImpl.java @@ -8,14 +8,21 @@ import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; +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.IGroupable; import gov.nist.secauto.metaschema.core.model.IModelDefinition; import gov.nist.secauto.metaschema.core.model.INamedModelInstanceGrouped; +import gov.nist.secauto.metaschema.core.model.JsonGroupAsBehavior; import gov.nist.secauto.metaschema.core.util.ObjectUtils; +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.model.annotations.BoundChoiceGroup; @@ -53,6 +60,55 @@ public TypeName getJavaItemType() { return getParentTypeInfo().getTypeResolver().getClassName(getInstance()); } + /** + * Get the Java field type for this choice group instance. + * + *

+ * 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}). + * + * @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 _propertyBindings; + /** + * Provides binding configuration for a choice group within the parent assembly. + */ + @BoundAssembly( + formalName = "Choice Group Binding", + description = "Provides binding configuration for a choice group within the parent assembly.", + useName = "choice-group-binding", + maxOccurs = -1, + groupAs = @GroupAs(name = "choice-group-bindings", inJson = JsonGroupAsBehavior.LIST)) + private List _choiceGroupBindings; + /** * Constructs a new * {@code gov.nist.secauto.metaschema.databind.config.binding.MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding} @@ -809,6 +823,63 @@ public boolean removePropertyBinding(PropertyBinding item) { return _propertyBindings != null && _propertyBindings.remove(value); } + /** + * Get the choice Group Binding. + * + *

+ * Provides binding configuration for a choice group within the parent assembly. + * + * @return the choice-group-binding value + */ + @NonNull + public List getChoiceGroupBindings() { + if (_choiceGroupBindings == null) { + _choiceGroupBindings = new LinkedList<>(); + } + return _choiceGroupBindings; + } + + /** + * Set the choice Group Binding. + * + *

+ * 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 value) { + _choiceGroupBindings = value; + } + + /** + * Add a new {@link ChoiceGroupBinding} item to the underlying collection. + * + * @param item + * the item to add + * @return {@code true} + */ + public boolean addChoiceGroupBinding(ChoiceGroupBinding item) { + ChoiceGroupBinding value = ObjectUtils.requireNonNull(item, "item cannot be null"); + if (_choiceGroupBindings == null) { + _choiceGroupBindings = new LinkedList<>(); + } + return _choiceGroupBindings.add(value); + } + + /** + * Remove the first matching {@link ChoiceGroupBinding} item from the underlying + * collection. + * + * @param item + * the item to remove + * @return {@code true} if the item was removed or {@code false} otherwise + */ + public boolean removeChoiceGroupBinding(ChoiceGroupBinding item) { + ChoiceGroupBinding value = ObjectUtils.requireNonNull(item, "item cannot be null"); + return _choiceGroupBindings != null && _choiceGroupBindings.remove(value); + } + @Override public String toString() { return new ReflectionToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE).toString(); @@ -1229,6 +1300,222 @@ public String toString() { } } } + + /** + * Provides binding configuration for a choice group within the parent assembly. + */ + @MetaschemaAssembly( + formalName = "Choice Group Binding", + description = "Provides binding configuration for a choice group within the parent assembly.", + name = "choice-group-binding", + moduleClass = MetaschemaBindingsModule.class) + public static class ChoiceGroupBinding implements IBoundObject { + private final IMetaschemaData __metaschemaData; + + /** + * The name of the choice group (matches the group-as name in the metaschema). + */ + @BoundFlag( + formalName = "Name", + description = "The name of the choice group (matches the group-as name in the metaschema).", + name = "name", + required = true, + typeAdapter = TokenAdapter.class) + private String _name; + + /** + * A fully qualified Java type for collection items. When specified, the + * generated field and getter will use this type instead of Object. + */ + @BoundField( + 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.", + useName = "item-type") + private ItemType _itemType; + + /** + * Constructs a new + * {@code gov.nist.secauto.metaschema.databind.config.binding.MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding.ChoiceGroupBinding} + * instance with no metadata. + */ + public ChoiceGroupBinding() { + this(null); + } + + /** + * Constructs a new + * {@code gov.nist.secauto.metaschema.databind.config.binding.MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding.ChoiceGroupBinding} + * instance with the specified metadata. + * + * @param data + * the metaschema data, or {@code null} if none + */ + public ChoiceGroupBinding(IMetaschemaData data) { + this.__metaschemaData = data; + } + + @Override + public IMetaschemaData getMetaschemaData() { + return __metaschemaData; + } + + /** + * Get the name. + * + *

+ * 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). 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). 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 choiceGroupBindings = new HashMap<>(); + choiceGroupBindings.put(groupAsName, choiceConfig); + + // Create a real resolver with mocked binding configuration + DefaultTypeResolver typeResolver = new DefaultTypeResolver(bindingConfig); + + context.checking(new Expectations() { + { + // Setup expectations for parent and instance + allowing(parentTypeInfo).getTypeResolver(); + will(returnValue(typeResolver)); + + allowing(choiceGroupInstance).getMaxOccurs(); + will(returnValue(-1)); // Unbounded collection + + allowing(choiceGroupInstance).getContainingDefinition(); + will(returnValue(assemblyDefinition)); + + allowing(bindingConfig).getBindingConfigurationForDefinition(assemblyDefinition); + will(returnValue(defConfig)); + + allowing(choiceGroupInstance).getGroupAsName(); + will(returnValue(groupAsName)); + + allowing(defConfig).getChoiceGroupBindings(); + will(returnValue(choiceGroupBindings)); + + allowing(choiceConfig).getItemTypeName(); + will(returnValue(itemTypeName)); + + allowing(choiceConfig).isUseWildcard(); + will(returnValue(true)); + + allowing(choiceGroupInstance).getJsonGroupAsBehavior(); + will(returnValue(JsonGroupAsBehavior.SINGLETON_OR_LIST)); + } + }); + + ChoiceGroupTypeInfoImpl typeInfo = new ChoiceGroupTypeInfoImpl(choiceGroupInstance, parentTypeInfo); + TypeName result = typeInfo.getJavaFieldType(); + + assertNotNull(result); + assertTrue(result instanceof ParameterizedTypeName, "Expected ParameterizedTypeName"); + + ParameterizedTypeName paramType = (ParameterizedTypeName) result; + assertEquals(ClassName.get(List.class), paramType.rawType); + assertEquals(1, paramType.typeArguments.size()); + + TypeName itemType = paramType.typeArguments.get(0); + assertTrue(itemType instanceof WildcardTypeName, "Expected WildcardTypeName for item type"); + + WildcardTypeName wildcardType = (WildcardTypeName) itemType; + assertEquals(1, wildcardType.upperBounds.size()); + assertEquals(itemClass, wildcardType.upperBounds.get(0)); + } + + /** + * Test that getJavaFieldType returns List with non-wildcard type when + * useWildcard is false. + */ + @Test + void testGetJavaFieldTypeReturnsNonWildcardTypeWhenNotConfigured() { + 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 choiceGroupBindings = new HashMap<>(); + choiceGroupBindings.put(groupAsName, choiceConfig); + + DefaultTypeResolver typeResolver = new DefaultTypeResolver(bindingConfig); + + context.checking(new Expectations() { + { + allowing(parentTypeInfo).getTypeResolver(); + will(returnValue(typeResolver)); + + allowing(choiceGroupInstance).getMaxOccurs(); + will(returnValue(-1)); + + allowing(choiceGroupInstance).getContainingDefinition(); + will(returnValue(assemblyDefinition)); + + allowing(bindingConfig).getBindingConfigurationForDefinition(assemblyDefinition); + will(returnValue(defConfig)); + + allowing(choiceGroupInstance).getGroupAsName(); + will(returnValue(groupAsName)); + + allowing(defConfig).getChoiceGroupBindings(); + will(returnValue(choiceGroupBindings)); + + allowing(choiceConfig).getItemTypeName(); + will(returnValue(itemTypeName)); + + allowing(choiceConfig).isUseWildcard(); + will(returnValue(false)); // No wildcard + + allowing(choiceGroupInstance).getJsonGroupAsBehavior(); + will(returnValue(JsonGroupAsBehavior.SINGLETON_OR_LIST)); + } + }); + + ChoiceGroupTypeInfoImpl typeInfo = new ChoiceGroupTypeInfoImpl(choiceGroupInstance, parentTypeInfo); + TypeName result = typeInfo.getJavaFieldType(); + + assertNotNull(result); + assertTrue(result instanceof ParameterizedTypeName, "Expected ParameterizedTypeName"); + + ParameterizedTypeName paramType = (ParameterizedTypeName) result; + assertEquals(ClassName.get(List.class), paramType.rawType); + assertEquals(1, paramType.typeArguments.size()); + + // Should be the item class directly, not a wildcard + TypeName itemType = paramType.typeArguments.get(0); + assertEquals(itemClass, itemType); + } + + /** + * Test that getJavaFieldType returns Map with wildcard type when keyed. + */ + @Test + void testGetJavaFieldTypeReturnsMapWithWildcardForKeyedGroups() { + 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 choiceGroupBindings = new HashMap<>(); + choiceGroupBindings.put(groupAsName, choiceConfig); + + DefaultTypeResolver typeResolver = new DefaultTypeResolver(bindingConfig); + + context.checking(new Expectations() { + { + allowing(parentTypeInfo).getTypeResolver(); + will(returnValue(typeResolver)); + + allowing(choiceGroupInstance).getMaxOccurs(); + will(returnValue(-1)); + + allowing(choiceGroupInstance).getContainingDefinition(); + will(returnValue(assemblyDefinition)); + + allowing(bindingConfig).getBindingConfigurationForDefinition(assemblyDefinition); + will(returnValue(defConfig)); + + allowing(choiceGroupInstance).getGroupAsName(); + will(returnValue(groupAsName)); + + allowing(defConfig).getChoiceGroupBindings(); + will(returnValue(choiceGroupBindings)); + + allowing(choiceConfig).getItemTypeName(); + will(returnValue(itemTypeName)); + + allowing(choiceConfig).isUseWildcard(); + will(returnValue(true)); + + allowing(choiceGroupInstance).getJsonGroupAsBehavior(); + will(returnValue(JsonGroupAsBehavior.KEYED)); // Map collection type + } + }); + + ChoiceGroupTypeInfoImpl typeInfo = new ChoiceGroupTypeInfoImpl(choiceGroupInstance, parentTypeInfo); + TypeName result = typeInfo.getJavaFieldType(); + + assertNotNull(result); + assertTrue(result instanceof ParameterizedTypeName, "Expected ParameterizedTypeName"); + + ParameterizedTypeName paramType = (ParameterizedTypeName) result; + assertEquals(ClassName.get(Map.class), paramType.rawType); + assertEquals(2, paramType.typeArguments.size()); + + // Key should be String + TypeName keyType = paramType.typeArguments.get(0); + assertEquals(ClassName.get(String.class), keyType); + + // Value should be wildcard + TypeName valueType = paramType.typeArguments.get(1); + assertTrue(valueType instanceof WildcardTypeName, "Expected WildcardTypeName for value type"); + + WildcardTypeName wildcardType = (WildcardTypeName) valueType; + assertEquals(1, wildcardType.upperBounds.size()); + assertEquals(itemClass, wildcardType.upperBounds.get(0)); + } + + /** + * Test that getJavaFieldType returns non-collection type when maxOccurs is 1. + */ + @Test + void testGetJavaFieldTypeReturnsSingletonWhenMaxOccursIsOne() { + ClassName itemClass = ClassName.get(Object.class); + IBindingConfiguration bindingConfig = context.mock(IBindingConfiguration.class); + DefaultTypeResolver typeResolver = new DefaultTypeResolver(bindingConfig); + + context.checking(new Expectations() { + { + allowing(parentTypeInfo).getTypeResolver(); + will(returnValue(typeResolver)); + + allowing(choiceGroupInstance).getMaxOccurs(); + will(returnValue(1)); // Single occurrence + + allowing(choiceGroupInstance).getContainingDefinition(); + will(returnValue(assemblyDefinition)); + + allowing(bindingConfig).getBindingConfigurationForDefinition(assemblyDefinition); + will(returnValue(null)); + } + }); + + ChoiceGroupTypeInfoImpl typeInfo = new ChoiceGroupTypeInfoImpl(choiceGroupInstance, parentTypeInfo); + TypeName result = typeInfo.getJavaFieldType(); + + assertNotNull(result); + assertEquals(itemClass, result); + } + + /** + * Test that getJavaFieldType returns List of Object when no binding + * configuration exists. + */ + @Test + void testGetJavaFieldTypeReturnsListOfObjectWhenNoBindingConfig() { + ClassName itemClass = ClassName.get(Object.class); + IBindingConfiguration bindingConfig = context.mock(IBindingConfiguration.class); + DefaultTypeResolver typeResolver = new DefaultTypeResolver(bindingConfig); + + context.checking(new Expectations() { + { + allowing(parentTypeInfo).getTypeResolver(); + will(returnValue(typeResolver)); + + allowing(choiceGroupInstance).getMaxOccurs(); + will(returnValue(-1)); + + allowing(choiceGroupInstance).getContainingDefinition(); + will(returnValue(assemblyDefinition)); + + allowing(bindingConfig).getBindingConfigurationForDefinition(assemblyDefinition); + will(returnValue(null)); // No binding config + + allowing(choiceGroupInstance).getJsonGroupAsBehavior(); + will(returnValue(JsonGroupAsBehavior.SINGLETON_OR_LIST)); + } + }); + + ChoiceGroupTypeInfoImpl typeInfo = new ChoiceGroupTypeInfoImpl(choiceGroupInstance, parentTypeInfo); + TypeName result = typeInfo.getJavaFieldType(); + + assertNotNull(result); + assertTrue(result instanceof ParameterizedTypeName, "Expected ParameterizedTypeName"); + + ParameterizedTypeName paramType = (ParameterizedTypeName) result; + assertEquals(ClassName.get(List.class), paramType.rawType); + assertEquals(1, paramType.typeArguments.size()); + assertEquals(itemClass, paramType.typeArguments.get(0)); + } + + /** + * Test that getJavaFieldType handles maxOccurs greater than 1. + */ + @Test + void testGetJavaFieldTypeWithMaxOccursGreaterThanOne() { + 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 choiceGroupBindings = new HashMap<>(); + choiceGroupBindings.put(groupAsName, choiceConfig); + + DefaultTypeResolver typeResolver = new DefaultTypeResolver(bindingConfig); + + context.checking(new Expectations() { + { + allowing(parentTypeInfo).getTypeResolver(); + will(returnValue(typeResolver)); + + allowing(choiceGroupInstance).getMaxOccurs(); + will(returnValue(5)); // Fixed upper bound > 1 + + allowing(choiceGroupInstance).getContainingDefinition(); + will(returnValue(assemblyDefinition)); + + allowing(bindingConfig).getBindingConfigurationForDefinition(assemblyDefinition); + will(returnValue(defConfig)); + + allowing(choiceGroupInstance).getGroupAsName(); + will(returnValue(groupAsName)); + + allowing(defConfig).getChoiceGroupBindings(); + will(returnValue(choiceGroupBindings)); + + allowing(choiceConfig).getItemTypeName(); + will(returnValue(itemTypeName)); + + allowing(choiceConfig).isUseWildcard(); + will(returnValue(true)); + + allowing(choiceGroupInstance).getJsonGroupAsBehavior(); + will(returnValue(JsonGroupAsBehavior.SINGLETON_OR_LIST)); + } + }); + + ChoiceGroupTypeInfoImpl typeInfo = new ChoiceGroupTypeInfoImpl(choiceGroupInstance, parentTypeInfo); + TypeName result = typeInfo.getJavaFieldType(); + + assertNotNull(result); + assertTrue(result instanceof ParameterizedTypeName, "Expected ParameterizedTypeName"); + + ParameterizedTypeName paramType = (ParameterizedTypeName) result; + assertEquals(ClassName.get(List.class), paramType.rawType); + + TypeName itemType = paramType.typeArguments.get(0); + assertTrue(itemType instanceof WildcardTypeName, "Expected WildcardTypeName"); + } +} diff --git a/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/DefaultTypeResolverTest.java b/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/DefaultTypeResolverTest.java new file mode 100644 index 000000000..3fd2e13cb --- /dev/null +++ b/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/DefaultTypeResolverTest.java @@ -0,0 +1,260 @@ +/* + * 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 com.squareup.javapoet.ClassName; + +import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition; +import gov.nist.secauto.metaschema.core.model.IChoiceGroupInstance; +import gov.nist.secauto.metaschema.core.util.ObjectUtils; +import gov.nist.secauto.metaschema.databind.codegen.config.DefaultBindingConfiguration; +import gov.nist.secauto.metaschema.databind.codegen.config.DefaultChoiceGroupBindingConfiguration; +import gov.nist.secauto.metaschema.databind.codegen.config.DefaultDefinitionBindingConfiguration; +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.config.binding.MetaschemaBindings; + +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.Map; + +/** + * Unit tests for {@link DefaultTypeResolver} choice group type resolution. + */ +class DefaultTypeResolverTest { + + @RegisterExtension + JUnit5Mockery context = new JUnit5Mockery() { + { + setThreadingPolicy(new Synchroniser()); + } + }; + + private final IAssemblyDefinition assemblyDefinition = context.mock(IAssemblyDefinition.class); + private final IChoiceGroupInstance choiceGroupInstance = context.mock(IChoiceGroupInstance.class); + + /** + * Test that getClassName returns Object when no binding configuration exists + * for the choice group. + */ + @Test + void testGetClassNameReturnsObjectWhenNoBindingConfig() { + IBindingConfiguration bindingConfig = context.mock(IBindingConfiguration.class); + DefaultTypeResolver resolver = new DefaultTypeResolver(bindingConfig); + + context.checking(new Expectations() { + { + oneOf(choiceGroupInstance).getContainingDefinition(); + will(returnValue(assemblyDefinition)); + oneOf(bindingConfig).getBindingConfigurationForDefinition(assemblyDefinition); + will(returnValue(null)); + } + }); + + ClassName result = resolver.getClassName(choiceGroupInstance); + + assertNotNull(result); + assertEquals(ClassName.get(Object.class), result); + } + + /** + * Test that getClassName returns Object when binding configuration exists but + * has no choice group binding. + */ + @Test + void testGetClassNameReturnsObjectWhenNoChoiceGroupBinding() { + IBindingConfiguration bindingConfig = context.mock(IBindingConfiguration.class); + IDefinitionBindingConfiguration defConfig = context.mock(IDefinitionBindingConfiguration.class); + DefaultTypeResolver resolver = new DefaultTypeResolver(bindingConfig); + + context.checking(new Expectations() { + { + oneOf(choiceGroupInstance).getContainingDefinition(); + will(returnValue(assemblyDefinition)); + oneOf(bindingConfig).getBindingConfigurationForDefinition(assemblyDefinition); + will(returnValue(defConfig)); + oneOf(choiceGroupInstance).getGroupAsName(); + will(returnValue("test-choices")); + oneOf(defConfig).getChoiceGroupBindings(); + will(returnValue(new HashMap<>())); + } + }); + + ClassName result = resolver.getClassName(choiceGroupInstance); + + assertNotNull(result); + assertEquals(ClassName.get(Object.class), result); + } + + /** + * Test that getClassName returns configured item type when binding + * configuration specifies one. + */ + @Test + void testGetClassNameReturnsConfiguredItemType() { + IBindingConfiguration bindingConfig = context.mock(IBindingConfiguration.class); + IDefinitionBindingConfiguration defConfig = context.mock(IDefinitionBindingConfiguration.class); + IChoiceGroupBindingConfiguration choiceConfig = context.mock(IChoiceGroupBindingConfiguration.class); + DefaultTypeResolver resolver = new DefaultTypeResolver(bindingConfig); + + String groupAsName = "test-choices"; + String itemTypeName = "com.example.ITestInterface"; + + Map choiceGroupBindings = new HashMap<>(); + choiceGroupBindings.put(groupAsName, choiceConfig); + + context.checking(new Expectations() { + { + oneOf(choiceGroupInstance).getContainingDefinition(); + will(returnValue(assemblyDefinition)); + oneOf(bindingConfig).getBindingConfigurationForDefinition(assemblyDefinition); + will(returnValue(defConfig)); + oneOf(choiceGroupInstance).getGroupAsName(); + will(returnValue(groupAsName)); + oneOf(defConfig).getChoiceGroupBindings(); + will(returnValue(choiceGroupBindings)); + allowing(choiceConfig).getItemTypeName(); + will(returnValue(itemTypeName)); + } + }); + + ClassName result = resolver.getClassName(choiceGroupInstance); + + assertNotNull(result); + assertEquals("com.example", result.packageName()); + assertEquals("ITestInterface", result.simpleName()); + } + + /** + * Test that getClassName returns Object when choice group binding exists but + * item type is null. + */ + @Test + void testGetClassNameReturnsObjectWhenItemTypeIsNull() { + IBindingConfiguration bindingConfig = context.mock(IBindingConfiguration.class); + IDefinitionBindingConfiguration defConfig = context.mock(IDefinitionBindingConfiguration.class); + IChoiceGroupBindingConfiguration choiceConfig = context.mock(IChoiceGroupBindingConfiguration.class); + DefaultTypeResolver resolver = new DefaultTypeResolver(bindingConfig); + + String groupAsName = "test-choices"; + + Map choiceGroupBindings = new HashMap<>(); + choiceGroupBindings.put(groupAsName, choiceConfig); + + context.checking(new Expectations() { + { + oneOf(choiceGroupInstance).getContainingDefinition(); + will(returnValue(assemblyDefinition)); + oneOf(bindingConfig).getBindingConfigurationForDefinition(assemblyDefinition); + will(returnValue(defConfig)); + oneOf(choiceGroupInstance).getGroupAsName(); + will(returnValue(groupAsName)); + oneOf(defConfig).getChoiceGroupBindings(); + will(returnValue(choiceGroupBindings)); + allowing(choiceConfig).getItemTypeName(); + will(returnValue(null)); + } + }); + + ClassName result = resolver.getClassName(choiceGroupInstance); + + assertNotNull(result); + assertEquals(ClassName.get(Object.class), result); + } + + /** + * Test edge case: getClassName with nested class item type name. + */ + @Test + void testGetClassNameWithNestedClassType() { + IBindingConfiguration bindingConfig = context.mock(IBindingConfiguration.class); + IDefinitionBindingConfiguration defConfig = context.mock(IDefinitionBindingConfiguration.class); + IChoiceGroupBindingConfiguration choiceConfig = context.mock(IChoiceGroupBindingConfiguration.class); + DefaultTypeResolver resolver = new DefaultTypeResolver(bindingConfig); + + String groupAsName = "test-choices"; + String itemTypeName = "com.example.OuterClass.InnerInterface"; + + Map choiceGroupBindings = new HashMap<>(); + choiceGroupBindings.put(groupAsName, choiceConfig); + + context.checking(new Expectations() { + { + oneOf(choiceGroupInstance).getContainingDefinition(); + will(returnValue(assemblyDefinition)); + oneOf(bindingConfig).getBindingConfigurationForDefinition(assemblyDefinition); + will(returnValue(defConfig)); + oneOf(choiceGroupInstance).getGroupAsName(); + will(returnValue(groupAsName)); + oneOf(defConfig).getChoiceGroupBindings(); + will(returnValue(choiceGroupBindings)); + allowing(choiceConfig).getItemTypeName(); + will(returnValue(itemTypeName)); + } + }); + + ClassName result = resolver.getClassName(choiceGroupInstance); + + assertNotNull(result); + assertEquals("com.example", result.packageName()); + assertEquals("InnerInterface", result.simpleName()); + } + + /** + * Test that getClassName ignores isUseWildcard setting and always returns the + * simple class name. The wildcard behavior is handled by + * ChoiceGroupTypeInfoImpl when generating collection types, not by the type + * resolver. + */ + @Test + void testGetClassNameIgnoresWildcardSetting() { + IBindingConfiguration bindingConfig = context.mock(IBindingConfiguration.class); + IDefinitionBindingConfiguration defConfig = context.mock(IDefinitionBindingConfiguration.class); + IChoiceGroupBindingConfiguration choiceConfig = context.mock(IChoiceGroupBindingConfiguration.class); + DefaultTypeResolver resolver = new DefaultTypeResolver(bindingConfig); + + String groupAsName = "test-choices"; + String itemTypeName = "com.example.ITestInterface"; + + Map choiceGroupBindings = new HashMap<>(); + choiceGroupBindings.put(groupAsName, choiceConfig); + + context.checking(new Expectations() { + { + oneOf(choiceGroupInstance).getContainingDefinition(); + will(returnValue(assemblyDefinition)); + oneOf(bindingConfig).getBindingConfigurationForDefinition(assemblyDefinition); + will(returnValue(defConfig)); + oneOf(choiceGroupInstance).getGroupAsName(); + will(returnValue(groupAsName)); + oneOf(defConfig).getChoiceGroupBindings(); + will(returnValue(choiceGroupBindings)); + allowing(choiceConfig).getItemTypeName(); + will(returnValue(itemTypeName)); + // isUseWildcard is available but not used by getClassName + allowing(choiceConfig).isUseWildcard(); + will(returnValue(true)); + } + }); + + ClassName result = resolver.getClassName(choiceGroupInstance); + + // getClassName returns the base class, not a wildcard type + // The wildcard wrapping is done in ChoiceGroupTypeInfoImpl + assertNotNull(result); + assertEquals("com.example", result.packageName()); + assertEquals("ITestInterface", result.simpleName()); + } +} diff --git a/databind/src/test/resources/metaschema/binding-config-with-choice-groups.xml b/databind/src/test/resources/metaschema/binding-config-with-choice-groups.xml new file mode 100644 index 000000000..770c93291 --- /dev/null +++ b/databind/src/test/resources/metaschema/binding-config-with-choice-groups.xml @@ -0,0 +1,37 @@ + + + + + + + + + TestAssembly + + + + + gov.nist.secauto.metaschema.core.model.IModelElement + + + + + java.lang.String + + + + + + + + + + + SimpleAssembly + + + +