pubid 2.0.0.pre.alpha.12 → 2.0.0.pre.alpha.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +43 -1
  3. data/data/ieee/update_codes.yaml +17 -4
  4. data/data/nist/update_codes.yaml +7 -3
  5. data/lib/pubid/adobe/builder.rb +2 -0
  6. data/lib/pubid/all_parts.rb +201 -0
  7. data/lib/pubid/all_parts_identifier.rb +19 -0
  8. data/lib/pubid/amca/CLAUDE.md +47 -0
  9. data/lib/pubid/amca/builder.rb +3 -5
  10. data/lib/pubid/amca/identifiers/base.rb +10 -0
  11. data/lib/pubid/amca/identifiers/publication.rb +13 -0
  12. data/lib/pubid/amca/parser.rb +2 -1
  13. data/lib/pubid/amca/renderer.rb +22 -33
  14. data/lib/pubid/amca/urn_generator.rb +21 -2
  15. data/lib/pubid/amca/urn_parser.rb +36 -10
  16. data/lib/pubid/ansi/builder.rb +6 -0
  17. data/lib/pubid/api/CLAUDE.md +23 -0
  18. data/lib/pubid/api/builder.rb +2 -0
  19. data/lib/pubid/ashrae/CLAUDE.md +13 -0
  20. data/lib/pubid/ashrae/builder.rb +58 -14
  21. data/lib/pubid/ashrae/identifiers/errata.rb +14 -2
  22. data/lib/pubid/ashrae/identifiers/interpretation.rb +2 -10
  23. data/lib/pubid/ashrae/parser.rb +62 -28
  24. data/lib/pubid/ashrae/renderer.rb +32 -1
  25. data/lib/pubid/ashrae/urn_generator.rb +32 -9
  26. data/lib/pubid/asme/CLAUDE.md +25 -0
  27. data/lib/pubid/asme/builder.rb +16 -9
  28. data/lib/pubid/asme/components/code.rb +2 -0
  29. data/lib/pubid/asme/identifiers/standard.rb +6 -1
  30. data/lib/pubid/asme/parser.rb +41 -14
  31. data/lib/pubid/astm/CLAUDE.md +9 -0
  32. data/lib/pubid/astm/builder.rb +2 -0
  33. data/lib/pubid/astm/components/code.rb +2 -0
  34. data/lib/pubid/astm/parser.rb +4 -1
  35. data/lib/pubid/bipm/CLAUDE.md +11 -0
  36. data/lib/pubid/bipm/builder.rb +2 -0
  37. data/lib/pubid/bsi/CLAUDE.md +93 -0
  38. data/lib/pubid/bsi/builder.rb +13 -11
  39. data/lib/pubid/bsi/identifiers/addendum_document.rb +2 -0
  40. data/lib/pubid/bsi/identifiers/adopted_european_norm.rb +6 -54
  41. data/lib/pubid/bsi/identifiers/adopted_international_standard.rb +5 -22
  42. data/lib/pubid/bsi/identifiers/amendment.rb +36 -12
  43. data/lib/pubid/bsi/identifiers/bundled_identifier.rb +2 -0
  44. data/lib/pubid/bsi/identifiers/consolidated_identifier.rb +23 -26
  45. data/lib/pubid/bsi/identifiers/corrigendum.rb +29 -12
  46. data/lib/pubid/bsi/identifiers/expert_commentary.rb +6 -7
  47. data/lib/pubid/bsi/identifiers/national_annex.rb +18 -20
  48. data/lib/pubid/bsi/identifiers/root_identity.rb +31 -0
  49. data/lib/pubid/bsi/identifiers/set.rb +2 -0
  50. data/lib/pubid/bsi/identifiers/supplement_document.rb +2 -0
  51. data/lib/pubid/bsi/identifiers.rb +1 -0
  52. data/lib/pubid/bsi/parser.rb +8 -8
  53. data/lib/pubid/bsi/renderer.rb +20 -20
  54. data/lib/pubid/bsi/urn_generator.rb +28 -18
  55. data/lib/pubid/builder/base.rb +27 -0
  56. data/lib/pubid/calconnect/builder.rb +2 -0
  57. data/lib/pubid/calconnect/identifier.rb +4 -0
  58. data/lib/pubid/ccsds/builder.rb +2 -0
  59. data/lib/pubid/ccsds/identifier.rb +8 -0
  60. data/lib/pubid/cen_cenelec/CLAUDE.md +59 -0
  61. data/lib/pubid/cen_cenelec/builder.rb +6 -1
  62. data/lib/pubid/cen_cenelec/identifier.rb +10 -27
  63. data/lib/pubid/cen_cenelec/identifiers/amendment.rb +3 -10
  64. data/lib/pubid/cen_cenelec/identifiers/corrigendum.rb +3 -10
  65. data/lib/pubid/cen_cenelec/parser.rb +11 -3
  66. data/lib/pubid/cie/CLAUDE.md +58 -0
  67. data/lib/pubid/cie/builder.rb +2 -0
  68. data/lib/pubid/cie/components/language.rb +2 -0
  69. data/lib/pubid/components/adoption.rb +2 -0
  70. data/lib/pubid/components/code.rb +2 -0
  71. data/lib/pubid/components/date.rb +8 -6
  72. data/lib/pubid/components/edition.rb +2 -0
  73. data/lib/pubid/components/iteration.rb +2 -0
  74. data/lib/pubid/components/language.rb +2 -0
  75. data/lib/pubid/components/locality.rb +2 -0
  76. data/lib/pubid/components/publisher.rb +2 -0
  77. data/lib/pubid/components/relationship.rb +2 -0
  78. data/lib/pubid/components/stage.rb +2 -0
  79. data/lib/pubid/components/supplement.rb +2 -0
  80. data/lib/pubid/components/type.rb +2 -0
  81. data/lib/pubid/components/typed_stage.rb +8 -0
  82. data/lib/pubid/csa/CLAUDE.md +41 -0
  83. data/lib/pubid/csa/builder.rb +2 -0
  84. data/lib/pubid/csa/identifier.rb +13 -1
  85. data/lib/pubid/csa/renderer.rb +12 -12
  86. data/lib/pubid/csa/single_identifier.rb +17 -0
  87. data/lib/pubid/doi/builder.rb +2 -0
  88. data/lib/pubid/easc/builder.rb +2 -0
  89. data/lib/pubid/ecma/CLAUDE.md +28 -0
  90. data/lib/pubid/ecma/builder.rb +2 -0
  91. data/lib/pubid/ecma/identifier.rb +7 -0
  92. data/lib/pubid/etsi/CLAUDE.md +34 -0
  93. data/lib/pubid/etsi/builder.rb +2 -0
  94. data/lib/pubid/etsi/components/code.rb +6 -0
  95. data/lib/pubid/etsi/components/version.rb +2 -0
  96. data/lib/pubid/etsi/identifiers/etsi_standard.rb +7 -0
  97. data/lib/pubid/evs/CLAUDE.md +58 -0
  98. data/lib/pubid/evs/builder.rb +2 -0
  99. data/lib/pubid/gb/CLAUDE.md +140 -0
  100. data/lib/pubid/gb/builder.rb +7 -2
  101. data/lib/pubid/gb/identifier.rb +5 -3
  102. data/lib/pubid/gb/identifiers/all_parts.rb +17 -0
  103. data/lib/pubid/gb/identifiers.rb +1 -0
  104. data/lib/pubid/gb/renderer.rb +0 -1
  105. data/lib/pubid/gost/CLAUDE.md +64 -0
  106. data/lib/pubid/gost/builder.rb +3 -1
  107. data/lib/pubid/gost/identifier.rb +5 -0
  108. data/lib/pubid/iala/CLAUDE.md +82 -0
  109. data/lib/pubid/iala/builder.rb +2 -0
  110. data/lib/pubid/iana/CLAUDE.md +7 -0
  111. data/lib/pubid/iana/builder.rb +2 -0
  112. data/lib/pubid/identifier.rb +161 -17
  113. data/lib/pubid/idf/builder.rb +6 -1
  114. data/lib/pubid/idf/identifier.rb +5 -0
  115. data/lib/pubid/idf/identifiers/all_parts.rb +17 -0
  116. data/lib/pubid/idf/identifiers.rb +1 -0
  117. data/lib/pubid/iec/CLAUDE.md +31 -0
  118. data/lib/pubid/iec/builder.rb +7 -1
  119. data/lib/pubid/iec/components/consolidated_amendment.rb +4 -0
  120. data/lib/pubid/iec/components/sheet.rb +2 -0
  121. data/lib/pubid/iec/components/trf_info.rb +2 -0
  122. data/lib/pubid/iec/components/vap_suffix.rb +2 -0
  123. data/lib/pubid/iec/identifier.rb +7 -2
  124. data/lib/pubid/iec/identifiers/all_parts.rb +19 -0
  125. data/lib/pubid/iec/identifiers.rb +1 -0
  126. data/lib/pubid/iec/renderer.rb +0 -1
  127. data/lib/pubid/iec/urn_generator.rb +9 -1
  128. data/lib/pubid/iec/urn_parser.rb +3 -2
  129. data/lib/pubid/ieee/CLAUDE.md +97 -0
  130. data/lib/pubid/ieee/builder.rb +134 -5
  131. data/lib/pubid/ieee/components/code.rb +2 -0
  132. data/lib/pubid/ieee/components/draft.rb +35 -2
  133. data/lib/pubid/ieee/components/typed_stage.rb +2 -0
  134. data/lib/pubid/ieee/identifiers/base.rb +20 -0
  135. data/lib/pubid/ieee/identifiers/iec_ieee_copublished.rb +9 -0
  136. data/lib/pubid/ieee/identifiers/joint_development.rb +17 -10
  137. data/lib/pubid/ieee/identifiers/project_draft_identifier.rb +8 -1
  138. data/lib/pubid/ieee/parser.rb +134 -21
  139. data/lib/pubid/ieee/renderer.rb +40 -7
  140. data/lib/pubid/ieee/urn_generator.rb +31 -0
  141. data/lib/pubid/ietf/CLAUDE.md +7 -0
  142. data/lib/pubid/ietf/builder.rb +2 -0
  143. data/lib/pubid/iho/builder.rb +2 -0
  144. data/lib/pubid/isbn/builder.rb +2 -0
  145. data/lib/pubid/iso/CLAUDE.md +47 -0
  146. data/lib/pubid/iso/builder.rb +19 -5
  147. data/lib/pubid/iso/components/publisher.rb +2 -0
  148. data/lib/pubid/iso/identifier.rb +6 -14
  149. data/lib/pubid/iso/identifiers/all_parts.rb +19 -0
  150. data/lib/pubid/iso/identifiers/directives_supplement.rb +4 -2
  151. data/lib/pubid/iso/identifiers.rb +1 -0
  152. data/lib/pubid/iso/rendering_style.rb +0 -1
  153. data/lib/pubid/itu/CLAUDE.md +69 -0
  154. data/lib/pubid/itu/builder.rb +2 -0
  155. data/lib/pubid/itu/components/code.rb +2 -0
  156. data/lib/pubid/itu/components/designation.rb +2 -0
  157. data/lib/pubid/itu/components/sector.rb +2 -0
  158. data/lib/pubid/itu/components/series.rb +2 -0
  159. data/lib/pubid/itu/identifiers/supplement.rb +15 -0
  160. data/lib/pubid/jcgm/CLAUDE.md +7 -0
  161. data/lib/pubid/jcgm/builder.rb +2 -0
  162. data/lib/pubid/jcgm/components/publisher.rb +2 -0
  163. data/lib/pubid/jis/builder.rb +5 -1
  164. data/lib/pubid/jis/identifier.rb +5 -17
  165. data/lib/pubid/jis/identifiers/all_parts.rb +19 -0
  166. data/lib/pubid/jis/identifiers.rb +1 -0
  167. data/lib/pubid/jis/renderer.rb +0 -2
  168. data/lib/pubid/jis/urn_generator.rb +0 -1
  169. data/lib/pubid/nist/CLAUDE.md +56 -0
  170. data/lib/pubid/nist/builder.rb +2 -0
  171. data/lib/pubid/nist/components/edition.rb +2 -0
  172. data/lib/pubid/nist/components/issue_number.rb +2 -0
  173. data/lib/pubid/nist/components/part.rb +2 -0
  174. data/lib/pubid/nist/components/stage.rb +2 -0
  175. data/lib/pubid/nist/components/supplement.rb +2 -0
  176. data/lib/pubid/nist/components/translation.rb +2 -0
  177. data/lib/pubid/nist/components/update.rb +2 -0
  178. data/lib/pubid/nist/components/version.rb +2 -0
  179. data/lib/pubid/nist/components/volume.rb +2 -0
  180. data/lib/pubid/nist/identifiers/base.rb +24 -3
  181. data/lib/pubid/nist/parser.rb +15 -2
  182. data/lib/pubid/nist/preprocessor.rb +53 -2
  183. data/lib/pubid/oasis/CLAUDE.md +19 -0
  184. data/lib/pubid/oasis/builder.rb +2 -0
  185. data/lib/pubid/oasis/identifier.rb +19 -0
  186. data/lib/pubid/ogc/CLAUDE.md +34 -0
  187. data/lib/pubid/ogc/builder.rb +2 -0
  188. data/lib/pubid/ogc/identifier.rb +11 -0
  189. data/lib/pubid/oiml/CLAUDE.md +189 -0
  190. data/lib/pubid/oiml/builder.rb +20 -0
  191. data/lib/pubid/oiml/components/code.rb +6 -0
  192. data/lib/pubid/oiml/identifier.rb +13 -0
  193. data/lib/pubid/oiml/identifiers/annex.rb +4 -0
  194. data/lib/pubid/oiml/identifiers/certification_system.rb +34 -0
  195. data/lib/pubid/oiml/identifiers/code_number.rb +8 -0
  196. data/lib/pubid/oiml/identifiers/dual_published.rb +174 -0
  197. data/lib/pubid/oiml/identifiers.rb +2 -0
  198. data/lib/pubid/oiml/parser.rb +35 -4
  199. data/lib/pubid/oiml/renderer.rb +23 -1
  200. data/lib/pubid/oiml/single_identifier.rb +4 -0
  201. data/lib/pubid/oiml/supplement_identifier.rb +7 -0
  202. data/lib/pubid/oiml/urn_generator.rb +28 -0
  203. data/lib/pubid/oiml.rb +5 -0
  204. data/lib/pubid/omg/CLAUDE.md +15 -0
  205. data/lib/pubid/omg/builder.rb +2 -0
  206. data/lib/pubid/parser/grammar.rb +23 -0
  207. data/lib/pubid/plateau/builder.rb +2 -0
  208. data/lib/pubid/plateau/identifiers/base.rb +4 -0
  209. data/lib/pubid/plateau/supplement_identifier.rb +14 -2
  210. data/lib/pubid/plateau/urn_generator.rb +7 -1
  211. data/lib/pubid/renderers/human_readable.rb +0 -1
  212. data/lib/pubid/sae/builder.rb +2 -0
  213. data/lib/pubid/sae/components/date.rb +2 -0
  214. data/lib/pubid/sae/components/type.rb +2 -0
  215. data/lib/pubid/subset_match.rb +197 -0
  216. data/lib/pubid/tgpp/CLAUDE.md +43 -0
  217. data/lib/pubid/tgpp/builder.rb +2 -0
  218. data/lib/pubid/tgpp/identifier.rb +14 -0
  219. data/lib/pubid/type_resolver.rb +14 -2
  220. data/lib/pubid/un/builder.rb +2 -0
  221. data/lib/pubid/version.rb +1 -1
  222. data/lib/pubid/w3c/CLAUDE.md +7 -0
  223. data/lib/pubid/w3c/builder.rb +2 -0
  224. data/lib/pubid/xsf/CLAUDE.md +11 -0
  225. data/lib/pubid/xsf/builder.rb +2 -0
  226. data/lib/pubid.rb +16 -3
  227. metadata +44 -2
