pubid 2.0.6 → 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/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/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 +15 -2
|
@@ -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
|
|
@@ -168,6 +168,7 @@ files:
|
|
|
168
168
|
- lib/pubid/ansi/identifiers/american_national_standard.rb
|
|
169
169
|
- lib/pubid/ansi/identifiers/standard.rb
|
|
170
170
|
- lib/pubid/ansi/parser.rb
|
|
171
|
+
- lib/pubid/ansi/pubid_urn_schema.rb
|
|
171
172
|
- lib/pubid/ansi/renderer.rb
|
|
172
173
|
- lib/pubid/ansi/single_identifier.rb
|
|
173
174
|
- lib/pubid/ansi/urn_generator.rb
|
|
@@ -244,6 +245,7 @@ files:
|
|
|
244
245
|
- lib/pubid/astm/identifiers/technical_report.rb
|
|
245
246
|
- lib/pubid/astm/identifiers/work_in_progress.rb
|
|
246
247
|
- lib/pubid/astm/parser.rb
|
|
248
|
+
- lib/pubid/astm/pubid_urn_schema.rb
|
|
247
249
|
- lib/pubid/astm/renderer.rb
|
|
248
250
|
- lib/pubid/astm/single_identifier.rb
|
|
249
251
|
- lib/pubid/astm/urn_generator.rb
|
|
@@ -602,6 +604,7 @@ files:
|
|
|
602
604
|
- lib/pubid/iec/identifiers/white_paper.rb
|
|
603
605
|
- lib/pubid/iec/identifiers/working_document.rb
|
|
604
606
|
- lib/pubid/iec/parser.rb
|
|
607
|
+
- lib/pubid/iec/pubid_urn_schema.rb
|
|
605
608
|
- lib/pubid/iec/renderer.rb
|
|
606
609
|
- lib/pubid/iec/rendering_style.rb
|
|
607
610
|
- lib/pubid/iec/single_identifier.rb
|
|
@@ -657,6 +660,7 @@ files:
|
|
|
657
660
|
- lib/pubid/ieee/parser.rb
|
|
658
661
|
- lib/pubid/ieee/pre_parser.rb
|
|
659
662
|
- lib/pubid/ieee/project_renderer.rb
|
|
663
|
+
- lib/pubid/ieee/pubid_urn_schema.rb
|
|
660
664
|
- lib/pubid/ieee/renderer.rb
|
|
661
665
|
- lib/pubid/ieee/typed_stages.rb
|
|
662
666
|
- lib/pubid/ieee/urn_generator.rb
|
|
@@ -731,6 +735,7 @@ files:
|
|
|
731
735
|
- lib/pubid/iso/identifiers/technology_trends_assessments.rb
|
|
732
736
|
- lib/pubid/iso/normalizer.rb
|
|
733
737
|
- lib/pubid/iso/parser.rb
|
|
738
|
+
- lib/pubid/iso/pubid_urn_schema.rb
|
|
734
739
|
- lib/pubid/iso/rendering_style.rb
|
|
735
740
|
- lib/pubid/iso/single_identifier.rb
|
|
736
741
|
- lib/pubid/iso/supplement_identifier.rb
|
|
@@ -769,6 +774,7 @@ files:
|
|
|
769
774
|
- lib/pubid/itu/identifiers/supplement.rb
|
|
770
775
|
- lib/pubid/itu/model.rb
|
|
771
776
|
- lib/pubid/itu/parser.rb
|
|
777
|
+
- lib/pubid/itu/pubid_urn_schema.rb
|
|
772
778
|
- lib/pubid/itu/urn_generator.rb
|
|
773
779
|
- lib/pubid/itu/urn_parser.rb
|
|
774
780
|
- lib/pubid/jcgm.rb
|
|
@@ -853,6 +859,7 @@ files:
|
|
|
853
859
|
- lib/pubid/nist/parser.rb
|
|
854
860
|
- lib/pubid/nist/parser_output_normalizer.rb
|
|
855
861
|
- lib/pubid/nist/preprocessor.rb
|
|
862
|
+
- lib/pubid/nist/pubid_urn_schema.rb
|
|
856
863
|
- lib/pubid/nist/renderer.rb
|
|
857
864
|
- lib/pubid/nist/router.rb
|
|
858
865
|
- lib/pubid/nist/series.rb
|
|
@@ -944,6 +951,12 @@ files:
|
|
|
944
951
|
- lib/pubid/plateau/urn_generator.rb
|
|
945
952
|
- lib/pubid/plateau/urn_parser.rb
|
|
946
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
|
|
947
960
|
- lib/pubid/renderers.rb
|
|
948
961
|
- lib/pubid/renderers/annotator.rb
|
|
949
962
|
- lib/pubid/renderers/base.rb
|