relaton 3.0.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/relaton/3gpp/CLAUDE.md +31 -0
- data/lib/relaton/3gpp/bibliography.rb +1 -1
- data/lib/relaton/3gpp/data_fetcher.rb +32 -9
- data/lib/relaton/3gpp/date.rb +13 -0
- data/lib/relaton/3gpp/item.rb +3 -0
- data/lib/relaton/3gpp/parser.rb +9 -6
- data/lib/relaton/bib/converter/csl.rb +3 -2
- data/lib/relaton/bib/model/docidentifier.rb +61 -0
- data/lib/relaton/bib/model/item_shared.rb +12 -1
- data/lib/relaton/bib.rb +2 -1
- data/lib/relaton/bipm/CLAUDE.md +14 -0
- data/lib/relaton/bsi/model/docidentifier.rb +0 -4
- data/lib/relaton/calconnect/data_fetcher.rb +11 -3
- data/lib/relaton/ccsds/data/fetcher.rb +2 -1
- data/lib/relaton/ccsds/model/docidentifier.rb +0 -4
- data/lib/relaton/cie/authors_parser.rb +78 -0
- data/lib/relaton/cie/data_fetcher.rb +23 -18
- data/lib/relaton/cie.rb +1 -0
- data/lib/relaton/core/data_fetcher.rb +23 -1
- data/lib/relaton/core.rb +7 -0
- data/lib/relaton/db/cache.rb +3 -3
- data/lib/relaton/db.rb +20 -1
- 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/iana/data_fetcher.rb +53 -4
- data/lib/relaton/iec/data_fetcher.rb +3 -2
- data/lib/relaton/iec/model/docidentifier.rb +0 -4
- data/lib/relaton/ieee/CLAUDE.md +30 -4
- data/lib/relaton/ieee/bibliography.rb +87 -32
- data/lib/relaton/ieee/data_fetcher.rb +1 -1
- data/lib/relaton/ieee/docidentifier.rb +37 -0
- data/lib/relaton/ieee/idams_parser.rb +2 -2
- data/lib/relaton/ieee/item.rb +3 -0
- data/lib/relaton/ieee/item_data.rb +7 -1
- 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/jis/hit_collection.rb +1 -2
- data/lib/relaton/jis.rb +1 -0
- 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 +6 -0
- data/lib/relaton/nist/docidentifier.rb +0 -4
- data/lib/relaton/nist/hit_collection.rb +1 -1
- 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/plateau/hit_collection.rb +1 -1
- data/lib/relaton/plateau.rb +1 -0
- data/lib/relaton/un/hit_collection.rb +4 -3
- data/lib/relaton/version.rb +1 -1
- data/lib/relaton.rb +1 -0
- metadata +40 -24
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f15745a534625b27b3d013d7b3d6edd7e0185f1bcc1c545f7b44dc6059d9d113
|
|
4
|
+
data.tar.gz: ac15b5259abb3c980cd6bee9d10c5f2f0ae12fea955478ca28f7da82027ab6f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: de3f2c7182f6a2b1737a54e9168bcab856dbc81fe169f293b5b932f0224e8cb95187f506829230068ebc5308884e89a918515b03838f969c9c9870020442efff
|
|
7
|
+
data.tar.gz: a4bfe1d14667d8907eecd43fe880b02e7d15ceab20d2c6302c8f5c220d6c4398b3adefa056ff8815639686ad85674713c5c3a4759bd5ad57ffdd1e42b9bc39e1
|
data/lib/relaton/3gpp/CLAUDE.md
CHANGED
|
@@ -154,6 +154,37 @@ guards does not depend on it. `Processor` also sets
|
|
|
154
154
|
`@pubid_flavor = :Tgpp`, so `Core::Processor#prefixes` reads
|
|
155
155
|
`Pubid::Tgpp.prefixes` (`["3GPP"]`).
|
|
156
156
|
|
|
157
|
+
### Dates: published, then the Release start and end
|
|
158
|
+
|
|
159
|
+
`Parser#parse_date` emits the `published` date (CSV `Date`) first, then
|
|
160
|
+
`project-start` from the CSV `Project Start` column and `released` from
|
|
161
|
+
`Project End` (relaton-3gpp#13). #15 defines the Release end date as the date
|
|
162
|
+
the Release is released, and the issue thread asks for it as the release date
|
|
163
|
+
of the specification — hence `released`, not `project-end`. Both are the dates
|
|
164
|
+
of the document's **Release**, not of the document: every version in one
|
|
165
|
+
Release carries the same pair, so neither fits a standard type (`issued` would
|
|
166
|
+
date a 2004 `REL-98` version to 1999). They also stay in `ext/release`
|
|
167
|
+
(`project_start` / `project_end`). An empty column gives no date;
|
|
168
|
+
`@errors[:date]` tracks the published date only.
|
|
169
|
+
|
|
170
|
+
- **`Relaton::ThreeGpp::Date`** (`date.rb`) extends `Bib::Date`'s `type`
|
|
171
|
+
value list with the two types, and `Item` shadows `date` with it (as it does
|
|
172
|
+
`docidentifier`). Without it `validate!` rejects the types; the RelaxNG
|
|
173
|
+
grammar already accepts any text. The shadow covers the top-level item
|
|
174
|
+
only: an item nested in a `relation` (the transposition) reloads as
|
|
175
|
+
`Bib::ItemBase` with plain `Bib::Date`s. Nothing validates those today.
|
|
176
|
+
- **The constant hides Ruby's `::Date`** inside `Relaton::ThreeGpp`. Write
|
|
177
|
+
`::Date.parse` / `::Date.today` in this flavor; a bare `Date.today` now
|
|
178
|
+
raises `NoMethodError`.
|
|
179
|
+
- **Select the publication date by type, never by `date[0]`.**
|
|
180
|
+
`DataFetcher#published_date` does this for the transposition check: a row
|
|
181
|
+
with no `Date` still has the two Release dates, so `date.any?` no longer
|
|
182
|
+
means "has a publication date".
|
|
183
|
+
- `spec/3gpp/fixtures/bib.xml`, `bibdata.xml` and `bib.yaml` are rendered from
|
|
184
|
+
the `3gpp_get_document` cassette, i.e. from the published
|
|
185
|
+
`relaton-data-3gpp` record. They get the two dates only after that repo
|
|
186
|
+
re-crawls and the cassette is re-recorded — do not hand-add them.
|
|
187
|
+
|
|
157
188
|
## Testing
|
|
158
189
|
|
|
159
190
|
- `bundle exec rake spec:3gpp` — run the 3GPP suite.
|
|
@@ -25,7 +25,7 @@ module Relaton
|
|
|
25
25
|
return unless resp.code == "200"
|
|
26
26
|
|
|
27
27
|
item = Item.from_yaml(resp.body)
|
|
28
|
-
item.fetched = Date.today.to_s
|
|
28
|
+
item.fetched = ::Date.today.to_s
|
|
29
29
|
item
|
|
30
30
|
rescue SocketError, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET,
|
|
31
31
|
EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
|
|
@@ -42,7 +42,14 @@ module Relaton
|
|
|
42
42
|
index.remove_all # if renewal
|
|
43
43
|
end
|
|
44
44
|
CSV.open(file, "r:bom|utf-8", headers: true, col_sep: ";").each do |row|
|
|
45
|
-
|
|
45
|
+
# The row carries the whole record payload, so an unchanged row
|
|
46
|
+
# skips the parse and the write (the digest ledger). The row is
|
|
47
|
+
# only read when the ledger is on.
|
|
48
|
+
key = @digest_ledger ? "#{row[0]}/#{row['Version']}" : nil
|
|
49
|
+
next if key && unchanged?(key, row.to_h)
|
|
50
|
+
|
|
51
|
+
file = save_doc Parser.parse(row, @errors)
|
|
52
|
+
note_digest key, row.to_h, file: file if key
|
|
46
53
|
end
|
|
47
54
|
File.write CURRENT, @current.to_yaml, encoding: "UTF-8"
|
|
48
55
|
index.save
|
|
@@ -132,7 +139,11 @@ module Relaton
|
|
|
132
139
|
@files << file
|
|
133
140
|
index_primary bib1.docidentifier&.detect(&:primary), file
|
|
134
141
|
end
|
|
135
|
-
|
|
142
|
+
return nil if bib1.nil?
|
|
143
|
+
|
|
144
|
+
File.write file, serialize(bib1), encoding: "UTF-8"
|
|
145
|
+
record_written file
|
|
146
|
+
file
|
|
136
147
|
end
|
|
137
148
|
|
|
138
149
|
#
|
|
@@ -210,11 +221,9 @@ module Relaton
|
|
|
210
221
|
# @return [Array<Relaton3gpp::BibliographicItem, Boolean>] main bibliographic item,
|
|
211
222
|
# related bibliographic item, true if relation has been added
|
|
212
223
|
#
|
|
213
|
-
def transposed_relation(bib, existed)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
end
|
|
217
|
-
return [existed, bib, true] if bib.date.none? && existed.date.any?
|
|
224
|
+
def transposed_relation(bib, existed)
|
|
225
|
+
return [bib, existed, false] unless published_date(existed)
|
|
226
|
+
return [existed, bib, true] unless published_date(bib)
|
|
218
227
|
|
|
219
228
|
check_transposed_date bib, existed
|
|
220
229
|
end
|
|
@@ -229,16 +238,30 @@ module Relaton
|
|
|
229
238
|
# related bibliographic item, true if relation has been added
|
|
230
239
|
#
|
|
231
240
|
def check_transposed_date(bib, existed)
|
|
232
|
-
|
|
241
|
+
bib_date = published_date(bib).at
|
|
242
|
+
existed_date = published_date(existed).at
|
|
243
|
+
if bib_date < existed_date
|
|
233
244
|
add_transposed_relation bib, existed
|
|
234
245
|
[bib, existed, true]
|
|
235
|
-
elsif
|
|
246
|
+
elsif bib_date > existed_date
|
|
236
247
|
add_transposed_relation existed, bib
|
|
237
248
|
[existed, bib, true]
|
|
238
249
|
else [bib, existed, false]
|
|
239
250
|
end
|
|
240
251
|
end
|
|
241
252
|
|
|
253
|
+
#
|
|
254
|
+
# The publication date. The date array also holds the Release start and
|
|
255
|
+
# end, which say nothing about which version came first.
|
|
256
|
+
#
|
|
257
|
+
# @param [Relaton::ThreeGpp::ItemData] bib bibliographic item
|
|
258
|
+
#
|
|
259
|
+
# @return [Relaton::ThreeGpp::Date, nil]
|
|
260
|
+
#
|
|
261
|
+
def published_date(bib)
|
|
262
|
+
bib.date.detect { |d| d.type == "published" }
|
|
263
|
+
end
|
|
264
|
+
|
|
242
265
|
#
|
|
243
266
|
# Add transposed relation
|
|
244
267
|
#
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Relaton
|
|
2
|
+
module ThreeGpp
|
|
3
|
+
# A bibliographic date that also accepts the start of the document's 3GPP
|
|
4
|
+
# Release (`project-start`) and its end, which is the release date of the
|
|
5
|
+
# Release (`released`, relaton-3gpp#13). The RelaxNG grammar takes any text
|
|
6
|
+
# as a date type; only the Bib::Date value list needs the two extra types.
|
|
7
|
+
class Date < Bib::Date
|
|
8
|
+
RELEASE_TYPES = %w[project-start released].freeze
|
|
9
|
+
|
|
10
|
+
attribute :type, :string, values: Bib::Date.attributes[:type].options[:values] + RELEASE_TYPES
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/relaton/3gpp/item.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require_relative "item_data"
|
|
2
2
|
require_relative "ext"
|
|
3
3
|
require_relative "docidentifier"
|
|
4
|
+
require_relative "date"
|
|
4
5
|
|
|
5
6
|
module Relaton
|
|
6
7
|
module ThreeGpp
|
|
@@ -13,6 +14,8 @@ module Relaton
|
|
|
13
14
|
# Item, so this one declaration covers all three.
|
|
14
15
|
attribute :docidentifier, Docidentifier, collection: true,
|
|
15
16
|
initialize_empty: true
|
|
17
|
+
# Same shadow for the dates, which carry the Release start and end.
|
|
18
|
+
attribute :date, Date, collection: true, initialize_empty: true
|
|
16
19
|
end
|
|
17
20
|
end
|
|
18
21
|
end
|
data/lib/relaton/3gpp/parser.rb
CHANGED
|
@@ -143,17 +143,20 @@ module Relaton
|
|
|
143
143
|
# end
|
|
144
144
|
|
|
145
145
|
#
|
|
146
|
-
# Parse date
|
|
146
|
+
# Parse date. The published date comes first, then the start of the
|
|
147
|
+
# document's Release and its end as the release date (relaton-3gpp#13).
|
|
147
148
|
#
|
|
148
|
-
# @return [Array<
|
|
149
|
+
# @return [Array<Relaton::ThreeGpp::Date>] date
|
|
149
150
|
#
|
|
150
151
|
def parse_date
|
|
151
152
|
dates = []
|
|
152
153
|
if @row["Date"]
|
|
153
|
-
on = Date.parse(@row["Date"]).to_s
|
|
154
|
-
dates <<
|
|
154
|
+
on = ::Date.parse(@row["Date"]).to_s
|
|
155
|
+
dates << Date.new(type: "published", at: on)
|
|
155
156
|
end
|
|
156
157
|
@errors[:date] &&= dates.empty?
|
|
158
|
+
dates << Date.new(type: "project-start", at: project_start.to_s) if project_start
|
|
159
|
+
dates << Date.new(type: "released", at: project_end.to_s) if project_end
|
|
157
160
|
dates
|
|
158
161
|
end
|
|
159
162
|
|
|
@@ -388,12 +391,12 @@ module Relaton
|
|
|
388
391
|
|
|
389
392
|
def project_start
|
|
390
393
|
@errors[:project_start] &&= @row["Project Start"].nil? || @row["Project Start"].empty?
|
|
391
|
-
Date.parse(@row["Project Start"]) if @row["Project Start"]
|
|
394
|
+
::Date.parse(@row["Project Start"]) if @row["Project Start"]
|
|
392
395
|
end
|
|
393
396
|
|
|
394
397
|
def project_end
|
|
395
398
|
@errors[:project_end] &&= @row["Project End"].nil? || @row["Project End"].empty?
|
|
396
|
-
Date.parse(@row["Project End"]) if @row["Project End"]
|
|
399
|
+
::Date.parse(@row["Project End"]) if @row["Project End"]
|
|
397
400
|
end
|
|
398
401
|
end
|
|
399
402
|
end
|
|
@@ -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
|
|
@@ -59,10 +59,21 @@ module Relaton
|
|
|
59
59
|
def self.prune_attribute(base, attr_name, xml_name)
|
|
60
60
|
return unless base.attributes.key?(attr_name)
|
|
61
61
|
|
|
62
|
+
# lutaml-model 0.8.142 shares a subclass's mappings/attributes with
|
|
63
|
+
# its superclass until the class's own DSL forks them (copy-on-write).
|
|
64
|
+
# These deletes must stay private to `base`: without the fork, pruning
|
|
65
|
+
# Bibdata's :id erased it from Bibitem (and Bibitem's :ext from
|
|
66
|
+
# Bibdata), and every from_xml raised "Attribute ... not found".
|
|
67
|
+
# 0.8.141 and older give each class private copies and have no
|
|
68
|
+
# lutaml_cow_fork!, where mutating in place was already safe.
|
|
69
|
+
if base.respond_to?(:lutaml_cow_fork!)
|
|
70
|
+
base.lutaml_cow_fork!(:mappings)
|
|
71
|
+
base.lutaml_cow_fork!(:attributes)
|
|
72
|
+
end
|
|
62
73
|
xml_mapping = base.mappings[:xml]
|
|
63
74
|
xml_mapping.instance_variable_get(:@elements).delete(xml_name)
|
|
64
75
|
xml_mapping.instance_variable_get(:@attributes).delete(xml_name)
|
|
65
|
-
base.
|
|
76
|
+
base.class_attributes.delete(attr_name)
|
|
66
77
|
end
|
|
67
78
|
|
|
68
79
|
XML_BODY = lambda do # rubocop:disable Metrics/BlockLength
|
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:
|
|
@@ -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
|
|
@@ -70,8 +70,14 @@ module Relaton::Calconnect
|
|
|
70
70
|
# @param [Hash] doc
|
|
71
71
|
#
|
|
72
72
|
def parse_page(doc)
|
|
73
|
+
# The document payload carries the whole record, so an unchanged
|
|
74
|
+
# payload skips the scrape and the write (the digest ledger). The
|
|
75
|
+
# payload is only read when the ledger is on.
|
|
76
|
+
return true if @digest_ledger && unchanged?(doc["id"], doc)
|
|
77
|
+
|
|
73
78
|
bib = Scraper.new(@errors).parse_page doc
|
|
74
|
-
write_doc doc["id"], bib
|
|
79
|
+
file = write_doc doc["id"], bib
|
|
80
|
+
note_digest doc["id"], doc, file: file if @digest_ledger
|
|
75
81
|
true
|
|
76
82
|
rescue StandardError => e
|
|
77
83
|
Util.warn "Document: #{doc['id']}"
|
|
@@ -95,7 +101,9 @@ module Relaton::Calconnect
|
|
|
95
101
|
# Write first, index second: an id pubid rejects is skipped from the
|
|
96
102
|
# index, and the document still has to reach disk.
|
|
97
103
|
File.write file, serialize(bib), encoding: "UTF-8"
|
|
104
|
+
record_written file
|
|
98
105
|
add_to_index bib, file
|
|
106
|
+
file
|
|
99
107
|
end
|
|
100
108
|
|
|
101
109
|
#
|
|
@@ -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
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Relaton
|
|
2
|
+
module Cie
|
|
3
|
+
# CIE publishes its author lists as one inconsistently formatted string:
|
|
4
|
+
# "Surname, I.X." pairs, bare "Forename Surname" runs, Chinese-style
|
|
5
|
+
# "Surname Given" without a comma, compound surnames ("Nilsson
|
|
6
|
+
# Tengelin"), broken hyphens ("Dam- Hansen"), " and " before the final
|
|
7
|
+
# author, and "et al." tails (relaton-cie#5). The comma is overloaded --
|
|
8
|
+
# it separates authors and marks the surname/forename boundary -- so the
|
|
9
|
+
# parse walks comma-separated segments with three rules:
|
|
10
|
+
#
|
|
11
|
+
# 1. a single-word segment binds the following segment as its forenames
|
|
12
|
+
# ("Hemphälä, Hillevi"; initials "A.Sh." bind the same way);
|
|
13
|
+
# 2. a multi-word segment followed by an initials-only segment binds
|
|
14
|
+
# them ("Nilsson Tengelin, M."), because initials never stand alone;
|
|
15
|
+
# 3. any other segment is one author kept whole (completename) --
|
|
16
|
+
# "Glimne Susanne", "Annika K. Jägerbrand", "Pan Jiangen": which word
|
|
17
|
+
# is the surname would be a guess, so none is made.
|
|
18
|
+
module AuthorsParser
|
|
19
|
+
Author = Struct.new(:surname, :forenames, :completename)
|
|
20
|
+
|
|
21
|
+
# Uppercase-led, dot/hyphen/space-separated short parts only:
|
|
22
|
+
# "A.Sh.", "H.T.B", "W-T. M.", "N." -- never "Li" (lowercase tail),
|
|
23
|
+
# "Guillaume" (long undotted part) or "Bierman".
|
|
24
|
+
INITIALS_RX = /\A\p{Lu}(?:[.\s-]+\p{Lu}\p{Ll}{0,2})*\.?\z/.freeze
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# @param string [String] the CIE authors string
|
|
28
|
+
# @return [Array<Author>]
|
|
29
|
+
def parse(string)
|
|
30
|
+
authors = []
|
|
31
|
+
segments = segmentize(string)
|
|
32
|
+
i = 0
|
|
33
|
+
while i < segments.length
|
|
34
|
+
seg = segments[i]
|
|
35
|
+
nxt = segments[i + 1]
|
|
36
|
+
i += if initials?(seg)
|
|
37
|
+
# Defensive: a stray initials segment binds left.
|
|
38
|
+
authors.last&.forenames << seg
|
|
39
|
+
1
|
|
40
|
+
elsif single_word?(seg) || nxt && initials?(nxt)
|
|
41
|
+
# "Surname, Forename": a single-word segment binds
|
|
42
|
+
# whatever follows; a multi-word one only binds trailing
|
|
43
|
+
# initials, which never stand alone.
|
|
44
|
+
authors << Author.new(seg, nxt ? [nxt] : [], nil)
|
|
45
|
+
nxt ? 2 : 1
|
|
46
|
+
else
|
|
47
|
+
authors << Author.new(nil, nil, seg)
|
|
48
|
+
1
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
authors
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param seg [String]
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def single_word?(seg)
|
|
57
|
+
!seg.include?(" ")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @param seg [String]
|
|
61
|
+
# @return [Boolean]
|
|
62
|
+
def initials?(seg)
|
|
63
|
+
INITIALS_RX.match? seg
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def segmentize(string)
|
|
69
|
+
string
|
|
70
|
+
.gsub(/(\p{L})- (?=\p{Lu})/, '\1-')
|
|
71
|
+
.gsub(%r{\s*,?\s+and\s+}i, ", ")
|
|
72
|
+
.sub(/\s*,?\s*et\s+al\.?\s*\z/i, "")
|
|
73
|
+
.split(/\s*[,;]\s*/).map(&:strip).reject(&:empty?)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -329,28 +329,21 @@ module Relaton
|
|
|
329
329
|
def fetch_contributor(doc) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/CyclomaticComplexity
|
|
330
330
|
authors = doc.xpath('//hgroup/p[not(@class="pub_date")]').text.gsub "\"", ""
|
|
331
331
|
contribs = []
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
surname = Bib::LocalizedString.new content: sname, language: "en", script: "Latn"
|
|
343
|
-
forename = []
|
|
344
|
-
forename << Bib::FullNameType::Forename.new(content: fname, language: "en", script: "Latn") if fname
|
|
345
|
-
(init&.strip || "").split(/(?:,|\.)(?:-|\s)?/).each do |int|
|
|
346
|
-
forename << Bib::FullNameType::Forename.new(content: "", initial: int.strip, language: "en", script: "Latn")
|
|
347
|
-
end
|
|
348
|
-
fullname = Bib::FullName.new surname: surname, forename: forename
|
|
332
|
+
AuthorsParser.parse(authors).each do |author|
|
|
333
|
+
fullname =
|
|
334
|
+
if author.completename
|
|
335
|
+
Bib::FullName.new completename: name_string(author.completename)
|
|
336
|
+
else
|
|
337
|
+
Bib::FullName.new(
|
|
338
|
+
surname: name_string(author.surname),
|
|
339
|
+
forename: author.forenames.map { |f| forename(f) },
|
|
340
|
+
)
|
|
341
|
+
end
|
|
349
342
|
person = Bib::Person.new name: fullname
|
|
350
343
|
role = Bib::Contributor::Role.new type: "author"
|
|
351
344
|
contribs << Bib::Contributor.new(person: person, role: [role])
|
|
352
|
-
@errors[:contributor_author] &&= contribs.empty?
|
|
353
345
|
end
|
|
346
|
+
@errors[:contributor_author] &&= contribs.empty?
|
|
354
347
|
org_name = Bib::TypedLocalizedString.new(content: "Commission Internationale de L'Eclairage")
|
|
355
348
|
abbrev = Bib::LocalizedString.new content: "CIE"
|
|
356
349
|
org_uri = Bib::Uri.new content: "cie.co.at"
|
|
@@ -359,6 +352,18 @@ module Relaton
|
|
|
359
352
|
contribs << Bib::Contributor.new(organization: org, role: [org_role])
|
|
360
353
|
end
|
|
361
354
|
|
|
355
|
+
def name_string(str)
|
|
356
|
+
Bib::LocalizedString.new content: str, language: "en", script: "Latn"
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def forename(str)
|
|
360
|
+
if AuthorsParser.initials?(str)
|
|
361
|
+
Bib::FullNameType::Forename.new(initial: str.sub(/\.\z/, ""))
|
|
362
|
+
else
|
|
363
|
+
Bib::FullNameType::Forename.new(content: str)
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
362
367
|
def fetch_ext
|
|
363
368
|
Ext.new(doctype: fetch_doctype, flavor: "cie")
|
|
364
369
|
end
|
data/lib/relaton/cie.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
require "fileutils"
|
|
2
3
|
|
|
3
4
|
module Relaton
|
|
@@ -36,6 +37,17 @@ module Relaton
|
|
|
36
37
|
# @return [Boolean] whether this run prunes the existing output tree
|
|
37
38
|
attr_accessor :prune
|
|
38
39
|
|
|
40
|
+
# Record a file this run wrote through a flavor's own writer, so the
|
|
41
|
+
# ledger's save (which keeps only files this run owned) and
|
|
42
|
+
# #prune_unwritten account for it. Core's write_unique records its
|
|
43
|
+
# own writes; a flavor writing directly calls this after File.write.
|
|
44
|
+
#
|
|
45
|
+
# @param file [String]
|
|
46
|
+
# @return [void]
|
|
47
|
+
def record_written(file)
|
|
48
|
+
@written << file
|
|
49
|
+
end
|
|
50
|
+
|
|
39
51
|
# @return [Boolean] whether this run consults and maintains the
|
|
40
52
|
# content-digest ledger (see #unchanged?). Off by default: a flavor
|
|
41
53
|
# turns it on only where its enumeration entry determines the record
|
|
@@ -91,6 +103,16 @@ module Relaton
|
|
|
91
103
|
ledger_new[key] = { "digest" => entry_digest(entry), "file" => file }
|
|
92
104
|
end
|
|
93
105
|
|
|
106
|
+
# The file a previous run recorded for `key`, for a caller that
|
|
107
|
+
# skipped the record via #unchanged? and still needs to know where it
|
|
108
|
+
# lives (an alias pointing at another record's file).
|
|
109
|
+
#
|
|
110
|
+
# @param key [String]
|
|
111
|
+
# @return [String, nil]
|
|
112
|
+
def ledger_file(key)
|
|
113
|
+
ledger.dig(key, "file")
|
|
114
|
+
end
|
|
115
|
+
|
|
94
116
|
# Persist the ledger: this run's noted entries over the loaded ones,
|
|
95
117
|
# keeping only entries whose file this run recorded as written.
|
|
96
118
|
#
|
|
@@ -340,7 +362,7 @@ module Relaton
|
|
|
340
362
|
def entry_digest(entry)
|
|
341
363
|
require "digest"
|
|
342
364
|
require "json"
|
|
343
|
-
Digest::SHA256.hexdigest JSON.
|
|
365
|
+
Digest::SHA256.hexdigest Yeptris::JSON.dump(entry)
|
|
344
366
|
end
|
|
345
367
|
|
|
346
368
|
# Write, clobbering whatever is there. The caller has already established
|
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"
|