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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/lib/relaton/3gpp/bibliography.rb +19 -6
  3. data/lib/relaton/3gpp/processor.rb +6 -0
  4. data/lib/relaton/bib/item_data.rb +14 -0
  5. data/lib/relaton/bipm/bibliography.rb +24 -21
  6. data/lib/relaton/bsi/bibliography.rb +26 -23
  7. data/lib/relaton/calconnect/bibliography.rb +6 -6
  8. data/lib/relaton/calconnect/hit_collection.rb +4 -1
  9. data/lib/relaton/ccsds/bibliography.rb +15 -10
  10. data/lib/relaton/ccsds/hit_collection.rb +1 -1
  11. data/lib/relaton/ccsds/processor.rb +3 -2
  12. data/lib/relaton/cen/bibliography.rb +27 -24
  13. data/lib/relaton/cie/bibliography.rb +11 -9
  14. data/lib/relaton/cie/scrapper.rb +15 -8
  15. data/lib/relaton/core/processor.rb +61 -8
  16. data/lib/relaton/core/request_error.rb +21 -3
  17. data/lib/relaton/db/cache.rb +49 -19
  18. data/lib/relaton/db/cache_entry.rb +9 -0
  19. data/lib/relaton/db/registry.rb +128 -28
  20. data/lib/relaton/db.rb +162 -48
  21. data/lib/relaton/ecma/bibliography.rb +18 -13
  22. data/lib/relaton/etsi/bibliography.rb +8 -7
  23. data/lib/relaton/gb/bibliography.rb +20 -12
  24. data/lib/relaton/iala/bibliography.rb +15 -12
  25. data/lib/relaton/iana/bibliography.rb +12 -12
  26. data/lib/relaton/iana/parser.rb +5 -2
  27. data/lib/relaton/iec/bibliography.rb +18 -7
  28. data/lib/relaton/iec/processor.rb +5 -0
  29. data/lib/relaton/ieee/bibliography.rb +19 -13
  30. data/lib/relaton/ieee/processor.rb +16 -0
  31. data/lib/relaton/ieee/rawbib_id_parser.rb +2 -2
  32. data/lib/relaton/ietf/bibliography.rb +11 -9
  33. data/lib/relaton/ietf/scraper.rb +5 -4
  34. data/lib/relaton/iso/bibliography.rb +11 -9
  35. data/lib/relaton/itu/bibliography.rb +9 -4
  36. data/lib/relaton/jis/bibliography.rb +22 -11
  37. data/lib/relaton/nist/bibliography.rb +25 -21
  38. data/lib/relaton/oasis/bibliography.rb +16 -13
  39. data/lib/relaton/ogc/bibliography.rb +15 -14
  40. data/lib/relaton/ogc/hit_collection.rb +9 -7
  41. data/lib/relaton/ogc/processor.rb +7 -1
  42. data/lib/relaton/omg/bibliography.rb +9 -9
  43. data/lib/relaton/omg/scraper.rb +3 -2
  44. data/lib/relaton/plateau/bibliography.rb +9 -7
  45. data/lib/relaton/plateau/hit_collection.rb +2 -1
  46. data/lib/relaton/version.rb +1 -1
  47. data/lib/relaton/xsf/bibliography.rb +11 -6
  48. metadata +18 -4
data/lib/relaton/db.rb CHANGED
@@ -63,25 +63,23 @@ module Relaton
63
63
  # or "YYYY-MM-DD")
64
64
  #
65
65
  # @return [nil, RelatonBib::BibliographicItem, ...]
66
+ # @raise [Relaton::UnknownReferenceError] no flavor recognizes the
67
+ # reference (see Registry#route)
66
68
  ##
67
- def fetch(text, year = nil, opts = {})
69
+ def fetch(text, year = nil, opts = {}) # rubocop:disable Metrics/MethodLength
68
70
  reference = text.strip
69
- require "pubid"
70
- parsed = begin
71
- Pubid.parse(reference)
72
- rescue Pubid::Errors::Error, Parslet::ParseFailed
73
- nil
74
- end
75
- stdclass = (parsed && @registry.class_by_pubid(parsed)) ||
76
- @registry.class_by_ref(reference) || return
71
+ stdclass, pubid = @registry.route(reference)
77
72
  processor = @registry[stdclass]
