pubid 2.0.0.pre.alpha.8 → 2.0.0.pre.alpha.11

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 (672) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +33 -11
  3. data/conformance/pending.yaml +4 -0
  4. data/data/bipm/update_codes.yaml +9 -0
  5. data/data/iec/update_codes.yaml +3 -0
  6. data/data/ieee/update_codes.yaml +107 -1
  7. data/{archived-gems/pubid-nist → data/nist}/series.yaml +34 -0
  8. data/data/nist/update_codes.yaml +8 -0
  9. data/lib/pubid/adobe/builder.rb +52 -0
  10. data/lib/pubid/adobe/identifier.rb +47 -0
  11. data/lib/pubid/adobe/identifiers/publication.rb +31 -0
  12. data/lib/pubid/adobe/identifiers/tech_note.rb +32 -0
  13. data/lib/pubid/adobe/identifiers.rb +10 -0
  14. data/lib/pubid/adobe/parser.rb +136 -0
  15. data/lib/pubid/adobe/renderer.rb +39 -0
  16. data/lib/pubid/adobe/urn_generator.rb +42 -0
  17. data/lib/pubid/adobe/urn_parser.rb +69 -0
  18. data/lib/pubid/adobe.rb +61 -0
  19. data/lib/pubid/amca/builder.rb +4 -4
  20. data/lib/pubid/amca/identifier.rb +1 -3
  21. data/lib/pubid/amca/identifiers/base.rb +50 -33
  22. data/lib/pubid/amca/identifiers/interpretation.rb +18 -10
  23. data/lib/pubid/amca/identifiers/publication.rb +14 -10
  24. data/lib/pubid/amca/identifiers/standard.rb +3 -1
  25. data/lib/pubid/amca/identifiers.rb +0 -1
  26. data/lib/pubid/amca/parser.rb +1 -1
  27. data/lib/pubid/amca/renderer.rb +4 -4
  28. data/lib/pubid/amca/single_identifier.rb +1 -1
  29. data/lib/pubid/amca/urn_generator.rb +2 -2
  30. data/lib/pubid/amca.rb +5 -4
  31. data/lib/pubid/ansi/builder.rb +2 -2
  32. data/lib/pubid/ansi/identifier.rb +18 -0
  33. data/lib/pubid/ansi/parser.rb +1 -1
  34. data/lib/pubid/ansi/renderer.rb +2 -2
  35. data/lib/pubid/ansi.rb +5 -5
  36. data/lib/pubid/api/builder.rb +16 -2
  37. data/lib/pubid/api/identifier.rb +25 -4
  38. data/lib/pubid/api/identifiers/mpms.rb +10 -8
  39. data/lib/pubid/api/parser.rb +1 -1
  40. data/lib/pubid/api/renderer.rb +16 -7
  41. data/lib/pubid/api/single_identifier.rb +5 -12
  42. data/lib/pubid/api.rb +1 -0
  43. data/lib/pubid/ashrae/builder.rb +53 -32
  44. data/lib/pubid/ashrae/identifier.rb +3 -4
  45. data/lib/pubid/ashrae/identifiers/addenda_package.rb +7 -1
  46. data/lib/pubid/ashrae/identifiers/addendum.rb +9 -1
  47. data/lib/pubid/ashrae/identifiers/base.rb +94 -12
  48. data/lib/pubid/ashrae/identifiers/combined_addenda.rb +8 -1
  49. data/lib/pubid/ashrae/identifiers/errata.rb +9 -0
  50. data/lib/pubid/ashrae/identifiers/guideline.rb +3 -0
  51. data/lib/pubid/ashrae/identifiers/interpretation.rb +16 -0
  52. data/lib/pubid/ashrae/identifiers/standard.rb +3 -0
  53. data/lib/pubid/ashrae/identifiers.rb +0 -1
  54. data/lib/pubid/ashrae/parser.rb +37 -17
  55. data/lib/pubid/ashrae/renderer.rb +20 -20
  56. data/lib/pubid/ashrae/single_identifier.rb +1 -1
  57. data/lib/pubid/ashrae/supplement_identifier.rb +21 -6
  58. data/lib/pubid/ashrae/urn_generator.rb +8 -2
  59. data/lib/pubid/ashrae.rb +10 -4
  60. data/lib/pubid/asme/builder.rb +11 -2
  61. data/lib/pubid/asme/identifier.rb +9 -0
  62. data/lib/pubid/asme/identifiers/standard.rb +86 -2
  63. data/lib/pubid/asme/parser.rb +1 -1
  64. data/lib/pubid/asme/renderer.rb +3 -3
  65. data/lib/pubid/asme/single_identifier.rb +8 -1
  66. data/lib/pubid/asme/urn_generator.rb +2 -2
  67. data/lib/pubid/asme.rb +1 -0
  68. data/lib/pubid/astm/builder.rb +10 -3
  69. data/lib/pubid/astm/identifier.rb +9 -0
  70. data/lib/pubid/astm/identifiers/adjunct.rb +16 -1
  71. data/lib/pubid/astm/identifiers/code_number.rb +81 -0
  72. data/lib/pubid/astm/identifiers/data_series.rb +2 -0
  73. data/lib/pubid/astm/identifiers/iso_dual_published.rb +16 -0
  74. data/lib/pubid/astm/identifiers/manual.rb +10 -0
  75. data/lib/pubid/astm/identifiers/monograph.rb +2 -0
  76. data/lib/pubid/astm/identifiers/research_report.rb +10 -0
  77. data/lib/pubid/astm/identifiers/standard.rb +10 -0
  78. data/lib/pubid/astm/identifiers/technical_report.rb +2 -0
  79. data/lib/pubid/astm/identifiers/work_in_progress.rb +2 -0
  80. data/lib/pubid/astm/identifiers.rb +1 -0
  81. data/lib/pubid/astm/parser.rb +1 -1
  82. data/lib/pubid/astm/renderer.rb +1 -1
  83. data/lib/pubid/astm/single_identifier.rb +31 -1
  84. data/lib/pubid/astm/urn_generator.rb +7 -5
  85. data/lib/pubid/astm.rb +1 -0
  86. data/lib/pubid/bipm/builder.rb +163 -0
  87. data/lib/pubid/bipm/identifier.rb +285 -0
  88. data/lib/pubid/bipm/identifiers/committee_document.rb +36 -0
  89. data/lib/pubid/bipm/identifiers/guide.rb +35 -0
  90. data/lib/pubid/bipm/identifiers/meeting.rb +53 -0
  91. data/lib/pubid/bipm/identifiers/mep.rb +33 -0
  92. data/lib/pubid/bipm/identifiers/metrologia_article.rb +48 -0
  93. data/lib/pubid/bipm/identifiers/si_brochure.rb +33 -0
  94. data/lib/pubid/bipm/identifiers.rb +15 -0
  95. data/lib/pubid/bipm/parser.rb +237 -0
  96. data/lib/pubid/bipm/renderer.rb +118 -0
  97. data/lib/pubid/bipm/urn_generator.rb +58 -0
  98. data/lib/pubid/bipm/urn_parser.rb +60 -0
  99. data/lib/pubid/bipm.rb +92 -0
  100. data/lib/pubid/bsi/builder.rb +62 -52
  101. data/lib/pubid/bsi/identifier.rb +0 -1
  102. data/lib/pubid/bsi/identifiers/addendum_document.rb +2 -2
  103. data/lib/pubid/bsi/identifiers/adopted_european_norm.rb +36 -7
  104. data/lib/pubid/bsi/identifiers/adopted_international_standard.rb +7 -6
  105. data/lib/pubid/bsi/identifiers/amendment.rb +4 -4
  106. data/lib/pubid/bsi/identifiers/british_industrial_practice.rb +0 -1
  107. data/lib/pubid/bsi/identifiers/bundled_identifier.rb +10 -0
  108. data/lib/pubid/bsi/identifiers/committee_document.rb +10 -1
  109. data/lib/pubid/bsi/identifiers/consolidated_identifier.rb +6 -0
  110. data/lib/pubid/bsi/identifiers/corrigendum.rb +4 -4
  111. data/lib/pubid/bsi/identifiers/expert_commentary.rb +5 -5
  112. data/lib/pubid/bsi/identifiers/handbook.rb +0 -2
  113. data/lib/pubid/bsi/identifiers/practice_guide.rb +0 -1
  114. data/lib/pubid/bsi/identifiers/set.rb +11 -0
  115. data/lib/pubid/bsi/identifiers/standalone_amendment.rb +10 -1
  116. data/lib/pubid/bsi/identifiers/supplement_document.rb +2 -2
  117. data/lib/pubid/bsi/identifiers/value_added_publication.rb +6 -6
  118. data/lib/pubid/bsi/parser.rb +1 -1
  119. data/lib/pubid/bsi/renderer.rb +71 -59
  120. data/lib/pubid/bsi/single_identifier.rb +64 -7
  121. data/lib/pubid/bsi/urn_generator.rb +11 -2
  122. data/lib/pubid/bsi.rb +7 -1
  123. data/lib/pubid/builder/base.rb +23 -11
  124. data/lib/pubid/bundled_identifier.rb +19 -9
  125. data/lib/pubid/calconnect/builder.rb +38 -0
  126. data/lib/pubid/calconnect/identifier.rb +121 -0
  127. data/lib/pubid/calconnect/identifiers/standard.rb +29 -0
  128. data/lib/pubid/calconnect/identifiers.rb +9 -0
  129. data/lib/pubid/calconnect/parser.rb +54 -0
  130. data/lib/pubid/calconnect/renderer.rb +36 -0
  131. data/lib/pubid/calconnect/urn_generator.rb +33 -0
  132. data/lib/pubid/calconnect/urn_parser.rb +34 -0
  133. data/lib/pubid/calconnect.rb +83 -0
  134. data/lib/pubid/ccsds/builder.rb +1 -1
  135. data/lib/pubid/ccsds/identifier.rb +10 -3
  136. data/lib/pubid/ccsds/identifiers/base.rb +4 -2
  137. data/lib/pubid/ccsds/identifiers/corrigendum.rb +4 -4
  138. data/lib/pubid/ccsds/parser.rb +1 -1
  139. data/lib/pubid/ccsds/single_identifier.rb +2 -2
  140. data/lib/pubid/ccsds/supplement_identifier.rb +6 -6
  141. data/lib/pubid/ccsds/urn_generator.rb +3 -3
  142. data/lib/pubid/ccsds.rb +1 -0
  143. data/lib/pubid/cen_cenelec/builder.rb +131 -72
  144. data/lib/pubid/cen_cenelec/identifier.rb +115 -3
  145. data/lib/pubid/cen_cenelec/identifiers/adopted_european_norm.rb +27 -16
  146. data/lib/pubid/cen_cenelec/identifiers/amendment.rb +46 -6
  147. data/lib/pubid/cen_cenelec/identifiers/base.rb +8 -9
  148. data/lib/pubid/cen_cenelec/identifiers/cen_report.rb +1 -1
  149. data/lib/pubid/cen_cenelec/identifiers/cen_workshop_agreement.rb +1 -1
  150. data/lib/pubid/cen_cenelec/identifiers/cenelec_harmonization_document.rb +1 -1
  151. data/lib/pubid/cen_cenelec/identifiers/consolidated_identifier.rb +29 -14
  152. data/lib/pubid/cen_cenelec/identifiers/corrigendum.rb +47 -8
  153. data/lib/pubid/cen_cenelec/identifiers/european_norm.rb +60 -3
  154. data/lib/pubid/cen_cenelec/identifiers/european_prestandard.rb +27 -2
  155. data/lib/pubid/cen_cenelec/identifiers/european_specification.rb +1 -1
  156. data/lib/pubid/cen_cenelec/identifiers/fragment.rb +15 -5
  157. data/lib/pubid/cen_cenelec/identifiers/guide.rb +1 -1
  158. data/lib/pubid/cen_cenelec/identifiers/harmonization_document.rb +1 -1
  159. data/lib/pubid/cen_cenelec/identifiers/technical_report.rb +1 -1
  160. data/lib/pubid/cen_cenelec/identifiers/technical_specification.rb +1 -1
  161. data/lib/pubid/cen_cenelec/parser.rb +1 -1
  162. data/lib/pubid/cen_cenelec/renderer.rb +55 -60
  163. data/lib/pubid/cen_cenelec/single_identifier.rb +20 -2
  164. data/lib/pubid/cen_cenelec/urn_generator.rb +66 -44
  165. data/lib/pubid/cen_cenelec.rb +104 -82
  166. data/lib/pubid/cie/builder.rb +149 -24
  167. data/lib/pubid/cie/identifier.rb +13 -4
  168. data/lib/pubid/cie/identifiers/bundle.rb +35 -5
  169. data/lib/pubid/cie/identifiers/code_attributes.rb +52 -0
  170. data/lib/pubid/cie/identifiers/conference.rb +11 -6
  171. data/lib/pubid/cie/identifiers/corrigendum.rb +17 -23
  172. data/lib/pubid/cie/identifiers/dual_published.rb +6 -6
  173. data/lib/pubid/cie/identifiers/identical.rb +9 -9
  174. data/lib/pubid/cie/identifiers/joint_published.rb +10 -10
  175. data/lib/pubid/cie/identifiers/proceedings.rb +41 -0
  176. data/lib/pubid/cie/identifiers/standard.rb +43 -8
  177. data/lib/pubid/cie/identifiers/supplement.rb +27 -25
  178. data/lib/pubid/cie/identifiers/tutorial_bundle.rb +7 -3
  179. data/lib/pubid/cie/identifiers.rb +2 -0
  180. data/lib/pubid/cie/parser.rb +55 -6
  181. data/lib/pubid/cie/single_identifier.rb +40 -1
  182. data/lib/pubid/cie/supplement_identifier.rb +29 -16
  183. data/lib/pubid/cie/urn_generator.rb +11 -8
  184. data/lib/pubid/cie.rb +7 -2
  185. data/lib/pubid/components/date.rb +21 -5
  186. data/lib/pubid/conformance/checks.rb +72 -0
  187. data/lib/pubid/conformance/corpus/case.rb +55 -0
  188. data/lib/pubid/conformance/corpus.rb +36 -0
  189. data/lib/pubid/conformance/generator.rb +212 -0
  190. data/lib/pubid/conformance/pending.rb +63 -0
  191. data/lib/pubid/conformance/runner.rb +153 -0
  192. data/lib/pubid/conformance.rb +55 -0
  193. data/lib/pubid/core.rb +2 -0
  194. data/lib/pubid/csa/builder.rb +48 -48
  195. data/lib/pubid/csa/composite_identifier.rb +18 -7
  196. data/lib/pubid/csa/identifier.rb +187 -80
  197. data/lib/pubid/csa/identifiers/bundled.rb +25 -9
  198. data/lib/pubid/csa/identifiers/canadian_adopted.rb +13 -13
  199. data/lib/pubid/csa/identifiers/cec.rb +44 -4
  200. data/lib/pubid/csa/identifiers/combined.rb +97 -69
  201. data/lib/pubid/csa/identifiers/csa_adopted.rb +4 -4
  202. data/lib/pubid/csa/identifiers/package.rb +4 -4
  203. data/lib/pubid/csa/parser.rb +12 -8
  204. data/lib/pubid/csa/renderer.rb +13 -5
  205. data/lib/pubid/csa/single_identifier.rb +93 -4
  206. data/lib/pubid/csa/urn_generator.rb +9 -2
  207. data/lib/pubid/csa/wrapper_identifier.rb +43 -10
  208. data/lib/pubid/csa.rb +7 -1
  209. data/lib/pubid/doi/builder.rb +18 -0
  210. data/lib/pubid/doi/identifier.rb +49 -0
  211. data/lib/pubid/doi/identifiers/resource.rb +30 -0
  212. data/lib/pubid/doi/identifiers.rb +9 -0
  213. data/lib/pubid/doi/parser.rb +52 -0
  214. data/lib/pubid/doi/renderer.rb +13 -0
  215. data/lib/pubid/doi.rb +56 -0
  216. data/lib/pubid/easc/builder.rb +55 -0
  217. data/lib/pubid/easc/identifier.rb +59 -0
  218. data/lib/pubid/easc/identifiers/pmg.rb +20 -0
  219. data/lib/pubid/easc/identifiers/rmg.rb +19 -0
  220. data/lib/pubid/easc/identifiers.rb +10 -0
  221. data/lib/pubid/easc/parser.rb +88 -0
  222. data/lib/pubid/easc/renderer.rb +48 -0
  223. data/lib/pubid/easc/urn_generator.rb +46 -0
  224. data/lib/pubid/easc/urn_parser.rb +57 -0
  225. data/lib/pubid/easc.rb +53 -0
  226. data/lib/pubid/ecma/builder.rb +45 -0
  227. data/lib/pubid/ecma/identifier.rb +147 -0
  228. data/lib/pubid/ecma/identifiers/memento.rb +30 -0
  229. data/lib/pubid/ecma/identifiers/standard.rb +31 -0
  230. data/lib/pubid/ecma/identifiers/technical_report.rb +31 -0
  231. data/lib/pubid/ecma/identifiers.rb +11 -0
  232. data/lib/pubid/ecma/parser.rb +76 -0
  233. data/lib/pubid/ecma/renderer.rb +67 -0
  234. data/lib/pubid/ecma/urn_generator.rb +46 -0
  235. data/lib/pubid/ecma/urn_parser.rb +51 -0
  236. data/lib/pubid/ecma.rb +68 -0
  237. data/lib/pubid/errors.rb +80 -0
  238. data/lib/pubid/etsi/builder.rb +13 -4
  239. data/lib/pubid/etsi/identifier.rb +1 -3
  240. data/lib/pubid/etsi/identifiers/base.rb +43 -11
  241. data/lib/pubid/etsi/identifiers/etsi_standard.rb +207 -4
  242. data/lib/pubid/etsi/identifiers/supplement_identifier.rb +45 -3
  243. data/lib/pubid/etsi/identifiers.rb +0 -1
  244. data/lib/pubid/etsi/parser.rb +9 -3
  245. data/lib/pubid/etsi/renderer.rb +8 -2
  246. data/lib/pubid/etsi.rb +6 -5
  247. data/lib/pubid/evs/builder.rb +26 -0
  248. data/lib/pubid/evs/identifier.rb +37 -0
  249. data/lib/pubid/evs/identifiers/national_adoption.rb +17 -0
  250. data/lib/pubid/evs/identifiers.rb +9 -0
  251. data/lib/pubid/evs/parser.rb +27 -0
  252. data/lib/pubid/evs/renderer.rb +30 -0
  253. data/lib/pubid/evs/urn_generator.rb +30 -0
  254. data/lib/pubid/evs/urn_parser.rb +80 -0
  255. data/lib/pubid/evs.rb +90 -0
  256. data/lib/pubid/export/exporter.rb +2 -1
  257. data/lib/pubid/export/flavor_exporter.rb +18 -3
  258. data/lib/pubid/export.rb +1 -0
  259. data/lib/pubid/format_detector.rb +1 -0
  260. data/lib/pubid/gb/builder.rb +45 -0
  261. data/lib/pubid/gb/identifier.rb +67 -0
  262. data/lib/pubid/gb/identifiers/standard.rb +34 -0
  263. data/lib/pubid/gb/identifiers.rb +9 -0
  264. data/lib/pubid/gb/parser.rb +63 -0
  265. data/lib/pubid/gb/renderer.rb +35 -0
  266. data/lib/pubid/gb.rb +88 -0
  267. data/lib/pubid/gost/builder.rb +185 -0
  268. data/lib/pubid/gost/identifier.rb +47 -0
  269. data/lib/pubid/gost/identifiers/foreign_reference.rb +25 -0
  270. data/lib/pubid/gost/identifiers/harmonized.rb +39 -0
  271. data/lib/pubid/gost/identifiers/identical_adoption.rb +39 -0
  272. data/lib/pubid/gost/identifiers/interstate_standard.rb +20 -0
  273. data/lib/pubid/gost/identifiers/national_standard.rb +19 -0
  274. data/lib/pubid/gost/identifiers.rb +13 -0
  275. data/lib/pubid/gost/parser.rb +105 -0
  276. data/lib/pubid/gost/renderer.rb +48 -0
  277. data/lib/pubid/gost/urn_generator.rb +46 -0
  278. data/lib/pubid/gost/urn_parser.rb +52 -0
  279. data/lib/pubid/gost.rb +50 -0
  280. data/lib/pubid/iala/builder.rb +22 -27
  281. data/lib/pubid/iala/identifier.rb +36 -8
  282. data/lib/pubid/iala/identifiers/advice.rb +1 -1
  283. data/lib/pubid/iala/identifiers/annex.rb +10 -10
  284. data/lib/pubid/iala/identifiers/general_assembly.rb +5 -2
  285. data/lib/pubid/iala/identifiers/guideline.rb +3 -1
  286. data/lib/pubid/iala/identifiers/letter.rb +1 -1
  287. data/lib/pubid/iala/identifiers/manual.rb +3 -1
  288. data/lib/pubid/iala/identifiers/model_course.rb +3 -1
  289. data/lib/pubid/iala/identifiers/recommendation.rb +3 -1
  290. data/lib/pubid/iala/identifiers/report.rb +1 -1
  291. data/lib/pubid/iala/identifiers/resolution.rb +1 -1
  292. data/lib/pubid/iala/identifiers/standard.rb +3 -1
  293. data/lib/pubid/iala/identifiers.rb +0 -1
  294. data/lib/pubid/iala/parser.rb +10 -2
  295. data/lib/pubid/iala/renderer.rb +1 -1
  296. data/lib/pubid/iala/urn_generator.rb +1 -1
  297. data/lib/pubid/iala/urn_parser.rb +24 -3
  298. data/lib/pubid/iala.rb +5 -4
  299. data/lib/pubid/iana/builder.rb +22 -0
  300. data/lib/pubid/iana/identifier.rb +140 -0
  301. data/lib/pubid/iana/identifiers/registry.rb +69 -0
  302. data/lib/pubid/iana/identifiers.rb +9 -0
  303. data/lib/pubid/iana/parser.rb +38 -0
  304. data/lib/pubid/iana/renderer.rb +32 -0
  305. data/lib/pubid/iana/urn_generator.rb +19 -0
  306. data/lib/pubid/iana/urn_parser.rb +24 -0
  307. data/lib/pubid/iana.rb +70 -0
  308. data/lib/pubid/identifier.rb +797 -26
  309. data/lib/pubid/identifier_metadata.rb +1 -0
  310. data/lib/pubid/idf/builder.rb +4 -4
  311. data/lib/pubid/idf/identifier.rb +18 -9
  312. data/lib/pubid/idf/parser.rb +3 -3
  313. data/lib/pubid/idf/renderer.rb +5 -5
  314. data/lib/pubid/idf/supplement_identifier.rb +3 -3
  315. data/lib/pubid/idf/urn_generator.rb +1 -1
  316. data/lib/pubid/idf.rb +7 -1
  317. data/lib/pubid/iec/builder.rb +49 -32
  318. data/lib/pubid/iec/components.rb +0 -1
  319. data/lib/pubid/iec/identifier.rb +112 -77
  320. data/lib/pubid/iec/identifiers/amendment.rb +6 -2
  321. data/lib/pubid/iec/identifiers/base.rb +2 -2
  322. data/lib/pubid/iec/identifiers/component_specification.rb +0 -10
  323. data/lib/pubid/iec/identifiers/conformity_assessment.rb +0 -10
  324. data/lib/pubid/iec/identifiers/consolidated_identifier.rb +6 -0
  325. data/lib/pubid/iec/identifiers/corrigendum.rb +2 -2
  326. data/lib/pubid/iec/identifiers/fragment_identifier.rb +13 -15
  327. data/lib/pubid/iec/identifiers/guide.rb +0 -16
  328. data/lib/pubid/iec/identifiers/international_standard.rb +22 -6
  329. data/lib/pubid/iec/identifiers/operational_document.rb +0 -10
  330. data/lib/pubid/iec/identifiers/publicly_available_specification.rb +0 -12
  331. data/lib/pubid/iec/identifiers/sheet_identifier.rb +12 -12
  332. data/lib/pubid/iec/identifiers/societal_technology_trend_report.rb +0 -10
  333. data/lib/pubid/iec/identifiers/systems_reference_document.rb +0 -10
  334. data/lib/pubid/iec/identifiers/technical_group.rb +30 -0
  335. data/lib/pubid/iec/identifiers/technical_report.rb +1 -18
  336. data/lib/pubid/iec/identifiers/technical_specification.rb +1 -18
  337. data/lib/pubid/iec/identifiers/technology_report.rb +0 -10
  338. data/lib/pubid/iec/identifiers/test_report_form.rb +0 -11
  339. data/lib/pubid/iec/identifiers/vap_identifier.rb +12 -12
  340. data/lib/pubid/iec/identifiers/white_paper.rb +0 -10
  341. data/lib/pubid/iec/identifiers/working_document.rb +24 -10
  342. data/lib/pubid/iec/identifiers.rb +1 -0
  343. data/lib/pubid/iec/parser.rb +53 -28
  344. data/lib/pubid/iec/renderer.rb +31 -18
  345. data/lib/pubid/iec/single_identifier.rb +34 -11
  346. data/lib/pubid/iec/supplement_identifier.rb +32 -19
  347. data/lib/pubid/iec/urn_generator.rb +227 -61
  348. data/lib/pubid/iec/urn_parser.rb +42 -4
  349. data/lib/pubid/iec.rb +2 -6
  350. data/lib/pubid/ieee/aiee/builder.rb +17 -3
  351. data/lib/pubid/ieee/aiee/identifier.rb +46 -62
  352. data/lib/pubid/ieee/aiee/parser.rb +1 -1
  353. data/lib/pubid/ieee/builder.rb +386 -69
  354. data/lib/pubid/ieee/compaction.rb +121 -0
  355. data/lib/pubid/ieee/components/draft.rb +47 -7
  356. data/lib/pubid/ieee/identifier.rb +1 -3
  357. data/lib/pubid/ieee/identifiers/adopted_standard.rb +62 -5
  358. data/lib/pubid/ieee/identifiers/amendment.rb +40 -0
  359. data/lib/pubid/ieee/identifiers/base.rb +148 -18
  360. data/lib/pubid/ieee/identifiers/code_number.rb +82 -0
  361. data/lib/pubid/ieee/identifiers/conformance_identifier.rb +6 -2
  362. data/lib/pubid/ieee/identifiers/corrigendum.rb +17 -2
  363. data/lib/pubid/ieee/identifiers/csa_dual_published.rb +24 -3
  364. data/lib/pubid/ieee/identifiers/dual_identifier.rb +3 -3
  365. data/lib/pubid/ieee/identifiers/dual_published.rb +58 -3
  366. data/lib/pubid/ieee/identifiers/iec_ieee_copublished.rb +102 -5
  367. data/lib/pubid/ieee/identifiers/interpretation_identifier.rb +25 -1
  368. data/lib/pubid/ieee/identifiers/joint_development.rb +64 -15
  369. data/lib/pubid/ieee/identifiers/multi_numbered_identifier.rb +58 -6
  370. data/lib/pubid/ieee/identifiers/nesc/base.rb +90 -19
  371. data/lib/pubid/ieee/identifiers/nesc/draft.rb +20 -5
  372. data/lib/pubid/ieee/identifiers/nesc/edition.rb +32 -0
  373. data/lib/pubid/ieee/identifiers/nesc/handbook.rb +16 -5
  374. data/lib/pubid/ieee/identifiers/nesc/redline.rb +11 -2
  375. data/lib/pubid/ieee/identifiers/nesc/standard.rb +18 -3
  376. data/lib/pubid/ieee/identifiers/nesc.rb +1 -0
  377. data/lib/pubid/ieee/identifiers/parenthetical_identifier.rb +3 -3
  378. data/lib/pubid/ieee/identifiers/project_draft_identifier.rb +14 -6
  379. data/lib/pubid/ieee/identifiers/redlined_standard.rb +4 -4
  380. data/lib/pubid/ieee/identifiers/si_standard.rb +4 -1
  381. data/lib/pubid/ieee/identifiers/standard.rb +4 -1
  382. data/lib/pubid/ieee/identifiers/supplement_identifier.rb +26 -7
  383. data/lib/pubid/ieee/identifiers.rb +2 -1
  384. data/lib/pubid/ieee/ire/builder.rb +7 -6
  385. data/lib/pubid/ieee/ire/identifier.rb +48 -33
  386. data/lib/pubid/ieee/ire/parser.rb +1 -1
  387. data/lib/pubid/ieee/nesc/builder.rb +33 -20
  388. data/lib/pubid/ieee/nesc/parser.rb +16 -15
  389. data/lib/pubid/ieee/parser.rb +462 -44
  390. data/lib/pubid/ieee/pre_parser.rb +1 -1
  391. data/lib/pubid/ieee/renderer.rb +158 -57
  392. data/lib/pubid/ieee/typed_stages.rb +12 -1
  393. data/lib/pubid/ieee/urn_generator.rb +35 -8
  394. data/lib/pubid/ieee.rb +63 -4
  395. data/lib/pubid/ieee_debug.rb +1 -0
  396. data/lib/pubid/ietf/builder.rb +78 -0
  397. data/lib/pubid/ietf/identifier.rb +5 -0
  398. data/lib/pubid/ietf/identifiers/base.rb +72 -0
  399. data/lib/pubid/ietf/identifiers/bcp.rb +36 -0
  400. data/lib/pubid/ietf/identifiers/fyi.rb +36 -0
  401. data/lib/pubid/ietf/identifiers/internet_draft.rb +45 -0
  402. data/lib/pubid/ietf/identifiers/rfc.rb +27 -0
  403. data/lib/pubid/ietf/identifiers/serialization.rb +51 -0
  404. data/lib/pubid/ietf/identifiers/std.rb +36 -0
  405. data/lib/pubid/ietf/identifiers.rb +14 -0
  406. data/lib/pubid/ietf/parser.rb +61 -0
  407. data/lib/pubid/ietf/renderer.rb +64 -0
  408. data/lib/pubid/ietf/urn_generator.rb +37 -0
  409. data/lib/pubid/ietf/urn_parser.rb +43 -0
  410. data/lib/pubid/ietf.rb +86 -0
  411. data/lib/pubid/iho/identifier.rb +1 -3
  412. data/lib/pubid/iho/identifiers/base.rb +11 -10
  413. data/lib/pubid/iho/identifiers/bibliographic.rb +1 -1
  414. data/lib/pubid/iho/identifiers/circular_letter.rb +1 -1
  415. data/lib/pubid/iho/identifiers/miscellaneous.rb +1 -1
  416. data/lib/pubid/iho/identifiers/publication.rb +1 -1
  417. data/lib/pubid/iho/identifiers/standard.rb +1 -1
  418. data/lib/pubid/iho/identifiers.rb +0 -1
  419. data/lib/pubid/iho/parser.rb +1 -1
  420. data/lib/pubid/iho.rb +8 -7
  421. data/lib/pubid/isbn/builder.rb +44 -0
  422. data/lib/pubid/isbn/check_digit.rb +53 -0
  423. data/lib/pubid/isbn/identifier.rb +81 -0
  424. data/lib/pubid/isbn/identifiers/book.rb +30 -0
  425. data/lib/pubid/isbn/identifiers.rb +9 -0
  426. data/lib/pubid/isbn/parser.rb +45 -0
  427. data/lib/pubid/isbn/renderer.rb +16 -0
  428. data/lib/pubid/isbn.rb +53 -0
  429. data/lib/pubid/iso/builder.rb +9 -4
  430. data/lib/pubid/iso/bundled_identifier.rb +5 -3
  431. data/lib/pubid/iso/combined_identifier.rb +6 -4
  432. data/lib/pubid/iso/identifier.rb +29 -8
  433. data/lib/pubid/iso/identifiers/directives_supplement.rb +12 -12
  434. data/lib/pubid/iso/normalizer.rb +2 -2
  435. data/lib/pubid/iso/parser.rb +17 -13
  436. data/lib/pubid/iso/supplement_identifier.rb +19 -6
  437. data/lib/pubid/iso/urn_generator.rb +4 -1
  438. data/lib/pubid/iso/urn_parser.rb +1 -1
  439. data/lib/pubid/iso.rb +7 -1
  440. data/lib/pubid/itu/builder.rb +230 -29
  441. data/lib/pubid/itu/components/code.rb +48 -5
  442. data/lib/pubid/itu/components/designation.rb +35 -0
  443. data/lib/pubid/itu/components.rb +1 -0
  444. data/lib/pubid/itu/identifier.rb +1 -3
  445. data/lib/pubid/itu/identifiers/addendum.rb +15 -0
  446. data/lib/pubid/itu/identifiers/amendment.rb +7 -29
  447. data/lib/pubid/itu/identifiers/annex.rb +13 -2
  448. data/lib/pubid/itu/identifiers/annex_of_recommendation.rb +87 -0
  449. data/lib/pubid/itu/identifiers/appendix_of_recommendation.rb +92 -0
  450. data/lib/pubid/itu/identifiers/base.rb +357 -25
  451. data/lib/pubid/itu/identifiers/combined_identifier.rb +99 -23
  452. data/lib/pubid/itu/identifiers/corrigendum.rb +16 -23
  453. data/lib/pubid/itu/identifiers/errata.rb +15 -0
  454. data/lib/pubid/itu/identifiers/handbook.rb +38 -0
  455. data/lib/pubid/itu/identifiers/question.rb +63 -0
  456. data/lib/pubid/itu/identifiers/recommendation.rb +3 -2
  457. data/lib/pubid/itu/identifiers/report.rb +39 -0
  458. data/lib/pubid/itu/identifiers/special_publication.rb +3 -1
  459. data/lib/pubid/itu/identifiers/standard_serialization.rb +58 -0
  460. data/lib/pubid/itu/identifiers/supplement.rb +119 -14
  461. data/lib/pubid/itu/identifiers.rb +10 -1
  462. data/lib/pubid/itu/model.rb +1 -1
  463. data/lib/pubid/itu/parser.rb +481 -27
  464. data/lib/pubid/itu/urn_generator.rb +36 -6
  465. data/lib/pubid/itu/urn_parser.rb +8 -0
  466. data/lib/pubid/itu.rb +12 -3
  467. data/lib/pubid/jcgm/builder.rb +24 -14
  468. data/lib/pubid/jcgm/identifier.rb +10 -0
  469. data/lib/pubid/jcgm/identifiers/amendment.rb +0 -2
  470. data/lib/pubid/jcgm/identifiers/corrigendum.rb +34 -0
  471. data/lib/pubid/jcgm/identifiers/gum_guide.rb +3 -3
  472. data/lib/pubid/jcgm/identifiers/meeting.rb +47 -0
  473. data/lib/pubid/jcgm/identifiers.rb +2 -0
  474. data/lib/pubid/jcgm/parser.rb +74 -7
  475. data/lib/pubid/jcgm/renderer.rb +33 -12
  476. data/lib/pubid/jcgm/single_identifier.rb +84 -6
  477. data/lib/pubid/jcgm/supplement_identifier.rb +39 -4
  478. data/lib/pubid/jcgm/urn_generator.rb +21 -13
  479. data/lib/pubid/jcgm/urn_parser.rb +164 -10
  480. data/lib/pubid/jcgm.rb +10 -0
  481. data/lib/pubid/jis/identifier.rb +62 -3
  482. data/lib/pubid/jis/parser.rb +1 -1
  483. data/lib/pubid/jis.rb +1 -0
  484. data/lib/pubid/nist/builder.rb +29 -9
  485. data/lib/pubid/nist/caster.rb +7 -45
  486. data/lib/pubid/nist/circular_supplement_builder.rb +7 -5
  487. data/lib/pubid/nist/components/stage.rb +2 -2
  488. data/lib/pubid/nist/components/supplement.rb +5 -2
  489. data/lib/pubid/nist/configuration.rb +6 -2
  490. data/lib/pubid/nist/identifier.rb +1 -3
  491. data/lib/pubid/nist/identifiers/base.rb +133 -39
  492. data/lib/pubid/nist/identifiers/circular.rb +10 -3
  493. data/lib/pubid/nist/identifiers/circular_supplement.rb +14 -5
  494. data/lib/pubid/nist/identifiers/commercial_standard.rb +1 -1
  495. data/lib/pubid/nist/identifiers/commercial_standard_emergency.rb +1 -1
  496. data/lib/pubid/nist/identifiers/commercial_standards_monthly.rb +8 -2
  497. data/lib/pubid/nist/identifiers/crpl_report.rb +8 -2
  498. data/lib/pubid/nist/identifiers/dated_document.rb +1 -1
  499. data/lib/pubid/nist/identifiers/federal_information_processing_standards.rb +1 -1
  500. data/lib/pubid/nist/identifiers/grant_contractor_report.rb +1 -1
  501. data/lib/pubid/nist/identifiers/handbook.rb +10 -3
  502. data/lib/pubid/nist/identifiers/internal_report.rb +13 -6
  503. data/lib/pubid/nist/identifiers/letter_circular.rb +1 -1
  504. data/lib/pubid/nist/identifiers/miscellaneous_publication.rb +15 -7
  505. data/lib/pubid/nist/identifiers/monograph.rb +15 -8
  506. data/lib/pubid/nist/identifiers/ncstar.rb +2 -2
  507. data/lib/pubid/nist/identifiers/nsrds.rb +1 -1
  508. data/lib/pubid/nist/identifiers/owmwp.rb +1 -1
  509. data/lib/pubid/nist/identifiers/report.rb +15 -7
  510. data/lib/pubid/nist/identifiers/special_publication.rb +10 -1
  511. data/lib/pubid/nist/identifiers/technical_note.rb +1 -1
  512. data/lib/pubid/nist/identifiers.rb +0 -1
  513. data/lib/pubid/nist/parser.rb +27 -4
  514. data/lib/pubid/nist/preprocessor.rb +15 -0
  515. data/lib/pubid/nist/router.rb +2 -2
  516. data/lib/pubid/nist/series/ir.rb +0 -3
  517. data/lib/pubid/nist/supplement_identifier.rb +24 -9
  518. data/lib/pubid/nist.rb +19 -13
  519. data/lib/pubid/oasis/builder.rb +79 -0
  520. data/lib/pubid/oasis/identifier.rb +160 -0
  521. data/lib/pubid/oasis/identifiers/standard.rb +29 -0
  522. data/lib/pubid/oasis/identifiers.rb +9 -0
  523. data/lib/pubid/oasis/parser.rb +34 -0
  524. data/lib/pubid/oasis/renderer.rb +29 -0
  525. data/lib/pubid/oasis/urn_generator.rb +25 -0
  526. data/lib/pubid/oasis/urn_parser.rb +22 -0
  527. data/lib/pubid/oasis.rb +80 -0
  528. data/lib/pubid/ogc/builder.rb +33 -0
  529. data/lib/pubid/ogc/identifier.rb +81 -0
  530. data/lib/pubid/ogc/identifiers/document.rb +26 -0
  531. data/lib/pubid/ogc/identifiers.rb +10 -0
  532. data/lib/pubid/ogc/parser.rb +40 -0
  533. data/lib/pubid/ogc/renderer.rb +33 -0
  534. data/lib/pubid/ogc/urn_generator.rb +14 -0
  535. data/lib/pubid/ogc/urn_parser.rb +22 -0
  536. data/lib/pubid/ogc.rb +72 -0
  537. data/lib/pubid/oiml/builder.rb +36 -8
  538. data/lib/pubid/oiml/identifier.rb +9 -1
  539. data/lib/pubid/oiml/identifiers/basic_publication.rb +2 -0
  540. data/lib/pubid/oiml/identifiers/bulletin.rb +36 -0
  541. data/lib/pubid/oiml/identifiers/code_number.rb +90 -0
  542. data/lib/pubid/oiml/identifiers/document.rb +2 -0
  543. data/lib/pubid/oiml/identifiers/expert_report.rb +2 -0
  544. data/lib/pubid/oiml/identifiers/guide.rb +2 -0
  545. data/lib/pubid/oiml/identifiers/recommendation.rb +2 -0
  546. data/lib/pubid/oiml/identifiers/seminar_report.rb +2 -0
  547. data/lib/pubid/oiml/identifiers/vocabulary.rb +2 -0
  548. data/lib/pubid/oiml/identifiers.rb +1 -0
  549. data/lib/pubid/oiml/parser.rb +12 -13
  550. data/lib/pubid/oiml/renderer.rb +13 -13
  551. data/lib/pubid/oiml/single_identifier.rb +41 -37
  552. data/lib/pubid/oiml/supplement_identifier.rb +36 -9
  553. data/lib/pubid/oiml/urn_parser.rb +23 -6
  554. data/lib/pubid/oiml.rb +10 -0
  555. data/lib/pubid/omg/builder.rb +20 -0
  556. data/lib/pubid/omg/identifier.rb +61 -0
  557. data/lib/pubid/omg/identifiers/specification.rb +32 -0
  558. data/lib/pubid/omg/identifiers.rb +9 -0
  559. data/lib/pubid/omg/parser.rb +93 -0
  560. data/lib/pubid/omg/renderer.rb +31 -0
  561. data/lib/pubid/omg.rb +52 -0
  562. data/lib/pubid/parser/grammar.rb +74 -0
  563. data/lib/pubid/parser.rb +2 -0
  564. data/lib/pubid/parsers/mr_string.rb +120 -7
  565. data/lib/pubid/plateau/builder.rb +8 -4
  566. data/lib/pubid/plateau/identifier.rb +1 -3
  567. data/lib/pubid/plateau/identifiers/base.rb +1 -6
  568. data/lib/pubid/plateau/identifiers/handbook.rb +1 -1
  569. data/lib/pubid/plateau/identifiers/technical_report.rb +1 -1
  570. data/lib/pubid/plateau/identifiers.rb +0 -1
  571. data/lib/pubid/plateau/parser.rb +16 -6
  572. data/lib/pubid/plateau/renderer.rb +1 -1
  573. data/lib/pubid/plateau/supplement_identifier.rb +11 -11
  574. data/lib/pubid/plateau.rb +14 -4
  575. data/lib/pubid/prefixes_support.rb +1 -0
  576. data/lib/pubid/renderers/annotator.rb +233 -0
  577. data/lib/pubid/renderers/base.rb +13 -0
  578. data/lib/pubid/renderers/directives_renderer.rb +3 -3
  579. data/lib/pubid/renderers/human_readable.rb +6 -4
  580. data/lib/pubid/renderers/mr_string.rb +54 -6
  581. data/lib/pubid/renderers/supplement_renderer.rb +1 -1
  582. data/lib/pubid/renderers.rb +1 -0
  583. data/lib/pubid/rendering/numbering.rb +25 -7
  584. data/lib/pubid/rendering/supplement.rb +4 -4
  585. data/lib/pubid/rendering.rb +1 -0
  586. data/lib/pubid/sae/builder.rb +1 -1
  587. data/lib/pubid/sae/identifier.rb +1 -3
  588. data/lib/pubid/sae/identifiers/base.rb +19 -9
  589. data/lib/pubid/sae/identifiers.rb +0 -1
  590. data/lib/pubid/sae/parser.rb +1 -1
  591. data/lib/pubid/sae.rb +4 -3
  592. data/lib/pubid/schema/declaration.rb +33 -0
  593. data/lib/pubid/schema/error.rb +14 -0
  594. data/lib/pubid/schema/identifier_type.rb +23 -0
  595. data/lib/pubid/schema/loader.rb +117 -0
  596. data/lib/pubid/schema/typed_stage.rb +24 -0
  597. data/lib/pubid/schema.rb +25 -0
  598. data/lib/pubid/tgpp/builder.rb +41 -0
  599. data/lib/pubid/tgpp/identifier.rb +107 -0
  600. data/lib/pubid/tgpp/identifiers/technical_report.rb +32 -0
  601. data/lib/pubid/tgpp/identifiers/technical_specification.rb +33 -0
  602. data/lib/pubid/tgpp/identifiers.rb +11 -0
  603. data/lib/pubid/tgpp/parser.rb +78 -0
  604. data/lib/pubid/tgpp/renderer.rb +52 -0
  605. data/lib/pubid/tgpp/urn_generator.rb +39 -0
  606. data/lib/pubid/tgpp/urn_parser.rb +30 -0
  607. data/lib/pubid/tgpp.rb +84 -0
  608. data/lib/pubid/type_resolver.rb +74 -0
  609. data/lib/pubid/un/builder.rb +42 -0
  610. data/lib/pubid/un/identifier.rb +40 -0
  611. data/lib/pubid/un/identifiers/document.rb +24 -0
  612. data/lib/pubid/un/identifiers.rb +9 -0
  613. data/lib/pubid/un/parser.rb +25 -0
  614. data/lib/pubid/un/renderer.rb +11 -0
  615. data/lib/pubid/un.rb +47 -0
  616. data/lib/pubid/urn_parser/errors.rb +8 -1
  617. data/lib/pubid/urn_parser.rb +1 -0
  618. data/lib/pubid/utils.rb +1 -0
  619. data/lib/pubid/version.rb +1 -1
  620. data/lib/pubid/w3c/builder.rb +55 -0
  621. data/lib/pubid/w3c/identifier.rb +130 -0
  622. data/lib/pubid/w3c/identifiers/candidate_recommendation.rb +31 -0
  623. data/lib/pubid/w3c/identifiers/candidate_recommendation_draft.rb +31 -0
  624. data/lib/pubid/w3c/identifiers/draft_note.rb +31 -0
  625. data/lib/pubid/w3c/identifiers/note.rb +30 -0
  626. data/lib/pubid/w3c/identifiers/obsolete_recommendation.rb +31 -0
  627. data/lib/pubid/w3c/identifiers/proposed_edited_recommendation.rb +31 -0
  628. data/lib/pubid/w3c/identifiers/proposed_recommendation.rb +31 -0
  629. data/lib/pubid/w3c/identifiers/recommendation.rb +31 -0
  630. data/lib/pubid/w3c/identifiers/standard.rb +12 -0
  631. data/lib/pubid/w3c/identifiers/superseded_recommendation.rb +31 -0
  632. data/lib/pubid/w3c/identifiers/working_draft.rb +30 -0
  633. data/lib/pubid/w3c/identifiers.rb +25 -0
  634. data/lib/pubid/w3c/parser.rb +43 -0
  635. data/lib/pubid/w3c/renderer.rb +35 -0
  636. data/lib/pubid/w3c/urn_generator.rb +18 -0
  637. data/lib/pubid/w3c/urn_parser.rb +36 -0
  638. data/lib/pubid/w3c.rb +68 -0
  639. data/lib/pubid/xsf/builder.rb +17 -0
  640. data/lib/pubid/xsf/identifier.rb +66 -0
  641. data/lib/pubid/xsf/identifiers/xep.rb +31 -0
  642. data/lib/pubid/xsf/identifiers.rb +9 -0
  643. data/lib/pubid/xsf/parser.rb +38 -0
  644. data/lib/pubid/xsf/renderer.rb +23 -0
  645. data/lib/pubid/xsf/urn_generator.rb +13 -0
  646. data/lib/pubid/xsf/urn_parser.rb +18 -0
  647. data/lib/pubid/xsf.rb +82 -0
  648. data/lib/pubid.rb +424 -22
  649. data/lib/tasks/conformance.rake +34 -0
  650. data/lib/tasks/docs.rake +3 -3
  651. data/lib/tasks/schema.rake +88 -0
  652. data/schema/core/joint_prefixes.yaml +24 -0
  653. data/schema/iec.yaml +1017 -0
  654. data/schema/iso.yaml +1586 -0
  655. data/schema/schema.schema.yaml +96 -0
  656. metadata +241 -19
  657. data/archived-gems/pubid-ccsds/update_codes.yaml +0 -1
  658. data/archived-gems/pubid-iec/stages.yaml +0 -129
  659. data/archived-gems/pubid-iec/update_codes.yaml +0 -67
  660. data/archived-gems/pubid-ieee/update_codes.yaml +0 -104
  661. data/archived-gems/pubid-iso/stages.yaml +0 -106
  662. data/archived-gems/pubid-iso/update_codes.yaml +0 -4
  663. data/archived-gems/pubid-itu/i18n.yaml +0 -13
  664. data/archived-gems/pubid-itu/series.yaml +0 -42
  665. data/archived-gems/pubid-nist/publishers.yaml +0 -6
  666. data/archived-gems/pubid-nist/update_codes.yaml +0 -93
  667. data/archived-gems/pubid-plateau/update_codes.yaml +0 -6
  668. data/lib/pubid/cen_cenelec/supplement_identifier.rb +0 -48
  669. data/lib/pubid/cie/components/code.rb +0 -80
  670. data/lib/pubid/iala/identifiers/base.rb +0 -14
  671. data/lib/pubid/iec/components/code.rb +0 -36
  672. /data/{archived-gems/pubid-nist → data/nist}/stages.yaml +0 -0
