Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .claude/rules/new-metaschema-feature-checklist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# New Metaschema Feature Checklist

When adding a new Metaschema model feature (e.g., a new instance type), systematically evaluate **all** areas below. Missing any area risks incomplete support.

## Mandatory Areas

### 1. Core Model

- [ ] Create interface extending appropriate base (`IModelInstanceAbsolute`, etc.)
- [ ] Add `ModelType` enum value if applicable
- [ ] Update container model interfaces and implementations (`IContainerModelAssemblySupport`, `DefaultContainerModelAssemblySupport`, model builder)
- [ ] Update visitor pattern: `IModelElementVisitor`, `AbstractModelElementVisitor`, and ALL implementing visitors
- [ ] Write core model tests

### 2. Databind Binding Layer

- [ ] Create annotation in `databind/model/annotations/`
- [ ] Create binding interface in `databind/model/` and implementation in `databind/model/impl/`
- [ ] Create Metaschema binding implementation in `databind/model/metaschema/impl/`
- [ ] Update `AssemblyModelGenerator` (and `ChoiceModelGenerator` if applicable)
- [ ] Update class introspection (`DefinitionAssembly` or similar) to scan for new annotation

### 3. Databind I/O (if feature handles data)

- [ ] Update `MetaschemaXmlReader` and `MetaschemaXmlWriter`
- [ ] Update `MetaschemaJsonReader` and `MetaschemaJsonWriter`
- [ ] Create format-specific wrapper classes if needed
- [ ] Write round-trip tests

### 4. Code Generation

- [ ] Create type info class in `databind/codegen/typeinfo/` following existing patterns
- [ ] Update `AssemblyDefinitionTypeInfoImpl` to process the new instance type
- [ ] Update `ITypeResolver` if definition resolution is needed
- [ ] Write codegen tests (compile test module, verify generated annotations/fields)

### 5. Schema Generation

- [ ] Update XML Schema generator for XSD output
- [ ] Update JSON Schema generator for JSON Schema output
- [ ] Write schema generation tests for both formats

### 6. Constraint Processing

- [ ] Update `ConstraintComposingVisitor` visitor method
- [ ] Determine if constraints can target the feature; if not, use `illegalTargetError()`

## Conditional Areas

| Area | When Needed | Key Files |
|------|-------------|-----------|
| Metapath/Query | Feature is queryable or affects traversal | `core/.../metapath/` |
| Maven Plugin | New build configuration needed | `metaschema-maven-plugin/` |
| CLI | New commands or output options | `metaschema-cli/`, `cli-processor/` |
| Testing Module | Test infrastructure changes | `metaschema-testing/`, `unit-tests.yaml` |

## Verification

