Skip to content

feat: add @Validation.MaxItems and @Validation.MinItems support for compositions - Single opencode agent#754

Open
samyuktaprabhu wants to merge 2 commits intomainfrom
feat/validation-min-max-items-v2
Open

feat: add @Validation.MaxItems and @Validation.MinItems support for compositions - Single opencode agent#754
samyuktaprabhu wants to merge 2 commits intomainfrom
feat/validation-min-max-items-v2

Conversation

@samyuktaprabhu
Copy link
Contributor

@samyuktaprabhu samyuktaprabhu commented Mar 13, 2026

// single opencode agent w/ mcp server

Summary

  • Adds @Validation.MaxItems and @Validation.MinItems annotation support for composition properties, allowing apps to constrain the number of items in a composition (e.g., attachments)
  • Implements ItemsCountValidationHandler on @Before CREATE/UPDATE for ApplicationService with draft-aware behavior: warnings during draft activation, errors on direct active operations
  • Supports integer literal values and property reference resolution for annotation values

Design

Annotations

extend my.Incidents with {
  @Validation.MaxItems : 20
  @Validation.MinItems : 2
  attachments: Composition of many Attachments;
}

Draft Behavior

  • Direct active CREATE/UPDATE: errors via messages.throwIfError() — blocks the save
  • Draft activation (DRAFT_SAVE → CREATE/UPDATE): warnings only — draft allows invalid state by concept

Error Message Targeting

Messages target the composition property (messages.error(...).target("in", b -> b.to(compositionName))) so Fiori elements highlights the table.

i18n Override Pattern

  • Base keys: Validation_MaxItems, Validation_MinItems in messages.properties
  • Per-entity/property override: define Validation_MaxItems/<EntityName>/<PropertyName> in the app's messages.properties — the handler checks via ResourceBundle fallback

Annotation Value Resolution

  • Integer literals: @Validation.MaxItems: 20 → used directly
  • String integer: @Validation.MaxItems: '20' → parsed to int
  • Property reference: @Validation.MaxItems: 'stock' → resolved from entity data at runtime
  • Unknown/unresolvable values return -1 (skips validation)

Files Changed

File Change
ItemsCountValidationHandler.java New — handler + annotation resolution logic
ItemsCountValidationHandlerTest.java New — 30+ unit tests covering all scenarios
Registration.java Wire new handler alongside existing ones
messages.properties Add Validation_MaxItems and Validation_MinItems keys
db-model.cds (unit test) Add limitedAttachments with annotations
data-model.cds (integration test) Add itemsLimitedAttachments with annotations
attachments.cds (bookshop sample) Demonstrate annotations on Books.attachments
RegistrationTest.java Updated handler count from 8 to 9
AssociationCascaderTest.java Updated for new composition in test model

Testing

All 356 unit tests pass (0 failures, 0 errors).

…ompositions

Implements item-count validation for composition properties annotated with
@Validation.MaxItems and/or @Validation.MinItems. This allows applications to
constrain the number of items in a composition (e.g., attachments).

Key design decisions:
- Handler fires on @before CREATE/UPDATE on ApplicationService
- Draft detection via ThreadDataStorageReader (reuses existing pattern)
  - Draft activation (DRAFT_SAVE -> CREATE/UPDATE): warnings only
  - Direct active CREATE/UPDATE: errors (throwIfError)
- Annotation values support integers and property references
- Messages target the composition table for proper Fiori elements display
- i18n keys: Validation_MaxItems, Validation_MinItems in messages.properties
- Per-entity/property message override via ResourceBundle fallback pattern:
  Validation_MaxItems/<EntityName>/<PropertyName>

Files changed:
- New: ItemsCountValidationHandler.java (handler + annotation resolution)
- New: ItemsCountValidationHandlerTest.java (comprehensive unit tests)
- Modified: Registration.java (wire new handler)
- Modified: messages.properties (add validation messages)
- Modified: db-model.cds (test model with annotations)
- Modified: data-model.cds (integration test model)
- Modified: attachments.cds (sample bookshop demo)
- Modified: RegistrationTest.java (updated handler count to 9)
- Modified: AssociationCascaderTest.java (updated for new composition)
@hyperspace-insights
Copy link
Contributor

Summary

The following content is AI-generated and provides a summary of the pull request:


Add @Validation.MaxItems and @Validation.MinItems Support for Compositions

New Feature

✨ Introduces @Validation.MaxItems and @Validation.MinItems annotation support for composition properties, allowing apps to constrain the number of items in a composition (e.g., attachments). The validation is draft-aware: during draft activation it reports warnings, while direct active CREATE/UPDATE operations report errors and block the save.

Changes

  • ItemsCountValidationHandler.java (new): Core event handler that validates item counts on @Before CREATE/UPDATE for ApplicationService. Supports integer literals, string-parsed integers, and property reference resolution for annotation values. Message keys follow an i18n override pattern (Validation_MaxItems/<EntityName>/<PropertyName>) with fallback to the base key. Returns -1 for unresolvable values, skipping validation.

  • Registration.java: Wires the new ItemsCountValidationHandler into the application service event handler registration.

  • messages.properties: Adds i18n keys Validation_MaxItems and Validation_MinItems with descriptive default messages including composition name, limit, and current count.

  • ItemsCountValidationHandlerTest.java (new): Comprehensive unit test suite (30+ tests) covering max/min violations, draft activation warnings, boundary conditions, annotation value resolution (integer, string, property reference), message key resolution, and non-composition element skipping.

  • RegistrationTest.java: Updated expected handler count from 8 to 9 to include ItemsCountValidationHandler, with corresponding inclusion/exclusion checks.

  • AssociationCascaderTest.java: Updated child node index assertions to account for the new limitedAttachments composition added to the test model.

  • db-model.cds (unit test): Added stock field and limitedAttachments composition with @Validation.MaxItems: 5 and @Validation.MinItems: 1 annotations.

  • data-model.cds (integration test): Added itemsLimitedAttachments composition with @Validation.MaxItems: 5 and @Validation.MinItems: 1 annotations.

  • attachments.cds (bookshop sample): Demonstrates the annotations on Books.attachments with @Validation.MaxItems: 20 and @Validation.MinItems: 2.


  • 🔄 Regenerate and Update Summary
  • ✏️ Insert as PR Description (deletes this comment)
  • 🗑️ Delete comment
