From 94c575405a2c26533bee651606966055752f29f2 Mon Sep 17 00:00:00 2001 From: HassanAkbar Date: Wed, 22 Jul 2026 12:56:13 +0500 Subject: [PATCH 1/3] restore @id on IsoSts element classes --- TODO.sts-refactor/00-overview.md | 22 +++---- TODO.sts-refactor/03-namespace-coupling.md | 32 +++++----- lib/sts/iso_sts/doc_number.rb | 5 +- lib/sts/iso_sts/doc_type.rb | 5 +- lib/sts/iso_sts/fpage.rb | 2 + lib/sts/iso_sts/ics.rb | 5 +- lib/sts/iso_sts/is_proof.rb | 9 ++- lib/sts/iso_sts/issue.rb | 2 + lib/sts/iso_sts/lpage.rb | 2 + lib/sts/iso_sts/originator.rb | 5 +- lib/sts/iso_sts/page_range.rb | 2 + lib/sts/iso_sts/part_number.rb | 5 +- lib/sts/iso_sts/proj_id.rb | 5 +- lib/sts/iso_sts/pub_date.rb | 2 + lib/sts/iso_sts/pub_id.rb | 2 + lib/sts/iso_sts/release_version.rb | 5 +- lib/sts/iso_sts/sdo.rb | 5 +- lib/sts/iso_sts/secretariat.rb | 5 +- lib/sts/iso_sts/suppl_number.rb | 5 +- lib/sts/iso_sts/suppl_type.rb | 5 +- lib/sts/iso_sts/suppl_version.rb | 5 +- lib/sts/iso_sts/urn.rb | 5 +- lib/sts/iso_sts/version.rb | 5 +- lib/sts/iso_sts/volume.rb | 2 + lib/sts/iso_sts/year.rb | 2 + spec/iso_sts/iso_sts_element_spec.rb | 70 ++++++++++++++-------- 26 files changed, 154 insertions(+), 65 deletions(-) diff --git a/TODO.sts-refactor/00-overview.md b/TODO.sts-refactor/00-overview.md index 3e82413..5d148c0 100644 --- a/TODO.sts-refactor/00-overview.md +++ b/TODO.sts-refactor/00-overview.md @@ -167,21 +167,23 @@ grep -r "method_missing|respond_to_missing|Object.const_get|\.send" lib/ ### Architectural Items (High Effort) - `03-namespace-coupling.md` — IN PROGRESS: 63→20 IsoSts→NisoSts references - remaining (issue #40). Source of truth is `ISOSTS.xsd`, NOT the NisoSts - models — they disagree on nearly every element. + remaining (issue #40). ISOSTS.xsd governs content models and all non-`@id` + attributes; the `@id` follows the 86948b9 convention. - `04-register-versioning.md` — Version the models via lutaml-model Registers - `11-duplicate-models.md` — 44 overlapping element resolution (depends on 03) ### Known bugs — schema conformance (separate from 03) The 2026-05-07 "@id added to all models (XSD-verified)" pass verified against -the **NISO** XSD and applied the result to IsoSts. ISOSTS disagrees: -- **16 IsoSts models silently drop ISOSTS attributes that exist** — real data - loss. `sub`/`sup` lose `arrange`+`specific-use`; `ext-link` loses 5 xlink - attrs; `mixed-citation` loses 6; `graphic`, `copyright-*`, `edition`, - `title`, `label`, `uri`, `named-content`, `underline`, `meta-date`, `body` - lose others. -- **25 IsoSts models carry an `@id` ISOSTS does not define** — harmless (never - populated on parse, never emitted on serialise), but dead surface. +the **NISO** XSD and applied the result to IsoSts. +- **Real bug — dropped ISOSTS attributes.** Some IsoSts models silently drop + ISOSTS attributes that exist in real documents — real data loss. `sub`/`sup` + lose `arrange`+`specific-use`; `ext-link` loses its `xlink:*` set; + `mixed-citation` loses several; `graphic`, `copyright-*`, `edition`, `title`, + `label`, `uri`, `named-content`, `underline`, `meta-date`, `body` lose + others. Regenerate the exact `name=`+`ref=` table when filing the issue. +- **Not a bug — the conventional `@id`.** IsoSts models carry an `@id` that + ISOSTS does not itself define; this is the deliberate 86948b9 convention + (NISO-XSD-verified), not dead surface. ## Next Action Two independent tracks: diff --git a/TODO.sts-refactor/03-namespace-coupling.md b/TODO.sts-refactor/03-namespace-coupling.md index 06124ab..2f706c0 100644 --- a/TODO.sts-refactor/03-namespace-coupling.md +++ b/TODO.sts-refactor/03-namespace-coupling.md @@ -14,41 +14,42 @@ evolves, so coupling them violates OCP. PR #31 reduced this from 157 to 63 references. Issue #40 reduced it further, from 63 to 20. -## Source of truth: ISOSTS.xsd, NOT the NisoSts models +## Content models from ISOSTS.xsd; `@id` from the 86948b9 convention -The obvious approach — duplicate each NisoSts class into IsoSts — produces -**schema-incorrect models**. The NisoSts models disagree with -`reference-docs/isosts-v1/xsd/ISOSTS.xsd` on nearly every element: +IsoSts models carry `@id` following the compatibility convention established in +86948b9 (NISO-XSD-verified). ISOSTS.xsd governs element content +models and all non-`@id` attributes; it is not an authority that forbids the +conventional `@id`. So the two schemas can legitimately differ per element: -| Element | ISOSTS says | NisoSts model | +| Element | ISOSTS content/attrs | NisoSts model | |---|---|---| -| `year` (:948) | `content-type`, `specific-use`, `xml:lang` — no `@id` | `@id` only | -| `doc-type` (:6378) | `type="xs:string"` — no attributes at all | `@id` + content | +| `doc-type` (:6378) | `type="xs:string"` | `@id` + content | | `ics` (:6373) | `type="xs:string"` | `@id` + `ics-desc` child | | `fpage` | `content-type`, `seq`, `specific-use`, `xml:lang`; no children | `@id` + bold/italic | | `license` (:51) | `license-type`, `specific-use`, `xml:lang` | `@id`, `xlink:href` | -| `ruby` | **not an ISOSTS element** | exists (NISO only) | `feature_doc.xml` declares ``, and the ISOSTS DTD agrees with ISOSTS.xsd (both derive `bold`, `sub` etc. from the -JATS 0.4 modules). ISOSTS.xsd is a faithful conversion and is the authority. +JATS 0.4 modules) on content models. **Round-tripping does not prove correctness.** A model that invents or drops an attribute still parses and serialises symmetrically, so the suite stays green while the model is wrong. Assert the exact attribute set per element instead. -Attribute lists must be **generated** from the XSD, never hand-read: `version` -on `tex-math` and `specific-use` on `pub-id` sit after long `xs:enumeration` -blocks and are invisible to a truncated read. +Non-`@id` attribute lists must be **generated** from the XSD, never hand-read: +`version` on `tex-math` and `specific-use` on `pub-id` sit after long +`xs:enumeration` blocks and are invisible to a truncated read. ## Done in issue #40 — 43 refs removed, 27 classes added - **14 `xs:string` elements** (`originator`, `doc-type`, `doc-number`, `part-number`, `version`, `suppl-type`, `suppl-number`, `suppl-version`, `urn`, `sdo`, `proj-id`, `release-version`, `ics`, `secretariat`) — modelled - as content-only IsoSts classes with no attributes. 23 refs. + as content IsoSts classes that also carry the conventional `@id` (86948b9); + `secretariat` is content plus `@id`, not content-only. 23 refs. - **3 `permissions` refs** repointed to the existing `IsoSts::Permissions`. -- **`ruby` deleted** from `StyledContent` — ISOSTS defines no such element. +- **`ruby` deleted** from `StyledContent` — ISOSTS's `styled-content` content + model omits `` (NISO permits it transitively via the emphasis group). Behaviour change: `` in `` no longer round-trips. - **13 element classes** modelled from ISOSTS.xsd: `Year`, `PubDate`, `ReleaseVersionId`, `IsProof`, `AltText`, `LongDesc`, `TexMath`, `PubId`, @@ -114,6 +115,7 @@ reaches `TbxIsoTml`/MathML. Those are shared namespaces, outside this ADR. ## Verification 1. `grep -rho "Sts::NisoSts::" lib/sts/iso_sts/ | wc -l` → `20` -2. Every attribute on an IsoSts model traces to a line in `ISOSTS.xsd` +2. Every *non-`@id`* attribute on an IsoSts model traces to a line in + `ISOSTS.xsd`; `@id` follows the 86948b9 convention 3. Autoload registry 1:1 with the directory (112/112) 4. `bundle exec rspec` green; `bundle exec rubocop` clean diff --git a/lib/sts/iso_sts/doc_number.rb b/lib/sts/iso_sts/doc_number.rb index 9237d6b..8b61532 100644 --- a/lib/sts/iso_sts/doc_number.rb +++ b/lib/sts/iso_sts/doc_number.rb @@ -2,14 +2,17 @@ module Sts module IsoSts - # ISOSTS declares as type="xs:string" -- no attributes. + # ISOSTS declares as type="xs:string". The @id follows the + # NisoSts convention established in 86948b9, not ISOSTS itself. class DocNumber < Lutaml::Model::Serializable attribute :content, :string + attribute :id, :string xml do element "doc-number" map_content to: :content + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/doc_type.rb b/lib/sts/iso_sts/doc_type.rb index a729ab0..28fd5d6 100644 --- a/lib/sts/iso_sts/doc_type.rb +++ b/lib/sts/iso_sts/doc_type.rb @@ -2,14 +2,17 @@ module Sts module IsoSts - # ISOSTS declares as type="xs:string" -- no attributes. + # ISOSTS declares as type="xs:string". The @id follows the + # NisoSts convention established in 86948b9, not ISOSTS itself. class DocType < Lutaml::Model::Serializable attribute :content, :string + attribute :id, :string xml do element "doc-type" map_content to: :content + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/fpage.rb b/lib/sts/iso_sts/fpage.rb index de76153..7c8021c 100644 --- a/lib/sts/iso_sts/fpage.rb +++ b/lib/sts/iso_sts/fpage.rb @@ -8,6 +8,7 @@ class Fpage < Lutaml::Model::Serializable attribute :seq, :string attribute :specific_use, :string attribute :xml_lang, :string + attribute :id, :string xml do element "fpage" @@ -17,6 +18,7 @@ class Fpage < Lutaml::Model::Serializable map_attribute "seq", to: :seq map_attribute "specific-use", to: :specific_use map_attribute "xml:lang", to: :xml_lang + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/ics.rb b/lib/sts/iso_sts/ics.rb index acaa863..3ede99f 100644 --- a/lib/sts/iso_sts/ics.rb +++ b/lib/sts/iso_sts/ics.rb @@ -2,14 +2,17 @@ module Sts module IsoSts - # ISOSTS declares as type="xs:string" -- no attributes. + # ISOSTS declares as type="xs:string". The @id follows the + # NisoSts convention established in 86948b9, not ISOSTS itself. class Ics < Lutaml::Model::Serializable attribute :content, :string + attribute :id, :string xml do element "ics" map_content to: :content + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/is_proof.rb b/lib/sts/iso_sts/is_proof.rb index 2835330..98051c1 100644 --- a/lib/sts/iso_sts/is_proof.rb +++ b/lib/sts/iso_sts/is_proof.rb @@ -2,11 +2,16 @@ module Sts module IsoSts - # ISOSTS declares as an empty complexType: its presence is the - # whole signal -- no attributes, no content. + # ISOSTS declares as an empty complexType: its presence carries + # no content. The @id follows the NisoSts convention established in 86948b9, + # not ISOSTS itself. class IsProof < Lutaml::Model::Serializable + attribute :id, :string + xml do element "is-proof" + + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/issue.rb b/lib/sts/iso_sts/issue.rb index be0b74e..2d52c19 100644 --- a/lib/sts/iso_sts/issue.rb +++ b/lib/sts/iso_sts/issue.rb @@ -8,6 +8,7 @@ class Issue < Lutaml::Model::Serializable attribute :seq, :string attribute :specific_use, :string attribute :xml_lang, :string + attribute :id, :string xml do element "issue" @@ -17,6 +18,7 @@ class Issue < Lutaml::Model::Serializable map_attribute "seq", to: :seq map_attribute "specific-use", to: :specific_use map_attribute "xml:lang", to: :xml_lang + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/lpage.rb b/lib/sts/iso_sts/lpage.rb index 870f153..e0cd813 100644 --- a/lib/sts/iso_sts/lpage.rb +++ b/lib/sts/iso_sts/lpage.rb @@ -7,6 +7,7 @@ class Lpage < Lutaml::Model::Serializable attribute :content_type, :string attribute :specific_use, :string attribute :xml_lang, :string + attribute :id, :string xml do element "lpage" @@ -15,6 +16,7 @@ class Lpage < Lutaml::Model::Serializable map_attribute "content-type", to: :content_type map_attribute "specific-use", to: :specific_use map_attribute "xml:lang", to: :xml_lang + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/originator.rb b/lib/sts/iso_sts/originator.rb index 21d32c9..2287daf 100644 --- a/lib/sts/iso_sts/originator.rb +++ b/lib/sts/iso_sts/originator.rb @@ -2,14 +2,17 @@ module Sts module IsoSts - # ISOSTS declares as type="xs:string" -- no attributes. + # ISOSTS declares as type="xs:string". The @id follows the + # NisoSts convention established in 86948b9, not ISOSTS itself. class Originator < Lutaml::Model::Serializable attribute :content, :string + attribute :id, :string xml do element "originator" map_content to: :content + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/page_range.rb b/lib/sts/iso_sts/page_range.rb index e02d62e..e84f21a 100644 --- a/lib/sts/iso_sts/page_range.rb +++ b/lib/sts/iso_sts/page_range.rb @@ -7,6 +7,7 @@ class PageRange < Lutaml::Model::Serializable attribute :content_type, :string attribute :specific_use, :string attribute :xml_lang, :string + attribute :id, :string xml do element "page-range" @@ -15,6 +16,7 @@ class PageRange < Lutaml::Model::Serializable map_attribute "content-type", to: :content_type map_attribute "specific-use", to: :specific_use map_attribute "xml:lang", to: :xml_lang + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/part_number.rb b/lib/sts/iso_sts/part_number.rb index d86ee78..b6d4664 100644 --- a/lib/sts/iso_sts/part_number.rb +++ b/lib/sts/iso_sts/part_number.rb @@ -2,14 +2,17 @@ module Sts module IsoSts - # ISOSTS declares as type="xs:string" -- no attributes. + # ISOSTS declares as type="xs:string". The @id follows the + # NisoSts convention established in 86948b9, not ISOSTS itself. class PartNumber < Lutaml::Model::Serializable attribute :content, :string + attribute :id, :string xml do element "part-number" map_content to: :content + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/proj_id.rb b/lib/sts/iso_sts/proj_id.rb index a34ab85..044bd4c 100644 --- a/lib/sts/iso_sts/proj_id.rb +++ b/lib/sts/iso_sts/proj_id.rb @@ -2,14 +2,17 @@ module Sts module IsoSts - # ISOSTS declares as type="xs:string" -- no attributes. + # ISOSTS declares as type="xs:string". The @id follows the + # NisoSts convention established in 86948b9, not ISOSTS itself. class ProjId < Lutaml::Model::Serializable attribute :content, :string + attribute :id, :string xml do element "proj-id" map_content to: :content + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/pub_date.rb b/lib/sts/iso_sts/pub_date.rb index 8a1c429..857a9e9 100644 --- a/lib/sts/iso_sts/pub_date.rb +++ b/lib/sts/iso_sts/pub_date.rb @@ -5,12 +5,14 @@ module IsoSts class PubDate < Lutaml::Model::Serializable attribute :content, :string attribute :pub_type, :string + attribute :id, :string xml do element "pub-date" map_content to: :content map_attribute "pub-type", to: :pub_type + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/pub_id.rb b/lib/sts/iso_sts/pub_id.rb index e12f808..7eceb72 100644 --- a/lib/sts/iso_sts/pub_id.rb +++ b/lib/sts/iso_sts/pub_id.rb @@ -6,6 +6,7 @@ class PubId < Lutaml::Model::Serializable attribute :content, :string attribute :pub_id_type, :string attribute :specific_use, :string + attribute :id, :string xml do element "pub-id" @@ -13,6 +14,7 @@ class PubId < Lutaml::Model::Serializable map_content to: :content map_attribute "pub-id-type", to: :pub_id_type map_attribute "specific-use", to: :specific_use + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/release_version.rb b/lib/sts/iso_sts/release_version.rb index ff06313..075f49f 100644 --- a/lib/sts/iso_sts/release_version.rb +++ b/lib/sts/iso_sts/release_version.rb @@ -2,14 +2,17 @@ module Sts module IsoSts - # ISOSTS declares as type="xs:string" -- no attributes. + # ISOSTS declares as type="xs:string". The @id follows the + # NisoSts convention established in 86948b9, not ISOSTS itself. class ReleaseVersion < Lutaml::Model::Serializable attribute :content, :string + attribute :id, :string xml do element "release-version" map_content to: :content + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/sdo.rb b/lib/sts/iso_sts/sdo.rb index 54285d3..e4154cb 100644 --- a/lib/sts/iso_sts/sdo.rb +++ b/lib/sts/iso_sts/sdo.rb @@ -2,14 +2,17 @@ module Sts module IsoSts - # ISOSTS declares as type="xs:string" -- no attributes. + # ISOSTS declares as type="xs:string". The @id follows the + # NisoSts convention established in 86948b9, not ISOSTS itself. class Sdo < Lutaml::Model::Serializable attribute :content, :string + attribute :id, :string xml do element "sdo" map_content to: :content + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/secretariat.rb b/lib/sts/iso_sts/secretariat.rb index 3dbd3a7..630308b 100644 --- a/lib/sts/iso_sts/secretariat.rb +++ b/lib/sts/iso_sts/secretariat.rb @@ -2,14 +2,17 @@ module Sts module IsoSts - # ISOSTS declares as type="xs:string" -- no attributes. + # ISOSTS declares as type="xs:string". The @id follows the + # NisoSts convention established in 86948b9, not ISOSTS itself. class Secretariat < Lutaml::Model::Serializable attribute :content, :string + attribute :id, :string xml do element "secretariat" map_content to: :content + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/suppl_number.rb b/lib/sts/iso_sts/suppl_number.rb index 98f156d..6057b71 100644 --- a/lib/sts/iso_sts/suppl_number.rb +++ b/lib/sts/iso_sts/suppl_number.rb @@ -2,14 +2,17 @@ module Sts module IsoSts - # ISOSTS declares as type="xs:string" -- no attributes. + # ISOSTS declares as type="xs:string". The @id follows the + # NisoSts convention established in 86948b9, not ISOSTS itself. class SupplNumber < Lutaml::Model::Serializable attribute :content, :string + attribute :id, :string xml do element "suppl-number" map_content to: :content + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/suppl_type.rb b/lib/sts/iso_sts/suppl_type.rb index 0d83ea5..b792eeb 100644 --- a/lib/sts/iso_sts/suppl_type.rb +++ b/lib/sts/iso_sts/suppl_type.rb @@ -2,14 +2,17 @@ module Sts module IsoSts - # ISOSTS declares as type="xs:string" -- no attributes. + # ISOSTS declares as type="xs:string". The @id follows the + # NisoSts convention established in 86948b9, not ISOSTS itself. class SupplType < Lutaml::Model::Serializable attribute :content, :string + attribute :id, :string xml do element "suppl-type" map_content to: :content + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/suppl_version.rb b/lib/sts/iso_sts/suppl_version.rb index 997b837..59700d0 100644 --- a/lib/sts/iso_sts/suppl_version.rb +++ b/lib/sts/iso_sts/suppl_version.rb @@ -2,14 +2,17 @@ module Sts module IsoSts - # ISOSTS declares as type="xs:string" -- no attributes. + # ISOSTS declares as type="xs:string". The @id follows the + # NisoSts convention established in 86948b9, not ISOSTS itself. class SupplVersion < Lutaml::Model::Serializable attribute :content, :string + attribute :id, :string xml do element "suppl-version" map_content to: :content + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/urn.rb b/lib/sts/iso_sts/urn.rb index 09f2fcc..4dc65f3 100644 --- a/lib/sts/iso_sts/urn.rb +++ b/lib/sts/iso_sts/urn.rb @@ -2,14 +2,17 @@ module Sts module IsoSts - # ISOSTS declares as type="xs:string" -- no attributes. + # ISOSTS declares as type="xs:string". The @id follows the + # NisoSts convention established in 86948b9, not ISOSTS itself. class Urn < Lutaml::Model::Serializable attribute :content, :string + attribute :id, :string xml do element "urn" map_content to: :content + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/version.rb b/lib/sts/iso_sts/version.rb index 9c11fc7..9aea1d1 100644 --- a/lib/sts/iso_sts/version.rb +++ b/lib/sts/iso_sts/version.rb @@ -2,14 +2,17 @@ module Sts module IsoSts - # ISOSTS declares as type="xs:string" -- no attributes. + # ISOSTS declares as type="xs:string". The @id follows the + # NisoSts convention established in 86948b9, not ISOSTS itself. class Version < Lutaml::Model::Serializable attribute :content, :string + attribute :id, :string xml do element "version" map_content to: :content + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/volume.rb b/lib/sts/iso_sts/volume.rb index 47e5d6a..fe3105a 100644 --- a/lib/sts/iso_sts/volume.rb +++ b/lib/sts/iso_sts/volume.rb @@ -8,6 +8,7 @@ class Volume < Lutaml::Model::Serializable attribute :seq, :string attribute :specific_use, :string attribute :xml_lang, :string + attribute :id, :string xml do element "volume" @@ -17,6 +18,7 @@ class Volume < Lutaml::Model::Serializable map_attribute "seq", to: :seq map_attribute "specific-use", to: :specific_use map_attribute "xml:lang", to: :xml_lang + map_attribute "id", to: :id end end end diff --git a/lib/sts/iso_sts/year.rb b/lib/sts/iso_sts/year.rb index 736bb4d..d44dd64 100644 --- a/lib/sts/iso_sts/year.rb +++ b/lib/sts/iso_sts/year.rb @@ -7,6 +7,7 @@ class Year < Lutaml::Model::Serializable attribute :content_type, :string attribute :specific_use, :string attribute :xml_lang, :string + attribute :id, :string xml do element "year" @@ -15,6 +16,7 @@ class Year < Lutaml::Model::Serializable map_attribute "content-type", to: :content_type map_attribute "specific-use", to: :specific_use map_attribute "xml:lang", to: :xml_lang + map_attribute "id", to: :id end end end diff --git a/spec/iso_sts/iso_sts_element_spec.rb b/spec/iso_sts/iso_sts_element_spec.rb index 4e8c4b2..977dd0d 100644 --- a/spec/iso_sts/iso_sts_element_spec.rb +++ b/spec/iso_sts/iso_sts_element_spec.rb @@ -428,10 +428,10 @@ end end - # ISOSTS declares these elements as type="xs:string": text content, no - # attributes. They are modelled as IsoSts classes rather than plain strings - # because an empty element () does not survive a :string round-trip -- - # for a collection it parses to [], losing the element entirely. + # ISOSTS declares these elements as type="xs:string": text content, plus the + # conventional @id (86948b9). They are modelled as IsoSts classes rather than + # plain strings because an empty element () does not survive a :string + # round-trip -- for a collection it parses to [], losing the element entirely. describe "ISOSTS xs:string elements" do { Originator: "originator", @@ -454,9 +454,9 @@ expect(model.mappings_for(:xml).root_element).to eq(element) end - it "IsoSts::#{klass} has only content, as ISOSTS defines no attributes" do + it "IsoSts::#{klass} models content plus the conventional @id" do model = described_class.const_get(klass) - expect(model.attributes.keys).to eq(%i[content]) + expect(model.attributes.keys).to eq(%i[content id]) end it "IsoSts::#{klass} round-trips an empty <#{element}/>" do @@ -497,34 +497,31 @@ end end - # Attribute sets below are transcribed from ISOSTS.xsd. Round-tripping does - # not prove them: a model that invents or drops an attribute still parses and - # serialises symmetrically. Asserting the exact set is what catches that. + # Non-@id attribute sets below are transcribed from ISOSTS.xsd; the @id, + # where present, follows the 86948b9 convention. Round-tripping does not prove + # them: a model that invents or drops an attribute still parses and serialises + # symmetrically. Asserting the exact set is what catches that. describe "ISOSTS-modelled element classes" do { - Year: %i[content content_type specific_use xml_lang], - PubDate: %i[content pub_type], + Year: %i[content content_type specific_use xml_lang id], + PubDate: %i[content pub_type id], ReleaseVersionId: %i[content id], AltText: %i[content id content_type specific_use xml_lang], LongDesc: %i[content id content_type specific_use xml_lang], TexMath: %i[content id content_type notation version], - PubId: %i[content pub_id_type specific_use], - Volume: %i[content content_type seq specific_use xml_lang], - Issue: %i[content content_type seq specific_use xml_lang], - Fpage: %i[content content_type seq specific_use xml_lang], - Lpage: %i[content content_type specific_use xml_lang], - PageRange: %i[content content_type specific_use xml_lang], + PubId: %i[content pub_id_type specific_use id], + Volume: %i[content content_type seq specific_use xml_lang id], + Issue: %i[content content_type seq specific_use xml_lang id], + Fpage: %i[content content_type seq specific_use xml_lang id], + Lpage: %i[content content_type specific_use xml_lang id], + PageRange: %i[content content_type specific_use xml_lang id], }.each do |klass, expected| - it "IsoSts::#{klass} models exactly the ISOSTS attribute set" do + it "IsoSts::#{klass} models its configured attribute set" do expect(described_class.const_get(klass).attributes.keys) .to match_array(expected) end end - it "IsoSts::Year has no id, which ISOSTS does not define" do - expect(described_class::Year.attributes).not_to have_key(:id) - end - it "IsoSts::Fpage carries seq but IsoSts::Lpage does not" do expect(described_class::Fpage.attributes).to have_key(:seq) expect(described_class::Lpage.attributes).not_to have_key(:seq) @@ -535,8 +532,8 @@ expect(described_class::Fpage.attributes).not_to have_key(:italic) end - it "IsoSts::IsProof is an empty element" do - expect(described_class::IsProof.attributes).to be_empty + it "IsoSts::IsProof carries only @id and has no content" do + expect(described_class::IsProof.attributes.keys).to eq(%i[id]) round_tripped = described_class::IsProof .to_xml(described_class::IsProof.new).strip expect(round_tripped).to eq("") @@ -606,4 +603,29 @@ .to eq(described_class::WiNumber) end end + + # A model can declare `attribute :id` yet omit `map_attribute "id"`, which + # still satisfies the key-set assertions above while silently dropping @id on + # parse and omitting it on serialise. Every fixture is @id-free, so only an + # explicit round-trip proves the mapping exists. + describe "@id round-trips through parse and serialise" do + %i[ + DocNumber DocType Fpage Ics IsProof Issue Lpage Originator PageRange + PartNumber ProjId PubDate PubId ReleaseVersion Sdo SupplNumber SupplType + SupplVersion Urn Version Volume Year Secretariat + ].each do |klass| + it "IsoSts::#{klass} preserves @id through a round-trip" do + model = described_class.const_get(klass) + element = model.mappings_for(:xml).root_element + xml = if klass == :IsProof + %(<#{element} id="x-1"/>) + else + %(<#{element} id="x-1">content) + end + parsed = model.from_xml(xml) + expect(parsed.id).to eq("x-1") + expect(model.to_xml(parsed)).to include('id="x-1"') + end + end + end end From 07264c8b032acc10eb30a6da251395d8b9cbf796 Mon Sep 17 00:00:00 2001 From: HassanAkbar Date: Wed, 22 Jul 2026 13:33:30 +0500 Subject: [PATCH 2/3] add dropped ISOSTS attributes and fix xml:lang mappings --- TODO.sts-refactor/00-overview.md | 20 ++-- lib/sts/iso_sts/body.rb | 2 + lib/sts/iso_sts/copyright_holder.rb | 6 ++ lib/sts/iso_sts/copyright_statement.rb | 6 ++ lib/sts/iso_sts/copyright_year.rb | 4 + lib/sts/iso_sts/edition.rb | 6 ++ lib/sts/iso_sts/ext_link.rb | 10 ++ lib/sts/iso_sts/graphic.rb | 10 ++ lib/sts/iso_sts/label.rb | 4 + lib/sts/iso_sts/mixed_citation.rb | 14 ++- lib/sts/iso_sts/named_content.rb | 2 + lib/sts/iso_sts/sec.rb | 2 +- lib/sts/iso_sts/standard.rb | 4 +- lib/sts/iso_sts/sub.rb | 4 + lib/sts/iso_sts/sup.rb | 4 + lib/sts/iso_sts/term_sec.rb | 2 +- lib/sts/iso_sts/title.rb | 4 + lib/sts/iso_sts/underline.rb | 2 + lib/sts/iso_sts/uri.rb | 2 + spec/iso_sts/iso_sts_element_spec.rb | 141 ++++++++++++++++++++++++- 20 files changed, 233 insertions(+), 16 deletions(-) diff --git a/TODO.sts-refactor/00-overview.md b/TODO.sts-refactor/00-overview.md index 5d148c0..88d8e0e 100644 --- a/TODO.sts-refactor/00-overview.md +++ b/TODO.sts-refactor/00-overview.md @@ -172,18 +172,24 @@ grep -r "method_missing|respond_to_missing|Object.const_get|\.send" lib/ - `04-register-versioning.md` — Version the models via lutaml-model Registers - `11-duplicate-models.md` — 44 overlapping element resolution (depends on 03) -### Known bugs — schema conformance (separate from 03) +### Schema conformance — dropped ISOSTS attributes The 2026-05-07 "@id added to all models (XSD-verified)" pass verified against the **NISO** XSD and applied the result to IsoSts. -- **Real bug — dropped ISOSTS attributes.** Some IsoSts models silently drop - ISOSTS attributes that exist in real documents — real data loss. `sub`/`sup` - lose `arrange`+`specific-use`; `ext-link` loses its `xlink:*` set; - `mixed-citation` loses several; `graphic`, `copyright-*`, `edition`, `title`, - `label`, `uri`, `named-content`, `underline`, `meta-date`, `body` lose - others. Regenerate the exact `name=`+`ref=` table when filing the issue. +- **Fixed — dropped ISOSTS attributes restored.** 15 IsoSts models were + silently dropping ISOSTS attributes present in real documents (data loss): + `sub`/`sup` (`arrange`+`specific-use`), `ext-link`/`mixed-citation`/`uri`/ + `named-content` (`xlink:*`), `graphic` (`xlink:*`+`originator`), `copyright-*`, + `edition`, `title`, `label`, `underline`, `body`. This change adds them and + fixes `sec`/`standard`/`term-sec`, which mapped `xml:lang` under the wrong XML + name. (`meta-date` was NOT lossy — it already maps `type`.) - **Not a bug — the conventional `@id`.** IsoSts models carry an `@id` that ISOSTS does not itself define; this is the deliberate 86948b9 convention (NISO-XSD-verified), not dead surface. +- **Remaining follow-up (separate change).** 7 spurious attributes on 5 models + (model carries an attr ISOSTS does not define: `graphic` `type`, `body`/`back` + `content-type`, `content-language`/`language` extras), and the 6 classes left + without `@id` in 86948b9 (`monospace`, `sc`, `strike`, `underline`, `uri`, + `standard_ref`). ## Next Action Two independent tracks: diff --git a/lib/sts/iso_sts/body.rb b/lib/sts/iso_sts/body.rb index 806deb4..780b842 100644 --- a/lib/sts/iso_sts/body.rb +++ b/lib/sts/iso_sts/body.rb @@ -5,6 +5,7 @@ module IsoSts class Body < Lutaml::Model::Serializable attribute :id, :string attribute :content_type, :string + attribute :specific_use, :string attribute :paragraph, ::Sts::IsoSts::Paragraph, collection: true attribute :sec, ::Sts::IsoSts::Sec, collection: true attribute :term_sec, ::Sts::IsoSts::TermSec, collection: true @@ -31,6 +32,7 @@ class Body < Lutaml::Model::Serializable map_attribute "id", to: :id map_attribute "content-type", to: :content_type + map_attribute "specific-use", to: :specific_use map_element "p", to: :paragraph map_element "sec", to: :sec diff --git a/lib/sts/iso_sts/copyright_holder.rb b/lib/sts/iso_sts/copyright_holder.rb index edb2c7c..24cbdac 100644 --- a/lib/sts/iso_sts/copyright_holder.rb +++ b/lib/sts/iso_sts/copyright_holder.rb @@ -4,6 +4,9 @@ module Sts module IsoSts class CopyrightHolder < Lutaml::Model::Serializable attribute :id, :string + attribute :content_type, :string + attribute :specific_use, :string + attribute :xml_lang, :string attribute :content, :string, collection: true attribute :sub, ::Sts::IsoSts::Sub attribute :sup, ::Sts::IsoSts::Sup @@ -11,6 +14,9 @@ class CopyrightHolder < Lutaml::Model::Serializable xml do element "copyright-holder" map_attribute "id", to: :id + map_attribute "content-type", to: :content_type + map_attribute "specific-use", to: :specific_use + map_attribute "xml:lang", to: :xml_lang mixed_content map_content to: :content diff --git a/lib/sts/iso_sts/copyright_statement.rb b/lib/sts/iso_sts/copyright_statement.rb index 574a4b5..c0bf501 100644 --- a/lib/sts/iso_sts/copyright_statement.rb +++ b/lib/sts/iso_sts/copyright_statement.rb @@ -4,6 +4,9 @@ module Sts module IsoSts class CopyrightStatement < Lutaml::Model::Serializable attribute :id, :string + attribute :content_type, :string + attribute :specific_use, :string + attribute :xml_lang, :string attribute :content, :string, collection: true attribute :bold, ::Sts::IsoSts::Bold attribute :italic, ::Sts::IsoSts::Italic @@ -13,6 +16,9 @@ class CopyrightStatement < Lutaml::Model::Serializable xml do element "copyright-statement" map_attribute "id", to: :id + map_attribute "content-type", to: :content_type + map_attribute "specific-use", to: :specific_use + map_attribute "xml:lang", to: :xml_lang mixed_content map_content to: :content diff --git a/lib/sts/iso_sts/copyright_year.rb b/lib/sts/iso_sts/copyright_year.rb index f732a52..7191047 100644 --- a/lib/sts/iso_sts/copyright_year.rb +++ b/lib/sts/iso_sts/copyright_year.rb @@ -4,12 +4,16 @@ module Sts module IsoSts class CopyrightYear < Lutaml::Model::Serializable attribute :id, :string + attribute :content_type, :string + attribute :specific_use, :string attribute :content, :string xml do element "copyright-year" map_attribute "id", to: :id + map_attribute "content-type", to: :content_type + map_attribute "specific-use", to: :specific_use map_content to: :content end diff --git a/lib/sts/iso_sts/edition.rb b/lib/sts/iso_sts/edition.rb index b9d9a89..d5d9b40 100644 --- a/lib/sts/iso_sts/edition.rb +++ b/lib/sts/iso_sts/edition.rb @@ -4,10 +4,16 @@ module Sts module IsoSts class Edition < Lutaml::Model::Serializable attribute :id, :string + attribute :content_type, :string + attribute :specific_use, :string + attribute :xml_lang, :string attribute :content, :string, collection: true xml do element "edition" map_attribute "id", to: :id + map_attribute "content-type", to: :content_type + map_attribute "specific-use", to: :specific_use + map_attribute "xml:lang", to: :xml_lang mixed_content map_content to: :content diff --git a/lib/sts/iso_sts/ext_link.rb b/lib/sts/iso_sts/ext_link.rb index b968d0b..4f03f3b 100644 --- a/lib/sts/iso_sts/ext_link.rb +++ b/lib/sts/iso_sts/ext_link.rb @@ -8,6 +8,11 @@ class ExtLink < Lutaml::Model::Serializable attribute :specific_use, :string attribute :xml_lang, :string attribute :xlink_href, :string + attribute :xlink_type, :string + attribute :xlink_role, :string + attribute :xlink_title, :string + attribute :xlink_show, :string + attribute :xlink_actuate, :string attribute :content, :string attribute :bold, ::Sts::IsoSts::Bold attribute :italic, ::Sts::IsoSts::Italic @@ -21,6 +26,11 @@ class ExtLink < Lutaml::Model::Serializable map_attribute "specific-use", to: :specific_use map_attribute "xml:lang", to: :xml_lang map_attribute "xlink:href", to: :xlink_href + map_attribute "xlink:type", to: :xlink_type + map_attribute "xlink:role", to: :xlink_role + map_attribute "xlink:title", to: :xlink_title + map_attribute "xlink:show", to: :xlink_show + map_attribute "xlink:actuate", to: :xlink_actuate map_content to: :content diff --git a/lib/sts/iso_sts/graphic.rb b/lib/sts/iso_sts/graphic.rb index bb24037..3b2bccb 100644 --- a/lib/sts/iso_sts/graphic.rb +++ b/lib/sts/iso_sts/graphic.rb @@ -13,7 +13,12 @@ class Graphic < Lutaml::Model::Serializable attribute :mime_subtype, :string attribute :xlink_href, :string attribute :xlink_type, :string + attribute :xlink_role, :string + attribute :xlink_title, :string + attribute :xlink_show, :string + attribute :xlink_actuate, :string attribute :graphic_type, :string + attribute :originator, :string attribute :label, ::Sts::IsoSts::Label attribute :caption, ::Sts::IsoSts::Caption attribute :alt_text, ::Sts::IsoSts::AltText @@ -33,7 +38,12 @@ class Graphic < Lutaml::Model::Serializable map_attribute "mime-subtype", to: :mime_subtype map_attribute "xlink:href", to: :xlink_href map_attribute "xlink:type", to: :xlink_type + map_attribute "xlink:role", to: :xlink_role + map_attribute "xlink:title", to: :xlink_title + map_attribute "xlink:show", to: :xlink_show + map_attribute "xlink:actuate", to: :xlink_actuate map_attribute "type", to: :graphic_type + map_attribute "originator", to: :originator map_element "label", to: :label map_element "caption", to: :caption diff --git a/lib/sts/iso_sts/label.rb b/lib/sts/iso_sts/label.rb index 1372881..aeffc3d 100644 --- a/lib/sts/iso_sts/label.rb +++ b/lib/sts/iso_sts/label.rb @@ -4,6 +4,8 @@ module Sts module IsoSts class Label < Lutaml::Model::Serializable attribute :id, :string + attribute :alt, :string + attribute :xml_lang, :string attribute :content, :string, collection: true attribute :bold, ::Sts::IsoSts::Bold attribute :italic, ::Sts::IsoSts::Italic @@ -19,6 +21,8 @@ class Label < Lutaml::Model::Serializable xml do element "label" map_attribute "id", to: :id + map_attribute "alt", to: :alt + map_attribute "xml:lang", to: :xml_lang mixed_content map_content to: :content diff --git a/lib/sts/iso_sts/mixed_citation.rb b/lib/sts/iso_sts/mixed_citation.rb index 74f67aa..bc0e99a 100644 --- a/lib/sts/iso_sts/mixed_citation.rb +++ b/lib/sts/iso_sts/mixed_citation.rb @@ -9,6 +9,12 @@ class MixedCitation < Lutaml::Model::Serializable attribute :publication_format, :string attribute :specific_use, :string attribute :xml_lang, :string + attribute :xlink_type, :string + attribute :xlink_href, :string + attribute :xlink_role, :string + attribute :xlink_title, :string + attribute :xlink_show, :string + attribute :xlink_actuate, :string attribute :content, :string, collection: true attribute :bold, ::Sts::IsoSts::Bold, collection: true attribute :italic, ::Sts::IsoSts::Italic, collection: true @@ -35,7 +41,7 @@ class MixedCitation < Lutaml::Model::Serializable attribute :publisher, ::Sts::NisoSts::Publisher attribute :pub_id, ::Sts::IsoSts::PubId, collection: true - xml do + xml do # rubocop:disable Metrics/BlockLength element "mixed-citation" mixed_content @@ -45,6 +51,12 @@ class MixedCitation < Lutaml::Model::Serializable map_attribute "publication-format", to: :publication_format map_attribute "specific-use", to: :specific_use map_attribute "xml:lang", to: :xml_lang + map_attribute "xlink:type", to: :xlink_type + map_attribute "xlink:href", to: :xlink_href + map_attribute "xlink:role", to: :xlink_role + map_attribute "xlink:title", to: :xlink_title + map_attribute "xlink:show", to: :xlink_show + map_attribute "xlink:actuate", to: :xlink_actuate map_content to: :content diff --git a/lib/sts/iso_sts/named_content.rb b/lib/sts/iso_sts/named_content.rb index b7f3785..7341a92 100644 --- a/lib/sts/iso_sts/named_content.rb +++ b/lib/sts/iso_sts/named_content.rb @@ -9,6 +9,7 @@ class NamedContent < Lutaml::Model::Serializable attribute :specific_use, :string attribute :xml_lang, :string attribute :xlink_href, :string + attribute :xlink_type, :string attribute :xlink_role, :string attribute :xlink_title, :string attribute :xlink_show, :string @@ -29,6 +30,7 @@ class NamedContent < Lutaml::Model::Serializable map_attribute "specific-use", to: :specific_use map_attribute "xml:lang", to: :xml_lang map_attribute "xlink:href", to: :xlink_href + map_attribute "xlink:type", to: :xlink_type map_attribute "xlink:role", to: :xlink_role map_attribute "xlink:title", to: :xlink_title map_attribute "xlink:show", to: :xlink_show diff --git a/lib/sts/iso_sts/sec.rb b/lib/sts/iso_sts/sec.rb index b17fc8a..0640612 100644 --- a/lib/sts/iso_sts/sec.rb +++ b/lib/sts/iso_sts/sec.rb @@ -44,7 +44,7 @@ class Sec < Lutaml::Model::Serializable ordered map_attribute "id", to: :id - map_attribute "lang", to: :xml_lang + map_attribute "xml:lang", to: :xml_lang map_attribute "sec-type", to: :sec_type map_attribute "specific-use", to: :specific_use map_attribute "originator", to: :originator diff --git a/lib/sts/iso_sts/standard.rb b/lib/sts/iso_sts/standard.rb index 48a7554..d013538 100644 --- a/lib/sts/iso_sts/standard.rb +++ b/lib/sts/iso_sts/standard.rb @@ -4,7 +4,7 @@ module Sts module IsoSts class Standard < Lutaml::Model::Serializable attribute :id, :string - attribute :lang, :string + attribute :xml_lang, :string attribute :dtd_version, :string attribute :specific_use, :string attribute :front, ::Sts::IsoSts::Front @@ -20,7 +20,7 @@ class Standard < Lutaml::Model::Serializable ::Lutaml::Xml::W3c::XlinkNamespace, ] map_attribute "id", to: :id - map_attribute "lang", to: :lang + map_attribute "xml:lang", to: :xml_lang map_attribute "dtd-version", to: :dtd_version map_attribute "specific-use", to: :specific_use diff --git a/lib/sts/iso_sts/sub.rb b/lib/sts/iso_sts/sub.rb index 49825af..ceb0a83 100644 --- a/lib/sts/iso_sts/sub.rb +++ b/lib/sts/iso_sts/sub.rb @@ -4,10 +4,14 @@ module Sts module IsoSts class Sub < Lutaml::Model::Serializable attribute :id, :string + attribute :arrange, :string + attribute :specific_use, :string attribute :content, :string, collection: true xml do element "sub" map_attribute "id", to: :id + map_attribute "arrange", to: :arrange + map_attribute "specific-use", to: :specific_use mixed_content map_content to: :content diff --git a/lib/sts/iso_sts/sup.rb b/lib/sts/iso_sts/sup.rb index f77ac39..c9aa05e 100644 --- a/lib/sts/iso_sts/sup.rb +++ b/lib/sts/iso_sts/sup.rb @@ -4,10 +4,14 @@ module Sts module IsoSts class Sup < Lutaml::Model::Serializable attribute :id, :string + attribute :arrange, :string + attribute :specific_use, :string attribute :content, :string, collection: true xml do element "sup" map_attribute "id", to: :id + map_attribute "arrange", to: :arrange + map_attribute "specific-use", to: :specific_use mixed_content map_content to: :content diff --git a/lib/sts/iso_sts/term_sec.rb b/lib/sts/iso_sts/term_sec.rb index d872843..258a23d 100644 --- a/lib/sts/iso_sts/term_sec.rb +++ b/lib/sts/iso_sts/term_sec.rb @@ -27,7 +27,7 @@ class TermSec < Lutaml::Model::Serializable ordered map_attribute "id", to: :id - map_attribute "lang", to: :xml_lang + map_attribute "xml:lang", to: :xml_lang map_attribute "sec-type", to: :sec_type map_attribute "specific-use", to: :specific_use map_attribute "originator", to: :originator diff --git a/lib/sts/iso_sts/title.rb b/lib/sts/iso_sts/title.rb index d459af0..2ad5fce 100644 --- a/lib/sts/iso_sts/title.rb +++ b/lib/sts/iso_sts/title.rb @@ -4,6 +4,8 @@ module Sts module IsoSts class Title < Lutaml::Model::Serializable attribute :id, :string + attribute :content_type, :string + attribute :specific_use, :string attribute :content, :string, collection: true attribute :bold, ::Sts::IsoSts::Bold, collection: true attribute :italic, ::Sts::IsoSts::Italic, collection: true @@ -20,6 +22,8 @@ class Title < Lutaml::Model::Serializable xml do element "title" map_attribute "id", to: :id + map_attribute "content-type", to: :content_type + map_attribute "specific-use", to: :specific_use mixed_content map_content to: :content diff --git a/lib/sts/iso_sts/underline.rb b/lib/sts/iso_sts/underline.rb index 67145ce..23759f7 100644 --- a/lib/sts/iso_sts/underline.rb +++ b/lib/sts/iso_sts/underline.rb @@ -3,6 +3,7 @@ module Sts module IsoSts class Underline < Lutaml::Model::Serializable + attribute :underline_style, :string attribute :specific_use, :string attribute :content, :string, collection: true attribute :bold, ::Sts::IsoSts::Bold, collection: true @@ -14,6 +15,7 @@ class Underline < Lutaml::Model::Serializable element "underline" mixed_content + map_attribute "underline-style", to: :underline_style map_attribute "specific-use", to: :specific_use map_content to: :content map_element "bold", to: :bold diff --git a/lib/sts/iso_sts/uri.rb b/lib/sts/iso_sts/uri.rb index a7dbe0c..47e28ad 100644 --- a/lib/sts/iso_sts/uri.rb +++ b/lib/sts/iso_sts/uri.rb @@ -7,6 +7,7 @@ class Uri < Lutaml::Model::Serializable attribute :specific_use, :string attribute :xml_lang, :string attribute :xlink_href, :string + attribute :xlink_type, :string attribute :xlink_role, :string attribute :xlink_title, :string attribute :xlink_show, :string @@ -21,6 +22,7 @@ class Uri < Lutaml::Model::Serializable map_attribute "specific-use", to: :specific_use map_attribute "xml:lang", to: :xml_lang map_attribute "xlink:href", to: :xlink_href + map_attribute "xlink:type", to: :xlink_type map_attribute "xlink:role", to: :xlink_role map_attribute "xlink:title", to: :xlink_title map_attribute "xlink:show", to: :xlink_show diff --git a/spec/iso_sts/iso_sts_element_spec.rb b/spec/iso_sts/iso_sts_element_spec.rb index 977dd0d..e8b18b7 100644 --- a/spec/iso_sts/iso_sts_element_spec.rb +++ b/spec/iso_sts/iso_sts_element_spec.rb @@ -428,10 +428,11 @@ end end - # ISOSTS declares these elements as type="xs:string": text content, plus the - # conventional @id (86948b9). They are modelled as IsoSts classes rather than - # plain strings because an empty element () does not survive a :string - # round-trip -- for a collection it parses to [], losing the element entirely. + # ISOSTS declares these elements as type="xs:string" (text content, no + # attributes). The @id they carry is the 86948b9 convention, not declared by + # ISOSTS. They are modelled as IsoSts classes, not plain strings, because an + # empty element () does not survive a :string round-trip -- for a + # collection it parses to [], losing the element entirely. describe "ISOSTS xs:string elements" do { Originator: "originator", @@ -628,4 +629,136 @@ end end end + + # xlink:type is dropped on parse and omitted on serialise when unmapped. A + # round-trip on inline XML proves the mapping exists rather than merely + # asserting an accessor. These four models gained the xlink:type mapping in + # this change (ext-link/mixed-citation/uri also carry it in the fixtures). + describe "xlink:type round-trips through parse and serialise" do + { + ExtLink: "ext-link", + MixedCitation: "mixed-citation", + Uri: "uri", + NamedContent: "named-content", + }.each do |klass, element| + it "IsoSts::#{klass} preserves xlink:type through a round-trip" do + model = described_class.const_get(klass) + xml = %(<#{element} xlink:type="simple">text) + parsed = model.from_xml(xml) + expect(parsed.xlink_type).to eq("simple") + expect(model.to_xml(parsed)).to include('xlink:type="simple"') + end + end + end + + # These three previously mapped ISOSTS xml:lang under the wrong XML name (bare + # lang), losing xml:lang on parse and emitting bare lang on serialise. This + # round-trip guards the corrected mapping; exact-set key assertions cannot, + # because all three expose :xml_lang either way. + describe "xml:lang round-trips through parse and serialise" do + { + Standard: "standard", + Sec: "sec", + TermSec: "term-sec", + }.each do |klass, element| + it "IsoSts::#{klass} preserves xml:lang and emits xml:lang not lang" do + model = described_class.const_get(klass) + xml = %(<#{element} xml:lang="en"/>) + parsed = model.from_xml(xml) + expect(parsed.xml_lang).to eq("en") + serialised = model.to_xml(parsed) + expect(serialised).to include('xml:lang="en"') + expect(serialised).not_to match(/\slang=/) + end + end + end + + # The fixtures never carry these attributes, so a round-trip is the only proof + # the added mappings parse and serialise for their attribute shapes. + describe "ISOSTS attributes absent from fixtures still round-trip" do + it "IsoSts::CopyrightHolder preserves content-type" do + xml = %(ACME) + parsed = described_class::CopyrightHolder.from_xml(xml) + expect(parsed.content_type).to eq("foo") + expect(described_class::CopyrightHolder.to_xml(parsed)) + .to include('content-type="foo"') + end + + it "IsoSts::Label preserves alt" do + xml = %() + parsed = described_class::Label.from_xml(xml) + expect(parsed.alt).to eq("a") + expect(described_class::Label.to_xml(parsed)).to include('alt="a"') + end + + it "IsoSts::Sub preserves arrange" do + xml = %(2) + parsed = described_class::Sub.from_xml(xml) + expect(parsed.arrange).to eq("stack") + expect(described_class::Sub.to_xml(parsed)).to include('arrange="stack"') + end + end + + # Exact attribute-key sets after restoring the ISOSTS attributes these models + # silently dropped. Round-tripping cannot prove the set: a model that invents + # or drops an attribute still parses and serialises symmetrically. Asserting + # the exact set is what catches that. + describe "IsoSts element classes model their full ISOSTS attribute set" do + { + Sub: %i[id arrange specific_use content], + Sup: %i[id arrange specific_use content], + ExtLink: %i[ + id ext_link_type specific_use xml_lang xlink_href xlink_type xlink_role + xlink_title xlink_show xlink_actuate content bold italic named_content + ], + MixedCitation: %i[ + id publication_type publisher_type publication_format specific_use + xml_lang xlink_type xlink_href xlink_role xlink_title xlink_show + xlink_actuate content bold italic sub sup std ext_link uri named_content + styled_content fn xref break person_group collab year source + article_title volume issue fpage lpage page_range publisher pub_id + ], + CopyrightHolder: %i[ + id content_type specific_use xml_lang content sub sup + ], + CopyrightStatement: %i[ + id content_type specific_use xml_lang content bold italic ext_link uri + ], + CopyrightYear: %i[id content_type specific_use content], + Edition: %i[id content_type specific_use xml_lang content], + Title: %i[ + id content_type specific_use content bold italic sub sup xref break + styled_content monospace sc inline_formula std + ], + Label: %i[ + id alt xml_lang content bold italic sub sup inline_formula break + styled_content ext_link uri named_content + ], + Uri: %i[ + content_type specific_use xml_lang xlink_href xlink_type xlink_role + xlink_title xlink_show xlink_actuate content + ], + NamedContent: %i[ + id alt content_type specific_use xml_lang xlink_href xlink_type + xlink_role xlink_title xlink_show xlink_actuate content bold italic + sub sup + ], + Graphic: %i[ + id position orientation specific_use xml_lang content_type mimetype + mime_subtype xlink_href xlink_type xlink_role xlink_title xlink_show + xlink_actuate graphic_type originator label caption alt_text long_desc + ], + Underline: %i[underline_style specific_use content bold italic sub sup], + Body: %i[ + id content_type specific_use paragraph sec term_sec list def_list + disp_formula table_wrap fig non_normative_note non_normative_example + preformat styled_content array ref_list disp_quote editing_instruction + ], + }.each do |klass, expected| + it "IsoSts::#{klass} models its configured attribute set" do + expect(described_class.const_get(klass).attributes.keys) + .to match_array(expected) + end + end + end end From 827cf4e1ce00dfb760698c1a18c1f6bb79fceba9 Mon Sep 17 00:00:00 2001 From: HassanAkbar Date: Wed, 22 Jul 2026 16:28:08 +0500 Subject: [PATCH 3/3] resolve copilot comments --- spec/iso_sts/iso_sts_element_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/iso_sts/iso_sts_element_spec.rb b/spec/iso_sts/iso_sts_element_spec.rb index e8b18b7..666c3e0 100644 --- a/spec/iso_sts/iso_sts_element_spec.rb +++ b/spec/iso_sts/iso_sts_element_spec.rb @@ -643,7 +643,8 @@ }.each do |klass, element| it "IsoSts::#{klass} preserves xlink:type through a round-trip" do model = described_class.const_get(klass) - xml = %(<#{element} xlink:type="simple">text) + ns = 'xmlns:xlink="http://www.w3.org/1999/xlink"' + xml = %(<#{element} #{ns} xlink:type="simple">text) parsed = model.from_xml(xml) expect(parsed.xlink_type).to eq("simple") expect(model.to_xml(parsed)).to include('xlink:type="simple"')