78
73
  ref = if processor.respond_to?(:urn_to_code)
79
74
  processor.urn_to_code(reference)&.first
80
75
  else reference
81
76
  end
82
77
  ref ||= reference
78
+ # The routed pubid is the parse of `reference`; a URN rewritten to a
79
+ # code is parsed again by the flavor.
80
+ pubid = nil unless ref == reference
83
81
  result = combine_doc ref, year, opts, stdclass
84
- result || check_bibliocache(ref, year, opts, stdclass)
82
+ result || fetch_routed(ref, year, opts, stdclass, pubid)
85
83
  end
86
84
 
87
85
  # @see Relaton::Db#fetch
@@ -119,7 +117,7 @@ module Relaton
119
117
  # nil if document not found
120
118
  #
121
119
  def fetch_async(ref, year = nil, opts = {}, &block) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
122
- stdclass = @registry.class_by_ref ref
120
+ stdclass = async_route ref
123
121
  if stdclass
124
122
  unless @queues[stdclass]
125
123
  processor = @registry[stdclass]
@@ -141,23 +139,32 @@ module Relaton
141
139
  end
142
140
  end
143
141
 
142
+ #
143
+ # Fetch with the flavor the caller names, else the routed one.
144
+ #
145
+ # @param stdclass [Symbol, String, nil] a processor short name
146
+ # (`:relaton_iso`, as relaton-cli passes it) or a prefix (`"ISO"`)
147
+ # @raise [Relaton::UnknownReferenceError] no flavor is named and none
148
+ # recognizes the reference
149
+ #
144
150
  def fetch_std(code, year = nil, stdclass = nil, opts = {})
145
- std = nil
146
- @registry.processors.each do |name, processor|
147
- std = name if processor.prefix == stdclass
148
- end
149
- std = @registry.class_by_ref(code) or return nil unless std
151
+ std = named_class(stdclass)
152
+ # A flavor the caller names is the one asked: no co-publisher.
153
+ return check_bibliocache(code, year, opts, std) if std
150
154
 
151
- check_bibliocache(code, year, opts, std)
155
+ std, pubid = @registry.route(code)
156
+ fetch_routed(code, year, opts, std, pubid)
152
157
  end
153
158
 
154
159
  # The document identifier class corresponding to the given code
155
160
  # @param code [String]
156
161
  # @return [Array]
157
162
  def docid_type(code)
158
- stdclass = @registry.class_by_ref(code) or return [nil, code]
163
+ stdclass, = @registry.route(code)
159
164
  _, code = strip_id_wrapper(code, stdclass)
160
165
  [@registry[stdclass].idtype, code]
166
+ rescue UnknownReferenceError
167
+ [nil, code]
161
168
  end
162
169
 
163
170
  # @param key [String]
@@ -186,19 +193,42 @@ module Relaton
186
193
 
187
194
  private
188
195
 
189
- def fetch_doc(code, year, opts, processor)
190
- if Db.configuration.use_api then fetch_api(code, year, opts, processor)
191
- else processor.get(code, year, opts)
196
+ # The flavor routing picks for `fetch_async`'s per-flavor queue; nil
197
+ # (logged) when no flavor recognizes the reference.
198
+ def async_route(ref)
199
+ @registry.route(ref).first
200
+ rescue UnknownReferenceError => e
201
+ Util.info e.message, key: ref
202
+ nil
203
+ end
204
+
205
+ # The processor a caller names by short name (`:relaton_iso`) or prefix.
206
+ def named_class(stdclass)
207
+ return unless stdclass
208
+
209
+ short = stdclass.to_sym
210
+ return short if @registry.processors.key?(short)
211
+
212
+ @registry.processors.detect { |_, p| p.prefix == stdclass.to_s }&.first
213
+ end
214
+
215
+ # The flavor's `get` receives the parsed pubid when there is one
216
+ # (relaton#205), else the String. The API request sends the String the
217
+ # caller wrote.
218
+ def fetch_doc(code, year, opts, processor, query = nil)
219
+ if Db.configuration.use_api
220
+ fetch_api(code, year, opts, processor, query)
221
+ else processor.get(query || code, year, opts)
192
222
  end
193
223
  end
194
224
 
