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
18 changes: 18 additions & 0 deletions .claude/rules/unit-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,21 @@ Every test class should prioritize edge case coverage over happy paths:
3. Add tests before completing the work

Use the `unit-test-writing` skill for the detailed workflow.

## Legacy Code Coverage

**When improving or refactoring existing classes, add tests for legacy functionality.**

This ensures:
- Existing behavior is documented through tests
- Regressions are caught if refactoring breaks something
- Test coverage improves incrementally over time

### Process

1. **Before changes**: Write tests capturing current behavior of code you're touching
2. **Verify tests pass**: Confirms tests accurately reflect existing behavior
3. **Make improvements**: Refactor or enhance the code
4. **Verify tests still pass**: Confirms behavioral equivalence

This approach builds test coverage organically as the codebase evolves.
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@
import gov.nist.secauto.metaschema.databind.codegen.typeinfo.def.IAssemblyDefinitionTypeInfo;
import gov.nist.secauto.metaschema.databind.model.annotations.GroupAs;

import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;

abstract class AbstractModelInstanceTypeInfo<INSTANCE extends IModelInstanceAbsolute>
extends AbstractInstanceTypeInfo<INSTANCE, IAssemblyDefinitionTypeInfo>
Expand Down Expand Up @@ -74,6 +77,29 @@ public TypeName getJavaFieldType() {
return retval;
}

@Override
public boolean isCollectionType() {
IModelInstanceAbsolute instance = getInstance();
int maxOccurs = instance.getMaxOccurs();
return maxOccurs == -1 || maxOccurs > 1;
}

@Nullable
@Override
public Class<?> getCollectionImplementationClass() {
IModelInstanceAbsolute instance = getInstance();
int maxOccurs = instance.getMaxOccurs();
if (maxOccurs == -1 || maxOccurs > 1) {
// This is a collection - return the appropriate implementation class
if (JsonGroupAsBehavior.KEYED.equals(instance.getJsonGroupAsBehavior())) {
return LinkedHashMap.class;
}
return LinkedList.class;
}
// Not a collection
return null;
}

@NonNull
protected abstract AnnotationSpec.Builder newBindingAnnotation();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ public AbstractNamedModelInstanceTypeInfo(
super(instance, parentDefinition);
}

@Override
public boolean isRequired() {
INSTANCE instance = getInstance();
// A model instance is required if minOccurs >= 1 AND it's a single item (not a
// collection)
return instance.getMinOccurs() >= 1 && instance.getMaxOccurs() == 1;
}

@Override
public boolean isCollectionType() {
INSTANCE instance = getInstance();
// A collection has maxOccurs > 1 or unbounded (-1)
return instance.getMaxOccurs() == -1 || instance.getMaxOccurs() > 1;
}

@NonNull
@Override
public String getBaseName() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package gov.nist.secauto.metaschema.databind.codegen.typeinfo;

import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
Expand All @@ -21,7 +22,52 @@
import javax.lang.model.element.Modifier;

import edu.umd.cs.findbugs.annotations.NonNull;

import edu.umd.cs.findbugs.annotations.Nullable;

