pubid 2.0.0.pre.alpha.9 → 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 (432) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +33 -11
  3. data/conformance/pending.yaml +4 -0
  4. data/lib/pubid/adobe/identifier.rb +1 -3
  5. data/lib/pubid/adobe/parser.rb +10 -2
  6. data/lib/pubid/adobe.rb +1 -0
  7. data/lib/pubid/amca/builder.rb +2 -2
  8. data/lib/pubid/amca/identifiers/base.rb +49 -27
  9. data/lib/pubid/amca/identifiers/interpretation.rb +17 -9
  10. data/lib/pubid/amca/identifiers/publication.rb +13 -9
  11. data/lib/pubid/amca/identifiers/standard.rb +2 -0
  12. data/lib/pubid/amca/parser.rb +1 -1
  13. data/lib/pubid/amca/renderer.rb +4 -4
  14. data/lib/pubid/amca/urn_generator.rb +2 -2
  15. data/lib/pubid/amca.rb +2 -1
  16. data/lib/pubid/ansi/builder.rb +2 -2
  17. data/lib/pubid/ansi/identifier.rb +18 -0
  18. data/lib/pubid/ansi/parser.rb +1 -1
  19. data/lib/pubid/ansi/renderer.rb +2 -2
  20. data/lib/pubid/ansi.rb +5 -5
  21. data/lib/pubid/api/builder.rb +16 -2
  22. data/lib/pubid/api/identifier.rb +25 -4
  23. data/lib/pubid/api/identifiers/mpms.rb +10 -8
  24. data/lib/pubid/api/parser.rb +1 -1
  25. data/lib/pubid/api/renderer.rb +16 -7
  26. data/lib/pubid/api/single_identifier.rb +5 -12
  27. data/lib/pubid/api.rb +1 -0
  28. data/lib/pubid/ashrae/builder.rb +31 -10
  29. data/lib/pubid/ashrae/identifiers/addenda_package.rb +6 -0
  30. data/lib/pubid/ashrae/identifiers/addendum.rb +8 -0
  31. data/lib/pubid/ashrae/identifiers/base.rb +81 -5
  32. data/lib/pubid/ashrae/identifiers/combined_addenda.rb +7 -0
  33. data/lib/pubid/ashrae/identifiers/errata.rb +9 -0
  34. data/lib/pubid/ashrae/identifiers/guideline.rb +3 -0
  35. data/lib/pubid/ashrae/identifiers/interpretation.rb +16 -0
  36. data/lib/pubid/ashrae/identifiers/standard.rb +3 -0
  37. data/lib/pubid/ashrae/parser.rb +23 -8
  38. data/lib/pubid/ashrae/renderer.rb +6 -6
  39. data/lib/pubid/ashrae/supplement_identifier.rb +17 -2
  40. data/lib/pubid/ashrae/urn_generator.rb +8 -2
  41. data/lib/pubid/ashrae.rb +7 -1
  42. data/lib/pubid/asme/builder.rb +11 -2
  43. data/lib/pubid/asme/identifier.rb +9 -0
  44. data/lib/pubid/asme/identifiers/standard.rb +86 -2
  45. data/lib/pubid/asme/parser.rb +1 -1
  46. data/lib/pubid/asme/renderer.rb +3 -3
  47. data/lib/pubid/asme/single_identifier.rb +8 -1
  48. data/lib/pubid/asme/urn_generator.rb +2 -2
  49. data/lib/pubid/asme.rb +1 -0
  50. data/lib/pubid/astm/builder.rb +10 -3
  51. data/lib/pubid/astm/identifier.rb +9 -0
  52. data/lib/pubid/astm/identifiers/adjunct.rb +16 -1
  53. data/lib/pubid/astm/identifiers/code_number.rb +81 -0
  54. data/lib/pubid/astm/identifiers/data_series.rb +2 -0
  55. data/lib/pubid/astm/identifiers/iso_dual_published.rb +16 -0
  56. data/lib/pubid/astm/identifiers/manual.rb +10 -0
  57. data/lib/pubid/astm/identifiers/monograph.rb +2 -0
  58. data/lib/pubid/astm/identifiers/research_report.rb +10 -0
  59. data/lib/pubid/astm/identifiers/standard.rb +10 -0
  60. data/lib/pubid/astm/identifiers/technical_report.rb +2 -0
  61. data/lib/pubid/astm/identifiers/work_in_progress.rb +2 -0
  62. data/lib/pubid/astm/identifiers.rb +1 -0
  63. data/lib/pubid/astm/parser.rb +1 -1
  64. data/lib/pubid/astm/renderer.rb +1 -1
  65. data/lib/pubid/astm/single_identifier.rb +31 -1
  66. data/lib/pubid/astm/urn_generator.rb +7 -5
  67. data/lib/pubid/astm.rb +1 -0
  68. data/lib/pubid/bipm/builder.rb +66 -10
  69. data/lib/pubid/bipm/identifier.rb +134 -9
  70. data/lib/pubid/bipm/identifiers/committee_document.rb +12 -0
  71. data/lib/pubid/bipm/identifiers/guide.rb +11 -0
  72. data/lib/pubid/bipm/identifiers/meeting.rb +12 -0
  73. data/lib/pubid/bipm/identifiers/mep.rb +12 -0
  74. data/lib/pubid/bipm/identifiers/metrologia_article.rb +24 -0
  75. data/lib/pubid/bipm/identifiers/si_brochure.rb +13 -0
  76. data/lib/pubid/bipm/parser.rb +65 -12
  77. data/lib/pubid/bipm/renderer.rb +8 -0
  78. data/lib/pubid/bipm/urn_parser.rb +11 -3
  79. data/lib/pubid/bipm.rb +7 -1
  80. data/lib/pubid/bsi/builder.rb +48 -38
  81. data/lib/pubid/bsi/identifiers/adopted_european_norm.rb +36 -7
  82. data/lib/pubid/bsi/identifiers/adopted_international_standard.rb +7 -6
  83. data/lib/pubid/bsi/identifiers/british_industrial_practice.rb +0 -1
  84. data/lib/pubid/bsi/identifiers/bundled_identifier.rb +10 -0
  85. data/lib/pubid/bsi/identifiers/committee_document.rb +10 -1
  86. data/lib/pubid/bsi/identifiers/handbook.rb +0 -2
  87. data/lib/pubid/bsi/identifiers/practice_guide.rb +0 -1
  88. data/lib/pubid/bsi/identifiers/set.rb +11 -0
  89. data/lib/pubid/bsi/identifiers/standalone_amendment.rb +10 -1
  90. data/lib/pubid/bsi/parser.rb +1 -1
  91. data/lib/pubid/bsi/renderer.rb +63 -51
  92. data/lib/pubid/bsi/single_identifier.rb +16 -7
  93. data/lib/pubid/bsi/urn_generator.rb +11 -2
  94. data/lib/pubid/bsi.rb +7 -1
  95. data/lib/pubid/builder/base.rb +18 -8
  96. data/lib/pubid/bundled_identifier.rb +16 -6
  97. data/lib/pubid/calconnect/identifier.rb +9 -5
  98. data/lib/pubid/calconnect/parser.rb +1 -1
  99. data/lib/pubid/calconnect.rb +7 -1
  100. data/lib/pubid/ccsds/identifier.rb +9 -2
  101. data/lib/pubid/ccsds/identifiers/base.rb +4 -2
  102. data/lib/pubid/ccsds/identifiers/corrigendum.rb +2 -2
  103. data/lib/pubid/ccsds/parser.rb +1 -1
  104. data/lib/pubid/ccsds/single_identifier.rb +2 -2
  105. data/lib/pubid/ccsds.rb +1 -0
  106. data/lib/pubid/cen_cenelec/builder.rb +101 -76
  107. data/lib/pubid/cen_cenelec/identifier.rb +115 -3
  108. data/lib/pubid/cen_cenelec/identifiers/adopted_european_norm.rb +27 -16
  109. data/lib/pubid/cen_cenelec/identifiers/amendment.rb +46 -6
  110. data/lib/pubid/cen_cenelec/identifiers/base.rb +8 -9
  111. data/lib/pubid/cen_cenelec/identifiers/cen_report.rb +1 -1
  112. data/lib/pubid/cen_cenelec/identifiers/cen_workshop_agreement.rb +1 -1
  113. data/lib/pubid/cen_cenelec/identifiers/cenelec_harmonization_document.rb +1 -1
  114. data/lib/pubid/cen_cenelec/identifiers/consolidated_identifier.rb +29 -20
  115. data/lib/pubid/cen_cenelec/identifiers/corrigendum.rb +47 -8
  116. data/lib/pubid/cen_cenelec/identifiers/european_prestandard.rb +27 -2
  117. data/lib/pubid/cen_cenelec/identifiers/european_specification.rb +1 -1
  118. data/lib/pubid/cen_cenelec/identifiers/fragment.rb +15 -5
  119. data/lib/pubid/cen_cenelec/identifiers/guide.rb +1 -1
  120. data/lib/pubid/cen_cenelec/identifiers/harmonization_document.rb +1 -1
  121. data/lib/pubid/cen_cenelec/identifiers/technical_report.rb +1 -1
  122. data/lib/pubid/cen_cenelec/identifiers/technical_specification.rb +1 -1
  123. data/lib/pubid/cen_cenelec/parser.rb +1 -1
  124. data/lib/pubid/cen_cenelec/renderer.rb +55 -60
  125. data/lib/pubid/cen_cenelec/single_identifier.rb +20 -2
  126. data/lib/pubid/cen_cenelec/urn_generator.rb +66 -44
  127. data/lib/pubid/cen_cenelec.rb +28 -10
  128. data/lib/pubid/cie/builder.rb +10 -0
  129. data/lib/pubid/cie/identifier.rb +6 -1
  130. data/lib/pubid/cie/identifiers/bundle.rb +4 -2
  131. data/lib/pubid/cie/identifiers/conference.rb +2 -2
  132. data/lib/pubid/cie/identifiers/corrigendum.rb +2 -2
  133. data/lib/pubid/cie/identifiers/dual_published.rb +2 -2
  134. data/lib/pubid/cie/identifiers/identical.rb +2 -2
  135. data/lib/pubid/cie/identifiers/joint_published.rb +2 -2
  136. data/lib/pubid/cie/identifiers/proceedings.rb +8 -6
  137. data/lib/pubid/cie/identifiers/standard.rb +22 -13
  138. data/lib/pubid/cie/identifiers/supplement.rb +2 -2
  139. data/lib/pubid/cie/identifiers/tutorial_bundle.rb +2 -2
  140. data/lib/pubid/cie/parser.rb +1 -1
  141. data/lib/pubid/cie.rb +7 -1
  142. data/lib/pubid/conformance/checks.rb +72 -0
  143. data/lib/pubid/conformance/corpus/case.rb +55 -0
  144. data/lib/pubid/conformance/corpus.rb +36 -0
  145. data/lib/pubid/conformance/generator.rb +212 -0
  146. data/lib/pubid/conformance/pending.rb +63 -0
  147. data/lib/pubid/conformance/runner.rb +153 -0
  148. data/lib/pubid/conformance.rb +55 -0
  149. data/lib/pubid/core.rb +2 -0
  150. data/lib/pubid/csa/builder.rb +47 -47
  151. data/lib/pubid/csa/composite_identifier.rb +14 -19
  152. data/lib/pubid/csa/identifier.rb +143 -81
  153. data/lib/pubid/csa/identifiers/bundled.rb +23 -13
  154. data/lib/pubid/csa/identifiers/canadian_adopted.rb +13 -13
  155. data/lib/pubid/csa/identifiers/cec.rb +44 -4
  156. data/lib/pubid/csa/identifiers/combined.rb +95 -73
  157. data/lib/pubid/csa/identifiers/csa_adopted.rb +4 -4
  158. data/lib/pubid/csa/identifiers/package.rb +2 -2
  159. data/lib/pubid/csa/parser.rb +6 -2
  160. data/lib/pubid/csa/renderer.rb +13 -5
  161. data/lib/pubid/csa/single_identifier.rb +93 -4
  162. data/lib/pubid/csa/urn_generator.rb +9 -2
  163. data/lib/pubid/csa/wrapper_identifier.rb +39 -23
  164. data/lib/pubid/csa.rb +7 -1
  165. data/lib/pubid/doi/identifier.rb +6 -3
  166. data/lib/pubid/doi/parser.rb +1 -1
  167. data/lib/pubid/doi.rb +1 -0
  168. data/lib/pubid/easc/identifier.rb +0 -2
  169. data/lib/pubid/easc/parser.rb +10 -2
  170. data/lib/pubid/easc.rb +1 -0
  171. data/lib/pubid/ecma/builder.rb +23 -9
  172. data/lib/pubid/ecma/identifier.rb +81 -10
  173. data/lib/pubid/ecma/parser.rb +39 -4
  174. data/lib/pubid/ecma/renderer.rb +29 -8
  175. data/lib/pubid/ecma/urn_generator.rb +34 -13
  176. data/lib/pubid/ecma/urn_parser.rb +30 -14
  177. data/lib/pubid/ecma.rb +1 -0
  178. data/lib/pubid/errors.rb +80 -0
  179. data/lib/pubid/etsi/builder.rb +6 -1
  180. data/lib/pubid/etsi/identifiers/base.rb +34 -20
  181. data/lib/pubid/etsi/identifiers/etsi_standard.rb +120 -41
  182. data/lib/pubid/etsi/identifiers/supplement_identifier.rb +10 -0
  183. data/lib/pubid/etsi/parser.rb +1 -1
  184. data/lib/pubid/etsi.rb +1 -0
  185. data/lib/pubid/evs/builder.rb +26 -0
  186. data/lib/pubid/evs/identifier.rb +37 -0
  187. data/lib/pubid/evs/identifiers/national_adoption.rb +17 -0
  188. data/lib/pubid/evs/identifiers.rb +9 -0
  189. data/lib/pubid/evs/parser.rb +27 -0
  190. data/lib/pubid/evs/renderer.rb +30 -0
  191. data/lib/pubid/evs/urn_generator.rb +30 -0
  192. data/lib/pubid/evs/urn_parser.rb +80 -0
  193. data/lib/pubid/evs.rb +90 -0
  194. data/lib/pubid/export.rb +1 -0
  195. data/lib/pubid/format_detector.rb +1 -0
  196. data/lib/pubid/gb/identifier.rb +6 -3
  197. data/lib/pubid/gb/parser.rb +1 -1
  198. data/lib/pubid/gb.rb +1 -0
  199. data/lib/pubid/gost/builder.rb +46 -9
  200. data/lib/pubid/gost/identifier.rb +0 -2
  201. data/lib/pubid/gost/identifiers/foreign_reference.rb +1 -1
  202. data/lib/pubid/gost/parser.rb +10 -2
  203. data/lib/pubid/gost.rb +1 -0
  204. data/lib/pubid/iala/identifier.rb +9 -6
  205. data/lib/pubid/iala/parser.rb +10 -2
  206. data/lib/pubid/iala/urn_parser.rb +6 -2
  207. data/lib/pubid/iala.rb +1 -0
  208. data/lib/pubid/iana/builder.rb +3 -1
  209. data/lib/pubid/iana/identifier.rb +74 -6
  210. data/lib/pubid/iana/identifiers/registry.rb +36 -0
  211. data/lib/pubid/iana/parser.rb +1 -1
  212. data/lib/pubid/iana/renderer.rb +3 -0
  213. data/lib/pubid/iana/urn_generator.rb +5 -1
  214. data/lib/pubid/iana.rb +1 -0
  215. data/lib/pubid/identifier.rb +594 -10
  216. data/lib/pubid/identifier_metadata.rb +1 -0
  217. data/lib/pubid/idf/builder.rb +3 -3
  218. data/lib/pubid/idf/identifier.rb +18 -9
  219. data/lib/pubid/idf/parser.rb +1 -1
  220. data/lib/pubid/idf/renderer.rb +4 -4
  221. data/lib/pubid/idf/urn_generator.rb +1 -1
  222. data/lib/pubid/idf.rb +7 -1
  223. data/lib/pubid/iec/builder.rb +14 -7
  224. data/lib/pubid/iec/components.rb +0 -1
  225. data/lib/pubid/iec/identifier.rb +73 -83
  226. data/lib/pubid/iec/identifiers/amendment.rb +6 -2
  227. data/lib/pubid/iec/identifiers/component_specification.rb +0 -10
  228. data/lib/pubid/iec/identifiers/conformity_assessment.rb +0 -10
  229. data/lib/pubid/iec/identifiers/corrigendum.rb +2 -2
  230. data/lib/pubid/iec/identifiers/fragment_identifier.rb +2 -4
  231. data/lib/pubid/iec/identifiers/guide.rb +0 -16
  232. data/lib/pubid/iec/identifiers/international_standard.rb +15 -2
  233. data/lib/pubid/iec/identifiers/operational_document.rb +0 -10
  234. data/lib/pubid/iec/identifiers/publicly_available_specification.rb +0 -12
  235. data/lib/pubid/iec/identifiers/societal_technology_trend_report.rb +0 -10
  236. data/lib/pubid/iec/identifiers/systems_reference_document.rb +0 -10
  237. data/lib/pubid/iec/identifiers/technical_report.rb +0 -17
  238. data/lib/pubid/iec/identifiers/technical_specification.rb +0 -17
  239. data/lib/pubid/iec/identifiers/technology_report.rb +0 -10
  240. data/lib/pubid/iec/identifiers/test_report_form.rb +0 -11
  241. data/lib/pubid/iec/identifiers/white_paper.rb +0 -10
  242. data/lib/pubid/iec/identifiers/working_document.rb +24 -10
  243. data/lib/pubid/iec/parser.rb +2 -18
  244. data/lib/pubid/iec/renderer.rb +13 -10
  245. data/lib/pubid/iec/single_identifier.rb +33 -10
  246. data/lib/pubid/iec/urn_generator.rb +227 -61
  247. data/lib/pubid/iec/urn_parser.rb +42 -4
  248. data/lib/pubid/iec.rb +1 -0
  249. data/lib/pubid/ieee/aiee/identifier.rb +2 -2
  250. data/lib/pubid/ieee/aiee/parser.rb +1 -1
  251. data/lib/pubid/ieee/builder.rb +45 -1
  252. data/lib/pubid/ieee/identifiers/adopted_standard.rb +32 -0
  253. data/lib/pubid/ieee/identifiers/base.rb +42 -5
  254. data/lib/pubid/ieee/identifiers/csa_dual_published.rb +21 -0
  255. data/lib/pubid/ieee/identifiers/dual_published.rb +55 -0
  256. data/lib/pubid/ieee/identifiers/interpretation_identifier.rb +23 -0
  257. data/lib/pubid/ieee/identifiers/joint_development.rb +11 -7
  258. data/lib/pubid/ieee/identifiers/multi_numbered_identifier.rb +57 -5
  259. data/lib/pubid/ieee/identifiers/nesc/base.rb +2 -2
  260. data/lib/pubid/ieee/identifiers/nesc/draft.rb +2 -2
  261. data/lib/pubid/ieee/identifiers/nesc/handbook.rb +2 -2
  262. data/lib/pubid/ieee/identifiers/nesc/redline.rb +2 -2
  263. data/lib/pubid/ieee/identifiers/nesc/standard.rb +2 -2
  264. data/lib/pubid/ieee/ire/identifier.rb +2 -2
  265. data/lib/pubid/ieee/ire/parser.rb +1 -1
  266. data/lib/pubid/ieee/nesc/parser.rb +1 -1
  267. data/lib/pubid/ieee/parser.rb +1 -1
  268. data/lib/pubid/ieee/renderer.rb +5 -1
  269. data/lib/pubid/ieee/urn_generator.rb +32 -3
  270. data/lib/pubid/ieee.rb +7 -1
  271. data/lib/pubid/ieee_debug.rb +1 -0
  272. data/lib/pubid/ietf/builder.rb +24 -8
  273. data/lib/pubid/ietf/identifiers/base.rb +25 -28
  274. data/lib/pubid/ietf/identifiers/bcp.rb +11 -0
  275. data/lib/pubid/ietf/identifiers/fyi.rb +11 -0
  276. data/lib/pubid/ietf/identifiers/internet_draft.rb +18 -2
  277. data/lib/pubid/ietf/identifiers/rfc.rb +2 -0
  278. data/lib/pubid/ietf/identifiers/serialization.rb +51 -0
  279. data/lib/pubid/ietf/identifiers/std.rb +11 -0
  280. data/lib/pubid/ietf/identifiers.rb +1 -0
  281. data/lib/pubid/ietf/parser.rb +27 -9
  282. data/lib/pubid/ietf/renderer.rb +44 -6
  283. data/lib/pubid/ietf/urn_generator.rb +20 -6
  284. data/lib/pubid/ietf/urn_parser.rb +10 -2
  285. data/lib/pubid/ietf.rb +12 -2
  286. data/lib/pubid/iho/identifiers/base.rb +10 -3
  287. data/lib/pubid/iho/parser.rb +1 -1
  288. data/lib/pubid/iho.rb +1 -0
  289. data/lib/pubid/isbn/identifier.rb +25 -7
  290. data/lib/pubid/isbn/parser.rb +1 -1
  291. data/lib/pubid/isbn.rb +1 -0
  292. data/lib/pubid/iso/bundled_identifier.rb +5 -3
  293. data/lib/pubid/iso/combined_identifier.rb +5 -3
  294. data/lib/pubid/iso/identifier.rb +12 -7
  295. data/lib/pubid/iso/identifiers/directives_supplement.rb +4 -4
  296. data/lib/pubid/iso/parser.rb +1 -1
  297. data/lib/pubid/iso.rb +7 -1
  298. data/lib/pubid/itu/identifiers/addendum.rb +2 -2
  299. data/lib/pubid/itu/identifiers/amendment.rb +2 -2
  300. data/lib/pubid/itu/identifiers/base.rb +19 -5
  301. data/lib/pubid/itu/identifiers/corrigendum.rb +2 -2
  302. data/lib/pubid/itu/identifiers/errata.rb +2 -2
  303. data/lib/pubid/itu/identifiers/supplement.rb +2 -2
  304. data/lib/pubid/itu/parser.rb +1 -1
  305. data/lib/pubid/itu.rb +1 -0
  306. data/lib/pubid/jcgm/builder.rb +1 -1
  307. data/lib/pubid/jcgm/identifier.rb +10 -0
  308. data/lib/pubid/jcgm/identifiers/meeting.rb +1 -1
  309. data/lib/pubid/jcgm/parser.rb +10 -3
  310. data/lib/pubid/jcgm/renderer.rb +10 -3
  311. data/lib/pubid/jcgm/single_identifier.rb +24 -15
  312. data/lib/pubid/jcgm/urn_generator.rb +1 -1
  313. data/lib/pubid/jcgm/urn_parser.rb +156 -14
  314. data/lib/pubid/jcgm.rb +10 -0
  315. data/lib/pubid/jis/identifier.rb +10 -3
  316. data/lib/pubid/jis/parser.rb +1 -1
  317. data/lib/pubid/jis.rb +1 -0
  318. data/lib/pubid/nist/components/stage.rb +2 -2
  319. data/lib/pubid/nist/configuration.rb +6 -8
  320. data/lib/pubid/nist/identifiers/base.rb +12 -2
  321. data/lib/pubid/nist/identifiers/circular.rb +9 -2
  322. data/lib/pubid/nist/identifiers/circular_supplement.rb +14 -5
  323. data/lib/pubid/nist/identifiers/commercial_standards_monthly.rb +7 -1
  324. data/lib/pubid/nist/identifiers/crpl_report.rb +7 -1
  325. data/lib/pubid/nist/identifiers/handbook.rb +9 -2
  326. data/lib/pubid/nist/identifiers/internal_report.rb +12 -5
  327. data/lib/pubid/nist/identifiers/miscellaneous_publication.rb +14 -6
  328. data/lib/pubid/nist/identifiers/monograph.rb +14 -7
  329. data/lib/pubid/nist/identifiers/report.rb +14 -6
  330. data/lib/pubid/nist/parser.rb +1 -1
  331. data/lib/pubid/nist/supplement_identifier.rb +16 -1
  332. data/lib/pubid/nist.rb +7 -1
  333. data/lib/pubid/oasis/builder.rb +12 -10
  334. data/lib/pubid/oasis/identifier.rb +89 -11
  335. data/lib/pubid/oasis/parser.rb +1 -1
  336. data/lib/pubid/oasis.rb +7 -1
  337. data/lib/pubid/ogc/identifier.rb +9 -5
  338. data/lib/pubid/ogc/parser.rb +1 -1
  339. data/lib/pubid/ogc.rb +1 -0
  340. data/lib/pubid/oiml/builder.rb +5 -1
  341. data/lib/pubid/oiml/identifiers/basic_publication.rb +2 -0
  342. data/lib/pubid/oiml/identifiers/bulletin.rb +36 -0
  343. data/lib/pubid/oiml/identifiers/code_number.rb +90 -0
  344. data/lib/pubid/oiml/identifiers/document.rb +2 -0
  345. data/lib/pubid/oiml/identifiers/expert_report.rb +2 -0
  346. data/lib/pubid/oiml/identifiers/guide.rb +2 -0
  347. data/lib/pubid/oiml/identifiers/recommendation.rb +2 -0
  348. data/lib/pubid/oiml/identifiers/seminar_report.rb +2 -0
  349. data/lib/pubid/oiml/identifiers/vocabulary.rb +2 -0
  350. data/lib/pubid/oiml/identifiers.rb +1 -0
  351. data/lib/pubid/oiml/parser.rb +1 -1
  352. data/lib/pubid/oiml/single_identifier.rb +26 -42
  353. data/lib/pubid/oiml/supplement_identifier.rb +27 -0
  354. data/lib/pubid/oiml.rb +7 -1
  355. data/lib/pubid/omg/builder.rb +1 -0
  356. data/lib/pubid/omg/identifier.rb +20 -3
  357. data/lib/pubid/omg/parser.rb +64 -11
  358. data/lib/pubid/omg/renderer.rb +13 -1
  359. data/lib/pubid/omg.rb +2 -1
  360. data/lib/pubid/parser/grammar.rb +74 -0
  361. data/lib/pubid/parser.rb +2 -0
  362. data/lib/pubid/parsers/mr_string.rb +19 -0
  363. data/lib/pubid/plateau/parser.rb +1 -1
  364. data/lib/pubid/plateau.rb +10 -0
  365. data/lib/pubid/prefixes_support.rb +1 -0
  366. data/lib/pubid/renderers/annotator.rb +233 -0
  367. data/lib/pubid/renderers/base.rb +13 -0
  368. data/lib/pubid/renderers/directives_renderer.rb +3 -3
  369. data/lib/pubid/renderers/human_readable.rb +3 -3
  370. data/lib/pubid/renderers/mr_string.rb +15 -9
  371. data/lib/pubid/renderers.rb +1 -0
  372. data/lib/pubid/rendering/numbering.rb +25 -7
  373. data/lib/pubid/rendering.rb +1 -0
  374. data/lib/pubid/sae/identifiers/base.rb +9 -2
  375. data/lib/pubid/sae/parser.rb +1 -1
  376. data/lib/pubid/sae.rb +1 -0
  377. data/lib/pubid/schema/declaration.rb +33 -0
  378. data/lib/pubid/schema/error.rb +14 -0
  379. data/lib/pubid/schema/identifier_type.rb +23 -0
  380. data/lib/pubid/schema/loader.rb +117 -0
  381. data/lib/pubid/schema/typed_stage.rb +24 -0
  382. data/lib/pubid/schema.rb +25 -0
  383. data/lib/pubid/tgpp/builder.rb +1 -1
  384. data/lib/pubid/tgpp/identifier.rb +21 -5
  385. data/lib/pubid/tgpp/identifiers/technical_report.rb +2 -2
  386. data/lib/pubid/tgpp/identifiers/technical_specification.rb +2 -2
  387. data/lib/pubid/tgpp/parser.rb +22 -6
  388. data/lib/pubid/tgpp/renderer.rb +21 -5
  389. data/lib/pubid/tgpp/urn_generator.rb +27 -8
  390. data/lib/pubid/tgpp/urn_parser.rb +8 -6
  391. data/lib/pubid/tgpp.rb +7 -1
  392. data/lib/pubid/type_resolver.rb +16 -1
  393. data/lib/pubid/un/identifier.rb +6 -3
  394. data/lib/pubid/un/parser.rb +1 -1
  395. data/lib/pubid/un.rb +1 -0
  396. data/lib/pubid/urn_parser/errors.rb +8 -1
  397. data/lib/pubid/urn_parser.rb +1 -0
  398. data/lib/pubid/utils.rb +1 -0
  399. data/lib/pubid/version.rb +1 -1
  400. data/lib/pubid/w3c/builder.rb +5 -5
  401. data/lib/pubid/w3c/identifier.rb +38 -9
  402. data/lib/pubid/w3c/parser.rb +1 -1
  403. data/lib/pubid/w3c/renderer.rb +1 -1
  404. data/lib/pubid/w3c/urn_generator.rb +2 -2
  405. data/lib/pubid/w3c.rb +1 -0
  406. data/lib/pubid/xsf/identifier.rb +9 -5
  407. data/lib/pubid/xsf/parser.rb +17 -4
  408. data/lib/pubid/xsf.rb +7 -1
  409. data/lib/pubid.rb +399 -21
  410. data/lib/tasks/conformance.rake +34 -0
  411. data/lib/tasks/docs.rake +3 -3
  412. data/lib/tasks/schema.rake +88 -0
  413. data/schema/core/joint_prefixes.yaml +24 -0
  414. data/schema/iec.yaml +1017 -0
  415. data/schema/iso.yaml +1586 -0
  416. data/schema/schema.schema.yaml +96 -0
  417. metadata +38 -17
  418. data/archived-gems/pubid-ccsds/update_codes.yaml +0 -1
  419. data/archived-gems/pubid-iec/stages.yaml +0 -129
  420. data/archived-gems/pubid-iec/update_codes.yaml +0 -67
  421. data/archived-gems/pubid-ieee/update_codes.yaml +0 -104
  422. data/archived-gems/pubid-iso/stages.yaml +0 -106
  423. data/archived-gems/pubid-iso/update_codes.yaml +0 -4
  424. data/archived-gems/pubid-itu/i18n.yaml +0 -13
  425. data/archived-gems/pubid-itu/series.yaml +0 -42
  426. data/archived-gems/pubid-nist/publishers.yaml +0 -6
  427. data/archived-gems/pubid-nist/series.yaml +0 -121
  428. data/archived-gems/pubid-nist/update_codes.yaml +0 -93
  429. data/archived-gems/pubid-plateau/update_codes.yaml +0 -6
  430. data/lib/pubid/cen_cenelec/supplement_identifier.rb +0 -48
  431. data/lib/pubid/iec/components/code.rb +0 -36
  432. /data/{archived-gems/pubid-nist → data/nist}/stages.yaml +0 -0