@@ -7,8 +7,22 @@ module Pubid
7
7
  # Contains a base identifier plus amendment parameters
8
8
  class Amendment < SingleIdentifier
9
9
  attribute :base, ::Pubid::Identifier, polymorphic: true
10
- attribute :amendment_number, :string
11
- attribute :amendment_year, :integer
10
+ # The amendment's ordinal lives in the `number` inherited from
11
+ # SingleIdentifier — already a `:string` there, which is what the
12
+ # ordinal is ("1", "11", "AA") — so the row reads `number:` rather than
13
+ # `amendment_number:`.
14
+ #
15
+ # This does NOT make the ordinal a document number: the amended
16
+ # standard is reached through `base`, and `#root` walks it, which is
17
+ # what relaton-index keys on.
18
+ #
19
+ # The year lives in the inherited `date` component, the natural home:
20
+ # an amendment's year belongs to the supplement, not to the standard,
21
+ # and `#exclude` protects it via `supplement_date_attributes` below,
22
+ # so `exclude(:date)` on a consolidated identifier drops only the
23
+ # standard's date. The flat scalar serialization rule renders a
24
+ # degenerate `date` as a bare `year:` key, matching CEN and every
25
+ # other BSI row.
12
26
  attribute :separator, :string, default: -> { "+" }