195
- def fetch_api(code, year, opts, processor)
225
+ def fetch_api(code, year, opts, processor, query = nil)
196
226
  url = "#{Db.configuration.api_host}" \
197
227
  "/api/v1/document?#{params(code, year, opts)}"
198
228
  rsp = Net::HTTP.get_response URI(url)
199
229
  processor.from_xml rsp.body if rsp.code == "200"
200
230
  rescue Errno::ECONNREFUSED
201
- processor.get(code, year, opts)
231
+ processor.get(query || code, year, opts)
202
232
  end
203
233
 
204
234
  def params(code, year, opts)
@@ -281,15 +311,36 @@ module Relaton
281
311
  # that query is not cached. A processor with no pubid class gets the
282
312
  # legacy string key.
283
313
  #
314
+ # @param pubid [Pubid::Identifier, nil] the reference as routing parsed
315
+ # it, so the processor does not parse it again
284
316
  # @return [Pubid::Identifier, String, nil]
285
317
  #
286
- def cache_key(code, year, opts, stdclass)
318
+ def cache_key(code, year, opts, stdclass, pubid = nil)
287
319
  processor = @registry[stdclass]
288
320
  unless processor.pubid_class
289
321
  return std_id(code, year, opts, stdclass).first
290
322
  end
291
323
 
292
- processor.cache_key(code, year, opts)
324
+ processor.cache_key(code, year, opts, pubid)
325
+ end
326
+
327
+ #
328
+ # The pubid `get` receives, and the cache key folded from it: one parse
329
+ # of the reference serves both (relaton#205). A processor with no pubid
330
+ # class gets the String and the legacy string key.
331
+ #
332
+ # @param pubid [Pubid::Identifier, nil] routing's parse
333
+ # @return [Array(Pubid::Identifier, Object)] the query pubid (nil: `get`
334
+ # gets the String) and the key (nil: not cached)
335
+ #
336
+ def query_and_key(code, year, opts, stdclass, pubid)
337
+ processor = @registry[stdclass]
338
+ unless processor.pubid_class
339
+ return [nil, std_id(code, year, opts, stdclass).first]
340
+ end
341
+
342
+ query = processor.query_pubid(code, opts, pubid)
343
+ [query, query && processor.cache_key(code, year, opts, query)]
293
344
  end
294
345
 
295
346
  # The key of a fetched document, from its primary identifier. The
@@ -317,53 +368,112 @@ module Relaton
317
368
  [prefix, code]
318
369
  end
319
370
 
320
- def bib_retval(entry, stdclass)
321
- if entry && !entry.match?(/^not_found/)
322
- @registry[stdclass].from_xml(entry)
371
+ # The routed flavor, then its co-publishers' flavors (relaton#205).
372
+ def fetch_routed(code, year, opts, stdclass, pubid)
373
+ check_bibliocache(code, year, opts, stdclass, pubid: pubid) ||
374
+ copublisher_fetch(code, year, opts, stdclass, pubid)
375
+ end
376
+
377
+ #
378
+ # A co-published document the lead flavor's catalog does not have:
379
+ # ask the co-publishers' flavors, in the order the parsed pubid holds
380
+ # them (relaton#205; pubid#469, #472). Each one keys, caches and fetches
381
+ # with its own flavor, so a second fetch is answered from the caches.
382
+ # A co-publisher whose flavor gives no pubid for the lead's printed form
383
+ # is skipped. Only that probe parse is rescued: an error from inside the
384
+ # co-publisher's fetch propagates.
385
+ #
386
+ # @return [Relaton::Bib::ItemData, nil]
387
+ #
388
+ def copublisher_fetch(code, year, opts, stdclass, pubid)
389
+ # The reference as the lead read it: no `PREFIX(...)` wrapper, no en dash.
390
+ _, code = strip_id_wrapper(code, stdclass)
391
+ copublisher_classes(code, opts, stdclass, pubid).each do |publisher, co|
392
+ Util.info "Not found; trying co-publisher `#{publisher}`", key: code
393
+ query = copublisher_query(code, opts, co, publisher) or next
394
+ bib = check_bibliocache(code, year, opts, co, pubid: query)
395
+ return bib if bib
396
+ end
397
+ nil
398
+ end
399
+
400
+ # @return [Array<Array(String, Symbol)>] each co-publisher that a flavor
401
+ # other than the lead serves, with that flavor
402
+ def copublisher_classes(code, opts, stdclass, pubid)
403
+ processor = @registry[stdclass]
404
+ query = processor.query_pubid(code, opts, pubid) or return []
405
+
406
+ processor.copublishers(query).filter_map do |publisher|
407
+ co = @registry.class_by_publisher(publisher)
408
+ [publisher, co] if co && co != stdclass
409
+ end.uniq(&:last)
410
+ end
411
+
412
+ # The co-publisher flavor's own parse of the reference, or nil when it
413
+ # gives none (a parse error, or a flavor that reads it as a miss: IEEE),
414
+ # which is logged.
415
+ #
416
+ # @return [Pubid::Identifier, nil]
417
+ def copublisher_query(code, opts, stdclass, publisher)
418
+ query = begin
419
+ @registry[stdclass].query_pubid(code, opts)
420
+ rescue ::Pubid::Errors::Error, Parslet::ParseFailed
421
+ nil
323
422
  end
