relaton 3.0.0.pre.alpha.5 → 3.0.0.pre.alpha.6
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/lib/relaton/3gpp/bibliography.rb +19 -6
- data/lib/relaton/3gpp/processor.rb +6 -0
- data/lib/relaton/bib/item_data.rb +14 -0
- data/lib/relaton/bipm/bibliography.rb +24 -21
- data/lib/relaton/bsi/bibliography.rb +26 -23
- data/lib/relaton/calconnect/bibliography.rb +6 -6
- data/lib/relaton/calconnect/hit_collection.rb +4 -1
- data/lib/relaton/ccsds/bibliography.rb +15 -10
- data/lib/relaton/ccsds/hit_collection.rb +1 -1
- data/lib/relaton/ccsds/processor.rb +3 -2
- data/lib/relaton/cen/bibliography.rb +27 -24
- data/lib/relaton/cie/bibliography.rb +11 -9
- data/lib/relaton/cie/scrapper.rb +15 -8
- data/lib/relaton/core/processor.rb +61 -8
- data/lib/relaton/core/request_error.rb +21 -3
- data/lib/relaton/db/cache.rb +49 -19
- data/lib/relaton/db/cache_entry.rb +9 -0
- data/lib/relaton/db/registry.rb +128 -28
- data/lib/relaton/db.rb +162 -48
- data/lib/relaton/ecma/bibliography.rb +18 -13
- data/lib/relaton/etsi/bibliography.rb +8 -7
- data/lib/relaton/gb/bibliography.rb +20 -12
- data/lib/relaton/iala/bibliography.rb +15 -12
- data/lib/relaton/iana/bibliography.rb +12 -12
- data/lib/relaton/iana/parser.rb +5 -2
- data/lib/relaton/iec/bibliography.rb +18 -7
- data/lib/relaton/iec/processor.rb +5 -0
- data/lib/relaton/ieee/bibliography.rb +19 -13
- data/lib/relaton/ieee/processor.rb +16 -0
- data/lib/relaton/ieee/rawbib_id_parser.rb +2 -2
- data/lib/relaton/ietf/bibliography.rb +11 -9
- data/lib/relaton/ietf/scraper.rb +5 -4
- data/lib/relaton/iso/bibliography.rb +11 -9
- data/lib/relaton/itu/bibliography.rb +9 -4
- data/lib/relaton/jis/bibliography.rb +22 -11
- data/lib/relaton/nist/bibliography.rb +25 -21
- data/lib/relaton/oasis/bibliography.rb +16 -13
- data/lib/relaton/ogc/bibliography.rb +15 -14
- data/lib/relaton/ogc/hit_collection.rb +9 -7
- data/lib/relaton/ogc/processor.rb +7 -1
- data/lib/relaton/omg/bibliography.rb +9 -9
- data/lib/relaton/omg/scraper.rb +3 -2
- data/lib/relaton/plateau/bibliography.rb +9 -7
- data/lib/relaton/plateau/hit_collection.rb +2 -1
- data/lib/relaton/version.rb +1 -1
- data/lib/relaton/xsf/bibliography.rb +11 -6
- metadata +18 -4
|
@@ -47,14 +47,67 @@ module Relaton
|
|
|
47
47
|
@pubid_class ||= ::Pubid.const_get(flavor)::Identifier
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
# Whether the flavor's identifiers print without a publisher token
|
|
51
|
+
# (OGC `19-025r1`, 3GPP `TS 23.207:REL-18/18.0.0`). Routing then takes
|
|
52
|
+
# an exact parse by the flavor although no prefix of it claims the
|
|
53
|
+
# reference (Registry#route).
|
|
54
|
+
#
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def bare_identifiers?
|
|
57
|
+
false
|
|
58
|
+
end
|
|
59
|
+
|
|
50
60
|
# Parse a query reference into a pubid. A flavor that normalizes a
|
|
51
|
-
# reference before it parses overrides this. A
|
|
52
|
-
#
|
|
61
|
+
# reference before it parses overrides this. A reference the flavor's
|
|
62
|
+
# grammar cannot read gives no pubid (relaton#235): `get` then
|
|
63
|
+
# receives the String and normalizes it itself, and the query is not
|
|
64
|
+
# keyed - the same reading as a flavor's own miss.
|
|
53
65
|
#
|
|
54
66
|
# @param ref [String]
|
|
55
67
|
# @return [Pubid::Identifier, nil]
|
|
56
68
|
def cache_pubid(ref)
|
|
57
69
|
pubid_class&.parse(ref)
|
|
70
|
+
rescue ArgumentError, ::Pubid::Errors::Error, Parslet::ParseFailed
|
|
71
|
+
# relaton-bib carries the log facade; core specs load without it.
|
|
72
|
+
require "relaton/bib"
|
|
73
|
+
Relaton::Bib::Util.info "does not parse as this flavor's pubid; " \
|
|
74
|
+
"passing the reference to the flavor as " \
|
|
75
|
+
"written", "relaton-db", key: ref
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The reference as `get` receives it and the cache keys it (relaton#205):
|
|
80
|
+
# the parse routing already made, else the flavor's own. Nil when the
|
|
81
|
+
# flavor gives no pubid: `get` then receives the String, and the query
|
|
82
|
+
# is not cached. A flavor overrides this when an option keeps a query
|
|
83
|
+
# out of the cache and away from the pubid (CCSDS format).
|
|
84
|
+
#
|
|
85
|
+
# @param ref [String]
|
|
86
|
+
# @param opts [Hash]
|
|
87
|
+
# @param parsed [Pubid::Identifier, nil] routing's parse, of this
|
|
88
|
+
# flavor's class
|
|
89
|
+
# @return [Pubid::Identifier, nil]
|
|
90
|
+
def query_pubid(ref, _opts = {}, parsed = nil)
|
|
91
|
+
pubid = parsed || cache_pubid(ref)
|
|
92
|
+
# A flavor that reads an unparseable reference as a miss (IANA,
|
|
93
|
+
# IEEE) gives nil or the raw String: no pubid then. The nil test comes
|
|
94
|
+
# first: without a pubid class, pubid may not be loaded.
|
|
95
|
+
pubid if !pubid.nil? && pubid.is_a?(::Pubid::Identifier)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The publishers of a co-published identifier after the lead, in the
|
|
99
|
+
# order the parsed pubid holds them: the printed order (pubid#472).
|
|
100
|
+
# Relaton::Db asks their flavors when the lead's catalog has no record
|
|
101
|
+
# (relaton#205). A flavor whose pubid keeps them elsewhere overrides
|
|
102
|
+
# this (IEEE).
|
|
103
|
+
#
|
|
104
|
+
# @param pubid [Pubid::Identifier] the query pubid (#query_pubid)
|
|
105
|
+
# @return [Array<String>] publisher abbreviations, e.g. `["IEC"]`
|
|
106
|
+
def copublishers(pubid)
|
|
107
|
+
pubid = pubid.identifiers.first if pubid.all_parts?
|
|
108
|
+
return [] unless pubid.respond_to?(:copublishers)
|
|
109
|
+
|
|
110
|
+
Array(pubid.copublishers).map(&:to_s)
|
|
58
111
|
end
|
|
59
112
|
|
|
60
113
|
# Set the year on a parsed pubid, where the flavor's `get` applies it. The
|
|
@@ -88,12 +141,12 @@ module Relaton
|
|
|
88
141
|
# @param ref [String]
|
|
89
142
|
# @param year [String, Integer, nil]
|
|
90
143
|
# @param opts [Hash]
|
|
91
|
-
# @
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
144
|
+
# @param parsed [Pubid::Identifier, nil] the reference as already parsed
|
|
145
|
+
# with this flavor's class (#query_pubid); parsed here otherwise
|
|
146
|
+
# @return [Pubid::Identifier, nil] never the given object itself when a
|
|
147
|
+
# year or all-parts is folded in: those fold on copies
|
|
148
|
+
def cache_key(ref, year, opts, parsed = nil)
|
|
149
|
+
pubid = query_pubid(ref, opts, parsed) or return
|
|
97
150
|
|
|
98
151
|
pubid = fold_year(pubid, year) if year
|
|
99
152
|
pubid = pubid.to_all_parts if opts[:all_parts] && !pubid.all_parts?
|
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
module Relaton
|
|
2
|
+
# The base of relaton's own errors. It depends on nothing, so
|
|
3
|
+
# `relaton/index` requires this file alone and does not load the rest of
|
|
4
|
+
# `relaton/core`.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
2
7
|
# Transport failure while fetching remote data; `Relaton::Db#net_retry`
|
|
3
|
-
# retries it.
|
|
4
|
-
# alone and does not load the rest of `relaton/core`.
|
|
8
|
+
# retries it.
|
|
5
9
|
#
|
|
6
10
|
# The class is named `Relaton::RequestError`, not `Relaton::Core::...`: an
|
|
7
11
|
# error raised without a message uses the class name as its message, and
|
|
8
12
|
# relaton-cli prints that message.
|
|
9
|
-
class RequestError <
|
|
13
|
+
class RequestError < Error; end
|
|
14
|
+
|
|
15
|
+
# `Relaton::Db` found no flavor for a reference: no flavor's pubid grammar
|
|
16
|
+
# reads it exactly, and no flavor's prefix matches it. relaton-cli rescues
|
|
17
|
+
# it, logs the message and returns no document.
|
|
18
|
+
class UnknownReferenceError < Error
|
|
19
|
+
# @return [String] the reference no flavor recognizes
|
|
20
|
+
attr_reader :reference
|
|
21
|
+
|
|
22
|
+
# @param reference [String]
|
|
23
|
+
def initialize(reference)
|
|
24
|
+
@reference = reference
|
|
25
|
+
super("`#{reference}` is not a recognized standards identifier")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
10
28
|
|
|
11
29
|
module Core
|
|
12
30
|
RequestError = Relaton::RequestError
|
data/lib/relaton/db/cache.rb
CHANGED
|
@@ -27,7 +27,9 @@ module Relaton
|
|
|
27
27
|
VERSIONS_KEY = "_versions".freeze
|
|
28
28
|
STRING_FLAVOR = "_key".freeze
|
|
29
29
|
PUBID_FLAVOR = "_pubid".freeze
|
|
30
|
+
# The legacy string form of a not-found entry (`not_found <date>`).
|
|
30
31
|
NOT_FOUND = /\Anot_found/
|
|
32
|
+
# Days an undated entry, or any not_found entry, stays valid.
|
|
31
33
|
UNDATED_TTL = 60
|
|
32
34
|
WRAPPED_KEY = /\A(?<prefix>[^(\s]+)\((?<code>.+)\)\z/m
|
|
33
35
|
# What a row may add to a dated key and still answer it: `===` also
|
|
@@ -76,20 +78,29 @@ module Relaton
|
|
|
76
78
|
@versions = {}
|
|
77
79
|
end
|
|
78
80
|
|
|
79
|
-
# Read the
|
|
80
|
-
#
|
|
81
|
+
# Read the entry for a key: the row with this key, else, for a dated
|
|
82
|
+
# pubid, a row the key is a subset of.
|
|
81
83
|
#
|
|
82
84
|
# @param key [Pubid::Identifier, String]
|
|
83
|
-
# @return [String, nil]
|
|
84
|
-
def
|
|
85
|
+
# @return [String, Relaton::Db::NotFound, nil] document XML or not found
|
|
86
|
+
def read(key)
|
|
85
87
|
found = lookup(key)
|
|
86
88
|
found && read_entry(found.last)
|
|
87
89
|
end
|
|
90
|
+
|
|
91
|
+
# The entry for a key as a string, a not-found entry as
|
|
92
|
+
# `not_found <date>`.
|
|
93
|
+
#
|
|
94
|
+
# @param key [Pubid::Identifier, String]
|
|
95
|
+
# @return [String, nil]
|
|
96
|
+
def [](key)
|
|
97
|
+
read(key)&.to_s
|
|
98
|
+
end
|
|
88
99
|
alias get []
|
|
89
100
|
|
|
90
101
|
# @param key [Pubid::Identifier, String]
|
|
91
|
-
# @param value [String, nil] document XML
|
|
92
|
-
# deletes the row
|
|
102
|
+
# @param value [String, Relaton::Db::NotFound, nil] document XML, a
|
|
103
|
+
# not-found entry (also as `not_found <date>`); nil deletes the row
|
|
93
104
|
def []=(key, value)
|
|
94
105
|
store key, value
|
|
95
106
|
end
|
|
@@ -100,18 +111,20 @@ module Relaton
|
|
|
100
111
|
# point to one document file.
|
|
101
112
|
#
|
|
102
113
|
# @param key [Pubid::Identifier, String] query key
|
|
103
|
-
# @param value [String, nil] document XML
|
|
114
|
+
# @param value [String, Relaton::Db::NotFound, nil] document XML, a
|
|
115
|
+
# not-found entry (also as `not_found <date>`); nil deletes the row
|
|
104
116
|
# @param item_key [Pubid::Identifier, String, nil] the document's key
|
|
105
|
-
# @return [String, nil] value
|
|
117
|
+
# @return [String, Relaton::Db::NotFound, nil] value
|
|
106
118
|
#
|
|
107
119
|
def store(key, value, item_key: nil) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
108
120
|
return delete(key) if value.nil?
|
|
109
121
|
|
|
110
122
|
bucket, key = resolve key
|
|
123
|
+
value = coerce value
|
|
111
124
|
fetched = fetched_of value
|
|
112
125
|
record_version bucket
|
|
113
|
-
if value.
|
|
114
|
-
upsert bucket,
|
|
126
|
+
if value.is_a? NotFound
|
|
127
|
+
upsert bucket, new_row(key, CacheEntry::NOT_FOUND, nil, fetched)
|
|
115
128
|
return value
|
|
116
129
|
end
|
|
117
130
|
|
|
@@ -120,9 +133,9 @@ module Relaton
|
|
|
120
133
|
record_version item_bucket
|
|
121
134
|
@rows.adapter.transaction do
|
|
122
135
|
@docs.set file, value
|
|
123
|
-
upsert item_bucket,
|
|
136
|
+
upsert item_bucket, new_row(item_key, CacheEntry::DOC, file, fetched)
|
|
124
137
|
unless canonical(key) == canonical(item_key)
|
|
125
|
-
upsert bucket,
|
|
138
|
+
upsert bucket, new_row(key, CacheEntry::DOC, file, fetched)
|
|
126
139
|
end
|
|
127
140
|
end
|
|
128
141
|
value
|
|
@@ -154,12 +167,16 @@ module Relaton
|
|
|
154
167
|
end
|
|
155
168
|
|
|
156
169
|
#
|
|
157
|
-
# Save the row of a key, and its document, from another cache.
|
|
170
|
+
# Save the row of a key, and its document, from another cache. A row
|
|
171
|
+
# the cache already holds is kept: a local cache keeps its own value
|
|
172
|
+
# for a key the global cache also holds (local over global).
|
|
158
173
|
#
|
|
159
174
|
# @param key [Pubid::Identifier, String]
|
|
160
175
|
# @param other [Relaton::Db::Cache]
|
|
161
176
|
#
|
|
162
177
|
def clone_entry(key, other)
|
|
178
|
+
return if lookup(key)
|
|
179
|
+
|
|
163
180
|
found = other.lookup(key) or return
|
|
164
181
|
row_id, row = found
|
|
165
182
|
store row_id, other.read_entry(row)
|
|
@@ -171,7 +188,9 @@ module Relaton
|
|
|
171
188
|
lookup(key)&.last&.dig("fetched")
|
|
172
189
|
end
|
|
173
190
|
|
|
174
|
-
# An undated entry expires after 60 days, a dated
|
|
191
|
+
# An undated entry expires after 60 days, a dated document never. A
|
|
192
|
+
# not_found entry expires after 60 days even for a dated query: the
|
|
193
|
+
# document can be published later.
|
|
175
194
|
# @param key [Pubid::Identifier, String]
|
|
176
195
|
# @param year [String, nil]
|
|
177
196
|
def valid_entry?(key, year)
|
|
@@ -243,9 +262,11 @@ module Relaton
|
|
|
243
262
|
end
|
|
244
263
|
|
|
245
264
|
# @param row [Hash]
|
|
246
|
-
# @return [String, nil] document XML or
|
|
265
|
+
# @return [String, Relaton::Db::NotFound, nil] document XML or not found
|
|
247
266
|
def read_entry(row)
|
|
248
|
-
|
|
267
|
+
if row["status"] == CacheEntry::NOT_FOUND
|
|
268
|
+
return NotFound.new(fetched: row["fetched"])
|
|
269
|
+
end
|
|
249
270
|
|
|
250
271
|
@docs.get row["file"]
|
|
251
272
|
end
|
|
@@ -405,7 +426,9 @@ module Relaton
|
|
|
405
426
|
def valid_row?(row, year)
|
|
406
427
|
return false unless read_entry(row)
|
|
407
428
|
|
|
408
|
-
year
|
|
429
|
+
return year if year && row["status"] == CacheEntry::DOC
|
|
430
|
+
|
|
431
|
+
Date.today - Date.parse(row["fetched"]) < UNDATED_TTL
|
|
409
432
|
end
|
|
410
433
|
|
|
411
434
|
#
|
|
@@ -442,7 +465,7 @@ module Relaton
|
|
|
442
465
|
end
|
|
443
466
|
end
|
|
444
467
|
|
|
445
|
-
def
|
|
468
|
+
def new_row(key, status, file, fetched)
|
|
446
469
|
row = CacheEntry.new(status: status, file: file, fetched: fetched)
|
|
447
470
|
if key.is_a?(String) then row.key = key
|
|
448
471
|
else row.id = canonical(key)
|
|
@@ -517,8 +540,15 @@ module Relaton
|
|
|
517
540
|
@docs.delete file unless used
|
|
518
541
|
end
|
|
519
542
|
|
|
543
|
+
# A legacy `not_found <date>` string becomes a NotFound value.
|
|
544
|
+
def coerce(value)
|
|
545
|
+
return value unless value.is_a?(String) && value.match?(NOT_FOUND)
|
|
546
|
+
|
|
547
|
+
NotFound.new(fetched: value[/\d{4}-\d{2}-\d{2}/] || Date.today.to_s)
|
|
548
|
+
end
|
|
549
|
+
|
|
520
550
|
def fetched_of(value)
|
|
521
|
-
date = if value.
|
|
551
|
+
date = if value.is_a? NotFound then value.fetched
|
|
522
552
|
else value[%r{<fetched>\s*([^<\s]+)\s*</fetched>}, 1]
|
|
523
553
|
end
|
|
524
554
|
date || Date.today.to_s
|
|
@@ -2,6 +2,15 @@ require "lutaml/model"
|
|
|
2
2
|
|
|
3
3
|
module Relaton
|
|
4
4
|
class Db
|
|
5
|
+
# The cached answer "the flavor has no such document", with the date it
|
|
6
|
+
# was fetched. Db and Cache pass it in place of the document XML.
|
|
7
|
+
NotFound = Data.define(:fetched) do
|
|
8
|
+
# @return [String] the legacy string form, `not_found <date>`
|
|
9
|
+
def to_s
|
|
10
|
+
"not_found #{fetched}"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
5
14
|
# One row of the cache index. A row is keyed either by a pubid (`id`, its
|
|
6
15
|
# `to_hash`) or, for a processor with no pubid class, by a string (`key`).
|
|
7
16
|
# `file` names the document in the doc store; several rows can share one
|
data/lib/relaton/db/registry.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "singleton"
|
|
2
|
+
require_relative "../core/request_error"
|
|
2
3
|
|
|
3
4
|
module Relaton
|
|
4
5
|
class Db
|
|
@@ -111,37 +112,42 @@ module Relaton
|
|
|
111
112
|
end
|
|
112
113
|
|
|
113
114
|
#
|
|
114
|
-
#
|
|
115
|
-
# name.
|
|
116
|
-
PUBID_FLAVOR_ALIASES = { "cencenelec" => "cen", "tgpp" => "3gpp" }.freeze
|
|
117
|
-
|
|
118
|
-
# Flavors routed by the parsed Pubid class (relaton#205, pilot). Only
|
|
119
|
-
# processors on this list answer parse-first routing: an allowlist,
|
|
120
|
-
# not a guess. Pubid::Un is deliberately absent — its Document
|
|
121
|
-
# identifier parses DOI-shaped strings ("10.17487/RFC3986") that must
|
|
122
|
-
# not be hijacked away from their current handling. DOI and ISBN are
|
|
123
|
-
# present: their canonical forms carry their own token (`doi:…`,
|
|
124
|
-
# `ISBN …`), which pubid detects exactly as the prefix regex routes it.
|
|
125
|
-
PARSE_ROUTED_FLAVORS = %w[
|
|
126
|
-
bipm bs cencenelec calconnect cc ccsds cen cie csa doi
|
|
127
|
-
ecma ecs etsi gost iala iana iec ieee ietf isbn iso itu
|
|
128
|
-
jcgm jis nist oasis ogc oiml plateau w3c xsf 3gpp
|
|
129
|
-
].freeze
|
|
130
|
-
|
|
131
|
-
# Find the processor that owns the parsed Pubid's flavor. The
|
|
132
|
-
# namespace is matched against the processor short name, with the
|
|
133
|
-
# alias table for the spellings that differ.
|
|
115
|
+
# Route a reference to its flavor (relaton#205).
|
|
134
116
|
#
|
|
135
|
-
#
|
|
136
|
-
#
|
|
117
|
+
# 1. The reference is parsed with pubid. Only the parsing flavor's own
|
|
118
|
+
# **exact** parse counts (#exact_parse): `Pubid.parse` falls back to a
|
|
119
|
+
# partial parse by any flavor (`ATN5014` as `IEC ATN5014`), and a
|
|
120
|
+
# permissive grammar reads another publisher's string exactly
|
|
121
|
+
# (`ISO REF` as IEC).
|
|
122
|
+
# 2. The parse routes by class ancestry (#processor_by_pubid), so the
|
|
123
|
+
# registration order does not matter.
|
|
124
|
+
# 3. A co-published identifier routes to the co-publisher its printed
|
|
125
|
+
# form names first (`ISO/IEC …` → ISO, `IEC/ISO …` → IEC). This is
|
|
126
|
+
# relaton's routing policy (#205): there is no canonical form of the
|
|
127
|
+
# identifier, pubid does not route records (pubid#469), and
|
|
128
|
+
# `Pubid.parse` tries the owners of a joint prefix in alphabetical
|
|
129
|
+
# order.
|
|
130
|
+
# 4. Otherwise the prefix regex (#class_by_ref) routes: a combined
|
|
131
|
+
# reference, the `PREFIX(code)` wrapper, a spelling a flavor
|
|
132
|
+
# normalizes, a flavor's free text.
|
|
133
|
+
#
|
|
134
|
+
# @param reference [String]
|
|
135
|
+
# @return [Array(Symbol, Pubid::Identifier)] the standard class, and the
|
|
136
|
+
# parsed pubid when it is that flavor's own exact parse (else nil)
|
|
137
|
+
# @raise [Relaton::UnknownReferenceError] no flavor recognizes it
|
|
137
138
|
#
|
|
138
|
-
def
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
def route(reference) # rubocop:disable Metrics/CyclomaticComplexity
|
|
140
|
+
parsed = exact_parse reference
|
|
141
|
+
stdclass = parsed && class_by_parsed(parsed, reference)
|
|
142
|
+
parsed = nil unless stdclass
|
|
143
|
+
lead = joint_lead reference
|
|
144
|
+
return [lead, stdclass == lead ? parsed : nil] if lead
|
|
145
|
+
return [stdclass, parsed] if stdclass
|
|
142
146
|
|
|
143
|
-
|
|
144
|
-
|
|
147
|
+
stdclass = class_by_ref(reference)
|
|
148
|
+
raise UnknownReferenceError, reference unless stdclass
|
|
149
|
+
|
|
150
|
+
[stdclass, nil]
|
|
145
151
|
end
|
|
146
152
|
|
|
147
153
|
# Find processor by refernce or prefix
|
|
@@ -189,8 +195,102 @@ module Relaton
|
|
|
189
195
|
processors_by_prefix(prefix).map { |processor| flavor_module(processor) }
|
|
190
196
|
end
|
|
191
197
|
|
|
198
|
+
#
|
|
199
|
+
# The flavor that serves a publisher's catalog: the processor whose own
|
|
200
|
+
# prefix is the publisher (`IEC` → `:relaton_iec`). Nil for a publisher
|
|
201
|
+
# no flavor serves (`ASTM`). Used for a co-publisher (relaton#205).
|
|
202
|
+
#
|
|
203
|
+
# @param publisher [String]
|
|
204
|
+
# @return [Symbol, nil] the standard class
|
|
205
|
+
#
|
|
206
|
+
def class_by_publisher(publisher)
|
|
207
|
+
processors_by_prefix(publisher)
|
|
208
|
+
.detect { |processor| processor.prefix.casecmp?(publisher) }&.short
|
|
209
|
+
end
|
|
210
|
+
|
|
192
211
|
private
|
|
193
212
|
|
|
213
|
+
# The reference parsed by pubid, when the parse renders the reference
|
|
214
|
+
# back (pubid's round-trip test). pubid itself is loaded here, not at
|
|
215
|
+
# registration. `Pubid.parse` raises a plain ArgumentError for a URN no
|
|
216
|
+
# flavor owns (`urn:foo:bar`): that is no parse either.
|
|
217
|
+
#
|
|
218
|
+
# @param reference [String]
|
|
219
|
+
# @return [Pubid::Identifier, nil]
|
|
220
|
+
def exact_parse(reference)
|
|
221
|
+
require "pubid"
|
|
222
|
+
parsed = ::Pubid.parse reference
|
|
223
|
+
parsed if parsed.to_s == reference
|
|
224
|
+
rescue ArgumentError, ::Pubid::Errors::Error, Parslet::ParseFailed
|
|
225
|
+
nil
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Whether the flavor of +pubid+ owns a prefix +reference+ starts with.
|
|
229
|
+
def claims?(pubid, reference)
|
|
230
|
+
flavor = ::Pubid.const_get pubid.class.name.split("::")[1]
|
|
231
|
+
return false unless flavor.respond_to?(:prefixes)
|
|
232
|
+
|
|
233
|
+
flavor.prefixes.any? { |pref| prefix_of?(pref, reference) }
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# A prefix matches when the reference ends there or goes on with a
|
|
237
|
+
# separator, or when the prefix itself ends with one (`doi:10.…`).
|
|
238
|
+
def prefix_of?(prefix, reference)
|
|
239
|
+
return false unless reference.start_with?(prefix)
|
|
240
|
+
|
|
241
|
+
rest = reference[prefix.length]
|
|
242
|
+
rest.nil? || !alnum?(rest) || !alnum?(prefix[-1])
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def alnum?(char)
|
|
246
|
+
char.match?(/[[:alnum:]]/)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# The flavor of an exact parse, when the parse is that flavor's own: the
|
|
250
|
+
# flavor claims the reference by one of its prefixes, or its identifiers
|
|
251
|
+
# print without one (Core::Processor#bare_identifiers?). A permissive
|
|
252
|
+
# grammar reads other publishers' strings exactly (`ISO REF` as IEC,
|
|
253
|
+
# `ABC 123456` as GB, a bare DOI as UN), but it does not claim them.
|
|
254
|
+
#
|
|
255
|
+
# @param pubid [Pubid::Identifier]
|
|
256
|
+
# @param reference [String]
|
|
257
|
+
# @return [Symbol, nil] the standard class
|
|
258
|
+
def class_by_parsed(pubid, reference)
|
|
259
|
+
processor = processor_by_pubid(pubid) or return
|
|
260
|
+
return unless processor.bare_identifiers? || claims?(pubid, reference)
|
|
261
|
+
|
|
262
|
+
processor.short
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# The co-publisher that a joint prefix names first (`ISO/IEC 27001` →
|
|
266
|
+
# ISO). Nil unless the reference starts with a prefix several flavors
|
|
267
|
+
# own.
|
|
268
|
+
#
|
|
269
|
+
# @param reference [String]
|
|
270
|
+
# @return [Symbol, nil] the standard class
|
|
271
|
+
def joint_lead(reference)
|
|
272
|
+
prefix = joint_prefixes.detect do |pref|
|
|
273
|
+
reference == pref || reference.start_with?("#{pref} ")
|
|
274
|
+
end
|
|
275
|
+
return unless prefix
|
|
276
|
+
|
|
277
|
+
class_by_publisher prefix.split("/").first
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Prefixes that more than one flavor owns (`ISO/IEC`, `ISO/IEC/IEEE`),
|
|
281
|
+
# longest first.
|
|
282
|
+
#
|
|
283
|
+
# @return [Array<String>]
|
|
284
|
+
def joint_prefixes
|
|
285
|
+
@joint_prefixes ||= processors.values.flat_map(&:prefixes).uniq
|
|
286
|
+
.select { |pref| pref.include?("/") && joint?(pref) }
|
|
287
|
+
.sort_by { |pref| -pref.size }
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def joint?(prefix)
|
|
291
|
+
processors_by_prefix(prefix).size > 1
|
|
292
|
+
end
|
|
293
|
+
|
|
194
294
|
# The flavor namespace for a processor: the module enclosing its class.
|
|
195
295
|
# Relaton::Iso::Processor -> Relaton::Iso. Derived from the class name so
|
|
196
296
|
# there's no @short string-munging and no 3gpp special case.
|