relaton 3.0.0.pre.alpha.4 → 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 (120) 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/adobe/processor.rb +9 -0
  5. data/lib/relaton/bib/converter/csl.rb +110 -0
  6. data/lib/relaton/bib/converter/ris.rb +104 -0
  7. data/lib/relaton/bib/converter/titles.rb +24 -0
  8. data/lib/relaton/bib/item_data.rb +23 -0
  9. data/lib/relaton/bib/model/item.rb +6 -6
  10. data/lib/relaton/bib/sanitizer.rb +71 -30
  11. data/lib/relaton/bib.rb +10 -0
  12. data/lib/relaton/bipm/bibliography.rb +24 -21
  13. data/lib/relaton/bipm/processor.rb +1 -0
  14. data/lib/relaton/bipm/rawdata_bipm_metrologia/affiliations.rb +6 -6
  15. data/lib/relaton/bipm/si_brochure_parser.rb +2 -2
  16. data/lib/relaton/bsi/bibliography.rb +26 -23
  17. data/lib/relaton/bsi/processor.rb +5 -0
  18. data/lib/relaton/calconnect/bibliography.rb +6 -6
  19. data/lib/relaton/calconnect/hit_collection.rb +4 -1
  20. data/lib/relaton/ccsds/bibliography.rb +16 -10
  21. data/lib/relaton/ccsds/hit_collection.rb +1 -1
  22. data/lib/relaton/ccsds/processor.rb +13 -0
  23. data/lib/relaton/cen/bibliography.rb +27 -24
  24. data/lib/relaton/cen/hit_collection.rb +1 -1
  25. data/lib/relaton/cen/processor.rb +5 -0
  26. data/lib/relaton/cen/scraper.rb +9 -9
  27. data/lib/relaton/cie/bibliography.rb +11 -9
  28. data/lib/relaton/cie/data_fetcher.rb +18 -18
  29. data/lib/relaton/cie/processor.rb +1 -0
  30. data/lib/relaton/cie/scrapper.rb +15 -8
  31. data/lib/relaton/cie.rb +1 -1
  32. data/lib/relaton/cloud.rb +127 -0
  33. data/lib/relaton/core/hit_collection.rb +6 -10
  34. data/lib/relaton/core/processor.rb +121 -0
  35. data/lib/relaton/core/request_error.rb +21 -3
  36. data/lib/relaton/db/cache.rb +474 -148
  37. data/lib/relaton/db/cache_entry.rb +42 -0
  38. data/lib/relaton/db/registry.rb +152 -0
  39. data/lib/relaton/db.rb +275 -102
  40. data/lib/relaton/doi/crossref.rb +23 -6
  41. data/lib/relaton/doi/processor.rb +1 -0
  42. data/lib/relaton/easc/processor.rb +1 -0
  43. data/lib/relaton/ecma/bibliography.rb +18 -13
  44. data/lib/relaton/ecma/data_fetcher.rb +1 -1
  45. data/lib/relaton/ecma/data_parser.rb +1 -1
  46. data/lib/relaton/ecma/edition_parser.rb +2 -2
  47. data/lib/relaton/ecma/memento_parser.rb +4 -4
  48. data/lib/relaton/ecma/standard_parser.rb +6 -6
  49. data/lib/relaton/etsi/bibliography.rb +8 -7
  50. data/lib/relaton/etsi/processor.rb +1 -0
  51. data/lib/relaton/gb/bibliography.rb +20 -12
  52. data/lib/relaton/gb/gb_scraper.rb +5 -5
  53. data/lib/relaton/gb/scraper.rb +16 -16
  54. data/lib/relaton/gb/sec_scraper.rb +8 -8
  55. data/lib/relaton/gb/t_scraper.rb +5 -5
  56. data/lib/relaton/gost/processor.rb +1 -0
  57. data/lib/relaton/iala/bibliography.rb +15 -12
  58. data/lib/relaton/iala/processor.rb +1 -0
  59. data/lib/relaton/iana/bibliography.rb +12 -12
  60. data/lib/relaton/iana/data_fetcher.rb +3 -3
  61. data/lib/relaton/iana/parser.rb +12 -5
  62. data/lib/relaton/iana/processor.rb +9 -0
  63. data/lib/relaton/iec/bibliography.rb +18 -7
  64. data/lib/relaton/iec/data_parser.rb +24 -9
  65. data/lib/relaton/iec/processor.rb +11 -0
  66. data/lib/relaton/iec.rb +1 -1
  67. data/lib/relaton/ieee/bibliography.rb +19 -13
  68. data/lib/relaton/ieee/data_fetcher.rb +1 -1
  69. data/lib/relaton/ieee/processor.rb +24 -0
  70. data/lib/relaton/ieee/rawbib_id_parser.rb +2 -2
  71. data/lib/relaton/ietf/bibliography.rb +11 -9
  72. data/lib/relaton/ietf/data_fetcher.rb +1 -1
  73. data/lib/relaton/ietf/processor.rb +1 -0
  74. data/lib/relaton/ietf/rfc/entry.rb +15 -19
  75. data/lib/relaton/ietf/scraper.rb +5 -4
  76. data/lib/relaton/iho/processor.rb +1 -0
  77. data/lib/relaton/index/file_io.rb +2 -2
  78. data/lib/relaton/index/pool.rb +4 -3
  79. data/lib/relaton/index/shard_source.rb +1 -1
  80. data/lib/relaton/index/type.rb +2 -5
  81. data/lib/relaton/isbn/open_library.rb +11 -7
  82. data/lib/relaton/isbn/processor.rb +10 -0
  83. data/lib/relaton/iso/bibliography.rb +11 -9
  84. data/lib/relaton/iso/data_parser.rb +2 -2
  85. data/lib/relaton/iso/processor.rb +5 -0
  86. data/lib/relaton/iso/scraper.rb +20 -20
  87. data/lib/relaton/itu/bibliography.rb +107 -16
  88. data/lib/relaton/itu/data_crawler_r.rb +2 -2
  89. data/lib/relaton/itu/hit_collection.rb +64 -52
  90. data/lib/relaton/itu/processor.rb +1 -0
  91. data/lib/relaton/itu/scraper.rb +13 -3
  92. data/lib/relaton/itu.rb +0 -2
  93. data/lib/relaton/jis/bibliography.rb +22 -11
  94. data/lib/relaton/jis/data_fetcher.rb +4 -4
  95. data/lib/relaton/jis/processor.rb +1 -0
  96. data/lib/relaton/jis/scraper.rb +8 -8
  97. data/lib/relaton/nist/bibliography.rb +25 -21
  98. data/lib/relaton/oasis/bibliography.rb +16 -13
  99. data/lib/relaton/oasis/browser_agent.rb +2 -2
  100. data/lib/relaton/oasis/data_parser.rb +5 -5
  101. data/lib/relaton/oasis/data_parser_utils.rb +2 -2
  102. data/lib/relaton/oasis/data_part_parser.rb +7 -7
  103. data/lib/relaton/ogc/bibliography.rb +15 -14
  104. data/lib/relaton/ogc/hit_collection.rb +9 -7
  105. data/lib/relaton/ogc/processor.rb +13 -0
  106. data/lib/relaton/oiml/processor.rb +1 -0
  107. data/lib/relaton/omg/bibliography.rb +9 -9
  108. data/lib/relaton/omg/scraper.rb +14 -13
  109. data/lib/relaton/omg.rb +1 -1
  110. data/lib/relaton/plateau/bibliography.rb +9 -7
  111. data/lib/relaton/plateau/hit_collection.rb +2 -1
  112. data/lib/relaton/plateau/processor.rb +1 -0
  113. data/lib/relaton/un/bibliography.rb +21 -10
  114. data/lib/relaton/un/processor.rb +1 -0
  115. data/lib/relaton/version.rb +1 -1
  116. data/lib/relaton/w3c/processor.rb +1 -0
  117. data/lib/relaton/xsf/bibliography.rb +11 -6
  118. data/lib/relaton.rb +13 -0
  119. metadata +60 -14
  120. data/lib/relaton/itu/pubid.rb +0 -199