/**
* Abstract base class for generating Java property code including fields,
* getters, and setters with appropriate null-safety annotations.
*
* <h2>Null-Safety Annotation Contract</h2>
*
* <p>
* Generated getters and setters receive null-safety annotations based on two
* factors: whether the property is required and whether it is a collection.
*
* <h3>Getter Annotations</h3>
* <ul>
* <li>{@code @NonNull} - Collection properties (lazy initialized) or required
* properties</li>
* <li>{@code @Nullable} - Optional non-collection properties</li>
* </ul>
*
* <h3>Setter Annotations</h3>
* <ul>
* <li>{@code @NonNull} - Collection properties or required properties</li>
* <li>{@code @Nullable} - Optional non-collection properties</li>
* </ul>
*
* <h2>Collection Handling</h2>
*
* <p>
* Collection properties (where {@link #isCollectionType()} returns
* {@code true}) use lazy initialization in their getters. The getter checks if
* the field is null and initializes it with a new collection instance from
* {@link #getCollectionImplementationClass()} before returning. This ensures
* collection getters never return null, allowing them to be annotated
* {@code @NonNull}.
*
* <p>
* <strong>Contract:</strong> Subclasses must ensure that
* {@link #isCollectionType()} and {@link #getCollectionImplementationClass()}
* are consistent: {@code isCollectionType()} returns {@code true} if and only
* if {@code getCollectionImplementationClass()} returns non-null.
*
* @param <PARENT>
* the type of the parent definition type info
* @see IPropertyTypeInfo#isCollectionType()
* @see IPropertyTypeInfo#getCollectionImplementationClass()
*/
public abstract class AbstractPropertyTypeInfo<PARENT extends IDefinitionTypeInfo>
extends AbstractTypeInfo<PARENT>
implements IPropertyTypeInfo {
Expand Down Expand Up @@ -53,27 +99,59 @@ public Set<IModelDefinition> build(@NonNull TypeSpec.Builder builder) {
return retval;
}

/**
* Build getter and setter methods for this property.
*
* <p>
* This method generates accessor methods with appropriate null-safety
* annotations and Javadoc based on the property's characteristics. Collection
* getters use lazy initialization to ensure they never return null.
*
* @param typeBuilder
* the class builder to add methods to
* @param fieldBuilder
* the field spec for the backing field
*/
protected void buildExtraMethods(
@NonNull TypeSpec.Builder typeBuilder,
@NonNull FieldSpec fieldBuilder) {

TypeName javaFieldType = getJavaFieldType();
String propertyName = getPropertyName();
{
Class<?> collectionImplClass = getCollectionImplementationClass();
// Collections are always @NonNull (lazy initialized), otherwise based on
// isRequired()
Class<?> nullAnnotation = collectionImplClass != null || isRequired() ? NonNull.class : Nullable.class;
MethodSpec.Builder method = MethodSpec.methodBuilder("get" + propertyName)
.returns(javaFieldType)
.addAnnotation(AnnotationSpec.builder(nullAnnotation).build())
.addModifiers(Modifier.PUBLIC);
assert method != null;
buildGetterJavadoc(method);

if (collectionImplClass != null) {
// Use lazy initialization for collections
method.beginControlFlow("if ($N == null)", fieldBuilder)
.addStatement("$N = new $T<>()", fieldBuilder, collectionImplClass)
.endControlFlow();
}
method.addStatement("return $N", fieldBuilder);
typeBuilder.addMethod(method.build());
}

{
ParameterSpec valueParam = ParameterSpec.builder(javaFieldType, "value").build();
// Add null-safety annotation to setter parameter
// Collections get @NonNull (lazy initialized), required properties get @NonNull
ParameterSpec.Builder paramBuilder = ParameterSpec.builder(javaFieldType, "value");
Class<?> paramAnnotation = isCollectionType() || isRequired() ? NonNull.class : Nullable.class;
paramBuilder.addAnnotation(AnnotationSpec.builder(paramAnnotation).build());
ParameterSpec valueParam = paramBuilder.build();
MethodSpec.Builder method = MethodSpec.methodBuilder("set" + propertyName)
.addModifiers(Modifier.PUBLIC)
.addParameter(valueParam);
assert method != null;
buildSetterJavadoc(method, "value");
method.addStatement("$N = $N", fieldBuilder, valueParam);
typeBuilder.addMethod(method.build());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,11 +386,16 @@ protected TypeSpec.Builder newClassBuilder(

builder.addMethod(MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addJavadoc("Constructs a new {@code $L} instance with no metadata.\n", typeInfo.getClassName())
.addStatement("this(null)")
.build());

builder.addMethod(MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addJavadoc("Constructs a new {@code $L} instance with the specified metadata.\n", typeInfo.getClassName())
.addJavadoc("\n")
.addJavadoc("@param data\n")
.addJavadoc(" the metaschema data, or {@code null} if none\n")
.addParameter(IMetaschemaData.class, "data")
.addStatement("this.$N = $N", "__metaschemaData", "data")
.build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public String getBaseName() {
return getInstance().getEffectiveName();
}

@Override
public boolean isRequired() {
return getInstance().isRequired();
}

@Override
public TypeName getJavaFieldType() {
return ObjectUtils.notNull(ClassName.get(getInstance().getDefinition().getJavaTypeAdapter().getJavaClass()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,101 @@
package gov.nist.secauto.metaschema.databind.codegen.typeinfo;

import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;

import gov.nist.secauto.metaschema.core.datatype.markup.MarkupLine;
import gov.nist.secauto.metaschema.core.model.INamedInstance;

import edu.umd.cs.findbugs.annotations.NonNull;

public interface INamedInstanceTypeInfo extends IInstanceTypeInfo {
@Override
INamedInstance getInstance();

/**
* {@inheritDoc}
*
* <p>
* This implementation adds the effective description from the named instance as
* the field's Javadoc content.
*/
@Override
default void buildFieldJavadoc(FieldSpec.Builder builder) {
MarkupLine description = getInstance().getEffectiveDescription();
if (description != null) {
builder.addJavadoc("$S", description.toHtml());
builder.addJavadoc("$L\n", description.toHtml());
}
}

/**
* {@inheritDoc}
*
* <p>
* This implementation generates getter Javadoc using the instance's formal name
* (if available) or property name, adds the effective description, and includes
* an appropriate {@code @return} tag based on whether the property is required
* or a collection.
*/
@Override
default void buildGetterJavadoc(@NonNull MethodSpec.Builder builder) {
MarkupLine description = getInstance().getEffectiveDescription();
String formalName = getInstance().getEffectiveFormalName();
String propertyName = getInstance().getEffectiveName();

// Use formal name if available, otherwise property name
if (formalName != null) {
builder.addJavadoc("Get the $L.\n", TypeInfoUtils.toLowerFirstChar(formalName));
} else {
builder.addJavadoc("Get the {@code $L} property.\n", propertyName);
}

// Add description as a second paragraph if available
if (description != null) {
builder.addJavadoc("\n");
builder.addJavadoc("<p>\n");
builder.addJavadoc("$L\n", description.toHtml());
}

builder.addJavadoc("\n");
// Collections are always @NonNull (lazy initialized), required properties are
// @NonNull
if (isRequired() || isCollectionType()) {
builder.addJavadoc("@return the $L value\n", propertyName);
} else {
builder.addJavadoc("@return the $L value, or {@code null} if not set\n", propertyName);
}
}

/**
* {@inheritDoc}
*
* <p>
* This implementation generates setter Javadoc using the instance's formal name
* (if available) or property name, adds the effective description, and includes
* a {@code @param} tag for the value parameter.
*/
@Override
default void buildSetterJavadoc(@NonNull MethodSpec.Builder builder, @NonNull String paramName) {
MarkupLine description = getInstance().getEffectiveDescription();
String formalName = getInstance().getEffectiveFormalName();
String propertyName = getInstance().getEffectiveName();

// Use formal name if available, otherwise property name
if (formalName != null) {
builder.addJavadoc("Set the $L.\n", TypeInfoUtils.toLowerFirstChar(formalName));
} else {
builder.addJavadoc("Set the {@code $L} property.\n", propertyName);
}

// Add description as a second paragraph if available
if (description != null) {
builder.addJavadoc("\n");
builder.addJavadoc("<p>\n");
builder.addJavadoc("$L\n", description.toHtml());
}

builder.addJavadoc("\n");
builder.addJavadoc("@param $L\n", paramName);
builder.addJavadoc(" the $L value to set\n", propertyName);
}
}
Loading
Loading