From b8657f15f9b5d69fb9b7bbc6b5ae4a66d488c25e Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Fri, 26 Dec 2025 14:20:06 -0500 Subject: [PATCH] feat(codegen): add typed choice group collection support Add binding configuration support for specifying item types on choice groups, enabling the code generator to produce typed collections like List instead of List. Changes: - Add choice-group-binding element to metaschema-bindings.yaml schema - Create IChoiceGroupBindingConfiguration interface and implementation - Update DefaultBindingConfiguration to parse choice-group-binding elements - Update DefaultTypeResolver to use item-type from binding configuration - Update ChoiceGroupTypeInfoImpl to generate wildcard bounded types - Add bootstrap POM for databind-metaschema module regeneration - Add comprehensive unit tests for new functionality Also updates .claude/rules/development-workflow.md to: - Strengthen TDD requirements with enforcement gates - Add mandatory parallel agent usage section - Clarify that test agents can run in parallel --- .claude/rules/development-workflow.md | 142 ++++++- databind-metaschema/pom-bootstrap.xml | 93 +++++ .../metaschema-model-bindings.xml | 63 +++ .../config/DefaultBindingConfiguration.java | 30 ++ ...efaultChoiceGroupBindingConfiguration.java | 79 ++++ ...DefaultDefinitionBindingConfiguration.java | 23 + .../codegen/config/IBindingConfiguration.java | 11 + .../IChoiceGroupBindingConfiguration.java | 61 +++ .../IDefinitionBindingConfiguration.java | 13 + .../typeinfo/ChoiceGroupTypeInfoImpl.java | 56 +++ .../codegen/typeinfo/DefaultTypeResolver.java | 14 +- .../codegen/typeinfo/ITypeResolver.java | 8 + .../config/binding/MetaschemaBindings.java | 287 +++++++++++++ .../main/metaschema/metaschema-bindings.yaml | 29 ++ .../DefaultBindingConfigurationTest.java | 105 +++++ ...ltChoiceGroupBindingConfigurationTest.java | 205 +++++++++ .../typeinfo/ChoiceGroupTypeInfoImplTest.java | 392 ++++++++++++++++++ .../typeinfo/DefaultTypeResolverTest.java | 260 ++++++++++++ .../binding-config-with-choice-groups.xml | 37 ++ 19 files changed, 1895 insertions(+), 13 deletions(-) create mode 100644 databind-metaschema/pom-bootstrap.xml create mode 100644 databind-metaschema/src/main/metaschema-bindings/metaschema-model-bindings.xml create mode 100644 databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultChoiceGroupBindingConfiguration.java create mode 100644 databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/config/IChoiceGroupBindingConfiguration.java create mode 100644 databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/config/DefaultChoiceGroupBindingConfigurationTest.java create mode 100644 databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/ChoiceGroupTypeInfoImplTest.java create mode 100644 databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/DefaultTypeResolverTest.java create mode 100644 databind/src/test/resources/metaschema/binding-config-with-choice-groups.xml 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 + + + +