@@ -8,35 +8,30 @@ module Pubid
8
8
  # collections of other identifiers or package materials.
9
9
  #
10
10
  # Examples:
11
- # - PackageIdentifier: Base + package materials
12
- # - Future: BundleIdentifier refactor to use composite
11
+ # - Package: base + package materials
13
12
  #
14
13
  # This follows the Composite pattern where an identifier can contain
15
14
  # other identifiers or additional metadata as a collection.
16
- class CompositeIdentifier < Lutaml::Model::Serializable
17
- # The primary/base identifier
18
- # Use attr_accessor since it can be any identifier object
19
- attr_accessor :base
15
+ class CompositeIdentifier < Identifier
16
+ # The primary/base identifier. A real lutaml attribute under the uniform
17
+ # `base` name, so `#root` walks it and it survives serialization — see
18
+ # WrapperIdentifier#base for why the type is the cross-flavor
19
+ # ::Pubid::Identifier.
20
+ attribute :base, ::Pubid::Identifier, polymorphic: true
20
21
 
21
22
  # Subclasses MUST implement to_s to define how they render
22
- def to_s
23
+ def to_s(**_opts)
23
24
  raise NotImplementedError, "Subclasses must implement to_s method"
24
25
  end
25
26
 
26
- # CSA composites serialise through `to_s`, so MR mirrors that with
27
- # ` ` → `.`, `:` → `.`, `/` → `-`, then lowercased (issue #142).
28
- def to_mr_string
29
- to_s.tr(" ", ".").tr(":", ".").tr("/", "-").downcase
27
+ # A package peels to the standard it packages — see
28
+ # WrapperIdentifier#base_document.
29
+ def base_document
30
+ base ? base.base_document : self
30
31
  end