@@ -5,23 +5,25 @@ module Relaton
5
5
  # IETF bibliography module
6
6
  module Bibliography
7
7
  class << self
8
- # @param code [String] the ECMA standard Code to look up (e..g "ECMA-6")
8
+ # @param ref [String, Pubid::Cie::Identifier] the CIE reference (e.g.
9
+ # "CIE 001-1980"), or its parse from Relaton::Db (relaton#205)
9
10
  # @return [Relaton::Cie::ItemData]
10
- def search(code)
11
- Scrapper.scrape_page code
11
+ def search(ref)
12
+ Scrapper.scrape_page ref
12
13
  end
13
14
 
14
- # @param code [String] the ECMA standard Code to look up (e..g "ECMA-6")
15
+ # @param ref [String, Pubid::Cie::Identifier] the CIE reference (e.g.
16
+ # "CIE 001-1980"), or its parse from Relaton::Db (relaton#205)
15
17
  # @param year [String] not used
16
18
  # @param opts [Hash] not used
17
19
  # @return [Relaton::Cie::ItemData] Relaton of reference
18
- def get(code, _year = nil, _opts = {})
19
- Util.info "Fetching from Relaton repository ...", key: code
20
- result = search code
20
+ def get(ref, _year = nil, _opts = {})
21
+ Util.info "Fetching from Relaton repository ...", key: ref.to_s
22
+ result = search ref
21
23
  if result