@@ -2,6 +2,74 @@
2
2
 
3
3
  module Pubid
4
4
  class Identifier < Lutaml::Model::Serializable
5
+ # Components that serialize as a bare scalar when they carry only their
6
+ # single significant field: attribute name => the key the scalar is emitted
7
+ # under. `date` is RENAMED to `year`, matching the flat shape ISO and IEC
8
+ # already emit through their own converters.
9
+ #
10
+ # `stage_iteration` is a Components::Iteration, which holds one `:string`
11
+ # field and whose `to_s`/`render` return it — degenerate by construction.
12
+ #
13
+ # Every flavor INHERITS the attribute, so the reach of this entry is not the
14
+ # set of flavors that map the key but the set that ever POPULATES it. That
15
+ # was measured over every flavor's whole pass-fixture corpus (~99k ids):
16
+ # only ISO does, 91 times, and ISO emits the bare scalar through its own
17
+ # converter — which the inflation guard below defers to — so its serialized
18
+ # shape is unchanged (verified byte-identical over all 7,573 ISO ids). IEC
19
+ # is the flavor this entry is for; it dropped its converter pair for a
20
+ # plain map. For every other flavor the `hash.key?` guard never fires.
21
+ FLAT_SCALAR_COMPONENTS = {
22
+ edition: "edition",
23
+ date: "year",
24
+ stage_iteration: "stage_iteration",
25
+ }.freeze
26
+
27
+ # The one field each of those components degenerates to.
28
+ FLAT_SCALAR_FIELDS = {
29
+ edition: :number,
30
+ date: :year,
31
+ stage_iteration: :number,
32
+ }.freeze
33
+
34
+ # The one field each degenerate component collapses to, keyed by the
35
+ # component CLASS rather than the attribute name — so a collection
36
+ # attribute (`languages`, `copublishers`) and a flavor subclass
37
+ # (Pubid::Iec::Components::Publisher) are both covered by one entry.
38
+ #
39
+ # `Components::Code` is deliberately absent. A String left in `number`
40
+ # renders and serializes correctly today, and coercing it would turn
41
+ # `to_hash` from {"number" => "1000"} into {"number" => {"value" => "1000"}}
42
+ # on the five flavors that still declare a Code (iso, nist, csa, sae,
43
+ # ccsds) — an index wire-format change, and one the `number` retype tranches
44
+ # sequence to land last. The two entries here are the components that RAISE
45
+ # when a String reaches them, so coercing them can only repair.
46
+ #
47
+ # IEC used to be a sixth, and its removal shows the cost of the split the
48
+ # other way round: because `parse` gave a Code and `new` left a String, a
49
+ # hand-built IEC identifier was never `==` the parsed one. Retyping the
50
+ # attribute — not coercing the value — is what reconciles the two paths.
51
+ DEGENERATE_COMPONENT_FIELDS = {
52
+ Components::Language => :code,
53
+ Components::Publisher => :body,
54
+ }.freeze
55
+
56
+ # Keys lutaml reads out of the attribute hash itself
57
+ # (Serializable#extract_register_id), so they are reserved rather than
58
+ # unknown and must survive the unknown-key check.
59
+ RESERVED_INIT_KEYS = %i[lutaml_register].freeze
60
+
61
+ # Scalar aliases accepted by the constructor for a component-valued
62
+ # attribute: alias => [target attribute, the component field it fills].
63
+ # `year` is the one the issue names — it is a READER over `date`, never an
64
+ # attribute, on every date-based flavor, so `new(number: "1000", year: 2023)`
65
+ # used to build an identifier with no year and say nothing.
66
+ SCALAR_ATTRIBUTE_ALIASES = {
67
+ year: [:date, :year],
68
+ month: [:date, :month],
69
+ day: [:date, :day],
70
+ }.freeze
71
+
72
+
5
73
  class << self