31
32
 
32
- def to_slug
33
- to_mr_string
34
- end
35
-
36
- # CSA composites are not Pubid::Identifier objects, so they do not inherit
37
- # #root. They are their own origin for matching purposes.
38
- def root
39
- self
33
+ def drop_supplements
34
+ base || self
40
35
  end
41
36
  end
42
37
  end
@@ -2,22 +2,38 @@
2
2
 
3
3
  module Pubid
4
4
  module Csa
5
- # Common base class for all CSA identifiers. SingleIdentifier (and the
6
- # concrete Identifiers::* types through it) descends from it, so every CSA
7
- # identifier is `is_a?(Pubid::Csa::Identifier)` natively and gets the shared
8
- # polymorphic `from_hash`. (The Wrapper/Composite container models are
9
- # separate Lutaml::Model types, not identifiers.)
5
+ # Common base class for EVERY CSA identifier — the single-document types
6
+ # through SingleIdentifier, and the container types (WrapperIdentifier,
7
+ # CompositeIdentifier, Bundled, Combined) directly. So every CSA identifier
8
+ # is `is_a?(Pubid::Csa::Identifier)` natively and gets the shared
9
+ # polymorphic `from_hash`, `#root`, `#exclude` and MR rendering. The
10
+ # containers used to be separate Lutaml::Model types, which is what made
11
+ # `root.number` raise rather than return the relaton index key.
10
12
  class Identifier < ::Pubid::Identifier