13
27
  # true for the trailing " AMD5" / " AMD AA" suffix form, false for the
14
28
  # compact "+A5" / "/A5" join form. Distinguishes the two when no year is
@@ -19,6 +33,23 @@ module Pubid
19
33
  base&.publisher
20
34
  end
21
35
 
36
+ # `publisher` reads through to the base, so it is never this
37
+ # amendment's own state. Lutaml only serializes the borrowed component
38
+ # when the member came from from_hash (the parse path shares the base
39
+ # instance, and lutaml omits the reference), which made
40
+ # from_hash(to_hash(id)).to_hash regrow a `publisher` key the parse
41
+ # path never emits. Drop it unconditionally to keep the wire symmetric.
42
+ def self.compact_hash(_model, hash)
43
+ hash.delete("publisher")
44
+ end
45
+
46
+ # See the comment on `date` above: protects the amendment's own date
47
+ # from a bare `exclude(:date)`/`exclude(:year)`, which otherwise
48
+ # recurses into `base` and drops the standard's date too.
49
+ def self.supplement_date_attributes
50
+ %i[date]
51
+ end
52
+
22
53
  # Base document = the standard this amendment applies to, fully peeled.
23
54
  def base_document
24
55
  base&.base_document || self
@@ -29,19 +60,12 @@ module Pubid
29
60
  base || self