```bash
mvn clean install -PCI -Prelease
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* SPDX-FileCopyrightText: none
* SPDX-License-Identifier: CC0-1.0
*/

package dev.metaschema.databind.codegen.typeinfo;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import java.util.Set;

import dev.metaschema.core.model.IAnyContent;
import dev.metaschema.core.model.IAnyInstance;
import dev.metaschema.core.model.IModelDefinition;
import dev.metaschema.core.util.CollectionUtil;
import dev.metaschema.databind.codegen.typeinfo.def.IAssemblyDefinitionTypeInfo;
import dev.metaschema.databind.model.annotations.BoundAny;
import edu.umd.cs.findbugs.annotations.NonNull;

/**
* Type information for generating a {@link BoundAny}-annotated field in a Java
* class corresponding to an {@link IAnyInstance} in a Metaschema assembly
* model.
* <p>
* The generated field captures unmodeled content using the {@link IAnyContent}
* interface, allowing round-trip preservation of arbitrary properties not
* defined by the Metaschema model.
*/
public class AnyInstanceTypeInfoImpl
extends AbstractInstanceTypeInfo<IAnyInstance, IAssemblyDefinitionTypeInfo> {

/**
* Constructs a new type information object for an any instance.
*
* @param instance
* the any instance
* @param parentDefinition
* the type information for the parent assembly definition containing
* this instance
*/
public AnyInstanceTypeInfoImpl(
@NonNull IAnyInstance instance,
@NonNull IAssemblyDefinitionTypeInfo parentDefinition) {
super(instance, parentDefinition);
}

@Override
public String getBaseName() {
return "any";
}

@Override
public boolean isRequired() {
return false;
}

@Override
public TypeName getJavaFieldType() {
return ClassName.get(IAnyContent.class);
}

@Override
public Set<IModelDefinition> buildField(
TypeSpec.Builder typeBuilder,
FieldSpec.Builder fieldBuilder) {
super.buildField(typeBuilder, fieldBuilder);
fieldBuilder.addAnnotation(BoundAny.class);
return CollectionUtil.emptySet();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import dev.metaschema.core.model.IAnyInstance;
import dev.metaschema.core.model.IAssemblyDefinition;
import dev.metaschema.core.model.IChoiceGroupInstance;
import dev.metaschema.core.model.IChoiceInstance;
Expand All @@ -23,6 +24,7 @@
import dev.metaschema.core.model.INamedModelInstanceAbsolute;
import dev.metaschema.core.util.CustomCollectors;
import dev.metaschema.core.util.ObjectUtils;
import dev.metaschema.databind.codegen.typeinfo.AnyInstanceTypeInfoImpl;
import dev.metaschema.databind.codegen.typeinfo.IInstanceTypeInfo;
import dev.metaschema.databind.codegen.typeinfo.IModelInstanceTypeInfo;
import dev.metaschema.databind.codegen.typeinfo.INamedModelInstanceTypeInfo;
Expand All @@ -45,19 +47,29 @@ class AssemblyDefinitionTypeInfoImpl

public AssemblyDefinitionTypeInfoImpl(@NonNull IAssemblyDefinition definition, @NonNull ITypeResolver typeResolver) {
super(definition, typeResolver);
this.instanceToTypeInfoMap = ObjectUtils.notNull(Lazy.of(() -> Stream.concat(
getFlagInstanceTypeInfos().stream(),
processModel(definition))
.collect(CustomCollectors.toMap(
IInstanceTypeInfo::getInstance,
CustomCollectors.identity(),
(key, v1, v2) -> {
if (LOGGER.isErrorEnabled()) {
LOGGER.error(String.format("Unexpected duplicate property name '%s'", key));
}
return ObjectUtils.notNull(v2);
},
LinkedHashMap::new))));
this.instanceToTypeInfoMap = ObjectUtils.notNull(Lazy.of(() -> {
Stream<? extends IInstanceTypeInfo> instances = Stream.concat(
getFlagInstanceTypeInfos().stream(),
processModel(definition));

// Add any instance if present on the assembly definition
IAnyInstance anyInstance = definition.getAnyInstance();
if (anyInstance != null) {
instances = Stream.concat(instances,
Stream.of(new AnyInstanceTypeInfoImpl(anyInstance, this)));
}

return instances.collect(CustomCollectors.toMap(
IInstanceTypeInfo::getInstance,
CustomCollectors.identity(),
(key, v1, v2) -> {
if (LOGGER.isErrorEnabled()) {
LOGGER.error(String.format("Unexpected duplicate property name '%s'", key));
}
return ObjectUtils.notNull(v2);
},
LinkedHashMap::new));
}));
this.propertyNameToTypeInfoMap = ObjectUtils.notNull(Lazy.of(() -> getInstanceTypeInfoMap().values().stream()
.collect(Collectors.toMap(
IInstanceTypeInfo::getPropertyName,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* SPDX-FileCopyrightText: none
* SPDX-License-Identifier: CC0-1.0
*/

package dev.metaschema.databind.codegen;

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 org.junit.jupiter.api.Test;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Paths;

import dev.metaschema.core.model.IAnyContent;
import dev.metaschema.core.model.IBoundObject;
import dev.metaschema.core.model.MetaschemaException;
import dev.metaschema.core.util.ObjectUtils;
import dev.metaschema.databind.io.BindingException;
import dev.metaschema.databind.model.annotations.BoundAny;

/**
* Tests that the code generator properly emits {@link BoundAny}-annotated
* fields for assemblies containing {@code <any/>} in their model.
*/
class AnyCodegenTest
extends AbstractMetaschemaTest {

@Test
void testAnyFieldGenerated()
throws MetaschemaException, IOException, ClassNotFoundException, BindingException,
NoSuchMethodException {
Class<? extends IBoundObject> rootClass = compileModule(
ObjectUtils.notNull(Paths.get("src/test/resources/metaschema/any/metaschema.xml")),
null,
"com.example.ns.any_test.Root",
ObjectUtils.notNull(generationDir));

// Verify a field annotated with @BoundAny exists
Field anyField = findBoundAnyField(rootClass);
assertNotNull(anyField, "Generated class should have a field annotated with @BoundAny");

// Verify the field type is IAnyContent
assertEquals(IAnyContent.class, anyField.getType(),
"@BoundAny field should be of type IAnyContent");

// Verify getter exists and returns IAnyContent
Method getter = rootClass.getMethod("getAny");
assertNotNull(getter, "Generated class should have getAny() method");
assertTrue(IAnyContent.class.isAssignableFrom(getter.getReturnType()),
"getAny() should return IAnyContent");

// Verify setter exists and accepts IAnyContent
Method setter = rootClass.getMethod("setAny", IAnyContent.class);
assertNotNull(setter, "Generated class should have setAny(IAnyContent) method");
}

/**
* Find the field annotated with {@link BoundAny} in the given class.
*
* @param clazz
* the class to search
* @return the field, or {@code null} if not found
*/
private static Field findBoundAnyField(Class<?> clazz) {
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(BoundAny.class)) {
return field;
}
}
return null;
}
}
Loading