6
74
  def format_registry
7
75
  @format_registry || superclass&.format_registry
@@ -13,19 +81,326 @@ module Pubid
13
81
  # `key_value` polymorphic_map reads `_type` only to VALIDATE; it does not
14
82
  # re-instantiate the concrete subclass. So a base-class `from_hash` would
15
83
  # return a bare base object and drop subtype-specific attributes (e.g. a
16
- # supplement's base_identifier). Route by `_type` to the concrete class
84
+ # supplement's base). Route by `_type` to the concrete class
17
85
  # named in `polymorphic_type_map`, then let its inherited from_hash (this
18
86
  # method again, where klass == self) fall through to lutaml's real work.
19
87
  #
20
88
  # This generalizes the per-flavor `*_TYPE_MAP` dispatch that ISO, JIS,
21
89
  # IEC, CCSDS and IHO each hand-rolled — one implementation, inherited by
22
- # every flavor base.
90
+ # every flavor base. Cross-flavor dispatch (a nested adopted identifier
91
+ # whose _type belongs to a different flavor) is delegated to
92
+ # {TypeResolver}, which knows about every registered flavor.
23
93
  def from_hash(data, options = {})
24
- type = data && (data["_type"] || data[:_type])
25
- klass = polymorphic_type_map[type]
94
+ klass = concrete_class_for(data)
26
95
  return klass.from_hash(data, options) if klass && klass != self
