pubid 2.0.0.pre.alpha.9 → 2.0.0.pre.alpha.12

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 (461) 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/components/date.rb +10 -4
  82. data/lib/pubid/bsi/identifiers/adopted_european_norm.rb +36 -7
  83. data/lib/pubid/bsi/identifiers/adopted_international_standard.rb +7 -6
  84. data/lib/pubid/bsi/identifiers/british_industrial_practice.rb +1 -2
  85. data/lib/pubid/bsi/identifiers/bundled_identifier.rb +10 -0
  86. data/lib/pubid/bsi/identifiers/committee_document.rb +10 -1
  87. data/lib/pubid/bsi/identifiers/handbook.rb +1 -3
  88. data/lib/pubid/bsi/identifiers/practice_guide.rb +1 -2
  89. data/lib/pubid/bsi/identifiers/set.rb +11 -0
  90. data/lib/pubid/bsi/identifiers/standalone_amendment.rb +10 -1
  91. data/lib/pubid/bsi/parser.rb +1 -1
  92. data/lib/pubid/bsi/renderer.rb +63 -51
  93. data/lib/pubid/bsi/single_identifier.rb +27 -9
  94. data/lib/pubid/bsi/urn_generator.rb +11 -2
  95. data/lib/pubid/bsi.rb +7 -1
  96. data/lib/pubid/builder/base.rb +18 -8
  97. data/lib/pubid/bundled_identifier.rb +16 -6
  98. data/lib/pubid/calconnect/identifier.rb +9 -5
  99. data/lib/pubid/calconnect/parser.rb +1 -1
  100. data/lib/pubid/calconnect.rb +7 -1
  101. data/lib/pubid/ccsds/identifier.rb +13 -2
  102. data/lib/pubid/ccsds/identifiers/base.rb +4 -2
  103. data/lib/pubid/ccsds/identifiers/corrigendum.rb +2 -2
  104. data/lib/pubid/ccsds/parser.rb +1 -1
  105. data/lib/pubid/ccsds/single_identifier.rb +16 -12
  106. data/lib/pubid/ccsds.rb +1 -0
  107. data/lib/pubid/cen_cenelec/builder.rb +101 -76
  108. data/lib/pubid/cen_cenelec/identifier.rb +115 -3
  109. data/lib/pubid/cen_cenelec/identifiers/adopted_european_norm.rb +27 -16
  110. data/lib/pubid/cen_cenelec/identifiers/amendment.rb +46 -6
  111. data/lib/pubid/cen_cenelec/identifiers/base.rb +8 -9
  112. data/lib/pubid/cen_cenelec/identifiers/cen_report.rb +1 -1
  113. data/lib/pubid/cen_cenelec/identifiers/cen_workshop_agreement.rb +1 -1
  114. data/lib/pubid/cen_cenelec/identifiers/cenelec_harmonization_document.rb +1 -1
  115. data/lib/pubid/cen_cenelec/identifiers/consolidated_identifier.rb +29 -20
  116. data/lib/pubid/cen_cenelec/identifiers/corrigendum.rb +47 -8
  117. data/lib/pubid/cen_cenelec/identifiers/european_prestandard.rb +27 -2
  118. data/lib/pubid/cen_cenelec/identifiers/european_specification.rb +1 -1
  119. data/lib/pubid/cen_cenelec/identifiers/fragment.rb +15 -5
  120. data/lib/pubid/cen_cenelec/identifiers/guide.rb +1 -1
  121. data/lib/pubid/cen_cenelec/identifiers/harmonization_document.rb +1 -1
  122. data/lib/pubid/cen_cenelec/identifiers/technical_report.rb +1 -1
  123. data/lib/pubid/cen_cenelec/identifiers/technical_specification.rb +1 -1
  124. data/lib/pubid/cen_cenelec/parser.rb +1 -1
  125. data/lib/pubid/cen_cenelec/renderer.rb +55 -60
  126. data/lib/pubid/cen_cenelec/single_identifier.rb +20 -2
  127. data/lib/pubid/cen_cenelec/urn_generator.rb +66 -44
  128. data/lib/pubid/cen_cenelec.rb +28 -10
  129. data/lib/pubid/cie/builder.rb +10 -0
  130. data/lib/pubid/cie/identifier.rb +6 -1
  131. data/lib/pubid/cie/identifiers/bundle.rb +4 -2
  132. data/lib/pubid/cie/identifiers/conference.rb +2 -2
  133. data/lib/pubid/cie/identifiers/corrigendum.rb +2 -2
  134. data/lib/pubid/cie/identifiers/dual_published.rb +2 -2
  135. data/lib/pubid/cie/identifiers/identical.rb +2 -2
  136. data/lib/pubid/cie/identifiers/joint_published.rb +2 -2
  137. data/lib/pubid/cie/identifiers/proceedings.rb +8 -6
  138. data/lib/pubid/cie/identifiers/standard.rb +22 -13
  139. data/lib/pubid/cie/identifiers/supplement.rb +2 -2
  140. data/lib/pubid/cie/identifiers/tutorial_bundle.rb +2 -2
  141. data/lib/pubid/cie/parser.rb +1 -1
  142. data/lib/pubid/cie.rb +7 -1
  143. data/lib/pubid/conformance/checks.rb +72 -0
  144. data/lib/pubid/conformance/corpus/case.rb +55 -0
  145. data/lib/pubid/conformance/corpus.rb +36 -0
  146. data/lib/pubid/conformance/generator.rb +212 -0
  147. data/lib/pubid/conformance/pending.rb +63 -0
  148. data/lib/pubid/conformance/runner.rb +153 -0
  149. data/lib/pubid/conformance.rb +55 -0
  150. data/lib/pubid/core.rb +2 -0
  151. data/lib/pubid/csa/builder.rb +47 -47
  152. data/lib/pubid/csa/composite_identifier.rb +14 -19
  153. data/lib/pubid/csa/identifier.rb +143 -81
  154. data/lib/pubid/csa/identifiers/bundled.rb +23 -13
  155. data/lib/pubid/csa/identifiers/canadian_adopted.rb +13 -13
  156. data/lib/pubid/csa/identifiers/cec.rb +44 -4
  157. data/lib/pubid/csa/identifiers/combined.rb +95 -73
  158. data/lib/pubid/csa/identifiers/csa_adopted.rb +4 -4
  159. data/lib/pubid/csa/identifiers/package.rb +2 -2
  160. data/lib/pubid/csa/parser.rb +6 -2
  161. data/lib/pubid/csa/renderer.rb +13 -5
  162. data/lib/pubid/csa/single_identifier.rb +93 -4
  163. data/lib/pubid/csa/urn_generator.rb +9 -2
  164. data/lib/pubid/csa/wrapper_identifier.rb +39 -23
  165. data/lib/pubid/csa.rb +7 -1
  166. data/lib/pubid/doi/identifier.rb +6 -3
  167. data/lib/pubid/doi/parser.rb +1 -1
  168. data/lib/pubid/doi.rb +1 -0
  169. data/lib/pubid/easc/identifier.rb +0 -2
  170. data/lib/pubid/easc/parser.rb +10 -2
  171. data/lib/pubid/easc.rb +1 -0
  172. data/lib/pubid/ecma/builder.rb +23 -9
  173. data/lib/pubid/ecma/identifier.rb +81 -10
  174. data/lib/pubid/ecma/parser.rb +39 -4
  175. data/lib/pubid/ecma/renderer.rb +29 -8
  176. data/lib/pubid/ecma/urn_generator.rb +34 -13
  177. data/lib/pubid/ecma/urn_parser.rb +30 -14
  178. data/lib/pubid/ecma.rb +1 -0
  179. data/lib/pubid/errors.rb +80 -0
  180. data/lib/pubid/etsi/builder.rb +6 -1
  181. data/lib/pubid/etsi/identifiers/base.rb +34 -20
  182. data/lib/pubid/etsi/identifiers/etsi_standard.rb +120 -41
  183. data/lib/pubid/etsi/identifiers/supplement_identifier.rb +10 -0
  184. data/lib/pubid/etsi/parser.rb +1 -1
  185. data/lib/pubid/etsi.rb +1 -0
  186. data/lib/pubid/evs/builder.rb +26 -0
  187. data/lib/pubid/evs/identifier.rb +37 -0
  188. data/lib/pubid/evs/identifiers/national_adoption.rb +22 -0
  189. data/lib/pubid/evs/identifiers.rb +9 -0
  190. data/lib/pubid/evs/parser.rb +27 -0
  191. data/lib/pubid/evs/renderer.rb +30 -0
  192. data/lib/pubid/evs/urn_generator.rb +30 -0
  193. data/lib/pubid/evs/urn_parser.rb +80 -0
  194. data/lib/pubid/evs.rb +90 -0
  195. data/lib/pubid/export.rb +1 -0
  196. data/lib/pubid/format_detector.rb +1 -0
  197. data/lib/pubid/gb/builder.rb +14 -9
  198. data/lib/pubid/gb/identifier.rb +29 -7
  199. data/lib/pubid/gb/parser.rb +5 -1
  200. data/lib/pubid/gb/renderer.rb +4 -3
  201. data/lib/pubid/gb.rb +2 -0
  202. data/lib/pubid/gost/builder.rb +46 -9
  203. data/lib/pubid/gost/identifier.rb +0 -2
  204. data/lib/pubid/gost/identifiers/foreign_reference.rb +1 -1
  205. data/lib/pubid/gost/parser.rb +10 -2
  206. data/lib/pubid/gost.rb +1 -0
  207. data/lib/pubid/iala/identifier.rb +9 -6
  208. data/lib/pubid/iala/parser.rb +10 -2
  209. data/lib/pubid/iala/urn_parser.rb +6 -2
  210. data/lib/pubid/iala.rb +1 -0
  211. data/lib/pubid/iana/builder.rb +3 -1
  212. data/lib/pubid/iana/identifier.rb +74 -6
  213. data/lib/pubid/iana/identifiers/registry.rb +36 -0
  214. data/lib/pubid/iana/parser.rb +1 -1
  215. data/lib/pubid/iana/renderer.rb +3 -0
  216. data/lib/pubid/iana/urn_generator.rb +5 -1
  217. data/lib/pubid/iana.rb +1 -0
  218. data/lib/pubid/identifier.rb +605 -13
  219. data/lib/pubid/identifier_metadata.rb +1 -0
  220. data/lib/pubid/idf/builder.rb +3 -3
  221. data/lib/pubid/idf/identifier.rb +25 -9
  222. data/lib/pubid/idf/identifiers/amendment.rb +1 -1
  223. data/lib/pubid/idf/identifiers/corrigendum.rb +1 -1
  224. data/lib/pubid/idf/identifiers/international_standard.rb +1 -1
  225. data/lib/pubid/idf/identifiers/reviewed_method.rb +1 -1
  226. data/lib/pubid/idf/parser.rb +1 -1
  227. data/lib/pubid/idf/renderer.rb +4 -4
  228. data/lib/pubid/idf/single_identifier.rb +1 -1
  229. data/lib/pubid/idf/urn_generator.rb +1 -1
  230. data/lib/pubid/idf.rb +7 -1
  231. data/lib/pubid/iec/builder.rb +14 -7
  232. data/lib/pubid/iec/components.rb +0 -1
  233. data/lib/pubid/iec/identifier.rb +73 -83
  234. data/lib/pubid/iec/identifiers/amendment.rb +6 -2
  235. data/lib/pubid/iec/identifiers/component_specification.rb +0 -10
  236. data/lib/pubid/iec/identifiers/conformity_assessment.rb +0 -10
  237. data/lib/pubid/iec/identifiers/corrigendum.rb +2 -2
  238. data/lib/pubid/iec/identifiers/fragment_identifier.rb +2 -4
  239. data/lib/pubid/iec/identifiers/guide.rb +0 -16
  240. data/lib/pubid/iec/identifiers/international_standard.rb +15 -2
  241. data/lib/pubid/iec/identifiers/operational_document.rb +0 -10
  242. data/lib/pubid/iec/identifiers/publicly_available_specification.rb +0 -12
  243. data/lib/pubid/iec/identifiers/societal_technology_trend_report.rb +0 -10
  244. data/lib/pubid/iec/identifiers/systems_reference_document.rb +0 -10
  245. data/lib/pubid/iec/identifiers/technical_report.rb +0 -17
  246. data/lib/pubid/iec/identifiers/technical_specification.rb +0 -17
  247. data/lib/pubid/iec/identifiers/technology_report.rb +0 -10
  248. data/lib/pubid/iec/identifiers/test_report_form.rb +0 -11
  249. data/lib/pubid/iec/identifiers/white_paper.rb +0 -10
  250. data/lib/pubid/iec/identifiers/working_document.rb +24 -10
  251. data/lib/pubid/iec/parser.rb +2 -18
  252. data/lib/pubid/iec/renderer.rb +13 -10
  253. data/lib/pubid/iec/single_identifier.rb +33 -10
  254. data/lib/pubid/iec/urn_generator.rb +227 -61
  255. data/lib/pubid/iec/urn_parser.rb +42 -4
  256. data/lib/pubid/iec.rb +1 -0
  257. data/lib/pubid/ieee/aiee/identifier.rb +2 -2
  258. data/lib/pubid/ieee/aiee/parser.rb +1 -1
  259. data/lib/pubid/ieee/builder.rb +87 -3
  260. data/lib/pubid/ieee/identifiers/adopted_standard.rb +32 -0
  261. data/lib/pubid/ieee/identifiers/base.rb +42 -5
  262. data/lib/pubid/ieee/identifiers/csa_dual_published.rb +21 -0
  263. data/lib/pubid/ieee/identifiers/dual_published.rb +55 -0
  264. data/lib/pubid/ieee/identifiers/interpretation_identifier.rb +23 -0
  265. data/lib/pubid/ieee/identifiers/joint_development.rb +11 -7
  266. data/lib/pubid/ieee/identifiers/multi_numbered_identifier.rb +57 -5
  267. data/lib/pubid/ieee/identifiers/nesc/base.rb +2 -2
  268. data/lib/pubid/ieee/identifiers/nesc/draft.rb +2 -2
  269. data/lib/pubid/ieee/identifiers/nesc/handbook.rb +2 -2
  270. data/lib/pubid/ieee/identifiers/nesc/redline.rb +2 -2
  271. data/lib/pubid/ieee/identifiers/nesc/standard.rb +2 -2
  272. data/lib/pubid/ieee/identifiers/si_standard.rb +5 -2
  273. data/lib/pubid/ieee/ire/identifier.rb +2 -2
  274. data/lib/pubid/ieee/ire/parser.rb +20 -2
  275. data/lib/pubid/ieee/nesc/parser.rb +1 -1
  276. data/lib/pubid/ieee/parser.rb +208 -33
  277. data/lib/pubid/ieee/project_renderer.rb +46 -0
  278. data/lib/pubid/ieee/renderer.rb +11 -8
  279. data/lib/pubid/ieee/urn_generator.rb +32 -3
  280. data/lib/pubid/ieee.rb +9 -1
  281. data/lib/pubid/ieee_debug.rb +1 -0
  282. data/lib/pubid/ietf/builder.rb +24 -8
  283. data/lib/pubid/ietf/identifiers/base.rb +25 -28
  284. data/lib/pubid/ietf/identifiers/bcp.rb +11 -0
  285. data/lib/pubid/ietf/identifiers/fyi.rb +11 -0
  286. data/lib/pubid/ietf/identifiers/internet_draft.rb +18 -2
  287. data/lib/pubid/ietf/identifiers/rfc.rb +2 -0
  288. data/lib/pubid/ietf/identifiers/serialization.rb +51 -0
  289. data/lib/pubid/ietf/identifiers/std.rb +11 -0
  290. data/lib/pubid/ietf/identifiers.rb +1 -0
  291. data/lib/pubid/ietf/parser.rb +27 -9
  292. data/lib/pubid/ietf/renderer.rb +44 -6
  293. data/lib/pubid/ietf/urn_generator.rb +20 -6
  294. data/lib/pubid/ietf/urn_parser.rb +10 -2
  295. data/lib/pubid/ietf.rb +12 -2
  296. data/lib/pubid/iho/identifiers/base.rb +10 -3
  297. data/lib/pubid/iho/parser.rb +1 -1
  298. data/lib/pubid/iho.rb +1 -0
  299. data/lib/pubid/isbn/identifier.rb +25 -7
  300. data/lib/pubid/isbn/parser.rb +1 -1
  301. data/lib/pubid/isbn.rb +1 -0
  302. data/lib/pubid/iso/builder.rb +10 -11
  303. data/lib/pubid/iso/bundled_identifier.rb +5 -3
  304. data/lib/pubid/iso/combined_identifier.rb +5 -3
  305. data/lib/pubid/iso/components.rb +0 -1
  306. data/lib/pubid/iso/identifier.rb +23 -11
  307. data/lib/pubid/iso/identifiers/directives.rb +7 -4
  308. data/lib/pubid/iso/identifiers/directives_supplement.rb +4 -4
  309. data/lib/pubid/iso/identifiers/tc_document.rb +31 -46
  310. data/lib/pubid/iso/normalizer.rb +1 -1
  311. data/lib/pubid/iso/parser.rb +1 -1
  312. data/lib/pubid/iso/urn_generator.rb +10 -10
  313. data/lib/pubid/iso.rb +12 -5
  314. data/lib/pubid/itu/builder.rb +10 -0
  315. data/lib/pubid/itu/identifiers/addendum.rb +2 -2
  316. data/lib/pubid/itu/identifiers/amendment.rb +2 -2
  317. data/lib/pubid/itu/identifiers/base.rb +29 -5
  318. data/lib/pubid/itu/identifiers/contribution.rb +31 -0
  319. data/lib/pubid/itu/identifiers/corrigendum.rb +2 -2
  320. data/lib/pubid/itu/identifiers/errata.rb +2 -2
  321. data/lib/pubid/itu/identifiers/supplement.rb +2 -2
  322. data/lib/pubid/itu/identifiers.rb +1 -0
  323. data/lib/pubid/itu/parser.rb +20 -3
  324. data/lib/pubid/itu.rb +1 -0
  325. data/lib/pubid/jcgm/builder.rb +1 -1
  326. data/lib/pubid/jcgm/identifier.rb +10 -0
  327. data/lib/pubid/jcgm/identifiers/meeting.rb +1 -1
  328. data/lib/pubid/jcgm/parser.rb +10 -3
  329. data/lib/pubid/jcgm/renderer.rb +10 -3
  330. data/lib/pubid/jcgm/single_identifier.rb +24 -15
  331. data/lib/pubid/jcgm/urn_generator.rb +1 -1
  332. data/lib/pubid/jcgm/urn_parser.rb +156 -14
  333. data/lib/pubid/jcgm.rb +10 -0
  334. data/lib/pubid/jis/identifier.rb +10 -3
  335. data/lib/pubid/jis/parser.rb +1 -1
  336. data/lib/pubid/jis.rb +1 -0
  337. data/lib/pubid/nist/builder.rb +12 -12
  338. data/lib/pubid/nist/components/stage.rb +2 -2
  339. data/lib/pubid/nist/configuration.rb +6 -8
  340. data/lib/pubid/nist/identifiers/base.rb +22 -5
  341. data/lib/pubid/nist/identifiers/circular.rb +9 -2
  342. data/lib/pubid/nist/identifiers/circular_supplement.rb +14 -5
  343. data/lib/pubid/nist/identifiers/commercial_standard_emergency.rb +1 -1
  344. data/lib/pubid/nist/identifiers/commercial_standards_monthly.rb +7 -1
  345. data/lib/pubid/nist/identifiers/crpl_report.rb +12 -7
  346. data/lib/pubid/nist/identifiers/federal_information_processing_standards.rb +2 -2
  347. data/lib/pubid/nist/identifiers/handbook.rb +9 -2
  348. data/lib/pubid/nist/identifiers/internal_report.rb +12 -5
  349. data/lib/pubid/nist/identifiers/miscellaneous_publication.rb +16 -8
  350. data/lib/pubid/nist/identifiers/monograph.rb +14 -7
  351. data/lib/pubid/nist/identifiers/report.rb +14 -6
  352. data/lib/pubid/nist/parser.rb +1 -1
  353. data/lib/pubid/nist/series/ir.rb +3 -7
  354. data/lib/pubid/nist/supplement_identifier.rb +16 -1
  355. data/lib/pubid/nist.rb +7 -1
  356. data/lib/pubid/oasis/builder.rb +12 -10
  357. data/lib/pubid/oasis/identifier.rb +89 -11
  358. data/lib/pubid/oasis/parser.rb +1 -1
  359. data/lib/pubid/oasis.rb +7 -1
  360. data/lib/pubid/ogc/identifier.rb +9 -5
  361. data/lib/pubid/ogc/parser.rb +1 -1
  362. data/lib/pubid/ogc.rb +1 -0
  363. data/lib/pubid/oiml/builder.rb +7 -3
  364. data/lib/pubid/oiml/identifiers/basic_publication.rb +2 -0
  365. data/lib/pubid/oiml/identifiers/bulletin.rb +40 -10
  366. data/lib/pubid/oiml/identifiers/code_number.rb +90 -0
  367. data/lib/pubid/oiml/identifiers/document.rb +2 -0
  368. data/lib/pubid/oiml/identifiers/expert_report.rb +2 -0
  369. data/lib/pubid/oiml/identifiers/guide.rb +2 -0
  370. data/lib/pubid/oiml/identifiers/recommendation.rb +2 -0
  371. data/lib/pubid/oiml/identifiers/seminar_report.rb +2 -0
  372. data/lib/pubid/oiml/identifiers/vocabulary.rb +2 -0
  373. data/lib/pubid/oiml/identifiers.rb +1 -0
  374. data/lib/pubid/oiml/parser.rb +8 -3
  375. data/lib/pubid/oiml/renderer.rb +3 -3
  376. data/lib/pubid/oiml/single_identifier.rb +26 -42
  377. data/lib/pubid/oiml/supplement_identifier.rb +27 -0
  378. data/lib/pubid/oiml/urn_generator.rb +4 -4
  379. data/lib/pubid/oiml.rb +7 -1
  380. data/lib/pubid/omg/builder.rb +1 -0
  381. data/lib/pubid/omg/identifier.rb +20 -3
  382. data/lib/pubid/omg/parser.rb +64 -11
  383. data/lib/pubid/omg/renderer.rb +13 -1
  384. data/lib/pubid/omg.rb +2 -1
  385. data/lib/pubid/parser/grammar.rb +74 -0
  386. data/lib/pubid/parser.rb +2 -0
  387. data/lib/pubid/parsers/mr_string.rb +19 -0
  388. data/lib/pubid/plateau/parser.rb +1 -1
  389. data/lib/pubid/plateau.rb +10 -0
  390. data/lib/pubid/prefixes_support.rb +1 -0
  391. data/lib/pubid/renderers/annotator.rb +233 -0
  392. data/lib/pubid/renderers/base.rb +13 -0
  393. data/lib/pubid/renderers/directives_renderer.rb +6 -4
  394. data/lib/pubid/renderers/human_readable.rb +3 -3
  395. data/lib/pubid/renderers/mr_string.rb +15 -9
  396. data/lib/pubid/renderers.rb +1 -0
  397. data/lib/pubid/rendering/numbering.rb +25 -7
  398. data/lib/pubid/rendering.rb +1 -0
  399. data/lib/pubid/sae/builder.rb +1 -1
  400. data/lib/pubid/sae/identifiers/base.rb +13 -3
  401. data/lib/pubid/sae/parser.rb +1 -1
  402. data/lib/pubid/sae/urn_generator.rb +1 -1
  403. data/lib/pubid/sae.rb +1 -0
  404. data/lib/pubid/schema/declaration.rb +33 -0
  405. data/lib/pubid/schema/error.rb +14 -0
  406. data/lib/pubid/schema/identifier_type.rb +23 -0
  407. data/lib/pubid/schema/loader.rb +117 -0
  408. data/lib/pubid/schema/typed_stage.rb +24 -0
  409. data/lib/pubid/schema.rb +25 -0
  410. data/lib/pubid/tgpp/builder.rb +1 -1
  411. data/lib/pubid/tgpp/identifier.rb +21 -5
  412. data/lib/pubid/tgpp/identifiers/technical_report.rb +2 -2
  413. data/lib/pubid/tgpp/identifiers/technical_specification.rb +2 -2
  414. data/lib/pubid/tgpp/parser.rb +22 -6
  415. data/lib/pubid/tgpp/renderer.rb +21 -5
  416. data/lib/pubid/tgpp/urn_generator.rb +27 -8
  417. data/lib/pubid/tgpp/urn_parser.rb +8 -6
  418. data/lib/pubid/tgpp.rb +7 -1
  419. data/lib/pubid/type_resolver.rb +16 -1
  420. data/lib/pubid/un/identifier.rb +6 -3
  421. data/lib/pubid/un/parser.rb +1 -1
  422. data/lib/pubid/un.rb +1 -0
  423. data/lib/pubid/urn_parser/errors.rb +8 -1
  424. data/lib/pubid/urn_parser.rb +1 -0
  425. data/lib/pubid/utils.rb +1 -0
  426. data/lib/pubid/version.rb +1 -1
  427. data/lib/pubid/w3c/builder.rb +5 -5
  428. data/lib/pubid/w3c/identifier.rb +38 -9
  429. data/lib/pubid/w3c/parser.rb +1 -1
  430. data/lib/pubid/w3c/renderer.rb +1 -1
  431. data/lib/pubid/w3c/urn_generator.rb +2 -2
  432. data/lib/pubid/w3c.rb +1 -0
  433. data/lib/pubid/xsf/identifier.rb +9 -5
  434. data/lib/pubid/xsf/parser.rb +17 -4
  435. data/lib/pubid/xsf.rb +7 -1
  436. data/lib/pubid.rb +399 -21
  437. data/lib/tasks/conformance.rake +34 -0
  438. data/lib/tasks/docs.rake +16 -14
  439. data/lib/tasks/schema.rake +88 -0
  440. data/schema/core/joint_prefixes.yaml +24 -0
  441. data/schema/iec.yaml +1017 -0
  442. data/schema/iso.yaml +1586 -0
  443. data/schema/schema.schema.yaml +96 -0
  444. metadata +40 -19
  445. data/archived-gems/pubid-ccsds/update_codes.yaml +0 -1
  446. data/archived-gems/pubid-iec/stages.yaml +0 -129
  447. data/archived-gems/pubid-iec/update_codes.yaml +0 -67
  448. data/archived-gems/pubid-ieee/update_codes.yaml +0 -104
  449. data/archived-gems/pubid-iso/stages.yaml +0 -106
  450. data/archived-gems/pubid-iso/update_codes.yaml +0 -4
  451. data/archived-gems/pubid-itu/i18n.yaml +0 -13
  452. data/archived-gems/pubid-itu/series.yaml +0 -42
  453. data/archived-gems/pubid-nist/publishers.yaml +0 -6
  454. data/archived-gems/pubid-nist/series.yaml +0 -121
  455. data/archived-gems/pubid-nist/update_codes.yaml +0 -93
  456. data/archived-gems/pubid-plateau/update_codes.yaml +0 -6
  457. data/lib/pubid/ccsds/identifiers/base_BASE_88929.rb +0 -70
  458. data/lib/pubid/cen_cenelec/supplement_identifier.rb +0 -48
  459. data/lib/pubid/iec/components/code.rb +0 -36
  460. data/lib/pubid/iso/components/code.rb +0 -24
  461. /data/{archived-gems/pubid-nist → data/nist}/stages.yaml +0 -0
