relaton 3.0.1 → 3.0.3
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/bib/converter/csl.rb +3 -2
- data/lib/relaton/bib/model/docidentifier.rb +61 -0
- data/lib/relaton/bib.rb +2 -1
- data/lib/relaton/bipm/CLAUDE.md +14 -0
- data/lib/relaton/bipm.rb +0 -1
- data/lib/relaton/bsi/model/docidentifier.rb +0 -4
- data/lib/relaton/calconnect/data_fetcher.rb +2 -2
- data/lib/relaton/ccsds/data/fetcher.rb +2 -1
- data/lib/relaton/ccsds/model/docidentifier.rb +0 -4
- data/lib/relaton/core/data_fetcher.rb +2 -1
- data/lib/relaton/core/processor.rb +1 -1
- data/lib/relaton/core.rb +7 -0
- data/lib/relaton/db/cache.rb +3 -3
- data/lib/relaton/db/registry.rb +1 -1
- data/lib/relaton/db.rb +22 -3
- data/lib/relaton/doi/crossref.rb +2 -1
- data/lib/relaton/doi/parser.rb +3 -2
- data/lib/relaton/etsi/data_fetcher.rb +3 -3
- data/lib/relaton/gb/sec_scraper.rb +3 -3
- data/lib/relaton/iec/data_fetcher.rb +3 -2
- data/lib/relaton/iec/model/docidentifier.rb +0 -4
- data/lib/relaton/ietf/CLAUDE.md +27 -2
- data/lib/relaton/ietf/bibxml_parser.rb +38 -0
- data/lib/relaton/ietf/data_fetcher.rb +3 -30
- data/lib/relaton/ietf/scraper.rb +60 -3
- data/lib/relaton/ietf/wg_name_resolver.rb +2 -2
- data/lib/relaton/iho/CLAUDE.md +4 -0
- data/lib/relaton/iho/bibliography.rb +7 -1
- data/lib/relaton/index/dataset_lock.rb +4 -3
- data/lib/relaton/index/shard_source.rb +3 -2
- data/lib/relaton/index/sqlite_backend.rb +5 -3
- data/lib/relaton/isbn/open_library.rb +2 -1
- data/lib/relaton/iso/data_fetcher.rb +6 -6
- data/lib/relaton/iso/model/docidentifier.rb +0 -4
- data/lib/relaton/itu/data_fetcher.rb +2 -2
- data/lib/relaton/itu/hit_collection.rb +3 -3
- data/lib/relaton/itu/recommendation_fields.rb +2 -2
- data/lib/relaton/jis/data_fetcher.rb +2 -1
- data/lib/relaton/json.rb +21 -0
- data/lib/relaton/logger/channels/gh_issue.rb +3 -3
- data/lib/relaton/logger/formatter_json.rb +2 -1
- data/lib/relaton/nist/CLAUDE.md +7 -0
- data/lib/relaton/nist/bibliography.rb +31 -0
- data/lib/relaton/nist/cswp.rb +137 -0
- data/lib/relaton/nist/cswp.yaml +155 -0
- data/lib/relaton/nist/docidentifier.rb +0 -4
- data/lib/relaton/nist/processor.rb +20 -0
- data/lib/relaton/nist/pubs_export.rb +2 -1
- data/lib/relaton/nist.rb +1 -0
- data/lib/relaton/ogc/data_fetcher.rb +2 -2
- data/lib/relaton/omg/scraper.rb +3 -3
- data/lib/relaton/plateau/data_fetcher.rb +2 -1
- data/lib/relaton/un/hit_collection.rb +4 -3
- data/lib/relaton/version.rb +1 -1
- data/lib/relaton.rb +1 -0
- metadata +45 -88
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a774b326943089257e3aef6e5402fc1e2d53d4ff557e8b19f845a62d64b9a99b
|
|
4
|
+
data.tar.gz: 665fa378d2380f79a4e2952df1ff3d0f7e1de6d91ead262d75261555a4170e5c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bfa2af357a4e288f0ce97fb82ff78ac58b39bc8f8fc8659584ebb29a4568a50175137b8b8cd32b6710aff65b8413cb4ab0d1ea674918187edd9a74592e331a6c
|
|
7
|
+
data.tar.gz: e2ad6a9822a6ffa5f9a625bb4095260b81b3906fcff8ba0cf4fbdcea0d543f9e2b14133c7dd29dad0f165c26713b16fc0f06ebfc06d134384d4d0f280ee873b1
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "yeptris"
|
|
4
|
+
|
|
4
5
|
|
|
5
6
|
module Relaton
|
|
6
7
|
module Bib
|
|
@@ -43,7 +44,7 @@ module Relaton
|
|
|
43
44
|
keywords = Array(@item.keyword).map { |k| k.content.to_s }.reject(&:empty?)
|
|
44
45
|
out[:keyword] = keywords unless keywords.empty?
|
|
45
46
|
out[:URL] = source_uri if source_uri
|
|
46
|
-
[out]
|
|
47
|
+
Yeptris::JSON.dump([out]) + "\n"
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
private
|
|
@@ -44,6 +44,67 @@ module Relaton
|
|
|
44
44
|
def to_all_parts!; end
|
|
45
45
|
|
|
46
46
|
def remove_date!; end
|
|
47
|
+
|
|
48
|
+
# Docidentifier types with an SDO-specific :external and :internal form
|
|
49
|
+
# (relaton#49), mapped to the pubid flavor that renders both. The
|
|
50
|
+
# :external form carries the publisher ("IETF RFC 8341",
|
|
51
|
+
# "BSI PAS 2031:2019"); the :internal form is the one the SDO uses for its
|
|
52
|
+
# own documents, without it ("SP 800-113" for "NIST SP 800-113"). pubid
|
|
53
|
+
# decides per identifier, so "BS EN ISO 8848:2021" and "CGPM RES (1889)"
|
|
54
|
+
# are the same in both forms. Every other type answers its content for
|
|
55
|
+
# both forms: pubid would drop "ISO" from an ISO identifier.
|
|
56
|
+
#
|
|
57
|
+
# The table keys on `type`, not on a flavor subclass, so it also covers a
|
|
58
|
+
# relation's nested `Bib::ItemData` and the `Bib::Docidentifier` objects
|
|
59
|
+
# BIPM and IETF build (a subclass-typed collection would make lutaml's
|
|
60
|
+
# YAML serializer reject those).
|
|
61
|
+
FORM_PUBID = {
|
|
62
|
+
"NIST" => :Nist,
|
|
63
|
+
"BIPM" => :Bipm,
|
|
64
|
+
"IETF" => :Ietf,
|
|
65
|
+
"Internet-Draft" => :Ietf,
|
|
66
|
+
"BSI" => :Bsi,
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
FORMS = %i[external internal].freeze
|
|
70
|
+
|
|
71
|
+
# The parsed identifier. A pubid-backed flavor subclass overrides it.
|
|
72
|
+
def pubid; end
|
|
73
|
+
|
|
74
|
+
# @param form [Symbol, nil] nil for the stored content, :external for the
|
|
75
|
+
# identifier with its publisher, :internal for the form the SDO uses
|
|
76
|
+
# for its own documents
|
|
77
|
+
# @raise [ArgumentError] for an unknown form
|
|
78
|
+
def to_s(form: nil)
|
|
79
|
+
return content.to_s unless form
|
|
80
|
+
|
|
81
|
+
form = form.to_sym
|
|
82
|
+
unless FORMS.include?(form)
|
|
83
|
+
raise ArgumentError, "unknown identifier form: #{form.inspect}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
form_content(form).to_s
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# pubid renders the form (`to_s(with_publisher:)`, pubid#620). A
|
|
92
|
+
# docidentifier with no parsed pubid (a plain `Bib::Docidentifier`) is
|
|
93
|
+
# parsed here; content that pubid cannot parse is returned unchanged.
|
|
94
|
+
def form_content(form)
|
|
95
|
+
flavor = FORM_PUBID[type]
|
|
96
|
+
id = flavor && (pubid || parse_form_pubid(flavor))
|
|
97
|
+
id ? id.to_s(with_publisher: form == :external) : content
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def parse_form_pubid(flavor)
|
|
101
|
+
return if content.to_s.empty?
|
|
102
|
+
|
|
103
|
+
require "pubid"
|
|
104
|
+
::Pubid.const_get(flavor).parse(content)
|
|
105
|
+
rescue StandardError
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
47
108
|
end
|
|
48
109
|
end
|
|
49
110
|
end
|
data/lib/relaton/bib.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "bibtex"
|
|
|
5
5
|
require "iso639"
|
|
6
6
|
require "rfcxml"
|
|
7
7
|
require "relaton/core"
|
|
8
|
+
require "yeptris"
|
|
8
9
|
require_relative "version"
|
|
9
10
|
require_relative "bib/util"
|
|
10
11
|
require_relative "bib/sanitizer"
|
|
@@ -41,7 +42,7 @@ module Relaton
|
|
|
41
42
|
# @return [Hash{String=>String}] schema versions
|
|
42
43
|
#
|
|
43
44
|
def schema_versions
|
|
44
|
-
@@schema_versions ||= JSON.
|
|
45
|
+
@@schema_versions ||= Yeptris::JSON.load File.read(File.join(__dir__, "bib/versions.json"))
|
|
45
46
|
end
|
|
46
47
|
end
|
|
47
48
|
|
data/lib/relaton/bipm/CLAUDE.md
CHANGED
|
@@ -144,6 +144,20 @@ demonstrated rather than argued.
|
|
|
144
144
|
builds `index-v1` with them, and `id_parser_spec.rb` still covers them. Only
|
|
145
145
|
`bibliography.rb` stopped requiring the file.
|
|
146
146
|
|
|
147
|
+
### Internal identifier
|
|
148
|
+
|
|
149
|
+
`docid.to_s(form: :internal)` drops the
|
|
150
|
+
`BIPM` publisher (relaton#49): `BIPM SI Brochure Part 1` →
|
|
151
|
+
`SI Brochure Part 1`. Committee ids (`CGPM RES (1889)`, `CCTF REC 2 (2009)`)
|
|
152
|
+
have no publisher token and are the same in both forms. The rule is the `"BIPM"` row of
|
|
153
|
+
`Bib::Docidentifier::FORM_PUBID`: BIPM docidentifiers carry no pubid,
|
|
154
|
+
so the content is parsed with `Pubid::Bipm` at call time and rendered with
|
|
155
|
+
`to_s(with_publisher: false)`; unparseable content is returned unchanged. It
|
|
156
|
+
also holds for a relation's nested item. Do not move it into a `Bipm::Docidentifier`
|
|
157
|
+
subclass: typing the item's `docidentifier` collection with a subclass makes
|
|
158
|
+
YAML serialization reject the `Bib::Docidentifier` objects the parsers build
|
|
159
|
+
(`IncorrectModelError`).
|
|
160
|
+
|
|
147
161
|
### Data Sources
|
|
148
162
|
|
|
149
163
|
The gem fetches from three external datasets:
|
data/lib/relaton/bipm.rb
CHANGED
|
@@ -44,10 +44,6 @@ module Relaton
|
|
|
44
44
|
c if c.is_a?(::Pubid::Bsi::Identifier) || c.is_a?(::Pubid::AllParts)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
def to_s
|
|
48
|
-
content.to_s
|
|
49
|
-
end
|
|
50
|
-
|
|
51
47
|
# Strip the publication date to build a most-recent (undated) reference.
|
|
52
48
|
# pubid's `exclude` propagates into nested identifiers (so a consolidated
|
|
53
49
|
# base date is dropped while the amendment is kept); `:month` is excluded
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
# frozen_string_literal:true
|
|
2
3
|
|
|
3
|
-
require "json"
|
|
4
4
|
require "mechanize"
|
|
5
5
|
require "relaton/core"
|
|
6
6
|
require "relaton/index"
|
|
@@ -54,7 +54,7 @@ module Relaton::Calconnect
|
|
|
54
54
|
# 304 Not Modified — nothing changed since the last fetch
|
|
55
55
|
return if resp.code == "304"
|
|
56
56
|
|
|
57
|
-
data =
|
|
57
|
+
data = Relaton::Json.parse_external resp.body
|
|
58
58
|
all_success = true
|
|
59
59
|
Array(data["documents"]).each { |doc| all_success &&= parse_page doc }
|
|
60
60
|
self.etag = resp.response["etag"] if all_success
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
require "json"
|
|
2
3
|
require "mechanize"
|
|
3
4
|
require "relaton/index"
|
|
@@ -43,7 +44,7 @@ module Relaton
|
|
|
43
44
|
#
|
|
44
45
|
def fetch_docs(url)
|
|
45
46
|
resp = agent.get(url)
|
|
46
|
-
json =
|
|
47
|
+
json = Relaton::Json.parse_external resp.body.match(/const config = (.*);/)[1]
|
|
47
48
|
@array = json["data"].map { |doc| parse_and_save doc, json["data"] }
|
|
48
49
|
end
|
|
49
50
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
require "fileutils"
|
|
2
3
|
|
|
3
4
|
module Relaton
|
|
@@ -361,7 +362,7 @@ module Relaton
|
|
|
361
362
|
def entry_digest(entry)
|
|
362
363
|
require "digest"
|
|
363
364
|
require "json"
|
|
364
|
-
Digest::SHA256.hexdigest JSON.
|
|
365
|
+
Digest::SHA256.hexdigest Yeptris::JSON.dump(entry)
|
|
365
366
|
end
|
|
366
367
|
|
|
367
368
|
# Write, clobbering whatever is there. The caller has already established
|
|
@@ -67,7 +67,7 @@ module Relaton
|
|
|
67
67
|
# @return [Pubid::Identifier, nil]
|
|
68
68
|
def cache_pubid(ref)
|
|
69
69
|
pubid_class&.parse(ref)
|
|
70
|
-
rescue ArgumentError, ::Pubid::Errors::Error
|
|
70
|
+
rescue ArgumentError, ::Pubid::Errors::Error
|
|
71
71
|
# relaton-bib carries the log facade; core specs load without it.
|
|
72
72
|
require "relaton/bib"
|
|
73
73
|
Relaton::Bib::Util.info "does not parse as this flavor's pubid; " \
|
data/lib/relaton/core.rb
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
require "relaton/logger"
|
|
2
|
+
|
|
3
|
+
module Relaton
|
|
4
|
+
# Registered here as well as in the root entry: flavor files and their
|
|
5
|
+
# specs load directly via require "relaton/<flavor>", never through
|
|
6
|
+
# relaton.rb, and the boundary parsers need this constant either way.
|
|
7
|
+
autoload :Json, "relaton/json"
|
|
8
|
+
end
|
|
2
9
|
require_relative "version"
|
|
3
10
|
require_relative "core/request_error"
|
|
4
11
|
require_relative "core/array_wrapper"
|
data/lib/relaton/db/cache.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
require "fileutils"
|
|
2
3
|
require "digest"
|
|
3
|
-
require "json"
|
|
4
4
|
require "date"
|
|
5
5
|
require "lutaml/store"
|
|
6
6
|
require_relative "cache_entry"
|
|
@@ -520,7 +520,7 @@ module Relaton
|
|
|
520
520
|
def canonical(key)
|
|
521
521
|
return key if key.is_a? String
|
|
522
522
|
|
|
523
|
-
JSON.
|
|
523
|
+
Yeptris::JSON.load Yeptris::JSON.dump(key.to_hash)
|
|
524
524
|
end
|
|
525
525
|
|
|
526
526
|
def row_key(row)
|
|
@@ -556,7 +556,7 @@ module Relaton
|
|
|
556
556
|
|
|
557
557
|
# A digest of the canonical key, independent of the hash order.
|
|
558
558
|
def digest(key)
|
|
559
|
-
Digest::SHA256.hexdigest JSON.
|
|
559
|
+
Digest::SHA256.hexdigest Yeptris::JSON.dump(canonical_sorted(key))
|
|
560
560
|
end
|
|
561
561
|
|
|
562
562
|
def canonical_sorted(key)
|
data/lib/relaton/db/registry.rb
CHANGED
data/lib/relaton/db.rb
CHANGED
|
@@ -127,7 +127,15 @@ module Relaton
|
|
|
127
127
|
rescue Relaton::RequestError => e
|
|
128
128
|
args[3].call e
|
|
129
129
|
rescue StandardError => e
|
|
130
|
-
|
|
130
|
+
# An unparseable reference ("ISO Standards Tag Set" in a
|
|
131
|
+
# vocabulary corpus) is a not-found input, not a failure: the
|
|
132
|
+
# citation keeps its literal text and the build continues
|
|
133
|
+
# (relaton#412). Real errors keep the ERROR level.
|
|
134
|
+
if unreadable_reference?(e)
|
|
135
|
+
Util.info "`#{args[0]}` -- #{e.message}"
|
|
136
|
+
else
|
|
137
|
+
Util.error "`#{args[0]}` -- #{e.message}"
|
|
138
|
+
end
|
|
131
139
|
args[3].call nil
|
|
132
140
|
end
|
|
133
141
|
@queues[stdclass] =
|
|
@@ -213,6 +221,14 @@ module Relaton
|
|
|
213
221
|
|
|
214
222
|
private
|
|
215
223
|
|
|
224
|
+
# True when the error is a flavor's grammar refusing to read the
|
|
225
|
+
# reference. Both are raised by the flavors' pubid/parslet parses; a
|
|
226
|
+
# reference they cannot read is not-found, not a failure (relaton#412).
|
|
227
|
+
def unreadable_reference?(error)
|
|
228
|
+
(defined?(Pubid::Errors::Error) && error.is_a?(Pubid::Errors::Error)) ||
|
|
229
|
+
false
|
|
230
|
+
end
|
|
231
|
+
|
|
216
232
|
# The flavor routing picks for `fetch_async`'s per-flavor queue; nil
|
|
217
233
|
# (logged) when no flavor recognizes the reference.
|
|
218
234
|
def async_route(ref)
|
|
@@ -309,6 +325,9 @@ module Relaton
|
|
|
309
325
|
)
|
|
310
326
|
end
|
|
311
327
|
end
|
|
328
|
+
# Nothing resolved: answer not-found, not a stub that holds only the
|
|
329
|
+
# reference (relaton-un#14, metanorma-ietf#205).
|
|
330
|
+
doc.relation.empty? ? nil : doc
|
|
312
331
|
end
|
|
313
332
|
|
|
314
333
|
# The legacy string cache key, for a processor with no pubid class. The
|
|
@@ -370,7 +389,7 @@ module Relaton
|
|
|
370
389
|
return unless docid&.content
|
|
371
390
|
|
|
372
391
|
cache_key docid.content, nil, {}, stdclass
|
|
373
|
-
rescue ::Pubid::Errors::Error
|
|
392
|
+
rescue ::Pubid::Errors::Error
|
|
374
393
|
nil
|
|
375
394
|
end
|
|
376
395
|
|
|
@@ -437,7 +456,7 @@ module Relaton
|
|
|
437
456
|
def copublisher_query(code, opts, stdclass, publisher)
|
|
438
457
|
query = begin
|
|
439
458
|
@registry[stdclass].query_pubid(code, opts)
|
|
440
|
-
rescue ::Pubid::Errors::Error
|
|
459
|
+
rescue ::Pubid::Errors::Error
|
|
441
460
|
nil
|
|
442
461
|
end
|
|
443
462
|
return query if query
|
data/lib/relaton/doi/crossref.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
require "mechanize"
|
|
2
3
|
|
|
3
4
|
module Relaton
|
|
@@ -58,7 +59,7 @@ module Relaton
|
|
|
58
59
|
url = "https://api.crossref.org/works/#{CGI.escape(id)}"
|
|
59
60
|
loop do
|
|
60
61
|
resp = agent.get url
|
|
61
|
-
work =
|
|
62
|
+
work = Relaton::Json.parse_external resp.body
|
|
62
63
|
return work["message"] if work["status"] == "ok"
|
|
63
64
|
|
|
64
65
|
if n > 1
|
data/lib/relaton/doi/parser.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
module Relaton
|
|
2
3
|
module Doi
|
|
3
4
|
class Parser
|
|
@@ -856,7 +857,7 @@ module Relaton
|
|
|
856
857
|
throttled = 0
|
|
857
858
|
begin
|
|
858
859
|
resp = Crossref.agent.get url
|
|
859
|
-
|
|
860
|
+
Relaton::Json.parse_external(resp.body).dig("message", "items")
|
|
860
861
|
rescue Mechanize::ResponseCodeError => e
|
|
861
862
|
# 429 must be handled before the 4xx nil return: a throttle is not
|
|
862
863
|
# "no such record". Returning nil here made a rate-limited lookup look
|
|
@@ -881,7 +882,7 @@ module Relaton
|
|
|
881
882
|
retries += 1
|
|
882
883
|
retry if retries <= MAX_RETRIES
|
|
883
884
|
raise Relaton::RequestError, "Crossref network error after #{MAX_RETRIES} retries: #{e.message}"
|
|
884
|
-
rescue JSON::
|
|
885
|
+
rescue ::Yeptris::JSON::ParseError => e
|
|
885
886
|
raise Relaton::RequestError, "Crossref JSON parsing error: #{e.message}"
|
|
886
887
|
end
|
|
887
888
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
require "date"
|
|
2
|
-
require "json"
|
|
3
3
|
require "mechanize"
|
|
4
4
|
require "relaton/core"
|
|
5
5
|
require "relaton/index"
|
|
@@ -146,11 +146,11 @@ module Relaton
|
|
|
146
146
|
# @raise [BadPage] when the body is not a JSON array
|
|
147
147
|
#
|
|
148
148
|
def parse_page(page, body)
|
|
149
|
-
records =
|
|
149
|
+
records = Relaton::Json.parse_external(body)
|
|
150
150
|
return records if records.is_a?(Array)
|
|
151
151
|
|
|
152
152
|
raise BadPage, bad_page_message(page, body)
|
|
153
|
-
rescue JSON::
|
|
153
|
+
rescue ::Yeptris::JSON::ParseError
|
|
154
154
|
raise BadPage, bad_page_message(page, body)
|
|
155
155
|
end
|
|
156
156
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
|
|
4
5
|
require "net/http"
|
|
5
|
-
require "json"
|
|
6
6
|
require "moxml"
|
|
7
7
|
require_relative "scraper"
|
|
8
8
|
require_relative "item"
|
|
@@ -24,8 +24,8 @@ module Relaton
|
|
|
24
24
|
uri = URI "https://hbba.sacinfo.org.cn/stdQueryList"
|
|
25
25
|
resp = Net::HTTP.post uri, URI.encode_www_form({ key: text }),
|
|
26
26
|
"Content-Type" => "application/x-www-form-urlencoded"
|
|
27
|
-
# res =
|
|
28
|
-
json =
|
|
27
|
+
# res = Relaton::Json.parse_external Net::HTTP.get(uri)
|
|
28
|
+
json = Relaton::Json.parse_external resp.body
|
|
29
29
|
hits = json["records"].map do |h|
|
|
30
30
|
Hit.new pid: h["pk"], docref: h["code"], status: h["status"], scraper: self
|
|
31
31
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
require "relaton/core"
|
|
2
3
|
require_relative "../iec"
|
|
3
4
|
require_relative "data_parser"
|
|
@@ -135,7 +136,7 @@ module Relaton
|
|
|
135
136
|
Util.warn "#{res.body}"
|
|
136
137
|
break
|
|
137
138
|
end
|
|
138
|
-
json =
|
|
139
|
+
json = Relaton::Json.parse_external res.body
|
|
139
140
|
json[list_key].each { |pub| fetch_pub pub }
|
|
140
141
|
page += 1
|
|
141
142
|
next_page = res["link"]&.include? "rel=\"last\""
|
|
@@ -191,7 +192,7 @@ module Relaton
|
|
|
191
192
|
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
|
192
193
|
http.request req
|
|
193
194
|
end
|
|
194
|
-
|
|
195
|
+
Relaton::Json.parse_external(res.body)["access_token"]
|
|
195
196
|
end
|
|
196
197
|
end
|
|
197
198
|
|
|
@@ -46,10 +46,6 @@ module Relaton
|
|
|
46
46
|
original_content
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
def to_s
|
|
50
|
-
content.to_s
|
|
51
|
-
end
|
|
52
|
-
|
|
53
49
|
# `remove_part!`/`remove_date!` are unnecessary here: `to_all_parts`'s
|
|
54
50
|
# own identity computation already excludes part/date (down the whole
|
|
55
51
|
# base chain, so a supplement's own year is dropped too — a disclosed
|
data/lib/relaton/ietf/CLAUDE.md
CHANGED
|
@@ -36,7 +36,7 @@ Converter classes use `Bib::NamespaceHelper` which resolves `namespace` by takin
|
|
|
36
36
|
|
|
37
37
|
### Key Flows
|
|
38
38
|
|
|
39
|
-
1. **Single document lookup**: `Processor#get` → `Bibliography.get(code)` → `Scraper.scrape_page` → parses the reference with `Pubid::Ietf` → searches the one combined `index-v2` from `relaton-data-ietf` → fetches that row's YAML
|
|
39
|
+
1. **Single document lookup**: `Processor#get` → `Bibliography.get(code)` → `Scraper.scrape_page` → parses the reference with `Pubid::Ietf` → searches the one combined `index-v2` from `relaton-data-ietf` → fetches that row's YAML; a versioned draft the index lacks is read from bib.ietf.org (see below)
|
|
40
40
|
|
|
41
41
|
2. **Bulk data fetching**: `DataFetcher` extends `Relaton::Core::DataFetcher` with three datasets:
|
|
42
42
|
- `ietf-rfcsubseries` / `ietf-rfc-entries`: parse `rfc-index.xml` via `Rfc::Index` / `Rfc::Entry#to_item`
|
|
@@ -119,6 +119,30 @@ The old plain-string index matched that by substring; matching is exact now.
|
|
|
119
119
|
A reference pubid cannot parse returns nil, so `Bibliography.get "CN 8341"` logs
|
|
120
120
|
"Not found." rather than raising.
|
|
121
121
|
|
|
122
|
+
### An index miss on a versioned draft reads bib.ietf.org (relaton-ietf#125)
|
|
123
|
+
|
|
124
|
+
The data repo crawls the rsync `bibxml-ids` snapshot, and that snapshot keeps
|
|
125
|
+
only some versions of each draft: `draft-ietf-vcarddav-vcardrev` has only `-22`,
|
|
126
|
+
so `-06` was "Not found." When `fetch_doc` misses and the query is a
|
|
127
|
+
`Pubid::Ietf::Identifiers::InternetDraft` **with a version**, `fetch_bibxml` reads
|
|
128
|
+
`https://bib.ietf.org/public/rfc/bibxml-ids/reference.I-D.<draft>-NN.xml`
|
|
129
|
+
(`BIBXML_IDS`). That service resolves any version the datatracker knows and
|
|
130
|
+
answers 404 otherwise; the datatracker's own `doc/bibxml3/...` URL answers 404
|
|
131
|
+
for these old versions. The parse is the crawler's: `BibXMLParser.to_utf8`
|
|
132
|
+
(the CP1252 repair, shared with `DataFetcher#read_bibxml`), `BibXMLParser.parse`,
|
|
133
|
+
and the version from the identifier, as `process_singleton` sets it.
|
|
134
|
+
|
|
135
|
+
Only a 404 is "not found". Any other non-200 (429, 5xx) raises
|
|
136
|
+
`Relaton::RequestError`, so `Db#net_retry` retries it and `Db::Cache` does not
|
|
137
|
+
store an outage as a not-found row. A 200 whose body does not parse logs a
|
|
138
|
+
warning and returns nil; that rescue names the parse errors and is not
|
|
139
|
+
`StandardError`, so a transport error still becomes `RequestError`.
|
|
140
|
+
|
|
141
|
+
RFCs, sub-series and unversioned drafts do **not** fall back: the index holds
|
|
142
|
+
all of them, so a miss is a real "not found". A record read this way has no
|
|
143
|
+
`updates`/`updatedBy` neighbours — the crawl links those, and one fetch cannot.
|
|
144
|
+
Old bibxml carries no `target`, so such a record has a `TXT` URI and no `src`.
|
|
145
|
+
|
|
122
146
|
### The producer: `DataFetcher` writes `index-v2`
|
|
123
147
|
|
|
124
148
|
`DataFetcher` builds the index during the crawl, from the identifier it already
|
|
@@ -294,7 +318,8 @@ smart quotes and accented Latin letters. `File.read(encoding: "UTF-8")` only
|
|
|
294
318
|
*tags* a string; it validates nothing, so those reached lutaml as invalid UTF-8
|
|
295
319
|
and `Rfcxml::V3::Reference.from_xml` raised `Lutaml::Model::InvalidFormatError`.
|
|
296
320
|
|
|
297
|
-
`read_bibxml` transcodes them
|
|
321
|
+
`read_bibxml` transcodes them through `BibXMLParser.to_utf8`
|
|
322
|
+
(`bibxml_parser.rb`, shared with the scraper's bib.ietf.org fallback). Three things make that the
|
|
298
323
|
right call rather than `scrub`:
|
|
299
324
|
|
|
300
325
|
- **All of them decode losslessly as CP1252** — 125 of the ~122k published files
|
|
@@ -13,6 +13,44 @@ module Relaton
|
|
|
13
13
|
FromRfc.new(rfc).transform
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
#
|
|
17
|
+
# Decode bibxml bytes as UTF-8, recovering Windows-1252 bytes.
|
|
18
|
+
#
|
|
19
|
+
# These files declare `encoding='UTF-8'` but some carry CP1252 — smart
|
|
20
|
+
# quotes and accented Latin letters. `File.read(encoding: "UTF-8")` only
|
|
21
|
+
# *tags* the string, so those reach lutaml as invalid UTF-8 and it raises.
|
|
22
|
+
#
|
|
23
|
+
# `scrub` with a block transcodes each invalid *run* as CP1252 while
|
|
24
|
+
# leaving valid UTF-8 untouched. Both halves matter:
|
|
25
|
+
#
|
|
26
|
+
# * Not plain `scrub`, which substitutes U+FFFD: `client’s` would become
|
|
27
|
+
# `client�s` and `Muñoz` `Mu�oz` — author surnames included.
|
|
28
|
+
# The damage is length-preserving, so a length check will not catch it.
|
|
29
|
+
# * Not a whole-file CP1252 re-decode, which mangles a file that is
|
|
30
|
+
# genuinely UTF-8 apart from one stray byte: `Muñoz café ’` would come
|
|
31
|
+
# back as `Muñoz café ’`, silently, since the result is valid UTF-8 and
|
|
32
|
+
# nothing raises. No such file exists in today's corpus (0 of the 125
|
|
33
|
+
# affected contain valid multi-byte UTF-8) but it grows daily, and
|
|
34
|
+
# "decodes losslessly as CP1252" is weak evidence of correctness —
|
|
35
|
+
# CP1252 maps 251 of 256 byte values.
|
|
36
|
+
#
|
|
37
|
+
# `undef: :replace` covers the five bytes CP1252 leaves undefined
|
|
38
|
+
# (0x81 0x8D 0x8F 0x90 0x9D), so this returns valid UTF-8 rather than
|
|
39
|
+
# raising and costing the whole file.
|
|
40
|
+
#
|
|
41
|
+
# @param bytes [String] raw bytes, any encoding tag
|
|
42
|
+
# @return [String] UTF-8, valid encoding
|
|
43
|
+
#
|
|
44
|
+
def to_utf8(bytes)
|
|
45
|
+
utf8 = bytes.dup.force_encoding(Encoding::UTF_8)
|
|
46
|
+
return utf8 if utf8.valid_encoding?
|
|
47
|
+
|
|
48
|
+
utf8.scrub do |bad|
|
|
49
|
+
bad.force_encoding(Encoding::WINDOWS_1252)
|
|
50
|
+
.encode(Encoding::UTF_8, undef: :replace)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
16
54
|
def pubid_type(id)
|
|
17
55
|
pref = id.match(/^(\S+)/)[1]
|
|
18
56
|
case pref
|
|
@@ -216,41 +216,14 @@ module Relaton
|
|
|
216
216
|
end
|
|
217
217
|
|
|
218
218
|
#
|
|
219
|
-
# Read a bibxml file as UTF-8, recovering Windows-1252 bytes.
|
|
220
|
-
#
|
|
221
|
-
# These files declare `encoding='UTF-8'` but some carry CP1252 — smart
|
|
222
|
-
# quotes and accented Latin letters. `File.read(encoding: "UTF-8")` only
|
|
223
|
-
# *tags* the string, so those reach lutaml as invalid UTF-8 and it raises.
|
|
224
|
-
#
|
|
225
|
-
# `scrub` with a block transcodes each invalid *run* as CP1252 while
|
|
226
|
-
# leaving valid UTF-8 untouched. Both halves matter:
|
|
227
|
-
#
|
|
228
|
-
# * Not plain `scrub`, which substitutes U+FFFD: `client’s` would become
|
|
229
|
-
# `client\uFFFDs` and `Muñoz` `Mu\uFFFDoz` — author surnames included.
|
|
230
|
-
# The damage is length-preserving, so a length check will not catch it.
|
|
231
|
-
# * Not a whole-file CP1252 re-decode, which mangles a file that is
|
|
232
|
-
# genuinely UTF-8 apart from one stray byte: `Muñoz café ’` would come
|
|
233
|
-
# back as `Muñoz café ’`, silently, since the result is valid UTF-8 and
|
|
234
|
-
# nothing raises. No such file exists in today's corpus (0 of the 125
|
|
235
|
-
# affected contain valid multi-byte UTF-8) but it grows daily, and
|
|
236
|
-
# "decodes losslessly as CP1252" is weak evidence of correctness —
|
|
237
|
-
# CP1252 maps 251 of 256 byte values.
|
|
238
|
-
#
|
|
239
|
-
# `undef: :replace` covers the five bytes CP1252 leaves undefined
|
|
240
|
-
# (0x81 0x8D 0x8F 0x90 0x9D), so this returns valid UTF-8 rather than
|
|
241
|
-
# raising and costing the whole file.
|
|
219
|
+
# Read a bibxml file as UTF-8, recovering Windows-1252 bytes. The repair
|
|
220
|
+
# is `BibXMLParser.to_utf8`, shared with `Scraper`'s bib.ietf.org fallback.
|
|
242
221
|
#
|
|
243
222
|
# @param path [String]
|
|
244
223
|
# @return [String] UTF-8, valid encoding
|
|
245
224
|
#
|
|
246
225
|
def read_bibxml(path)
|
|
247
|
-
|
|
248
|
-
return utf8 if utf8.valid_encoding?
|
|
249
|
-
|
|
250
|
-
utf8.scrub do |bad|
|
|
251
|
-
bad.force_encoding(Encoding::WINDOWS_1252)
|
|
252
|
-
.encode(Encoding::UTF_8, undef: :replace)
|
|
253
|
-
end
|
|
226
|
+
BibXMLParser.to_utf8 File.binread(path)
|
|
254
227
|
end
|
|
255
228
|
|
|
256
229
|
#
|