pubid 2.0.8 → 2.0.9
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.
- checksums.yaml +4 -4
- data/README.adoc +2 -1
- data/data/parg/sae.json +1 -1
- data/lib/pubid/api/pubid_urn_schema.rb +2 -0
- data/lib/pubid/cen_cenelec/CLAUDE.md +17 -0
- data/lib/pubid/cen_cenelec/identifiers/cenelec_harmonization_document.rb +4 -1
- data/lib/pubid/cen_cenelec/identifiers.rb +1 -1
- data/lib/pubid/cen_cenelec/pubid_urn_schema.rb +35 -0
- data/lib/pubid/cen_cenelec.rb +1 -0
- data/lib/pubid/etsi/CLAUDE.md +9 -0
- data/lib/pubid/etsi/pubid_urn_schema.rb +36 -0
- data/lib/pubid/etsi.rb +1 -0
- data/lib/pubid/gost/CLAUDE.md +12 -0
- data/lib/pubid/gost/pubid_urn_schema.rb +34 -0
- data/lib/pubid/gost.rb +1 -0
- data/lib/pubid/identifier.rb +7 -1
- data/lib/pubid/iec/pubid_urn_schema.rb +11 -4
- data/lib/pubid/plateau/CLAUDE.md +13 -0
- data/lib/pubid/plateau/pubid_urn_schema.rb +25 -0
- data/lib/pubid/plateau.rb +1 -0
- data/lib/pubid/pubid_urn/decoder.rb +8 -2
- data/lib/pubid/pubid_urn/encoder.rb +14 -2
- data/lib/pubid/pubid_urn/schema.rb +18 -4
- data/lib/pubid/pubid_urn.rb +40 -0
- data/lib/pubid/sae/CLAUDE.md +78 -0
- data/lib/pubid/sae/builder.rb +89 -11
- data/lib/pubid/sae/components/date.rb +12 -3
- data/lib/pubid/sae/components/type.rb +54 -2
- data/lib/pubid/sae/identifiers/base.rb +54 -1
- data/lib/pubid/sae/renderer.rb +17 -20
- data/lib/pubid/sae/urn_generator.rb +6 -1
- data/lib/pubid/sae/urn_parser.rb +8 -3
- data/lib/pubid/version.rb +1 -1
- data/lib/pubid.rb +20 -0
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 014b932f4ce8d6a4da07b23e611467dc460afc19aa73f9be3af191cab1007a02
|
|
4
|
+
data.tar.gz: 284a4f85f2adcdabe735a20c5f427bf15a242c8b660c66d2ebde54f1938a2665
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bdecc7d28c6b0bc0c2550d9a643e48324f5f40c8feb3faf3e931c6b2928f573f1c07e01db60d30c016fab9e60bca035e4cf9f4b064c9a47a973b1aa5e500cea3
|
|
7
|
+
data.tar.gz: 112284d94ca1994025773f8de8ba04840e3560fc339a955c07241e1c0def5bae291f7285aef723ee370669cc8682d7ef88a1d1afd21a37d1c2cb2e890bc965eb
|
data/README.adoc
CHANGED
|
@@ -93,7 +93,8 @@ id.to_urn # => "urn:iso:std:iso:9001:amd:2020:v1"
|
|
|
93
93
|
`#to_urn` follows the registered grammar of the organization, which can
|
|
94
94
|
drop values (RFC 5141 has no year). `#to_pubid_urn` is pubid's own URN: it
|
|
95
95
|
carries every value, always round-trips through `Pubid.parse_urn`, and is
|
|
96
|
-
safe to use as an HTTP path. ISO, IEEE, ANSI, ITU, NIST
|
|
96
|
+
safe to use as an HTTP path. ISO, IEEE, ANSI, ITU, NIST, ASTM, API, ASHRAE,
|
|
97
|
+
ASME, JCGM, OIML, ETSI, GOST and PLATEAU support it now; the other flavors follow
|
|
97
98
|
(link:docs/URN-SCHEMES.adoc[URN schemes]).
|
|
98
99
|
|
|
99
100
|
[source,ruby]
|
data/data/parg/sae.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"bindings":null,"version":"0.1.0","grammar":"PubidSae","shape":"parsanol-tree/v2","binding_version":1,"entries":{"identifier":{"root":"identifier","grammar":{"atoms":[{"Str":{"pattern":"SAE"}},{"Named":{"name":"publisher","atom":0}},{"Entity":{"atom":3}},{"Str":{"pattern":" "}},{"Entity":{"atom":12}},{"Str":{"pattern":"AMS"}},{"Str":{"pattern":"AIR"}},{"Str":{"pattern":"ARP"}},{"Str":{"pattern":"AS"}},{"Str":{"pattern":"J"}},{"Str":{"pattern":"MA"}},{"Alternative":{"atoms":[5,6,7,8,9,10]}},{"Named":{"name":"type","atom":11}},{"Entity":{"atom":3}},{"Repetition":{"atom":13,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":29}},{"Entity":{"atom":17}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Repetition":{"atom":16,"min":1,"max":1,"tag":"Repetition"}},{"Entity":{"atom":20}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":19,"min":1,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[18,21]}},{"Entity":{"atom":20}},{"Repetition":{"atom":23,"min":1,"max":null,"tag":"Repetition"}},{"Entity":{"atom":17}},{"Repetition":{"atom":25,"min":0,"max":1,"tag":"Repetition"}},{"Sequence":{"atoms":[24,26]}},{"Alternative":{"atoms":[22,27]}},{"Named":{"name":"number","atom":28}},{"Entity":{"atom":31}},{"Str":{"pattern":":"}},{"Named":{"name":"colon","atom":30}},{"Entity":{"atom":36}},{"Entity":{"atom":20}},{"Repetition":{"atom":34,"min":4,"max":4,"tag":"Repetition"}},{"Named":{"name":"year","atom":35}},{"Sequence":{"atoms":[32,33]}},{"Repetition":{"atom":37,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[1,2,4,14,15,38]}}],"root":39},"bindings":[{"capture":"type","path":"type","type":"string","card":null,"preprocess":null},{"capture":"number","path":"number","type":"string","card":null,"preprocess":null},{"capture":"year","path":"year","type":"string","card":null,"preprocess":null},{"capture":"publisher","path":"publisher","type":"string","card":null,"preprocess":null}]}},"preprocess":{},"tables":{},"lint":{"order_warnings":[]},"default_entry":"identifier","render":{},"derive":{},"tests":[{"entry":"identifier","kind":"accept","input":"SAE J1939","expect":{}},{"entry":"identifier","kind":"accept","input":"SAE AMS2430","expect":{}},{"entry":"identifier","kind":"accept","input":"SAE ARP4754A:2010","expect":{}},{"entry":"identifier","kind":"reject","input":"nonsense","expect":{}}],"docs":{},"source":"# Port of lib/pubid/sae/parser.rb: SAE <AMS|AIR|ARP|AS|J|MA> <number>[:year]\n\ngrammar PubidSae version \"0.1.0\" edition 2 {\n doc_type = (\"AMS\" / \"AIR\" / \"ARP\" / \"AS\" / \"J\" / \"MA\") as type\n number = ((1*1letter 1*digit) / (1*digit *1letter)) as number\n year = (4digit) as year\n identifier = \"SAE\" as publisher space doc_type [space] number [(colon year)]\n letter = %x41-5A\n digit = %x30-39\n space = \" \"\n colon = \":\"\n entry identifier: identifier\n\n # Typed model bindings: every bound capture lands on the\n # consumer model's field of the same name.\n bindings identifier {\n type -> type (string)\n number -> number (string)\n year -> year (string)\n publisher -> publisher (string)\n }\n\n\n test {\n accept \"SAE J1939\"\n accept \"SAE AMS2430\"\n accept \"SAE ARP4754A:2010\"\n reject \"nonsense\"\n }\n}\n","source_hash":"2d3a161caeb9c059ce2600050b823aea373712c16cfa662e5f82166c6f7ee01a","unicode_tables":"onigmo (Ruby 3.4.8)","compiler_version":"1.3.114","edition":2,"checksum":"sha256:cdcc10602aa864cbccccd23ab6baaeed8e7edc4e4a8075c6c7a8f90324d7b5e1"}
|
|
1
|
+
{"bindings":null,"version":"0.2.0","grammar":"PubidSae","shape":"parsanol-tree/v2","binding_version":1,"entries":{"identifier":{"root":"identifier","grammar":{"atoms":[{"Str":{"pattern":"SAE"}},{"Named":{"name":"publisher","atom":0}},{"Entity":{"atom":3}},{"Str":{"pattern":" "}},{"Entity":{"atom":86}},{"Str":{"pattern":"AMS-QQ-A"}},{"Str":{"pattern":"AMS-QQ-C"}},{"Str":{"pattern":"AMS-QQ-N"}},{"Str":{"pattern":"AMS-QQ-P"}},{"Str":{"pattern":"AMS-QQ-S"}},{"Str":{"pattern":"AMS-QQ-W"}},{"Str":{"pattern":"AMS-WW-T"}},{"Str":{"pattern":"GEIA-STD"}},{"Str":{"pattern":"AMS-DTL"}},{"Str":{"pattern":"AMS-PRF"}},{"Str":{"pattern":"AMS-STD"}},{"Str":{"pattern":"EIA-STD"}},{"Str":{"pattern":"GEIA-HB"}},{"Str":{"pattern":"GEIA-OP"}},{"Str":{"pattern":"GEIA-TB"}},{"Str":{"pattern":"GEIASTD"}},{"Str":{"pattern":"AMSDTL"}},{"Str":{"pattern":"AMSPRF"}},{"Str":{"pattern":"AMSQQA"}},{"Str":{"pattern":"AMSQQC"}},{"Str":{"pattern":"AMSQQN"}},{"Str":{"pattern":"AMSQQP"}},{"Str":{"pattern":"AMSQQS"}},{"Str":{"pattern":"AMSQQW"}},{"Str":{"pattern":"AMSSTD"}},{"Str":{"pattern":"AMSWWT"}},{"Str":{"pattern":"EIA-IS"}},{"Str":{"pattern":"EIASTD"}},{"Str":{"pattern":"GEIAHB"}},{"Str":{"pattern":"GEIAOP"}},{"Str":{"pattern":"GEIATB"}},{"Str":{"pattern":"AMS-A"}},{"Str":{"pattern":"AMS-B"}},{"Str":{"pattern":"AMS-C"}},{"Str":{"pattern":"AMS-D"}},{"Str":{"pattern":"AMS-F"}},{"Str":{"pattern":"AMS-G"}},{"Str":{"pattern":"AMS-H"}},{"Str":{"pattern":"AMS-I"}},{"Str":{"pattern":"AMS-L"}},{"Str":{"pattern":"AMS-M"}},{"Str":{"pattern":"AMS-P"}},{"Str":{"pattern":"AMS-R"}},{"Str":{"pattern":"AMS-S"}},{"Str":{"pattern":"AMS-T"}},{"Str":{"pattern":"AMS-W"}},{"Str":{"pattern":"EIAIS"}},{"Str":{"pattern":"USCAR"}},{"Str":{"pattern":"AMSA"}},{"Str":{"pattern":"AMSB"}},{"Str":{"pattern":"AMSC"}},{"Str":{"pattern":"AMSD"}},{"Str":{"pattern":"AMSF"}},{"Str":{"pattern":"AMSG"}},{"Str":{"pattern":"AMSH"}},{"Str":{"pattern":"AMSI"}},{"Str":{"pattern":"AMSL"}},{"Str":{"pattern":"AMSM"}},{"Str":{"pattern":"AMSP"}},{"Str":{"pattern":"AMSR"}},{"Str":{"pattern":"AMSS"}},{"Str":{"pattern":"AMST"}},{"Str":{"pattern":"AMSW"}},{"Str":{"pattern":"GEIA"}},{"Str":{"pattern":"AIR"}},{"Str":{"pattern":"AMS"}},{"Str":{"pattern":"ARD"}},{"Str":{"pattern":"ARP"}},{"Str":{"pattern":"CMB"}},{"Str":{"pattern":"CRP"}},{"Str":{"pattern":"EIA"}},{"Str":{"pattern":"MAM"}},{"Str":{"pattern":"MAP"}},{"Str":{"pattern":"SSB"}},{"Str":{"pattern":"TSB"}},{"Str":{"pattern":"AS"}},{"Str":{"pattern":"JA"}},{"Str":{"pattern":"MA"}},{"Str":{"pattern":"MS"}},{"Str":{"pattern":"J"}},{"Alternative":{"atoms":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84]}},{"Named":{"name":"type","atom":85}},{"Entity":{"atom":90}},{"Entity":{"atom":3}},{"Str":{"pattern":"-"}},{"Alternative":{"atoms":[88,89]}},{"Named":{"name":"type_sep","atom":87}},{"Repetition":{"atom":91,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":97}},{"Entity":{"atom":95}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":94,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"number","atom":96}},{"Entity":{"atom":107}},{"Str":{"pattern":"-"}},{"Str":{"pattern":"_"}},{"Alternative":{"atoms":[99,100]}},{"Entity":{"atom":95}},{"Repetition":{"atom":102,"min":1,"max":3,"tag":"Repetition"}},{"Named":{"name":"dash","atom":103}},{"Entity":{"atom":95}},{"Lookahead":{"atom":105,"positive":false}},{"Sequence":{"atoms":[101,104,106]}},{"Repetition":{"atom":98,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":113}},{"Entity":{"atom":111}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Repetition":{"atom":110,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"tail","atom":112}},{"Repetition":{"atom":109,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":130}},{"Str":{"pattern":"/"}},{"Entity":{"atom":111}},{"Repetition":{"atom":117,"min":0,"max":null,"tag":"Repetition"}},{"Entity":{"atom":95}},{"Repetition":{"atom":119,"min":1,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[118,120]}},{"Entity":{"atom":111}},{"Repetition":{"atom":122,"min":1,"max":null,"tag":"Repetition"}},{"Alternative":{"atoms":[121,123]}},{"Named":{"name":"part","atom":124}},{"Entity":{"atom":111}},{"Repetition":{"atom":126,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"part_tail","atom":127}},{"Repetition":{"atom":128,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[116,125,129]}},{"Repetition":{"atom":115,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":137}},{"Str":{"pattern":"_A"}},{"Entity":{"atom":95}},{"Repetition":{"atom":134,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"amendment","atom":135}},{"Sequence":{"atoms":[133,136]}},{"Repetition":{"atom":132,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":153}},{"Str":{"pattern":":"}},{"Entity":{"atom":95}},{"Repetition":{"atom":141,"min":4,"max":4,"tag":"Repetition"}},{"Named":{"name":"year","atom":142}},{"Sequence":{"atoms":[140,143]}},{"Str":{"pattern":"_"}},{"Entity":{"atom":95}},{"Repetition":{"atom":146,"min":4,"max":4,"tag":"Repetition"}},{"Named":{"name":"year","atom":147}},{"Entity":{"atom":95}},{"Repetition":{"atom":149,"min":2,"max":2,"tag":"Repetition"}},{"Named":{"name":"month","atom":150}},{"Sequence":{"atoms":[145,148,151]}},{"Alternative":{"atoms":[144,152]}},{"Repetition":{"atom":139,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[1,2,4,92,93,108,114,131,138,154]}}],"root":155},"bindings":[{"capture":"publisher","path":"publisher","type":"string","card":null,"preprocess":null},{"capture":"type","path":"type","type":"string","card":null,"preprocess":null},{"capture":"number","path":"number","type":"string","card":null,"preprocess":null},{"capture":"dash","path":"dash","type":"string","card":null,"preprocess":null},{"capture":"tail","path":"tail","type":"string","card":null,"preprocess":null},{"capture":"part","path":"part","type":"string","card":null,"preprocess":null},{"capture":"part_tail","path":"part_tail","type":"string","card":null,"preprocess":null},{"capture":"amendment","path":"amendment","type":"string","card":null,"preprocess":null},{"capture":"year","path":"year","type":"string","card":null,"preprocess":null},{"capture":"month","path":"month","type":"string","card":null,"preprocess":null}]}},"preprocess":{},"tables":{},"lint":{"order_warnings":[]},"default_entry":"identifier","render":{},"derive":{},"tests":[{"entry":"identifier","kind":"accept","input":"SAE J1939","expect":{}},{"entry":"identifier","kind":"accept","input":"SAE J 3016","expect":{}},{"entry":"identifier","kind":"accept","input":"SAE AMS2430","expect":{}},{"entry":"identifier","kind":"accept","input":"SAE ARP4754A:2010","expect":{}},{"entry":"identifier","kind":"accept","input":"SAE AMS-QQ-A-250/4B","expect":{}},{"entry":"identifier","kind":"accept","input":"SAE AMSQQA250/4B","expect":{}},{"entry":"identifier","kind":"reject","input":"nonsense","expect":{}},{"entry":"identifier","kind":"reject","input":"SAE J3016_2021","expect":{}},{"entry":"identifier","kind":"reject","input":"SAE XYZ1","expect":{}},{"entry":"identifier","kind":"example","input":"SAE J3016_202104","expect":{"type":"J","number":"3016","year":"2021","month":"04"}},{"entry":"identifier","kind":"example","input":"SAE AMS4121J","expect":{"type":"AMS","number":"4121","tail":"J"}},{"entry":"identifier","kind":"example","input":"SAE AS22520/45A","expect":{"type":"AS","number":"22520","part":"45","part_tail":"A"}},{"entry":"identifier","kind":"example","input":"SAE AMS5622/H1025","expect":{"type":"AMS","number":"5622","part":"H1025"}},{"entry":"identifier","kind":"example","input":"SAE J2945/AS","expect":{"type":"J","number":"2945","part":"AS"}},{"entry":"identifier","kind":"example","input":"SAE J1939-71:2020","expect":{"type":"J","number":"1939","dash":"71","year":"2020"}},{"entry":"identifier","kind":"example","input":"SAE EIA649_1A","expect":{"type":"EIA","number":"649","dash":"1","tail":"A"}},{"entry":"identifier","kind":"example","input":"SAE AS5259/7A_A1","expect":{"type":"AS","number":"5259","part":"7","part_tail":"A","amendment":"1"}},{"entry":"identifier","kind":"example","input":"SAE AMSSTD595","expect":{"type":"AMSSTD","number":"595"}},{"entry":"identifier","kind":"example","input":"SAE GEIA-STD-0001","expect":{"type":"GEIA-STD","number":"0001"}}],"docs":{},"source":"# SAE identifiers, in SAE's printed form and in the hyphen-free form of its\n# Crossref DOI suffixes (10.4271/<code>):\n#\n# SAE <type>[ |-]<number>[-<dash>][<tail>][/<part>[<part_tail>]][_A<n>][:yyyy|_yyyymm]\n#\n# The grammar only segments. What a letter tail means (a revision, the\n# metric M of a J report, a digital-annex DA) and which spelling of the type\n# is printed is decided by pubid's lib/pubid/sae/builder.rb, which owns the\n# type table (Pubid::Sae::Components::Type::TYPES). The doc_type\n# alternatives are that table's keys plus their hyphen-free spellings,\n# longest first: PEG commits to the first alternative that matches, so\n# \"AMS\" must come after every \"AMS...\" family.\n\ngrammar PubidSae version \"0.2.0\" edition 2 {\n doc_type = (\"AMS-QQ-A\" / \"AMS-QQ-C\" / \"AMS-QQ-N\" / \"AMS-QQ-P\" / \"AMS-QQ-S\" / \"AMS-QQ-W\" / \"AMS-WW-T\" / \"GEIA-STD\" / \"AMS-DTL\" / \"AMS-PRF\" / \"AMS-STD\" / \"EIA-STD\" / \"GEIA-HB\" / \"GEIA-OP\" / \"GEIA-TB\" / \"GEIASTD\" / \"AMSDTL\" / \"AMSPRF\" / \"AMSQQA\" / \"AMSQQC\" / \"AMSQQN\" / \"AMSQQP\" / \"AMSQQS\" / \"AMSQQW\" / \"AMSSTD\" / \"AMSWWT\" / \"EIA-IS\" / \"EIASTD\" / \"GEIAHB\" / \"GEIAOP\" / \"GEIATB\" / \"AMS-A\" / \"AMS-B\" / \"AMS-C\" / \"AMS-D\" / \"AMS-F\" / \"AMS-G\" / \"AMS-H\" / \"AMS-I\" / \"AMS-L\" / \"AMS-M\" / \"AMS-P\" / \"AMS-R\" / \"AMS-S\" / \"AMS-T\" / \"AMS-W\" / \"EIAIS\" / \"USCAR\" / \"AMSA\" / \"AMSB\" / \"AMSC\" / \"AMSD\" / \"AMSF\" / \"AMSG\" / \"AMSH\" / \"AMSI\" / \"AMSL\" / \"AMSM\" / \"AMSP\" / \"AMSR\" / \"AMSS\" / \"AMST\" / \"AMSW\" / \"GEIA\" / \"AIR\" / \"AMS\" / \"ARD\" / \"ARP\" / \"CMB\" / \"CRP\" / \"EIA\" / \"MAM\" / \"MAP\" / \"SSB\" / \"TSB\" / \"AS\" / \"JA\" / \"MA\" / \"MS\" / \"J\") as type\n type_sep = space / \"-\"\n number = (1*digit) as number\n # A dash number: J1939-71, EIA649-2, and the DOI's EIA649_1. At most three\n # digits and no digit after, so \"_202104\" stays a date.\n dash = (\"-\" / \"_\") (1*3digit) as dash !digit\n tail = (1*letter) as tail\n part = \"/\" ((*letter 1*digit) / 1*letter) as part [(1*letter) as part_tail]\n amendment = \"_A\" (1*digit) as amendment\n date = (\":\" (4digit) as year) / (\"_\" (4digit) as year (2digit) as month)\n identifier = \"SAE\" as publisher space doc_type [type_sep] number [dash] [tail] [part] [amendment] [date]\n letter = %x41-5A\n digit = %x30-39\n space = \" \"\n entry identifier: identifier\n\n bindings identifier {\n publisher -> publisher (string)\n type -> type (string)\n number -> number (string)\n dash -> dash (string)\n tail -> tail (string)\n part -> part (string)\n part_tail -> part_tail (string)\n amendment -> amendment (string)\n year -> year (string)\n month -> month (string)\n }\n\n test {\n accept \"SAE J1939\"\n accept \"SAE J 3016\"\n accept \"SAE AMS2430\"\n accept \"SAE ARP4754A:2010\"\n accept \"SAE AMS-QQ-A-250/4B\"\n accept \"SAE AMSQQA250/4B\"\n reject \"nonsense\"\n reject \"SAE J3016_2021\"\n reject \"SAE XYZ1\"\n example \"SAE J3016_202104\" { type: \"J\", number: \"3016\", year: \"2021\", month: \"04\" }\n example \"SAE AMS4121J\" { type: \"AMS\", number: \"4121\", tail: \"J\" }\n example \"SAE AS22520/45A\" { type: \"AS\", number: \"22520\", part: \"45\", part_tail: \"A\" }\n example \"SAE AMS5622/H1025\" { type: \"AMS\", number: \"5622\", part: \"H1025\" }\n example \"SAE J2945/AS\" { type: \"J\", number: \"2945\", part: \"AS\" }\n example \"SAE J1939-71:2020\" { type: \"J\", number: \"1939\", dash: \"71\", year: \"2020\" }\n example \"SAE EIA649_1A\" { type: \"EIA\", number: \"649\", dash: \"1\", tail: \"A\" }\n example \"SAE AS5259/7A_A1\" { type: \"AS\", number: \"5259\", part: \"7\", part_tail: \"A\", amendment: \"1\" }\n example \"SAE AMSSTD595\" { type: \"AMSSTD\", number: \"595\" }\n example \"SAE GEIA-STD-0001\" { type: \"GEIA-STD\", number: \"0001\" }\n }\n}\n","source_hash":"204b7183faedf1026e76016e92d3e9904d81930ed04d7a9d54849ac78f1ad6e7","unicode_tables":"onigmo (Ruby 3.4.10)","compiler_version":"1.3.129","edition":2,"semantics_hash":"sha256:58d95b3087380a9ddb6598898095eac06e5cf639bc63d699c0db8d51c88c15f0","checksum":"sha256:ea9c116c71abfbf523abb94e20bede94a0b9613621dd2d4cae1990aad6f3e774"}
|
|
@@ -57,3 +57,20 @@ keeps `#matches?(other, ignore:)`.
|
|
|
57
57
|
decision, on the reading that a published norm should carry a published
|
|
58
58
|
stage by default. It is fixed at the matching layer instead: no grammar
|
|
59
59
|
change, no index re-crawl.
|
|
60
|
+
|
|
61
|
+
## `locate_type` and `CenelecHarmonizationDocument`
|
|
62
|
+
|
|
63
|
+
`CenCenelec.locate_type` returns the **first** class in `identifier_types`
|
|
64
|
+
whose `type[:key]` matches, and `identifier_types` reads
|
|
65
|
+
`Identifiers.constants`, whose order Ruby does not guarantee.
|
|
66
|
+
`CenelecHarmonizationDocument` shared the key `:hd` with
|
|
67
|
+
`HarmonizationDocument`, and its autoload was misspelled
|
|
68
|
+
(`CENelecHarmonizationDocument`), so it joined the list only when something
|
|
69
|
+
else had loaded its file before the list was memoized. Then `HD 123:2020`
|
|
70
|
+
could build the CENELEC class: 6 CEN examples failed in one spec order only,
|
|
71
|
+
exposed by the pubid-URN GOST wave, not caused by it. The autoload is fixed
|
|
72
|
+
and the class has its own key `:chd` (the token `docs/URN-SCHEMES.adoc`
|
|
73
|
+
gives it); `spec/pubid/cen_cenelec/type_key_spec.rb` loads the class and
|
|
74
|
+
rebuilds the list, so it does not depend on order. **Every type key in a
|
|
75
|
+
flavor must be unique**; ANSI has the same shape (`Standard` and
|
|
76
|
+
`AmericanNationalStandard` both declare `ans`) and is not checked here.
|
|
@@ -19,8 +19,11 @@ module Pubid
|
|
|
19
19
|
),
|
|
20
20
|
].freeze
|
|
21
21
|
|
|
22
|
+
# Its own key: CenCenelec.locate_type returns the first class with
|
|
23
|
+
# a matching key, and HarmonizationDocument owns :hd ("HD 123:2020").
|
|
24
|
+
# A shared key made the class choice depend on load order.
|
|
22
25
|
def self.type
|
|
23
|
-
{ key: :
|
|
26
|
+
{ key: :chd, title: "CENELEC Harmonization Document", short: "HD" }
|
|
24
27
|
end
|
|
25
28
|
end
|
|
26
29
|
end
|
|
@@ -7,7 +7,7 @@ module Pubid
|
|
|
7
7
|
"#{__dir__}/identifiers/adopted_european_norm"
|
|
8
8
|
autoload :Amendment, "#{__dir__}/identifiers/amendment"
|
|
9
9
|
autoload :Base, "#{__dir__}/identifiers/base"
|
|
10
|
-
autoload :
|
|
10
|
+
autoload :CenelecHarmonizationDocument,
|
|
11
11
|
"#{__dir__}/identifiers/cenelec_harmonization_document"
|
|
12
12
|
autoload :CenReport, "#{__dir__}/identifiers/cen_report"
|
|
13
13
|
autoload :CenWorkshopAgreement,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module CenCenelec
|
|
5
|
+
# The pubid-scheme URN of CEN/CENELEC — PARTIAL (issue #656;
|
|
6
|
+
# docs/URN-SCHEMES.adoc).
|
|
7
|
+
#
|
|
8
|
+
# Only the types a GOST adoption can name are declared: an EN, an
|
|
9
|
+
# adopted EN ("EN ISO 12100:2010", no number of its own), its amendment
|
|
10
|
+
# and corrigendum, a CEN TS or TR, and the consolidated
|
|
11
|
+
# "EN 14988:2017+A1:2020". The atoms keep the doc's canonical order; an
|
|
12
|
+
# absent atom writes nothing, so the CEN wave can add types and atoms
|
|
13
|
+
# without changing these URNs. Until then cen_cenelec stays pending in
|
|
14
|
+
# the contract spec.
|
|
15
|
+
PubidUrnSchema = Pubid::PubidUrn.define("cencenelec") do
|
|
16
|
+
atom "publisher", "pub"
|
|
17
|
+
atom "copublishers", "co", kind: :list
|
|
18
|
+
atom "part", "part"
|
|
19
|
+
atom "year", "year"
|
|
20
|
+
atom "month", "month"
|
|
21
|
+
atom "version", "v"
|
|
22
|
+
atom "stage", "stage"
|
|
23
|
+
atom "adopted", "adopted", kind: :id
|
|
24
|
+
atom "identifiers", "ids", kind: :ids
|
|
25
|
+
|
|
26
|
+
type "pubid:cencenelec:adopted-european-norm", "aen", core: []
|
|
27
|
+
type "pubid:cencenelec:amendment", "amd"
|
|
28
|
+
type "pubid:cencenelec:consolidated-identifier", "consol", core: []
|
|
29
|
+
type "pubid:cencenelec:corrigendum", "cor"
|
|
30
|
+
type "pubid:cencenelec:european-norm", "en"
|
|
31
|
+
type "pubid:cencenelec:technical-report", "tr"
|
|
32
|
+
type "pubid:cencenelec:technical-specification", "ts"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/pubid/cen_cenelec.rb
CHANGED
|
@@ -19,6 +19,7 @@ module Pubid
|
|
|
19
19
|
autoload :Identifier, "#{__dir__}/cen_cenelec/identifier"
|
|
20
20
|
autoload :Identifiers, "#{__dir__}/cen_cenelec/identifiers"
|
|
21
21
|
autoload :Parser, "#{__dir__}/cen_cenelec/parser"
|
|
22
|
+
autoload :PubidUrnSchema, "#{__dir__}/cen_cenelec/pubid_urn_schema"
|
|
22
23
|
autoload :Renderer, "#{__dir__}/cen_cenelec/renderer"
|
|
23
24
|
autoload :SingleIdentifier, "#{__dir__}/cen_cenelec/single_identifier"
|
|
24
25
|
autoload :UrnGenerator, "#{__dir__}/cen_cenelec/urn_generator"
|
data/lib/pubid/etsi/CLAUDE.md
CHANGED
|
@@ -32,3 +32,12 @@ keeps `#matches?(other, ignore:)`.
|
|
|
32
32
|
- `SUBSET_PAIRS["etsi"]` in `spec/pubid/subset_match_spec.rb` moved from
|
|
33
33
|
`ETSI EN 300 175` to `ETSI EN 300 175-1`: the reference now states its
|
|
34
34
|
part, and the version and the date are what it omits.
|
|
35
|
+
|
|
36
|
+
## pubid-scheme URN (issue #656)
|
|
37
|
+
|
|
38
|
+
`lib/pubid/etsi/pubid_urn_schema.rb`; rulings in `docs/URN-SCHEMES.adoc`
|
|
39
|
+
(`[[urn-etsi]]`). A supplement's `number` is an Integer ordinal, so its core
|
|
40
|
+
is `:int` (`core: [["number", :int]]`), and the codec refuses a String there.
|
|
41
|
+
`minor` and `day` are placed although no pass fixture sets them, because
|
|
42
|
+
`EtsiStandard`'s `key_value` block maps them: a schema must place every key
|
|
43
|
+
the mapping can write, not only the keys the corpus shows.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Etsi
|
|
5
|
+
# The pubid-scheme URN of ETSI (issue #656; docs/URN-SCHEMES.adoc).
|
|
6
|
+
#
|
|
7
|
+
# ETSI EN 300 175-1 V2.9.1 (2022-01)
|
|
8
|
+
# urn:pubid:etsi:es:300~20175:type-EN:part-1:year-2022:month-01:v-2.9.1
|
|
9
|
+
#
|
|
10
|
+
# ETSI ETR 108/A1 ed.1 (1995-08)
|
|
11
|
+
# urn:pubid:etsi:es:108:type-ETR:year-1995:month-08:v-1:isedition-true:
|
|
12
|
+
# amd:1
|
|
13
|
+
#
|
|
14
|
+
# The number is the core of every type. A supplement's number is its
|
|
15
|
+
# Integer ordinal, so it is an :int core; the document it supplements
|
|
16
|
+
# comes first, along +base+. The official urn:etsi: form (RFC 8515)
|
|
17
|
+
# stays on #to_urn.
|
|
18
|
+
PubidUrnSchema = Pubid::PubidUrn.define("etsi") do
|
|
19
|
+
atom "type", "type"
|
|
20
|
+
atom "parts", "part", kind: :list
|
|
21
|
+
atom "minor", "minor"
|
|
22
|
+
atom "year", "year"
|
|
23
|
+
atom "month", "month"
|
|
24
|
+
atom "day", "day"
|
|
25
|
+
atom "version", "v"
|
|
26
|
+
atom "is_edition", "isedition", kind: :bool
|
|
27
|
+
|
|
28
|
+
supplement = [["number", :int]]
|
|
29
|
+
type "pubid:etsi:amendment", "amd", core: supplement
|
|
30
|
+
type "pubid:etsi:corrigendum", "cor", core: supplement
|
|
31
|
+
type "pubid:etsi:etsi-standard", "es"
|
|
32
|
+
type "pubid:etsi:identifier", "id"
|
|
33
|
+
type "pubid:etsi:supplement-identifier", "si", core: supplement
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/pubid/etsi.rb
CHANGED
|
@@ -17,6 +17,7 @@ module Pubid
|
|
|
17
17
|
autoload :Identifier, "#{__dir__}/etsi/identifiers/base"
|
|
18
18
|
autoload :Identifiers, "#{__dir__}/etsi/identifiers"
|
|
19
19
|
autoload :Parser, "#{__dir__}/etsi/parser"
|
|
20
|
+
autoload :PubidUrnSchema, "#{__dir__}/etsi/pubid_urn_schema"
|
|
20
21
|
autoload :Renderer, "#{__dir__}/etsi/renderer"
|
|
21
22
|
autoload :UrnGenerator, "#{__dir__}/etsi/urn_generator"
|
|
22
23
|
autoload :UrnParser, "#{__dir__}/etsi/urn_parser"
|
data/lib/pubid/gost/CLAUDE.md
CHANGED
|
@@ -62,3 +62,15 @@ a deterministic reproduction of the race (not a reliance on catching the rare
|
|
|
62
62
|
full-suite failure): it temporarily `Registry.unregister(:bsi)`s the usual
|
|
63
63
|
race winner and asserts routing still resolves to `Pubid::Iso` correctly —
|
|
64
64
|
proving the fix is prefix routing, not a lucky alphabetical ordering.
|
|
65
|
+
|
|
66
|
+
## pubid-scheme URN (issue #656)
|
|
67
|
+
|
|
68
|
+
`lib/pubid/gost/pubid_urn_schema.rb`; rulings in `docs/URN-SCHEMES.adoc`
|
|
69
|
+
(`[[urn-gost]]`). The adopted documents nest with their own flavor's schema,
|
|
70
|
+
so this wave added a partial `lib/pubid/cen_cenelec/pubid_urn_schema.rb`.
|
|
71
|
+
**Open gap:** `ForeignReference#raw` is not mapped in `key_value`, so
|
|
72
|
+
`to_hash` drops the text of an adopted document of an unknown flavor
|
|
73
|
+
(`(OECD 460:2017)`): two such adoptions share one hash and one URN, and
|
|
74
|
+
`from_hash` loses the parenthesis. Mapping `raw` changes `to_hash`, and the
|
|
75
|
+
`pubid-testsuite` GOST cases store the hash without it, so the fix must land
|
|
76
|
+
together with a testsuite update. The schema's `raw-` atom is ready for it.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Gost
|
|
5
|
+
# The pubid-scheme URN of GOST (issue #656; docs/URN-SCHEMES.adoc).
|
|
6
|
+
#
|
|
7
|
+
# ГОСТ ISO 6848 -> urn:pubid:gost:is:6848:co-ISO
|
|
8
|
+
#
|
|
9
|
+
# ГОСТ Р 58904-2020/ISO/TR 25901-1:2016
|
|
10
|
+
# urn:pubid:gost:ns:58904:year-2020:ia:58904:year-2020:
|
|
11
|
+
# adopted-(iso:tr:25901:part-1:year-2016)
|
|
12
|
+
#
|
|
13
|
+
# A harmonized or identical adoption wraps the GOST standard in +base+
|
|
14
|
+
# (written first) and repeats its number and year, as #to_hash does.
|
|
15
|
+
# The adopted documents are nested with their own flavor's schema
|
|
16
|
+
# (cencenelec, iec, iso, ieee, astm).
|
|
17
|
+
PubidUrnSchema = Pubid::PubidUrn.define("gost") do
|
|
18
|
+
atom "publisher", "pub"
|
|
19
|
+
atom "copublisher", "co"
|
|
20
|
+
atom "year", "year"
|
|
21
|
+
atom "subtype", "subtype"
|
|
22
|
+
atom "raw", "raw"
|
|
23
|
+
atom "adopted_identifiers", "adoptedidentifiers", kind: :ids
|
|
24
|
+
atom "adopted", "adopted", kind: :id
|
|
25
|
+
|
|
26
|
+
type "pubid:gost:foreign-reference", "fr"
|
|
27
|
+
type "pubid:gost:harmonized", "h"
|
|
28
|
+
type "pubid:gost:identical-adoption", "ia"
|
|
29
|
+
type "pubid:gost:identifier", "id"
|
|
30
|
+
type "pubid:gost:interstate-standard", "is"
|
|
31
|
+
type "pubid:gost:national-standard", "ns"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/pubid/gost.rb
CHANGED
|
@@ -14,6 +14,7 @@ module Pubid
|
|
|
14
14
|
autoload :Identifier, "#{__dir__}/gost/identifier"
|
|
15
15
|
autoload :Identifiers, "#{__dir__}/gost/identifiers"
|
|
16
16
|
autoload :Parser, "#{__dir__}/gost/parser"
|
|
17
|
+
autoload :PubidUrnSchema, "#{__dir__}/gost/pubid_urn_schema"
|
|
17
18
|
autoload :Renderer, "#{__dir__}/gost/renderer"
|
|
18
19
|
autoload :UrnGenerator, "#{__dir__}/gost/urn_generator"
|
|
19
20
|
autoload :UrnParser, "#{__dir__}/gost/urn_parser"
|
data/lib/pubid/identifier.rb
CHANGED
|
@@ -1468,7 +1468,13 @@ module Pubid
|
|
|
1468
1468
|
@hash ||= compute_hash
|
|
1469
1469
|
end
|
|
1470
1470
|
|
|
1471
|
-
|
|
1471
|
+
# The second parameter is lutaml's: ComparableModel#== (an alias of its
|
|
1472
|
+
# #eql?(other, compared_objects = {})) recurses with two arguments when
|
|
1473
|
+
# an attribute holds a model of the SAME class, such as the standard
|
|
1474
|
+
# an IEEE "Revision of" id revises. A one-parameter override made #==
|
|
1475
|
+
# raise ArgumentError there. The cycle guard is lutaml's; this method
|
|
1476
|
+
# keeps its own semantics and does not need it.
|
|
1477
|
+
def eql?(other, _compared_objects = nil)
|
|
1472
1478
|
return false unless other.is_a?(self.class)
|
|
1473
1479
|
|
|
1474
1480
|
hash == other.hash && self == other
|
|
@@ -5,10 +5,12 @@ module Pubid
|
|
|
5
5
|
# The pubid-scheme URN of IEC — PARTIAL (issue #656;
|
|
6
6
|
# docs/URN-SCHEMES.adoc).
|
|
7
7
|
#
|
|
8
|
-
# Only
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
8
|
+
# Only the types other flavors nest are declared: the International
|
|
9
|
+
# Standard (IEC/IEEE co-publications, GOST adoptions), and the TS, TR,
|
|
10
|
+
# amendment and consolidated identifier a GOST adoption can name
|
|
11
|
+
# ("IEC 60079-0:2017+AMD1:2020"). IEC's own wave declares the rest of the
|
|
12
|
+
# flavor; until then IEC stays pending in the contract spec. The official
|
|
13
|
+
# RFC 5141-style urn:iec:std: form stays on #to_urn.
|
|
12
14
|
PubidUrnSchema = Pubid::PubidUrn.define("iec") do
|
|
13
15
|
atom "publisher", "pub"
|
|
14
16
|
atom "copublishers", "co", kind: :list
|
|
@@ -23,10 +25,15 @@ module Pubid
|
|
|
23
25
|
atom "stage_iteration", "iter"
|
|
24
26
|
atom "database", "database"
|
|
25
27
|
atom "languages.*.code", "lang"
|
|
28
|
+
atom "identifiers", "ids", kind: :ids
|
|
26
29
|
|
|
27
30
|
facet "languages.*.original_code", "langsrc"
|
|
28
31
|
|
|
32
|
+
type "pubid:iec:amendment", "amd"
|
|
33
|
+
type "pubid:iec:consolidated-identifier", "consol", core: []
|
|
29
34
|
type "pubid:iec:international-standard", "is"
|
|
35
|
+
type "pubid:iec:technical-report", "tr"
|
|
36
|
+
type "pubid:iec:technical-specification", "ts"
|
|
30
37
|
end
|
|
31
38
|
end
|
|
32
39
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# PLATEAU flavor notes
|
|
2
|
+
|
|
3
|
+
Read this before you change `lib/pubid/plateau/` or `spec/pubid/plateau/`. The
|
|
4
|
+
root `CLAUDE.md` keeps the cross-flavor contract that every flavor obeys.
|
|
5
|
+
|
|
6
|
+
## pubid-scheme URN (issue #656)
|
|
7
|
+
|
|
8
|
+
`lib/pubid/plateau/pubid_urn_schema.rb`; rulings in `docs/URN-SCHEMES.adoc`
|
|
9
|
+
(`[[urn-plateau]]`). PLATEAU is the one flavor whose `number` stays an
|
|
10
|
+
Integer, so its core is `:int` (`core: [["number", :int]]`): `#00` is the core
|
|
11
|
+
`0`, and `00` in a URN is not canonical. The Annex type has no `number`; it
|
|
12
|
+
has no core, its document comes first along `base`, and its `letter` is an
|
|
13
|
+
atom. No input makes an Annex today, so the spec builds one by hand.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Plateau
|
|
5
|
+
# The pubid-scheme URN of PLATEAU (issue #656; docs/URN-SCHEMES.adoc).
|
|
6
|
+
#
|
|
7
|
+
# PLATEAU Handbook #00 第1.0版 -> urn:pubid:plateau:hb:0:ed-1.0
|
|
8
|
+
#
|
|
9
|
+
# PLATEAU keeps +number+ as an Integer (the one flavor the String retype
|
|
10
|
+
# left out), so it is an :int core. An Annex has no number of its own:
|
|
11
|
+
# its document comes first, along +base+, and its letter is an atom.
|
|
12
|
+
PubidUrnSchema = Pubid::PubidUrn.define("plateau") do
|
|
13
|
+
atom "publisher", "pub"
|
|
14
|
+
atom "edition", "ed"
|
|
15
|
+
atom "annex", "annex", kind: :int
|
|
16
|
+
|
|
17
|
+
number = [["number", :int]]
|
|
18
|
+
type "pubid:plateau:annex", "annex", core: [],
|
|
19
|
+
atoms: [%w[letter letter]]
|
|
20
|
+
type "pubid:plateau:handbook", "hb", core: number
|
|
21
|
+
type "pubid:plateau:identifier", "id", core: number
|
|
22
|
+
type "pubid:plateau:technical-report", "tr", core: number
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/pubid/plateau.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Pubid
|
|
|
13
13
|
autoload :Identifier, "#{__dir__}/plateau/identifiers/base"
|
|
14
14
|
autoload :Identifiers, "#{__dir__}/plateau/identifiers"
|
|
15
15
|
autoload :Parser, "#{__dir__}/plateau/parser"
|
|
16
|
+
autoload :PubidUrnSchema, "#{__dir__}/plateau/pubid_urn_schema"
|
|
16
17
|
autoload :Renderer, "#{__dir__}/plateau/renderer"
|
|
17
18
|
autoload :SupplementIdentifier, "#{__dir__}/plateau/supplement_identifier"
|
|
18
19
|
autoload :UrnGenerator, "#{__dir__}/plateau/urn_generator"
|
|
@@ -62,18 +62,24 @@ module Pubid
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def decode_core(tokens, index, node, schema)
|
|
65
|
-
schema.core.
|
|
65
|
+
schema.core.each_with_index do |parts, i|
|
|
66
66
|
break if index >= tokens.size
|
|
67
67
|
|
|
68
68
|
token = tokens[index]
|
|
69
69
|
break if PubidUrn.structural?(token)
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
unless token == NULL
|
|
72
|
+
Paths.put(node, parts, core_value(token, schema.core_kinds[i]))
|
|
73
|
+
end
|
|
72
74
|
index += 1
|
|
73
75
|
end
|
|
74
76
|
index
|
|
75
77
|
end
|
|
76
78
|
|
|
79
|
+
def core_value(token, kind)
|
|
80
|
+
kind == :int ? integer(token) : Escape.decode(token)
|
|
81
|
+
end
|
|
82
|
+
|
|
77
83
|
def decode_atoms(tokens, index, node, schema)
|
|
78
84
|
last = -1
|
|
79
85
|
while index < tokens.size &&
|
|
@@ -69,11 +69,23 @@ module Pubid
|
|
|
69
69
|
def core_segments(node, schema)
|
|
70
70
|
values = schema.core.map { |parts| Paths.take(node, parts) }
|
|
71
71
|
values.pop while !values.empty? && values.last.nil?
|
|
72
|
-
values.map do |value|
|
|
73
|
-
|
|
72
|
+
values.each_with_index.map do |value, i|
|
|
73
|
+
next NULL if value.nil?
|
|
74
|
+
next core_integer(value, schema, i) if schema.core_kinds[i] == :int
|
|
75
|
+
|
|
76
|
+
core_value(value, schema)
|
|
74
77
|
end
|
|
75
78
|
end
|
|
76
79
|
|
|
80
|
+
# A decimal never has the shape of structure, so it needs no escape.
|
|
81
|
+
def core_integer(value, schema, index)
|
|
82
|
+
return value.to_s if value.is_a?(Integer)
|
|
83
|
+
|
|
84
|
+
raise Errors::InvalidInputError,
|
|
85
|
+
"#{schema.type} #{schema.core[index].join('.')}: " \
|
|
86
|
+
"#{value.inspect} is not int"
|
|
87
|
+
end
|
|
88
|
+
|
|
77
89
|
# A core value that has the shape of structure (PubidUrn::STRUCTURE)
|
|
78
90
|
# gets its first character escaped.
|
|
79
91
|
def core_value(value, schema)
|
|
@@ -48,13 +48,15 @@ module Pubid
|
|
|
48
48
|
|
|
49
49
|
# The URN shape of one concrete identifier type.
|
|
50
50
|
class TypeSchema
|
|
51
|
-
attr_reader :flavor, :type, :token, :core, :atoms, :facets
|
|
51
|
+
attr_reader :flavor, :type, :token, :core, :core_kinds, :atoms, :facets
|
|
52
52
|
|
|
53
|
+
# +core+ is a list of [path, kind] pairs; kind is :string or :int.
|
|
53
54
|
def initialize(flavor:, type:, token:, core:, atoms:, facets:)
|
|
54
55
|
@flavor = flavor
|
|
55
56
|
@type = type
|
|
56
57
|
@token = token
|
|
57
|
-
@core = core.map { |path| path.split(".") }.freeze
|
|
58
|
+
@core = core.map { |path, _| path.split(".") }.freeze
|
|
59
|
+
@core_kinds = core.map { |_, kind| kind }.freeze
|
|
58
60
|
@atoms = atoms.freeze
|
|
59
61
|
@facets = facets.freeze
|
|
60
62
|
@atom_index = atoms.each_with_index.to_h { |atom, i| [atom.prefix, i] }
|
|
@@ -86,6 +88,7 @@ module Pubid
|
|
|
86
88
|
NAME = /\A[a-z][a-z0-9]*\z/
|
|
87
89
|
KINDS = %i[string list bool bools int id ids].freeze
|
|
88
90
|
FACET_KINDS = %i[string list bool bools].freeze
|
|
91
|
+
CORE_KINDS = %i[string int].freeze
|
|
89
92
|
# "f" opens the facet group; "type" is the type-metadata URN
|
|
90
93
|
# (Pubid::IdentifierMetadata, "urn:pubid:<flavor>:type:<key>").
|
|
91
94
|
RESERVED_TOKENS = %w[f type].freeze
|
|
@@ -110,7 +113,9 @@ module Pubid
|
|
|
110
113
|
|
|
111
114
|
# @param type [String] the +_type+ the class serializes
|
|
112
115
|
# @param token [String] the short type token of the URN
|
|
113
|
-
# @param core [Array<String>] the positional core paths
|
|
116
|
+
# @param core [Array<String, Array>] the positional core paths; an
|
|
117
|
+
# entry is a path (a String value) or [path, kind], kind :string or
|
|
118
|
+
# :int
|
|
114
119
|
# @param atoms [Array<Array>] extra atoms: [path, prefix, kind]
|
|
115
120
|
# @param facets [Array<Array>] extra facets: [path, name, kind]
|
|
116
121
|
def type(type, token, core: %w[number], atoms: [], facets: [])
|
|
@@ -126,7 +131,8 @@ module Pubid
|
|
|
126
131
|
check_unique!(all_facets.map(&:name), "facet name", type)
|
|
127
132
|
|
|
128
133
|
schema = TypeSchema.new(flavor: flavor, type: type, token: token,
|
|
129
|
-
core: core
|
|
134
|
+
core: core.map { |spec| build_core(*spec) },
|
|
135
|
+
atoms: all_atoms,
|
|
130
136
|
facets: all_facets)
|
|
131
137
|
@types[type] = schema
|
|
132
138
|
@tokens[token] = schema
|
|
@@ -142,6 +148,14 @@ module Pubid
|
|
|
142
148
|
|
|
143
149
|
private
|
|
144
150
|
|
|
151
|
+
def build_core(path, kind = :string)
|
|
152
|
+
unless CORE_KINDS.include?(kind)
|
|
153
|
+
raise ArgumentError, "unknown core kind #{kind.inspect}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
[path, kind]
|
|
157
|
+
end
|
|
158
|
+
|
|
145
159
|
def build_atom(path, prefix, kind = nil)
|
|
146
160
|
check_name!(prefix, "atom prefix")
|
|
147
161
|
Atom.new(path, prefix, check_kind!(path, kind, KINDS, "atom"))
|
data/lib/pubid/pubid_urn.rb
CHANGED
|
@@ -106,6 +106,46 @@ module Pubid
|
|
|
106
106
|
string.is_a?(String) && string.b[0, PREFIX.length].downcase == PREFIX
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
+
# The slug form (relaton#441): the URN wearing HTTP-path clothes.
|
|
110
|
+
# Each ":" segment becomes a "/" segment, after the "urn:pubid:"
|
|
111
|
+
# prefix is cut — "urn:pubid:iso:is:9001:year-2015" is
|
|
112
|
+
# "iso/is/9001/year-2015". The contract's path-safety gate
|
|
113
|
+
# guarantees no segment carries a "/", so the transform is
|
|
114
|
+
# bijective: {from_slug} rebuilds the exact URN.
|
|
115
|
+
#
|
|
116
|
+
# @param source [Identifier, String] an identifier or its
|
|
117
|
+
# pubid-scheme URN
|
|
118
|
+
# @return [String] the slug, with no leading or trailing "/"
|
|
119
|
+
# @raise [Errors::InvalidInputError] when a String is not a
|
|
120
|
+
# canonical pubid-scheme URN
|
|
121
|
+
def to_slug(source)
|
|
122
|
+
urn = source.respond_to?(:to_pubid_urn) ? source.to_pubid_urn : source
|
|
123
|
+
parse(urn) unless source.respond_to?(:to_pubid_urn)
|
|
124
|
+
urn.delete_prefix(PREFIX).tr(":", "/")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# The slug parse side: "/"-joined segments rebuild the URN and
|
|
128
|
+
# parse through the canonical gate, so a slug of a landed flavor
|
|
129
|
+
# round-trips by +to_hash+ exactly like its URN.
|
|
130
|
+
#
|
|
131
|
+
# @param string [String] the slug
|
|
132
|
+
# @return [Identifier]
|
|
133
|
+
# @raise [Errors::InvalidInputError] when +string+ is not a String
|
|
134
|
+
# or is longer than MAX_LENGTH
|
|
135
|
+
# @raise [Errors::UrnParseError] for a malformed, unknown or
|
|
136
|
+
# non-canonical slug
|
|
137
|
+
def from_slug(string)
|
|
138
|
+
unless string.is_a?(String)
|
|
139
|
+
raise Errors::InvalidInputError, Pubid::INPUT_NOT_A_STRING_MESSAGE
|
|
140
|
+
end
|
|
141
|
+
raise Errors::InvalidInputError,
|
|
142
|
+
"slug exceeds #{MAX_LENGTH} characters" if string.length > MAX_LENGTH
|
|
143
|
+
raise Errors::UrnParseError, "slug has empty segments" if
|
|
144
|
+
string.split("/", -1).any?(&:empty?)
|
|
145
|
+
|
|
146
|
+
parse("#{PREFIX}#{string.tr('/', ':')}")
|
|
147
|
+
end
|
|
148
|
+
|
|
109
149
|
# @return [FlavorSchema, nil]
|
|
110
150
|
def flavor_schema(flavor)
|
|
111
151
|
return @schemas[flavor] if @schemas.key?(flavor)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# SAE flavor notes
|
|
2
|
+
|
|
3
|
+
## Source of the codes: SAE's Crossref deposits
|
|
4
|
+
|
|
5
|
+
relaton indexes every SAE standard from Crossref (prefix `10.4271`, type
|
|
6
|
+
`standard`). The DOI suffix is SAE's own edition code, lower-case and without
|
|
7
|
+
hyphens: `10.4271/j3016_202104` is `SAE J3016_202104`, `10.4271/amsqqa250_4b`
|
|
8
|
+
is `SAE AMS-QQ-A-250/4B`. relaton upper-cases the suffix and prefixes `SAE `,
|
|
9
|
+
so pubid must read the hyphen-free spelling as well as the printed one.
|
|
10
|
+
|
|
11
|
+
Measured on the full deposit list (2026-11-04, 47,692 DOIs): **46,726
|
|
12
|
+
parse**, and every parsed code round-trips through `to_s`, `from_hash` and
|
|
13
|
+
the URN, with **0 `to_hash` collisions**. The 966 that do not parse are:
|
|
14
|
+
|
|
15
|
+
- **791 CP\*** (`CPGM`, `CPFD`, `CPKW`, …): collected-paper products, not
|
|
16
|
+
standards. Out of scope by decision.
|
|
17
|
+
- **175 one-offs**, none more than 32 of a kind: `SUP1` supplements
|
|
18
|
+
(`AS4468SUP1A`, `ARP4737SUP1`), language tails (`AS9101F_PTBR`,
|
|
19
|
+
`IA9137_DEDE`), `J2534-2/10_5_00`, `_4Q2012`, type-less `SAE6857`,
|
|
20
|
+
`_MMYYYY` dates (`CS1939_042012`, `J2012DA_012013` — the builder's
|
|
21
|
+
month check refuses the J ones, which would otherwise read as year 120,
|
|
22
|
+
month 13), `ISO/SAE21434`, `IEEE/EIA12207`, and ~30 families of 1–8
|
|
23
|
+
documents whose printed form is unknown (`HEB`, `EQB`, `TAHB`, `AMSLP`,
|
|
24
|
+
`AMSHHG`, …). Add a family by adding it to `Components::Type::TYPES` and
|
|
25
|
+
re-baking the grammar (below).
|
|
26
|
+
|
|
27
|
+
## Model
|
|
28
|
+
|
|
29
|
+
| attribute | example | `===` |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `type.abbr` (printed form) | `AMS`, `AMS-QQ-A`, `GEIA-STD` | stated |
|
|
32
|
+
| `number` | `4121`, `0001`, `1939DA`, `174M`, `1939-71` | stated |
|
|
33
|
+
| `part` | `2` of `J2337/2`, `H1025` of `AMS5622/H1025` | strict |
|
|
34
|
+
| `revision` | `J` of `AMS4121J`, `6` of `USCAR2-6` | wildcard |
|
|
35
|
+
| `amendment` | `1` of `AS5259/7A_A1` | strict |
|
|
36
|
+
| `date` (`year`, `month`) | `J3016_202104` | month is a wildcard |
|
|
37
|
+
|
|
38
|
+
The grammar only segments the code; `builder.rb` decides what each segment
|
|
39
|
+
means, because the meaning depends on the family:
|
|
40
|
+
|
|
41
|
+
- **A one-letter tail is the revision** (`AMS4121J`, `AS22520/45A`), so every
|
|
42
|
+
revision of a document shares one `number` bucket and `SAE AMS4121 ===`
|
|
43
|
+
each of them. relaton then picks the highest revision.
|
|
44
|
+
- **Except the `M` of a J or JA report**, which marks the metric document
|
|
45
|
+
(`J174M`) and stays in `number`. AMS revisions do reach `M` (`AMS2400M`),
|
|
46
|
+
so the exception is per family (`Type::METRIC_SUFFIX`).
|
|
47
|
+
- **A longer tail is another document** (`J1939DA` digital annex,
|
|
48
|
+
`J2735SET`) and stays in `number`, in its own bucket.
|
|
49
|
+
- **A dash number is part of `number`** (`J1939-71`, `EIA649-2`; the DOI
|
|
50
|
+
writes some of them `_1`: `EIA649_1A` → `EIA-649-1A`). For USCAR the dash
|
|
51
|
+
is the revision (`USCAR2-6` is USCAR-2 Revision 6, `Type::DASH_REVISION`).
|
|
52
|
+
- **`part` and `amendment` are `subset_strict`**: `SAE J2337` does not match
|
|
53
|
+
`SAE J2337/2`, and `SAE AS5259/7A` does not match `SAE AS5259/7A_A1`.
|
|
54
|
+
|
|
55
|
+
Rendering follows SAE: no space after the type (`SAE J3016`, the old
|
|
56
|
+
`SAE J 3016` still parses), a hyphen after a family that kept a military or
|
|
57
|
+
industry series hyphen (`Type::TYPES` holds the separator), `_YYYYMM` when
|
|
58
|
+
the month is known and `:YYYY` otherwise. The amendment tail `_A1` has no
|
|
59
|
+
known printed form and keeps the DOI's.
|
|
60
|
+
|
|
61
|
+
## The grammar is baked here from a hand-off source
|
|
62
|
+
|
|
63
|
+
`data/parg/sae.json` was compiled with the pinned parsanol from the source
|
|
64
|
+
recorded in hand-off `pubid__pubid-grammar__sae-crossref-coverage` (the same
|
|
65
|
+
text sits in the artifact's `source` field). Until pubid-grammar lands it,
|
|
66
|
+
`pubid-grammar/grammars/sae.parg` is the OLD grammar, so `parsanol parg
|
|
67
|
+
check` against it reports stale. To change the grammar, extract the source
|
|
68
|
+
(`jq -r .source data/parg/sae.json`), edit, and re-bake:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bundle exec parsanol parg compile sae.parg -o data/parg/sae.json
|
|
72
|
+
bundle exec parsanol parg test data/parg/sae.json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The `doc_type` alternatives are the `Type::TYPES` keys plus their
|
|
76
|
+
hyphen-free spellings, longest first (PEG commits to the first match, so
|
|
77
|
+
`AMS` comes after every `AMS…`). `spec/pubid/sae/type_table_spec.rb` fails
|
|
78
|
+
when the two lists drift or an alternative shadows a longer one.
|
data/lib/pubid/sae/builder.rb
CHANGED
|
@@ -2,31 +2,109 @@
|
|
|
2
2
|
|
|
3
3
|
module Pubid
|
|
4
4
|
module Sae
|
|
5
|
+
# Builds a Pubid::Sae::Identifier from the grammar's bound captures.
|
|
6
|
+
#
|
|
7
|
+
# The grammar only segments the code (data/parg/sae.json):
|
|
8
|
+
# +type number [dash] [tail] [/part [part_tail]] [_A amendment] [date]+.
|
|
9
|
+
# The meaning of each segment is decided here, because it depends on the
|
|
10
|
+
# family:
|
|
11
|
+
#
|
|
12
|
+
# * +type+ is stored in its printed form (+AMSQQA+ -> +AMS-QQ-A+).
|
|
13
|
+
# * A one-letter +tail+ or +part_tail+ is the revision (+AMS4121J+,
|
|
14
|
+
# +AS22520/45A+), except the +M+ of a J or JA report, which marks the
|
|
15
|
+
# metric document (+J174M+) and stays in the number.
|
|
16
|
+
# * A longer tail names another document (+J1939DA+, the digital annex;
|
|
17
|
+
# +J2735SET+) and stays in the number, so it keeps its own index bucket.
|
|
18
|
+
# * A +dash+ is a USCAR revision (+USCAR2-6+); in every other family it
|
|
19
|
+
# is part of the number (+J1939-71+, +EIA649-2+).
|
|
5
20
|
class Builder < Pubid::Builder::Base
|
|
6
21
|
def self.build(parsed_data)
|
|
7
22
|
new.build(parsed_data)
|
|
8
23
|
end
|
|
9
24
|
|
|
25
|
+
def build(data)
|
|
26
|
+
data = flatten_array(data) if data.is_a?(Array)
|
|
27
|
+
data = data.transform_keys(&:to_sym)
|
|
28
|
+
Identifier.new(**attributes_for(data))
|
|
29
|
+
end
|
|
30
|
+
|
|
10
31
|
private
|
|
11
32
|
|
|
12
33
|
def default_identifier_class
|
|
13
34
|
Identifier
|
|
14
35
|
end
|
|
15
36
|
|
|
16
|
-
def
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
def attributes_for(data)
|
|
38
|
+
abbr = Components::Type.canonical(data[:type])
|
|
39
|
+
attrs = { type: Components::Type.new(abbr: abbr),
|
|
40
|
+
number: data[:number].to_s }
|
|
41
|
+
apply_dash(attrs, abbr, data[:dash])
|
|
42
|
+
apply_tail(attrs, abbr, data[:tail], part: data[:part])
|
|
43
|
+
apply_part(attrs, data[:part], data[:part_tail])
|
|
44
|
+
attrs[:amendment] = data[:amendment].to_s if data[:amendment]
|
|
45
|
+
attrs[:date] = date_for(data) if data[:year]
|
|
46
|
+
attrs
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def apply_dash(attrs, abbr, dash)
|
|
50
|
+
return unless dash
|
|
51
|
+
|
|
52
|
+
if Components::Type::DASH_REVISION.include?(abbr)
|
|
53
|
+
attrs[:revision] = dash.to_s
|
|
54
|
+
else
|
|
55
|
+
attrs[:number] += "-#{dash}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# A one-letter tail before a part (AMS4121J/2) or after a USCAR dash
|
|
60
|
+
# revision (USCAR2-6A) has no place in the model: no SAE code has that
|
|
61
|
+
# shape, and storing it in `number` would split the index bucket and
|
|
62
|
+
# print a string that does not parse back. Such input is refused.
|
|
63
|
+
def apply_tail(attrs, abbr, tail, part:)
|
|
64
|
+
return unless tail
|
|
65
|
+
|
|
66
|
+
tail = tail.to_s
|
|
67
|
+
if !revision_letter?(abbr, tail)
|
|
68
|
+
attrs[:number] += tail
|
|
69
|
+
elsif part || attrs[:revision]
|
|
70
|
+
refuse("a revision letter #{tail} before a part or after a " \
|
|
71
|
+
"dash revision")
|
|
72
|
+
else
|
|
73
|
+
attrs[:revision] = tail
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def apply_part(attrs, part, part_tail)
|
|
78
|
+
return unless part
|
|
79
|
+
|
|
80
|
+
attrs[:part] = part.to_s
|
|
81
|
+
return unless part_tail
|
|
82
|
+
|
|
83
|
+
if part_tail.to_s.length == 1
|
|
84
|
+
attrs[:revision] = part_tail.to_s
|
|
26
85
|
else
|
|
27
|
-
|
|
86
|
+
attrs[:part] += part_tail.to_s
|
|
28
87
|
end
|
|
29
88
|
end
|
|
89
|
+
|
|
90
|
+
def revision_letter?(abbr, tail)
|
|
91
|
+
return false unless tail.length == 1
|
|
92
|
+
|
|
93
|
+
!(tail == "M" && Components::Type::METRIC_SUFFIX.include?(abbr))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def date_for(data)
|
|
97
|
+
month = data[:month]&.to_i
|
|
98
|
+
refuse("month #{data[:month]}") if month && !(1..12).cover?(month)
|
|
99
|
+
|
|
100
|
+
Components::Date.new(year: data[:year].to_i, month: month)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def refuse(what)
|
|
104
|
+
raise Pubid::Errors::ParseError.new(
|
|
105
|
+
"SAE identifier with #{what} is not supported", nil, flavor: "sae"
|
|
106
|
+
)
|
|
107
|
+
end
|
|
30
108
|
end
|
|
31
109
|
end
|
|
32
110
|
end
|
|
@@ -5,12 +5,18 @@ require "lutaml/model"
|
|
|
5
5
|
module Pubid
|
|
6
6
|
module Sae
|
|
7
7
|
module Components
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# The edition date of an SAE document.
|
|
9
|
+
#
|
|
10
|
+
# SAE dates a J report by year and month: its edition code is
|
|
11
|
+
# +J3016_202104+, the April 2021 edition. A reference may state the
|
|
12
|
+
# year alone (+SAE J3016:2021+); the month is then nil, and nil is a
|
|
13
|
+
# wildcard under +===+, so the year-only reference matches every
|
|
14
|
+
# edition of that year.
|
|
10
15
|
class Date < Lutaml::Model::Serializable
|
|
11
16
|
include ::Pubid::SubsetMatch
|
|
12
17
|
|
|
13
18
|
attribute :year, :integer
|
|
19
|
+
attribute :month, :integer
|
|
14
20
|
|
|
15
21
|
def present?
|
|
16
22
|
!year.nil?
|
|
@@ -20,8 +26,11 @@ module Pubid
|
|
|
20
26
|
render
|
|
21
27
|
end
|
|
22
28
|
|
|
29
|
+
# +202104+ with a month (SAE's edition code), +2021+ without.
|
|
23
30
|
def render(context: nil)
|
|
24
|
-
year.to_s
|
|
31
|
+
return year.to_s unless month
|
|
32
|
+
|
|
33
|
+
format("%<year>04d%<month>02d", year: year, month: month)
|
|
25
34
|
end
|
|
26
35
|
end
|
|
27
36
|
end
|
|
@@ -5,13 +5,65 @@ require "lutaml/model"
|
|
|
5
5
|
module Pubid
|
|
6
6
|
module Sae
|
|
7
7
|
module Components
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# The SAE document family: the letters before the number.
|
|
9
|
+
#
|
|
10
|
+
# +abbr+ holds the printed form (+AMS-QQ-A+). SAE's DOI suffixes drop
|
|
11
|
+
# the hyphens (+10.4271/amsqqa250_4b+), so the parser reads both
|
|
12
|
+
# spellings and the builder stores the printed one (see
|
|
13
|
+
# +.canonical+).
|
|
10
14
|
class Type < Lutaml::Model::Serializable
|
|
11
15
|
include ::Pubid::SubsetMatch
|
|
12
16
|
|
|
17
|
+
# Printed form => the separator SAE prints between it and the number.
|
|
18
|
+
# A family transferred from a military or industry series keeps that
|
|
19
|
+
# series' hyphen (+AMS-QQ-A-250+, +GEIA-STD-0001+, +EIA-649+); the
|
|
20
|
+
# SAE-native families print the number glued (+AMS4121J+, +J3016+).
|
|
21
|
+
# The grammar's +doc_type+ alternatives list exactly these keys plus
|
|
22
|
+
# their hyphen-free spellings; +spec/pubid/sae/type_table_spec.rb+
|
|
23
|
+
# keeps the two in step.
|
|
24
|
+
TYPES = {
|
|
25
|
+
"AMS" => "", "AS" => "", "J" => "", "ARP" => "", "AIR" => "",
|
|
26
|
+
"MA" => "", "MAM" => "", "MAP" => "", "JA" => "", "MS" => "",
|
|
27
|
+
"ARD" => "", "TSB" => "", "CMB" => "", "SSB" => "",
|
|
28
|
+
"CRP" => "-", "USCAR" => "-",
|
|
29
|
+
"EIA" => "-", "EIA-IS" => "-", "EIA-STD" => "-",
|
|
30
|
+
"GEIA" => "-", "GEIA-STD" => "-", "GEIA-HB" => "-", "GEIA-OP" => "-",
|
|
31
|
+
"GEIA-TB" => "-",
|
|
32
|
+
"AMS-STD" => "-", "AMS-DTL" => "-", "AMS-WW-T" => "-",
|
|
33
|
+
"AMS-QQ-A" => "-", "AMS-QQ-C" => "-", "AMS-QQ-N" => "-",
|
|
34
|
+
"AMS-QQ-P" => "-", "AMS-QQ-S" => "-", "AMS-QQ-W" => "-",
|
|
35
|
+
"AMS-PRF" => "-",
|
|
36
|
+
"AMS-A" => "-", "AMS-B" => "-", "AMS-C" => "-", "AMS-D" => "-",
|
|
37
|
+
"AMS-F" => "-",
|
|
38
|
+
"AMS-G" => "-", "AMS-H" => "-", "AMS-I" => "-", "AMS-L" => "-",
|
|
39
|
+
"AMS-M" => "-", "AMS-P" => "-", "AMS-R" => "-", "AMS-S" => "-",
|
|
40
|
+
"AMS-T" => "-", "AMS-W" => "-"
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
# Hyphen-free spelling => printed form.
|
|
44
|
+
CANONICAL = TYPES.keys.to_h { |abbr| [abbr.delete("-"), abbr] }.freeze
|
|
45
|
+
|
|
46
|
+
# The families whose revision is a dash number (+USCAR2-6+ is
|
|
47
|
+
# USCAR-2 Revision 6), not a letter.
|
|
48
|
+
DASH_REVISION = %w[USCAR].freeze
|
|
49
|
+
|
|
50
|
+
# The families whose trailing +M+ marks the metric edition of a
|
|
51
|
+
# document (+J174M+), not a revision.
|
|
52
|
+
METRIC_SUFFIX = %w[J JA].freeze
|
|
53
|
+
|
|
13
54
|
attribute :abbr, :string
|
|
14
55
|
|
|
56
|
+
# @param spelling [String] +AMSQQA+, +AMS-QQ-A+ or +ams-qq-a+
|
|
57
|
+
# @return [String, nil] the printed form, nil for an unknown family
|
|
58
|
+
def self.canonical(spelling)
|
|
59
|
+
CANONICAL[spelling.to_s.upcase.delete("-")]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The separator printed between this family and its number.
|
|
63
|
+
def separator
|
|
64
|
+
TYPES.fetch(abbr.to_s, "")
|
|
65
|
+
end
|
|
66
|
+
|
|
15
67
|
def to_s
|
|
16
68
|
render
|
|
17
69
|
end
|
|
@@ -4,7 +4,7 @@ module Pubid
|
|
|
4
4
|
module Sae
|
|
5
5
|
# Base SAE Identifier. Canonical name Pubid::Sae::Identifier. SAE has a single identifier class that IS the
|
|
6
6
|
# document type, so this class is both the base and the only concrete type.
|
|
7
|
-
# Handles
|
|
7
|
+
# Handles every SAE document family (Components::Type::TYPES).
|
|
8
8
|
class Identifier < ::Pubid::Identifier
|
|
9
9
|
def self.parse(input)
|
|
10
10
|
unless input.is_a?(String)
|
|
@@ -26,8 +26,19 @@ module Pubid
|
|
|
26
26
|
# a bare alias of the shared component and never carried more than
|
|
27
27
|
# `value`, so this is a representation change only.
|
|
28
28
|
attribute :number, :string
|
|
29
|
+
# The revision letter (AMS4121J -> "J"), or USCAR's revision number
|
|
30
|
+
# (USCAR2-6 -> "6"). Kept out of `number`, so every revision of a
|
|
31
|
+
# document shares one index bucket and a reference without a revision
|
|
32
|
+
# matches them all under `===`.
|
|
33
|
+
attribute :revision, :string
|
|
34
|
+
# The amendment of a revision: AS5259/7A_A1 -> "1".
|
|
35
|
+
attribute :amendment, :string
|
|
29
36
|
attribute :date, Sae::Components::Date
|
|
30
37
|
|
|
38
|
+
# A part (J2337/2) and an amendment (_A1) are other documents: a
|
|
39
|
+
# reference without one names the document that has none.
|
|
40
|
+
subset_strict :part, :amendment
|
|
41
|
+
|
|
31
42
|
def self.type
|
|
32
43
|
{ key: :base, web: :standard, title: "Standard", short: "SAE" }
|
|
33
44
|
end
|
|
@@ -44,6 +55,48 @@ module Pubid
|
|
|
44
55
|
def mr_type
|
|
45
56
|
type&.abbr&.to_s&.downcase
|
|
46
57
|
end
|
|
58
|
+
|
|
59
|
+
# The code after the type and its separator, without the date:
|
|
60
|
+
# "4121J", "22520/45A", "5259/7A_A1", "2-6" (USCAR-2 Revision 6).
|
|
61
|
+
# The renderer prints it after the type; the URN carries it as one
|
|
62
|
+
# segment.
|
|
63
|
+
def designation
|
|
64
|
+
dash = dash_revision?
|
|
65
|
+
code = number.to_s
|
|
66
|
+
code += "-#{revision}" if revision && dash
|
|
67
|
+
code += "/#{part}" if part
|
|
68
|
+
code += revision.to_s if revision && !dash
|
|
69
|
+
code += "_A#{amendment}" if amendment
|
|
70
|
+
code
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# A revision after a part (AS22520/45A) is the part document's
|
|
74
|
+
# revision, so dropping the part drops it too. Without this,
|
|
75
|
+
# `exclude(:part)` would turn it into revision A of the base document
|
|
76
|
+
# (AS22520A) and `matches?(other, ignore: [:part])` would pair the two.
|
|
77
|
+
def exclude(*args)
|
|
78
|
+
args += [:revision] if part && args.include?(:part)
|
|
79
|
+
super
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# A numbered revision of a USCAR family (USCAR-2-6). A letter revision
|
|
83
|
+
# (USCAR3A) prints glued, as in every other family.
|
|
84
|
+
def dash_revision?
|
|
85
|
+
Sae::Components::Type::DASH_REVISION.include?(type&.abbr.to_s) &&
|
|
86
|
+
revision.to_s.match?(/\A\d+\z/)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Each segment carries a letter, so a dash number (J1939-71 ->
|
|
90
|
+
# "1939-71") never collides with a part (J1939/71 -> "1939-p71").
|
|
91
|
+
def mr_number_with_part
|
|
92
|
+
return nil unless number
|
|
93
|
+
|
|
94
|
+
segments = [number]
|
|
95
|
+
segments << "p#{part}" if part
|
|
96
|
+
segments << "r#{revision}" if revision
|
|
97
|
+
segments << "a#{amendment}" if amendment
|
|
98
|
+
segments.join("-").downcase
|
|
99
|
+
end
|
|
47
100
|
end
|
|
48
101
|
|
|
49
102
|
end
|
data/lib/pubid/sae/renderer.rb
CHANGED
|
@@ -2,34 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
module Pubid
|
|
4
4
|
module Sae
|
|
5
|
-
# Human-readable renderer for SAE identifiers
|
|
5
|
+
# Human-readable renderer for SAE identifiers, in the form SAE prints:
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
# "SAE
|
|
9
|
-
# "SAE
|
|
10
|
-
# "SAE
|
|
7
|
+
# "SAE J3016" "SAE AMS4121J"
|
|
8
|
+
# "SAE J3016_202104" "SAE ARP4754A:2010"
|
|
9
|
+
# "SAE AS22520/45A" "SAE AMS-QQ-A-250/4B"
|
|
10
|
+
# "SAE AS5259/7A_A1" "SAE USCAR-2-6"
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
12
|
+
# A dated edition with a month renders SAE's native edition code
|
|
13
|
+
# (+_YYYYMM+); a year alone renders +:YYYY+.
|
|
14
14
|
class Renderer < ::Pubid::Renderers::Base
|
|
15
15
|
def render(context: nil, **_opts)
|
|
16
16
|
id = @id
|
|
17
|
+
type = id.type
|
|
18
|
+
code = type ? "#{type.abbr}#{type.separator}" : ""
|
|
19
|
+
result = [id.publisher, code + id.designation].compact.join(" ")
|
|
20
|
+
result + date_suffix(id, context)
|
|
21
|
+
end
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# Publisher and type
|
|
21
|
-
parts << id.publisher if id.publisher
|
|
22
|
-
parts << id.type.to_s if id.type
|
|
23
|
-
|
|
24
|
-
# Number
|
|
25
|
-
parts << id.number.to_s if id.number
|
|
26
|
-
|
|
27
|
-
result = parts.join(" ")
|
|
23
|
+
private
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
def date_suffix(id, context)
|
|
26
|
+
return "" unless id.date&.year
|
|
31
27
|
|
|
32
|
-
|
|
28
|
+
separator = id.date.month ? "_" : ":"
|
|
29
|
+
"#{separator}#{id.date.render(context:)}"
|
|
33
30
|
end
|
|
34
31
|
end
|
|
35
32
|
end
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module Pubid
|
|
4
4
|
module Sae
|
|
5
|
+
# urn:sae:{type|std}:{designation}[:{yyyy|yyyymm}]
|
|
6
|
+
#
|
|
7
|
+
# The designation is the printed code after the type (Identifier#
|
|
8
|
+
# designation): "4121J", "22520/45A", "5259/7A_A1". The date segment is
|
|
9
|
+
# the year, or SAE's year-and-month edition date.
|
|
5
10
|
class UrnGenerator < Pubid::UrnGenerator::Base
|
|
6
11
|
def urn_type
|
|
7
12
|
return "std" unless identifier.type
|
|
@@ -12,7 +17,7 @@ module Pubid
|
|
|
12
17
|
def urn_number
|
|
13
18
|
return nil unless identifier.number
|
|
14
19
|
|
|
15
|
-
identifier.
|
|
20
|
+
identifier.designation
|
|
16
21
|
end
|
|
17
22
|
|
|
18
23
|
def generate
|
data/lib/pubid/sae/urn_parser.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Pubid
|
|
|
5
5
|
# Parses SAE URNs back into identifiers.
|
|
6
6
|
#
|
|
7
7
|
# UrnGenerator emits:
|
|
8
|
-
# urn:sae:{type|std}:{
|
|
8
|
+
# urn:sae:{type|std}:{designation}[:{yyyy|yyyymm}]
|
|
9
9
|
# where slot[1] is overwritten with the publisher string when a publisher
|
|
10
10
|
# is set. The common single-publisher case keeps slot[1] = "sae".
|
|
11
11
|
#
|
|
@@ -25,10 +25,15 @@ module Pubid
|
|
|
25
25
|
|
|
26
26
|
private
|
|
27
27
|
|
|
28
|
+
# The designation follows the type with no separator; the grammar
|
|
29
|
+
# reads a glued number for every family. A six-digit date segment is
|
|
30
|
+
# SAE's year-and-month edition date.
|
|
28
31
|
def build_text(type_token, number, year)
|
|
29
32
|
head = type_token == "std" ? "" : type_token.upcase
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
# The designation's letters are upper-case in every SAE code; a URN
|
|
34
|
+
# that a consumer lower-cased (urn:sae:ams:4121j) still resolves.
|
|
35
|
+
text = "SAE #{head}#{number.to_s.upcase}"
|
|
36
|
+
text += "#{year.to_s.length == 6 ? '_' : ':'}#{year}" if year
|
|
32
37
|
text
|
|
33
38
|
end
|
|
34
39
|
end
|
data/lib/pubid/version.rb
CHANGED
data/lib/pubid.rb
CHANGED
|
@@ -300,6 +300,26 @@ module Pubid
|
|
|
300
300
|
urn_parser.parse(string)
|
|
301
301
|
end
|
|
302
302
|
|
|
303
|
+
# The slug form (relaton#441): the pubid-scheme URN of +source+ in
|
|
304
|
+
# HTTP-path clothes — "/"-joined segments after the "urn:pubid:"
|
|
305
|
+
# prefix ("iso/is/9001/year-2015"). Bijective with
|
|
306
|
+
# {PubidUrn.from_slug}.
|
|
307
|
+
#
|
|
308
|
+
# @param source [Identifier, String] an identifier or its pubid-scheme URN
|
|
309
|
+
# @return [String]
|
|
310
|
+
def self.to_slug(source)
|
|
311
|
+
PubidUrn.to_slug(source)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Parse a slug (see {PubidUrn.from_slug}).
|
|
315
|
+
#
|
|
316
|
+
# @param string [String] the slug
|
|
317
|
+
# @return [Identifier]
|
|
318
|
+
def self.from_slug(string)
|
|
319
|
+
eager_load_flavors!
|
|
320
|
+
PubidUrn.from_slug(string)
|
|
321
|
+
end
|
|
322
|
+
|
|
303
323
|
# Parse a pubid-scheme URN, and only that: an official URN such as
|
|
304
324
|
# "urn:iso:std:…" raises. For a consumer that takes a slug and must not
|
|
305
325
|
# accept a lossy official URN by mistake (relaton/relaton#441).
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pubid
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-11-
|
|
11
|
+
date: 2026-11-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -361,6 +361,7 @@ files:
|
|
|
361
361
|
- lib/pubid/cen_cenelec/identifiers/technical_report.rb
|
|
362
362
|
- lib/pubid/cen_cenelec/identifiers/technical_specification.rb
|
|
363
363
|
- lib/pubid/cen_cenelec/parser.rb
|
|
364
|
+
- lib/pubid/cen_cenelec/pubid_urn_schema.rb
|
|
364
365
|
- lib/pubid/cen_cenelec/renderer.rb
|
|
365
366
|
- lib/pubid/cen_cenelec/single_identifier.rb
|
|
366
367
|
- lib/pubid/cen_cenelec/urn_generator.rb
|
|
@@ -479,6 +480,7 @@ files:
|
|
|
479
480
|
- lib/pubid/etsi/identifiers/etsi_standard.rb
|
|
480
481
|
- lib/pubid/etsi/identifiers/supplement_identifier.rb
|
|
481
482
|
- lib/pubid/etsi/parser.rb
|
|
483
|
+
- lib/pubid/etsi/pubid_urn_schema.rb
|
|
482
484
|
- lib/pubid/etsi/renderer.rb
|
|
483
485
|
- lib/pubid/etsi/urn_generator.rb
|
|
484
486
|
- lib/pubid/etsi/urn_parser.rb
|
|
@@ -521,6 +523,7 @@ files:
|
|
|
521
523
|
- lib/pubid/gost/identifiers/interstate_standard.rb
|
|
522
524
|
- lib/pubid/gost/identifiers/national_standard.rb
|
|
523
525
|
- lib/pubid/gost/parser.rb
|
|
526
|
+
- lib/pubid/gost/pubid_urn_schema.rb
|
|
524
527
|
- lib/pubid/gost/renderer.rb
|
|
525
528
|
- lib/pubid/gost/urn_generator.rb
|
|
526
529
|
- lib/pubid/gost/urn_parser.rb
|
|
@@ -943,6 +946,7 @@ files:
|
|
|
943
946
|
- lib/pubid/parsers/urn.rb
|
|
944
947
|
- lib/pubid/pg.rb
|
|
945
948
|
- lib/pubid/plateau.rb
|
|
949
|
+
- lib/pubid/plateau/CLAUDE.md
|
|
946
950
|
- lib/pubid/plateau/builder.rb
|
|
947
951
|
- lib/pubid/plateau/identifier.rb
|
|
948
952
|
- lib/pubid/plateau/identifiers.rb
|
|
@@ -951,6 +955,7 @@ files:
|
|
|
951
955
|
- lib/pubid/plateau/identifiers/handbook.rb
|
|
952
956
|
- lib/pubid/plateau/identifiers/technical_report.rb
|
|
953
957
|
- lib/pubid/plateau/parser.rb
|
|
958
|
+
- lib/pubid/plateau/pubid_urn_schema.rb
|
|
954
959
|
- lib/pubid/plateau/renderer.rb
|
|
955
960
|
- lib/pubid/plateau/supplement_identifier.rb
|
|
956
961
|
- lib/pubid/plateau/urn_generator.rb
|
|
@@ -982,6 +987,7 @@ files:
|
|
|
982
987
|
- lib/pubid/rendering/stage.rb
|
|
983
988
|
- lib/pubid/rendering/supplement.rb
|
|
984
989
|
- lib/pubid/sae.rb
|
|
990
|
+
- lib/pubid/sae/CLAUDE.md
|
|
985
991
|
- lib/pubid/sae/builder.rb
|
|
986
992
|
- lib/pubid/sae/components.rb
|
|
987
993
|
- lib/pubid/sae/components/code.rb
|