30
61
  end
31
62
 
32
- # Uniform supplement interface (shared with Corrigendum) so callers need
33
- # not special-case the class.
63
+ # Names the supplement class, so callers need not special-case it. The
64
+ # ordinal and the year need no such method: Amendment and Corrigendum —
65
+ # in BSI and in CEN — all declare them as `number` and `year`.
34
66
  def supplement_type
35
67
  :amendment
36
68
  end
37
-
38
- def supplement_number
39
- amendment_number
40
- end
41
-
42
- def supplement_year
43
- amendment_year
44
- end
45
69
  end
46
70
  end
47
71
  end
@@ -12,6 +12,8 @@ module Pubid
12
12
  # BS SP 13; 14; 15 and 16:1949
13
13
  # BS 4048:Parts 1 and 2:1966
14
14
  class BundledIdentifier < SingleIdentifier
15
+ include RootIdentity
16
+
15
17
  attribute :identifiers, ::Pubid::Identifier, collection: true,
16
18
  polymorphic: true
17
19
  attribute :separators, :string, collection: true # Separators between identifiers
@@ -6,6 +6,8 @@ module Pubid
6
6
  # Consolidated Identifier - contains base document plus supplements
7
7
  # Example: "BS 4592-0:2006+A1:2012" = [BS 4592-0:2006, Amendment 1:2012]
8
8
  class ConsolidatedIdentifier < SingleIdentifier
9
+ include RootIdentity
10
+
9
11
  attribute :identifiers, ::Pubid::Identifier, polymorphic: true,
10
12
  collection: true
11
13
 
@@ -19,11 +21,11 @@ module Pubid
19
21
  # Append supplement info to URN
20
22
  identifiers[1..].each do |id|
21
23
  if id.is_a?(Amendment)
22
- urn += ":amd:#{id.amendment_number}"
23
- urn += ":#{id.amendment_year}" if id.amendment_year
24
+ urn += ":amd:#{id.number}"
25
+ urn += ":#{id.year}" if id.year
24
26
  elsif id.is_a?(Corrigendum)
25
- urn += ":cor:#{id.corrigendum_number}"
26
- urn += ":#{id.corrigendum_year}" if id.corrigendum_year
27
+ urn += ":cor:#{id.number}"
28
+ urn += ":#{id.year}" if id.year
27
29
  end
28
30
  end
29
31
  urn
@@ -34,30 +36,25 @@ module Pubid
34
36
  identifiers&.first&.publisher
35
37
  end
36
38
 
37
- def number
38
- identifiers&.first&.number
39
- end
40
-
41
- def year
42
- base = identifiers&.first
43
- base.year if base&.class&.attributes&.key?(:year)
44
- end
45
-
46
- def date
47
- base = identifiers&.first
48
- base.date if base&.class&.attributes&.key?(:date)
49
- end
50
-
51
- def parts
52
- base = identifiers&.first
53
- base.parts if base&.class&.attributes&.key?(:parts)
54
- end
55
-
56
- def part
57
- base = identifiers&.first
58
- base.part if base&.class&.attributes&.key?(:part)
39
+ # See Identifiers::Amendment.compact_hash: the publisher is derived
40
+ # from the first member and must stay off the wrapper's own wire entry,
41
+ # or from_hash round-trips regrow it.
42
+ def self.compact_hash(_model, hash)
43
+ hash.delete("publisher")
59
44
  end
60
45
 
46
+ # `number`, `part`, `parts`, `date` and `year` are deliberately NOT
47
+ # delegated. They used to read `identifiers.first`, one level only, so
48
+ # they answered with the member's value when that member was a plain
49
+ # standard and with nil when it was itself a wrapper (an adoption) —
50
+ # the same accessor reporting two different things. They also shadowed
51
+ # real lutaml attributes, and the `date` one handed lutaml a foreign
52
+ # `Pubid::Components::Date` where BSI declares its own subclass, which
53
+ # is what made `to_hash` raise.
54
+ #
55
+ # A BSI wrapper owns no identity: `#root` carries it, recursively and
56
+ # for every layer. See `Identifiers::RootIdentity` and
57
+ # lib/pubid/bsi/CLAUDE.md.
61
58
  def type
62
59
  base = identifiers&.first
63
60
  base.type if base&.class&.attributes&.key?(:type)
@@ -7,14 +7,38 @@ module Pubid
7
7
  # Contains a base identifier plus corrigendum parameters
8
8
  class Corrigendum < SingleIdentifier
9
9
  attribute :base, ::Pubid::Identifier, polymorphic: true
