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,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Nist
|
|
5
|
+
# The pubid-scheme URN of NIST (issue #656; docs/URN-SCHEMES.adoc).
|
|
6
|
+
#
|
|
7
|
+
# NIST SP 800-53r5
|
|
8
|
+
# urn:pubid:nist:sp:800-53:pub-NIST:series-SP:subseries-800:edtype-r:ed-5
|
|
9
|
+
#
|
|
10
|
+
# The core is the document number. +publisher+ and +series+ are atoms,
|
|
11
|
+
# because a few series print no number ("NIST OWMWP 06-13-2018" keeps
|
|
12
|
+
# its date there) and the series spelling varies inside one type
|
|
13
|
+
# ("CIRC", "NBS.CIRC", "LCIRC").
|
|
14
|
+
#
|
|
15
|
+
# +update+ and +update_component+ serialize every field of their
|
|
16
|
+
# component, so each field is its own atom. Reducing them to one
|
|
17
|
+
# identifying value changes #to_hash and needs a relaton-data-nist
|
|
18
|
+
# re-crawl; that is a separate decision (docs/URN-SCHEMES.adoc).
|
|
19
|
+
#
|
|
20
|
+
# Facets: +parsed_format+ (the input was an MR string), the printed
|
|
21
|
+
# prefix of a revision (" Rev. " against "r") and the bracketed
|
|
22
|
+
# +translation+ ("(spa)"), whose code is the +lang+ atom.
|
|
23
|
+
PubidUrnSchema = Pubid::PubidUrn.define("nist") do
|
|
24
|
+
atom "publisher", "pub"
|
|
25
|
+
atom "series", "series"
|
|
26
|
+
atom "subseries", "subseries"
|
|
27
|
+
atom "part.type", "parttype"
|
|
28
|
+
atom "part.value", "part"
|
|
29
|
+
atom "section", "section"
|
|
30
|
+
atom "volume", "vol"
|
|
31
|
+
atom "edition.type", "edtype"
|
|
32
|
+
atom "edition.id", "ed"
|
|
33
|
+
atom "edition.additional_text", "edtext"
|
|
34
|
+
atom "edition_year", "edyear"
|
|
35
|
+
atom "supplement.number", "sup"
|
|
36
|
+
atom "supplement.suffix", "supsuffix"
|
|
37
|
+
atom "supplement.year", "supyear"
|
|
38
|
+
atom "supplement.month", "supmonth"
|
|
39
|
+
atom "supplement.year_end", "supyearend"
|
|
40
|
+
atom "supplement_date_range_start", "suprangestart"
|
|
41
|
+
atom "supplement_date_range_end", "suprangeend"
|
|
42
|
+
atom "supplement.has_revision", "suprev", kind: :bool
|
|
43
|
+
atom "implicit_supplement", "implicitsup", kind: :bool
|
|
44
|
+
atom "update.prefix", "updprefix"
|
|
45
|
+
atom "update.number", "upd"
|
|
46
|
+
atom "update.year", "updyear"
|
|
47
|
+
atom "update.month", "updmonth"
|
|
48
|
+
atom "update_component.prefix", "ucprefix"
|
|
49
|
+
atom "update_component.number", "uc"
|
|
50
|
+
atom "update_component.year", "ucyear"
|
|
51
|
+
atom "update_component.month", "ucmonth"
|
|
52
|
+
atom "revision_year", "revyear"
|
|
53
|
+
atom "revision_month", "revmonth"
|
|
54
|
+
atom "revised_date", "reviseddate"
|
|
55
|
+
atom "update_number", "updnumber"
|
|
56
|
+
atom "update_year", "updnumberyear"
|
|
57
|
+
atom "stage.type", "stagetype"
|
|
58
|
+
atom "stage.id", "stage"
|
|
59
|
+
atom "draft_number", "draftnumber"
|
|
60
|
+
atom "version_component.value", "v"
|
|
61
|
+
atom "version", "version"
|
|
62
|
+
atom "year", "year", kind: :int
|
|
63
|
+
atom "month", "month", kind: :int
|
|
64
|
+
atom "date_year", "dateyear"
|
|
65
|
+
atom "date_month", "datemonth"
|
|
66
|
+
atom "date_day", "dateday"
|
|
67
|
+
atom "dated_seq", "datedseq"
|
|
68
|
+
atom "prefix", "prefix"
|
|
69
|
+
atom "range_notation", "range"
|
|
70
|
+
atom "translation_component.code", "lang"
|
|
71
|
+
atom "draft", "draft"
|
|
72
|
+
atom "index", "index"
|
|
73
|
+
atom "insert", "insert"
|
|
74
|
+
atom "errata", "errata"
|
|
75
|
+
atom "appendix", "appendix"
|
|
76
|
+
atom "addendum", "addendum"
|
|
77
|
+
atom "addendum_number", "addendumnumber"
|
|
78
|
+
|
|
79
|
+
facet "publisher_was_parsed", "pubparsed", kind: :bool
|
|
80
|
+
facet "parsed_format", "format"
|
|
81
|
+
facet "edition.original_prefix", "edprefix"
|
|
82
|
+
facet "translation", "translationsrc"
|
|
83
|
+
|
|
84
|
+
type "pubid:nist:identifier", "id"
|
|
85
|
+
type "pubid:nist:circular", "circ"
|
|
86
|
+
type "pubid:nist:circular-supplement", "circsup"
|
|
87
|
+
type "pubid:nist:commercial-standard", "cs"
|
|
88
|
+
type "pubid:nist:commercial-standard-emergency", "cse"
|
|
89
|
+
type "pubid:nist:commercial-standards-monthly", "csm"
|
|
90
|
+
type "pubid:nist:crpl-report", "crpl"
|
|
91
|
+
type "pubid:nist:dated-document", "dd"
|
|
92
|
+
type "pubid:nist:federal-information-processing-standards", "fips"
|
|
93
|
+
type "pubid:nist:grant-contractor-report", "gcr"
|
|
94
|
+
type "pubid:nist:handbook", "hb"
|
|
95
|
+
type "pubid:nist:interagency-report", "ir"
|
|
96
|
+
type "pubid:nist:letter-circular", "lc"
|
|
97
|
+
type "pubid:nist:miscellaneous-publication", "mp"
|
|
98
|
+
type "pubid:nist:monograph", "mono"
|
|
99
|
+
type "pubid:nist:ncstar", "ncstar"
|
|
100
|
+
type "pubid:nist:nsrds", "nsrds"
|
|
101
|
+
type "pubid:nist:owmwp", "owmwp"
|
|
102
|
+
type "pubid:nist:report", "rpt"
|
|
103
|
+
type "pubid:nist:special-publication", "sp"
|
|
104
|
+
type "pubid:nist:technical-note", "tn"
|
|
105
|
+
type "pubid:nist:supplement-identifier", "sup"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
data/lib/pubid/nist.rb
CHANGED
|
@@ -21,6 +21,7 @@ module Pubid
|
|
|
21
21
|
autoload :Caster, "#{__dir__}/nist/caster"
|
|
22
22
|
autoload :CircularSupplementBuilder, "#{__dir__}/nist/circular_supplement_builder"
|
|
23
23
|
autoload :Components, "#{__dir__}/nist/components"
|
|
24
|
+
autoload :PubidUrnSchema, "#{__dir__}/nist/pubid_urn_schema"
|
|
24
25
|
autoload :Configuration, "#{__dir__}/nist/configuration"
|
|
25
26
|
autoload :Identifier, "#{__dir__}/nist/identifiers/base"
|
|
26
27
|
autoload :Identifiers, "#{__dir__}/nist/identifiers"
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module PubidUrn
|
|
5
|
+
# Reads the body of a pubid-scheme URN back into an identifier hash.
|
|
6
|
+
# The inverse of Encoder; strict about structure. Canonical form (atom
|
|
7
|
+
# order, escapes, trailing "~") is checked by PubidUrn.parse, which
|
|
8
|
+
# re-encodes the result and compares.
|
|
9
|
+
class Decoder
|
|
10
|
+
FACET = /\A(\d+)\.([a-z0-9]+)-(.*)\z/
|
|
11
|
+
|
|
12
|
+
def initialize(body)
|
|
13
|
+
@body = body
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Hash] the identifier hash (+_type+ and +base+ included)
|
|
17
|
+
def to_identifier_hash
|
|
18
|
+
flavor, rest = @body.split(":", 2)
|
|
19
|
+
fail!("no type segment") if rest.nil? || rest.empty?
|
|
20
|
+
decode_identifier(rest, flavor)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def decode_identifier(body, flavor)
|
|
26
|
+
tokens = PubidUrn.split_top(body, ":")
|
|
27
|
+
nodes = []
|
|
28
|
+
schemas = []
|
|
29
|
+
i = 0
|
|
30
|
+
while i < tokens.size
|
|
31
|
+
token = tokens[i]
|
|
32
|
+
if token == "f"
|
|
33
|
+
decode_facets(tokens.drop(i + 1), nodes, schemas)
|
|
34
|
+
break
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
schema = node_schema(token, flavor)
|
|
38
|
+
node = { "_type" => schema.type }
|
|
39
|
+
i = decode_core(tokens, i + 1, node, schema)
|
|
40
|
+
i = decode_atoms(tokens, i, node, schema)
|
|
41
|
+
nodes << node
|
|
42
|
+
schemas << schema
|
|
43
|
+
end
|
|
44
|
+
fail!("no identifier") if nodes.empty?
|
|
45
|
+
|
|
46
|
+
nodes.each_cons(2) { |base, node| node["base"] = base }
|
|
47
|
+
nodes.last
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def node_schema(token, flavor)
|
|
51
|
+
if token == "type"
|
|
52
|
+
fail!("type-metadata URN names a type, not a document")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
schema = if PubidUrn.structural?(token)
|
|
56
|
+
owner, name = PubidUrn.split_token(token, flavor)
|
|
57
|
+
PubidUrn.flavor_schema(owner)&.for_token(name)
|
|
58
|
+
end
|
|
59
|
+
fail!("unknown type or segment #{token.inspect}") unless schema
|
|
60
|
+
|
|
61
|
+
schema
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def decode_core(tokens, index, node, schema)
|
|
65
|
+
schema.core.each do |parts|
|
|
66
|
+
break if index >= tokens.size
|
|
67
|
+
|
|
68
|
+
token = tokens[index]
|
|
69
|
+
break if PubidUrn.structural?(token)
|
|
70
|
+
|
|
71
|
+
Paths.put(node, parts, Escape.decode(token)) unless token == NULL
|
|
72
|
+
index += 1
|
|
73
|
+
end
|
|
74
|
+
index
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def decode_atoms(tokens, index, node, schema)
|
|
78
|
+
last = -1
|
|
79
|
+
while index < tokens.size &&
|
|
80
|
+
(position = schema.atom_position(tokens[index]))
|
|
81
|
+
if position <= last
|
|
82
|
+
fail!("atom #{tokens[index].inspect} is out of order or repeated")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
last = position
|
|
86
|
+
atom = schema.atoms[position]
|
|
87
|
+
raw = tokens[index].split("-", 2).last
|
|
88
|
+
Paths.put(node, atom.parts, atom_value(raw, atom))
|
|
89
|
+
index += 1
|
|
90
|
+
end
|
|
91
|
+
index
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def atom_value(raw, atom)
|
|
95
|
+
case atom.kind
|
|
96
|
+
when :int then integer(raw)
|
|
97
|
+
when :id then nested(raw)
|
|
98
|
+
when :ids then PubidUrn.split_top(raw, ",").map { |item| nested(item) }
|
|
99
|
+
else slot_value(raw, atom)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The value of a :string, :bool or list atom, or of a facet.
|
|
104
|
+
def slot_value(raw, slot)
|
|
105
|
+
return scalar(raw, slot) unless slot.list?
|
|
106
|
+
|
|
107
|
+
PubidUrn.split_top(raw, ",", allow_empty: true).map do |item|
|
|
108
|
+
item == NULL ? nil : scalar(item, slot)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def scalar(raw, slot)
|
|
113
|
+
slot.element == :bool ? boolean(raw) : Escape.decode(raw)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def boolean(raw)
|
|
117
|
+
return true if raw == "true"
|
|
118
|
+
return false if raw == "false"
|
|
119
|
+
|
|
120
|
+
fail!("#{raw.inspect} is not a boolean")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def integer(raw)
|
|
124
|
+
unless /\A-?[0-9]+\z/.match?(raw)
|
|
125
|
+
fail!("#{raw.inspect} is not an integer")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
Integer(raw, 10)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def nested(raw)
|
|
132
|
+
unless raw.start_with?("(") && raw.end_with?(")")
|
|
133
|
+
fail!("nested identifier #{raw.inspect} needs brackets")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
Decoder.new(raw[1..-2]).to_identifier_hash
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def decode_facets(tokens, nodes, schemas)
|
|
140
|
+
fail!("empty facet group") if tokens.empty?
|
|
141
|
+
|
|
142
|
+
tokens.each do |token|
|
|
143
|
+
match = FACET.match(token) || fail!("bad facet #{token.inspect}")
|
|
144
|
+
index = Integer(match[1], 10)
|
|
145
|
+
schema = schemas[index] || fail!("facet #{token.inspect} has no node")
|
|
146
|
+
facet = schema.facet(match[2]) || fail!("bad facet #{token.inspect}")
|
|
147
|
+
Paths.put(nodes[index], facet.parts, slot_value(match[3], facet))
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def fail!(message)
|
|
152
|
+
raise Errors::UrnParseError, "pubid URN #{@body.inspect}: #{message}"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module PubidUrn
|
|
5
|
+
# Writes the pubid-scheme URN of an identifier hash.
|
|
6
|
+
#
|
|
7
|
+
# urn:pubid:<flavor>:<node>[:<node>…][:f:<facet>…]
|
|
8
|
+
# <node> = <type>[:<core>…][:<prefix>-<value>…]
|
|
9
|
+
#
|
|
10
|
+
# The nodes go root first, along +base+: "ISO 9001:2015/Amd 1:2016" is
|
|
11
|
+
# "is:9001:year-2015:amd:1:year-2016", so every supplement of a document
|
|
12
|
+
# shares its slug prefix. A core value is positional; an absent one in
|
|
13
|
+
# the middle is "~", and absent ones at the end are dropped. An optional
|
|
14
|
+
# value is a prefixed atom, so it leaves no segment when it is absent.
|
|
15
|
+
class Encoder
|
|
16
|
+
def initialize(hash)
|
|
17
|
+
@hash = hash
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [String] the body after "urn:pubid:"
|
|
21
|
+
def body
|
|
22
|
+
flavor = PubidUrn.flavor_segment(@hash["_type"])
|
|
23
|
+
"#{flavor}:#{encode_identifier(@hash, flavor)}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def encode_identifier(hash, flavor)
|
|
29
|
+
segments = []
|
|
30
|
+
facets = []
|
|
31
|
+
chain(hash).each_with_index do |node, index|
|
|
32
|
+
schema = PubidUrn.type_schema!(node["_type"],
|
|
33
|
+
error: Errors::InvalidInputError)
|
|
34
|
+
node = deep_dup(node)
|
|
35
|
+
node.delete("_type")
|
|
36
|
+
segments << token(schema, flavor)
|
|
37
|
+
segments.concat(core_segments(node, schema))
|
|
38
|
+
segments.concat(atom_segments(node, schema))
|
|
39
|
+
facets.concat(facet_segments(node, schema, index))
|
|
40
|
+
check_placed!(node, schema)
|
|
41
|
+
end
|
|
42
|
+
segments.push("f", *facets) unless facets.empty?
|
|
43
|
+
segments.join(":")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The nodes from the root document to +hash+ itself.
|
|
47
|
+
def chain(hash)
|
|
48
|
+
nodes = []
|
|
49
|
+
node = hash
|
|
50
|
+
while node
|
|
51
|
+
unless node.is_a?(Hash) && node["_type"].is_a?(String)
|
|
52
|
+
raise Errors::InvalidInputError,
|
|
53
|
+
"a pubid URN base must be an identifier hash, " \
|
|
54
|
+
"got #{node.inspect}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
nodes.unshift(node.except("base"))
|
|
58
|
+
node = node["base"]
|
|
59
|
+
end
|
|
60
|
+
nodes
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def token(schema, flavor)
|
|
64
|
+
return schema.token if schema.flavor == flavor
|
|
65
|
+
|
|
66
|
+
"#{schema.flavor}.#{schema.token}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def core_segments(node, schema)
|
|
70
|
+
values = schema.core.map { |parts| Paths.take(node, parts) }
|
|
71
|
+
values.pop while !values.empty? && values.last.nil?
|
|
72
|
+
values.map do |value|
|
|
73
|
+
value.nil? ? NULL : core_value(value, schema)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# A core value that has the shape of structure (PubidUrn::STRUCTURE)
|
|
78
|
+
# gets its first character escaped.
|
|
79
|
+
def core_value(value, schema)
|
|
80
|
+
unless value.is_a?(String) && !value.empty?
|
|
81
|
+
raise Errors::InvalidInputError,
|
|
82
|
+
"core value of #{schema.type} must be a non-empty String, " \
|
|
83
|
+
"got #{value.inspect}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
encoded = Escape.encode(value)
|
|
87
|
+
return encoded unless PubidUrn.structural?(encoded)
|
|
88
|
+
|
|
89
|
+
Escape.force(value[0]) + Escape.encode(value[1..])
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def atom_segments(node, schema)
|
|
93
|
+
schema.atoms.filter_map do |atom|
|
|
94
|
+
value = Paths.take(node, atom.parts)
|
|
95
|
+
next if value.nil?
|
|
96
|
+
|
|
97
|
+
"#{atom.prefix}-#{atom_value(value, atom, schema)}"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def atom_value(value, atom, schema)
|
|
102
|
+
case atom.kind
|
|
103
|
+
when :int then typed(value, Integer, atom, schema).to_s
|
|
104
|
+
when :id then nested(typed(value, Hash, atom, schema))
|
|
105
|
+
when :ids then nested_list(typed(value, Array, atom, schema))
|
|
106
|
+
else slot_value(value, atom, schema)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The value of a :string, :bool or list atom, or of a facet.
|
|
111
|
+
def slot_value(value, slot, schema)
|
|
112
|
+
return scalar(value, slot, schema) unless slot.list?
|
|
113
|
+
|
|
114
|
+
typed(value, Array, slot, schema).map do |item|
|
|
115
|
+
item.nil? ? NULL : scalar(item, slot, schema)
|
|
116
|
+
end.join(",")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def scalar(value, slot, schema)
|
|
120
|
+
if slot.element == :bool
|
|
121
|
+
typed(value, [true, false], slot, schema).to_s
|
|
122
|
+
else
|
|
123
|
+
Escape.encode(typed(value, String, slot, schema))
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def nested(hash)
|
|
128
|
+
"(#{Encoder.new(hash).body})"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def nested_list(hashes)
|
|
132
|
+
hashes.map { |hash| nested(hash) }.join(",")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def facet_segments(node, schema, index)
|
|
136
|
+
schema.facets.filter_map do |facet|
|
|
137
|
+
value = Paths.take(node, facet.parts)
|
|
138
|
+
next if value.nil?
|
|
139
|
+
|
|
140
|
+
"#{index}.#{facet.name}-#{slot_value(value, facet, schema)}"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def typed(value, expected, slot, schema)
|
|
145
|
+
ok = if expected.is_a?(Array)
|
|
146
|
+
expected.include?(value)
|
|
147
|
+
else
|
|
148
|
+
value.is_a?(expected)
|
|
149
|
+
end
|
|
150
|
+
return value if ok
|
|
151
|
+
|
|
152
|
+
raise Errors::InvalidInputError,
|
|
153
|
+
"#{schema.type} #{slot.path}: #{value.inspect} " \
|
|
154
|
+
"is not #{slot.kind}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def check_placed!(node, schema)
|
|
158
|
+
left = node.reject { |_, value| Paths.blank?(value) }
|
|
159
|
+
return if left.empty?
|
|
160
|
+
|
|
161
|
+
raise Errors::InvalidInputError,
|
|
162
|
+
"the pubid URN schema of #{schema.type} " \
|
|
163
|
+
"does not place #{left.inspect}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def deep_dup(value)
|
|
167
|
+
case value
|
|
168
|
+
when Hash then value.to_h { |k, v| [k, deep_dup(v)] }
|
|
169
|
+
when Array then value.map { |v| deep_dup(v) }
|
|
170
|
+
else value
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module PubidUrn
|
|
5
|
+
# The value escape of the pubid-scheme URN.
|
|
6
|
+
#
|
|
7
|
+
# The URN is also an HTTP path slug (relaton/relaton#441), so it cannot
|
|
8
|
+
# use "%": servers and proxies decode "%2F" before they route, or they
|
|
9
|
+
# refuse it. "~XX" (uppercase hex of one UTF-8 byte) has no meaning in a
|
|
10
|
+
# path, and "~" is an RFC 3986 unreserved character. Every byte outside
|
|
11
|
+
# SAFE is escaped, so ":" "," "(" ")" "~" never occur raw in a value and
|
|
12
|
+
# stay free for the structure of the URN.
|
|
13
|
+
module Escape
|
|
14
|
+
SAFE = /[A-Za-z0-9._-]/
|
|
15
|
+
RAW_TOKEN = /\A(?:[A-Za-z0-9._-]|~[0-9A-F]{2})*\z/
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# @param value [String]
|
|
20
|
+
# @return [String]
|
|
21
|
+
def encode(value)
|
|
22
|
+
value.to_s.each_char.map { |char| encode_char(char) }.join
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Escape every character of +char+, safe or not. The codec uses it on
|
|
26
|
+
# the first character of a core value that would otherwise read as
|
|
27
|
+
# structure ("amd" -> "~61md").
|
|
28
|
+
#
|
|
29
|
+
# @param char [String] one character
|
|
30
|
+
# @return [String]
|
|
31
|
+
def force(char)
|
|
32
|
+
char.b.each_byte.map { |byte| format("~%02X", byte) }.join
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param token [String]
|
|
36
|
+
# @return [String]
|
|
37
|
+
# @raise [Pubid::Errors::UrnParseError] for a raw character outside the
|
|
38
|
+
# safe set, or a malformed or lower-case escape
|
|
39
|
+
def decode(token)
|
|
40
|
+
unless RAW_TOKEN.match?(token)
|
|
41
|
+
raise Pubid::Errors::UrnParseError,
|
|
42
|
+
"bad character or escape in pubid URN value #{token.inspect}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
bytes = token.b.gsub(/~([0-9A-F]{2})/n) { Regexp.last_match(1).hex.chr }
|
|
46
|
+
decoded = bytes.force_encoding(Encoding::UTF_8)
|
|
47
|
+
unless decoded.valid_encoding?
|
|
48
|
+
raise Pubid::Errors::UrnParseError,
|
|
49
|
+
"escape in pubid URN value #{token.inspect} is not UTF-8"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
decoded
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def encode_char(char)
|
|
56
|
+
SAFE.match?(char) ? char : force(char)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -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
|