423
+ return query if query
424
+
425
+ Util.info "Not found; co-publisher `#{publisher}` cannot read it",
426
+ key: code
427
+ nil
428
+ end
429
+
430
+ def bib_retval(entry, stdclass)
431
+ @registry[stdclass].from_xml(entry) if entry.is_a?(String)
324
432
  end
325
433
 
326
- def check_bibliocache(code, year, opts, stdclass) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
434
+ def check_bibliocache(code, year, opts, stdclass, pubid: nil) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
327
435
  _, searchcode = strip_id_wrapper(code, stdclass)
328
- id = cache_key(searchcode, year, opts, stdclass)
436
+ query, id = query_and_key(searchcode, year, opts, stdclass, pubid)
329
437
  db = id && (@local_db || @db)
330
438
  altdb = @local_db && @db ? @db : nil
331
439
  if db.nil?
332
440
  return if opts[:fetch_db]
333
441
 
334
- bibentry = new_bib_entry(searchcode, year, opts, stdclass)
442
+ bibentry = new_bib_entry(searchcode, year, opts, stdclass, query: query)
335
443
  return bib_retval(bibentry, stdclass)
336
444
  end
337
445
  if date_range?(opts)
338
- return check_date_range(searchcode, id, year, opts, stdclass)
446
+ return check_date_range(searchcode, id, year, opts, stdclass, query)
339
447
  end
340
448
 
341
449
  @semaphore.synchronize { db.expire id, year }
342
450
  if altdb
343
- return bib_retval(altdb[id], stdclass) if opts[:fetch_db]
451
+ return bib_retval(altdb.read(id), stdclass) if opts[:fetch_db]
344
452
 
345
453
  @semaphore.synchronize do
346
454
  db.clone_entry id, altdb if altdb.valid_entry? id, year
347
455
  end
348
- new_bib_entry(searchcode, year, opts, stdclass, db: db, id: id)
456
+ new_bib_entry(searchcode, year, opts, stdclass, db: db, id: id,
457
+ query: query)
349
458
  @semaphore.synchronize do
350
459
  altdb.clone_entry(id, db) if !altdb.valid_entry?(id, year)
351
460
  end
352
461
  else
353
- return bib_retval(db[id], stdclass) if opts[:fetch_db]
462
+ return bib_retval(db.read(id), stdclass) if opts[:fetch_db]
354
463
 
355
- new_bib_entry(searchcode, year, opts, stdclass, db: db, id: id)
464
+ new_bib_entry(searchcode, year, opts, stdclass, db: db, id: id,
465
+ query: query)
356
466
  end
357
- bib_retval(db[id], stdclass)
467
+ bib_retval(db.read(id), stdclass)
358
468
  end
359
469
 
360
470
  def new_bib_entry(code, year, opts, stdclass, **args)
361
- entry = @semaphore.synchronize { args[:db] && args[:db][args[:id]] }
471
+ entry = @semaphore.synchronize { args[:db]&.read(args[:id]) }
362
472
  if !entry || opts[:no_cache]
363
473
  return fetch_entry(code, year, opts, stdclass, **args)
364
474
  end
365
475
 
366
- if entry&.match?(/^not_found/)
476
+ if entry.is_a?(NotFound)
367
477
  Util.info "not found in cache, if you wish to " \