22
- Util.info "Found: `#{result.docidentifier.first.content}`", key: code
24
+ Util.info "Found: `#{result.docidentifier.first.content}`", key: ref.to_s
23
25
  else
24
- Util.info "Not found.", key: code
26
+ Util.info "Not found.", key: ref.to_s
25
27
  end
26
28
  result
27
29
  end
@@ -3,7 +3,7 @@
3
3
  require "English"
4
4
  require "fileutils"
5
5
  require "ferrum"
6
- require "nokogiri"
6
+ require "moxml"
7
7
  require "pubid"
8
8
  require "relaton/index"
9
9
  require "relaton/bib"
@@ -58,7 +58,7 @@ module Relaton
58
58
  def get(url)
59
59
  @browser.go_to(url)
60
60
  wait_for_challenge
61
- Nokogiri::HTML(@browser.body)
61
+ Moxml.new.parse_html(@browser.body)
62
62
  end
63
63
 
64
64
  def quit
@@ -181,7 +181,7 @@ module Relaton
181
181
  Util.error msg
182
182
  end
183
183
 
184
- # @param hit [Nokogiri::HTML::Document]
184
+ # @param hit [Moxml::Document]
185
185
  # @param doc [Mechanize::Page]
186
186
  # @return [Array<Relaton::Bib::Docidentifier>]
187
187
  def fetch_docid(hit, doc)
@@ -200,7 +200,7 @@ module Relaton
200
200
  else
201
201
  @errors[:docid_2] &&= true
202
202
  end
203
- isbn = doc.at('//h3[contains(.,"ISBN")]/following-sibling::span')&.text
203
+ isbn = doc.at_xpath('//h3[contains(.,"ISBN")]/following-sibling::span')&.text
204
204
  if isbn && !isbn.strip.empty?
205
205
  docid << Bib::Docidentifier.new(type: "ISBN", content: isbn)
206
206
  @errors[:docid_isbn] &&= false
@@ -211,7 +211,7 @@ module Relaton
211
211
  end
212
212
 
213
213
  def parse_code(hit, doc = nil)