@@ -31,16 +31,24 @@ module Pubid
31
31
  # @return [Identifier] parsed identifier
32
32
  def parse_urn(urn)
33
33
  unless urn.start_with?("urn:iec:std:")
34
- raise Errors::ParseError, "Invalid IEC URN: #{urn}"
34
+ raise Pubid::UrnParser::Errors::ParseError, "Invalid IEC URN: #{urn}"
35
35
  end
36
36
 
37
37
  code, lang, all_parts = urn_to_code(urn)
38
- raise Errors::ParseError, "Invalid IEC URN: #{urn}" unless code
38
+ unless code
39
+ raise Pubid::UrnParser::Errors::ParseError,
40
+ "Invalid IEC URN: #{urn}"
41
+ end
39
42
 
40
43
  id = Pubid::Iec::Identifier.parse(code)
41
44
  id.all_parts = true if all_parts && id.class.attributes.key?(:all_parts)
45
+ # The language slot joins codes with a hyphen ("en-fr"). Building one
46
+ # Language from the whole field gave a single bogus language that
47
+ # rendered as "(en-fr)".
42
48
  if lang && !lang.empty? && id.class.attributes.key?(:languages)
43
- id.languages = [::Pubid::Components::Language.new(code: lang)]
49
+ id.languages = lang.split("-").map do |code|
50
+ ::Pubid::Components::Language.new(code: code)
51
+ end
44
52
  end
45
53
  id
46
54
  end
@@ -57,7 +65,8 @@ module Pubid
57
65
  all_parts = false
58
66
 
59
67
  code = head.gsub("-", "/")
60
- code += " #{type}" unless type.nil? || type.empty?
68
+ type_code = type_slot_to_code(type)
69
+ code += " #{type_code}" unless type_code.empty?
61
70
  code += " #{num}"
62
71
  code += ":#{date}" unless date.nil? || date.empty?
63
72
  code += adjunct_to_code(fields[9..])
@@ -67,6 +76,8 @@ module Pubid
67
76
  # part or date, so to_s renders "IEC NNNN (all parts)").
68
77
  if deliv&.casecmp("SER")&.zero?
69
78
  all_parts = true
79
+ elsif (edition = edition_slot_to_code(deliv))
80
+ code += " #{edition}"
70
81
  elsif deliv && !deliv.empty?
71
82
  code += " #{deliv}"
72
83
  end
@@ -74,6 +85,33 @@ module Pubid
74
85
  [code, lang&.downcase, all_parts]
75
86
  end
76
87
 
88
+ # The type slot holds either the legacy type token ("TS"), a stage
89
+ # ("STAGE-10.20"), or both ("TS-STAGE-50.00"). A stage is written back as
90
+ # the abbreviation the grammar accepts, resolved from the registry by
91
+ # type code and harmonized code — so the round trip is exact in both
92
+ # directions. See issue #360 item 4.
93
+ def type_slot_to_code(type)
94
+ return "" if type.nil? || type.empty?
95
+
96
+ match = /\A(?:([A-Z]+)-)?STAGE-([\d.]+)\z/.match(type)
97
+ return type unless match
98
+
99
+ type_code = (match[1] || "IS").downcase
100
+ stage = Pubid::Iec.all_typed_stages.find do |s|
101
+ s.type_code.to_s == type_code &&
102
+ s.harmonized_stages&.first.to_s == match[2]
103
+ end
104
+ return "" unless stage
105
+
106
+ stage.abbr.first.to_s
107
+ end
108
+
109
+ # "ED-7" in the deliverable slot is an edition, not a deliverable code.
110
+ def edition_slot_to_code(deliv)
111
+ match = /\AED-(.+)\z/.match(deliv.to_s)
112
+ match && "ED#{match[1]}"
113
+ end
114
+
77
115
  # Port of Relaton::Iec.ajanct_to_code — recursively rebuilds amd/cor/ish
78
116
  # adjuncts from (relation, type, number, date) quartets. A "PLUS"
79
117
  # relation token (the consolidated "+" marker) yields "+"; otherwise "/".
data/lib/pubid/iec.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "pubid"
3
4
  module Pubid
4
5
  module Iec
5
6
  autoload :Components, "#{__dir__}/iec/components"
@@ -39,7 +39,7 @@ module Pubid
39
39
  Builder.new.build(parsed)
40
40
  end
41
41
 
42
- def to_s(date_format: nil, trademark: false)
42
+ def to_s(date_format: nil, trademark: false, **opts)
43
43
  result = [publisher]
44
44
  result << type if type
45
45
  result << code.to_s if code
@@ -81,7 +81,7 @@ module Pubid
81
81
  # document number/filename downstream). It stays reachable on
82
82
  # `relationships`.
83
83
 
84
- base
84
+ annotate_plain_render(base, **opts)
85
85
  end
86
86
  end
87
87
  end
@@ -7,7 +7,7 @@ module Pubid
7
7
  module Aiee
8
8
  # Parser for AIEE (American Institute of Electrical Engineers) identifiers
9
9
  # Historical period: 1884-1963 (merged into IEEE in 1963)
10
- class Parser < Parslet::Parser
10
+ class Parser < ::Pubid::Parser::Grammar
11
11
  # Basic building blocks
12
12
  rule(:space) { str(" ") }
13
13
  rule(:space?) { space.maybe }
@@ -290,6 +290,16 @@ module Pubid
290
290
  return build_interpretation_supplement(parsed_hash)
291
291
  end
292
292
 
293
+ # The same document with a TRAILING DATE parses FLAT — no :base subtree,
294
+ # just an :interpretation marker beside the number — so the branch above
295
+ # skipped it and determine_identifier_class built a base-less
296
+ # InterpretationIdentifier that dropped the number, the part and the
297
+ # publisher: "IEEE Std 1003.2-1992/INT, Dec. 1994 Ed." rendered as
298
+ # " 1003.2-1992/INT". Mirrors build_flat_corrigendum / build_flat_amendment.
299
+ if parsed_hash[:interpretation] && !parsed_hash[:base]
300
+ return build_flat_interpretation(parsed_hash)
301
+ end
302
+
293
303
  # Handle conformance supplements (check for base + conf_number)
294
304
  if parsed_hash[:base] && parsed_hash[:conf_number]
295
305
  return build_conformance_supplement(parsed_hash)
@@ -432,6 +442,35 @@ module Pubid
432
442
  )
433
443
  end
434
444
 
445
+ # Build an interpretation from a FLAT parse tree — the trailing-date form,
446
+ # where the :interpretation marker sits beside the base's own keys instead
447
+ # of over a :base subtree.
448
+ #
449
+ # The trailing date is the INTERPRETATION's date (an interpretation of the
450
+ # 1992 edition of 1003.2, issued in Dec 1994), so it goes on the wrapper;
451
+ # the base keeps its own `year`. That matches the nested form, whose
452
+ # `int_year` also lands on the wrapper.
453
+ # @param parsed_hash [Hash] flat parsed data carrying :interpretation
454
+ # @return [Identifiers::InterpretationIdentifier]
455
+ def build_flat_interpretation(parsed_hash)
456
+ int_year = extract_value(parsed_hash[:trailing_year]) if parsed_hash[:trailing_year]
457
+
458
+ base_hash = parsed_hash.reject do |key, _|
459
+ %i[interpretation trailing_month trailing_year].include?(key)
460
+ end
461
+ base = build_single_identifier(base_hash)
462
+
463
+ # The trailing MONTH is deliberately dropped, matching the nested form
464
+ # (whose `int_year` carries no month either). It was not preserved
465
+ # before this change, so nothing is lost; and setting it fed the URN's
466
+ # month_day_component, which put "Dec." — a word with a dot — into a
467
+ # URN segment.
468
+ Identifiers::InterpretationIdentifier.new(
469
+ base: base,
470
+ year: int_year,
471
+ )
472
+ end
473
+
435
474
  # Build amendment supplement with recursive base parsing. Mirrors
436
475
  # {#build_corrigendum_supplement} — reconstructs the base identifier
437
476
  # string, recursively parses it, and wraps it in an Amendment.
@@ -630,7 +669,14 @@ module Pubid
630
669
  # Extract publishers from joint_publishers
631
670
  if parsed[:joint_publishers]
632
671
  joint_pub_str = extract_value(parsed[:joint_publishers])
633
- attributes[:publishers] = joint_pub_str.split("/")
672
+ pubs = joint_pub_str.split("/")
673
+ attributes[:publishers] = pubs
674
+ # lutaml materializes attribute defaults during from_hash, so the
675
+ # deserialized joint carries publisher/copublisher alongside
676
+ # publishers; set the same shape on the parse path or the
677
+ # amendment-wrapped round-trip diverges on exactly those keys.
678
+ attributes[:publisher] = pubs.first
679
+ attributes[:copublisher] = pubs.drop(1)
634
680
  end
635
681
 
636
682
  # Build code with parts if present
@@ -654,6 +700,15 @@ module Pubid
654
700
  end
655
701
  attributes[:month] = extract_value(parsed[:month]) if parsed[:month]
656
702
 
703
+ # A text date trailing the DRAFT (pubid#216: "CD P26515/D1, March
704
+ # 2017") reaches the builder under :draft_month/:draft_year — the
705
+ # grammar captures them separately to avoid the :month/:year
706
+ # duplicate-subtree collision with the date clause.
707
+ if parsed[:draft_year]
708
+ attributes[:year] ||= extract_value(parsed[:draft_year])
709
+ attributes[:month] ||= extract_value(parsed[:draft_month])
710
+ end
711
+
657
712
  # Extract edition, from relaton's "/E-<n>" suffix (normalized to
658
713
  # "Edition <n>.0"). nil-residue hand-off item 1.
659
714
  attributes[:edition] = extract_value(parsed[:edition]) if parsed[:edition]
@@ -683,6 +738,10 @@ module Pubid
683
738
  attributes[:typed_stage] =
684
739
  Pubid::Ieee.locate_stage(stage_abbr)
685
740
  end
741
+ elsif parsed[:iso_published]
742
+ # Stage-less PUBLISHED joint form (pubid#317): ISO-led spelling,
743
+ # no project marker, no typed stage - renders as printed.
744
+ attributes[:lead_party] = "ISO"
686
745
  else
687
746
  # IEEE format - lead party is IEEE
688
747
  attributes[:lead_party] = "IEEE"
@@ -695,7 +754,19 @@ module Pubid
695
754
  Pubid::Ieee.locate_stage("P")
696
755
  end
697
756
 
698
- Identifiers::JointDevelopment.new(**attributes)
757
+ joint = Identifiers::JointDevelopment.new(**attributes)
758
+
759
+ # Amendment tail on the joint ISO-format form (pubid#317:
760
+ # "8802-11:2012/Amd.1:2014(E)"): wrap the joint id, mirroring
761
+ # build_flat_amendment.
762
+ return joint unless parsed[:amd_number]
763
+
764
+ amd_year = extract_value(parsed[:amd_year]) if parsed[:amd_year]
765
+ Identifiers::Amendment.new(
766
+ base: joint,
767
+ number: extract_value(parsed[:amd_number]),
768
+ year: amd_year,
769
+ )
699
770
  end
700
771
 
701
772
  # Build SI/PSI identifier from parsed data
@@ -770,9 +841,14 @@ module Pubid
770
841
  # Pattern: slash >> "C" >> digits >> dot >> digits >> dot >> digits >> dash >> year_digits
771
842
  # The parsed crossref is a string - extract the number part
772
843
  # Format: C62.22.1-1996 where C is part of the crossref notation
844
+ # The captured slice keeps the leading slash, which is a SEPARATOR in
845
+ # the printed reference, not part of the number. Storing it made the
846
+ # secondary's code "/C62.22.1-1996", which then failed the renderer's
847
+ # own cross-reference test (`/^C\d+\./`) and fell through to the
848
+ # " and " join. Strip it.
773
849
  secondary_id = Identifiers::Standard.new(
774
850
  publisher: "IEEE",
775
- number: extract_value(crossref),
851
+ number: extract_value(crossref).to_s.sub(%r{\A/}, ""),
776
852
  )
777
853
  elsif parsed[:secondary_joint]
778
854
  # Joint standard format: ", Std 1177-1989"
@@ -1145,6 +1221,14 @@ module Pubid
1145
1221
  return "P"
1146
1222
  elsif type_value == "Std"
1147
1223
  return "Std"
1224
+ elsif type_value == "Draft Std" && parsed[:draft].nil? && parsed[:digit_draft].nil?
1225
+ # A version-less "Draft Std" fabricates /D1 (issue #205);
1226
+ # deriving the stage from that fabricated version renders the
1227
+ # same P-prefixed form as an explicit "/D1" spelling, so the
1228
+ # render reaches its fixed point in one round (pubid#318).
1229
+ # Gated on no-draft: a dotted version ("D2.0") fails the registry
1230
+ # lookup above and must fall through, keeping "Draft Std".
1231
+ return "D1"
1148
1232
  elsif type_value.match?(/^No\.?$/)
1149
1233
  return type_value
1150
1234
  end
@@ -30,6 +30,38 @@ module Pubid
30
30
  ieee_identifier&.publisher || "IEEE"
31
31
  end
32
32
 
33
+ # An adoption names one document by two designations and carries no
34
+ # number of its own, so `root.number` — the key relaton-index sorts and
35
+ # bsearches on — was "". Walk to the IEEE designation, which is the one
36
+ # IEEE files the document under, and which is also the source of truth
37
+ # for `publisher` above.
38
+ def root
39
+ ieee_identifier ? ieee_identifier.root : self
40
+ end
41
+
42
+ # The document number must reach the URN and the MR slug too, not only
43
+ # `#root` — the project rule that an identity-bearing marker reaches
44
+ # every identity surface. `UrnGenerator#code_component` and the base
45
+ # `mr_number_with_part` both read `code_obj`, which a wrapper does not
46
+ # have, so all 66 adoptions collapsed onto 5 URNs and 5 slugs — and
47
+ # `to_slug` is an output FILENAME, so that is 66 documents overwriting
48
+ # each other. Delegate to the IEEE designation, as `#root` does.
49
+ #
50
+ # `code_obj` is a plain attr_accessor on the base, not a lutaml
51
+ # attribute, so overriding it as a method is safe here.
52
+ def code_obj
53
+ ieee_identifier&.code_obj
54
+ end
55
+
56
+ # Likewise the year: without it "AIEE No 15-1944" and "AIEE No 15-1959"
57
+ # — two different documents — produced the same slug "aiee.std.15".
58
+ # This is the `mr_year` HOOK, not a `#year` method: `year` is a lutaml
59
+ # attribute on the base, and a method of that name would shadow the
60
+ # generated accessor (the CLAUDE.md landmine).
61
+ def mr_year
62
+ (year || ieee_identifier&.year)&.to_s
63
+ end
64
+
33
65
  # `publisher` is *derived* from `ieee_identifier` (the serialized source
34
66
  # of truth), so it must not be serialized. Emitting it breaks the
35
67
  # canonical round-trip: the derived value is default-omitted on the parse
@@ -15,7 +15,9 @@ module Pubid
15
15
  # @return [String] URN representation
16
16
 
17
17
  attribute :publisher, :string, default: -> { "IEEE" }
18
- attribute :copublisher, :string, collection: true # IEC, ISO, ANSI, etc.
18
+ # IEC, ISO, ANSI, etc. `initialize_empty` makes from_hash give `[]`, the
19
+ # value a parsed identifier holds, so the two paths stay `==`.
20
+ attribute :copublisher, :string, collection: true, initialize_empty: true
19
21
  # NB: there is deliberately NO `attribute :code`. IEEE's document code is
20
22
  # serialized as the split index columns (number/prefix/parts/separator) on
21
23
  # concrete leaf types via the CodeNumber mixin; `code`/`code_obj` remain a
@@ -72,8 +74,19 @@ module Pubid
72
74
  # what the base `Pubid::Identifier#exclude`/matching machinery uses when it
73
75
  # rebuilds via `self.class.new(attrs)`; without it, `exclude`/`matches?`
74
76
  # raised ArgumentError for every IEEE identifier.
77
+ # Constructor parameters that are not lutaml attributes: `code` is
78
+ # runtime-only (kept as `code_obj`), and the draft forms are turned into
79
+ # component objects below.
80
+ def self.extra_init_keys
81
+ %i[code draft draft_obj]
82
+ end
83
+
75
84
  def initialize(args = {}, **kwargs)
76
85
  args = args.merge(kwargs) unless kwargs.empty?
86
+ # This override calls `super()` with no attributes and assigns through
87
+ # setters instead, so it bypasses the base constructor's coercion and
88
+ # unknown-key check. Apply them here to keep the cross-flavor contract.
89
+ args = self.class.normalize_init_attributes(args)
77
90
  super()
78
91
 
79
92
  # Handle typed_stage if provided
@@ -104,13 +117,26 @@ module Pubid
104
117
  self.draft = args[:draft].to_s
105
118
  end
106
119
 
107
- # Set other attributes
120
+ # Set other attributes.
121
+ #
122
+ # A key the class accepts but does not declare as a lutaml attribute
123
+ # (see `extra_init_keys`) is assigned through its plain accessor.
124
+ # Without that, `CsaDualPublished#csa_identifier` — an `attr_accessor`
125
+ # by design, "stored as-is (not a Lutaml model type)" — was never
126
+ # assigned at all, and `renderer.rb` read nil from it:
127
+ # `IEEE Std 844.1-2017/CSA C22.2 No. 293.1-17` rendered back as
128
+ # `IEEE Std 844.1-2017/CSA`, losing the CSA designation entirely.
108
129
  attrs = self.class.attributes
130
+ extra = self.class.extra_init_keys
109
131
  args.each do |key, value|
110
132
  next if %i[code draft draft_obj typed_stage].include?(key)
111
133
 
112
134
  setter = :"#{key}="
113
- public_send(setter, value) if attrs.key?(key)
135
+ if attrs.key?(key)
136
+ public_send(setter, value)
137
+ elsif extra.include?(key) && respond_to?(setter)
138
+ public_send(setter, value)
139
+ end
114
140
  end
115
141
  end
116
142
 
@@ -208,8 +234,13 @@ module Pubid
208
234
  # orchestrator that consumes a PreParser::Result and routes to the
209
235
  # correct builder.
210
236
  def self.parse(input)
237
+ unless input.is_a?(String)
238
+ raise Pubid::Errors::InvalidInputError,
239
+ Pubid::INPUT_NOT_A_STRING_MESSAGE
240
+ end
241
+
211
242
  if input.length > Pubid::MAX_INPUT_LENGTH
212
- raise ArgumentError, Pubid::INPUT_TOO_LONG_MESSAGE
243
+ raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
213
244
  end
214
245
 
215
246
  result = PreParser.preprocess(input)
@@ -275,9 +306,15 @@ module Pubid
275
306
  def self.build_aiee_asa_adoption(parts)
276
307
  aiee_id = parse_single(parts[0])
277
308
  asa_id = parse_single(parts[1])
309
+ # `adopted_identifierS` — AdoptedStandard declares a collection. The
310
+ # singular spelling was not an attribute, so lutaml dropped it without
311
+ # a word and the ASA half of these 13 identifiers was never stored:
312
+ # `AIEE No 18-1934 (ASA C55 1934)` rendered back as `AIEE No 18-1934`.
313
+ # The unknown-key contract turned that silent loss into a raise, which
314
+ # is how it was found.
278
315
  Identifiers::AdoptedStandard.new(
279
316
  ieee_identifier: aiee_id,
280
- adopted_identifier: asa_id,
317
+ adopted_identifiers: [asa_id],
281
318
  )
282
319
  end
283
320
  private_class_method :build_aiee_asa_adoption
@@ -11,6 +11,27 @@ module Pubid
11
11
  # CSA identifier is stored as-is (not a Lutaml model type)
12
12
  attr_accessor :csa_identifier
13
13
 
14
+ # `csa_identifier` is a real constructor parameter even though it is
15
+ # not a lutaml attribute, so the unknown-key contract must let it
16
+ # through — and the constructor must actually assign it.
17
+ def self.extra_init_keys
18
+ super + %i[csa_identifier]
19
+ end
20
+
21
+ # Walk to the IEEE designation for the relaton-index key: this wrapper
22
+ # names one document by an IEEE and a CSA number and holds neither
23
+ # itself, so `root.number` was "".
24
+ def root
25
+ ieee_identifier ? ieee_identifier.root : self
26
+ end
27
+
28
+ # Carry the number onto the URN and the MR slug as well as #root — see
29
+ # AdoptedStandard#code_obj. `#code` below delegates already, but the
30
+ # URN generator and the MR renderer both read `code_obj`.
31
+ def code_obj
32
+ ieee_identifier&.code_obj
33
+ end
34
+
14
35
  # Delegate common attributes to ieee_identifier
15
36
  def publisher
16
37
  ieee_identifier.publisher
@@ -18,6 +18,61 @@ module Pubid
18
18
  # Return array of both publishers
19
19
  [first_identifier&.publisher, second_identifier&.publisher].compact
20
20
  end
21
+
22
+ # `publisher` above is DERIVED from the two member identifiers (the
23
+ # serialized source of truth), so it must not be serialized itself.
24
+ # Emitting it breaks the canonical round-trip exactly as it did on
25
+ # Identifiers::AdoptedStandard and Identifiers::JointDevelopment: the
26
+ # derived value is default-omitted on the parse path (the attribute is
27
+ # unset), but re-emitted once from_hash materializes the attribute
28
+ # default, because the override then returns the members' publishers
29
+ # rather than the "IEEE" default. Worse here, it is an ARRAY, so the
30
+ # re-emitted value was the literal string `["IEEE", "IEEE"]`.
31
+ #
32
+ # This made all 25 DualPublished ids in the fixture corpus fail
33
+ # `from_hash(to_hash) == to_hash` — the gate relaton's index build
34
+ # uses, which SKIPS a document that fails it. Dropping the key is
35
+ # lossless: first_identifier and second_identifier rebuild it.
36
+ #
37
+ # A top-level to_hash delete is sufficient because DualPublished is
38
+ # only ever a top-level wrapper, never a nested `base` (where lutaml's
39
+ # own transform would bypass this override).
40
+ def to_hash(*args)
41
+ hash = super
42
+ hash.delete("publisher") if hash.is_a?(::Hash)
43
+ hash
44
+ end
45
+
46
+ # A dual-published document is one document under two designations, and
47
+ # this wrapper carries neither number itself — so `root.number`, the key
48
+ # relaton-index sorts and bsearches on, was "". Walk to the first
49
+ # designation, which is the one the printed reference leads with.
50
+ def root
51
+ first_identifier ? first_identifier.root : self
52
+ end
53
+
54
+ # Carry the number onto the URN and the MR slug as well as #root — see
55
+ # AdoptedStandard#code_obj for why all three surfaces need it.
56
+ def code_obj
57
+ first_identifier&.code_obj
58
+ end
59
+
60
+ # See AdoptedStandard#mr_year — without it the 1989 and the 1993
61
+ # printings of "IEEE Std 960 and IEEE Std 1177" shared one slug.
62
+ def mr_year
63
+ (year || first_identifier&.year)&.to_s
64
+ end
65
+
66
+ # `#publisher` above returns an ARRAY, and the inherited `mr_publisher`
67
+ # is `publisher&.to_s&.downcase` — so the MR slug contained a literal
68
+ # Ruby array (`["ieee", "asme"]`, brackets, quotes, comma and space
69
+ # included) and `to_slug` is an output FILENAME. Join the two instead,
70
+ # keeping both publishers in the slug: they are equally part of this
71
+ # document's identity, so by the project rule both must reach it.
72
+ def mr_publisher
73
+ names = Array(publisher).map { |p| p.to_s.downcase }.reject(&:empty?)
74
+ names.empty? ? nil : names.join("-")
75
+ end
21
76
  end
22
77
  end
23
78
  end
@@ -10,6 +10,29 @@ module Pubid
10
10
  # Uniform `year` (the interpretation year), not `int_year` — inherited
11
11
  # from the base, so no attribute is declared here.
12
12
 
13
+ # The interpreted standard's code must reach the URN. Before this
14
+ # change the flat-parsed form kept its number in the wrapper's own
15
+ # `code_obj` (the builder never built a base), so the URN carried it by
16
+ # accident; now that the number lives on `base` where it belongs, the
17
+ # URN generator has to be pointed at it or the URN silently loses the
18
+ # document number. `code_obj` is a plain attr_accessor, not a lutaml
19
+ # attribute, so overriding it as a method is safe.
20
+ def code_obj
21
+ super || base&.code_obj
22
+ end
23
+
24
+ # MR supplement suffix: `int.{year}` — mirrors Corrigendum/Amendment.
25
+ # Without it the MrString renderer slugs an interpretation FLAT off
26
+ # attributes a supplement does not have, instead of recursing into
27
+ # `base`, so every interpretation of every standard collapsed onto
28
+ # "ieee.std.{year}" with no document number at all — and `to_slug` is
29
+ # an output FILENAME.
30
+ def mr_supplement_suffix
31
+ segments = ["int"]
32
+ segments << year.to_s if year
33
+ segments.join(".")
34
+ end
35
+
13
36
  # TYPED_STAGES for interpretation
14
37
  # Interpretation uses "INT" abbreviation
15
38
  TYPED_STAGES = [
@@ -93,7 +93,9 @@ module Pubid
93
93
  # @param format [Symbol] :ieee or :iso (defaults to canonical_format)
94
94
  # @param trademark [Boolean] append the IEEE trademark symbol (™/®)
95
95
  # @return [String] formatted identifier
96
- def to_s(format: canonical_format, trademark: false)
96
+ # `**_opts` absorbs render flags this list does not name (`annotated:`),
97
+ # which the closed keyword list used to reject outright.
98
+ def to_s(format: canonical_format, trademark: false, **opts)
97
99
  # The mark goes after the code number, before the draft and the year
98
100
  # ("ISO/IEC/IEEE P26511™/D8-2018"), so it is threaded into the
99
101
  # format builders rather than appended to the finished string.
@@ -105,12 +107,14 @@ module Pubid
105
107
  ""
106
108
  end
107
109
 
108
- case format
109
- when :iso
110
- to_iso_format(mark)
111
- else
112
- to_ieee_format(mark)
113
- end
110
+ result = case format
111
+ when :iso
112
+ to_iso_format(mark)
113
+ else
114
+ to_ieee_format(mark)
115
+ end
116
+
117
+ annotate_plain_render(result, **opts)
114
118
  end
115
119
 
116
120
  private
@@ -9,12 +9,41 @@ module Pubid
9
9
  # - "IEEE Std 1299/C62.22.1-1996" (same document, two numbers)
10
10
  # - "IEEE Std 960-1989, Std 1177-1989" (same document, two numbers)
11
11
  class MultiNumberedIdentifier < Identifier
12
- # Primary identifier (main IEEE standard number)
13
- # Stored as-is (Lutaml model type checking doesn't work with polymorphic identifiers)
14
- attr_accessor :primary_identifier
12
+ # Both designations are real lutaml attributes.
13
+ #
14
+ # They used to be `attr_accessor`s, which lutaml cannot see — so
15
+ # `to_hash` emitted {"_type" => "pubid:ieee:multi-numbered-identifier"}
16
+ # and NOTHING else, `from_hash` could not rebuild either member, and
17
+ # `#exclude` (which iterates self.class.attributes) skipped them. The
18
+ # whole identity was dropped. That is the CSA container defect recorded
19
+ # in CLAUDE.md, and the fix is the same: make the members attributes.
20
+ #
21
+ # The type is the CROSS-FLAVOR ::Pubid::Identifier, not IEEE's own:
22
+ # `polymorphic: true` widens only to subclasses, and the second
23
+ # designation may belong to another body — the same reason
24
+ # AdoptedStandard's members carry that type.
25
+ attribute :primary_identifier, ::Pubid::Identifier, polymorphic: true
26
+ attribute :secondary_identifier, ::Pubid::Identifier, polymorphic: true
15
27
 
16
- # Secondary identifier (additional number for same document)
17
- attr_accessor :secondary_identifier
28
+ # One document under two numbers, neither of them held here, so
29
+ # `root.number` was "". Walk to the primary designation.
30
+ def root
31
+ primary_identifier ? primary_identifier.root : self
32
+ end
33
+
34
+ # Carry the number onto the URN and the MR slug as well as #root — see
35
+ # AdoptedStandard#code_obj.
36
+ def code_obj
37
+ primary_identifier&.code_obj
38
+ end
39
+
40
+ # See AdoptedStandard#mr_year. `year` below already delegates, so this
41
+ # only stringifies — it is kept explicit so the hook survives if that
42
+ # delegation is ever removed in favour of the URN generator's
43
+ # `year_component` root fallback.
44
+ def mr_year
45
+ year&.to_s
46
+ end
18
47
 
19
48
  def publisher
20
49
  primary_identifier&.publisher
@@ -27,6 +56,29 @@ module Pubid
27
56
  def year
28
57
  primary_identifier&.year
29
58
  end
59
+
60
+ # `publisher` and `year` are DERIVED from primary_identifier — and,
61
+ # since that member became a serialized attribute above, the derived
62
+ # values are now re-emitted after from_hash materializes the attribute
63
+ # defaults, while the parse path omits them as unset. That asymmetry
64
+ # breaks `from_hash(to_hash) == to_hash`, the gate relaton's index
65
+ # build uses to decide whether to keep a document.
66
+ #
67
+ # It is latent rather than live for the one corpus id (its primary is
68
+ # IEEE-published with no year, so both derived values match their
69
+ # defaults and are dropped anyway), but the class documents that its
70
+ # members may belong to another body — and a synthetic AIEE primary
71
+ # reproduces it immediately. Dropping the keys is lossless:
72
+ # primary_identifier rebuilds both. Same treatment as
73
+ # AdoptedStandard#to_hash and DualPublished#to_hash.
74
+ def to_hash(*args)
75
+ hash = super
76
+ return hash unless hash.is_a?(::Hash)
77
+
78
+ hash.delete("publisher")
79
+ hash.delete("year")
80
+ hash
81
+ end
30
82
  end
31
83
  end
32
84
  end
@@ -88,10 +88,10 @@ module Pubid
88
88
  #
89
89
  # @param trademark [Boolean] append the IEEE trademark symbol (™/®)
90
90
  # @return [String] String representation
91
- def to_s(trademark: false)
91
+ def to_s(trademark: false, **opts)
92
92
  result = ["IEEE Std", year, name_portion].compact.join(" ")
93
93
  result += trademark_symbol if trademark
94
- result
94
+ annotate_plain_render(result, **opts)
95
95
  end
96
96
 
97
97
  # Trademark symbol for this document's own code. A NESC string starts
@@ -29,12 +29,12 @@ module Pubid
29
29
  #
30
30
  # @param trademark [Boolean] append the IEEE trademark symbol (™/®)
31
31
  # @return [String] Draft format with optional month and year
32
- def to_s(trademark: false)
32
+ def to_s(trademark: false, **opts)
33
33
  parts = ["Draft National Electrical Safety Code"]
34
34
  parts << ", #{month} #{year}" if month && year
35
35
  result = parts.join
36
36
  result += trademark_symbol if trademark
37
- result
37
+ annotate_plain_render(result, **opts)
38
38
  end
39
39
  end
40
40
  end