368
478
  "ignore cache please use `no-cache` option.", key: code
369
479
  return
@@ -373,15 +483,16 @@ module Relaton
373
483
 
374
484
  def fetch_entry(code, year, opts, stdclass, **args) # rubocop:disable Metrics/AbcSize
375
485
  processor = @registry[stdclass]
376
- bib = net_retry(code, year, opts, processor, opts.fetch(:retries, 1))
486
+ bib = net_retry(code, year, opts, processor, opts.fetch(:retries, 1),
487
+ args[:query])
377
488
  entry = bib_entry bib
378
489
  return entry if args[:db].nil?
379
490
 
380
491
  # `no_cache` refreshes a cached entry, but a failed fetch does not
381
- # replace a cached document with `not_found`.
492
+ # replace a cached document with a not-found entry.
382
493
  refresh = opts[:no_cache] && bib.respond_to?(:to_xml)
383
494
  @semaphore.synchronize do
384
- if refresh || !args[:db][args[:id]]
495
+ if refresh || !args[:db].read(args[:id])
385
496
  save_bib args[:db], args[:id], bib, entry, stdclass
386
497
  end
387
498
  end
@@ -404,7 +515,7 @@ module Relaton
404
515
  # with the range, and its answer is cached under its own identifier only:
405
516
  # the query row keeps pointing to the latest edition.
406
517
  #
407
- def check_date_range(code, key, year, opts, stdclass) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
518
+ def check_date_range(code, key, year, opts, stdclass, query = nil) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
408
519
  caches = [@local_db, @db].compact
409
520
  cached = caches.flat_map { |c| c.candidates(key) }.filter_map do |_, xml|
410
521
  date = published_date(xml)
@@ -414,7 +525,8 @@ module Relaton
414
525
  return if opts[:fetch_db]
415
526
 
416
527
  processor = @registry[stdclass]
417
- bib = net_retry(code, year, opts, processor, opts.fetch(:retries, 1))
528
+ bib = net_retry(code, year, opts, processor, opts.fetch(:retries, 1),
529
+ query)
418
530
  return unless bib.respond_to?(:to_xml)
419
531
 
420
532
  entry = bib_entry bib
@@ -427,19 +539,21 @@ module Relaton
427
539
  bib_retval entry, stdclass
428
540
  end
429
541
 
430
- def net_retry(code, year, opts, processor, retries)
431
- fetch_doc code, year, opts, processor
542
+ # @param query [Pubid::Identifier, nil] what `get` receives in place of
543
+ # the String +code+ (Db#query_and_key)
544
+ def net_retry(code, year, opts, processor, retries, query = nil)
545
+ fetch_doc code, year, opts, processor, query
432
546
  rescue Relaton::RequestError => e
433
547
  raise e unless retries > 1
434
548
 
435
- net_retry(code, year, opts, processor, retries - 1)
549
+ net_retry(code, year, opts, processor, retries - 1, query)
436
550
  end
437
551
 
438
552
  def bib_entry(bib)
439
553
  if bib.respond_to?(:to_xml)
440
554
  bib.to_xml(bibdata: true)
441
555
  else
442
- "not_found #{Date.today}"
556
+ NotFound.new(fetched: Date.today.to_s)
443
557
  end
444
558
  end
445
559
 
@@ -43,7 +43,8 @@ module Relaton
43
43
  # `ECMA-418` does not match `ECMA-418-1`. The class must be identical,
44
44
  # which keeps `ECMA-100` and `ECMA TR/100` apart.
45
45
  #
46
- # @param ref [String] the ECMA reference (e.g. "ECMA-6", "ECMA-269 ed3 vol2")
46
+ # @param ref [String, Pubid::Ecma::Identifier] the ECMA reference
47
+ # (e.g. "ECMA-6", "ECMA-269 ed3 vol2"), or its parse
47
48
  #
48
49
  # @return [Array<Hash>] matching index rows
49
50
  #
@@ -62,7 +63,7 @@ module Relaton
62
63
  # regex accepted, so every reference shape the flavor has to handle
63
64
  # parses without normalization here.
64
65
  #
65
- # @param ref [String]
66
+ # @param ref [String, Pubid::Ecma::Identifier]
66
67
  # @return [Pubid::Ecma::Identifier, nil]
