relaton 3.0.0.pre.alpha.4 → 3.0.0.pre.alpha.5

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/lib/relaton/adobe/processor.rb +9 -0
  3. data/lib/relaton/bib/converter/csl.rb +110 -0
  4. data/lib/relaton/bib/converter/ris.rb +104 -0
  5. data/lib/relaton/bib/converter/titles.rb +24 -0
  6. data/lib/relaton/bib/item_data.rb +9 -0
  7. data/lib/relaton/bib/model/item.rb +6 -6
  8. data/lib/relaton/bib/sanitizer.rb +71 -30
  9. data/lib/relaton/bib.rb +10 -0
  10. data/lib/relaton/bipm/processor.rb +1 -0
  11. data/lib/relaton/bipm/rawdata_bipm_metrologia/affiliations.rb +6 -6
  12. data/lib/relaton/bipm/si_brochure_parser.rb +2 -2
  13. data/lib/relaton/bsi/processor.rb +5 -0
  14. data/lib/relaton/ccsds/bibliography.rb +1 -0
  15. data/lib/relaton/ccsds/processor.rb +12 -0
  16. data/lib/relaton/cen/hit_collection.rb +1 -1
  17. data/lib/relaton/cen/processor.rb +5 -0
  18. data/lib/relaton/cen/scraper.rb +9 -9
  19. data/lib/relaton/cie/data_fetcher.rb +18 -18
  20. data/lib/relaton/cie/processor.rb +1 -0
  21. data/lib/relaton/cie.rb +1 -1
  22. data/lib/relaton/cloud.rb +127 -0
  23. data/lib/relaton/core/hit_collection.rb +6 -10
  24. data/lib/relaton/core/processor.rb +68 -0
  25. data/lib/relaton/db/cache.rb +444 -148
  26. data/lib/relaton/db/cache_entry.rb +33 -0
  27. data/lib/relaton/db/registry.rb +52 -0
  28. data/lib/relaton/db.rb +131 -72
  29. data/lib/relaton/doi/crossref.rb +23 -6
  30. data/lib/relaton/doi/processor.rb +1 -0
  31. data/lib/relaton/easc/processor.rb +1 -0
  32. data/lib/relaton/ecma/data_fetcher.rb +1 -1
  33. data/lib/relaton/ecma/data_parser.rb +1 -1
  34. data/lib/relaton/ecma/edition_parser.rb +2 -2
  35. data/lib/relaton/ecma/memento_parser.rb +4 -4
  36. data/lib/relaton/ecma/standard_parser.rb +6 -6
  37. data/lib/relaton/etsi/processor.rb +1 -0
  38. data/lib/relaton/gb/gb_scraper.rb +5 -5
  39. data/lib/relaton/gb/scraper.rb +16 -16
  40. data/lib/relaton/gb/sec_scraper.rb +8 -8
  41. data/lib/relaton/gb/t_scraper.rb +5 -5
  42. data/lib/relaton/gost/processor.rb +1 -0
  43. data/lib/relaton/iala/processor.rb +1 -0
  44. data/lib/relaton/iana/data_fetcher.rb +3 -3
  45. data/lib/relaton/iana/parser.rb +8 -4
  46. data/lib/relaton/iana/processor.rb +9 -0
  47. data/lib/relaton/iec/data_parser.rb +24 -9
  48. data/lib/relaton/iec/processor.rb +6 -0
  49. data/lib/relaton/iec.rb +1 -1
  50. data/lib/relaton/ieee/data_fetcher.rb +1 -1
  51. data/lib/relaton/ieee/processor.rb +8 -0
  52. data/lib/relaton/ietf/data_fetcher.rb +1 -1
  53. data/lib/relaton/ietf/processor.rb +1 -0
  54. data/lib/relaton/ietf/rfc/entry.rb +15 -19
  55. data/lib/relaton/iho/processor.rb +1 -0
  56. data/lib/relaton/index/file_io.rb +2 -2
  57. data/lib/relaton/index/pool.rb +4 -3
  58. data/lib/relaton/index/shard_source.rb +1 -1
  59. data/lib/relaton/index/type.rb +2 -5
  60. data/lib/relaton/isbn/open_library.rb +11 -7
  61. data/lib/relaton/isbn/processor.rb +10 -0
  62. data/lib/relaton/iso/data_parser.rb +2 -2
  63. data/lib/relaton/iso/processor.rb +5 -0
  64. data/lib/relaton/iso/scraper.rb +20 -20
  65. data/lib/relaton/itu/bibliography.rb +99 -13
  66. data/lib/relaton/itu/data_crawler_r.rb +2 -2
  67. data/lib/relaton/itu/hit_collection.rb +64 -52
  68. data/lib/relaton/itu/processor.rb +1 -0
  69. data/lib/relaton/itu/scraper.rb +13 -3
  70. data/lib/relaton/itu.rb +0 -2
  71. data/lib/relaton/jis/data_fetcher.rb +4 -4
  72. data/lib/relaton/jis/processor.rb +1 -0
  73. data/lib/relaton/jis/scraper.rb +8 -8
  74. data/lib/relaton/oasis/browser_agent.rb +2 -2
  75. data/lib/relaton/oasis/data_parser.rb +5 -5
  76. data/lib/relaton/oasis/data_parser_utils.rb +2 -2
  77. data/lib/relaton/oasis/data_part_parser.rb +7 -7
  78. data/lib/relaton/ogc/processor.rb +7 -0
  79. data/lib/relaton/oiml/processor.rb +1 -0
  80. data/lib/relaton/omg/scraper.rb +11 -11
  81. data/lib/relaton/omg.rb +1 -1
  82. data/lib/relaton/plateau/processor.rb +1 -0
  83. data/lib/relaton/un/bibliography.rb +21 -10
  84. data/lib/relaton/un/processor.rb +1 -0
  85. data/lib/relaton/version.rb +1 -1
  86. data/lib/relaton/w3c/processor.rb +1 -0
  87. data/lib/relaton.rb +13 -0
  88. metadata +48 -16
  89. data/lib/relaton/itu/pubid.rb +0 -199