11
13
  def self.parse(input)
14
+ unless input.is_a?(String)
15
+ raise Pubid::Errors::InvalidInputError,
16
+ Pubid::INPUT_NOT_A_STRING_MESSAGE
17
+ end
18
+
12
19
  if input.length > Pubid::MAX_INPUT_LENGTH
13
- raise ArgumentError, Pubid::INPUT_TOO_LONG_MESSAGE
20
+ raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
14
21
  end
15
22
 
16
23
  # Filter out comments
17
- return nil if input.start_with?("#")
24
+ if input.start_with?("#")
25
+ raise Pubid::Errors::ParseError.new(
26
+ "Not a CSA identifier (comment): #{input}",
27
+ input: input, flavor: "csa",
28
+ )
29
+ end
18
30
 
19
- # Filter out non-standards
20
- return nil if input.match?(/^CSA (Communities|Group|Learning|OnDemand|Update)/)
31
+ # Filter out non-standards (memberships, courses, newsletters)
32
+ if input.match?(/^CSA (Communities|Group|Learning|OnDemand|Update)/)
33
+ raise Pubid::Errors::ParseError.new(
34
+ "Not a CSA standard: #{input}", input: input, flavor: "csa"
35
+ )
36
+ end
21
37
 
22
38
  # Preprocessing: normalize CEI to IEC (French name)