10
- attribute :corrigendum_number, :string
11
- attribute :corrigendum_year, :integer
10
+ # The ordinal lives in `number` (inherited, already a `:string`), and
11
+ # the year in the inherited `date` component, exactly as on Amendment
12
+ # — see that class for why `date` is now safe to use: `#exclude`
13
+ # protects it via `supplement_date_attributes` below, so
14
+ # `exclude(:date)` drops only the standard's date. The flat scalar
15
+ # serialization rule renders a degenerate `date` as a bare `year:`
16
+ # key, so relaton still compares a plain String across supplement
17
+ # classes and flavors.
18
+ #
19
+ # The year is nil for a year-less corrigendum (`BS 1234:2015+C1`),
20
+ # which parses; `number` is in practice always set, because the grammar
21
+ # requires a digit after the `C` — the unnumbered `+C:2016` form that
22
+ # CEN spells `AC` does not parse in BSI at all.
12
23
  attribute :separator, :string, default: -> { "+" }
13
24
 
14
25
  def publisher
15
26
  base&.publisher
16
27
  end
17
28
 
29
+ # See Amendment.compact_hash: the publisher is borrowed from the base
30
+ # and must never appear on the supplement's own wire entry.
31
+ def self.compact_hash(_model, hash)
32
+ hash.delete("publisher")
33
+ end
34
+
35
+ # See the comment on `date` above: protects the corrigendum's own
36
+ # date from a bare `exclude(:date)`/`exclude(:year)`, which otherwise
37
+ # recurses into `base` and drops the standard's date too.
38
+ def self.supplement_date_attributes
39
+ %i[date]
40
+ end
41
+
18
42
  # Base document = the standard this corrigendum applies to, fully peeled.
19
43
  def base_document
20
44
  base&.base_document || self
@@ -25,19 +49,12 @@ module Pubid
25
49
  base || self
26
50
  end
27
51
 
28
- # Uniform supplement interface (shared with Amendment) so callers need
29
- # not special-case the class.
52
+ # Names the supplement class, so callers need not special-case it. The
53
+ # ordinal and the year need no such method: Amendment and Corrigendum —
54
+ # in BSI and in CEN — all declare them as `number` and `year`.
30
55
  def supplement_type
31
56
  :corrigendum
32
57
  end
33
-
34
- def supplement_number
35
- corrigendum_number
36
- end
37
-
38
- def supplement_year
39
- corrigendum_year
40
- end
41
58
  end
42
59
  end
43
60
  end
@@ -10,6 +10,8 @@ module Pubid
10
10
  # 2. "ExComm" (abbreviated form)
11
11
  # 3. "ExComm (Fire)" (with optional topic suffix)
12
12
  class ExpertCommentary < SingleIdentifier
13
+ include RootIdentity
14
+
13
15
  attribute :base, ::Pubid::Identifier, polymorphic: true
14
16
  attribute :format, :string # "full", "abbr", "abbr_with_topic"
15
17
  attribute :topic, :string # e.g., "Fire"
@@ -18,13 +20,10 @@ module Pubid
18
20
  base&.publisher
19
21
  end
20
22
 
21
- def number
22
- base&.number
23
- end
24
-
25
- def year
26
- base&.year
27
- end
23
+ # `number` and `year` are deliberately NOT delegated — a BSI wrapper
24
+ # owns no identity, and a one-level delegation answered with nil
25
+ # whenever `base` was itself a wrapper. `#root` carries identity for
26
+ # every layer. See `Identifiers::RootIdentity` and lib/pubid/bsi/CLAUDE.md.
28
27
 
29
28
  # Base document = the commented standard, fully peeled.
30
29
  def base_document
@@ -6,8 +6,24 @@ module Pubid
6
6
  # National Annex (NA) identifier
7
7
  # Can have own supplements: "NA+A1:2012 to BASE"
8
8
  class NationalAnnex < SingleIdentifier
9
- attribute :na_supplements, ::Pubid::Identifier, polymorphic: true, collection: true # Supplements on the NA itself
10
- attribute :base_doc, ::Pubid::Identifier, polymorphic: true # The identifier after "to"
9
+ include RootIdentity
10
+
11
+ # Supplements on the NA itself. `initialize_empty: true` because the
12
+ # builder always passes an array (empty when the grammar matched no
13
+ # supplement) while `from_hash` never sets the attribute at all — the
14
+ # serialized hash omits an empty collection — so a parsed NA held `[]`
15
+ # and a deserialized one `nil`, and the two were not `==`. `#matches?`
16
+ # is `exclude(*ignore) == other.exclude(*ignore)`, so every index lookup
17
+ # of a supplement-less NA silently returned nothing. The `collection:
18
+ # true` lesson in CLAUDE.md, and the same one-line remedy.
19
+ attribute :na_supplements, ::Pubid::Identifier, polymorphic: true,
20
+ collection: true,
21
+ initialize_empty: true
22
+ # The identifier after "to", under the uniform parent accessor. `#root`
23
+ # and `#base_document` are inherited and walk it, so the delegating
24
+ # readers this class used to carry are gone — they shadowed real lutaml
25
+ # accessors and reached only one level down.
26
+ attribute :base, ::Pubid::Identifier, polymorphic: true
11
27
 