27
96
 
28
- super
97
+ super(inflate_scalar_components(data), options)
98
+ end
99
+
100
+ # Accept the flat scalar form that {#to_hash} now emits, and the nested
101
+ # form every stored row was written with. lutaml casts a Hash into a
102
+ # component but passes a String straight through (Attribute#cast_element),
103
+ # so the flat form has to be re-inflated here or it would be assigned raw.
104
+ #
105
+ # Reading BOTH shapes is what keeps published relaton-data indexes
106
+ # deserializing across this change.
107
+ def inflate_scalar_components(data)
108
+ return data unless data.is_a?(::Hash)
109
+
110
+ flat_scalar_components.each_with_object(data.dup) do |(attr, key), acc|
111
+ inflate_scalar_component(acc, attr, key)
112
+ end
113
+ end
114
+
115
+ # The flat-scalar table of this class: FLAT_SCALAR_COMPONENTS, plus the
116
+ # entries a flavor adds for its own classes. CEN/CENELEC adds
117
+ # `publisher` and `copublishers`; a table shared by every flavor would
118
+ # change the wire format of flavors with a published index (IEEE).
119
+ def flat_scalar_components
120
+ FLAT_SCALAR_COMPONENTS
121
+ end
122
+
123
+ # The one field each entry of #flat_scalar_components degenerates to.
124
+ def flat_scalar_fields
125
+ FLAT_SCALAR_FIELDS
126
+ end
127
+
128
+ # Hook: rewrite +hash+, the canonical serialization of +model+ (an
129
+ # instance of this class), into a shorter flavor-specific form. The
130
+ # canonicalizer calls it for every identifier, nested ones included, so
131
+ # it also reaches a `base` or an adopted document. A class that
132
+ # overrides it must also override #inflate_scalar_components to read the
133
+ # short form back. The default changes nothing.
134
+ def compact_hash(_model, _hash); end
135
+
136
+ # @api private
137
+ def inflate_scalar_component(data, attr_name, flat_key)
138
+ return unless component_attribute?(attr_name)
139
+ # A flavor with its OWN converter for the flat key (IEC's
140
+ # year_from_kv) already knows how to read it, and inflating would fight
141
+ # it. A plain `map "edition", to: :edition` is not such a converter —
142
+ # it hands the value to lutaml, which cannot cast a String into a
143
+ # component — so that one still needs inflating.
144
+ return if converted_hash_keys.include?(flat_key)
145
+ # Only when the flat key RENAMES the attribute (`date` -> `year`) can
146
+ # it collide with a different attribute. ~20 flavors model the edition
147
+ # as a real `year` attribute of their own (ASHRAE, ASME, CIE, CSA, JIS,
148
+ # OGC, IEEE …), and folding that into a `date` component silently drops
149
+ # the year on every round trip — the same collision
150
+ # `fold_scalar_aliases` guards against on the constructor path.
151
+ # `edition` keeps its own name, so it is never a collision.
152
+ return if flat_key != attr_name.to_s && declared_attribute?(flat_key)
153
+
154
+ key = data.key?(flat_key) ? flat_key : flat_key.to_sym
155
+ value = data[key]
156
+ return if value.nil? || value.is_a?(::Hash)
157
+
158
+ field = flat_scalar_fields.fetch(attr_name).to_s
159
+ if value.is_a?(::Array)
160
+ # A collection of components (`copublishers`) flattens to a list of
161
+ # scalars; anything else in a list is not the flat form.
162
+ return unless collection_attribute?(attr_name)
163
+ return unless value.all? { |v| scalar_value?(v) }
164
+
165
+ data.delete(key)
166
+ data[attr_name.to_s] = value.map { |v| { field => v.to_s } }
167
+ return
168
+ end
169
+
170
+ data.delete(key)
171
+ data[attr_name.to_s] = { field => value.to_s }
172
+ end
173
+
174
+ # True when +name+ is declared as a collection on this class.
175
+ def collection_attribute?(name)
176
+ attr = attributes[name] || attributes[name.to_s]
177
+ attr ? attr.collection? : false
178
+ end
179
+
180
+ # @api private
181
+ def scalar_value?(value)
182
+ !(value.nil? || value.is_a?(::Hash) || value.is_a?(::Array))
183
+ end
184
+
185
+ # True when +name+ is a declared attribute on this class, under either a
186
+ # Symbol or a String key.
187
+ def declared_attribute?(name)
188
+ attributes.key?(name.to_sym) || attributes.key?(name.to_s)
189
+ end
190
+
191
+ # True when +name+ is declared on this class as a component (a nested
192
+ # Serializable), rather than a plain scalar. The coercions are
193
+ # type-aware: ~16 classes declare `edition` as a plain :string and must
194
+ # keep the scalar they were given.
195
+ def component_attribute?(name)
196
+ attr = attributes[name] || attributes[name.to_s]
197
+ return false unless attr
198
+
199
+ type = attr.type
200
+ type.is_a?(Class) && type <= Lutaml::Model::Serializable
201
+ rescue StandardError
202
+ false
203
+ end
204
+
205
+ # Serialization keys this class reads through a converter of its own
206
+ # (a `key_value` rule declared with `with: {to:, from:}`). Those keys are
207
+ # already handled and must not be rewritten underneath the flavor.
208
+ #
209
+ # Empty for a flavor with no `key_value` block, which serializes
210
+ # attribute names directly.
211
+ def converted_hash_keys
212
+ @converted_hash_keys ||=
213
+ begin
214
+ rules = mappings[:hash]&.mappings || []
215
+ rules.reject { |rule| rule.custom_methods.nil? || rule.custom_methods.empty? }
216
+ .map { |rule| rule.name.to_s }
217
+ rescue StandardError
218
+ []
219
+ end
220
+ end
221
+
222
+ # The blessed constructor path (pubid#360 item 3).
223
+ #
224
+ # Two jobs, both of which lutaml declines to do:
225
+ #
226
+ # * coerce a scalar into the component the attribute declares, so
227
+ # `edition: 2` and `year: 2023` work as written;
228
+ # * refuse a key this class does not know, instead of dropping it.
229
+ #
230
+ # The refusal is the important half. lutaml reads only declared attribute
231
+ # names out of the hash (Serialize#initialize_attributes) and reports
232
+ # nothing about the rest, so `new(number: "1000", year: 2023)` returned an
233
+ # identifier with no year at all and no diagnostic — which is exactly how
234
+ # the issue was filed. A dropped key is a silent wrong answer; a raise is
235
+ # a loud one.
236
+ #
237
+ # Safe to apply here because neither internal rebuild path routes
238
+ # attribute keys through the constructor: `from_hash` calls `new` with no
239
+ # attributes and assigns through setters afterwards, and `#exclude`
240
+ # rebuilds from `self.class.attributes` — only declared names, by
241
+ # construction.
242
+ #
243
+ # @param attrs [Hash] caller-supplied attributes
244
+ # @return [Hash] the same attributes, coerced
245
+ # @raise [ArgumentError] naming every key this class does not declare
246
+ def normalize_init_attributes(attrs)
247
+ return attrs.dup unless attrs.is_a?(::Hash)
248
+
249
+ normalized = attrs.dup
250
+ fold_scalar_aliases(normalized)
251
+ coerce_component_scalars(normalized)
252
+ reject_unknown_keys(normalized)
253
+ normalized
254
+ end
255
+
256
+ # Fold `year:` / `month:` / `day:` into the `date` component. Only when
257
+ # the alias is not itself a declared attribute — ~20 flavors model the
258
+ # edition as a plain `year` attribute, and folding there would destroy it.
259
+ def fold_scalar_aliases(attrs)
260
+ SCALAR_ATTRIBUTE_ALIASES.each do |alias_name, (target, field)|
261
+ key = attrs.key?(alias_name) ? alias_name : alias_name.to_s
262
+ next unless attrs.key?(key)
263
+ next if attributes.key?(alias_name) || attributes.key?(alias_name.to_s)
264
+ next unless component_attribute?(target)
265
+
266
+ value = attrs.delete(key)
267
+ next if value.nil?
268
+
269
+ existing = attrs[target] || attrs[target.to_s] || {}
270
+ existing = component_to_hash(existing)
271
+ attrs.delete(target.to_s)
272
+ attrs[target] = existing.merge(field => value.to_s)
273
+ end
274
+ end
275
+
276
+ # Wrap a scalar in the component its attribute declares. A flavor that
277
+ # declares the attribute as a plain :string keeps the scalar untouched.
278
+ def coerce_component_scalars(attrs)
279
+ FLAT_SCALAR_FIELDS.each do |attr_name, field|
280
+ key = attrs.key?(attr_name) ? attr_name : attr_name.to_s
281
+ next unless attrs.key?(key)
282
+ next unless component_attribute?(attr_name)
283
+
284
+ value = attrs[key]
285
+ next if value.nil? || value.is_a?(::Hash) ||
286
+ value.is_a?(Lutaml::Model::Serialize)
287
+
288
+ attrs[key] = { field => value.to_s }
289
+ end
290
+
291
+ coerce_degenerate_components(attrs)
292
+ end
293
+
294
+ # Wrap a scalar in a degenerate component wherever the attribute declares
295
+ # one, INCLUDING a collection — the residue of pubid#360 item 3.
296
+ #
297
+ # lutaml casts a Hash element into the component but passes a String
298
+ # through untouched, so `languages: ["en"]` stored raw Strings and then
299
+ # raised in the renderer (`undefined method 'code'`) and in the URN
300
+ # generator. The Hash form `languages: [{code: "en"}]` already worked, so
301
+ # this is a coercion gap, not a lutaml limit.
302
+ def coerce_degenerate_components(attrs)
303
+ attrs.keys.each do |key|
304
+ next unless key.is_a?(::Symbol) || key.is_a?(::String)
305
+
306
+ field = degenerate_field_for(key)
307
+ next unless field
308
+
309
+ attrs[key] = wrap_degenerate(attrs[key], field)
310
+ end
311
+ end
312
+
313
+ # The degenerate field of the component +key+ declares, or nil when it
314
+ # declares none. A collection attribute reports its ELEMENT class, so one
315
+ # lookup covers both shapes; the `<=` test is what admits a flavor
316
+ # subclass such as Pubid::Iec::Components::Publisher.
317
+ def degenerate_field_for(key)
318
+ type = declared_component_type(key)
319
+ return nil unless type
320
+
321
+ DEGENERATE_COMPONENT_FIELDS.find { |klass, _| type <= klass }&.last
322
+ end
323
+
324
+ # The component class +key+ declares, or nil when the attribute is
325
+ # absent or declared as a plain scalar.
326
+ def declared_component_type(key)
327
+ type = (attributes[key.to_sym] || attributes[key.to_s])&.type
328
+ type.is_a?(::Class) ? type : nil
329
+ rescue StandardError
330
+ nil
331
+ end
332
+
333
+ # A scalar becomes `{field => scalar}` and an Array is mapped
334
+ # element-wise. nil, a Hash and an already-built component pass through
335
+ # untouched, so every shape that worked before still works.
336
+ def wrap_degenerate(value, field)
337
+ if value.is_a?(::Array)
338
+ return value.map { |v| wrap_degenerate(v, field) }
339
+ end
340
+ return value if value.nil? || value.is_a?(::Hash) ||
341
+ value.is_a?(Lutaml::Model::Serialize)
342
+
343
+ { field => value.to_s }
344
+ end
345
+
346
+ def reject_unknown_keys(attrs)
347
+ allowed = RESERVED_INIT_KEYS + extra_init_keys
348
+ unknown = attrs.keys.reject do |key|
349
+ # A key that is neither a Symbol nor a String cannot name an
350
+ # attribute, so it is unknown by definition — and `to_sym` on it
351
+ # would raise NoMethodError instead of reporting that.
352
+ next false unless key.is_a?(::Symbol) || key.is_a?(::String)
353
+
354
+ allowed.include?(key.to_sym) || declared_attribute?(key)
355
+ end
356
+ return if unknown.empty?
357
+
358
+ raise ArgumentError,
359
+ "unknown attribute#{'s' if unknown.size > 1} for " \
360
+ "#{name || self}: #{unknown.map(&:to_s).sort.join(', ')}"
361
+ end
362
+
363
+ # Constructor parameters a flavor accepts that are not lutaml attributes.
364
+ # IEEE takes `code:` and `draft:` this way — runtime-only values its
365
+ # `initialize` turns into component objects. Override to widen.
366
+ #
367
+ # @return [Array<Symbol>]
368
+ def extra_init_keys
369
+ []
370
+ end
371
+
372
+ # @api private
373
+ def component_to_hash(value)
374
+ case value
375
+ when ::Hash then value.transform_keys(&:to_sym)
376
+ when Lutaml::Model::Serialize then value.to_hash.transform_keys(&:to_sym)
377
+ else {}
378
+ end
379
+ end
380
+
381
+ # lutaml's nested polymorphic cast (Attribute#cast → apply_mappings)
382
+ # bypasses {from_hash}, so cross-flavor dispatch wouldn't fire for a
383
+ # nested attribute that carries another flavor's _type. Intercept
384
+ # apply_mappings and route to {from_hash} so the concrete class's own
385
+ # mappings (with their flavor-specific custom cast methods, e.g. ISO's
386
+ # number_from_kv) drive deserialization.
387
+ def apply_mappings(doc, format, options = {})
388
+ return super unless hash_with_type?(doc, format)
389
+
390
+ klass = concrete_class_for(doc)
391
+ return super unless klass && klass != self
392
+
393
+ klass.from_hash(doc, options)
394
+ end
395
+
396
+ # Resolve a polymorphic _type to the concrete class that owns it:
397
+ # this flavor's own map first, then any registered flavor's map via
398
+ # {TypeResolver}. Returns nil for blank or unknown types.
399
+ def concrete_class_for(data)
400
+ type = data && (data["_type"] || data[:_type])
401
+ return nil unless type
402
+
403
+ polymorphic_type_map[type] || ::Pubid::TypeResolver.resolve(type)
29
404
  end