23
39
  input = input.gsub("CEI/IEC", "IEC").gsub(/\bCEI\b/, "IEC")
@@ -46,16 +62,14 @@ module Pubid
46
62
 
47
63
  # Parse normally (will create Bundled or Combined identifier)
48
64
  tree = Parser.new.parse(normalized)
49
- result = Builder.new.build(tree)
65
+ result = build!(tree, input)
50
66
 
51
67
  # Apply CAN/CSA- prefix to the appropriate parts
52
- if result
53
- set_publisher_prefix(result, "CAN/CSA-")
68
+ set_publisher_prefix(result, "CAN/CSA-")
54
69
 
55
- # Handle reaffirmation if present
56
- if (wrapped_input =~ /\(R(\d{4})\)/) && result.class.attributes.key?(:reaffirmation)
57
- result.reaffirmation = $1
58
- end
70
+ # Handle reaffirmation if present
71
+ if (wrapped_input =~ /\(R(\d{4})\)/) && result.class.attributes.key?(:reaffirmation)
72
+ result.reaffirmation = $1
59
73
  end
60
74
 
61
75
  return result
@@ -91,8 +105,7 @@ module Pubid
91
105
  wrapped_input = wrapped_input.sub(/^CSA-/, "CSA ")
92
106
 
93
107
  # Parse the wrapped identifier recursively
94
- wrapped_identifier = parse(wrapped_input)
95
- return nil unless wrapped_identifier
108
+ base = parse(wrapped_input)
96
109
 
97
110
  # NOTE: Series identifiers should be wrapped in CanadianAdopted when
98
111
  # they have CAN/CSA- prefix. The Series will handle rendering correctly.
@@ -101,32 +114,35 @@ module Pubid
101
114
  # Set publisher prefix on wrapped identifier
102
115
  # For Series identifiers with CAN/ wrapper, use full "CAN/CSA-" prefix
103
116
  # For other identifiers, use the detected original_prefix ("CSA-" or "CSA")
104
- if wrapped_identifier.class.attributes.key?(:publisher_prefix)
105
- if is_can_csa && wrapped_identifier.is_a?(Identifiers::Series)
117
+ if base.class.attributes.key?(:publisher_prefix)
118
+ if is_can_csa && base.is_a?(Identifiers::Series)
106
119
  # Series gets full "CAN/CSA-" prefix for proper rendering
