pubid 2.0.8 → 2.0.10
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/adobe/pubid_urn_schema.rb +14 -0
- data/lib/pubid/adobe.rb +1 -0
- data/lib/pubid/api/pubid_urn_schema.rb +2 -0
- data/lib/pubid/calconnect/pubid_urn_schema.rb +20 -0
- data/lib/pubid/calconnect.rb +1 -0
- data/lib/pubid/ccsds/pubid_urn_schema.rb +22 -0
- data/lib/pubid/ccsds.rb +1 -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/easc/pubid_urn_schema.rb +20 -0
- data/lib/pubid/easc.rb +1 -0
- data/lib/pubid/ecma/pubid_urn_schema.rb +21 -0
- data/lib/pubid/ecma.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/evs/pubid_urn_schema.rb +35 -0
- data/lib/pubid/evs.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/iala/pubid_urn_schema.rb +30 -0
- data/lib/pubid/iala.rb +1 -0
- data/lib/pubid/iana/pubid_urn_schema.rb +17 -0
- data/lib/pubid/iana.rb +1 -0
- data/lib/pubid/identifier.rb +7 -1
- data/lib/pubid/iec/pubid_urn_schema.rb +11 -4
- data/lib/pubid/oasis/pubid_urn_schema.rb +21 -0
- data/lib/pubid/oasis.rb +1 -0
- data/lib/pubid/ogc/pubid_urn_schema.rb +18 -0
- data/lib/pubid/ogc.rb +1 -0
- 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/w3c/pubid_urn_schema.rb +27 -0
- data/lib/pubid/w3c.rb +1 -0
- data/lib/pubid/xsf/pubid_urn_schema.rb +14 -0
- data/lib/pubid/xsf.rb +1 -0
- data/lib/pubid.rb +20 -0
- metadata +20 -2
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Oasis
|
|
5
|
+
# The pubid-scheme URN of oasis (issue #656; docs/URN-SCHEMES.adoc).
|
|
6
|
+
#
|
|
7
|
+
# Declared from the doc's section: the atom, facet and type tables
|
|
8
|
+
# below are the doc's design, conforming token-for-token.
|
|
9
|
+
PubidUrnSchema = Pubid::PubidUrn.define("oasis") do
|
|
10
|
+
atom "part", "part"
|
|
11
|
+
atom "version", "v"
|
|
12
|
+
atom "stage", "stage"
|
|
13
|
+
atom "label", "label"
|
|
14
|
+
atom "original", "original"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
type "pubid:oasis:identifier", "id"
|
|
18
|
+
type "pubid:oasis:standard", "std"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/pubid/oasis.rb
CHANGED
|
@@ -17,6 +17,7 @@ module Pubid
|
|
|
17
17
|
autoload :Renderer, "#{__dir__}/oasis/renderer"
|
|
18
18
|
autoload :UrnGenerator, "#{__dir__}/oasis/urn_generator"
|
|
19
19
|
autoload :UrnParser, "#{__dir__}/oasis/urn_parser"
|
|
20
|
+
autoload :PubidUrnSchema, "#{__dir__}/oasis/pubid_urn_schema"
|
|
20
21
|
|
|
21
22
|
# Parse an OASIS identifier string
|
|
22
23
|
def self.parse(identifier)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Ogc
|
|
5
|
+
# The pubid-scheme URN of ogc (issue #656; docs/URN-SCHEMES.adoc).
|
|
6
|
+
#
|
|
7
|
+
# Declared from the doc's section: the atom, facet and type tables
|
|
8
|
+
# below are the doc's design, conforming token-for-token.
|
|
9
|
+
PubidUrnSchema = Pubid::PubidUrn.define("ogc") do
|
|
10
|
+
atom "year", "year"
|
|
11
|
+
atom "revision", "rev"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
type "pubid:ogc:document", "document"
|
|
15
|
+
type "pubid:ogc:identifier", "id"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/pubid/ogc.rb
CHANGED
|
@@ -20,6 +20,7 @@ module Pubid
|
|
|
20
20
|
autoload :Renderer, "#{__dir__}/ogc/renderer"
|
|
21
21
|
autoload :UrnGenerator, "#{__dir__}/ogc/urn_generator"
|
|
22
22
|
autoload :UrnParser, "#{__dir__}/ogc/urn_parser"
|
|
23
|
+
autoload :PubidUrnSchema, "#{__dir__}/ogc/pubid_urn_schema"
|
|
23
24
|
|
|
24
25
|
# Parse an OGC identifier string
|
|
25
26
|
def self.parse(identifier)
|
|
@@ -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
|