30
405
 
31
406
  # Map of polymorphic_name ("pubid:iso:corrigendum") => concrete class for
@@ -33,7 +408,8 @@ module Pubid
33
408
  # `Identifiers` namespace for Pubid::Identifier descendants and unioning
34
409
  # the flavor's `identifier_types` registry (which may register classes
35
410
  # living outside that namespace, e.g. ISO's BundledIdentifier). Memoized
36
- # per class.
411
+ # per class. Public so {TypeResolver} can read another flavor's map for
412
+ # cross-flavor polymorphic dispatch.
37
413
  def polymorphic_type_map
38
414
  @polymorphic_type_map ||=
39
415
  identifier_registry_classes.each_with_object({}) do |klass, map|
@@ -44,6 +420,12 @@ module Pubid
44
420
 
45
421
  private
46
422
 
423
+ def hash_with_type?(doc, format)
424
+ format == :hash && doc.is_a?(Hash) && (doc["_type"] || doc[:_type])
425
+ end
426
+
427
+ private
428
+
47
429
  # The flavor module namespacing this base, e.g. Pubid::Iso for
48
430
  # Pubid::Iso::Identifier (and for the inherited Pubid::Iso::Identifiers::
49
431
  # Corrigendum). Always the first two name components, so it is stable
@@ -59,7 +441,22 @@ module Pubid
59
441
  def identifier_registry_classes