@@ -53,7 +53,7 @@ module Relaton
53
53
  # @param doc [Mechanize::Page]
54
54
  # @return [Array<Relaton::Bib::LocalizedMarkedUpString>]
55
55
  def fetch_abstract(doc)
56
- content = doc.at("//tr[th[.='Abstract/Scope']]/td")
56
+ content = doc.at_xpath("//tr[th[.='Abstract/Scope']]/td")
57
57
  [Bib::Abstract.new(content: content.text, language: "en", script: "Latn")]
58
58
  end
59
59
 
@@ -68,7 +68,7 @@ module Relaton
68
68
  # @param doc [Mechanize::Page]
69
69
  # @return [Relaton::Bib::DocumentStatus, nil]
70
70
  def fetch_status(doc)
71
- s = doc.at("//tr[th[.='Status']]/td")
71
+ s = doc.at_xpath("//tr[th[.='Status']]/td")
72
72
  return unless s
73
73
 
74
74
  stage = Bib::Status::Stage.new(content: s.text.strip)
@@ -98,7 +98,7 @@ module Relaton
98
98
  doc.xpath(
99
99
  "//div[@id='DASHBOARD_LISTRELATIONS']/table/tr[th[.!='Sales Points']]",
100
100
  ).each_with_object([]) do |rt, acc|
101
- type = relation_type rt.at("th").text.downcase
101
+ type = relation_type rt.at_xpath("th").text.downcase
102
102
  rt.xpath("td/a").each do |r|
103
103
  acc << Bib::Relation.new(type: type, bibitem: create_relation(r))
104
104
  end
@@ -124,7 +124,7 @@ module Relaton
124
124
  # @param doc [Mechanize::Page]
125
125
  # @return [Array<Relaton::Bib::Title>]
126
126
  def fetch_titles(doc)
127
- te = doc.at("//tr[th[.='Title']]/td").text.strip
127
+ te = doc.at_xpath("//tr[th[.='Title']]/td").text.strip
128
128
  Bib::Title.from_string te, "en", "Latn"
129
129
  end
130
130
 
@@ -134,10 +134,10 @@ module Relaton
134
134
  def fetch_dates(doc) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
135
135
  doc.xpath("//div[@id='DASHBOARD_LISTIMPLEMENTATIONDATES']/table/tr")
