pubid 2.0.5 → 2.0.7
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 +16 -0
- data/data/parg/bipm.json +1 -1
- data/data/parg/bipm.parg +290 -0
- data/lib/pubid/ansi/pubid_urn_schema.rb +44 -0
- data/lib/pubid/ansi.rb +1 -0
- data/lib/pubid/astm/CLAUDE.md +8 -0
- data/lib/pubid/astm/pubid_urn_schema.rb +47 -0
- data/lib/pubid/astm.rb +1 -0
- data/lib/pubid/bipm/CLAUDE.md +2 -0
- data/lib/pubid/bipm/builder.rb +26 -1
- data/lib/pubid/bipm/identifiers/committee_document.rb +13 -1
- data/lib/pubid/bipm/renderer.rb +18 -3
- data/lib/pubid/bipm/urn_generator.rb +14 -1
- data/lib/pubid/bipm/urn_parser.rb +14 -1
- data/lib/pubid/identifier.rb +15 -0
- data/lib/pubid/iec/pubid_urn_schema.rb +32 -0
- data/lib/pubid/iec.rb +1 -0
- data/lib/pubid/ieee/CLAUDE.md +2 -0
- data/lib/pubid/ieee/pubid_urn_schema.rb +150 -0
- data/lib/pubid/ieee.rb +1 -0
- data/lib/pubid/iso/pubid_urn_schema.rb +61 -0
- data/lib/pubid/iso.rb +1 -0
- data/lib/pubid/itu/CLAUDE.md +15 -0
- data/lib/pubid/itu/pubid_urn_schema.rb +82 -0
- data/lib/pubid/itu.rb +1 -0
- data/lib/pubid/nist/CLAUDE.md +13 -0
- data/lib/pubid/nist/pubid_urn_schema.rb +108 -0
- data/lib/pubid/nist.rb +1 -0
- data/lib/pubid/pubid_urn/decoder.rb +156 -0
- data/lib/pubid/pubid_urn/encoder.rb +175 -0
- data/lib/pubid/pubid_urn/escape.rb +60 -0
- data/lib/pubid/pubid_urn/paths.rb +87 -0
- data/lib/pubid/pubid_urn/schema.rb +184 -0
- data/lib/pubid/pubid_urn.rb +226 -0
- data/lib/pubid/type_resolver.rb +12 -0
- data/lib/pubid/version.rb +1 -1
- data/lib/pubid.rb +18 -0
- metadata +16 -2
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module PubidUrn
|
|
5
|
+
# Reads and writes a dotted key path ("edition.number",
|
|
6
|
+
# "languages.*.code") in an identifier hash.
|
|
7
|
+
#
|
|
8
|
+
# +take+ removes what it reads, so after every core path, atom and facet
|
|
9
|
+
# has been taken, anything left in the node is a key that the schema
|
|
10
|
+
# does not place — the encoder refuses it rather than drop it silently.
|
|
11
|
+
#
|
|
12
|
+
# A degenerate component may serialize as a bare scalar ("edition" =>
|
|
13
|
+
# "2" rather than {"number" => "2"}, see FLAT_SCALAR_FIELDS). Where a
|
|
14
|
+
# path expects a Hash and finds a scalar, the scalar IS the value of
|
|
15
|
+
# the path. +put+ always writes the nested form, which +from_hash+ reads
|
|
16
|
+
# as well, so the rebuilt identifier serializes alike.
|
|
17
|
+
module Paths
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# @return [Object, nil] the value, or nil when the path is absent
|
|
21
|
+
def take(container, parts)
|
|
22
|
+
return nil unless container.is_a?(Hash) && container.key?(parts.first)
|
|
23
|
+
|
|
24
|
+
key, *rest = parts
|
|
25
|
+
return container.delete(key) if rest.empty?
|
|
26
|
+
|
|
27
|
+
value = container[key]
|
|
28
|
+
if rest.first == "*"
|
|
29
|
+
take_elements(container, key, value, rest.drop(1))
|
|
30
|
+
elsif value.is_a?(Hash)
|
|
31
|
+
take(value, rest)
|
|
32
|
+
elsif rest.size == 1
|
|
33
|
+
container.delete(key)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def take_elements(container, key, value, rest)
|
|
38
|
+
return nil unless value.is_a?(Array)
|
|
39
|
+
return container.delete(key) if rest.empty?
|
|
40
|
+
|
|
41
|
+
values = value.each_with_index.map do |element, i|
|
|
42
|
+
if element.is_a?(Hash)
|
|
43
|
+
take(element, rest)
|
|
44
|
+
elsif rest.size == 1 && !element.nil?
|
|
45
|
+
value[i] = {}
|
|
46
|
+
element
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
values.all?(&:nil?) ? nil : values
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Write +value+ (nil writes nothing).
|
|
53
|
+
def put(container, parts, value)
|
|
54
|
+
return if value.nil?
|
|
55
|
+
|
|
56
|
+
key, *rest = parts
|
|
57
|
+
if rest.empty?
|
|
58
|
+
container[key] = value
|
|
59
|
+
elsif rest.first == "*"
|
|
60
|
+
put_elements(container, key, value, rest.drop(1))
|
|
61
|
+
else
|
|
62
|
+
put(container[key] ||= {}, rest, value)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def put_elements(container, key, values, rest)
|
|
67
|
+
return container[key] = values if rest.empty?
|
|
68
|
+
|
|
69
|
+
elements = (container[key] ||= [])
|
|
70
|
+
values.each_with_index do |value, i|
|
|
71
|
+
next if value.nil?
|
|
72
|
+
|
|
73
|
+
put(elements[i] ||= {}, rest, value)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# True when nothing but empty containers is left.
|
|
78
|
+
def blank?(value)
|
|
79
|
+
case value
|
|
80
|
+
when Hash then value.each_value.all? { |v| blank?(v) }
|
|
81
|
+
when Array then value.all? { |v| blank?(v) }
|
|
82
|
+
else false
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module PubidUrn
|
|
5
|
+
# One prefixed atom: an optional value, found by its prefix and not by
|
|
6
|
+
# its position ("part-1", "co-IEC,IEEE", "ids-(iso:is:9001)").
|
|
7
|
+
#
|
|
8
|
+
# +path+ is a dotted key path into the node's +to_hash+; "*" walks the
|
|
9
|
+
# elements of an Array ("languages.*.code"). +kind+ is one of
|
|
10
|
+
# :string, :list (of Strings), :bool, :bools (a list of booleans), :int,
|
|
11
|
+
# :id (a nested identifier hash) or :ids (an Array of them). A path with
|
|
12
|
+
# "*" is always a list: :list, or :bools when its elements are booleans.
|
|
13
|
+
Atom = Struct.new(:path, :prefix, :kind) do
|
|
14
|
+
def parts
|
|
15
|
+
@parts ||= path.split(".")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def list?
|
|
19
|
+
LIST_KINDS.key?(kind)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Symbol] the kind of one value (of one element of a list)
|
|
23
|
+
def element
|
|
24
|
+
LIST_KINDS.fetch(kind, kind)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# A list kind => the kind of its elements.
|
|
29
|
+
LIST_KINDS = { list: :string, bools: :bool }.freeze
|
|
30
|
+
|
|
31
|
+
# A spelling-only value (+original_code+, +original_text+ …). Facets are
|
|
32
|
+
# written in the trailing "f" group, so a consumer can cut them off a slug.
|
|
33
|
+
# +kind+ is :string, :list, :bool or :bools, as for an Atom; a spelling
|
|
34
|
+
# flag ("V.25 ter" against "V.25ter") is a :bool facet.
|
|
35
|
+
Facet = Struct.new(:path, :name, :kind) do
|
|
36
|
+
def parts
|
|
37
|
+
@parts ||= path.split(".")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def list?
|
|
41
|
+
LIST_KINDS.key?(kind)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def element
|
|
45
|
+
LIST_KINDS.fetch(kind, kind)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The URN shape of one concrete identifier type.
|
|
50
|
+
class TypeSchema
|
|
51
|
+
attr_reader :flavor, :type, :token, :core, :atoms, :facets
|
|
52
|
+
|
|
53
|
+
def initialize(flavor:, type:, token:, core:, atoms:, facets:)
|
|
54
|
+
@flavor = flavor
|
|
55
|
+
@type = type
|
|
56
|
+
@token = token
|
|
57
|
+
@core = core.map { |path| path.split(".") }.freeze
|
|
58
|
+
@atoms = atoms.freeze
|
|
59
|
+
@facets = facets.freeze
|
|
60
|
+
@atom_index = atoms.each_with_index.to_h { |atom, i| [atom.prefix, i] }
|
|
61
|
+
@facet_names = facets.to_h { |facet| [facet.name, facet] }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [Integer, nil] the canonical position of the atom whose
|
|
65
|
+
# prefix starts +token+, or nil when +token+ is no atom of this type
|
|
66
|
+
def atom_position(token)
|
|
67
|
+
prefix = token[/\A([a-z0-9]+)-/, 1]
|
|
68
|
+
prefix && @atom_index[prefix]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def facet(name)
|
|
72
|
+
@facet_names[name]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The pubid-scheme schema of one flavor: the canonical atom order, the
|
|
77
|
+
# facets, and one TypeSchema for each concrete type.
|
|
78
|
+
#
|
|
79
|
+
# Atoms and facets declared at flavor level apply to every type, in the
|
|
80
|
+
# order of declaration; a type adds its own after them. A type token, an
|
|
81
|
+
# atom prefix and a facet name are a lower-case letter followed by
|
|
82
|
+
# lower-case letters and digits, so none can contain the "-" that ends a
|
|
83
|
+
# prefix or the "." that qualifies a token of another flavor, and each
|
|
84
|
+
# has the shape PubidUrn::STRUCTURE reads as structure.
|
|
85
|
+
class FlavorSchema
|
|
86
|
+
NAME = /\A[a-z][a-z0-9]*\z/
|
|
87
|
+
KINDS = %i[string list bool bools int id ids].freeze
|
|
88
|
+
FACET_KINDS = %i[string list bool bools].freeze
|
|
89
|
+
# "f" opens the facet group; "type" is the type-metadata URN
|
|
90
|
+
# (Pubid::IdentifierMetadata, "urn:pubid:<flavor>:type:<key>").
|
|
91
|
+
RESERVED_TOKENS = %w[f type].freeze
|
|
92
|
+
|
|
93
|
+
attr_reader :flavor, :types
|
|
94
|
+
|
|
95
|
+
def initialize(flavor)
|
|
96
|
+
@flavor = flavor
|
|
97
|
+
@atoms = []
|
|
98
|
+
@facets = []
|
|
99
|
+
@types = {}
|
|
100
|
+
@tokens = {}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def atom(path, prefix, kind: nil)
|
|
104
|
+
@atoms << build_atom(path, prefix, kind)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def facet(path, name, kind: nil)
|
|
108
|
+
@facets << build_facet(path, name, kind)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @param type [String] the +_type+ the class serializes
|
|
112
|
+
# @param token [String] the short type token of the URN
|
|
113
|
+
# @param core [Array<String>] the positional core paths
|
|
114
|
+
# @param atoms [Array<Array>] extra atoms: [path, prefix, kind]
|
|
115
|
+
# @param facets [Array<Array>] extra facets: [path, name, kind]
|
|
116
|
+
def type(type, token, core: %w[number], atoms: [], facets: [])
|
|
117
|
+
check_name!(token, "type token")
|
|
118
|
+
if RESERVED_TOKENS.include?(token) || @tokens.key?(token)
|
|
119
|
+
raise ArgumentError,
|
|
120
|
+
"type token #{token.inspect} is reserved or taken"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
all_atoms = @atoms + atoms.map { |spec| build_atom(*spec) }
|
|
124
|
+
check_unique!(all_atoms.map(&:prefix), "atom prefix", type)
|
|
125
|
+
all_facets = @facets + facets.map { |spec| build_facet(*spec) }
|
|
126
|
+
check_unique!(all_facets.map(&:name), "facet name", type)
|
|
127
|
+
|
|
128
|
+
schema = TypeSchema.new(flavor: flavor, type: type, token: token,
|
|
129
|
+
core: core, atoms: all_atoms,
|
|
130
|
+
facets: all_facets)
|
|
131
|
+
@types[type] = schema
|
|
132
|
+
@tokens[token] = schema
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def for_token(token)
|
|
136
|
+
@tokens[token]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def for_type(type)
|
|
140
|
+
@types[type]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def build_atom(path, prefix, kind = nil)
|
|
146
|
+
check_name!(prefix, "atom prefix")
|
|
147
|
+
Atom.new(path, prefix, check_kind!(path, kind, KINDS, "atom"))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def build_facet(path, name, kind = nil)
|
|
151
|
+
check_name!(name, "facet name")
|
|
152
|
+
Facet.new(path, name, check_kind!(path, kind, FACET_KINDS, "facet"))
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# A path with "*" reads a list, so its kind must be a list kind.
|
|
156
|
+
def check_kind!(path, kind, kinds, what)
|
|
157
|
+
listed = path.split(".").include?("*")
|
|
158
|
+
kind ||= listed ? :list : :string
|
|
159
|
+
unless kinds.include?(kind)
|
|
160
|
+
raise ArgumentError, "unknown #{what} kind #{kind.inspect}"
|
|
161
|
+
end
|
|
162
|
+
if listed && !LIST_KINDS.key?(kind)
|
|
163
|
+
raise ArgumentError, "#{what} #{path.inspect} reads a list, " \
|
|
164
|
+
"so its kind must be one of #{LIST_KINDS.keys}"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
kind
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def check_name!(name, what)
|
|
171
|
+
return if NAME.match?(name)
|
|
172
|
+
|
|
173
|
+
raise ArgumentError, "#{what} #{name.inspect} must match #{NAME.source}"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def check_unique!(names, what, type)
|
|
177
|
+
dup = names.tally.find { |_, count| count > 1 }
|
|
178
|
+
return unless dup
|
|
179
|
+
|
|
180
|
+
raise ArgumentError, "#{what} #{dup.first.inspect} repeats in #{type}"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
# The pubid-scheme URN (issue #656): pubid's own URN namespace, which
|
|
5
|
+
# carries the full typed model and always round-trips —
|
|
6
|
+
# +Pubid.parse_urn(id.to_pubid_urn).to_hash == id.to_hash+.
|
|
7
|
+
#
|
|
8
|
+
# It is a different thing from the official URN (+#to_urn+, e.g.
|
|
9
|
+
# "urn:iso:std:…"), which follows the registered grammar of the
|
|
10
|
+
# organization and may be lossy. relaton uses the pubid-scheme URN as its
|
|
11
|
+
# HTTP path slug (relaton/relaton#441), so the form is also path-safe:
|
|
12
|
+
# no "/", "%", "?", "#" and no empty segment.
|
|
13
|
+
#
|
|
14
|
+
# urn:pubid:iso:is:9001:year-2015:amd:1:year-2016
|
|
15
|
+
#
|
|
16
|
+
# The shape of each identifier type is declared in its flavor's
|
|
17
|
+
# +PubidUrnSchema+ (e.g. +lib/pubid/iso/pubid_urn_schema.rb+), through
|
|
18
|
+
# {define}. A flavor with no schema cannot emit one yet. The design of
|
|
19
|
+
# every flavor is in +docs/URN-SCHEMES.adoc+.
|
|
20
|
+
module PubidUrn
|
|
21
|
+
autoload :Escape, "pubid/pubid_urn/escape"
|
|
22
|
+
autoload :Atom, "pubid/pubid_urn/schema"
|
|
23
|
+
autoload :Facet, "pubid/pubid_urn/schema"
|
|
24
|
+
autoload :TypeSchema, "pubid/pubid_urn/schema"
|
|
25
|
+
autoload :FlavorSchema, "pubid/pubid_urn/schema"
|
|
26
|
+
autoload :Paths, "pubid/pubid_urn/paths"
|
|
27
|
+
autoload :Encoder, "pubid/pubid_urn/encoder"
|
|
28
|
+
autoload :Decoder, "pubid/pubid_urn/decoder"
|
|
29
|
+
|
|
30
|
+
PREFIX = "urn:pubid:"
|
|
31
|
+
# An absent core value in the middle of the core, or an absent element
|
|
32
|
+
# of a list. Distinct from an escape, which always has two hex digits.
|
|
33
|
+
NULL = "~"
|
|
34
|
+
|
|
35
|
+
# A raw segment that reads as structure and not as a core value: a type
|
|
36
|
+
# token or the facet marker ("amd", "f"), an atom ("year-2015"), or a
|
|
37
|
+
# token of another flavor ("iec.is", "3gpp.ts").
|
|
38
|
+
#
|
|
39
|
+
# The rule depends on the SHAPE of the segment only, never on which
|
|
40
|
+
# schemas exist. So a published URN keeps its meaning when a flavor, a
|
|
41
|
+
# type token or an atom is added later. Type tokens, atom prefixes and
|
|
42
|
+
# facet names therefore start with a lower-case letter
|
|
43
|
+
# (FlavorSchema::NAME). Digit-led numbers ("9001", "802.11", "800-53")
|
|
44
|
+
# and upper-case ones ("G.711") never match; a core value that does
|
|
45
|
+
# match gets its first character escaped ("iec.is" -> "~69ec.is").
|
|
46
|
+
STRUCTURE = /
|
|
47
|
+
\A(?:
|
|
48
|
+
[a-z][a-z0-9]*(?:-|\z) # token, "f", atom
|
|
49
|
+
| [a-z0-9]*[a-z][a-z0-9]*\.[a-z][a-z0-9]*\z # token of another flavor
|
|
50
|
+
)
|
|
51
|
+
/x
|
|
52
|
+
|
|
53
|
+
# The longest URN that parse accepts, and that generate emits. An
|
|
54
|
+
# escape is up to 3 characters for each UTF-8 byte (up to 9 for a CJK
|
|
55
|
+
# character), so the 1000-character limit of a printed identifier is too
|
|
56
|
+
# tight here. The parser is linear-time (anchored, non-backtracking
|
|
57
|
+
# patterns), so this bound is a guard against oversized input, not a
|
|
58
|
+
# ReDoS barrier.
|
|
59
|
+
MAX_LENGTH = 16 * Pubid::MAX_INPUT_LENGTH
|
|
60
|
+
|
|
61
|
+
@schemas = {}
|
|
62
|
+
|
|
63
|
+
class << self
|
|
64
|
+
# Declare the schema of a flavor. +flavor+ is the flavor segment of
|
|
65
|
+
# its +_type+ ("iso", "cencenelec", "3gpp").
|
|
66
|
+
#
|
|
67
|
+
# @yield the FlavorSchema, evaluated with instance_eval
|
|
68
|
+
# @return [FlavorSchema]
|
|
69
|
+
def define(flavor, &)
|
|
70
|
+
schema = FlavorSchema.new(flavor)
|
|
71
|
+
schema.instance_eval(&)
|
|
72
|
+
@schemas[flavor] = schema
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @param identifier [Pubid::Identifier]
|
|
76
|
+
# @return [String]
|
|
77
|
+
# @raise [Pubid::Errors::InvalidInputError] when its flavor has no
|
|
78
|
+
# schema yet, the schema does not place one of its values, or the URN
|
|
79
|
+
# would be longer than MAX_LENGTH
|
|
80
|
+
def generate(identifier)
|
|
81
|
+
urn = "#{PREFIX}#{Encoder.new(identifier.to_hash).body}"
|
|
82
|
+
if urn.length > MAX_LENGTH
|
|
83
|
+
raise Errors::InvalidInputError,
|
|
84
|
+
"pubid URN exceeds #{MAX_LENGTH} characters"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
urn
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @param string [String] a pubid-scheme URN
|
|
91
|
+
# @return [Pubid::Identifier]
|
|
92
|
+
# @raise [Pubid::Errors::InvalidInputError] when +string+ is not a
|
|
93
|
+
# String or is longer than MAX_LENGTH
|
|
94
|
+
# @raise [Pubid::Errors::UrnParseError] for a malformed, unknown or
|
|
95
|
+
# non-canonical URN
|
|
96
|
+
def parse(string)
|
|
97
|
+
check_input!(string)
|
|
98
|
+
identifier = build(string[PREFIX.length..])
|
|
99
|
+
check_canonical!(identifier, string)
|
|
100
|
+
identifier
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# True for "urn:pubid:…" (the NID is case-insensitive, RFC 8141).
|
|
104
|
+
# Compares bytes, so it is safe on a string with invalid encoding.
|
|
105
|
+
def pubid_urn?(string)
|
|
106
|
+
string.is_a?(String) && string.b[0, PREFIX.length].downcase == PREFIX
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @return [FlavorSchema, nil]
|
|
110
|
+
def flavor_schema(flavor)
|
|
111
|
+
return @schemas[flavor] if @schemas.key?(flavor)
|
|
112
|
+
|
|
113
|
+
mod = TypeResolver.flavor_module(flavor)
|
|
114
|
+
if mod&.const_defined?(:PubidUrnSchema, false)
|
|
115
|
+
mod.const_get(:PubidUrnSchema)
|
|
116
|
+
end
|
|
117
|
+
@schemas[flavor]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @return [TypeSchema]
|
|
121
|
+
def type_schema!(type, error:)
|
|
122
|
+
flavor = flavor_segment(type)
|
|
123
|
+
schema = flavor && flavor_schema(flavor)&.for_type(type)
|
|
124
|
+
return schema if schema
|
|
125
|
+
|
|
126
|
+
raise error, "no pubid URN schema for #{type.inspect}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# "iso" in "pubid:iso:technical-report".
|
|
130
|
+
def flavor_segment(type)
|
|
131
|
+
return nil unless type.is_a?(String)
|
|
132
|
+
|
|
133
|
+
type.split(":")[1]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Does +token+ (a raw, still escaped segment) read as structure? See
|
|
137
|
+
# STRUCTURE.
|
|
138
|
+
def structural?(token)
|
|
139
|
+
STRUCTURE.match?(token)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# A type token is "<token>" of the URN's own flavor, or "<flavor>.<token>"
|
|
143
|
+
# for a node of another flavor.
|
|
144
|
+
#
|
|
145
|
+
# @return [Array(String, String)] the owning flavor and the token
|
|
146
|
+
def split_token(token, flavor)
|
|
147
|
+
token.include?(".") ? token.split(".", 2) : [flavor, token]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Split +string+ at +separator+ outside brackets.
|
|
151
|
+
#
|
|
152
|
+
# @param allow_empty [Boolean] true for the items of a list, where an
|
|
153
|
+
# empty item is an empty String; a ":" segment is never empty
|
|
154
|
+
def split_top(string, separator, allow_empty: false)
|
|
155
|
+
parts = []
|
|
156
|
+
depth = 0
|
|
157
|
+
current = +""
|
|
158
|
+
string.each_char do |char|
|
|
159
|
+
depth += 1 if char == "("
|
|
160
|
+
depth -= 1 if char == ")"
|
|
161
|
+
check_depth!(string) if depth.negative?
|
|
162
|
+
|
|
163
|
+
if char == separator && depth.zero?
|
|
164
|
+
parts << current
|
|
165
|
+
current = +""
|
|
166
|
+
else
|
|
167
|
+
current << char
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
check_depth!(string) unless depth.zero?
|
|
171
|
+
|
|
172
|
+
parts << current
|
|
173
|
+
if !allow_empty && parts.any?(&:empty?)
|
|
174
|
+
raise Errors::UrnParseError, "empty segment in #{string.inspect}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
parts
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
def check_input!(string)
|
|
183
|
+
unless string.is_a?(String)
|
|
184
|
+
raise Errors::InvalidInputError, Pubid::INPUT_NOT_A_STRING_MESSAGE
|
|
185
|
+
end
|
|
186
|
+
if string.length > MAX_LENGTH
|
|
187
|
+
raise Errors::InvalidInputError,
|
|
188
|
+
"input exceeds #{MAX_LENGTH} characters"
|
|
189
|
+
end
|
|
190
|
+
unless string.valid_encoding? && pubid_urn?(string)
|
|
191
|
+
raise Errors::UrnParseError,
|
|
192
|
+
"not a pubid-scheme URN: #{string.inspect}"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# The URN is untrusted input: a value that decodes but cannot be cast
|
|
197
|
+
# into its attribute (a bad boolean, a String where a component goes)
|
|
198
|
+
# is a bad URN, so every failure here is reported as UrnParseError,
|
|
199
|
+
# with the original error as its cause.
|
|
200
|
+
def build(body)
|
|
201
|
+
Pubid.from_hash(Decoder.new(body).to_identifier_hash)
|
|
202
|
+
rescue Errors::UrnParseError
|
|
203
|
+
raise
|
|
204
|
+
rescue StandardError => e
|
|
205
|
+
raise Errors::UrnParseError,
|
|
206
|
+
"pubid URN #{body.inspect} does not build: #{e.message}"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def check_depth!(string)
|
|
210
|
+
raise Errors::UrnParseError, "unbalanced bracket in #{string.inspect}"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def check_canonical!(identifier, string)
|
|
214
|
+
canonical = identifier.to_pubid_urn
|
|
215
|
+
return if canonical[PREFIX.length..] == string[PREFIX.length..]
|
|
216
|
+
|
|
217
|
+
raise Errors::UrnParseError,
|
|
218
|
+
"#{string.inspect} is not canonical; " \
|
|
219
|
+
"the canonical form is #{canonical.inspect}"
|
|
220
|
+
rescue Errors::InvalidInputError => e
|
|
221
|
+
raise Errors::UrnParseError,
|
|
222
|
+
"#{string.inspect} does not re-encode: #{e.message}"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
data/lib/pubid/type_resolver.rb
CHANGED
|
@@ -29,6 +29,18 @@ module Pubid
|
|
|
29
29
|
shared_class(type) || flavor_class(type)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
# The flavor module that the flavor segment of a +_type+ names
|
|
33
|
+
# ("iso", "cencenelec", "3gpp").
|
|
34
|
+
#
|
|
35
|
+
# @param segment [String]
|
|
36
|
+
# @return [Module, nil]
|
|
37
|
+
def flavor_module(segment)
|
|
38
|
+
return nil unless segment.is_a?(String)
|
|
39
|
+
|
|
40
|
+
::Pubid.eager_load_flavors!
|
|
41
|
+
::Pubid::Registry.get(segment) || flavor_by_module_segment(segment)
|
|
42
|
+
end
|
|
43
|
+
|
|
32
44
|
private
|
|
33
45
|
|
|
34
46
|
def shared_class(type)
|
data/lib/pubid/version.rb
CHANGED
data/lib/pubid.rb
CHANGED
|
@@ -160,6 +160,7 @@ module Pubid
|
|
|
160
160
|
autoload :Version, "pubid/version"
|
|
161
161
|
autoload :Core, "pubid/core"
|
|
162
162
|
autoload :TypeResolver, "pubid/type_resolver"
|
|
163
|
+
autoload :PubidUrn, "pubid/pubid_urn"
|
|
163
164
|
|
|
164
165
|
|
|
165
166
|
# Require all flavor modules
|
|
@@ -281,8 +282,13 @@ module Pubid
|
|
|
281
282
|
#
|
|
282
283
|
# @param string [String] the URN
|
|
283
284
|
# @return [Identifier] the parsed identifier
|
|
285
|
+
#
|
|
286
|
+
# A pubid-scheme URN ("urn:pubid:…", see {PubidUrn}) is decoded by the
|
|
287
|
+
# shared codec; every other namespace goes to its flavor's UrnParser.
|
|
284
288
|
def self.parse_urn(string)
|
|
285
289
|
eager_load_flavors!
|
|
290
|
+
return PubidUrn.parse(string) if PubidUrn.pubid_urn?(string)
|
|
291
|
+
|
|
286
292
|
flavor = detect_flavor_from_urn(string)
|
|
287
293
|
flavor_module = Registry.get(flavor)
|
|
288
294
|
unless flavor_module
|
|
@@ -294,6 +300,18 @@ module Pubid
|
|
|
294
300
|
urn_parser.parse(string)
|
|
295
301
|
end
|
|
296
302
|
|
|
303
|
+
# Parse a pubid-scheme URN, and only that: an official URN such as
|
|
304
|
+
# "urn:iso:std:…" raises. For a consumer that takes a slug and must not
|
|
305
|
+
# accept a lossy official URN by mistake (relaton/relaton#441).
|
|
306
|
+
#
|
|
307
|
+
# @param string [String] a "urn:pubid:…" URN
|
|
308
|
+
# @return [Identifier]
|
|
309
|
+
# @raise [Pubid::Errors::UrnParseError]
|
|
310
|
+
def self.parse_pubid_urn(string)
|
|
311
|
+
eager_load_flavors!
|
|
312
|
+
PubidUrn.parse(string)
|
|
313
|
+
end
|
|
314
|
+
|
|
297
315
|
# Build an identifier from its serialized hash, without naming its flavor —
|
|
298
316
|
# the hash counterpart of {parse}.
|
|
299
317
|
#
|
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.7
|
|
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-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -71,6 +71,7 @@ files:
|
|
|
71
71
|
- data/parg/asme.json
|
|
72
72
|
- data/parg/astm.json
|
|
73
73
|
- data/parg/bipm.json
|
|
74
|
+
- data/parg/bipm.parg
|
|
74
75
|
- data/parg/bsi.json
|
|
75
76
|
- data/parg/calconnect.json
|
|
76
77
|
- data/parg/ccsds.json
|
|
@@ -167,6 +168,7 @@ files:
|
|
|
167
168
|
- lib/pubid/ansi/identifiers/american_national_standard.rb
|
|
168
169
|
- lib/pubid/ansi/identifiers/standard.rb
|
|
169
170
|
- lib/pubid/ansi/parser.rb
|
|
171
|
+
- lib/pubid/ansi/pubid_urn_schema.rb
|
|
170
172
|
- lib/pubid/ansi/renderer.rb
|
|
171
173
|
- lib/pubid/ansi/single_identifier.rb
|
|
172
174
|
- lib/pubid/ansi/urn_generator.rb
|
|
@@ -243,6 +245,7 @@ files:
|
|
|
243
245
|
- lib/pubid/astm/identifiers/technical_report.rb
|
|
244
246
|
- lib/pubid/astm/identifiers/work_in_progress.rb
|
|
245
247
|
- lib/pubid/astm/parser.rb
|
|
248
|
+
- lib/pubid/astm/pubid_urn_schema.rb
|
|
246
249
|
- lib/pubid/astm/renderer.rb
|
|
247
250
|
- lib/pubid/astm/single_identifier.rb
|
|
248
251
|
- lib/pubid/astm/urn_generator.rb
|
|
@@ -601,6 +604,7 @@ files:
|
|
|
601
604
|
- lib/pubid/iec/identifiers/white_paper.rb
|
|
602
605
|
- lib/pubid/iec/identifiers/working_document.rb
|
|
603
606
|
- lib/pubid/iec/parser.rb
|
|
607
|
+
- lib/pubid/iec/pubid_urn_schema.rb
|
|
604
608
|
- lib/pubid/iec/renderer.rb
|
|
605
609
|
- lib/pubid/iec/rendering_style.rb
|
|
606
610
|
- lib/pubid/iec/single_identifier.rb
|
|
@@ -656,6 +660,7 @@ files:
|
|
|
656
660
|
- lib/pubid/ieee/parser.rb
|
|
657
661
|
- lib/pubid/ieee/pre_parser.rb
|
|
658
662
|
- lib/pubid/ieee/project_renderer.rb
|
|
663
|
+
- lib/pubid/ieee/pubid_urn_schema.rb
|
|
659
664
|
- lib/pubid/ieee/renderer.rb
|
|
660
665
|
- lib/pubid/ieee/typed_stages.rb
|
|
661
666
|
- lib/pubid/ieee/urn_generator.rb
|
|
@@ -730,6 +735,7 @@ files:
|
|
|
730
735
|
- lib/pubid/iso/identifiers/technology_trends_assessments.rb
|
|
731
736
|
- lib/pubid/iso/normalizer.rb
|
|
732
737
|
- lib/pubid/iso/parser.rb
|
|
738
|
+
- lib/pubid/iso/pubid_urn_schema.rb
|
|
733
739
|
- lib/pubid/iso/rendering_style.rb
|
|
734
740
|
- lib/pubid/iso/single_identifier.rb
|
|
735
741
|
- lib/pubid/iso/supplement_identifier.rb
|
|
@@ -768,6 +774,7 @@ files:
|
|
|
768
774
|
- lib/pubid/itu/identifiers/supplement.rb
|
|
769
775
|
- lib/pubid/itu/model.rb
|
|
770
776
|
- lib/pubid/itu/parser.rb
|
|
777
|
+
- lib/pubid/itu/pubid_urn_schema.rb
|
|
771
778
|
- lib/pubid/itu/urn_generator.rb
|
|
772
779
|
- lib/pubid/itu/urn_parser.rb
|
|
773
780
|
- lib/pubid/jcgm.rb
|
|
@@ -852,6 +859,7 @@ files:
|
|
|
852
859
|
- lib/pubid/nist/parser.rb
|
|
853
860
|
- lib/pubid/nist/parser_output_normalizer.rb
|
|
854
861
|
- lib/pubid/nist/preprocessor.rb
|
|
862
|
+
- lib/pubid/nist/pubid_urn_schema.rb
|
|
855
863
|
- lib/pubid/nist/renderer.rb
|
|
856
864
|
- lib/pubid/nist/router.rb
|
|
857
865
|
- lib/pubid/nist/series.rb
|
|
@@ -943,6 +951,12 @@ files:
|
|
|
943
951
|
- lib/pubid/plateau/urn_generator.rb
|
|
944
952
|
- lib/pubid/plateau/urn_parser.rb
|
|
945
953
|
- lib/pubid/prefixes_support.rb
|
|
954
|
+
- lib/pubid/pubid_urn.rb
|
|
955
|
+
- lib/pubid/pubid_urn/decoder.rb
|
|
956
|
+
- lib/pubid/pubid_urn/encoder.rb
|
|
957
|
+
- lib/pubid/pubid_urn/escape.rb
|
|
958
|
+
- lib/pubid/pubid_urn/paths.rb
|
|
959
|
+
- lib/pubid/pubid_urn/schema.rb
|
|
946
960
|
- lib/pubid/renderers.rb
|
|
947
961
|
- lib/pubid/renderers/annotator.rb
|
|
948
962
|
- lib/pubid/renderers/base.rb
|