12
28
  TYPED_STAGES = [
13
29
  Pubid::Components::TypedStage.new(
@@ -24,24 +40,6 @@ module Pubid
24
40
  { key: :na,
25
41
  web: :national_annex, title: "National Annex", short: "NA" }
26
42
  end
27
-
28
- # Delegation methods to access wrapped base_doc attributes
29
- def number
30
- base_doc&.number || super
31
- end
32
-
33
- def date
34
- base_doc&.date || super
35
- end
36
-
37
- def part
38
- base_doc&.part || super
39
- end
40
-
41
- def subpart
42
- base_doc&.subpart || super
43
- end
44
-
45
43
  end
46
44
  end
47
45
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Bsi
5
+ module Identifiers
6
+ # MR-slug hooks for a BSI wrapper that carries no identity of its own.
7
+ #
8
+ # The base `mr_number_with_part` and `mr_year` read the receiver's own
9
+ # `number`, `part`, `subpart` and `date`. A wrapper — an adoption, a
10
+ # national annex, a consolidated edition, an Expert-commentary volume —
11
+ # leaves all four nil and keeps the identity on the document it wraps, so
12
+ # those hooks produced a slug with no number at all: 158 BSI identifiers
13
+ # collapsed onto the bare `bs`, and `to_slug` is an output FILENAME.
14
+ #
15
+ # `#root` and not `base`: a "DD ENV ISO 11079:1999" adopts a CEN
16
+ # EuropeanPrestandard, which is itself a wrapper around the ISO standard,
17
+ # so one level down is not far enough. `#root` recurses, and for a
18
+ # non-wrapper it is `self` — hence the `equal?` guard, which sends an
19
+ # ordinary identifier to the base hooks unchanged.
20
+ module RootIdentity
21
+ def mr_number_with_part
22
+ root.equal?(self) ? super : root.mr_number_with_part
23
+ end
24
+
25
+ def mr_year
26
+ root.equal?(self) ? super : root.mr_year
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -13,6 +13,8 @@ module Pubid
13
13
  # BS ISO 20400 + BS ISO 44001+BS ISO 44002
14
14
  # BS ISO 9001+BS ISO 14001
15
15
  class Set < SingleIdentifier
16
+ include RootIdentity
17
+
16
18
  attribute :identifiers, ::Pubid::Identifier, collection: true,
17
19
  polymorphic: true
18
20
  attribute :separators, :string, collection: true # Should all be " + "
@@ -9,6 +9,8 @@ module Pubid
9
9
  # Forward: "BS 1000:Supplement No. 1:1972", "BS 1722-1 Supplement No. 1:1974"
10
10
  # Reverse: "Supplement No. 1 (1970) to BS 1831:1969"
11
11
  class SupplementDocument < SingleIdentifier
12
+ include RootIdentity
13
+
12
14
  attribute :base, ::Pubid::Identifier, polymorphic: true
13
15
  attribute :supplement_number, :string
14
16
  attribute :supplement_year, :integer
@@ -35,6 +35,7 @@ module Pubid
35
35
  autoload :PublishedDocument, "#{__dir__}/identifiers/published_document"
36
36
  autoload :PubliclyAvailableSpecification,
37
37
  "#{__dir__}/identifiers/publicly_available_specification"
38
+ autoload :RootIdentity, "#{__dir__}/identifiers/root_identity"
38
39
  autoload :Section, "#{__dir__}/identifiers/section"
39
40
  autoload :Set, "#{__dir__}/identifiers/set"
40
41
  autoload :StandaloneAmendment,
@@ -412,49 +412,49 @@ module Pubid
412
412
  rule(:addendum_document) do
413
413
  # With multi-letter flex type prefix (CECC, E9111, etc.)
414
414
  (bs.as(:publisher) >> space >> multi_letter_prefix.as(:flex_prefix) >> space >> number.as(:number) >> iteration.as(:iteration) >> parts.as(:parts) >>
415
- (colon | space).as(:add_sep) >> str("Addendum") >> space >> str("No.").as(:add_no_prefix) >> space >>
415
+ (colon | space).as(:add_sep) >> str("Addendum") >> space.repeat(1) >> str("No.").as(:add_no_prefix) >> space.repeat(1) >>
416
416
  digits.as(:addendum_number) >> colon >> digit.repeat(4,
417
417
  4).as(:addendum_year)
418
418
  ).as(:addendum_document) |
419
419
  (bs.as(:publisher) >> space >> multi_letter_prefix.as(:flex_prefix) >> space >> number.as(:number) >> iteration.as(:iteration) >> parts.as(:parts) >>
420
- (colon | space).as(:add_sep) >> str("Addendum") >> space >>
420
+ (colon | space).as(:add_sep) >> str("Addendum") >> space.repeat(1) >>
421
421
  digits.as(:addendum_number) >> colon >> digit.repeat(4,
422
422
  4).as(:addendum_year)
423
423
  ).as(:addendum_document) |
424
424
  # With single-letter flex type prefix (M, etc.)
425
425
  (bs.as(:publisher) >> space >> single_letter_prefix.as(:flex_prefix) >> space >> number.as(:number) >> iteration.as(:iteration) >> parts.as(:parts) >>
426
- (colon | space).as(:add_sep) >> str("Addendum") >> space >> str("No.").as(:add_no_prefix) >> space >>
426
+ (colon | space).as(:add_sep) >> str("Addendum") >> space.repeat(1) >> str("No.").as(:add_no_prefix) >> space.repeat(1) >>
427
427
  digits.as(:addendum_number) >> colon >> digit.repeat(4,
428
428
  4).as(:addendum_year)
429
429
  ).as(:addendum_document) |
430
430
  (bs.as(:publisher) >> space >> single_letter_prefix.as(:flex_prefix) >> space >> number.as(:number) >> iteration.as(:iteration) >> parts.as(:parts) >>
431
- (colon | space).as(:add_sep) >> str("Addendum") >> space >>
431
+ (colon | space).as(:add_sep) >> str("Addendum") >> space.repeat(1) >>
432
432
  digits.as(:addendum_number) >> colon >> digit.repeat(4,
433
433
  4).as(:addendum_year)
434
434
  ).as(:addendum_document) |
435
435
  # With year before Addendum and SPACE separator: "BS NUMBER:YEAR Addendum No. N:YEAR"
436
436
  (bs.as(:publisher) >> space >> number.as(:number) >> iteration.as(:iteration) >> parts.as(:parts) >>
437
437
  colon >> digit.repeat(4,
438
- 4).as(:base_year) >> space.as(:add_sep) >> str("Addendum") >> space >> str("No.").as(:add_no_prefix) >> space >>
438
+ 4).as(:base_year) >> space.as(:add_sep) >> str("Addendum") >> space.repeat(1) >> str("No.").as(:add_no_prefix) >> space.repeat(1) >>
439
439
  digits.as(:addendum_number) >> colon >> digit.repeat(4,
440
440
  4).as(:addendum_year)
441
441
  ).as(:addendum_document) |
442
442
  # With year before Addendum and COLON separator: "BS NUMBER:YEAR:Addendum No. N:YEAR"
443
443
  (bs.as(:publisher) >> space >> number.as(:number) >> iteration.as(:iteration) >> parts.as(:parts) >>
444
444
  colon >> digit.repeat(4,
445
- 4).as(:base_year) >> colon.as(:add_sep) >> str("Addendum") >> space >> str("No.").as(:add_no_prefix) >> space >>
445
+ 4).as(:base_year) >> colon.as(:add_sep) >> str("Addendum") >> space.repeat(1) >> str("No.").as(:add_no_prefix) >> space.repeat(1) >>
446
446
  digits.as(:addendum_number) >> colon >> digit.repeat(4,
447
447
  4).as(:addendum_year)
448
448
  ).as(:addendum_document) |