107
- wrapped_identifier.publisher_prefix = "CAN/CSA-"
120
+ base.publisher_prefix = "CAN/CSA-"
108
121
  elsif original_prefix
109
- # For Combined identifiers, set on first identifier
110
- if wrapped_identifier.is_a?(Identifiers::Combined) && wrapped_identifier.first
111
- wrapped_identifier.first.publisher_prefix = original_prefix if wrapped_identifier.first.class.attributes.key?(:publisher_prefix)
122
+ # For Combined identifiers, set on the primary designation
123
+ if base.is_a?(Identifiers::Combined) && base.identifiers&.first
124
+ primary = base.identifiers.first
125
+ if primary.class.attributes.key?(:publisher_prefix)
126
+ primary.publisher_prefix = original_prefix
127
+ end
112
128
  else
113
129
  # For non-Combined identifiers, set directly
114
- wrapped_identifier.publisher_prefix = original_prefix
130
+ base.publisher_prefix = original_prefix
115
131
  end
116
132
  end
117
133
  end
118
134
 
119
- # Set reaffirmation on wrapped_identifier if it has the attribute
120
- if wrapped_identifier.class.attributes.key?(:reaffirmation) && reaffirm_year
121
- wrapped_identifier.reaffirmation = reaffirm_year
122
- if wrapped_identifier.class.attributes.key?(:original_reaffirmation_4digit)
123
- wrapped_identifier.original_reaffirmation_4digit = reaffirmation_was_4digit
135
+ # Set reaffirmation on base if it has the attribute
136
+ if base.class.attributes.key?(:reaffirmation) && reaffirm_year
137
+ base.reaffirmation = reaffirm_year
138
+ if base.class.attributes.key?(:original_reaffirmation_4digit)
139
+ base.original_reaffirmation_4digit = reaffirmation_was_4digit
124
140
  end
125
141
  end
126
142
 
127
143
  # Create CanadianAdoptedIdentifier wrapper
128
144
  result = Identifiers::CanadianAdopted.new
129
- result.wrapped_identifier = wrapped_identifier
145
+ result.base = base
130
146
  result.reaffirmation = reaffirm_year if reaffirm_year
131
147
 
132
148
  return result
@@ -157,44 +173,43 @@ module Pubid
157
173
  end
158
174
 
159
175
  # Parse the wrapped identifier recursively
160
- wrapped_identifier = parse(wrapped_input)
161
- return nil unless wrapped_identifier
176
+ base = parse(wrapped_input)
162
177
 
163
178
  # Set year_format for dash format identifiers (preserve original 2-digit year)
164
- if has_dash_year && wrapped_identifier.class.attributes.key?(:year_format)
165
- wrapped_identifier.year_format = "dash"
179
+ if has_dash_year && base.class.attributes.key?(:year_format)
180
+ base.year_format = "dash"
166
181
  # Mark original year as 2-digit so renderer converts back (1986 → 86)
167
- if wrapped_identifier.class.attributes.key?(:original_year_4digit)
168
- wrapped_identifier.original_year_4digit = false
182
+ if base.class.attributes.key?(:original_year_4digit)
183
+ base.original_year_4digit = false
169
184
  end
170
185
  end
171
186
 
172
187
  # Check if this is a Series identifier - return it directly with CAN3- prefix
173
188
  # Series identifiers are complete identifier types and handle the prefix themselves
174
189
  # They don't need to be wrapped in CanadianAdopted
175
- if wrapped_identifier.is_a?(Identifiers::Series)
176
- wrapped_identifier.publisher_prefix = "CAN3-"
177
- wrapped_identifier.reaffirmation = reaffirm_year if reaffirm_year
178
- wrapped_identifier.original_reaffirmation_4digit = reaffirmation_was_4digit
179
- return wrapped_identifier
190
+ if base.is_a?(Identifiers::Series)
191
+ base.publisher_prefix = "CAN3-"
192
+ base.reaffirmation = reaffirm_year if reaffirm_year
193
+ base.original_reaffirmation_4digit = reaffirmation_was_4digit
194
+ return base
180
195
  end
181
196
 
182
197
  # Set CAN3- as publisher prefix on wrapped identifier
183
- if wrapped_identifier.class.attributes.key?(:publisher_prefix)
184
- wrapped_identifier.publisher_prefix = "CAN3-"
198
+ if base.class.attributes.key?(:publisher_prefix)
199
+ base.publisher_prefix = "CAN3-"
185
200
  end
186
201
 
187
- # Set reaffirmation on wrapped_identifier if it has the attribute
188
- if wrapped_identifier.class.attributes.key?(:reaffirmation) && reaffirm_year
189
- wrapped_identifier.reaffirmation = reaffirm_year
190
- if wrapped_identifier.class.attributes.key?(:original_reaffirmation_4digit)
191
- wrapped_identifier.original_reaffirmation_4digit = reaffirmation_was_4digit
202
+ # Set reaffirmation on base if it has the attribute
203
+ if base.class.attributes.key?(:reaffirmation) && reaffirm_year
204
+ base.reaffirmation = reaffirm_year
205
+ if base.class.attributes.key?(:original_reaffirmation_4digit)
206
+ base.original_reaffirmation_4digit = reaffirmation_was_4digit
192
207
  end
193
208
  end
194
209
 
195
210
  # Create CanadianAdoptedIdentifier wrapper
196
211
  result = Identifiers::CanadianAdopted.new
197
- result.wrapped_identifier = wrapped_identifier
212
+ result.base = base
198
213
  result.reaffirmation = reaffirm_year if reaffirm_year
199
214
 
200
215
  return result
@@ -250,13 +265,20 @@ module Pubid
250
265
  "/Amd #{amendment_num}#{separator}#{amend_full_year}"
251
266
  end
252
267
 
253
- # Parse with appropriate flavor parser
254
- wrapped_identifier = parse_external_standard(wrapped_input)
255
- return nil unless wrapped_identifier
268
+ # Parse with appropriate flavor parser. The helper probes several
269
+ # flavors and reports failure with nil; the public contract is a
270
+ # raise, so translate here.
271
+ base = parse_external_standard(wrapped_input)
272
+ unless base
273
+ raise Pubid::Errors::ParseError.new(
274
+ "Unparseable adopted standard: #{input}",
275
+ input: input, flavor: "csa",
276
+ )
277
+ end
256
278
 
257
279
  # Create CsaAdoptedIdentifier wrapper
258
280
  result = Identifiers::CsaAdopted.new
259
- result.wrapped_identifier = wrapped_identifier
281
+ result.base = base
260
282
  result.reaffirmation = reaffirm_year if reaffirm_year
261
283
 
262
284
  return result
@@ -337,12 +359,9 @@ module Pubid
337
359
  break if token.match?(/^PACKAGE$/i)
338
360
 
339
361
  test_input = base_input.empty? ? token : "#{base_input} #{token}"
340
- parsed = parse(test_input)
341
- if parsed
342
- base_input = test_input
343
- else
344
- break
345
- end
362
+ break unless try_parse(test_input)
363
+
364
+ base_input = test_input
346
365
  end
347
366
 
348
367
  # Extract materials as everything between base and PACKAGE
@@ -354,9 +373,16 @@ module Pubid
354
373
  end
355
374
  end
356
375
 
357
- # Parse base identifier recursively
358
- base = base_input ? parse(base_input) : nil
359
- return nil unless base
376
+ # Parse the base identifier recursively. An unparseable base is a
377
+ # failed package, not a nil.
378
+ if base_input.nil? || base_input.empty?
379
+ raise Pubid::Errors::ParseError.new(
380
+ "Unparseable package base: #{input}",
381
+ input: input, flavor: "csa",
382
+ )
383
+ end
384
+
385
+ base = parse(base_input)
360
386
 