60
442
  (scanned_identifier_classes +
61
443
  registered_identifier_classes +
62
- additional_identifier_classes).uniq
444
+ additional_identifier_classes +
445
+ own_base_class).uniq
446
+ end
447
+
448
+ # The flavor's own Identifier base, when a builder instantiates it
449
+ # directly (BSI does, for the members of a bundled identifier). Such an
450
+ # object serializes with `_type: "pubid:<flavor>:identifier"`, and
451
+ # without an entry here that type resolves to nothing — so a nested
452
+ # polymorphic attribute typed ::Pubid::Identifier silently deserialized
453
+ # the member as the abstract root instead of the flavor class. Added
454
+ # LAST, so a concrete leaf that somehow claimed the same name still wins.
455
+ def own_base_class
456
+ return [] if self == ::Pubid::Identifier
457
+ return [] unless name && polymorphic_name
458
+
459
+ [self]
63
460
  end
64
461
 
65
462
  # Concrete identifier classes found directly in the flavor's `Identifiers`
@@ -115,8 +512,8 @@ module Pubid
115
512
  attribute :locality, Components::Locality
116
513
  attribute :typed_stage, Components::TypedStage
117
514
  attribute :all_parts, Lutaml::Model::Type::Boolean, default: false
118
- # base_identifier is declared by supplement subclasses with proper type
119
- def base_identifier
515
+ # base is declared by supplement subclasses with proper type
516
+ def base
120
517
  nil