449
449
  # Without flex type prefix - with "No." prefix
450
450
  (bs.as(:publisher) >> space >> number.as(:number) >> iteration.as(:iteration) >> parts.as(:parts) >>
451
- (colon | space).as(:add_sep) >> str("Addendum") >> space >> str("No.").as(:add_no_prefix) >> space >>
451
+ (colon | space).as(:add_sep) >> str("Addendum") >> space.repeat(1) >> str("No.").as(:add_no_prefix) >> space.repeat(1) >>
452
452
  digits.as(:addendum_number) >> colon >> digit.repeat(4,
453
453
  4).as(:addendum_year)
454
454
  ).as(:addendum_document) |
455
455
  # Without "No." prefix
456
456
  (bs.as(:publisher) >> space >> number.as(:number) >> iteration.as(:iteration) >> parts.as(:parts) >>
457
- (colon | space).as(:add_sep) >> str("Addendum") >> space >>
457
+ (colon | space).as(:add_sep) >> str("Addendum") >> space.repeat(1) >>
458
458
  digits.as(:addendum_number) >> colon >> digit.repeat(4,
459
459
  4).as(:addendum_year)
460
460
  ).as(:addendum_document)
@@ -212,7 +212,7 @@ module Pubid
212
212
  end
213
213
 
214
214
  result = prefix
215
- result += " #{id.adopted}" if id.adopted
215
+ result += " #{id.base}" if id.base
216
216
  result += " ED#{id.edition}" if id.edition
217
217
 
218
218
  result += " (R#{id.reaffirmation_year})" if id.reaffirmation_year
@@ -237,7 +237,7 @@ module Pubid
237
237
  end
238
238
 
239
239
  result = prefix
240
- result += " #{id.adopted}" if id.adopted
240
+ result += " #{id.base}" if id.base
241
241
  result += " ED#{id.edition}" if id.edition
242
242
 
243
243
  result += " (R#{id.reaffirmation_year})" if id.reaffirmation_year
@@ -303,16 +303,16 @@ module Pubid
303
303
 
304
304
  if id.amd_suffix_form
305
305
  # Trailing " AMD5" / " AMD AA" suffix form
306
- suffix = if id.amendment_number&.match?(/^[A-Z]+$/)
307
- " AMD #{id.amendment_number}"
306
+ suffix = if id.number&.match?(/^[A-Z]+$/)
307
+ " AMD #{id.number}"
308
308
  else
309
- " AMD#{id.amendment_number}"
309
+ " AMD#{id.number}"
310
310
  end
311
311
  base.empty? ? suffix.lstrip : "#{base}#{suffix}"
312
312
  else
313
313
  # Compact "+A5" / "/A5" join form, with an optional ":year"
314
- compact = "#{id.separator}A#{id.amendment_number}"
315
- compact += ":#{id.amendment_year}" if id.amendment_year
314
+ compact = "#{id.separator}A#{id.number}"
315
+ compact += ":#{id.year}" if id.year
316
316
  "#{base}#{compact}"
317
317
  end
318
318
  end
@@ -422,21 +422,21 @@ module Pubid
422
422
  id.identifiers[1..].each do |idd|
423
423
  if idd.is_a?(Identifiers::Amendment)
424
424
  if idd.amd_suffix_form
425
- result += if idd.amendment_number&.match?(/^[A-Z]+$/)
426
- " AMD #{idd.amendment_number}"
425
+ result += if idd.number&.match?(/^[A-Z]+$/)
426
+ " AMD #{idd.number}"
427
427
  else
428
- " AMD#{idd.amendment_number}"
428
+ " AMD#{idd.number}"
429
429
  end
430
430
  else
431
431
  sep = idd.separator || "+"
432
- result += "#{sep}A#{idd.amendment_number}"
433
- result += ":#{idd.amendment_year}" if idd.amendment_year
432
+ result += "#{sep}A#{idd.number}"
433
+ result += ":#{idd.year}" if idd.year
434
434
  end
435
435
  elsif idd.is_a?(Identifiers::Corrigendum)
436
436
  sep = idd.separator || "+"
437
437
  result += "#{sep}C"
438
- result += idd.corrigendum_number.to_s if idd.corrigendum_number
439
- result += ":#{idd.corrigendum_year}" if idd.corrigendum_year
438
+ result += idd.number.to_s if idd.number
439
+ result += ":#{idd.year}" if idd.year
440
440
  else
441
441
  result += idd.to_s
442
442
  end
@@ -485,8 +485,8 @@ module Pubid
485
485
  def render_corrigendum(id)
486
486
  result = id.base ? id.base.to_s : ""
487
487
  result += "#{id.separator}C"
488
- result += id.corrigendum_number.to_s if id.corrigendum_number
489
- result += ":#{id.corrigendum_year}" if id.corrigendum_year
488
+ result += id.number.to_s if id.number
489
+ result += ":#{id.year}" if id.year
490
490
  result
491
491
  end
492
492
 
@@ -753,17 +753,17 @@ module Pubid
753
753
  if id.na_supplements&.any?
754
754
  id.na_supplements.each do |supp|
755
755
  if supp.is_a?(Identifiers::Amendment)