67
68
  #
68
69
  # An unrecognized reference **raises**; like ISO, ETSI and 3GPP we let it
@@ -73,20 +74,24 @@ module Relaton
73
74
  # Rescuing here would collapse "this identifier is malformed" into "no
74
75
  # such document", leaving a caller unable to tell them apart.
75
76
  def parse_ref(ref)
77
+ # A pubid from Relaton::Db (relaton#205) is used as it is.
78
+ return ref unless ref.is_a?(String)
79
+
76
80
  ::Pubid::Ecma::Identifier.parse ref.to_s.strip
77
81
  end
78
82
 
79
- # @param code [String] the ECMA standard Code to look up (e..g "ECMA-6")
83
+ # @param ref [String, Pubid::Ecma::Identifier] the ECMA reference
84
+ # (e.g. "ECMA-6"), or its parse from Relaton::Db (relaton#205)
80
85
  # @param year [String] not used
81
86
  # @param opts [Hash] not used
82
87
  # @return [Relaton::Ecma::ItemData] Relaton of reference
83
- def get(code, _year = nil, _opts = {})
84
- Util.info "Fetching from Relaton repository ...", key: code
85
- result = fetch_doc(code)
88
+ def get(ref, _year = nil, _opts = {})
89
+ Util.info "Fetching from Relaton repository ...", key: ref.to_s
90
+ result = fetch_doc(ref)
86
91
  if result
87
- Util.info "Found: `#{result.docidentifier.first.content}`", key: code
92
+ Util.info "Found: `#{result.docidentifier.first.content}`", key: ref.to_s
88
93
  else
89
- Util.info "Not found.", key: code
94
+ Util.info "Not found.", key: ref.to_s
90
95
  end
91
96
  result
92
97
  end
@@ -106,7 +111,7 @@ module Relaton
106
111
  #
107
112
  # `r[:file]` breaks the tie, because the index sort is not stable.
108
113
  #
109
- # @param ref [String]
114
+ # @param ref [String, Pubid::Ecma::Identifier]
110
115
  # @return [Hash, nil]
111
116
  #
112
117
  def best_match(ref)
@@ -127,8 +132,8 @@ module Relaton
127
132
  edition.to_s.split(".").map(&:to_i)
128
133
  end
129
134
 
130
- def fetch_doc(code)
131
- row = best_match code
135
+ def fetch_doc(ref)
136
+ row = best_match ref
132
137
  return unless row
133
138
 
134
139
  url = "#{ENDPOINT}#{row[:file]}"
@@ -137,11 +142,11 @@ module Relaton
137
142
  rescue Mechanize::ResponseCodeError => e
138
143
  return if e.response_code == "404"
139
144
 
140
- raise Relaton::RequestError, "No document found for #{code} reference. #{e.message}"
145
+ raise Relaton::RequestError, "No document found for #{ref} reference. #{e.message}"
141
146
  rescue Mechanize::RedirectLimitReachedError, Timeout::Error,
142
147
  Mechanize::UnauthorizedError, Mechanize::UnsupportedSchemeError,
143
148
  Mechanize::ResponseReadError, Mechanize::ChunkedTerminationError => e
144
- raise Relaton::RequestError, "No document found for #{code} reference. #{e.message}"
149
+ raise Relaton::RequestError, "No document found for #{ref} reference. #{e.message}"
145
150
  end
146
151
  end
147
152
  end
@@ -6,14 +6,15 @@ module Relaton
6
6
  module Bibliography
7
7
  SOURCE = "https://raw.githubusercontent.com/relaton/relaton-data-etsi/refs/heads/v2/"
8
8
 
9
- # @param text [String]
9
+ # @param ref [String, ::Pubid::Etsi::Identifier]
10
10
  # @return [Relaton::Etsi::ItemData, nil]
11
- def search(text) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
11
+ def search(ref) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
12
12
  # An unrecognized reference raises Pubid::Errors::ParseError; like
13
13
  # ISO we let it propagate — the CLI turns it into a friendly message
14
14
  # and API callers rescue it themselves. Valid partial refs parse with
15
15
  # the omitted refinements (version/date/part) left blank.