136
136
  .each_with_object([]) do |d, acc|
137
- at = d.at("td").text
137
+ at = d.at_xpath("td").text
138
138
  next if at.empty?
139
139
 
140
- t = d.at("th").text
140
+ t = d.at_xpath("th").text
141
141
  type = case t
142
142
  when /DOR/ then "adapted"
143
143
  when /DAV/ then "issued"
@@ -154,8 +154,8 @@ module Relaton
154
154
  # @param doc [Mechanize::Page]
155
155
  # @return [Array<Relaton::Bib::Contributor>]
156
156
  def fetch_contributors(doc)
157
- code = doc.at("//tr/td/h1/text()").text
158
- title = doc.at("//tr/td[3]/h1").text
157
+ code = doc.at_xpath("//tr/td/h1/text()").text
158
+ title = doc.at_xpath("//tr/td[3]/h1").text
159
159
  %r{/(?<type>\w+)(?:\s(?<num>[^/]+))?$} =~ code
160
160
  org = owner_entity
161
161
 
@@ -190,7 +190,7 @@ module Relaton
190
190
  # @param doc [Mechanize::Page]
191
191
  # @return [Array<Bib::Copyright>]
192
192
  def fetch_copyright(doc)
193
- date = doc.at("//tr[th[.='date of Availability (DAV)']]/td").text
193
+ date = doc.at_xpath("//tr[th[.='date of Availability (DAV)']]/td").text
194
194
  owner = Bib::ContributionInfo.new(organization: owner_entity)
195
195
  from = date.match(/^\d{4}/).to_s
196
196
  [Bib::Copyright.new(owner: [owner], from: from)]
@@ -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]
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,74 @@ 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
+ # Parse a query reference into a pubid. A flavor that normalizes a
51
+ # reference before it parses overrides this. A parse error propagates:
52
+ # an unrecognized reference is not "not found".
53
+ #
54
+ # @param ref [String]
55
+ # @return [Pubid::Identifier, nil]
56
+ def cache_pubid(ref)
57
+ pubid_class&.parse(ref)
58
+ end
59
+
60
+ # Set the year on a parsed pubid, where the flavor's `get` applies it. The
61
+ # default sets the identifier's own year. A flavor whose `get` applies
62
+ # the year elsewhere overrides this (see #fold_year_on_root).
63
+ #
64
+ # @param pubid [Pubid::Identifier]
65
+ # @param year [String, Integer]
66
+ # @return [Pubid::Identifier]
67
+ def fold_year(pubid, year)
68
+ pubid.class.from_hash pubid.to_hash.merge("year" => year.to_s)
69
+ end
70
+
71
+ # Set the year on the document the identifier names (`#root`): the base
72
+ # document of a supplement, the adopted document of an adoption. For a
73
+ # flavor whose `get` applies the year there (ISO, CEN, BSI).
74
+ #
75
+ # @param pubid [Pubid::Identifier]
76
+ # @param year [String, Integer]
77
+ # @return [Pubid::Identifier]
78
+ def fold_year_on_root(pubid, year)
79
+ copy = pubid.class.from_hash pubid.to_hash
80
+ copy.root.date = ::Pubid::Components::Date.new(year: year.to_s)
81
+ copy
82
+ end
83
+
84
+ # The cache key for a query: the parsed pubid with the `year` and
85
+ # `all_parts` options folded in. The publication date range is not part
86
+ # of the identity, so it never enters the key.
87
+ #
88
+ # @param ref [String]
89
+ # @param year [String, Integer, nil]
90
+ # @param opts [Hash]
91
+ # @return [Pubid::Identifier, nil]
92
+ def cache_key(ref, year, opts)
93
+ pubid = cache_pubid(ref)
94
+ # A flavor that reads an unparseable reference as a miss (IANA, IEEE)
95
+ # gives nil or the raw String: no pubid key then.
96
+ return if pubid.nil? || !pubid.is_a?(::Pubid::Identifier)
97
+
98
+ pubid = fold_year(pubid, year) if year
99
+ pubid = pubid.to_all_parts if opts[:all_parts] && !pubid.all_parts?
100
+ pubid
101
+ end
102
+
35
103
  def initialize
36
104
  raise "This is an abstract class!"
37
105
  end