PR Bot Information

Version: 1.18.5 | 📖 Documentation | 🚨 Create Incident | 💬 Feedback

  • LLM: anthropic--claude-4.6-sonnet
  • Output Template: Default Template
  • Summary Prompt: Default Prompt
  • Event Trigger: pull_request.opened
  • Correlation ID: f5cc5ca0-1ede-11f1-8bbc-1d7039e73691

💌 Have ideas or want to contribute? Create an issue and share your thoughts with us!
📑 Check out the documentation for more information.
📬 Subscribe to the Hyperspace PR Bot DL to get the latest announcements and pilot features!

Made with ❤️ by Hyperspace.

Copy link
Contributor

@hyperspace-insights hyperspace-insights bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR introduces a well-structured ItemsCountValidationHandler with solid test coverage. A few issues were flagged: (1) resolveMessageKey creates a new ResourceBundle on every call without caching, which causes unnecessary I/O per event; (2) messages.throwIfError() is called unconditionally on every active operation regardless of whether any annotated composition was present; and (3) unresolvable annotation values silently skip validation with no log output, making misconfiguration hard to diagnose. Please address these before merging.

PR Bot Information

Version: 1.18.5 | 📖 Documentation | 🚨 Create Incident | 💬 Feedback

  • LLM: anthropic--claude-4.6-sonnet
  • Correlation ID: f5cc5ca0-1ede-11f1-8bbc-1d7039e73691
  • Event Trigger: pull_request.opened

static String resolveMessageKey(String baseKey, String entityName, String propertyName) {
String specificKey = baseKey + "/" + entityName + "/" + propertyName;
try {
ResourceBundle bundle = ResourceBundle.getBundle("messages");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: resolveMessageKey loads a new ResourceBundle on every call, with no caching

ResourceBundle.getBundle("messages") is called for every composition element that has a constraint, on every CREATE/UPDATE event. ResourceBundle.getBundle involves class-loading, I/O and synchronisation overhead. In practice, the static bundle is re-loaded thousands of times per request in a busy application.

Consider caching the bundle (or the lookup result) in a static final field, or wrapping getBundle in a try-catch at class initialisation time and storing the result:

Suggested change
ResourceBundle bundle = ResourceBundle.getBundle("messages");
static String resolveMessageKey(String baseKey, String entityName, String propertyName) {
String specificKey = baseKey + "/" + entityName + "/" + propertyName;
try {
ResourceBundle bundle = ResourceBundle.getBundle("messages");
bundle.getString(specificKey);
return specificKey;
} catch (MissingResourceException e) {
return baseKey;
}
}

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

});

if (!isDraftActivation) {
messages.throwIfError();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic Error: throwIfError() is called unconditionally after all elements are processed, even when no compositions with annotations were present

When isDraftActivation is false, messages.throwIfError() is always called at line 157, regardless of whether any annotated compositions were found or whether any error was added. While harmless if throwIfError is a no-op with no errors, the intent per the Javadoc is that this is only meaningful after violations were detected. More importantly, if no compositions have constraints, the loop short-circuits at line 103, but throwIfError is still invoked unconditionally outside the loop. This is wasteful and may cause unintended side-effects depending on the Messages implementation.

Consider only calling throwIfError() when at least one error was actually emitted, or move the call inside the loop after the violation check block.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

}
}
}
return -1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic Error: Silent fallback to -1 masks unresolvable annotation values

When resolveAnnotationValue cannot resolve the annotation value (unsupported type, non-existent property reference, non-numeric property value), it returns -1. The caller then skips validation with the >= 0 guard (if (maxItems >= 0 && ...) / if (minItems >= 0 && ...)). This means a misconfigured annotation like @Validation.MaxItems: 'nonExistentProp' silently produces no validation, offering no feedback to the developer. At minimum a logger.warn should be emitted when resolution fails to make misconfiguration diagnosable.

Suggested change
return -1;
logger.warn(
"Could not resolve annotation value '{}' to an integer. Skipping validation.",
annotationValue);
return -1;
}

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

Messages messages = context.getMessages();

entity
.elements()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: entity.elements() is consumed by the Stream.forEach inside a loop over data, but a Stream can only be iterated once

entity.elements() returns a Stream. At line 83 it is chained with .filter(...).forEach(element -> { for (CdsData d : data) { ... } }). The outer loop is over elements() (via forEach) and the inner loop is over data — this is correct and not a double-consumption issue here. However, if entity.elements() is ever called more than once (e.g. for re-validation), callers must be aware the stream is fresh each invocation. This is fine as long as CdsEntity.elements() returns a new stream each call, which is the typical CAP SDK contract but is worth confirming. No action required if confirmed; flag for awareness.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

@samyuktaprabhu samyuktaprabhu changed the title feat: add @Validation.MaxItems and @Validation.MinItems support for compositions feat: add @Validation.MaxItems and @Validation.MinItems support for compositions - Single opencode agent Mar 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant