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
data/lib/pubid.rb CHANGED
@@ -15,6 +15,8 @@ require "pubid/lutaml/no_store_registration"
15
15
  require "pubid/prefixes_support"
16
16
 
17
17
  module Pubid
18
+ autoload :Schema, "pubid/schema"
19
+ autoload :Conformance, "pubid/conformance"
18
20
  # Upper bound on the length of an identifier string accepted by any +parse+
19
21
  # entry point. Real-world standards identifiers are well under 200 characters;
20
22
  # this limit exists purely to keep pathological, attacker-controlled inputs
@@ -30,12 +32,28 @@ module Pubid
30
32
  INPUT_TOO_LONG_MESSAGE =
31
33
  "identifier string exceeds maximum length of #{MAX_INPUT_LENGTH} characters"
32
34
 
35
+ # Raised (as an ArgumentError) when +parse+ gets something that is not a
36
+ # String. Without this check the input reaches +.length+ and the caller gets a
37
+ # NoMethodError ("undefined method 'length' for nil"), a Ruby internal error
38
+ # that says nothing about the identifier contract. Every public +parse+ entry
39
+ # point rejects a non-String before it measures the length.
40
+ INPUT_NOT_A_STRING_MESSAGE = "identifier must be a String"
41
+
33
42
  # Registry for tracking all loaded flavors
34
43
  class Registry
35
44
  @flavors = {}
36
45
 
37
46
  class << self
38
- attr_reader :flavors
47
+ # The registered flavors, always key-sorted and frozen. Sorted so
48
+ # no behavior can observe registration (= module load) order -
49
+ # hosts autoload flavor modules in any order, and order-dependent
50
+ # iteration once routed the same identifier to different flavors
51
+ # per process. Frozen so the raw table is not public mutable
52
+ # state; #register is the only mutator.
53
+ # @return [Hash{String => Module}]
54
+ def flavors
55
+ @flavors.sort.to_h.freeze
56
+ end
39
57
 
40
58
  # Register a flavor with the registry
41
59
  # @param name [String, Symbol] Flavor name (e.g., :iso, :iec)
@@ -44,6 +62,15 @@ module Pubid
44
62
  @flavors[name.to_s.downcase] = flavor_module
45
63
  end
46
64
 
65
+ # Remove a flavor registration. Tests that register probe flavors
66
+ # MUST unregister them: registry-driven specs enumerate every
67
+ # registered flavor, so a leaked probe fails them for everyone
68
+ # else in the process.
69
+ # @param name [String, Symbol] Flavor name
70
+ def unregister(name)
71
+ @flavors.delete(name.to_s.downcase)
72
+ end
73
+
47
74
  # Get all registered flavor names
48
75
  # @return [Array<String>] Array of flavor names
49
76
  def flavor_names
@@ -57,30 +84,62 @@ module Pubid
57
84
  @flavors[name.to_s.downcase]
58
85
  end
59
86
 
87
+ # The canonical registered name of a module that may be registered
88
+ # under aliases (CenCenelec registers both cen_cenelec and cen).
89
+ # Canonical = the name the module's own prefix table is keyed by,
90
+ # so the answer cannot depend on registration or iteration order.
91
+ # @param flavor_module [Module]
92
+ # @return [String, nil]
93
+ def canonical_name(flavor_module)
94
+ if flavor_module.singleton_class.method_defined?(:prefix_flavor_key)
95
+ key = flavor_module.prefix_flavor_key.to_s
96
+ return key if @flavors[key] == flavor_module
97
+ end
98
+ @flavors.key(flavor_module)
99
+ end
100
+
60
101
  # Check if a flavor is registered
61
102
  # @param name [String, Symbol] Flavor name
62
103
  # @return [Boolean]
63
104
  def registered?(name)
64
105
  @flavors.key?(name.to_s.downcase)
65
106
  end
107
+
108
+ # Parse an identifier without naming its flavor.
109
+ #
110
+ # The pubid 1.x entry point, kept because consumers still call it —
111
+ # isodoc's +std_docid_semantic_parse+ among them. It delegates to
112
+ # {Pubid.parse}; it is not a second implementation.
113
+ #
114
+ # @param string [String] the identifier string
115
+ # @return [Pubid::Identifier]
116
+ # @raise [Parslet::ParseFailed] when no flavor can parse it
117
+ def parse(string, **opts)
118
+ Pubid.parse(string, **opts)
119
+ end
120
+
121
+ # Build an identifier from its hash without naming its flavor.
122
+ # It delegates to {Pubid.from_hash}, as {parse} delegates to
123
+ # {Pubid.parse}.
124
+ #
125
+ # @param data [Hash] an identifier hash
126
+ # @return [Pubid::Identifier]
127
+ # @raise [Pubid::Errors::InvalidInputError] see {Pubid.from_hash}
128
+ def from_hash(data)
129
+ Pubid.from_hash(data)
130
+ end
66
131
  end
67
132
  end
68
133
 
69
- # Canonical joint / co-publication leading tokens, injected symmetrically into
70
- # every participating flavor's +prefixes+ (see {PrefixesSupport}). Single
71
- # source of truth: editing an entry here updates both sides at once
72
- # (e.g. +"ISO/IEC"+ appears in +Pubid::Iso.prefixes+ and +Pubid::Iec.prefixes+),
73
- # so co-publication symmetry can never drift. Keyed by
134
+ # Canonical joint / co-publication leading tokens, sourced at boot from
135
+ # schema/core/joint_prefixes.yaml - the single source of truth. Injected
136
+ # symmetrically into every participating flavor's +prefixes+ (see
137
+ # {PrefixesSupport}) so co-publication symmetry can never drift. Keyed by
74
138
  # {PrefixesSupport#prefix_flavor_key}.
75
- JOINT_PREFIXES = {
76
- iso: ["ISO/IEC", "IEC/ISO", "ISO/IEC/IEEE"],
77
- iec: ["ISO/IEC", "IEC/ISO", "ISO/IEC/IEEE"],
78
- ieee: ["ISO/IEC/IEEE"],
79
- ansi: ["ANSI/ASHRAE", "ANSI/AMCA"],
80
- ashrae: ["ANSI/ASHRAE"],
81
- amca: ["ANSI/AMCA"],
82
- }.freeze
139
+ JOINT_PREFIXES = Schema::Loader.joint_prefixes_map
140
+ .transform_keys(&:to_sym).freeze
83
141
 
142
+ autoload :Errors, "pubid/errors"
84
143
  autoload :Parser, "pubid/parser"
85
144
  autoload :Components, "pubid/components"
86
145
  autoload :BundledIdentifier, "pubid/bundled_identifier"
@@ -119,6 +178,7 @@ module Pubid
119
178
  autoload :Csa, "pubid/csa"
120
179
  autoload :Ecma, "pubid/ecma"
121
180
  autoload :Etsi, "pubid/etsi"
181
+ autoload :Evs, "pubid/evs"
122
182
  autoload :Gost, "pubid/gost"
123
183
  autoload :Gb, "pubid/gb"
124
184
  autoload :Idf, "pubid/idf"
@@ -161,17 +221,47 @@ module Pubid
161
221
 
162
222
  # Unified parse entry point with auto-detection
163
223
  #
224
+ # A human-readable string is routed to its owning flavor by leading prefix
225
+ # token, using the {prefix_flavors} table. This restores the pubid 1.x
226
+ # +Pubid::Registry.parse+ capability, which isodoc still calls; before it,
227
+ # this method raised +No flavor specified+ for every human-readable string,
228
+ # so a reference whose flavor the caller did not already know could not be
229
+ # parsed at all.
230
+ #
164
231
  # @param string [String] The identifier string to parse
165
232
  # @param format [Symbol] :auto, :human, :mr_string, or :urn
166
233
  # @return [Identifier] The parsed identifier
234
+ # @raise [ArgumentError] for a non-String, or one over {MAX_INPUT_LENGTH}
235
+ # @raise [Parslet::ParseFailed] when no flavor can parse the string — the
236
+ # class every flavor's own +parse+ raises (see the parse-failure contract)
167
237
  def self.parse(string, format: :auto)
168
- raise ArgumentError, INPUT_TOO_LONG_MESSAGE if string.length > MAX_INPUT_LENGTH
238
+ unless string.is_a?(String)
239
+ raise Pubid::Errors::InvalidInputError,
240
+ Pubid::INPUT_NOT_A_STRING_MESSAGE
241
+ end
242
+ if string.length > MAX_INPUT_LENGTH
243
+ raise Pubid::Errors::InvalidInputError,
244
+ Pubid::INPUT_TOO_LONG_MESSAGE
245
+ end
169
246
 
170
247
  format = FormatDetector.detect(string) if format == :auto
171
248
 
172
249
  case format
173
250
  when :mr_string
174
- Parsers::MrString.parse(string)
251
+ # The MR detector is a shape heuristic (`/\A[A-Z]{2,}[.-]/`), and some
252
+ # human-readable identifiers have that shape — "ITU-T G.711" is the
253
+ # standing example. Fall through to prefix routing when the MR parse
254
+ # fails, rather than reporting an MR error for a string that was never
255
+ # an MR string. A genuine MR failure still surfaces, from the second
256
+ # attempt, as Parslet::ParseFailed.
257
+ #
258
+ # Narrow, for the reason spelled out on {parse_by_prefix}: only a parse
259
+ # failure means "wrong shape". Anything else is a defect and propagates.
260
+ begin
261
+ Parsers::MrString.parse(string)
262
+ rescue Parslet::ParseFailed
263
+ parse_by_prefix(string)
264
+ end
175
265
  when :urn
176
266
  eager_load_flavors!
177
267
  flavor = detect_flavor_from_urn(string)
@@ -182,18 +272,239 @@ module Pubid
182
272
  end
183
273
 
184
274
  urn_parser = flavor_module.const_get(:UrnParser)
185
- urn_parser.parse(string) else
186
- # Default to MR string parser for MR format, human-readable otherwise
187
- # The MR string parser converts to human-readable and delegates to flavor.parse
188
- raise ArgumentError,
189
- "No flavor specified. Use Pubid::Iso.parse() or another flavor-specific parse method."
275
+ urn_parser.parse(string)
276
+ else
277
+ parse_by_prefix(string)
278
+ end
279
+ end
280
+
281
+ # Build an identifier from its serialized hash, without naming its flavor —
282
+ # the hash counterpart of {parse}.
283
+ #
284
+ # The hash is what {Identifier#to_hash} emits, and what a relaton-data
285
+ # +index-v2.yaml+ row stores under +:id+. Its +_type+ key
286
+ # ("pubid:<flavor>:<type>") names the concrete class, so no prefix guessing
287
+ # is necessary. The class is resolved by {TypeResolver}, and the class's own
288
+ # +from_hash+ does the rest, nested identifiers of other flavors included.
289
+ #
290
+ # @example
291
+ # Pubid.from_hash("_type" => "pubid:iso:international-standard",
292
+ # "number" => "9001", "year" => "2015").to_s
293
+ # # => "ISO 9001:2015"
294
+ #
295
+ # @param data [Hash] an identifier hash. String and Symbol keys are both
296
+ # accepted.
297
+ # @return [Identifier] an instance of the class that +_type+ names
298
+ # @raise [Pubid::Errors::InvalidInputError] when +data+ is not a Hash, has no
299
+ # +_type+, or has a +_type+ that no registered flavor defines. These are
300
+ # the only failures it translates: once +_type+ names a class, an error
301
+ # from that class's own +from_hash+ (a malformed field, or a bad +_type+
302
+ # in a NESTED hash) propagates unchanged, so a flavor defect is not
303
+ # disguised as bad input.
304
+ def self.from_hash(data)
305
+ unless data.is_a?(Hash)
306
+ raise Pubid::Errors::InvalidInputError,
307
+ "identifier data must be a Hash, got #{data.class}"
308
+ end
309
+
310
+ identifier_class_for(data).from_hash(data)
311
+ end
312
+
313
+ # The concrete class that the +_type+ of +data+ names.
314
+ #
315
+ # @param data [Hash] an identifier hash
316
+ # @return [Class<Identifier>]
317
+ # @raise [Pubid::Errors::InvalidInputError] no +_type+, or an unknown one
318
+ # @api private
319
+ def self.identifier_class_for(data)
320
+ type = data["_type"] || data[:_type]
321
+ unless type
322
+ raise Pubid::Errors::InvalidInputError,
323
+ "identifier hash has no _type key: #{data.inspect[0, 200]}"
324
+ end
325
+
326
+ TypeResolver.resolve(type) ||
327
+ raise(Pubid::Errors::InvalidInputError,
328
+ "unknown identifier _type: #{type.inspect}")
329
+ end
330
+ private_class_method :identifier_class_for
331
+
332
+ # Route a human-readable identifier to its flavor by leading prefix token.
333
+ #
334
+ # Three passes, widening each time — the pubid 1.x +Registry.parse+ shape:
335
+ #
336
+ # 1. flavors owning the longest matching prefix ("ISO/IEC" before "ISO")
337
+ # 2. every other flavor, in registry order
338
+ #
339
+ # A longest-match-first order matters because prefixes nest: "ISO/IEC 2131"
340
+ # must not be handed to the flavor that merely owns "ISO".
341
+ #
342
+ # @param string [String] a human-readable identifier
343
+ # @return [Identifier]
344
+ # @raise [Parslet::ParseFailed] when no flavor accepts the string
345
+ # @api private
346
+ def self.parse_by_prefix(string)
347
+ eager_load_flavors!
348
+
349
+ first_error = nil
350
+ fallback = nil
351
+
352
+ prefix_owners = candidates_by_prefix(string)
353
+ (prefix_owners + other_candidates(prefix_owners)).each do |mod|
354
+ begin
355
+ parsed = mod.parse(string)
356
+ rescue Parslet::ParseFailed => e
357
+ # ONLY a parse failure means "not this flavor's identifier". Every
358
+ # other exception is a defect in that flavor and must propagate.
359
+ #
360
+ # A blanket `rescue StandardError` here would swallow exactly the
361
+ # `ArgumentError: unknown attribute…` that the constructor contract on
362
+ # `Identifier` now raises — the error that turned three silent
363
+ # data-loss bugs (CIE `s_prefix`, IEEE `adopted_identifier`,
364
+ # IEEE `csa_identifier`) into visible ones. Catching it one layer up
365
+ # would hand it straight back its disguise: a real builder bug in any
366
+ # flavor would read as "this string is not an identifier".
367
+ first_error ||= e
368
+ next
369
+ end
370
+
371
+ # An exact round trip is the strongest evidence the string belongs to
372
+ # this flavor. It matters because some grammars accept anything by
373
+ # design — adobe and iana take any slug — so without it the first such
374
+ # flavor in registry order claims every unclaimed string and invents
375
+ # structure that was not in the input. Same test isodoc applies before
376
+ # it will annotate a parsed id.
377
+ return parsed if parsed.to_s == string
378
+
379
+ # A non-exact parse is still usable — several flavors legitimately
380
+ # normalise on render, so the round trip is a preference, not a
381
+ # requirement (OGC prints `06-121r9` for `OGC 06-121r9`, and 32 ASHRAE /
382
+ # ASME / CSA / IEEE identifiers route only this way). What it must NOT
383
+ # come from is a flavor that accepts anything: that is how `iec.60050`
384
+ # came back as `IANA iec.60050`. Reporting that no flavor could be
385
+ # determined beats inventing one.
386
+ fallback ||= parsed unless accepts_anything?(mod)
387
+ end
388
+
389
+ return fallback if fallback
390
+ raise first_error if first_error
391
+
392
+ raise Parslet::ParseFailed,
393
+ "no registered flavor could parse #{string.inspect}"
394
+ end
395
+ private_class_method :parse_by_prefix
396
+
397
+ # Flavor modules that own a prefix +string+ starts with, longest prefix
398
+ # first. Deduplicated by module identity, so an alias registration is not
399
+ # tried twice.
400
+ #
401
+ # These are the flavors with an actual claim on the string; a parse from one
402
+ # of them is trusted even when the flavor normalises on render.
403
+ #
404
+ # @param string [String] a human-readable identifier
405
+ # @return [Array<Module>]
406
+ # @api private
407
+ def self.candidates_by_prefix(string)
408
+ ordered = []
409
+ routing_table.each do |prefix, modules|
410
+ ordered.concat(modules) if prefix_match?(string, prefix)
411
+ end
412
+ ordered.compact.uniq
413
+ end
414
+ private_class_method :candidates_by_prefix
415
+
416
+ # Every other registered flavor, for the exhaustive second pass.
417
+ #
418
+ # @param exclude [Array<Module>] modules already tried
419
+ # @return [Array<Module>]
420
+ # @api private
421
+ def self.other_candidates(exclude)
422
+ ordered = []
423
+ each_prefix_flavor_module { |mod| ordered << mod }
424
+ ordered.compact.uniq - exclude
425
+ end
426
+ private_class_method :other_candidates
427
+
428
+ # A meaningless string no real identifier scheme should claim. Used to find
429
+ # the flavors whose grammar accepts anything.
430
+ ROUTING_PROBE = "zzqx-nonsense-9714"
431
+ private_constant :ROUTING_PROBE
432
+
433
+ # True when +mod+'s grammar accepts an arbitrary slug.
434
+ #
435
+ # `adobe` and `iana` do, by design — their identifiers genuinely are free
436
+ # text. That makes them unsafe as a fallback: whichever came first in
437
+ # registry order would claim every string no other flavor matched, and
438
+ # answer with structure the input never had.
439
+ #
440
+ # Detected by probing rather than by a hardcoded list, so a flavor that
441
+ # acquires (or loses) an anything-goes grammar is classified correctly
442
+ # without anyone remembering to edit a constant. Computed once per module.
443
+ #
444
+ # @param mod [Module] a flavor module
445
+ # @return [Boolean]
446
+ # @api private
447
+ def self.accepts_anything?(mod)
448
+ @accepts_anything ||= {}
449
+ return @accepts_anything[mod] if @accepts_anything.key?(mod)
450
+
451
+ @accepts_anything[mod] =
452
+ begin
453
+ mod.parse(ROUTING_PROBE)
454
+ true
455
+ rescue StandardError, Parslet::ParseFailed
456
+ false
457
+ end
458
+ end
459
+ private_class_method :accepts_anything?
460
+
461
+ # Prefix => flavor modules, longest prefix first, built once.
462
+ #
463
+ # {prefix_flavors} rebuilds its index on every call, iterating every
464
+ # registered flavor; routing consulted it twice per parse. The registry is
465
+ # static once +eager_load_flavors!+ has run, so the table is memoised here
466
+ # rather than making the public method's cost implicit.
467
+ #
468
+ # The memo is never invalidated, which is safe for every flavor shipped in
469
+ # this gem — they are all +Pubid+-namespace constants that
470
+ # +eager_load_flavors!+ loads before the first parse. A flavor registered
471
+ # from OUTSIDE that namespace after the first {parse} call would miss its
472
+ # prefix priority (it is still tried in the exhaustive second pass, so it is
473
+ # reachable, just not preferred). Bust the memo in +Registry.register+ if
474
+ # out-of-namespace flavors ever become a supported extension point.
475
+ #
476
+ # @return [Hash{String => Array<Module>}]
477
+ # @api private
478
+ def self.routing_table
479
+ @routing_table ||= begin
480
+ index = prefix_flavors
481
+ index.keys
482
+ .sort_by { |prefix| -prefix.length }
483
+ .to_h { |prefix| [prefix, index[prefix].map { |key| Registry.get(key) }] }
190
484
  end
191
485
  end
486
+ private_class_method :routing_table
487
+
488
+ # True when +string+ starts with +prefix+ at a token boundary, so "ISO" does
489
+ # not claim "ISOFIX" and "BS" does not claim "BSI".
490
+ # @api private
491
+ def self.prefix_match?(string, prefix)
492
+ return false unless string.start_with?(prefix)
493
+
494
+ rest = string[prefix.length]
495
+ rest.nil? || !/[A-Za-z0-9]/.match?(rest)
496
+ end
497
+ private_class_method :prefix_match?
192
498
 
193
499
  def self.detect_flavor_from_urn(urn)
194
500
  # urn:iso:std:... → "iso"
195
501
  # urn:iec:std:... → "iec"
502
+ # urn:mrn:iala:pub:... → "iala": MRN URNs carry an assigning authority
503
+ # where ISO-style URNs carry their namespace, so routing follows the
504
+ # authority (works for any future MRN assignee, not only IALA).
196
505
  parts = urn.downcase.split(":")
506
+ return parts[2] if parts[1] == "mrn"
507
+
197
508
  parts[1] # The namespace part after "urn"
198
509
  end
199
510
 
@@ -267,4 +578,71 @@ module Pubid
267
578
  yield mod
268
579
  end
269
580
  end
581
+
582
+ # Typed-stage lookup by abbreviation, across flavors.
583
+ #
584
+ # Each flavor module memoises +all_typed_stages+ over its own +Identifiers+
585
+ # namespace, so an abbreviation only one flavor uses is invisible from any
586
+ # other: +Pubid::Iso.locate_stage("ADTS")+ is nil while
587
+ # +Pubid::Iec.locate_stage("ADTS")+ finds it. A consumer that holds an
588
+ # abbreviation but not the owning flavor had nowhere to ask.
589
+ #
590
+ # @param abbr [String, Symbol] the stage abbreviation (case-insensitive)
591
+ # @param flavor [Symbol, String, nil] search this flavor only; nil searches
592
+ # every registered flavor and returns the first match
593
+ # @return [Object, nil] the flavor's typed-stage object, or nil. The class is
594
+ # flavor-specific: most return {Pubid::Components::TypedStage}, but IEEE
595
+ # has its own +Ieee::Components::TypedStage+, which is not a subclass.
596
+ # @raise [ArgumentError] if +flavor+ names a flavor that is not registered
597
+ def self.locate_stage(abbr, flavor: nil)
598
+ return locate_stage_in(Registry.get(flavor), abbr, flavor) if flavor
599
+
600
+ each_stage_flavor_module do |mod|
601
+ stage = mod.locate_stage(abbr)
602
+ return stage if stage
603
+ end
604
+ nil
605
+ end
606
+
607
+ # Every flavor that knows +abbr+ — the diagnostic companion to
608
+ # {locate_stage}, for telling a consumer which module owns a stage.
609
+ #
610
+ # @param abbr [String, Symbol] the stage abbreviation (case-insensitive)
611
+ # @return [Array<Symbol>] registered flavor keys, sorted
612
+ def self.locate_stage_flavors(abbr)
613
+ found = []
614
+ each_stage_flavor_module do |mod, flavor_name|
615
+ found << flavor_name.to_sym if mod.locate_stage(abbr)
616
+ end
617
+ found.sort
618
+ end
619
+
620
+ # Yields each unique flavor module that implements +locate_stage+, exactly
621
+ # once. Three registered flavors (adobe, easc, gost) define no typed stages
622
+ # at all, so a cross-flavor sweep must skip them rather than raise on the
623
+ # first one it reaches; alias registrations are collapsed by module identity,
624
+ # as in {each_prefix_flavor_module}.
625
+ #
626
+ # @yieldparam mod [Module] a flavor module
627
+ # @yieldparam flavor_name [String] the registry key it was reached under
628
+ def self.each_stage_flavor_module
629
+ eager_load_flavors!
630
+ seen = {}
631
+ Registry.flavor_names.each do |flavor_name|
632
+ mod = Registry.get(flavor_name)
633
+ next if seen.key?(mod) || !mod.respond_to?(:locate_stage)
634
+
635
+ seen[mod] = true
636
+ yield mod, flavor_name
637
+ end
638
+ end
639
+
640
+ # @api private
641
+ def self.locate_stage_in(mod, abbr, flavor)
642
+ raise ArgumentError, "unknown flavor: #{flavor.inspect}" unless mod
643
+ return nil unless mod.respond_to?(:locate_stage)
644
+
645
+ mod.locate_stage(abbr)
646
+ end
647
+ private_class_method :locate_stage_in
270
648
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "fileutils"
5
+
6
+ namespace :conformance do
7
+ desc "Generate the neutral corpus from ground-truth fixtures"
8
+ task :generate, [:flavor] do |_t, args|
9
+ require "pubid"
10
+ require "pubid/conformance"
11
+ flavor = args[:flavor] || "iso"
12
+ tests_repo = ENV.fetch("PUBID_TESTSUITE_PATH",
13
+ File.expand_path("../../../pubid-testsuite",
14
+ __dir__))
15
+ output_dir = File.join(tests_repo, "tests", flavor)
16
+ FileUtils.mkdir_p(output_dir)
17
+ results = Pubid::Conformance::Generator.new(flavor)
18
+ .generate(output_dir: output_dir)
19
+ stats = results
20
+ puts "#{flavor}: #{stats[:files]} files, #{stats[:cases]} cases, " \
21
+ "#{stats[:aliases]} aliases, #{stats[:duplicates]} duplicates, " \
22
+ "#{stats[:reclassify]} reclassify, #{stats[:negative]} negative, " \
23
+ "#{stats[:debt]} debt, #{stats[:roundtrip_failures]} rt-failures"
24
+ end
25
+
26
+ desc "Run the neutral corpus against this implementation"
27
+ task :run do
28
+ require "pubid"
29
+ require "pubid/conformance"
30
+ failures = Pubid::Conformance::Runner.new.run.flatten
31
+ puts "TOTAL failures: #{failures.size}"
32
+ exit(failures.empty? ? 0 : 1)
33
+ end
34
+ end
data/lib/tasks/docs.rake CHANGED
@@ -1,24 +1,26 @@
1
- # frozen_string_literal
2
-
3
- require_relative "../../lib/pubid/core/pattern_doc_generator"
1
+ # frozen_string_literal: true
4
2
 
5
3
  namespace :docs do
6
4
  desc "Generate identifier pattern reference docs"
7
5
  task :patterns do
6
+ require "pubid"
7
+
8
8
  output_dir = File.join(__dir__, "..", "..", "docs", "identifier-patterns")
9
9
  FileUtils.mkdir_p(output_dir)
10
10
 
11
- flavors = Dir[File.join(__dir__, "..", "..", "lib", "pubid",
12
- "*")].select do |d|
13
- File.directory?(d)
14
- end
15
- .map do |d|
16
- File.basename(d)
17
- end
18
- .reject do |f|
19
- %w[core components rendering
20
- parser serializable utils].include?(f)
21
- end
11
+ lib_root = File.join(__dir__, "..", "..", "lib", "pubid")
12
+ # The registry is the authoritative flavor list. This replaces a
13
+ # filesystem glob over lib/pubid/* directories, whose reject list kept
14
+ # going stale and swept internal modules (parsers, renderers, lutaml,
15
+ # builder, utils, ...) in as "flavors". The module-derived directory
16
+ # (CenCenelec -> cen_cenelec, W3c -> w3c) must exist on disk.
17
+ Pubid.eager_load_flavors!
18
+ flavors = Pubid::Registry.flavor_names.filter_map do |name|
19
+ mod = Pubid::Registry.get(name)
20
+ dir = mod.name.split("::").last
21
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
22
+ dir if File.directory?(File.join(lib_root, dir))
23
+ end.uniq.sort
22
24
 
23
25
  flavors.each do |flavor|
24
26
  puts "Generating docs for #{flavor}..."
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+ require "fileutils"
6
+
7
+ # Extracts scheme declarations from the live model into schema/{flavor}.yaml.
8
+ # Seed tooling for TODO.restructure/01-02; TODO.restructure/07 inverts the
9
+ # direction (the identifier classes read these files instead of carrying the
10
+ # data in constants and class methods).
11
+ class SchemeExtractor
12
+ def initialize(flavor)
13
+ require "pubid"
14
+ require "pubid/export"
15
+
16
+ @flavor = flavor.to_s
17
+ Pubid.eager_load_flavors!
18
+ @flavor_module = Pubid::Registry.get(@flavor)
19
+ raise ArgumentError, "Unknown flavor: #{@flavor}" unless @flavor_module
20
+
21
+ exporter = Pubid::Export::FlavorExporter.new(@flavor.to_sym)
22
+ @export = JSON.parse(JSON.generate(exporter.export.to_h))
23
+ end
24
+
25
+ def payload
26
+ types = @export["identifier_types"].map { |type| type_entry(type) }
27
+ {
28
+ "flavor" => @flavor,
29
+ "schema_version" => "1.0.0",
30
+ "prefixes" => own_prefixes,
31
+ "identifier_types" => types,
32
+ }
33
+ end
34
+
35
+ def write(output_dir)
36
+ FileUtils.mkdir_p(output_dir)
37
+ path = File.join(output_dir, "#{@flavor}.yaml")
38
+ File.write(path, header + YAML.dump(payload))
39
+ puts "Extracted #{payload['identifier_types'].size} types to #{path}"
40
+ end
41
+
42
+ private
43
+
44
+ def own_prefixes
45
+ if @flavor_module.const_defined?(:PREFIXES, false)
46
+ @flavor_module.const_get(:PREFIXES).to_a
47
+ else
48
+ @flavor_module.prefixes
49
+ end
50
+ end
51
+
52
+ def type_entry(type)
53
+ stages = (type["typed_stages"] || []).map { |stage| stage_entry(stage) }
54
+ {
55
+ "key" => type["key"],
56
+ "title" => type["title"],
57
+ "short" => type["short"],
58
+ "abbr" => type["abbr"],
59
+ "typed_stages" => stages,
60
+ }.compact
61
+ end
62
+
63
+ def stage_entry(stage)
64
+ {
65
+ "stage_code" => stage["stage_code"],
66
+ "type_code" => stage["type_code"],
67
+ "abbr" => stage["abbr"],
68
+ "name" => stage["name"],
69
+ "harmonized_stages" => stage["harmonized_stages"],
70
+ }.compact
71
+ end
72
+
73
+ def header
74
+ [
75
+ "# Generated by rake schema:extract[#{@flavor}] - SSOT seed.",
76
+ "# Joint prefixes live in schema/core/joint_prefixes.yaml.\n",
77
+ ].join("\n")
78
+ end
79
+ end
80
+
81
+ namespace :schema do
82
+ desc "Extract scheme declarations into schema/{flavor}.yaml"
83
+ task :extract, [:flavor] do |_t, args|
84
+ flavor = args[:flavor] || "iso"
85
+ output_dir = File.expand_path("../../schema", __dir__)
86
+ SchemeExtractor.new(flavor).write(output_dir)
87
+ end
88
+ end
@@ -0,0 +1,24 @@
1
+ # Canonical joint / co-publication leading tokens, mirroring
2
+ # Pubid::JOINT_PREFIXES (lib/pubid.rb). Keyed by flavor; merged into each
3
+ # participating flavor's prefixes at load time so co-publication symmetry can
4
+ # never drift (an ISO/IEC token appears in both the iso and iec lists).
5
+ # TODO.restructure/05 makes this file the only home for joint tokens.
6
+ schema_version: 1.0.0
7
+ joint_prefixes:
8
+ iso:
9
+ - ISO/IEC
10
+ - IEC/ISO
11
+ - ISO/IEC/IEEE
12
+ iec:
13
+ - ISO/IEC
14
+ - IEC/ISO
15
+ - ISO/IEC/IEEE
16
+ ieee:
17
+ - ISO/IEC/IEEE
18
+ ansi:
19
+ - ANSI/ASHRAE
20
+ - ANSI/AMCA
21
+ ashrae:
22
+ - ANSI/ASHRAE
23
+ amca:
24
+ - ANSI/AMCA