16
- pubid = ::Pubid::Etsi.parse text
16
+ # A parsed pubid comes from Relaton::Db (relaton#205); it is only read.
17
+ pubid = ref.is_a?(String) ? ::Pubid::Etsi.parse(ref) : ref
17
18
 
18
19
  index = Relaton::Index.find_or_create :etsi, url: "#{SOURCE}#{INDEXFILE}.zip", file: "#{INDEXFILE}.yaml",
19
20
  pubid_class: ::Pubid::Etsi::Identifier
@@ -89,19 +90,19 @@ module Relaton
89
90
  [(id.version&.version).to_s.split(".").map(&:to_i), id.date.to_s]
90
91
  end
91
92
 
92
- # @param ref [String] the ETSI standard Code to look up
93
+ # @param ref [String, ::Pubid::Etsi::Identifier] the ETSI standard Code to look up
93
94
  # @param year [String, nil] year
94
95
  # @param opts [Hash] options
95
96
  # @return [Relaton::Etsi::ItemData, nil]
96
97
  def get(ref, _year = nil, _opts = {})
97
- Util.info "Fetching from Relaton repository ...", key: ref
98
+ Util.info "Fetching from Relaton repository ...", key: ref.to_s
98
99
  result = search(ref)
99
100
  unless result
100
- Util.info "Not found.", key: ref
101
+ Util.info "Not found.", key: ref.to_s
101
102
  return
102
103
  end
103
104
 
104
- Util.info "Found: `#{result.docidentifier[0].content}`", key: ref
105
+ Util.info "Found: `#{result.docidentifier[0].content}`", key: ref.to_s
105
106
  result
106
107
  end
107
108
 
@@ -9,15 +9,15 @@ module Relaton
9
9
  class Bibliography
10
10
  class << self
11
11
  # rubocop:disable Metrics/MethodLength
12
- # @param text [Strin] code of standard for search
12
+ # @param ref [Strin] code of standard for search
13
13
  # @return [RelatonGb::HitCollection]
14
- def search(text)
15
- case text
14
+ def search(ref)
15
+ case ref
16
16
  when /^(GB|GJ|GS)/
17
17
  # Scrape national standards.
18
- Util.info "Fetching from openstd.samr.gov.cn ...", key: text
18
+ Util.info "Fetching from openstd.samr.gov.cn ...", key: ref
19
19
  require_relative "gb_scraper"
20
- GbScraper.scrape_page text
20
+ GbScraper.scrape_page ref
21
21
  # when /^ZB/
22
22
  # Scrape proffesional.
23
23
  # when /^DB/
@@ -26,26 +26,34 @@ module Relaton
26
26
  # Enterprise standard
27
27
  when %r{^T/[^\s]{2,6}\s}
28
28
  # Scrape social standard.
29
- Util.info "Fetching from www.ttbz.org.cn ...", key: text
29
+ Util.info "Fetching from www.ttbz.org.cn ...", key: ref
30
30
  require_relative "t_scraper"
31
- TScraper.scrape_page text
31
+ TScraper.scrape_page ref
32
32
  else
33
33
  # Scrape sector standard.
34
34
  require "relaton/gb/sec_scraper"
35
- SecScraper.scrape_page text
35
+ SecScraper.scrape_page ref
36
36
  end
37
37
  end
38
38
  # rubocop:enable Metrics/MethodLength
39
39
 
40
- # @param code [String] the GB standard Code to look up (e..g "GB/T 20223")
40
+ # @param ref [String, Pubid::Gb::Identifier] the GB standard Code to
41
+ # look up (e.g. "GB/T 20223"), or its parse from Relaton::Db
42
+ # (relaton#205)
41
43
  # @param year [String] the year the standard was published (optional)
42
44
  # @param opts [Hash] options; restricted to :all_parts if all-parts reference is required
43
45
  # @return [Relaton::Gb::ItemData, nil]
44
- # @raise [Pubid::Errors::ParseError] when the code is not a GB
46
+ # @raise [Pubid::Errors::ParseError] when the reference is not a GB
45
47
  # identifier
46
- def get(code, year = nil, opts = {})
48
+ def get(ref, year = nil, opts = {}) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
47
49
  require "pubid"