361
387
  # Create PackageIdentifier
362
388
  result = Identifiers::Package.new
@@ -393,39 +419,61 @@ module Pubid
393
419
  normalized = normalized.gsub("CAN3-", "CSA ")
394
420
 
395
421
  tree = Parser.new.parse(normalized)
396
- result = Builder.new.build(tree)
422
+ result = build!(tree, input)
397
423
 
398
424
  # Set publisher prefix if detected
399
- if result && publisher_prefix
400
- set_publisher_prefix(result, publisher_prefix)
401
- end
425
+ set_publisher_prefix(result, publisher_prefix) if publisher_prefix
402
426
 
403
427
  # Set year format if detected as dash and not already set
404
- if result && has_dash_year && result.year_format.nil?
405
- result.year_format = "dash"
406
- end
428
+ result.year_format = "dash" if has_dash_year && result.year_format.nil?
407
429
 
408
430
  result
409
431
  rescue Parslet::ParseFailed => e
410
432
  raise e
411
433
  end
412
434
 
435
+ # Build a parse tree into an identifier, or fail loudly.
436
+ #
437
+ # Builder#build can return nil for a tree it recognises but cannot map
438
+ # to a class. `parse` must return an identifier or raise — every flavor
439
+ # but `api` already honours that — because a nil surfaces in the caller
440
+ # as a NoMethodError far from the input that caused it, rather than as a
441
+ # catchable parse failure.
442
+ def self.build!(tree, input)
443
+ built = Builder.new.build(tree)
444
+ return built if built
445
+
446
+ raise Pubid::Errors::ParseError.new(
447
+ "Unparseable CSA identifier: #{input}",
448
+ input: input, flavor: "csa",
449
+ )
450
+ end
451
+
452
+ # Probe whether a string parses. The package base scan walks
453
+ # progressively longer prefixes and keeps the longest that succeeds, so
454
+ # it is the one caller that genuinely wants a nil rather than a raise.
455
+ def self.try_parse(input)
456
+ parse(input)
457
+ rescue Parslet::ParseFailed, ArgumentError
458
+ nil
459
+ end
460
+
413
461
  def self.set_publisher_prefix(obj, prefix)
414
462
  # Set on main object if it has the attribute
415
463
  if obj.class.attributes.key?(:publisher_prefix)
416
464
  obj.publisher_prefix = prefix
417
465
  end
418
466
 
419
- # Set on combined identifier parts
467
+ # Set on combined identifier parts. The primary designation always
468
+ # takes the prefix; a trailing one only when it printed a publisher of
469
+ # its own (a bare continuation must stay bare).
420
470
  if obj.is_a?(Identifiers::Combined)
421
- if obj.first&.class&.attributes&.key?(:publisher_prefix)
422
- obj.first.publisher_prefix = prefix
423
- end
424
- if obj.second && obj.second.class.attributes.key?(:has_publisher) && obj.second.has_publisher && obj.second.class.attributes.key?(:publisher_prefix)
425
- obj.second.publisher_prefix = prefix
426
- end
427
- if obj.third && obj.third.class.attributes.key?(:has_publisher) && obj.third.has_publisher && obj.third.class.attributes.key?(:publisher_prefix)
428
- obj.third.publisher_prefix = prefix
471
+ Array(obj.identifiers).each_with_index do |part, index|
472
+ next unless part.class.attributes.key?(:publisher_prefix)
473
+ next if index.positive? &&
474
+ !(part.class.attributes.key?(:has_publisher) && part.has_publisher)
475
+
476
+ part.publisher_prefix = prefix
429
477
  end
430
478
  end
431
479
 
@@ -489,15 +537,29 @@ module Pubid
489
537
  end
490
538
 
491
539
  # CSA encodes its identity across many shape-specific attributes
492
- # (`publisher_prefix`, `code`, `no_number`, `series`, `series_prefix`,
540
+ # (`publisher_prefix`, `number`, `no_number`, `series`, `series_prefix`,
493
541
  # `package`, `reaffirmation`, `year` with prefix/format/French markers,
494
542
  # etc.) that the generic MrString renderer doesn't know about. The
495
543
  # CSA `to_s` already round-trips losslessly through the CSA parser, so
496
544
  # MR mirrors it: ` ` → `.`, `:` → `.` (so the year never looks like
497
545
  # another code segment), `/` → `-` (CAN/CSA- prefix), then lowercased
498
546
  # to match the all-lowercase MR convention (issue #142).
547
+ #
548
+ # Those three are the SEMANTIC mappings — they carry CSA's segment
549
+ # structure. Everything else is then neutralised by CHARSET, not by an
550
+ # enumerated escape list, so a character that appears in a future
551
+ # reference cannot leak into what `to_slug` hands a filesystem. CSA
552
+ # references really do carry `(`, `)`, `,`, `&` and `+` today
553
+ # (`CSA C22.2 NO. 100:14 (R2024)`, `… Code, Handbook & Training
554
+ # Package`, `… + A1:15`), and the old `tr` chain let all of them
555
+ # through — 601 of 816 slugs were not filename-safe. The BIPM `mr_slug`
556
+ # precedent.
499
557
  def to_mr_string
500
- to_s.tr(" ", ".").tr(":", ".").tr("/", "-").downcase
558
+ to_s
559
+ .downcase
560
+ .tr(" ", ".").tr(":", ".").tr("/", "-")
561
+ .gsub(/[^a-z0-9._-]+/, "-")
562
+ .gsub(/\A[-.]+|[-.]+\z/, "")
501
563
  end
502
564
 
503
565
  def to_slug
@@ -5,23 +5,33 @@ require "lutaml/model"
5
5
  module Pubid
6
6
  module Csa
7
7
  module Identifiers
8
- class Bundled < Lutaml::Model::Serializable
9
- attribute :base, Standard
10
- attribute :bundled_with, Standard, collection: true
8
+ class Bundled < Identifier
9
+ # The consolidated base document and the amendments bundled with it.
10
+ # Both are typed cross-flavor and polymorphic — see
11
+ # WrapperIdentifier#base — because a bundle's base is routinely a Cec,
12
+ # which is not a Standard subclass. #root is inherited: it walks
13
+ # `base` to the origin document.
14
+ attribute :base, ::Pubid::Identifier, polymorphic: true
15
+ attribute :bundled_with, ::Pubid::Identifier, polymorphic: true,
16
+ collection: true
11
17
  attribute :reaffirmation, :string
12
18
  attribute :original_reaffirmation_4digit, :boolean, default: -> {
13
19
  false
14
20
  }
15
21
  attribute :year_format, :string # Add for compatibility with identifier.rb
16
22
 
17
- # CSA bundles are not Pubid::Identifier objects, so they do not inherit
18
- # #root. They are their own origin for matching purposes.
19
- def root
20
- self
23
+ # A bundle is a base document consolidated with its amendments, so
24
+ # both matching primitives peel to that base.
25
+ def base_document
26
+ base ? base.base_document : self
21
27
  end
22
28
 
23
- def to_s
24
- # For Cec identifiers, use normalized form (code instead of cec_part + NO.)
29
+ def drop_supplements
30
+ base || self
31
+ end
32
+
33
+ def to_s(**opts)
34
+ # For Cec identifiers, use normalized form (number instead of cec_part + NO.)
25
35
  # This is used for "normalized form" rendering in bundled identifiers
26
36
  if base.is_a?(Cec)
27
37
  # Render base with normalized code format
@@ -37,7 +47,7 @@ module Pubid
37
47
  end
38
48
  end
39
49
  base_str = (needs_space ? "#{prefix} " : prefix) +
