Skip to content

Added strict cardinality validation - #720

Draft
HassanAkbar wants to merge 3 commits into
mainfrom
feat/strict-cardinality
Draft

Added strict cardinality validation#720
HassanAkbar wants to merge 3 commits into
mainfrom
feat/strict-cardinality

Conversation

@HassanAkbar

@HassanAkbar HassanAkbar commented Jul 3, 2026

Copy link
Copy Markdown
Member

Fixes #185.

Attributes now enforce their declared cardinality. No collection: means one
value; giving it several is a violation.

Where the error surfaces (deviation from #185)

#185 says "it should raise an error". This reports at #validate instead of at
parse, and #validate! raises ValidationError. The wording still holds, the
timing differs.

Parsing keeps every value it was given. Nothing is discarded, so the violation
stays visible and #validate reports it. Raising at parse would throw the
document away before anyone could inspect it, and it would break every caller
that parses first and checks later. Reporting at validate is non-breaking and
matches how every other rule in this library already works.

Flagging for the issue author to confirm.

Behavior changes

  • Nested models run their own validations now. Checks that never ran will start
    reporting.
  • A singular reader can return an array before you call #validate.
  • Enum value? shorthands answer false while an attribute is over-counted.
  • Assigning a scalar to a collection: attribute wraps it in a collection.

All four are in docs/_pages/breaking-changes.adoc.

The xmi benchmark gate was measuring the wrong thing

The gate flagged +7% allocations. There is no regression.

  • Real allocations are identical between this branch and main. Five paired runs,
    ratios 0.99988 to 1.00012. Two fixtures are bit-identical.
  • The new branches never run during an xmi parse. coerce_to_collection? fires
    0 times, the three model_transform guards 0 times.
  • bench_common.rb measured ObjectSpace.count_objects[:TOTAL], which counts
    heap slots, not allocations. It only moves when Ruby adds a page.
  • On identical code, six runs gave 381515, 98245, 98244, 98242, 98245, 98236 —
    a 3.9x spread, under-counting the true 400k by 4x. large fails its own 1.05
    gate against itself at 1.0562.
  • Switched to GC.stat[:total_allocated_objects]. Same six runs: 400003 then
    400001 five times. A 1.000005x spread.

The workflow runs the PR's copy of the harness for both sides, so both readings
change together. Stored baselines feed only the push-to-main job, not this gate.

All five downstream gates share this code. Thresholds tuned loose to absorb the
old noise stay satisfied. If a non-xmi gate goes red after this, it means a
pre-existing regression stopped being hidden — this PR did not cause it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses Issue #185 by enforcing strict cardinality for XML deserialization: attributes that are not declared as collections (collection: true / range) now error when multiple XML elements map to a singular attribute, aligning XML behavior with existing key/value (JSON) behavior.

Changes:

  • Enforce parse-time cardinality errors for non-collection map_element mappings when multiple elements are present.
  • Add specs covering strict cardinality behavior for XML vs JSON and preserving “lazy” validation behavior for ranged collections and map_content.
  • Update validation documentation to clarify collection semantics as cardinality (0..1 vs 0..* vs bounded ranges).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
spec/lutaml/model/cdata_spec.rb Marks element2 as a collection to match existing test data with repeated <element2> nodes under strict cardinality.
spec/lutaml/model/cardinality_strict_spec.rb Adds regression coverage for strict non-collection over-count in XML parsing and verifies unchanged lazy behaviors.
lib/lutaml/xml/model_transform.rb Preserves multi-occurrence arrays for singular attrs and raises CollectionTrueMissingError during XML parse for non-collection over-count.
docs/_pages/validation.adoc Clarifies that collection defines attribute cardinality and documents the related error types.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Comment thread lib/lutaml/model/attribute.rb Outdated
@HassanAkbar
HassanAkbar force-pushed the feat/strict-cardinality branch from 4a940c9 to d688d1a Compare July 14, 2026 09:34
@github-actions

Copy link
Copy Markdown

JS build check

Triggered [js-pr-check]
(https://github.com/lutaml/lutaml-model-js/actions/workflows/pr-check.yml)
against this PR's head (d688d1a).

The result will appear as a lutaml-model-js / pr-check status check
on this PR once the workflow run completes.

@HassanAkbar
HassanAkbar force-pushed the feat/strict-cardinality branch from d688d1a to dff2d3b Compare July 14, 2026 10:16
@github-actions

Copy link
Copy Markdown

JS build check

Triggered [js-pr-check]
(https://github.com/lutaml/lutaml-model-js/actions/workflows/pr-check.yml)
against this PR's head (dff2d3b).

The result will appear as a lutaml-model-js / pr-check status check
on this PR once the workflow run completes.

@github-actions

Copy link
Copy Markdown

JS build check

Triggered [js-pr-check]
(https://github.com/lutaml/lutaml-model-js/actions/workflows/pr-check.yml)
against this PR's head (e3e7788).

The result will appear as a lutaml-model-js / pr-check status check
on this PR once the workflow run completes.

@github-actions

Copy link
Copy Markdown

JS build check

Triggered [js-pr-check]
(https://github.com/lutaml/lutaml-model-js/actions/workflows/pr-check.yml)
against this PR's head (f12d9aa).

The result will appear as a lutaml-model-js / pr-check status check
on this PR once the workflow run completes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Comment thread lib/lutaml/model/collection_handler.rb

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

lib/lutaml/model/attribute.rb:677

  • When casting list input for a custom Collection whose element type is Lutaml::Model::Type::Hash, this branch casts each element first and then passes the casted results into the Collection initializer. Type::Hash.cast can normalize { "text" => "x" } into the scalar string "x"; the Collection initializer then casts again (calling Type::Hash.cast("x")), which will raise because it falls through to value.to_h for non-Hash/Array values.

To avoid this double-cast, skip the per-element cast(...) step for custom-collection attributes when the resolved element type is a Type::Value (e.g., Type::Hash) and let the Collection initializer perform the single cast.

        if collection_instance?(value) || value.is_a?(Array)
          merged_opts = options.merge(resolved_type: resolved_type,
                                      converted: true)
          return build_collection(value.map do |v|
            cast(v, format, register, merged_opts)

@HassanAkbar
HassanAkbar force-pushed the feat/strict-cardinality branch from f12d9aa to d22dd39 Compare July 29, 2026 07:55
@HassanAkbar
HassanAkbar requested a review from Copilot July 29, 2026 07:55
@github-actions

Copy link
Copy Markdown

JS build check

Triggered [js-pr-check]
(https://github.com/lutaml/lutaml-model-js/actions/workflows/pr-check.yml)
against this PR's head (d22dd39).

The result will appear as a lutaml-model-js / pr-check status check
on this PR once the workflow run completes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

@github-actions

Copy link
Copy Markdown

JS build check

Triggered [js-pr-check]
(https://github.com/lutaml/lutaml-model-js/actions/workflows/pr-check.yml)
against this PR's head (545d315).

The result will appear as a lutaml-model-js / pr-check status check
on this PR once the workflow run completes.

@HassanAkbar
HassanAkbar force-pushed the feat/strict-cardinality branch from 545d315 to 2b664f7 Compare July 29, 2026 10:40
@github-actions

Copy link
Copy Markdown

JS build check

Triggered [js-pr-check]
(https://github.com/lutaml/lutaml-model-js/actions/workflows/pr-check.yml)
against this PR's head (2b664f7).

The result will appear as a lutaml-model-js / pr-check status check
on this PR once the workflow run completes.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

JS build check

Triggered [js-pr-check]
(https://github.com/lutaml/lutaml-model-js/actions/workflows/pr-check.yml)
against this PR's head (0c28885).

The result will appear as a lutaml-model-js / pr-check status check
on this PR once the workflow run completes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.

Suppressed comments (2)

lib/lutaml/model/validation.rb:58

  • Nested model validation currently calls item.validate without threading the active register (or the child’s own lutaml_register). That can cause nested models to validate against the default register even when the parent is validating under a non-default register, producing incorrect attribute/type resolution and inconsistent behavior with serialization (which already prefers value.lutaml_register). Consider passing register: when the child #validate accepts it, falling back to a zero-arity call for downstream overrides that don’t declare parameters.
              next if Validation.visiting?(item)

              sub_errors = item.validate
              errors.concat(sub_errors) if sub_errors.is_a?(Array)

lib/lutaml/xml/model_transform.rb:339

  • For XML mapping onto a plain Ruby model (non-Serialize instance), cardinality violations have no later #validate phase to be reported. The new eager check only runs for non-collection attributes (!attr.collection?), so a ranged collection attribute on a PORO (e.g., collection: 0..2) can exceed its bounds without raising anywhere. It seems safer to run attr.valid_collection! for PORO mappings regardless of whether the attribute is a collection, while still skipping map_content rules.
          if !instance_is_serialize && attr && !attr.collection? &&
              !rule.content_mapping?
            attr.valid_collection!(value, context)
          end

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.

Be strict about adherence to cardinality via collection: true

2 participants