214
- code = hit.at("h3/a").text.strip.squeeze(" ").sub(/\u25b9/, "").gsub(" / ", "/")
214
+ code = hit.at_xpath(".//h3/a").text.strip.squeeze(" ").sub(/\u25b9/, "").gsub(" / ", "/")
215
215
  c2idx = %r{(?:\(|/)(?<c2>(?:ISO|IEC)\s[^()]+)} =~ code
216
216
  code = code[0...c2idx].strip if c2idx
217
217
  [primary_code(code, doc), c2]
@@ -221,7 +221,7 @@ module Relaton
221
221
  /^(?<code1>[^(]+)(?:\((?<code2>[a-zA-Z]+\d+,(?:\sPages)?[^)]+))?/ =~ code
222
222
  if code1&.match?(/^CIE/)
223
223
  parse_cie_code code1, code2, doc
224
- elsif (pcode = doc&.at('//h3[.="Product Code(s):"]/following-sibling::span'))
224
+ elsif (pcode = doc&.at_xpath('//h3[.="Product Code(s):"]/following-sibling::span'))
225
225
  "CIE #{pcode.text.strip.match(/[^,]+/)}"
226
226
  else
227
227
  num = code.match(/(?<=\()\w{2}\d+,.+(?=\))/).to_s.gsub(/,(?=\s)/, "").gsub(/,(?=\S)/, " ")
@@ -231,7 +231,7 @@ module Relaton
231
231
 
232
232
  def parse_cie_code(code1, code2, doc = nil) # rubocop:disable Metrics/CyclomaticComplexity
233
233
  code = code1.size > 25 && code2 ? "CIE #{code2.sub(/,(\sPages)?/, '')}" : code1
234
- add = doc&.at("//hgroup/h2")&.text&.match(/(Add)endum\s(\d+)$/)
234
+ add = doc&.at_xpath("//hgroup/h2")&.text&.match(/(Add)endum\s(\d+)$/)
235
235
  return code unless add
236
236
 
237
237
  "#{code} #{add[1]} #{add[2]}"
@@ -244,7 +244,7 @@ module Relaton
244
244
  # @param doc [Mechanize::Page]
245
245
  # @return [Array<Relaton::Bib::Title>]
246
246
  def fetch_title(doc)
247
- t = doc.at("//hgroup/h2/text()", "//hgroup/h1/text()")
247
+ t = doc.at_xpath("//hgroup/h2/text()|//hgroup/h1/text()")
248
248
  unless t && !t.text.strip.empty?
249
249
  @errors[:title] &&= true
250
250
  return []
@@ -270,7 +270,7 @@ module Relaton
270
270
  # @param doc [Mechanize::Page]
271
271
  # @return [String]
272
272
  def fetch_edition(doc)
273
- ed = doc.at("//h3[.='Edition:']/following-sibling::span")
273
+ ed = doc.at_xpath("//h3[.='Edition:']/following-sibling::span")
274
274
  @errors[:edition] &&= true
275
275
  return unless ed
276
276
 
@@ -285,16 +285,16 @@ module Relaton
285
285
  # @return [Array<Relaton::Cie::Relation>]
286
286
  def fetch_relation(doc) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
287
287
  rels = doc.xpath('//section[@class="history"]/ol/li[not(contains(@class,"selected-product"))]').map do |rel|
288
- ref = rel.at("a")
288
+ ref = rel.at_xpath(".//a")
289
289
  url = "https://www.techstreet.com#{ref[:href]}"
290
- title = Bib::Title.from_string ref.at('p/span[@class="title"]').text
291
- did = ref.at("h3").text
290
+ title = Bib::Title.from_string ref.at_xpath('.//p/span[@class="title"]').text
291
+ did = ref.at_xpath(".//h3").text
292
292
  docid = [Bib::Docidentifier.new(type: "CIE", content: did, primary: true)]
293
- on = ref.at("p/time")
293
+ on = ref.at_xpath(".//p/time")
294
294
  date = [Bib::Date.new(type: "published", at: on[:datetime])]
295
295
  source = [Bib::Uri.new(type: "src", content: url)]
296
296
  bibitem = ItemData.new docidentifier: docid, title: title, source: source, date: date
297
- type = ref.at('//li/i[contains(@class,"historical")]') ? "updates" : "updatedBy"
297
+ type = ref.at_xpath('//li/i[contains(@class,"historical")]') ? "updates" : "updatedBy"
298
298
  Bib::Relation.new(type: type, bibitem: bibitem)
299
299
  end
300
300
  @errors[:relation] &&= rels.empty?
@@ -313,7 +313,7 @@ module Relaton
313
313
  # @param doc [Mechanize::Page]
314
314
  # @return [Array<Relaton::Bib::LocalizedMarkedUpString>]
315
315
  def fetch_abstract(doc)
316
- content = doc.at('//div[contains(@class,"description")]')&.text&.strip
316
+ content = doc.at_xpath('//div[contains(@class,"description")]')&.text&.strip
317
317
  if content.nil? || content.empty?
318
318
  @errors[:abstract] &&= true
319
319
  return []
@@ -422,10 +422,10 @@ module Relaton
422
422
  # workers. Called directly (single stubbed agent) by specs and from the
423
423
  # pool with a per-worker agent/pacing/position.
424
424
  #
425
- # @param hit [Nokogiri::HTML::Element]
425
+ # @param hit [Moxml::Element]
426
426
  # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
427
427
  def parse_page(hit, pos = nil, worker_agent = agent, pacing = nil)
428
- url = hit.at('h3/a')[:href]
428
+ url = hit.at_xpath('.//h3/a')[:href]
429
429
  doc = time_req(pacing) { worker_agent.get url }
430
430
  @mutex.synchronize do
431
431
  item = ItemData.new(
@@ -460,7 +460,7 @@ module Relaton
460
460
  def collect_hits(url = URL, hits = [])
461
461
  result = time_req(listing_pacing) { agent.get url }
462
462
  hits.concat result.xpath("//li[@data-product]").to_a
463
- np = result.at '//a[@class="next_page"]'
463
+ np = result.at_xpath '//a[@class="next_page"]'
464
464
  return hits unless np
465
465
 
466
466
  next_href = np[:href]
@@ -6,6 +6,7 @@ module Relaton
6
6
  def initialize # rubocop:disable Lint/MissingSuper
7
7
  @short = :relaton_cie
8
8
  @prefix = "CIE"
9
+ @pubid_identifier = :Cie # Db cache key
9
10
  @defaultprefix = /^CIE(-|\s)/
10
11
  @idtype = "CIE"
11
12
  @datasets = %w[cie-techstreet]
@@ -6,14 +6,15 @@ module Relaton
6
6
  ENDPOINT = "https://raw.githubusercontent.com/relaton/relaton-data-cie/refs/heads/v2/".freeze
7
7
 
8
8
  class << self
9
- # @param code [String]
9
+ # @param ref [String, Pubid::Cie::Identifier] the reference, or the
10
+ # parse that Relaton::Db routed with (relaton#205)
10
11
  # @return [Relaton::Cie::ItemData]
11
- def scrape_page(code)
12
+ def scrape_page(ref)
12
13
  # An unrecognized reference raises; like ISO and 3GPP we let it
13
14
  # propagate -- relaton-cli rescues Pubid::Errors::Error and renders
14
15
  # "... is not a recognized standards identifier". Partial refs
15
16
  # (`CIE 001`, `CIE 15`) parse, so nothing valid is lost.
16
- pubid = ::Pubid::Cie.parse code
17
+ pubid = to_pubid ref
17
18
  index = Index.find_or_create :cie, url: "#{ENDPOINT}#{INDEXFILE}.zip", file: "#{INDEXFILE}.yaml",
18
19
  pubid_class: ::Pubid::Cie::Identifier
19
20
  # Pass the parsed pubid (not the raw String) so index-v2 narrows
@@ -26,25 +27,31 @@ module Relaton
26
27
  .min_by { |r| r[:id].to_s }
27
28
  return unless row
28
29
 
29
- parse_page "#{ENDPOINT}#{row[:file]}", code
30
+ parse_page "#{ENDPOINT}#{row[:file]}", ref.to_s
30
31
  end
31
32
 
32
33
  private
33
34
 
35
+ # @param ref [String, Pubid::Cie::Identifier]
36
+ # @return [Pubid::Cie::Identifier] parses only a String
37
+ def to_pubid(ref)
38
+ ref.is_a?(String) ? ::Pubid::Cie.parse(ref) : ref
39
+ end
40
+
34
41
  # @param url [String]
35
- # @param code [String]
42
+ # @param ref [String]
36
43
  # @retrurn [Relato::Cie::ItemData]
37
- def parse_page(url, code)
44
+ def parse_page(url, ref)
38
45
  resp = Mechanize.new.get url
39
46
  Item.from_yaml(resp.body).tap { |item| item.fetched = Date.today.to_s }
40
47
  rescue Mechanize::ResponseCodeError => e
41
48
  return if e.response_code == "404"
42
49
 
43
- raise Relaton::RequestError, "No document found for #{code} reference. #{e.message}"
50
+ raise Relaton::RequestError, "No document found for #{ref} reference. #{e.message}"
44
51
  rescue Mechanize::RedirectLimitReachedError, Timeout::Error,
45
52
  Mechanize::UnauthorizedError, Mechanize::UnsupportedSchemeError,
46
53
  Mechanize::ResponseReadError, Mechanize::ChunkedTerminationError => e
47
- raise Relaton::RequestError, "No document found for #{code} reference. #{e.message}"
54
+ raise Relaton::RequestError, "No document found for #{ref} reference. #{e.message}"
48
55
  end
49
56
  end
50
57
  end
data/lib/relaton/cie.rb CHANGED
@@ -1,4 +1,4 @@
1
- require "nokogiri"
1
+ require "moxml"
2
2
  # require "parslet"
3
3
  require "pubid"
4
4
  require "relaton/index"
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/store"
4
+
5
+ module Relaton
6
+ # Access to LutaML data repositories through lutaml-store — the cloud
7
+ # store (api.relaton.org), a Pages/raw host, or a local GCR-style package,
8
+ # all through one read API with an explicit local package cache.
9
+ #
10
+ # This module composes lutaml-store sources; it is deliberately
11
+ # independent of Relaton::Db's own cache machinery (see
12
+ # relaton#204/#205 for that rework).
13
+ module Cloud
14
+ module_function
15
+
16
+ # An explicitly configured source over a LutaML data repository.
17
+ #
18
+ # @param base_url [String] cloud store base (e.g. "https://api.relaton.org")
19
+ # @param collection [String] one collection (for relaton: a flavor)
20
+ # @param options [Hash] passthrough to Lutaml::Store::Source::Rest
21
+ # (headers:, timeout:, transport:, cache:, default_format:)
22
+ def source(base_url:, collection:, **options)
23
+ Lutaml::Store::Source.for(:rest, base_url: base_url, collection: collection, **options)
24
+ end
25
+
26
+ # A local package directory source (a downloaded distribution or a
27
+ # Mirror/Repository pull).
28
+ def local(path)
29
+ Lutaml::Store::Source.for(:directory, path: path)
30
+ end
31
+
32
+ # The record's bytes. NotFoundError means the repository definitively
33
+ # does not have the key; BackendError means the network/service broke.
34
+ #
35
+ # @param key [String] the collection's storage key (see .resolve_key)
36
+ def read(key, source:)
37
+ source.read(key)
38
+ end
39
+
40
+ # Resolves a reference to the collection's storage key. The contract's
41
+ # keys are URL-safe storage keys; slash-bearing docids live in the
42
+ # manifest's entries[].metadata.docid. Resolution is manifest-driven and
43
+ # explicit: exact storage-key match first, then the docid metadata,
44
+ # then nil — there is no path-encoding guesswork.
45
+ #
46
+ # @param ref [String] a storage key or a primary docidentifier
47
+ # @return [String, nil]
48
+ def resolve_key(ref, source:)
49
+ manifest = begin
50
+ source.manifest
51
+ rescue Lutaml::Store::NotFoundError
52
+ # a fresh local package holds no manifest yet — it holds no keys
53
+ return nil
54
+ end
55
+
56
+ return ref if manifest.key?(ref)
57
+
58
+ found = manifest.entries.find do |e|
59
+ e.metadata["docid"]&.casecmp?(ref)
60
+ end
61
+ found&.key
62
+ end
63
+
64
+ # The parsed record: the model class is declared by the caller — never
65
+ # inferred.
66
+ def get(key, model_class, source:)
67
+ source.get(key, model_class)
68
+ end
69
+
70
+ # Reference in, parsed model out: resolves the reference through the
71
+ # manifest, reads the record (from the cloud source, or the local
72
+ # package when one is given), and deserializes with the caller's model
73
+ # class. The record's format is self-describing (Format.guess), so the
74
+ # same call serves YAML, JSON and XML records.
75
+ #
76
+ # @param ref [String] a storage key or a primary docidentifier
77
+ # @param model_class [Class] e.g. Relaton::Bib::Item (YAML/JSON
78
+ # records) or Relaton::Bib::Bibdata (XML bibdata records)
79
+ # @param cache [Lutaml::Store::Source::Directory, nil] read-through
80
+ # local package (writes through on a miss)
81
+ # @return the model instance
82
+ def fetch(ref, model_class, source:, cache: nil)
83
+ if cache
84
+ # Offline-first: resolve and read against the local package before
85
+ # touching the source — its manifest carries the same docid metadata.
86
+ local_key = resolve_key(ref, source: cache)
87
+ if local_key
88
+ return deserialize(cache.read(local_key), model_class)
89
+ end
90
+ end
91
+
92
+ key = resolve_key(ref, source: source)
93
+ raise Lutaml::Store::NotFoundError, "unknown reference: #{ref.inspect}" unless key
94
+
95
+ body = if cache
96
+ Lutaml::Store::Repository.new(source: source, cache: cache).read(key)
97
+ else
98
+ source.read(key)
99
+ end
100
+
101
+ deserialize(body, model_class)
102
+ end
103
+
104
+ def deserialize(body, model_class)
105
+ Lutaml::Store::Format.resolve(Lutaml::Store::Format.guess(body))
106
+ .deserialize(body, model_class)
107
+ end
108
+
109
+ # Mirrors the whole collection into a GCR-style local package.
110
+ #
111
+ # @param into [String] target directory (a `<into>/<collection>` package
112
+ # is written)
113
+ # @return [Lutaml::Store::Source::Directory] a source over the package
114
+ def pull(source:, into:, collection: nil, force: false)
115
+ Lutaml::Store::Mirror.pull(source, into: into, collection: collection, force: force)
116
+ end
117
+
118
+ # Read-through convenience: a Repository over a cloud source with an
119
+ # explicit local package cache; reads served from the cache once pulled.
120
+ def repository(base_url:, collection:, cache_path:, **options)
121
+ Lutaml::Store::Repository.new(
122
+ source: source(base_url: base_url, collection: collection, **options),
123
+ cache: local(cache_path)
124
+ )
125
+ end
126
+ end
127
+ end
@@ -1,5 +1,5 @@
1
1
  require "forwardable"
2
- require "nokogiri"
2
+ require "moxml"
3
3
  require "weakref"
4
4
  require_relative "hit"
5
5
 
@@ -51,21 +51,17 @@ module Relaton
51
51
  # Renders the collection as XML
52
52
  #
53
53
  # @param opts [Hash] options
54
- # @option opts [Nokogiri::XML::Builder] :builder XML builder
54
+ # @option opts [Moxml::Builder] :builder XML builder
55
55
  # @option opts [Boolean] :bibdata render bibdata if true
56
56
  # @option opts [String, Symbol] :lang language
57
57
  #
58
58
  # @return [String] XML representation of the collection
59
59
  #
60
60
  def to_xml(**opts)
61
- builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
62
- xml.documents do
63
- @array.each do |hit|
64
- xml << hit.to_xml(**opts)
65
- end
66
- end
67
- end
68
- builder.to_xml
61
+ parts = @array.map { |hit| hit.to_xml(**opts) }.join
62
+ Moxml.parse(
63
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<documents>#{parts}</documents>",
64
+ ).to_xml(indent: 0, expand_empty: false)
69
65
  end
70
66
 
71
67
  #
@@ -32,6 +32,127 @@ module Relaton
32
32
  end
33
33
  end
34
34
 
35
+ # The pubid identifier class `Relaton::Db` keys its cache with. Read from
36
+ # @pubid_identifier, else @pubid_flavor. @pubid_identifier exists so a
37
+ # flavor can key its cache with pubid without also sourcing #prefixes
38
+ # from pubid. Nil for a processor with neither: `Db` then keys its cache
39
+ # with the legacy string key.
40
+ #
41
+ # @return [Class, nil]
42
+ def pubid_class
43
+ flavor = @pubid_identifier || @pubid_flavor
44
+ return unless flavor
45
+
46
+ require "pubid"
47
+ @pubid_class ||= ::Pubid.const_get(flavor)::Identifier
48
+ end
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
+
60
+ # Parse a query reference into a pubid. A flavor that normalizes a
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.
65
+ #
66
+ # @param ref [String]
67
+ # @return [Pubid::Identifier, nil]
68
+ def cache_pubid(ref)
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)
111
+ end
112
+
113
+ # Set the year on a parsed pubid, where the flavor's `get` applies it. The
114
+ # default sets the identifier's own year. A flavor whose `get` applies
115
+ # the year elsewhere overrides this (see #fold_year_on_root).
116
+ #
117
+ # @param pubid [Pubid::Identifier]
118
+ # @param year [String, Integer]
119
+ # @return [Pubid::Identifier]
120
+ def fold_year(pubid, year)
121
+ pubid.class.from_hash pubid.to_hash.merge("year" => year.to_s)
122
+ end
123
+
124
+ # Set the year on the document the identifier names (`#root`): the base
125
+ # document of a supplement, the adopted document of an adoption. For a
126
+ # flavor whose `get` applies the year there (ISO, CEN, BSI).
127
+ #
128
+ # @param pubid [Pubid::Identifier]
129
+ # @param year [String, Integer]
130
+ # @return [Pubid::Identifier]
131
+ def fold_year_on_root(pubid, year)
132
+ copy = pubid.class.from_hash pubid.to_hash
133
+ copy.root.date = ::Pubid::Components::Date.new(year: year.to_s)
134
+ copy
135
+ end
136
+
137
+ # The cache key for a query: the parsed pubid with the `year` and
138
+ # `all_parts` options folded in. The publication date range is not part
139
+ # of the identity, so it never enters the key.
140
+ #
141
+ # @param ref [String]
142
+ # @param year [String, Integer, nil]
143
+ # @param opts [Hash]
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
150
+
151
+ pubid = fold_year(pubid, year) if year
152
+ pubid = pubid.to_all_parts if opts[:all_parts] && !pubid.all_parts?
153
+ pubid
154
+ end
155
+
35
156
  def initialize
36
157
  raise "This is an abstract class!"
37
158
  end
@@ -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. It depends on nothing, so `relaton/index` requires this file
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 < StandardError; end
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