relaton 3.0.0.pre.alpha.16 → 3.0.0.pre.alpha.18
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/data/equivalence/adoption-chains.yaml +38 -0
- data/lib/relaton/3gpp/CLAUDE.md +187 -0
- data/lib/relaton/3gpp/README.adoc +149 -0
- data/lib/relaton/adobe/CLAUDE.md +104 -0
- data/lib/relaton/bib/CLAUDE.md +130 -0
- data/lib/relaton/bib/README.adoc +330 -0
- data/lib/relaton/bib/model/series.rb +1 -1
- data/lib/relaton/bipm/CLAUDE.md +173 -0
- data/lib/relaton/bipm/README.adoc +352 -0
- data/lib/relaton/bipm/data_fetcher.rb +3 -2
- data/lib/relaton/bsi/CLAUDE.md +35 -0
- data/lib/relaton/bsi/README.adoc +112 -0
- data/lib/relaton/calconnect/CLAUDE.md +177 -0
- data/lib/relaton/calconnect/README.adoc +140 -0
- data/lib/relaton/ccsds/CLAUDE.md +74 -0
- data/lib/relaton/ccsds/README.adoc +154 -0
- data/lib/relaton/cen/CLAUDE.md +219 -0
- data/lib/relaton/cen/README.adoc +155 -0
- data/lib/relaton/cie/CLAUDE.md +90 -0
- data/lib/relaton/cie/README.adoc +121 -0
- data/lib/relaton/core/CLAUDE.md +140 -0
- data/lib/relaton/core/README.adoc +64 -0
- data/lib/relaton/core/data_fetcher.rb +50 -3
- data/lib/relaton/db/CLAUDE.md +373 -0
- data/lib/relaton/db.rb +19 -1
- data/lib/relaton/doi/CLAUDE.md +105 -0
- data/lib/relaton/doi/README.adoc +89 -0
- data/lib/relaton/easc/CLAUDE.md +85 -0
- data/lib/relaton/ecma/CLAUDE.md +217 -0
- data/lib/relaton/ecma/README.adoc +178 -0
- data/lib/relaton/equivalence.rb +89 -0
- data/lib/relaton/etsi/CLAUDE.md +219 -0
- data/lib/relaton/etsi/README.adoc +137 -0
- data/lib/relaton/evs/CLAUDE.md +120 -0
- data/lib/relaton/evs/README.adoc +27 -0
- data/lib/relaton/gb/CLAUDE.md +47 -0
- data/lib/relaton/gb/README.adoc +169 -0
- data/lib/relaton/gost/CLAUDE.md +100 -0
- data/lib/relaton/iala/CLAUDE.md +97 -0
- data/lib/relaton/iana/CLAUDE.md +105 -0
- data/lib/relaton/iana/README.adoc +107 -0
- data/lib/relaton/iec/CLAUDE.md +13 -0
- data/lib/relaton/iec/README.adoc +280 -0
- data/lib/relaton/iec/data_fetcher.rb +24 -8
- data/lib/relaton/iec/project_fetcher.rb +48 -0
- data/lib/relaton/iec/project_parser.rb +111 -0
- data/lib/relaton/iec.rb +5 -0
- data/lib/relaton/ieee/CLAUDE.md +176 -0
- data/lib/relaton/ieee/README.adoc +131 -0
- data/lib/relaton/ietf/CLAUDE.md +357 -0
- data/lib/relaton/ietf/README.adoc +167 -0
- data/lib/relaton/iho/CLAUDE.md +70 -0
- data/lib/relaton/iho/README.adoc +119 -0
- data/lib/relaton/index/CLAUDE.md +207 -0
- data/lib/relaton/index/README.adoc +221 -0
- data/lib/relaton/index/dataset_lock.rb +122 -0
- data/lib/relaton/index/family.rb +63 -0
- data/lib/relaton/index/type.rb +32 -0
- data/lib/relaton/index.rb +3 -0
- data/lib/relaton/isbn/CLAUDE.md +47 -0
- data/lib/relaton/isbn/README.adoc +91 -0
- data/lib/relaton/iso/CLAUDE.md +38 -0
- data/lib/relaton/iso/README.adoc +370 -0
- data/lib/relaton/iso/data_fetcher.rb +16 -1
- data/lib/relaton/itu/CLAUDE.md +685 -0
- data/lib/relaton/itu/README.adoc +188 -0
- data/lib/relaton/jcgm/CLAUDE.md +107 -0
- data/lib/relaton/jis/CLAUDE.md +64 -0
- data/lib/relaton/jis/README.adoc +167 -0
- data/lib/relaton/logger/CLAUDE.md +37 -0
- data/lib/relaton/logger/README.adoc +120 -0
- data/lib/relaton/nist/CLAUDE.md +180 -0
- data/lib/relaton/nist/README.adoc +389 -0
- data/lib/relaton/nist/bibliography.rb +17 -1
- data/lib/relaton/nist/data_fetcher.rb +60 -4
- data/lib/relaton/nist/hit_collection.rb +121 -59
- data/lib/relaton/nist/scraper.rb +21 -7
- data/lib/relaton/oasis/CLAUDE.md +251 -0
- data/lib/relaton/oasis/README.adoc +132 -0
- data/lib/relaton/ogc/CLAUDE.md +242 -0
- data/lib/relaton/ogc/README.adoc +134 -0
- data/lib/relaton/oiml/CLAUDE.md +47 -0
- data/lib/relaton/oiml/README.adoc +70 -0
- data/lib/relaton/omg/CLAUDE.md +137 -0
- data/lib/relaton/omg/README.adoc +101 -0
- data/lib/relaton/plateau/CLAUDE.md +126 -0
- data/lib/relaton/plateau/README.adoc +139 -0
- data/lib/relaton/sdo/CLAUDE.md +81 -0
- data/lib/relaton/un/CLAUDE.md +91 -0
- data/lib/relaton/un/README.adoc +125 -0
- data/lib/relaton/version.rb +1 -1
- data/lib/relaton/w3c/CLAUDE.md +369 -0
- data/lib/relaton/w3c/README.adoc +140 -0
- data/lib/relaton/w3c/data_fetcher.rb +1 -1
- data/lib/relaton/xsf/CLAUDE.md +189 -0
- data/lib/relaton/xsf/README.adoc +127 -0
- data/lib/relaton.rb +1 -0
- metadata +83 -2
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Run the umbrella (Db) suite — specs live in spec/relaton/ and run from there
|
|
9
|
+
bundle exec rake spec:relaton
|
|
10
|
+
|
|
11
|
+
# Run a single spec file (specs are CWD-relative, so cd into the suite dir)
|
|
12
|
+
cd spec/relaton && bundle exec rspec db_spec.rb
|
|
13
|
+
|
|
14
|
+
# Run a specific test by line number
|
|
15
|
+
cd spec/relaton && bundle exec rspec db_spec.rb:234
|
|
16
|
+
|
|
17
|
+
# Lint
|
|
18
|
+
bundle exec rubocop
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Architecture
|
|
22
|
+
|
|
23
|
+
Relaton is a Ruby gem that fetches, caches, and manages bibliographic references to technical standards from 25+ organizations (ISO, IEC, IETF, NIST, IEEE, etc.).
|
|
24
|
+
|
|
25
|
+
### Plugin Registry Pattern
|
|
26
|
+
|
|
27
|
+
**Relaton::Registry** (singleton) auto-discovers and manages backend processor gems (relaton-iso, relaton-iec, relaton-ietf, etc.). Each processor implements the **Relaton::Processor** interface (`get`, `from_xml`, `from_yaml`, `grammar_hash`, `prefix`, `defaultprefix`). The registry routes reference codes to the correct processor by matching prefixes (e.g., "ISO 19115" → `:relaton_iso`).
|
|
28
|
+
|
|
29
|
+
Registration is **lazy**: `register_gems` requires only each flavor's lightweight `…/processor` file, never the heavy flavor top-level, so flavor deps load on first use rather than at startup. Any processor method touching a flavor constant must `require_relative "../<flavor>"` first — see the root `CLAUDE.md` "Registry is lazy" note and `spec/relaton/lazy_loading_spec.rb`.
|
|
30
|
+
|
|
31
|
+
### Global prefix register (relaton-db#103)
|
|
32
|
+
|
|
33
|
+
Separate from the reference-dispatch matching above, each processor declares the
|
|
34
|
+
**global document-ID prefixes its SDO owns** via `@prefixes` (Array<String>) in
|
|
35
|
+
`initialize` — e.g. NIST → `%w[NIST NBS]`, ISO → `["ISO", "ISO/IEC", "IEC/ISO",
|
|
36
|
+
"ISO/IEC/IEEE"]`. `Core::Processor#prefixes` defaults to `[prefix]`, so the ~24
|
|
37
|
+
single-prefix flavors need no declaration; only multi-/conflicting-prefix
|
|
38
|
+
flavors (iso, iec, nist, ieee, bsi) set it. **Joint prefixes are listed
|
|
39
|
+
symmetrically** by every co-publisher (both ISO and IEC list `ISO/IEC`; all three
|
|
40
|
+
of iso/iec/ieee list `ISO/IEC/IEEE`) so the register returns every owning flavor.
|
|
41
|
+
This is a distinct exact-match list, *not* the `@defaultprefix` regex — those
|
|
42
|
+
require a trailing `\s` and can't match a bare prefix like `"ISO/IEC"`.
|
|
43
|
+
|
|
44
|
+
Two lookups over it:
|
|
45
|
+
- `Registry#processors_by_prefix(prefix)` → the owning **processor objects**,
|
|
46
|
+
case-insensitive exact match, in registration (`SUPPORTED_GEMS`) order. **Lazy**
|
|
47
|
+
— touches no flavor constant. Use this if you must stay lazy.
|
|
48
|
+
- `Relaton.prefix_flavor(prefix)` (in `lib/relaton.rb`) → the owning **flavor
|
|
49
|
+
module(s)** as an Array (`[]` if none). Dereferencing a module constant
|
|
50
|
+
triggers that flavor's autoload, so this **loads** the matched flavor(s) — by
|
|
51
|
+
design, since the caller asked for the module.
|
|
52
|
+
|
|
53
|
+
Prefixes are **sourced from pubid** — the source of truth for each SDO's
|
|
54
|
+
identifier grammar (per @ronaldtse's review on relaton-db#103). A processor sets
|
|
55
|
+
`@pubid_flavor` to its Pubid module name (e.g. `:Iso`) in `initialize`, and
|
|
56
|
+
`Core::Processor#prefixes` lazily reads `Pubid::<Flavor>.prefixes`
|
|
57
|
+
(`require "pubid"` on first call, memoized). pubid returns each SDO's leading
|
|
58
|
+
identifier tokens, including non-obvious ones (BSI `DD`, NIST `FIPS`) and joint
|
|
59
|
+
forms listed symmetrically across co-publishers (`ISO/IEC` in both ISO's and
|
|
60
|
+
IEC's lists). Flavors with no `@pubid_flavor` fall back to `[prefix]`. The pubid
|
|
61
|
+
API (`Pubid.prefixes(flavor)` / `Pubid.prefix_flavors` / the `PrefixesSupport`
|
|
62
|
+
mixin) was specified in `HANDOFFS/metanorma__pubid.md` and implemented upstream;
|
|
63
|
+
relaton pins the pubid version that carries it.
|
|
64
|
+
|
|
65
|
+
Conflicting prefixes where the *same* base identifier is co-published as distinct
|
|
66
|
+
per-publisher documents (e.g. `IEC 80000-13` vs `ISO 80000-3` — different cover,
|
|
67
|
+
location, even foreword) are a **separate follow-up**: the register already
|
|
68
|
+
returns both flavors, but returning the correct publisher-specific *content*
|
|
69
|
+
touches `Db#fetch`, not the prefix list.
|
|
70
|
+
|
|
71
|
+
Scope note: only *prefixes* live here. Broader "SDO/organization metadata" (org
|
|
72
|
+
names — relaton-db#132; logos — metanorma#346) was deliberately kept **out** of
|
|
73
|
+
relaton proper, destined for a separate store/gem; don't add it to the processors.
|
|
74
|
+
|
|
75
|
+
### Db (lib/relaton/db.rb) — Main Public API
|
|
76
|
+
|
|
77
|
+
`Relaton::Db#fetch(ref, year, opts)` is the primary entry point. It:
|
|
78
|
+
1. Routes the reference with `Registry#route` (see **Routing** below)
|
|
79
|
+
2. Handles combined references (`+` for derivedFrom, `,` for amendments) in `combine_doc`
|
|
80
|
+
3. Delegates to `check_bibliocache` which manages the dual-cache lookup and network fetch flow
|
|
81
|
+
|
|
82
|
+
### Routing (`Registry#route`, relaton#205)
|
|
83
|
+
|
|
84
|
+
One rule for `fetch`, `fetch_db`, `fetch_async`, `fetch_std` and `docid_type`.
|
|
85
|
+
It returns `[stdclass, pubid]`:
|
|
86
|
+
|
|
87
|
+
1. **Parse with pubid, and keep only the parsing flavor's own exact parse.**
|
|
88
|
+
`Pubid.parse` falls back to a partial parse by any flavor (`ATN5014` → `IEC
|
|
89
|
+
ATN5014`), and a permissive grammar reads another publisher's string exactly
|
|
90
|
+
(`ISO REF` as IEC, `ABC 123456` as GB, a bare DOI as UN). A parse counts only
|
|
91
|
+
when it renders the reference back (`to_s == reference`, pubid's own
|
|
92
|
+
round-trip test) **and** the flavor claims the reference by one of its
|
|
93
|
+
`Pubid::<Flavor>.prefixes` (`prefix_of?`: the reference ends there or goes on
|
|
94
|
+
with a separator; a prefix that ends in punctuation, `doi:`, needs none).
|
|
95
|
+
A flavor whose identifiers print without a publisher token opts out of the
|
|
96
|
+
claim with `Core::Processor#bare_identifiers?`: OGC (`19-025r1`, the form
|
|
97
|
+
its documents carry) and 3GPP (`TS 23.207:REL-18/18.0.0`, its index form).
|
|
98
|
+
Without the opt-in both raised, and `TR 00.01U:UMTS/3.0.0` went to JIS by
|
|
99
|
+
the `TR` regex. A URN no flavor owns (`urn:foo:bar`) makes `Pubid.parse`
|
|
100
|
+
raise a plain `ArgumentError`, which counts as no parse.
|
|
101
|
+
2. **Route by class ancestry** (`processor_by_pubid`), so the registration order
|
|
102
|
+
does not matter (a spec routes with the processors reversed).
|
|
103
|
+
3. **A co-published identifier: the printed form decides.** There is no
|
|
104
|
+
canonical form of it (pubid#469), and `Pubid.parse` tries the owners of a
|
|
105
|
+
joint prefix alphabetically, so `ISO/IEC 27001` parses as IEC. A reference
|
|
106
|
+
that starts with a prefix several flavors own (`joint_prefixes`: `ISO/IEC`,
|
|
107
|
+
`IEC/ISO`, `ISO/IEC/IEEE`) routes to the owner whose own prefix is the first `/` token:
|
|
108
|
+
`ISO/IEC …` → ISO, `IEC/ISO …` → IEC. The parsed pubid is kept only when it
|
|
109
|
+
is that flavor's. This is **relaton's routing policy, not a pubid gap**: the
|
|
110
|
+
same string `ISO/IEC 27001:2022` is in both the ISO and the IEC catalogs, and
|
|
111
|
+
pubid must not elect one reading of it nor route records (pubid#469). The
|
|
112
|
+
#205 comment puts the choice here — the printed form first, then the
|
|
113
|
+
co-publisher's flavor when the first one has no record (**Co-publisher
|
|
114
|
+
fall-through**, below).
|
|
115
|
+
4. **Else the prefix regex** (`class_by_ref`): combined references, the
|
|
116
|
+
`PREFIX(code)` wrapper, `IEV`, URNs, and spellings a flavor normalizes on
|
|
117
|
+
render (`OGC 19-025r1`, `UN TRADE/…`, `3GPP TS …`, `IEEE 802.11-2016`).
|
|
118
|
+
5. **Else `Relaton::UnknownReferenceError`** (`< Relaton::Error`).
|
|
119
|
+
`fetch_async` logs it and yields nil; `docid_type` returns `[nil, code]`;
|
|
120
|
+
relaton-cli logs it and returns no document.
|
|
121
|
+
|
|
122
|
+
**One parse per fetch (#205 PR 2).** `Db#query_and_key` turns the routed pubid
|
|
123
|
+
into the **query pubid** once (`Core::Processor#query_pubid(ref, opts,
|
|
124
|
+
parsed)`: the routed parse, else `#cache_pubid`, kept only when it is a
|
|
125
|
+
`Pubid::Identifier`), keys the cache with it (`#cache_key(ref, year, opts,
|
|
126
|
+
query)`, which folds year/all_parts on **copies**), and hands that same object
|
|
127
|
+
to the flavor: `processor.get(query || code, year, opts)`. So
|
|
128
|
+
`db.fetch("ISO 19115-1")` is one grammar parse, and `db_spec.rb` counts it.
|
|
129
|
+
The year and `all_parts` still reach `get` as arguments; the pubid is unfolded.
|
|
130
|
+
A nil query pubid (no pubid class, a policy miss such as `IEV`, a CCSDS
|
|
131
|
+
format) sends the String and leaves the query uncached. `fetch_api` keeps the
|
|
132
|
+
String for its URL. If `urn_to_code` rewrote the reference, the routed parse is
|
|
133
|
+
of another string, and `query_pubid` parses the code. IEC's `urn_to_code` acts
|
|
134
|
+
on a `urn:` only: `Relaton::Iec.urn_to_code` splits on `:`, and it used to
|
|
135
|
+
rewrite `IEC 60034-1:1969+AMD1:1977+AMD2:1979+AMD3:1980 CSV` into
|
|
136
|
+
`1979+AMD3 1980 CSV`.
|
|
137
|
+
|
|
138
|
+
**Co-publisher fall-through (#205 PR 3).** A not-found from the lead flavor is
|
|
139
|
+
not the last word for a co-published document. The organizations serve the
|
|
140
|
+
joint portfolio by their own policies (IEC serves ISO-led documents, ISO serves
|
|
141
|
+
IEC-portfolio ones; the IEC index fixture holds ~7,450 `ISO/IEC` rows), so
|
|
142
|
+
`Db#fetch` (and `fetch_db`, `fetch_async`, and a `fetch_std` that routes) then
|
|
143
|
+
asks the co-publishers' flavors (`Db#copublisher_fetch`):
|
|
144
|
+
|
|
145
|
+
- **Order: the parsed pubid's, lead first.** `Core::Processor#copublishers(pubid)`
|
|
146
|
+
reads the publishers after the lead from the query pubid — `copublishers` for
|
|
147
|
+
ISO and IEC, IEEE's `publishers`/`copublisher` through its override. pubid#472
|
|
148
|
+
made the order the printed one. `Registry#class_by_publisher` maps each to the
|
|
149
|
+
flavor whose own prefix it is (`ASTM`, `IDF` have none, and are skipped).
|
|
150
|
+
- **Each co-publisher uses its own flavor end to end**: its own parse of the
|
|
151
|
+
String (`ISO/IEC 27001` reaches IEC as a `Pubid::Iec` pubid), its own cache
|
|
152
|
+
key and bucket, its own `not_found` row. A second fetch reads ISO's
|
|
153
|
+
`not_found` and IEC's cached document with no network call. No row crosses
|
|
154
|
+
flavors, because `bib_retval` must read XML with the flavor that wrote it.
|
|
155
|
+
- **A co-publisher whose flavor gives no pubid for the lead's form is
|
|
156
|
+
skipped**, with a log line: a parse error (`IEEE/ISO 11073-10101` in ISO or
|
|
157
|
+
IEC), or a flavor that reads the form as a miss (IEEE's `cache_pubid` gives
|
|
158
|
+
nil for `ISO/IEC/IEEE 15288:2023/DAmd 1`). Without that rule IEEE would get
|
|
159
|
+
the String with no cache key, so every fetch would call it again. There is
|
|
160
|
+
no re-render into the co-publisher's arrangement: pubid#469 elects no
|
|
161
|
+
reading, and #472's "ISO face" keeps the printed publisher order. **Only the
|
|
162
|
+
probe parse is rescued** (`Db#copublisher_query`): a parse error from inside
|
|
163
|
+
the co-publisher's `get` is a flavor or data bug, and it propagates.
|
|
164
|
+
- The co-publishers get the reference **as the lead read it**
|
|
165
|
+
(`strip_id_wrapper`: no `ISO(…)` wrapper, no en dash). Without that, a
|
|
166
|
+
wrapped or en-dash reference raised after the lead's miss.
|
|
167
|
+
- **Not a fall-through:** a lead hit; a `Relaton::RequestError` (a transport
|
|
168
|
+
failure is not "not found", and it propagates); a flavor the caller names in
|
|
169
|
+
`fetch_std`; each part of a combined reference (`combine_doc` calls
|
|
170
|
+
`check_bibliocache` directly); a URN, which reaches the co-publisher as the
|
|
171
|
+
URN String that its flavor cannot parse, so it is skipped.
|
|
172
|
+
- **Known cost of the per-flavor rows:** a repeat fetch logs the lead's
|
|
173
|
+
"not found in cache" before IEC's cached document answers, and when the lead
|
|
174
|
+
later gains the record, the co-publisher's copy answers until the lead's
|
|
175
|
+
`not_found` row expires (60 days) or the caller passes `no_cache`.
|
|
176
|
+
- On the pinned pubid (`27454393`, pre-#472) an IEEE joint development lists
|
|
177
|
+
its `publishers` in a reordered form (`IEEE/ISO/IEC 8802-3` → ISO, IEC, IEEE).
|
|
178
|
+
It does not change an answer today: ISO and IEC cannot parse an IEEE-led form,
|
|
179
|
+
so both are skipped.
|
|
180
|
+
|
|
181
|
+
**The flavor contract for `get`** (every flavor in this gem keeps it):
|
|
182
|
+
|
|
183
|
+
- **Accept a String or the flavor's pubid.** Parse only a String; a String-only
|
|
184
|
+
rewrite (`IEV`, `upcase`, an en dash, a `BIPM ` prefix) stays in the String
|
|
185
|
+
branch. Unwrap an `AllParts` pubid the way the String `(all parts)` is read.
|
|
186
|
+
- **Never mutate the pubid.** It is also the cache key, and `to_all_parts`
|
|
187
|
+
wraps the same object. Copy before a setter (`exclude`, or
|
|
188
|
+
`pubid.class.from_hash(pubid.to_hash)`); ISO `root.date=`, IEC `date=` and
|
|
189
|
+
JIS `year=` used to write into the caller's object.
|
|
190
|
+
- **Use `ref.to_s` for text** — log keys (`Util.info …, key: ref.to_s`; the
|
|
191
|
+
JSON log formatter would serialize a pubid), portal search text, messages.
|
|
192
|
+
Routing's own parse prints back as the reference. A pubid from
|
|
193
|
+
`#cache_pubid` (prefix-fallback routing, a named `fetch_std`, a
|
|
194
|
+
`combine_doc` piece, a `urn_to_code` rewrite) can print a **normalized**
|
|
195
|
+
form: `NISTIR 8200` → `NIST IR 8200`, `NIST SP 800-38A Add` → `… Add.`,
|
|
196
|
+
`CIPM Meeting 43` → `CIPM 43rd Meeting`, `IEEE 528` → `IEEE Std 528`,
|
|
197
|
+
`… Expert commentary` → `… Expert Commentary`, and OGC and 3GPP print with no
|
|
198
|
+
publisher token. So a text lookup must accept the normalized print: BSI's
|
|
199
|
+
`ExComm` rewrite is case-insensitive for this reason, and 3GPP logs with
|
|
200
|
+
`to_s(with_publisher: true)`.
|
|
201
|
+
- `spec/relaton/support/umbrella.rb` has the `pubid_of("ISO 19115-1")` matcher
|
|
202
|
+
for a stubbed `get`'s first argument.
|
|
203
|
+
|
|
204
|
+
`Relaton::Db#fetch_all(text, edition, year)` searches cached entries, filtering by text content (via `match_xml_text?`), edition, and/or year. Returns an array of deserialized bibliographic items from both local and global caches.
|
|
205
|
+
|
|
206
|
+
The dual-cache strategy uses a **global cache** (`~/.relaton/cache`) and an optional **local cache** (project-level). `check_bibliocache` checks local first, falls back to global, and syncs between them.
|
|
207
|
+
|
|
208
|
+
### Cache (lib/relaton/db/cache.rb) — pubid-keyed index on lutaml-store
|
|
209
|
+
|
|
210
|
+
relaton#189 item 2 / relaton#204. Two lutaml-store `FileSystem` stores under
|
|
211
|
+
`<dir>/v2/`:
|
|
212
|
+
|
|
213
|
+
- **`rows/`** — the index. One store key per **bucket**, `<flavor>/<root number>`
|
|
214
|
+
(`iso/19115`), whose value is the list of the bucket's `CacheEntry` rows
|
|
215
|
+
(`lib/relaton/db/cache_entry.rb`): `id` (the pubid's `to_hash`) or `key` (a
|
|
216
|
+
legacy string), `status` (`doc` / `not_found`), `file`, `fetched`. A lookup
|
|
217
|
+
reads one small bucket; a write is one atomic `update` of it. A `_versions`
|
|
218
|
+
key holds each flavor's `grammar_hash`; a changed one drops that flavor's
|
|
219
|
+
rows and documents on open.
|
|
220
|
+
- **`docs/`** — the XML documents. **Several rows can point to one document.**
|
|
221
|
+
A fetch whose answer has another identifier than the query
|
|
222
|
+
(`ISO 19115-1` → `ISO 19115-1:2014`) writes the query row and the item row,
|
|
223
|
+
both naming the same file. There are no redirect entries. A document is
|
|
224
|
+
deleted with the last row that points to it; a row whose document is gone is
|
|
225
|
+
a miss.
|
|
226
|
+
|
|
227
|
+
Matching: the **exact** row first (canonical `to_hash`, not pubid `==`, which
|
|
228
|
+
is false after a `from_hash` round trip for some flavors, e.g. UN). Only a
|
|
229
|
+
**dated** query then falls back to `query === row_id` in the same bucket —
|
|
230
|
+
an undated query keeps its own row, so the 60-day expiry of undated entries
|
|
231
|
+
still applies and it never answers with another query's year-stripped copy.
|
|
232
|
+
**`===` alone is too wide here**: it reads an omitted `part` as "any part", so
|
|
233
|
+
`ISO 19115:2003 === ISO 19115-1:2003` (and the same for IEC, BSI, JIS, GB).
|
|
234
|
+
`Cache#subset_of?` therefore also requires that every component the row adds
|
|
235
|
+
(at any depth) is allowed: only a language for the dated fallback
|
|
236
|
+
(`LANGUAGE_COMPONENTS`), a year/date/month or a language for the date-range
|
|
237
|
+
`candidates` (`EDITION_COMPONENTS`). A row with no `root.number` (DOI, ISBN)
|
|
238
|
+
goes to one of 256 digest buckets (`<flavor>/~<hex>`).
|
|
239
|
+
|
|
240
|
+
Expiry (`Cache#valid_row?`): an undated row, and **every** `not_found` row,
|
|
241
|
+
is valid for 60 days (`UNDATED_TTL`) from its `fetched` date; only a `doc`
|
|
242
|
+
row looked up with a `year` argument never expires (`fetch("ISO 9999:2030")`
|
|
243
|
+
passes no `year`, so its rows expire too). A `not_found` for a dated query
|
|
244
|
+
used to stay for ever (`year || age < 60`), so a document published after
|
|
245
|
+
the first miss was never fetched (#204).
|
|
246
|
+
|
|
247
|
+
Locking and atomic writes come from lutaml-store (≥ 0.3.2 — 0.3.0's
|
|
248
|
+
FileSystem adapter was unsafe, lutaml/lutaml-store#17): an exclusive `flock`
|
|
249
|
+
on `<root>/.lock` plus a per-root Monitor for `update`/`transaction`, and temp
|
|
250
|
+
file + rename per write. `Cache#store` writes the document and both rows in
|
|
251
|
+
one `rows` transaction; the lock order is always `rows`, then `docs`.
|
|
252
|
+
Two specs check this with the real lock, in two processes (#204):
|
|
253
|
+
`db_cache_spec.rb` ("keeps every row when two processes write") and
|
|
254
|
+
`db_spec.rb` ("keeps one consistent cache when two processes fetch", the whole
|
|
255
|
+
`Db#fetch` path). They start the processes with `Process.spawn`
|
|
256
|
+
(`run_children`, `spec/relaton/support/child_processes.rb`), not `fork`, so
|
|
257
|
+
they also run on Windows, where production has two separate commands that
|
|
258
|
+
share `~/.relaton/cache`. A child is a fresh Ruby: RSpec stubs do not reach
|
|
259
|
+
it, so its script replaces what it needs (the `Db` spec redefines ISO's
|
|
260
|
+
`Bibliography.get`). A spawned child loads its code for seconds and works
|
|
261
|
+
for less than one, so each script calls `start_barrier!` after its requires,
|
|
262
|
+
and the children start their work together. Both specs also put every row in
|
|
263
|
+
**one bucket** on purpose: two processes that write the same rows, or
|
|
264
|
+
different buckets, hide a lost update (the `Cache` spec used 50 buckets
|
|
265
|
+
before, and passed with no lock). Measured with the `flock` made a no-op
|
|
266
|
+
(loaded into the children through `RUBYOPT`): the `Cache` spec failed 5 of 5
|
|
267
|
+
runs and the `Db` spec 4 of 5; with a sleep added inside `update` too, both
|
|
268
|
+
failed 3 of 3. With the real lock they passed 20 of 20.
|
|
269
|
+
|
|
270
|
+
An old file-per-key cache (anything in `<dir>` beside `v2/`) is **moved** to
|
|
271
|
+
`<dir>-v1.bak` on open, never deleted — **once**: when the `.bak` exists the
|
|
272
|
+
old entries stay where they are, because an older relaton sharing the
|
|
273
|
+
directory writes the old layout again, and v2 ignores it.
|
|
274
|
+
|
|
275
|
+
Keys: a parsed pubid; a wrapped string (`ISO(ISO 19115-1)`), which the cache
|
|
276
|
+
parses through the prefix's processor — `load_entry`/`save_entry` still take
|
|
277
|
+
it; or a plain string no processor owns (bucket `_key/<string>`).
|
|
278
|
+
|
|
279
|
+
Values: `Db` and `Cache` pass the document XML (`String`) or a
|
|
280
|
+
`Relaton::Db::NotFound` (`Data` with `fetched`, in `cache_entry.rb`), never a
|
|
281
|
+
marker string. `Db` reads with `Cache#read` and tests `is_a?(NotFound)`; keep
|
|
282
|
+
`not_found` regexes out of `db.rb` (`db_spec.rb` guards it). The string API is
|
|
283
|
+
kept for compatibility: `Cache#[]` and `Db#load_entry` return
|
|
284
|
+
`"not_found <date>"` (`NotFound#to_s`), and `Cache#[]=`/`#store` still accept
|
|
285
|
+
that string, which `Cache#coerce` turns into a `NotFound` at the entrance.
|
|
286
|
+
|
|
287
|
+
### WorkersPool (lib/relaton/db/workers_pool.rb)
|
|
288
|
+
|
|
289
|
+
Thread pool for `fetch_async`. Default 10 threads per processor, overridable via `RELATON_FETCH_PARALLEL` env var.
|
|
290
|
+
|
|
291
|
+
### Cache key
|
|
292
|
+
|
|
293
|
+
`Db#query_and_key` asks the processor: `Core::Processor#cache_key(ref, year,
|
|
294
|
+
opts, query)` takes the query pubid (above; else it parses the reference **as
|
|
295
|
+
written** with the routed flavor's pubid class, `#cache_pubid` →
|
|
296
|
+
`pubid_class.parse`), then folds `year`
|
|
297
|
+
(`#fold_year`) and `all_parts` (`to_all_parts`) into the pubid.
|
|
298
|
+
`#pubid_class` reads `@pubid_identifier`, else `@pubid_flavor` —
|
|
299
|
+
`@pubid_identifier` exists so a flavor can key its cache with pubid without
|
|
300
|
+
also sourcing `#prefixes` from pubid.
|
|
301
|
+
|
|
302
|
+
- **Canonical references only.** Relaton does not rewrite a non-canonical
|
|
303
|
+
citation before it parses it: `I-D.draft-…`, `IETF RFC 8341`,
|
|
304
|
+
`BIPM Metrologia …`, a W3C URL, `NIST … (IPD)` / `(January 2014)` /
|
|
305
|
+
`NISTIR 8200:2018`, `JIS … (規格群)`, a lowercase `iec …`, an en dash — each
|
|
306
|
+
gives no key and reaches the flavor's `get` as written, which normalizes it
|
|
307
|
+
itself where it can (relaton#235). The canonical identifiers the data
|
|
308
|
+
repos publish all parse as written (measured over every spec index fixture);
|
|
309
|
+
pubid/pubid#463 and #464, which asked pubid to accept the other spellings,
|
|
310
|
+
were closed for this.
|
|
311
|
+
- **References with one key must get one answer from the flavor.** pubid can
|
|
312
|
+
read several spellings as one identifier (`doi:…`, `DOI:…` and a
|
|
313
|
+
`https://doi.org/…` URL are one `Pubid::Doi` key). If the flavor's `get`
|
|
314
|
+
answers only some of them, a miss on another spelling is cached as
|
|
315
|
+
`not_found` under the shared key and poisons the canonical one for 60 days.
|
|
316
|
+
So `Doi::Crossref.get` strips every such prefix. Check this whenever a
|
|
317
|
+
flavor's routing or its pubid grammar changes.
|
|
318
|
+
- **Generated references must be canonical too.** `combine_doc` joins an ITU
|
|
319
|
+
or NIST supplement with a space (`NIST SP 800-38A Add`), since pubid does not
|
|
320
|
+
parse `NIST SP 800-38A/Add`.
|
|
321
|
+
- **A processor overrides `cache_pubid` only for policy**, not to normalize:
|
|
322
|
+
Adobe, IANA, IEEE and ISBN read an unparseable reference as a miss, so their
|
|
323
|
+
parse error means "no key"; IEC answers `IEV` with the vocabulary, so `IEV`
|
|
324
|
+
gets no key.
|
|
325
|
+
- **A flavor parse error is a fallback, not a failure**: the query is not
|
|
326
|
+
keyed, and the flavor's `get` receives the reference as written
|
|
327
|
+
(`Core::Processor#cache_pubid` rescues to nil, with a log line; root
|
|
328
|
+
`CLAUDE.md`, "A query reference the flavor's grammar cannot read falls
|
|
329
|
+
back to the flavor as written").
|
|
330
|
+
- **The year goes where the flavor's `get` puts it.** The default
|
|
331
|
+
`#fold_year` sets the identifier's own year; ISO, CEN and BSI apply a year
|
|
332
|
+
to `#root` (the base document of a supplement, the adopted document of an
|
|
333
|
+
adoption), so their processors use `#fold_year_on_root`. A flavor with no
|
|
334
|
+
year component drops it, which matches their `get` (it ignores the year).
|
|
335
|
+
- **No key, no cache.** A processor with a pubid class that answers `nil` is
|
|
336
|
+
not cached for that query: a flavor miss by the flavor's own rule (Adobe,
|
|
337
|
+
IANA, IEEE, an incorrect ISBN, `IEV`) and a query whose answer the cache
|
|
338
|
+
cannot hold (a CCSDS format, which filters the item's sources — CCSDS
|
|
339
|
+
overrides `query_pubid`, so its `get` also receives the String; an OGC year,
|
|
340
|
+
which is not the pubid's document-number year — OGC keeps that rule in
|
|
341
|
+
`cache_key`, so its `get` still receives the pubid).
|
|
342
|
+
- **The publication date range is never in the key.** It selects among the
|
|
343
|
+
cached editions (`Cache#candidates` + `pub_date_in_range?`); on a miss the
|
|
344
|
+
flavor is asked with the range and the answer is cached under **its own**
|
|
345
|
+
identifier only, so the undated query row keeps pointing to the latest
|
|
346
|
+
edition. Bounds may be `YYYY`, `YYYY-MM` or `YYYY-MM-DD`.
|
|
347
|
+
- A processor with **no** pubid class (a third-party one) keeps the legacy
|
|
348
|
+
string key from `std_id` (`ISO(ISO 19115-1:2014 (all parts))`).
|
|
349
|
+
|
|
350
|
+
## Testing
|
|
351
|
+
|
|
352
|
+
- Umbrella (Db) specs live in `spec/relaton/` and run from there (`rake spec:relaton`)
|
|
353
|
+
- RSpec with VCR cassettes in `spec/relaton/vcr_cassettes/` for recorded HTTP interactions
|
|
354
|
+
- Tests create `testcache`/`testcache2` directories and clean them in `before(:each)`.
|
|
355
|
+
Assert cache contents through `Relaton::Db::Cache` (`#[]`, `#rows`, `#all`),
|
|
356
|
+
not through file names: the store names its files itself.
|
|
357
|
+
- Cache-related tests need `<fetched>` elements in XML for `valid_entry?` to return true
|
|
358
|
+
- Integration tests in `spec/relaton/relaton_spec.rb`; unit tests under `spec/relaton/`
|
|
359
|
+
- **ISO lookups are stubbed, not cassette-recorded.** Flavor gems (relaton-iso/iec/nist)
|
|
360
|
+
fetch a large live `index-v2` and deserialize every id through a pinned pubid build,
|
|
361
|
+
so a single drifted id in the live index makes the whole index unparseable and ISO
|
|
362
|
+
lookups return `nil`. Umbrella specs therefore stub `Relaton::Iso::Bibliography.get`
|
|
363
|
+
(and other flavors' `.get`) to return hand-built `ItemData` — the umbrella's job is to
|
|
364
|
+
test `Db` orchestration (`combine_doc`, caching, api fallback), not relaton-iso's index.
|
|
365
|
+
Build stub items with the `docidentifier:` key (not `docid:`) so the id survives the
|
|
366
|
+
cache XML round-trip. Don't reintroduce a live-index cassette for these.
|
|
367
|
+
|
|
368
|
+
## Style
|
|
369
|
+
|
|
370
|
+
- RuboCop config inherits from [Ribose OSS guides](https://github.com/riboseinc/oss-guides), target Ruby 3.3
|
|
371
|
+
- Each cache operation is atomic in lutaml-store (threads and processes). `Db`'s
|
|
372
|
+
`@semaphore` (Mutex) only serializes its own check-then-act sequences across
|
|
373
|
+
the two caches (validity check, clone, fetch-and-store).
|
data/lib/relaton/db.rb
CHANGED
|
@@ -153,7 +153,25 @@ module Relaton
|
|
|
153
153
|
return check_bibliocache(code, year, opts, std) if std
|
|
154
154
|
|
|
155
155
|
std, pubid = @registry.route(code)
|
|
156
|
-
fetch_routed(code, year, opts, std, pubid)
|
|
156
|
+
fetch_routed(code, year, opts, std, pubid) ||
|
|
157
|
+
fetch_equivalent(code, year, opts)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Cross-SDO equivalence as data: on a total miss, walk the equivalence
|
|
161
|
+
# tables from relaton/equivalence.rb — a national adoption falls back to
|
|
162
|
+
# the EN, the EN to the ISO. Each candidate goes through the full routed
|
|
163
|
+
# fetch (cache, co-publishers); an unrouteable candidate is skipped.
|
|
164
|
+
#
|
|
165
|
+
# @return [Relaton::Bib::ItemData, nil]
|
|
166
|
+
def fetch_equivalent(code, year, opts)
|
|
167
|
+
Equivalence.equivalents_for(code).each do |candidate|
|
|
168
|
+
Util.info "Not found; trying equivalent `#{candidate}`", key: code
|
|
169
|
+
std, pubid = @registry.route(candidate)
|
|
170
|
+
bib = fetch_routed(candidate, year, opts, std, pubid) and return bib
|
|
171
|
+
rescue UnknownReferenceError
|
|
172
|
+
next
|
|
173
|
+
end
|
|
174
|
+
nil
|
|
157
175
|
end
|
|
158
176
|
|
|
159
177
|
# The document identifier class corresponding to the given code
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project
|
|
6
|
+
|
|
7
|
+
relaton-doi is a Ruby gem that fetches bibliographic metadata via DOI identifiers from the Crossref API and converts them into Relaton bibliographic objects. It detects DOI patterns to produce flavor-specific items (NIST, IETF, BIPM, IEEE) or generic `Bib::ItemData`.
|
|
8
|
+
|
|
9
|
+
## Common Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bundle exec rake spec # Run all tests (default rake task)
|
|
13
|
+
bundle exec rspec spec/relaton/doi/parser_spec.rb # Run a single spec file
|
|
14
|
+
bundle exec rspec spec/relaton/doi/parser_spec.rb:224 # Run a single example by line
|
|
15
|
+
rubocop # Lint
|
|
16
|
+
rubocop -a # Lint with auto-correct
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Architecture
|
|
20
|
+
|
|
21
|
+
**Namespace:** `Relaton::Doi` (migrated from legacy `RelatonDoi`).
|
|
22
|
+
|
|
23
|
+
**Core flow:** `Crossref.get(doi)` → HTTP fetch from api.crossref.org → `Parser.parse(json_hash)` → flavor-specific `ItemData`
|
|
24
|
+
|
|
25
|
+
Key classes in `lib/relaton/doi/`:
|
|
26
|
+
|
|
27
|
+
- **`Crossref`** — module with `get(doi)` and `get_by_id(id)`. `get` takes a
|
|
28
|
+
String or a parsed `Pubid::Doi::Identifier`; `doi_of` reduces either to the
|
|
29
|
+
bare `prefix/suffix` the API takes. It strips a `doi:` scheme in any case
|
|
30
|
+
and a `doi.org` URL, because `Relaton::Db` keys all of those as one DOI — a
|
|
31
|
+
form this method did not read would get a miss cached for the others. Uses **Mechanize** (`Mechanize.new` with a custom `USER_AGENT`). `get_by_id` retries twice, backing off by `x-rate-limit-interval * n`, then raises `Relaton::RequestError`; only a 404 returns nil.
|
|
32
|
+
- **`Parser`** — largest file (~827 lines). Converts Crossref JSON hashes to Relaton objects. Factory method `parse(src)` delegates to `create_bibitem` which picks the right ItemData class based on DOI pattern (`/nist/` → `Nist::ItemData`, `/rfc\d+/` → `Ietf::ItemData`, etc.). Contains ~30 `parse_*` helper methods for individual bibliographic fields.
|
|
33
|
+
- **`Processor`** — `Relaton::Processor` subclass for the Relaton registry system. Entry point for `get`, `from_xml`, `hash_to_bib`.
|
|
34
|
+
- **`Util`** — logging utility, extends `Relaton::Bib::Util` with `PROGNAME = "relaton-doi"`.
|
|
35
|
+
|
|
36
|
+
## Test Setup
|
|
37
|
+
|
|
38
|
+
- **RSpec** with `expect` syntax only (monkey patching disabled)
|
|
39
|
+
- **VCR** cassettes in `spec/vcr_cassettes/` record Crossref HTTP responses, re-recorded every 7 days on purpose (see root `CLAUDE.md`). Because Crossref rate-limits, **re-record this suite alone** — under parallel `rake spec` it answers 429 and the empty body gets recorded as if it were data.
|
|
40
|
+
- **XML fixtures** in `spec/fixtures/` — expected output XML files. The `read_fixture` helper auto-substitutes today's date into `<fetched>` tags.
|
|
41
|
+
- **equivalent-xml** gem for XML comparison in integration tests
|
|
42
|
+
- Integration tests in `spec/relaton/doi_spec.rb` cover 40+ document types via VCR cassettes
|
|
43
|
+
- Unit tests in `spec/relaton/doi/parser_spec.rb` test Parser methods directly with hash inputs
|
|
44
|
+
|
|
45
|
+
## Rate limiting
|
|
46
|
+
|
|
47
|
+
Crossref advertises `X-Rate-Limit-Limit: 10` / `X-Rate-Limit-Interval: 1s` and
|
|
48
|
+
`X-Concurrency-Limit: 3`. Both request paths honour that and **fail loud rather
|
|
49
|
+
than degrade**:
|
|
50
|
+
|
|
51
|
+
- `Parser#fetch_crossref` handles `429` *before* its generic 4xx→nil return,
|
|
52
|
+
sleeping `retry_delay * attempt` (`Retry-After`, else `X-Rate-Limit-Interval`,
|
|
53
|
+
else a 1s floor) with the **sleep itself** clamped to `MAX_RETRY_DELAY`, for up
|
|
54
|
+
to `MAX_RETRIES` attempts, then raising `Relaton::RequestError`. Only the
|
|
55
|
+
numeric form of `Retry-After` is used — RFC 9110 also permits an HTTP-date, and
|
|
56
|
+
scanning digits out of a date would yield the day-of-month.
|
|
57
|
+
- `Crossref.get_by_id` retries twice with `Crossref.backoff`, then raises; only a
|
|
58
|
+
404 returns nil.
|
|
59
|
+
|
|
60
|
+
Both apply a **1-second floor**, because a throttled response may carry no
|
|
61
|
+
rate-limit headers at all (the 429s observed here had only
|
|
62
|
+
`Date`/`Content-Length`/`Connection`). Treating a missing header as `0` would
|
|
63
|
+
retry with no delay — hammering the endpoint that just asked us to slow down.
|
|
64
|
+
|
|
65
|
+
**A throttle must never collapse into "not found."** `fetch_crossref`'s callers
|
|
66
|
+
(`#parent_item`, `#fetch_location`) read nil as "this record has no parent /
|
|
67
|
+
no location", so a 429 returning nil silently produced an *incomplete* item —
|
|
68
|
+
e.g. a book chapter with none of its editors — instead of an error. That is also
|
|
69
|
+
how 429s ended up baked into cassettes as if they were data; see the cassette
|
|
70
|
+
notes in the root `CLAUDE.md`.
|
|
71
|
+
|
|
72
|
+
## Key Constants in Parser
|
|
73
|
+
|
|
74
|
+
- `TYPES` — maps 23 Crossref document types to Relaton types (e.g., `"book-chapter"` → `"inbook"`)
|
|
75
|
+
- `REALATION_TYPES` — maps 37 Crossref relation types to Relaton relation types
|
|
76
|
+
- `COUNTRIES` — `%w[USA]`, used by `parse_place` to distinguish country vs region
|
|
77
|
+
|
|
78
|
+
## Crossref markup
|
|
79
|
+
|
|
80
|
+
Crossref returns abstracts and titles as JATS, and it sometimes encodes that markup as HTML
|
|
81
|
+
entities. `Parser#normalize_markup` decodes the entities; `create_title`, `parse_abstract`,
|
|
82
|
+
`parse_series`, `included_in_relation` and `create_org` all route through it, so an
|
|
83
|
+
affiliation name no longer goes out double escaped (`AT&T` instead of `AT&T`).
|
|
84
|
+
|
|
85
|
+
**It decodes only content that holds no markup of its own** (`TAG_RE`), and it passes `nil`
|
|
86
|
+
through. Crossref escapes an ampersand and a literal angle bracket *inside* real markup, so
|
|
87
|
+
an unconditional decode — which is what upstream relaton-doi#26 does — turns
|
|
88
|
+
`<jats:p>Smith & Jones</jats:p>` into markup that no longer parses. The sanitizer then
|
|
89
|
+
gives up on it and an unescapable string reaches the output: metanorma-pdfa#99 again, on the
|
|
90
|
+
far more common input. A `nil` reaches `create_org` from a funder entry carrying only a DOI
|
|
91
|
+
and from an affiliation carrying only a ROR id, and `CGI.unescapeHTML(nil)` raises
|
|
92
|
+
`TypeError`, which aborted the whole fetch.
|
|
93
|
+
|
|
94
|
+
The JATS also carries a `jats:` prefix on the elements and sometimes an `xlink:` prefix on an
|
|
95
|
+
attribute, which Relaton never declares. **Removing those prefixes is not this flavor's job**
|
|
96
|
+
— `Relaton::Bib::Sanitizer` runs on every `Bib::Title` and `Bib::Abstract` assignment and
|
|
97
|
+
declares each undeclared prefix so the markup parses and maps to the basicdoc set. Upstream
|
|
98
|
+
relaton-doi (relaton-doi#26) carries its own `drop_namespaces` only because that sanitizer fix
|
|
99
|
+
is not in a released relaton-bib; here both ship in one gem, so this flavor keeps the entity
|
|
100
|
+
decode alone. Do not add a second prefix-stripping layer. The unit specs in
|
|
101
|
+
`spec/doi/relaton/doi/parser_spec.rb` pin the behaviour, not the layer, so they hold either way.
|
|
102
|
+
|
|
103
|
+
`spec/doi/relaton/doi_spec.rb`'s shared `"fetch document"` example asserts
|
|
104
|
+
`Nokogiri::XML(xml).errors` is empty. That is the property relaton-render requires
|
|
105
|
+
(metanorma-pdfa#99), and it guards every integration example.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
= Relaton::Doi retrieves bibliographic items using DOI
|
|
2
|
+
|
|
3
|
+
Relaton::Doi is a Ruby gem that implements the
|
|
4
|
+
https://github.com/metanorma/metanorma-model-iso#iso-bibliographic-item[IsoBibliographicItem model].
|
|
5
|
+
|
|
6
|
+
You can use it to retrieve metadata of Standards from https://crossref.org, and
|
|
7
|
+
access such metadata through the `BibliographicItem` object.
|
|
8
|
+
|
|
9
|
+
To collect metadata this gem makes a few requests to the CrossRef API. It may take a few seconds to get the metadata.
|
|
10
|
+
|
|
11
|
+
== Usage
|
|
12
|
+
|
|
13
|
+
=== Retrieving items of known type using DOI
|
|
14
|
+
|
|
15
|
+
If the resulting bibliographic item returned from CrossRef is a known Relaton
|
|
16
|
+
flavor, such as a NIST or IEEE standard, the corresponding Relaton class object
|
|
17
|
+
will be returned via the call.
|
|
18
|
+
|
|
19
|
+
[source,ruby]
|
|
20
|
+
----
|
|
21
|
+
require 'relaton/doi'
|
|
22
|
+
=> true
|
|
23
|
+
|
|
24
|
+
# get NIST standard
|
|
25
|
+
Relaton::Doi::Crossref.get "doi:10.6028/nist.ir.8245"
|
|
26
|
+
[relaton-doi] INFO: (doi:10.6028/nist.ir.8245) Fetching from search.crossref.org ...
|
|
27
|
+
[relaton-doi] INFO: (doi:10.6028/nist.ir.8245) Found: `10.6028/nist.ir.8245`
|
|
28
|
+
=> #<Relaton::Nist::ItemData:0x000000011040a338
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
# get RFC standard
|
|
32
|
+
Relaton::Doi::Crossref.get "doi:10.17487/RFC0001"
|
|
33
|
+
[relaton-doi] INFO: (doi:10.17487/RFC0001) Fetching from search.crossref.org ...
|
|
34
|
+
[relaton-doi] INFO: (doi:10.17487/RFC0001) Found: `10.17487/rfc0001`
|
|
35
|
+
=> #<Relaton::Ietf::ItemData:0x0000000110be8410
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
# get BIPM standard
|
|
39
|
+
Relaton::Doi::Crossref.get "doi:10.1088/0026-1394/29/6/001"
|
|
40
|
+
[relaton-doi] INFO: (doi:10.1088/0026-1394/29/6/001) Fetching from search.crossref.org ...
|
|
41
|
+
[relaton-doi] INFO: (doi:10.1088/0026-1394/29/6/001) Found: `10.1088/0026-1394/29/6/001`
|
|
42
|
+
=> #<Relaton::Bipm::ItemData:0x00000001118dcbf8
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
# get IEEE standard
|
|
46
|
+
Relaton::Doi::Crossref.get "doi:10.1109/ieeestd.2014.6835311"
|
|
47
|
+
[relaton-doi] INFO: (doi:10.1109/ieeestd.2014.6835311) Fetching from search.crossref.org ...
|
|
48
|
+
[relaton-doi] INFO: (doi:10.1109/ieeestd.2014.6835311) Found: `10.1109/ieeestd.2014.6835311`
|
|
49
|
+
=> #<Relaton::Ieee::ItemData:0x0000000110ec5c00
|
|
50
|
+
...
|
|
51
|
+
----
|
|
52
|
+
|
|
53
|
+
=== Retrieving items of unknown types via DOI
|
|
54
|
+
|
|
55
|
+
If the resulting bibliographic item returned from CrossRef is of an unknown type
|
|
56
|
+
to Relaton, an instance of RelatonBib::BibliographicItem will be returned.
|
|
57
|
+
|
|
58
|
+
[source,ruby]
|
|
59
|
+
----
|
|
60
|
+
Relaton::Doi::Crossref.get "doi:10.1109/ACCESS.2017.2739804"
|
|
61
|
+
[relaton-doi] INFO: (doi:10.1109/ACCESS.2017.2739804) Fetching from search.crossref.org ...
|
|
62
|
+
[relaton-doi] INFO: (doi:10.1109/ACCESS.2017.2739804) Found: `10.1109/access.2017.2739804`
|
|
63
|
+
=> #<Relaton::Bib::ItemData:0x0000000110f0bde0
|
|
64
|
+
...
|
|
65
|
+
----
|
|
66
|
+
|
|
67
|
+
=== Logging
|
|
68
|
+
|
|
69
|
+
Relaton::Doi uses the relaton-logger gem for logging. By default, it logs to STDOUT. To change the log levels and add other loggers, read the https://github.com/relaton/relaton-logger#usage[relaton-logger] documentation.
|
|
70
|
+
|
|
71
|
+
== Development
|
|
72
|
+
|
|
73
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
74
|
+
`rake spec` to run the tests. You can also run `bin/console` for an interactive
|
|
75
|
+
prompt that will allow you to experiment.
|
|
76
|
+
|
|
77
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
78
|
+
release a new version, update the version number in `version.rb`, and then run
|
|
79
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
|
80
|
+
git commits and the created tag, and push the `.gem` file to
|
|
81
|
+
https://rubygems.org[rubygems.org].
|
|
82
|
+
|
|
83
|
+
== Contributing
|
|
84
|
+
|
|
85
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/relaton/relaton-doi.
|
|
86
|
+
|
|
87
|
+
== License
|
|
88
|
+
|
|
89
|
+
The gem is available as open source under the terms of the https://opensource.org/licenses/MIT[MIT License].
|