48
- pubid = ::Pubid::Gb::Identifier.parse(code)
50
+ # A parsed pubid comes from Relaton::Db (relaton#205); it is never
51
+ # mutated (`exclude` below copies).
52
+ pubid = ref.is_a?(String) ? ::Pubid::Gb::Identifier.parse(ref) : ref
53
+ if pubid.all_parts?
54
+ opts = opts.merge(all_parts: true)
55
+ pubid = pubid.identifiers.first
56
+ end
49
57
  year = (year || pubid.year)&.to_s
50
58
  pubid = pubid.exclude(:year)
51
59
  pubid.part = "1" if opts[:all_parts]
@@ -8,15 +8,15 @@ module Relaton
8
8
  class << self
9
9
  # Search for an IALA publication by its identifier.
10
10
  #
11
- # @param text [String] the IALA reference to look up (e.g. "IALA S1070")
11
+ # @param ref [String] the IALA reference to look up (e.g. "IALA S1070")
12
12
  # @param _year [String, nil] optional edition/year filter
13
13
  # @param _opts [Hash] options (unused)
14
14
  # @return [Relaton::Iala::Item, nil]
15
- def search(text, _year = nil, _opts = {})
16
- Util.info "Fetching from Relaton repository ...", key: text
17
- row = best_match text
15
+ def search(ref, _year = nil, _opts = {})
16
+ Util.info "Fetching from Relaton repository ...", key: ref.to_s
17
+ row = best_match ref
18
18
  unless row
19
- Util.info "Not found.", key: text
19
+ Util.info "Not found.", key: ref.to_s
20
20
  return
21
21
  end
22
22
 
@@ -27,7 +27,7 @@ module Relaton
27
27
  end
28
28
 
29
29
  item = Relaton::Iala::Item.from_yaml resp.body
30
- Util.info "Found: `#{item.docidentifier.first&.content}`", key: text
30
+ Util.info "Found: `#{item.docidentifier.first&.content}`", key: ref.to_s
31
31
  item.tap { |i| i.fetched = Date.today.to_s }
32
32
  rescue SocketError, Errno::EINVAL, Errno::ECONNRESET, EOFError,
33
33
  Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
@@ -59,7 +59,7 @@ module Relaton
59
59
  # must never match each other. An `Annex` row carries its base
60
60
  # identifier, which `===` compares with its own subset match.
61
61
  #
62
- # @param text [String]
62
+ # @param ref [String]
63
63
  # @return [Hash, nil]
64
64
  #
65
65
  # The substring-scan fallback this used to take when parsing failed is
@@ -68,8 +68,8 @@ module Relaton
68
68
  # ids, so an ambiguous reference silently resolved to whichever row
69
69
  # sorted first, instead of telling the caller the reference is not an
70
70
  # identifier. ecma, w3c and xsf never had one.
71
- def best_match(text)
72
- pubid = parse_ref text
71
+ def best_match(ref)
72
+ pubid = parse_ref ref
73
73
  rows = index.search(pubid)
74
74
  rows.max_by { |r| [edition_key(r[:id].edition), language_key(r[:id]), r[:file]] }
75
75
  end
@@ -114,7 +114,7 @@ module Relaton
114
114
  # zero-pads the number to its type's canonical width, so `IALA M1`,
115
115
  # `M0001` and `R1016:ed2.0(F)` all parse without normalization here.
116
116
  #
117
- # @param text [String]
117
+ # @param ref [String, Pubid::Iala::Identifier]
118
118
  # @return [Pubid::Iala::Identifier, nil]
119
119
  #
120
120
  # An unrecognized reference **raises**; like ISO, ETSI and 3GPP we let it
@@ -124,8 +124,11 @@ module Relaton
124
124
  # logs it through the `StandardError` arm at `lib/relaton/db.rb:122`.
125
125
  # Rescuing here would collapse "this identifier is malformed" into "no
126
126
  # such document", leaving a caller unable to tell them apart.
127
- def parse_ref(text)
128
- ::Pubid::Iala::Identifier.parse text.to_s.strip
127
+ def parse_ref(ref)
128
+ # A parsed pubid comes from Relaton::Db (relaton#205); it is used as it is.
129
+ return ref unless ref.is_a?(String)
130
+
131
+ ::Pubid::Iala::Identifier.parse ref.to_s.strip
129
132
  end
130
133
 
131
134
  # The index is pubid-backed: `pubid_class:` is what makes