40
- base.code.value.to_s + # Use normalized code (e.g., "C22.2-1")
50
+ base.number.value.to_s + # Normalized code (e.g. "C22.2-1")
41
51
  ":#{year_display}"
42
52
  parts = [base_str]
43
53
  else
@@ -50,7 +60,7 @@ module Pubid
50
60
  bundled_with.each do |bundled|
51
61
  # For Cec identifiers, use normalized code format
52
62
  if bundled.is_a?(Cec)
53
- bundled_part = bundled.code.value.to_s # Normalized code (e.g., "C22.2-2")
63
+ bundled_part = bundled.number.value.to_s # e.g. "C22.2-2"
54
64
  if bundled.year
55
65
  # Use dash if year_format is dash, otherwise colon
56
66
  separator = bundled.year_format == "dash" ? "-" : ":"
@@ -68,7 +78,7 @@ module Pubid
68
78
  else
69
79
  # Standard rendering for other identifier types
70
80
  bundled_part = ""
71
- bundled_part += bundled.code.to_s if bundled.code
81
+ bundled_part += bundled.number.to_s if bundled.number
72
82
 
73
83
  if bundled.year
74
84
  # Use dash if year_format is dash, otherwise colon
@@ -123,7 +133,7 @@ module Pubid
123
133
  end
124
134
  end
125
135
 
126
- result
136
+ annotate_plain_render(result, **opts)
127
137
  end
128
138
  end
129
139
  end
@@ -12,40 +12,40 @@ module Pubid
12
12
  #
13
13
  # This is a wrapper pattern where:
14
14
  # - CAN/ indicates Canadian adoption
15
- # - The wrapped_identifier is the actual CSA standard being adopted
15
+ # - The base is the actual CSA standard being adopted
16
16
  #
17
17
  # This is semantically different from a string prefix because:
18
18
  # - CAN/ wraps an entire identifier (which may itself be complex)
19
19
  # - The wrapped identifier is recursively parsed as a full CSA identifier
20
20
  # - Proper object composition, not string manipulation
21
21
  class CanadianAdopted < WrapperIdentifier
22
- def to_s
22
+ def to_s(**opts)
23
23
  # For CAN3- identifiers, don't add CAN/ prefix (CAN3- is already complete)
24
24
  # For Series with CAN/CSA- prefix, don't add CAN/ (it's already complete)
25
25
  # For CAN/CSA- identifiers, CAN/ wraps CSA- part
26
- if wrapped_identifier.class.attributes.key?(:publisher_prefix) && wrapped_identifier.publisher_prefix
27
- prefix = wrapped_identifier.publisher_prefix
26
+ if base.class.attributes.key?(:publisher_prefix) && base.publisher_prefix
27
+ prefix = base.publisher_prefix
28
28
  # CAN3- is standalone, just render wrapped identifier
29
29
  # CAN/CSA- is already complete for Series identifiers
30
- if prefix == "CAN3-" || (prefix == "CAN/CSA-" && wrapped_identifier.is_a?(Identifiers::Series))
31
- result = wrapped_identifier.to_s
30
+ if prefix == "CAN3-" || (prefix == "CAN/CSA-" && base.is_a?(Identifiers::Series))
31
+ result = base.to_s
32
32
  else
33
33
  # For other cases, prepend CAN/ prefix
34
- result = "CAN/#{wrapped_identifier}"
34
+ result = "CAN/#{base}"
35
35
  end
36
36
  else
37
37
  # No publisher_prefix, prepend CAN/
38
- result = "CAN/#{wrapped_identifier}"
38
+ result = "CAN/#{base}"
39
39
  end
40
- # Only add reaffirmation from wrapper if wrapped_identifier doesn't have one
40
+ # Only add reaffirmation from wrapper if base doesn't have one
41
41
  # (to avoid duplicates - Identifier.parse sets it on both)
42
- if reaffirmation && !(wrapped_identifier.class.attributes.key?(:reaffirmation) && wrapped_identifier.reaffirmation)
42
+ if reaffirmation && !(base.class.attributes.key?(:reaffirmation) && base.reaffirmation)
43
43
  # Determine if space is needed before reaffirmation
44
44
  # Space needed if year is 2-digit and reaffirmation is 4-digit (original format)
45
45
  # No space if both year and reaffirmation are 2-digit
46
46
  # Check original_year_4digit flag - if false, year was originally 2-digit
47
- year_was_2digit = wrapped_identifier.class.attributes.key?(:original_year_4digit) &&
48
- !wrapped_identifier.original_year_4digit
47
+ year_was_2digit = base.class.attributes.key?(:original_year_4digit) &&
48
+ !base.original_year_4digit
49
49
  # Check original_reaffirmation_4digit flag
50
50
  reaffirmation_was_4digit = original_reaffirmation_4digit
51
51
 
@@ -74,7 +74,7 @@ module Pubid
74
74
  result += "(R#{reaffirmation_str})"
75
75
  end
76
76
  end
77
- result
77
+ annotate_plain_render(result, **opts)
78
78
  end
79
79
  end
80
80
  end
@@ -13,12 +13,52 @@ module Pubid
13
13
  attribute :cec_part, Components::Code # C22.2, C22.3, C22.4, C22.6
14
14
  attribute :no_number, Components::Code # Number after NO.
15
15
 
16
- # Combined code attribute for compatibility
17
- # Returns cec_part + "-" + no_number (e.g., "C22.2-1")
18
- def code
16
+ # `CSA C22.2 NO. 286:23` stores the joined "C22.2-286" in `number`, which
17
+ # appears nowhere in the printed form; the two halves it does print live
18
+ # in `cec_part` and `no_number`. Naming them gives the identifier its
19
+ # docnumber spans back.
20
+ def annotation_tokens
21
+ super + [
22
+ [:publisher_prefix, "publisher"],
23
+ [:cec_part, "docnumber"],
24
+ [:no_number, "docnumber"],
25
+ ]
26
+ end
27
+
28
+ # Merged with SingleIdentifier's block (lutaml combines an inherited
29
+ # key_value block with a subclass one), so this adds only the half of
30
+ # the code that Cec keeps separately.
31
+ key_value do
32
+ map "cec_part",
33
+ with: { to: :cec_part_to_kv, from: :cec_part_from_kv }
34
+ end
35
+
36
+ def cec_part_to_kv(model, doc)
37
+ emit_kv(doc, "cec_part", model.cec_part)
38
+ end
39
+
40
+ def cec_part_from_kv(model, value)
41
+ model.cec_part = Components::Code.new(value: value.to_s)
42
+ end
43
+
44
+ # The document code, synthesised from its two halves:
45
+ # cec_part + "-" + no_number (e.g. "C22.2-1"). This is the index key.
46
+ #
47
+ # It shadows the reader lutaml generates for the inherited `number`
48
+ # attribute, which is safe because Cec declares no `number` of its own
49
+ # — the ITU precedent (itu/identifiers/base.rb). `cec_part` and
50
+ # `no_number` are themselves serialized, so from_hash reproduces the
51
+ # synthesised value.
52
+ #
53
+ # Deliberately NOT memoised: `@number` is the very ivar lutaml uses to
54
+ # store the inherited attribute, so caching there would both discard a
55
+ # value assigned through the generated writer and go stale if
56
+ # `cec_part`/`no_number` were reassigned — and `number` is the relaton
57
+ # index key. The concatenation is trivial, so there is nothing to save.
58
+ def number
19
59
  return nil unless cec_part && no_number
20
60
 
21
- @code ||= Components::Code.new(value: "#{cec_part.value}-#{no_number.value}")
61
+ Components::Code.new(value: "#{cec_part.value}-#{no_number.value}")
22
62
  end
23
63
  end
24
64
  end