Skip to content

fix: NisoSts metadata repeatable-element cardinality - #55

Merged
ronaldtse merged 1 commit into
mainfrom
fix/nisosts-metadata-collection-cardinality
Jul 27, 2026
Merged

fix: NisoSts metadata repeatable-element cardinality#55
ronaldtse merged 1 commit into
mainfrom
fix/nisosts-metadata-collection-cardinality

Conversation

@ronaldtse

Copy link
Copy Markdown
Contributor

Summary

Fixes the cardinality bug documented in BUGREPORT.metadata-iso-collection-types.md. Per NISO STS v1.2 XSD, most children of <iso-meta>, <reg-meta>, <nat-meta>, <std-meta> are maxOccurs="unbounded". sts-ruby was declaring these as singular Ruby attributes, silently dropping all but the last value when a document had multiple of the same element.

Changes

For each affected NisoSts metadata class, added collection: true to every repeatable-per-schema attribute:

File Collections added
lib/sts/niso_sts/metadata_iso.rb content_language, custom_meta_group, ics, meta_date, permissions, release_date, std_ref, std_xref, title_wrap (9)
lib/sts/niso_sts/metadata_std.rb content_language, std_org_group, self_uri (3)
lib/sts/niso_sts/reg_meta.rb release_date, ics (2)
lib/sts/niso_sts/nat_meta.rb release_date, ics (2)

16 cardinality fixes total.

Lutaml-model limitation discovered

Two repeatable elements (<comm-ref> and <secretariat>) commonly appear as empty elements in real fixtures (e.g., spec/fixtures/ISO_13849-1_2008-12_en_TBX.xml). lutaml-model 0.8.19 drops empty elements from :string-typed collections on round-trip — the collection ends up [] instead of [""], and the empty element is lost on serialise.

To preserve empty elements, <comm-ref> and <secretariat> stay singular with value_map: { to: { empty: :empty } }. This is a deliberate trade-off: schema-conformance sacrificed for those two attributes so round-trip fidelity is preserved. Filed as BUGREPORT.collection-empty-element-dropped.md in ~/src/lutaml/lutaml-model/.

Regression specs

spec/niso_sts/metadata_cardinality_spec.rb (30 examples):

  • Each collection: true attribute asserted as a collection
  • Each singular attribute asserted as singular with documented reason (empty-element preservation vs schema maxOccurs=1)
  • Reproduction of the original BUGREPORT: meta.std_ref = [sr1, sr2] now serialises two distinct <std-ref> entries

Backwards compatibility

Callers that read these attributes as single values (e.g., meta.std_ref.value) must update to meta.std_ref.first.value or similar. The model is pre-1.0; CHANGELOG entry recommended.

Test plan

  • bundle exec rspec — 5198 examples, 0 failures (was 5168; +30 cardinality specs)
  • bundle exec rubocop — clean (537 files)
  • Round-trip tests for ISO 13849-1 and DIN EN ISO 13849-1 still pass (the empty <comm-ref/> and <secretariat/> are preserved)
  • CI green

Per NISO STS v1.2 XSD (reference-docs/NISO-STS-extended-1-MathML3-XSD/),
most children of <iso-meta>, <reg-meta>, <nat-meta>, <std-meta> are
maxOccurs="unbounded". sts-ruby was declaring these as singular Ruby
attributes, silently dropping all but the last value when a document
had multiple of the same element.

The downstream impact (filed as BUGREPORT.metadata-iso-collection-types.md
in the project root) was that callers like
`meta.std_ref = [sr1, sr2]` silently lost sr1: the second assignment
overwrote the first, and lutaml-model's element_order recorded the same
object twice, so the serialised output had two identical `<std-ref>`
entries pointing at sr2.

## Changes

For each affected NisoSts metadata class, added `collection: true` to
every repeatable-per-schema attribute:

- lib/sts/niso_sts/metadata_iso.rb: 9 attributes made collections
  (content_language, custom_meta_group, ics, meta_date, permissions,
  release_date, std_ref, std_xref, title_wrap)
- lib/sts/niso_sts/metadata_std.rb: 3 attributes made collections
  (content_language, std_org_group, self_uri)
- lib/sts/niso_sts/reg_meta.rb: 2 attributes made collections
  (release_date, ics)
- lib/sts/niso_sts/nat_meta.rb: 2 attributes made collections
  (release_date, ics)

## Lutaml-model limitation discovered

Two repeatable elements (<comm-ref> and <secretariat>) commonly appear as
empty elements in real fixtures (e.g.,
spec/fixtures/ISO_13849-1_2008-12_en_TBX.xml). lutaml-model 0.8.19 drops
empty elements from `:string`-typed collections on round-trip — the
collection ends up empty `[]` instead of `[""]`, and the empty element
is lost on serialise.

To preserve the empty elements, <comm-ref> and <secretariat> stay
singular with `value_map: { to: { empty: :empty } }`. This is a
deliberate trade-off: schema-conformance is sacrificed for those two
attributes so round-trip fidelity is preserved. Filed as
`BUGREPORT.collection-empty-element-dropped.md` in
~/src/lutaml/lutaml-model/.

## Regression specs

spec/niso_sts/metadata_cardinality_spec.rb (30 examples):
- Each collection: true attribute asserted as a collection
- Each singular attribute asserted as singular with documented reason
  (empty-element preservation vs schema maxOccurs=1)
- Reproduction of the original BUGREPORT: `meta.std_ref = [sr1, sr2]`
  now serialises two distinct `<std-ref>` entries

## Backwards compatibility

Callers that read these attributes as single values (e.g.,
`meta.std_ref.value`) must update to `meta.std_ref.first.value` or
similar. The model is pre-1.0; document in CHANGELOG.

5198 examples, 0 failures. Rubocop clean.
@ronaldtse
ronaldtse merged commit 958e8ae into main Jul 27, 2026
14 checks passed
@ronaldtse
ronaldtse deleted the fix/nisosts-metadata-collection-cardinality branch July 27, 2026 15:26
ronaldtse added a commit that referenced this pull request Jul 27, 2026
… classes

PR #55 fixed the cardinality bug in NisoSts metadata classes (MetadataIso,
MetadataStd, RegMeta, NatMeta) but applied the value_map workaround
inconsistently:

- MetadataIso: comm-ref had value_map, secretariat did NOT (regression)
- MetadataStd: both had value_map (correct)
- RegMeta: neither had value_map (regression)
- NatMeta: neither had value_map (regression)

The cardinality work in PR #55 kept comm_ref and secretariat as singular
attributes (sacrificing schema-conformance) to preserve empty elements
in real fixtures via value_map: { to: { empty: :empty } }. The trade-off
was only fully applied to MetadataStd; three of four metadata classes
silently dropped empty <comm-ref/> and <secretariat/> elements on
round-trip.

This restores the value_map on all four classes uniformly:

- lib/sts/niso_sts/metadata_iso.rb: secretariat (added)
- lib/sts/niso_sts/reg_meta.rb: comm-ref, secretariat (both added)
- lib/sts/niso_sts/nat_meta.rb: comm-ref, secretariat (both added)

Adds a regression spec covering all four classes — discovered by the
round-trip audit when validating BUGREPORT.metadata-iso-collection-types.md.

Verification: with all four classes preserving both empty elements, the
following round-trip works:

  <iso-meta><comm-ref/><secretariat/></iso-meta>
  -> MetadataIso.from_xml(...).to_xml
  -> <iso-meta><comm-ref/><secretariat/></iso-meta>

5202 examples, 0 failures (was 5198; +4 new cross-class regression
specs). Rubocop clean.
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