121
518
  end
122
519
 
@@ -162,6 +559,137 @@ module Pubid
162
559
  model.class.attributes(register).each do |name, attr|
163
560
  canonicalize_attr(model, hash, name, attr) unless name == :_type
164
561
  end
562
+ flatten_scalar_components(model, hash)
563
+ # Nested components are not identifiers and have no such hook.
564
+ if model.class.respond_to?(:compact_hash)
565
+ model.class.compact_hash(model, hash)
566
+ end
567
+ end
568
+
569
+ # A component that carries only ONE meaningful value serializes as that
570
+ # value: `edition: "2"`, not `edition: {number: "2"}`; `year: "2024"`, not
571
+ # `date: {year: "2024"}`. That is the shape an index row wants to be read
572
+ # in, and the shape a caller can write without knowing pubid's component
573
+ # classes — the complaint in pubid#360 item 3.
574
+ #
575
+ # The test is per INSTANCE, not per flavor, because one flavor's corpus
576
+ # holds both shapes. Of 9,730 dated IEC identifiers, 9,728 carry a year
577
+ # alone but 10 carry a month (`IEC CA 01:2025-10`) and 2 are undated
578
+ # (`IEC 60050:--`); a flavor-wide switch would have discarded those.
579
+ #
580
+ # Flavors that already emit the flat form are untouched for free: IEC and
581
+ # ISO serialize the date as top-level year/month/day, so no "date" key ever
582
+ # reaches this method.
583
+ def flatten_scalar_components(model, hash)
584
+ flat_scalar_table(model).each do |attr_name, flat_key|
585
+ flatten_scalar_component(model, hash, attr_name, flat_key)
586
+ end
587
+ end
588
+
589
+ # The flat-scalar table of +model+'s class. The canonicalizer also runs on
590
+ # nested components, which are not identifiers and have only the shared
591
+ # tables.
592
+ def flat_scalar_table(model, fields: false)
593
+ klass = model.class
594
+ if klass.respond_to?(:flat_scalar_components)
595
+ fields ? klass.flat_scalar_fields : klass.flat_scalar_components
596
+ else
597
+ fields ? FLAT_SCALAR_FIELDS : FLAT_SCALAR_COMPONENTS
598
+ end
599
+ end
600
+
601
+ # Replace +hash+'s serialization of +attr_name+ with a bare scalar, when
602
+ # the component holds nothing but its single significant field.
603
+ def flatten_scalar_component(model, hash, attr_name, flat_key)
604
+ key = hash.key?(attr_name.to_s) ? attr_name.to_s : attr_name
605
+ return unless hash.key?(key)
606
+ return unless model.respond_to?(attr_name)
607
+
608
+ field = flat_scalar_table(model, fields: true).fetch(attr_name)
609
+ if hash[key].is_a?(::Array)
610
+ return flatten_scalar_collection(model, hash, attr_name, key, field)
611
+ end
612
+ return unless hash[key].is_a?(::Hash)
613
+
614
+ component = model.public_send(attr_name)
615
+ return unless component.is_a?(Lutaml::Model::Serialize)
616
+
617
+ scalar = degenerate_scalar(component, field)
618
+ return if scalar.nil?
619
+ # Never overwrite a key the flavor already emits under the flat name,
620
+ # and never claim a name the flavor declares as a real attribute of its
621
+ # own — ~20 flavors have a `year` attribute, and renaming `date` onto it
622
+ # would make the two indistinguishable on the way back in.
623
+ if flat_key != key.to_s
624
+ return if hash.key?(flat_key) || model.class.declared_attribute?(flat_key)
625
+ end
626
+
627
+ replace_key_in_place(hash, key, flat_key, scalar)
628
+ end
629
+
630
+ # A collection of components (`copublishers`) serializes as a list of
631
+ # scalars, but only when EVERY element is degenerate; a list that mixes
632
+ # the two shapes could not be read back. The key keeps its name.
633
+ def flatten_scalar_collection(model, hash, attr_name, key, field)
634
+ components = model.public_send(attr_name)
635
+ return unless components.is_a?(::Array) &&
636
+ components.size == hash[key].size &&
637
+ components.all?(Lutaml::Model::Serialize)
638
+
639
+ scalars = components.map { |c| degenerate_scalar(c, field) }
640
+ return if scalars.any?(&:nil?)
641
+
642
+ hash[key] = scalars
643
+ end
644
+
645
+ # Replace +old_key+ with +new_key+ WITHOUT moving it to the end.
646
+ #
647
+ # A plain delete-then-assign appends, which would reorder every key after
648
+ # the one being flattened — cosmetic for `==`, but it rewrites the byte
649
+ # order of every generated relaton-data YAML row for no reason.
650
+ def replace_key_in_place(hash, old_key, new_key, value)
651
+ if old_key.to_s == new_key
652
+ hash[old_key] = value
653
+ return
654
+ end
655
+
656
+ rebuilt = hash.each_with_object({}) do |(k, v), acc|
657
+ if k == old_key
658
+ acc[new_key] = value
659
+ else
660
+ acc[k] = v
661
+ end
662
+ end
663
+
664
+ hash.replace(rebuilt)
665
+ end
666
+
667
+ # The scalar +component+ degenerates to, or nil when it carries anything
668
+ # besides +field+.
669
+ #
670
+ # A +field+ holding another component (ISO's edition number is a
671
+ # Components::Code) is deliberately NOT flattened: the flat form would come
672
+ # back as a plain String, so `from_hash(to_hash) == parse` would break —
673
+ # the silent inequality that CLAUDE.md records as the costliest failure
674
+ # mode in this codebase, because `#matches?` is built on `==`.
675
+ def degenerate_scalar(component, field)
676
+ register = component.lutaml_register
677
+ found = nil
678
+
679
+ component.class.attributes(register).each do |name, attr|
680
+ value = component.public_send(name)
681
+ # `Utils.empty?(nil)` is false, so nil needs its own test — an
682
+ # unpopulated attribute is exactly what makes a component degenerate.
683
+ next if value.nil? || Lutaml::Model::Utils.empty?(value)
684
+ next if attr.default_set?(register, component) &&
685
+ value == attr.default(register, component)
686
+ return nil unless name == field
687
+ return nil if value.is_a?(Lutaml::Model::Serialize)
688
+
689
+ found = value
690
+ end
691
+
692
+ found&.to_s
165
693
  end
166
694
 
167
695
  # Canonicalize a single attribute against its serialized value in +hash+:
@@ -208,10 +736,13 @@ module Pubid
208
736
  end
209
737
  end
210
738
  private :canonicalize_hash, :canonicalize_attr, :default_valued?,
211
- :canonicalize_nested, :canonicalize_collection
739
+ :canonicalize_nested, :canonicalize_collection,
740
+ :flatten_scalar_components, :flatten_scalar_component,
741
+ :flatten_scalar_collection, :flat_scalar_table,
742
+ :degenerate_scalar, :replace_key_in_place
212
743
 
213
744
  def initialize(attrs = {}, options = {})
214
- attrs = attrs.dup
745
+ attrs = self.class.normalize_init_attributes(attrs)
215
746
  attrs[:_type] ||= self.class.polymorphic_name
216
747
  super
217
748
  end
@@ -229,7 +760,7 @@ module Pubid
229
760
  end
230
761
 
231
762
  def root
232
- return base_identifier.root if base_identifier
763
+ return base.root if base
233
764
 
234
765
  self
235
766
  end
@@ -246,8 +777,75 @@ module Pubid
246
777
  raise ArgumentError, "No renderer registered for format: #{format}"
247
778
  end
248
779
 
249
- context = build_rendering_context(renderer, format:, **opts)
250
- renderer.new(self).render(context:, **opts.slice(:with_edition))
780
+ # `:trademark` (IEEE) and `:with_volume` (ECMA) are render-time flags
781
+ # consumed by the renderer, not the rendering context — and some flavors
782
+ # override build_rendering_context with a strict signature, so strip them
783
+ # here rather than widening that signature.
784
+ ctx_opts = opts.except(:trademark, :with_volume)
785
+ context = build_rendering_context(renderer, format:, **ctx_opts)
786
+ render_opts = opts.slice(:with_edition, :trademark, :with_volume)
787
+ result = renderer.new(self).render(context:, **render_opts)
788
+
789
+ annotate_rendered(result, format, ctx_opts[:annotated], context)
790
+ end
791
+
792
+ # Apply semantic spans when the renderer did not.
793
+ #
794
+ # `context.annotated` reaches all 39 flavor renderers and all 39 ignore it,
795
+ # so annotation was a v1 -> v2 regression for every flavor but ISO. This is
796
+ # the single choke point every renderer already passes through, which is
797
+ # why the fallback lives here rather than in each renderer.
798
+ #
799
+ # The `<span` test is what keeps ISO's exact, render-time placement: a
800
+ # renderer that annotated itself is left alone.
801
+ def annotate_rendered(result, format, annotated, context)
802
+ return result unless annotated && format == :human
803
+ return result unless result.is_a?(String)
804
+ return result if result.include?("<span")
805
+
806
+ Renderers::Annotator.new(self, context).annotate(result)
807
+ end
808
+ private :annotate_rendered
809
+
810
+ # The same fallback, for a `to_s` that composes its string by hand instead
811
+ # of going through {#render} — ITU, CCSDS, CIE and CSA wrappers all do.
812
+ # Those never reach the choke point above, so they apply it on the way out.
813
+ #
814
+ # Every such `to_s` must also ACCEPT `annotated:`. Before they did, 13 type
815
+ # families raised on the flag (`wrong number of arguments (given 1,
816
+ # expected 0)` for the ones declaring no parameters at all), which is worse
817
+ # than a missing span: a wrapper is exactly the shape a consumer renders.
818
+ #
819
+ # This is deliberately a per-class opt-in rather than a module prepended to
820
+ # every identifier class. That was tried: prepending to ~500 ancestor
821
+ # chains made a GOST identical adoption render its adopted ISO Technical
822
+ # Report as `ISO TR …` instead of `ISO/TR …`, but only under full-suite
823
+ # load — the multi-flavor nondeterminism this file warns about elsewhere.
824
+ # An explicit call in the handful of classes that need it changes no
825
+ # ancestor chain and cannot have that effect.
826
+ #
827
+ # @param rendered [String] the plain rendering this method produced
828
+ # @param opts [Hash] the render options it was called with
829
+ # @return [String] annotated when `annotated:` was asked for, else as-is
830
+ def annotate_plain_render(rendered, **opts)
831
+ annotate_rendered(rendered, :human, opts[:annotated], nil)
832
+ end
833
+ protected :annotate_plain_render
834
+
835
+ # The tokens {Renderers::Annotator} looks for in this identifier's rendered
836
+ # string, as [attribute_name, css_class] pairs.
837
+ #
838
+ # The shared table names the components a printed identifier usually
839
+ # carries — publisher, type, number, part, year. A flavor that keeps its
840
+ # identity somewhere else answers with its own names appended: IEC's
841
+ # working documents are `technical_committee`/`wd_number`/`wd_stage`, and
842
+ # CSA's electrical code prints `cec_part`/`no_number` rather than the
843
+ # `number` it stores. Without this they carried no annotatable token at all.
844
+ #
845
+ # Appending rather than replacing keeps whatever the shared table does
846
+ # find; a token whose value is not in the string is skipped anyway.
847
+ def annotation_tokens
848
+ Renderers::Annotator::TOKENS
251
849
  end