756
- result += "+A#{supp.amendment_number}:#{supp.amendment_year}"
756
+ result += "+A#{supp.number}:#{supp.year}"
757
757
  elsif supp.is_a?(Identifiers::Corrigendum)
758
- result += "+C#{supp.corrigendum_number}:#{supp.corrigendum_year}"
758
+ result += "+C#{supp.number}:#{supp.year}"
759
759
  end
760
760
  end
761
761
  end
762
762
 
763
763
  result += " to "
764
764
 
765
- result += if id.base_doc
766
- id.base_doc.to_s
765
+ result += if id.base
766
+ id.base.to_s
767
767
  else
768
768
  render_single_identifier(id)
769
769
  end
@@ -21,15 +21,15 @@ module Pubid
21
21
  parts << identifier.flex_prefix.to_s.downcase
22
22
  end
23
23
 
24
- # Fall back to the wrapped document's number. A wrapper — an adopted
25
- # European norm, a bundle, a set — carries no number of its own, and
26
- # `AdoptedEuropeanNorm#number` only delegates ONE level, so a
27
- # "DD ENV ISO 11079:1999" (which adopts a CEN prestandard that is
28
- # itself a wrapper around the ISO standard) emitted the identity-free
24
+ # Read identity from the wrapped document when the wrapper has none of
25
+ # its own. A wrapper — an adopted European norm, a bundle, a set —
26
+ # carries no number, part, subpart or date, so a
27
+ # "DD ENV ISO 11079:1999" (which adopts a CEN prestandard that is itself
28
+ # a wrapper around the ISO standard) emitted the identity-free
29
29
  # `urn:bsi:dd` — the same URN as every other DD adoption. `#root`
30
30
  # recurses to the origin document, and for a non-wrapper it is `self`,
31
31
  # so this changes nothing for an ordinary identifier.
32
- urn_number = identifier.number || identifier.root.number
32
+ urn_number = identity(:number)
33
33
  if urn_number
34
34
  number = urn_number.to_s
35
35
  if identifier.iteration && !identifier.iteration.empty?
@@ -38,25 +38,22 @@ module Pubid
38
38
  parts << number
39
39
  end
40
40
 
41
- if identifier.part
42
- part = identifier.part.to_s
43
- parts << "-#{part}"
44
- end
41
+ urn_part = identity(:part)
42
+ parts << "-#{urn_part}" if urn_part
45
43
 
46
- if identifier.subpart
47
- subpart = identifier.subpart.to_s
48
- parts << "-#{subpart}"
49
- end
44
+ urn_subpart = identity(:subpart)
45
+ parts << "-#{urn_subpart}" if urn_subpart
50
46
 
51
47
  if identifier.second_number
52
48
  second = identifier.second_number.to_s
53
49
  parts << "/#{second}"
54
50
  end
55
51
 
56
- if identifier.date&.is_a?(::Pubid::Components::Date) && identifier.date.present?
57
- parts << identifier.date.render(context: URN_CONTEXT)
58
- elsif identifier.year
59
- parts << identifier.year.to_s
52
+ urn_date = identity(:date)
53
+ if urn_date.is_a?(::Pubid::Components::Date) && urn_date.present?
54
+ parts << urn_date.render(context: URN_CONTEXT)
55
+ elsif (urn_year = identity(:year))
56
+ parts << urn_year.to_s
60
57
  end
61
58
 
62
59
  if identifier.month
@@ -87,6 +84,19 @@ module Pubid
87
84
 
88
85
  parts.join(":")
89
86
  end
87
+
88
+ private
89
+
90
+ # An identity attribute of the identifier, or of the document it wraps.
91
+ # `#root` is `self` for a non-wrapper, so an ordinary identifier reads its
92
+ # own value twice and nothing changes.
93
+ def identity(attr)
94
+ own = identifier.public_send(attr)
95
+ return own if own
96
+
97
+ root = identifier.root
98
+ root.equal?(identifier) ? nil : root.public_send(attr)
99
+ end
90
100
  end
91
101
  end
92
102
  end
@@ -195,3 +195,30 @@ module Pubid
195
195
  end
196
196
  end
197
197
  end
198
+
199
+ module Pubid
200
+ module Builder
201
+ # A trailing "(all parts)" is stripped by the shared Grammar and marked
202
+ # on the parse tree (:all_parts). Prepended to Builder::Base so every
203
+ # subclass — including the ones that override #build — wraps the built
204
+ # identifier in its flavor's all-parts class (Pubid::AllParts).
205
+ module AllPartsWrap
206
+ # Parslet tops are a Hash or an Array of Hashes (UN's multi-token tree
207
+ # is an Array); the marker rides every element.
208
+ def all_parts_marked?(data)
209
+ case data
210
+ when Hash then data[:all_parts]
211
+ when Array then data.any? { |e| all_parts_marked?(e) }
212
+ else false
213
+ end
214
+ end
215
+
216
+ def build(data, *args, **kwargs)
217
+ marked = all_parts_marked?(data)
218
+ built = super
219
+ marked ? built.to_all_parts : built
220
+ end
221
+ end
222
+
223
+ end
224
+ end
@@ -36,3 +36,5 @@ module Pubid
36
36
  end
37
37
  end
38
38
  end
39
+
40
+ Pubid::Calconnect::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -18,6 +18,10 @@ module Pubid
18
18
  attribute :number, :string
19
19
  attribute :date, ::Pubid::Components::Date
20
20
 
21
+ # A nil `series` means the document has none: `CC 36010` is not
22
+ # `CC/WD 36010`, which is a working draft of it.
23
+ subset_strict :series
24
+
21
25
  # Polymorphic type map for lutaml::Model key_value (de)serialization,
22
26
  # mapping the concrete class's polymorphic_name to its class name so a
23
27
  # stored hash rebuilds the correct identifier type via from_hash.
@@ -55,3 +55,5 @@ module Pubid
55
55
  end
56
56
  end
57
57
  end
58
+
59
+ Pubid::Ccsds::Builder.prepend(Pubid::Builder::AllPartsWrap)