252
850
 
253
851
  def to_s(**opts)
@@ -258,38 +856,126 @@ module Pubid
258
856
  render(format: :mr_string)
259
857
  end
260
858
 
261
- # MR string template methods — flavors override as needed
859
+ # Filesystem-/URL-safe slug derived from the MR string. Defaults to
860
+ # `to_mr_string` because every flavor except NIST already emits an
861
+ # all-lowercase, filename-safe MR (only `[a-z0-9.-]` characters, plus
862
+ # `_` as the supplement separator and `-` as the copublisher separator).
863
+ # Flavors whose MR is not slug-safe (notably NIST, whose MR format is
864
+ # fixed by the pubid standard) override this to project the MR into a
865
+ # slug-safe form.
866
+ def to_slug
867
+ to_mr_string
868
+ end
869
+
870
+ # MR string template methods — flavors override as needed.
871
+ #
872
+ # The MR format is a lossless, dot-separated, all-lowercase, filename-safe
873
+ # slug mirroring `to_s`'s structure:
874
+ #
875
+ # {publisher}[-{copublisher}…][.{type}].{number}[-{part}[-{subpart}…]]
876
+ # [.{year}[-{month}[-{day}]]|’--’}][.{edition}][.{language}-{language}…]
877
+ # [.all-parts][_supplements…]
878
+ #
879
+ # Supplements append `_{type}.{number}.{year}` recursively, so a chained
880
+ # supplement like `…/Amd 3:2016/Cor 1:2017` round-trips as
881
+ # `…_amd.3.2016_cor.1.2017`. Copublishers join with `-`
882
+ # (`iso-iec.17031-1.2020`) so the slug never contains a path separator.
883
+ # Distinct identifiers never collide on `to_mr_string` (issue #142).
262
884
  def mr_publisher
263
- publisher&.to_s
885
+ publisher&.to_s&.downcase&.tr("/", "-")
264
886
  end
265
887
 
266
888
  def mr_type
267
889
  return nil unless typed_stage
268
890
 
269
891
  code = typed_stage.type_code
270
- return nil if code.nil? || code.empty? || code == "is"
892
+ return nil if code.nil? || code.empty? || code.to_s == "is"
271
893
 
272
- code
894
+ code.to_s.downcase
273
895
  end
274
896
 
275
897
  def mr_number_with_part
276
898
  num = mr_number
277
899
  return nil unless num
278
900
 
279
- prt = mr_part
280
- prt ? "#{num}-#{prt}" : num
901
+ segments = [num]
902
+ segments << mr_part if part
903
+ segments << mr_subpart if subpart
904
+ segments.compact.join("-").downcase
905
+ end
906
+
907
+ # Join MR segments and return nil rather than "" when nothing survives.
908
+ #
909
+ # Renderers::MrString does `parts.compact.join(".")`, and `compact` removes
910
+ # nil but NOT an empty string — so a hook that returns "" contributes a
911
+ # blank segment and the slug gains a double dot. The base
912
+ # #mr_number_with_part guards this with its own `return nil unless num`;
913
+ # flavor overrides that append their own marker to `super` need the same
914
+ # guard, and this is it.
915
+ def mr_join(*segments)
916
+ joined = segments.compact.reject { |s| s.to_s.empty? }.join("-")
917
+ joined.empty? ? nil : joined
281
918
  end
282
919
 
283
920
  def mr_number
284
- number&.to_s
921
+ number&.to_s&.downcase
285
922
  end
286
923
 
287
924
  def mr_part
288
- part&.to_s
925
+ part&.to_s&.downcase
926
+ end
927
+
928
+ def mr_subpart
929
+ subpart&.to_s&.downcase
289
930
  end
290
931
 
291
932
  def mr_year
292
- date&.year&.to_s
933
+ return nil unless date
934
+ return "--" if date.respond_to?(:undated?) && date.undated?
935
+ return nil unless date.year
936
+
937
+ result = date.year.to_s
938
+ result += "-#{date.month}" if date.respond_to?(:month) && date.month
939
+ result += "-#{date.day}" if date.respond_to?(:day) && date.day
940
+ result
941
+ end
942
+
943
+ def mr_edition
944
+ return nil unless edition&.number
945
+
946
+ "ed#{edition.number}"
947
+ end
948
+
949
+ # Hook for flavors whose identity includes a volume discriminator — ECMA-269
950
+ # ed3 vol1..vol4 are four distinct documents sharing one docidentifier.
951
+ # Literally nil here, NOT `volume&.to_s`: Pubid::Nist::Identifiers::Base
952
+ # declares its own `volume` attribute with different MR semantics, so a
953
+ # generic reader would change NIST's slug. A flavor that needs the segment
954
+ # overrides this (see Pubid::Ecma::Identifier#mr_volume).
955
+ def mr_volume
956
+ nil
957
+ end
958
+
959
+ def mr_languages
960
+ return nil unless languages&.any?
961
+
962
+ # Hyphen-joined, no parens — parens are shell-/URL-unsafe.
963
+ languages.map { |l| l.code.to_s.downcase }.join("-")
964
+ end
965
+
966
+ def mr_all_parts
967
+ return nil unless all_parts
968
+
969
+ "all-parts"
970
+ end
971
+
972
+ # Hook: supplement / wrapper subclasses return the `{type}.{number}.{year}`
973
+ # suffix that distinguishes them from their base. `nil` for non-supplements.
974
+ # The base MrString renderer recurses into `base` whenever this returns
975
+ # a non-nil value, so chained supplements (Cor → Amd → IS) render fully.
976
+ # The suffix is appended with `_` (not `/`) so the MR stays filename-safe.
977
+ def mr_supplement_suffix
978
+ nil
293
979
  end
294
980
 
295
981
  # URN template methods — flavors override as needed
@@ -332,14 +1018,21 @@ module Pubid
332
1018
  end
333
1019
 
334
1020
  excluded_args = args.dup
335
- # Map :year to :date since identifiers store years inside date
336
- excluded_args << :date if excluded_args.delete(:year)
1021
+ # Map :year to :date (year-in-date flavors), but ALSO keep :year so a
1022
+ # flavor that models the edition as a plain `year` attribute (e.g. GOST)
1023
+ # has it excluded too. The loop below nils whichever name the flavor has.
1024
+ excluded_args << :date if excluded_args.include?(:year)
337
1025
 
338
1026
  attrs = self.class.attributes.each_with_object({}) do |(name, _), h|
339
1027
  value = excluded_args.include?(name) ? nil : public_send(name)
340
1028
  h[name] = exclude_from_nested(value, args)
341
1029
  end
342
- self.class.new(attrs)
1030
+ # Splat the rebuilt attributes as keywords. Flavors whose identifier
1031
+ # #initialize is keyword-only (ITU, IEEE, …) would raise ArgumentError on
1032
+ # a positional hash under Ruby 3 kwarg separation; the base
1033
+ # initialize(attrs = {}, options = {}) still accepts **attrs because Ruby
1034
+ # passes keywords to a no-keyword method as a trailing positional Hash.
1035
+ self.class.new(**attrs)
343
1036
  end
344
1037
 
345
1038
  # The standard this identifier supplements, dropping its own supplement
@@ -388,6 +1081,84 @@ module Pubid
388
1081
  false
389
1082
  end
390
1083
 
1084
+ # --- Relational algebra (pubid/pubid#247) ---
1085
+ # Each predicate returns true / false. Predicates that don't apply to
1086
+ # this identifier type (e.g. supplement checks on a non-wrapper) return
1087
+ # false rather than raising. This makes them safe for chain-of-checks
1088
+ # patterns like `id.related_to?(other)`.
1089
+
1090
+ # Self is a supplement (amendment / corrigendum / errata / supplement)
1091
+ # of +other+. Self must be a wrapper (has a +base+ attribute) whose
1092
+ # inner base equals +other+.
1093
+ def supplement_of?(other)
1094
+ return false unless other.is_a?(::Pubid::Identifier)
1095
+ return false unless self.class.attributes.key?(:base)
1096
+
1097
+ base == other
1098
+ end
1099
+
1100
+ # Inverse of #supplement_of? — self is the base document and +other+
1101
+ # is a supplement attached to it.
1102
+ def has_supplement?(other)
1103
+ other.is_a?(::Pubid::Identifier) && other.supplement_of?(self)
1104
+ end
1105
+
1106
+ # Self and +other+ refer to the same document but differ only in the
1107
+ # trailing date / year — the "dated vs undated" reference pattern.
1108
+ def dated_version_of?(other)
1109
+ return false unless other.is_a?(::Pubid::Identifier)
1110
+
1111
+ matches?(other, ignore: [:date, :year]) && self != other
1112
+ end
1113
+
1114
+ # Self and +other+ have the same publisher + number but differ in
1115
+ # part, subpart, or edition. This is the "sibling documents" pattern:
1116
+ # same base standard, different sub-component.
1117
+ def sibling_of?(other)
1118
+ return false unless other.is_a?(::Pubid::Identifier)
1119
+
1120
+ publisher == other.publisher &&
1121
+ number == other.number &&
1122
+ self != other
1123
+ end
1124
+
1125
+ # Self is an all-parts collection that covers +other+.
1126
+ def includes?(other)
1127
+ return false unless other.is_a?(::Pubid::Identifier)
1128
+ return false unless all_parts
1129
+
1130
+ root == other.root
1131
+ end
1132
+
1133
+ # Self is a draft version of +other+ (the published identifier).
1134
+ # IEEE uses draft_status + draft; ISO/IEC use typed_stage.
1135
+ def draft_of?(other)
1136
+ return false unless other.is_a?(::Pubid::Identifier)
1137
+
1138
+ root == other.root &&
1139
+ self != other &&
1140
+ matches?(other, ignore: [:date, :year, :stage, :typed_stage])
1141
+ end
1142
+
1143
+ # Self and +other+ have the same publisher + number + part but differ
1144
+ # in edition / revision marker.
1145
+ def edition_of?(other)
1146
+ return false unless other.is_a?(::Pubid::Identifier)
1147
+
1148
+ publisher == other.publisher &&
1149
+ number == other.number &&
1150
+ part == other.part &&
1151
+ self != other
1152
+ end
1153
+
1154
+ # Any relational predicate holds.
1155
+ def related_to?(other)
1156
+ %i[supplement_of? has_supplement? dated_version_of? edition_of?
1157
+ sibling_of? includes? draft_of?].any? do |m|
1158
+ public_send(m, other)
1159
+ end
1160
+ end
1161
+
391
1162
  def hash
392
1163
  @hash ||= compute_hash
393
1164
  end