relaton 3.0.0.pre.alpha.17 → 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,221 @@
|
|
|
1
|
+
= Relaton::Index
|
|
2
|
+
|
|
3
|
+
This gem is a part of [Relaton](https://github.com/relaton) project. It provides a way to index Relaton documents' files and search them by references.
|
|
4
|
+
|
|
5
|
+
For the data-repository layout, the index file specification (schema, `:id` v1/v2 shapes), and the GitHub Pages publishing contract, see link:../../../docs/data-repository-format.adoc[docs/data-repository-format.adoc] at the repo root.
|
|
6
|
+
|
|
7
|
+
== Usage
|
|
8
|
+
|
|
9
|
+
=== Creating an index object
|
|
10
|
+
|
|
11
|
+
The gem provides the `Relaton::Index.find_or_create {type}, url: {url}, file: {filename}, pubid_class: {class}` method to create an index object. The first argument is the type of dataset (ISO, IEC, IHO, etc.). `url` is the URL to the zipped remote index file, `filename` is the local index file, and `pubid_class` is a class that implements `Pubid::Core::Identifier` for deserializing ID hashes into structured identifier objects. All three keywords are optional.
|
|
12
|
+
|
|
13
|
+
If the URL is specified and the local file in a `/{home}/.relaton/{type}` dir doesn't exist or is outdated, the index file will be downloaded from the URL saved as a local file and an index object will be created from the file. If the file in the `/{home}/.relaton/{type}` exists and is actual, the index object will be created from the local file.
|
|
14
|
+
|
|
15
|
+
If the URL isn't specified, the index object will be created from the local file in the current dir `./{filename}`. If the file doesn't exist, the empty index object will be created.
|
|
16
|
+
|
|
17
|
+
If the filename isn't specified, a default `index,yaml` filename will be used.
|
|
18
|
+
|
|
19
|
+
If the keys are specified, the local index file will be checked for the presence of the keys. If the keys are not found, the index file will be downloaded from the URL or an empty index object will be created if the URL isn't specified.
|
|
20
|
+
|
|
21
|
+
It's possible to create many index objects for different types of datasets. The index objects are saved in the pool. The pool is a hash with the type of dataset as a key and the index object as a value. The pool is a class variable of the `Relaton::Index` class. The pool is used to find an existing index object by the type of dataset. The `Relaton::Index.find_or_create` method returns an existing index object if it was created before.
|
|
22
|
+
|
|
23
|
+
If the URL or filename is specified and has a new value, different from what was used to create the index object before, the index object will be recreated.
|
|
24
|
+
|
|
25
|
+
[source,ruby]
|
|
26
|
+
----
|
|
27
|
+
require 'relaton/index'
|
|
28
|
+
|
|
29
|
+
# Create a new index object. The first argument is the type of dataset (ISO, IEC, IHO, etc.) URL and filename are optional.
|
|
30
|
+
Relaton::Index.find_or_create :IHO, url: "https://raw.githubusercontent.com/relaton/relaton-data-iho/master/index.zip", file: "index-iho.yaml"
|
|
31
|
+
|
|
32
|
+
# Find an existing index object (created before).
|
|
33
|
+
Relaton::Index.find_or_create :IHO
|
|
34
|
+
|
|
35
|
+
# If the URL or filename is specified and has a new value (different from what it was before), the index object will be recreated.
|
|
36
|
+
Relaton::Index.find_or_create :IHO, url: nil, file: "index.yaml"
|
|
37
|
+
|
|
38
|
+
# Remove the index from the pool.
|
|
39
|
+
Relaton::Index.close :IHO
|
|
40
|
+
----
|
|
41
|
+
|
|
42
|
+
The aim of this gem is to be used by Relaton libraries in two ways:
|
|
43
|
+
- indexing documents' files in GitHub repositories
|
|
44
|
+
- searching a document by a reference when the Relaton library fetches a document
|
|
45
|
+
|
|
46
|
+
=== Indexing
|
|
47
|
+
|
|
48
|
+
In this case, the Relaton library creates an index object and adds documents' files to it. By default, the index object is saved to the `index.yaml` file in the root of the repository. The filename can be changed using the `filename` setting.
|
|
49
|
+
|
|
50
|
+
[source,ruby]
|
|
51
|
+
----
|
|
52
|
+
# Create a new index object or fetch an existing one. The first argument is the type of dataset (ISO, IEC, IHO, etc.) URL should not be specified.
|
|
53
|
+
index = Relaton::Index.find_or_create :IHO
|
|
54
|
+
|
|
55
|
+
# Add a document to the index or update it if it already exists.
|
|
56
|
+
index.add_or_update "B-4 2.19.0", "data/b-4_2_19_0.xml"
|
|
57
|
+
|
|
58
|
+
# Save the index to the `index.yaml` file in the current directory.
|
|
59
|
+
index.save
|
|
60
|
+
----
|
|
61
|
+
|
|
62
|
+
=== Searching
|
|
63
|
+
|
|
64
|
+
In this case, the Relaton library should create an index object and search for a document by reference. The gem looks for the `.relaton/[TYPE]/index.yaml` file in the user's home directory. The `[TYPE]` is one of downcased ISO, IEC, IHO, etc. If the file is not found or is older than 24 hours then it will be downloaded from the URL specified in the `find_or_create` method. The URL can be specified as `true` if the index should not be fetched from the URL.
|
|
65
|
+
|
|
66
|
+
[source,ruby]
|
|
67
|
+
----
|
|
68
|
+
# Create a new index object or fetch an existing one. URL should be specified. If the index file is not found or is older than 24 hours, it will be downloaded from the URL. By default, the index is saved as `index.yaml` file to the `/[HOME]/.relaton/iho/` folder. If the URL is specified as `true`, the index won't be fetched from the URL.
|
|
69
|
+
index = Relaton::Index.find_or_create :IHO, url: "https://raw.githubusercontent.com/relaton/relaton-data-iho/master/index.zip"
|
|
70
|
+
|
|
71
|
+
# Search for a document by reference
|
|
72
|
+
index.search "B-4"
|
|
73
|
+
# => [{ id: "B-4 2.19.0", file: "data/b-4_2_19_0.xml" }]
|
|
74
|
+
|
|
75
|
+
# Search for a document by reference using a block
|
|
76
|
+
index.search do |row|
|
|
77
|
+
# do something with the index row
|
|
78
|
+
row[:id] == "B-4"
|
|
79
|
+
end
|
|
80
|
+
# => [{ id: "B-4 2.19.0", file: "data/b-4_2_19_0.xml" }]
|
|
81
|
+
----
|
|
82
|
+
|
|
83
|
+
A string reference matches a row as a substring, as above. An identifier
|
|
84
|
+
reference takes pubid's asymmetric subset match `reference === row[:id]`: a
|
|
85
|
+
component the reference leaves out matches any value, so `OGC 12-128` answers
|
|
86
|
+
with the `12-128r19` row. Pass a block for exact equality:
|
|
87
|
+
|
|
88
|
+
[source,ruby]
|
|
89
|
+
----
|
|
90
|
+
index.search(pubid) { |row| row[:id] == pubid }
|
|
91
|
+
----
|
|
92
|
+
|
|
93
|
+
=== Using pubid_class for structured identifiers
|
|
94
|
+
|
|
95
|
+
The `pubid_class` option allows index entries to be deserialized into structured identifier objects instead of plain strings or hashes. The class must include `Pubid::Core::Identifier` and provide a `.create(**hash)` factory method.
|
|
96
|
+
|
|
97
|
+
When `pubid_class` is specified, each `:id` hash in the index is converted into a pubid object, enabling structured access to identifier components (e.g., publisher, number, part, year).
|
|
98
|
+
|
|
99
|
+
[source,ruby]
|
|
100
|
+
----
|
|
101
|
+
require 'relaton/index'
|
|
102
|
+
require 'pubid-iso'
|
|
103
|
+
|
|
104
|
+
# Create an index with pubid_class to deserialize IDs into Pubid::Iso::Identifier objects
|
|
105
|
+
index = Relaton::Index.find_or_create :ISO,
|
|
106
|
+
url: "https://raw.githubusercontent.com/relaton/relaton-data-iso/main/index-v2.zip",
|
|
107
|
+
pubid_class: Pubid::Iso::Identifier
|
|
108
|
+
|
|
109
|
+
# Search returns entries with structured pubid objects as IDs
|
|
110
|
+
results = index.search "ISO 1"
|
|
111
|
+
results.first[:id]
|
|
112
|
+
# => #<Pubid::Iso::Identifier: ISO 1>
|
|
113
|
+
results.first[:id].publisher
|
|
114
|
+
# => "ISO"
|
|
115
|
+
----
|
|
116
|
+
|
|
117
|
+
=== Reading the machine index on a Pages site
|
|
118
|
+
|
|
119
|
+
A data repository's GitHub Pages site publishes a sharded copy of its index
|
|
120
|
+
(`index/manifest.json` and `index/shard-NNNNN.json`, see
|
|
121
|
+
`docs/data-repository-format.adoc`). Give its base URL as `pages_url:` in place
|
|
122
|
+
of `url:`, with a `pubid_class:`:
|
|
123
|
+
|
|
124
|
+
[source,ruby]
|
|
125
|
+
----
|
|
126
|
+
index = Relaton::Index.find_or_create :W3C,
|
|
127
|
+
pages_url: "https://relaton.github.io/relaton-data-w3c/",
|
|
128
|
+
pubid_class: Pubid::W3c::Identifier
|
|
129
|
+
index.search(Pubid::W3c::Identifier.parse("W3C REC-xml-names"))
|
|
130
|
+
----
|
|
131
|
+
|
|
132
|
+
A parsed query reads the manifest and the one shard
|
|
133
|
+
`crc32(id.root.number.to_s) % shards`. A String query and a search with only a
|
|
134
|
+
block read the whole index, which the manifest names. Nothing is written to
|
|
135
|
+
disk: the manifest, the shards and the whole index stay in memory for 24 hours.
|
|
136
|
+
|
|
137
|
+
* An absent shard (HTTP 404) is "not found", and the search returns `[]`.
|
|
138
|
+
* A network error, or an HTTP status other than 200 and 404, raises
|
|
139
|
+
`Relaton::RequestError`.
|
|
140
|
+
* A missing or unsupported manifest, or a row that the pubid class cannot read,
|
|
141
|
+
raises `Relaton::Index::Error`.
|
|
142
|
+
|
|
143
|
+
The manifest also carries `sha256` and `bytes` for the whole-index zip it
|
|
144
|
+
names, so a dataset can be pinned and the fetched artifact verified.
|
|
145
|
+
`Relaton::Index::DatasetLock.capture(pages_url)` writes that pin as a lockfile
|
|
146
|
+
and verifies the artifact later; see `docs/data-repository-format.adoc`.
|
|
147
|
+
|
|
148
|
+
There is no fallback to the `url:` zip.
|
|
149
|
+
|
|
150
|
+
=== Family, edition and part views
|
|
151
|
+
|
|
152
|
+
`Type#family(reference)` returns the rows that share the reference's root
|
|
153
|
+
number — the base document, its editions, its parts, and any supplement
|
|
154
|
+
layer — as a `Relaton::Index::Family`:
|
|
155
|
+
|
|
156
|
+
[source,ruby]
|
|
157
|
+
----
|
|
158
|
+
family = index.family("ISO 1")
|
|
159
|
+
family.members # every row in the family
|
|
160
|
+
family.editions # {"2015" => [...], "2020" => [...]}
|
|
161
|
+
family.parts # {"1" => [...]}
|
|
162
|
+
----
|
|
163
|
+
|
|
164
|
+
A String reference is parsed by the index's `pubid_class`; a parsed identifier
|
|
165
|
+
is used as given. Narrowing uses the same number bucket as `search`, so a large
|
|
166
|
+
index is not scanned in full.
|
|
167
|
+
|
|
168
|
+
=== Remove all index records
|
|
169
|
+
|
|
170
|
+
This method removes all records from the index object. The index file is not removed.
|
|
171
|
+
|
|
172
|
+
[source,ruby]
|
|
173
|
+
----
|
|
174
|
+
index.remove_all
|
|
175
|
+
----
|
|
176
|
+
|
|
177
|
+
=== Remove index file
|
|
178
|
+
|
|
179
|
+
This method removes the index file. The index object is cleared and can be used for indexing or loading a file from the specified URL.
|
|
180
|
+
|
|
181
|
+
[source,ruby]
|
|
182
|
+
----
|
|
183
|
+
index.remove_file
|
|
184
|
+
----
|
|
185
|
+
|
|
186
|
+
=== Configuration
|
|
187
|
+
|
|
188
|
+
The gem can be configured by using the `Relaton::Index.config` method. The following settings are available:
|
|
189
|
+
|
|
190
|
+
- `filename` - the name of the index file. By default, it is `index.yaml`.
|
|
191
|
+
- `storage` - the storage class or module. By default, it is `FileStorage` module. It can be any class or module that implements the `ctime`, `read`, and `write` class methods.
|
|
192
|
+
- `storage_dir` - the directory where the `.relaton/[TYPE]` folder is created. By default, it's a user's home dir. This setting works only when the gem is used for searching. When indexing, the index is always saved in a current folder.
|
|
193
|
+
|
|
194
|
+
[source,ruby]
|
|
195
|
+
----
|
|
196
|
+
Relaton::Index.config do |config|
|
|
197
|
+
config.filename = "index-v1.yaml"
|
|
198
|
+
config.storage = S3Storage
|
|
199
|
+
config.storage_dir = "/"
|
|
200
|
+
end
|
|
201
|
+
----
|
|
202
|
+
|
|
203
|
+
It's also possible to redefine file name for a specific type of index:
|
|
204
|
+
[source,ruby]
|
|
205
|
+
----
|
|
206
|
+
index = Relaton::Index.find_or_create :IHO, file: "index-v2.yaml"
|
|
207
|
+
----
|
|
208
|
+
|
|
209
|
+
== Development
|
|
210
|
+
|
|
211
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
212
|
+
|
|
213
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
214
|
+
|
|
215
|
+
== Contributing
|
|
216
|
+
|
|
217
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/relaton/relaton-index.
|
|
218
|
+
|
|
219
|
+
== License
|
|
220
|
+
|
|
221
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "time"
|
|
7
|
+
require "uri"
|
|
8
|
+
require "lutaml/model"
|
|
9
|
+
|
|
10
|
+
module Relaton
|
|
11
|
+
module Index
|
|
12
|
+
# A pinned dataset: the machine-index manifest's content address written to
|
|
13
|
+
# a file, so a build can reproduce the exact index it consumed years later.
|
|
14
|
+
#
|
|
15
|
+
# The manifest (docs/data-repository-format.adoc) carries the SHA-256 and
|
|
16
|
+
# size of the published whole-index zip. Capture records them; #verify
|
|
17
|
+
# re-checks the bytes a consumer fetched.
|
|
18
|
+
#
|
|
19
|
+
# lock = Relaton::Index::DatasetLock.capture(
|
|
20
|
+
# "https://relaton.github.io/relaton-data-iso/",
|
|
21
|
+
# )
|
|
22
|
+
# File.write(".relaton-lock.yaml", lock.to_yaml)
|
|
23
|
+
#
|
|
24
|
+
# lock = Relaton::Index::DatasetLock.from_yaml(File.read(".relaton-lock.yaml"))
|
|
25
|
+
# lock.verify(File.binread(zip_path))
|
|
26
|
+
class DatasetLock < Lutaml::Model::Serializable
|
|
27
|
+
attribute :version, :integer
|
|
28
|
+
attribute :pages_url, :string
|
|
29
|
+
attribute :index, :string
|
|
30
|
+
attribute :count, :integer
|
|
31
|
+
attribute :shards, :integer
|
|
32
|
+
attribute :generated, :string
|
|
33
|
+
attribute :sha256, :string
|
|
34
|
+
attribute :bytes, :integer
|
|
35
|
+
attribute :captured_at, :string
|
|
36
|
+
|
|
37
|
+
SHA256_HEX = /\A\h{64}\z/
|
|
38
|
+
|
|
39
|
+
# The manifest names the artifact; these fields pin it. A manifest
|
|
40
|
+
# published before content addressing has no sha256 and cannot be
|
|
41
|
+
# locked: refusing is the point, a lock without a hash guarantees
|
|
42
|
+
# nothing.
|
|
43
|
+
def self.from_manifest(pages_url, manifest, captured_at: Time.now.utc.iso8601)
|
|
44
|
+
unless manifest.is_a?(Hash)
|
|
45
|
+
raise Error, "manifest is not a JSON object: #{manifest.class}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
sha = manifest["sha256"]
|
|
49
|
+
unless sha.is_a?(String) && SHA256_HEX.match?(sha)
|
|
50
|
+
raise Error,
|
|
51
|
+
"manifest for #{pages_url} carries no sha256 content address; " \
|
|
52
|
+
"republish the index with a generator that emits one"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
index = manifest["index"]
|
|
56
|
+
unless index.is_a?(String) && !index.empty?
|
|
57
|
+
raise Error, "manifest for #{pages_url} names no index artifact"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
new(
|
|
61
|
+
version: 1,
|
|
62
|
+
pages_url: pages_url,
|
|
63
|
+
index: index,
|
|
64
|
+
count: manifest["count"],
|
|
65
|
+
shards: manifest["shards"],
|
|
66
|
+
generated: manifest["generated"],
|
|
67
|
+
sha256: sha,
|
|
68
|
+
bytes: manifest["bytes"],
|
|
69
|
+
captured_at: captured_at,
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Fetch the site's manifest and return the lock for it. +fetcher+ is the
|
|
74
|
+
# injection point for tests and alternate transports; it must answer
|
|
75
|
+
# (pages_url, **timeouts) with the parsed manifest.
|
|
76
|
+
def self.capture(pages_url, fetcher: nil, open_timeout: 15, read_timeout: 30)
|
|
77
|
+
fetch = fetcher || method(:fetch_manifest)
|
|
78
|
+
from_manifest(
|
|
79
|
+
pages_url,
|
|
80
|
+
fetch.call(pages_url, open_timeout: open_timeout, read_timeout: read_timeout),
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.fetch_manifest(pages_url, open_timeout: 15, read_timeout: 30)
|
|
85
|
+
base = pages_url.to_s
|
|
86
|
+
base = "#{base}/" unless base.end_with?("/")
|
|
87
|
+
uri = URI.join(base, "index/manifest.json")
|
|
88
|
+
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
|
|
89
|
+
open_timeout:, read_timeout:) do |http|
|
|
90
|
+
http.get(uri.request_uri)
|
|
91
|
+
end
|
|
92
|
+
raise Error, "cannot fetch #{uri}: HTTP #{res.code}" unless res.code == "200"
|
|
93
|
+
|
|
94
|
+
JSON.parse(res.body)
|
|
95
|
+
rescue JSON::ParserError => e
|
|
96
|
+
raise Error, "malformed manifest at #{uri}: #{e.message}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# The published whole-index zip this lock addresses.
|
|
100
|
+
def zip_url
|
|
101
|
+
"#{pages_url.to_s.chomp('/')}/#{index}.zip"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Raise unless +data+ is byte-for-byte the artifact this lock pins.
|
|
105
|
+
# Returns true so it reads as an assertion in build scripts.
|
|
106
|
+
def verify(data)
|
|
107
|
+
actual = Digest::SHA256.hexdigest(data)
|
|
108
|
+
if actual != sha256
|
|
109
|
+
raise Error,
|
|
110
|
+
"dataset hash mismatch for #{zip_url}: expected #{sha256}, got #{actual}"
|
|
111
|
+
end
|
|
112
|
+
if bytes && data.bytesize != bytes
|
|
113
|
+
raise Error,
|
|
114
|
+
"dataset size mismatch for #{zip_url}: expected #{bytes} bytes, " \
|
|
115
|
+
"got #{data.bytesize}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
true
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Relaton
|
|
4
|
+
module Index
|
|
5
|
+
# A document's family: every index row whose root number matches the
|
|
6
|
+
# reference's — the base document, its editions, parts, and any supplement
|
|
7
|
+
# layer (root walks a wrapper's base chain). Rows are the same
|
|
8
|
+
# `{ id:, file: }` hashes #search returns.
|
|
9
|
+
#
|
|
10
|
+
# The views are driven by the components the identifiers actually state:
|
|
11
|
+
# #editions groups rows by year/edition/version, #parts by part/parts.
|
|
12
|
+
# An identifier that states none of those appears only in #members.
|
|
13
|
+
class Family
|
|
14
|
+
attr_reader :reference, :members
|
|
15
|
+
|
|
16
|
+
def initialize(reference, members)
|
|
17
|
+
@reference = reference
|
|
18
|
+
@members = members
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [String] the family's narrowing key
|
|
22
|
+
def root_number
|
|
23
|
+
reference.root.number.to_s
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Rows grouped by their stated edition, ascending.
|
|
27
|
+
# @return [Hash{String => Array<Hash>}]
|
|
28
|
+
def editions
|
|
29
|
+
group_by(%w[year edition version])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Rows grouped by their stated part, ascending.
|
|
33
|
+
# @return [Hash{String => Array<Hash>}]
|
|
34
|
+
def parts
|
|
35
|
+
group_by(%w[part parts subpart])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def group_by(names)
|
|
41
|
+
grouped = Hash.new { |hash, key| hash[key] = [] }
|
|
42
|
+
members.each do |row|
|
|
43
|
+
value = component(row[:id], names)
|
|
44
|
+
grouped[value] << row unless value.empty?
|
|
45
|
+
end
|
|
46
|
+
grouped.sort_by { |key, _| key }.to_h
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def component(id, names)
|
|
50
|
+
hash = id.root.to_hash
|
|
51
|
+
names.each do |name|
|
|
52
|
+
value = hash[name]
|
|
53
|
+
next if value.nil?
|
|
54
|
+
|
|
55
|
+
value = value.first if value.is_a?(Array)
|
|
56
|
+
text = value.to_s
|
|
57
|
+
return text unless text.strip.empty?
|
|
58
|
+
end
|
|
59
|
+
""
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/relaton/index/type.rb
CHANGED
|
@@ -130,6 +130,27 @@ module Relaton
|
|
|
130
130
|
items.select { |i| match_item(i, id) }
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
+
#
|
|
134
|
+
# The reference's family: rows sharing its root number — the base
|
|
135
|
+
# document, its editions and parts, and any supplement layer (root walks
|
|
136
|
+
# a wrapper's base chain). Narrowing uses the same number bucket as
|
|
137
|
+
# #search, so a large index is not scanned in full.
|
|
138
|
+
#
|
|
139
|
+
# @param [String, Pubid::Identifier] reference
|
|
140
|
+
# @return [Relaton::Index::Family]
|
|
141
|
+
#
|
|
142
|
+
def family(reference)
|
|
143
|
+
ref = coerce_reference(reference)
|
|
144
|
+
root = ref.root
|
|
145
|
+
members = search_candidates(ref).select do |row|
|
|
146
|
+
id = row[:id]
|
|
147
|
+
id.is_a?(::Pubid::Identifier) &&
|
|
148
|
+
id.root.class == root.class &&
|
|
149
|
+
id.root.number.to_s == root.number.to_s
|
|
150
|
+
end
|
|
151
|
+
Family.new(ref, members)
|
|
152
|
+
end
|
|
153
|
+
|
|
133
154
|
#
|
|
134
155
|
# Save index to storage
|
|
135
156
|
#
|
|
@@ -223,6 +244,17 @@ module Relaton
|
|
|
223
244
|
ShardSource.new(@dir, @pages_url, @pubid_class) if @pages_url
|
|
224
245
|
end
|
|
225
246
|
|
|
247
|
+
# A String query is parsed by this index's flavor; a parsed identifier is
|
|
248
|
+
# used as given. A pubid-less index cannot answer a family query.
|
|
249
|
+
def coerce_reference(reference)
|
|
250
|
+
return reference unless reference.is_a?(String)
|
|
251
|
+
|
|
252
|
+
klass = @file_io.pubid_class
|
|
253
|
+
raise ArgumentError, "a String reference needs a pubid-backed index" unless klass
|
|
254
|
+
|
|
255
|
+
klass.parse(reference)
|
|
256
|
+
end
|
|
257
|
+
|
|
226
258
|
def search_candidates(id)
|
|
227
259
|
# A parsed query reads only its own shard. `@index`, once loaded, is
|
|
228
260
|
# never refreshed, so the shards stay the fresher answer.
|
data/lib/relaton/index.rb
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What this gem does
|
|
6
|
+
|
|
7
|
+
Retrieves bibliographic items from the OpenLibrary API by ISBN (10 or 13 digit) and returns `Relaton::Bib::ItemData` objects. Part of the Relaton family of gems.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
bundle exec rspec # run all tests
|
|
13
|
+
bundle exec rspec spec/relaton/isbn/parser_spec.rb # run single spec file
|
|
14
|
+
bundle exec rubocop # lint
|
|
15
|
+
bin/console # IRB with gem loaded
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
Request flow: `OpenLibrary.get(code)` → `Isbn.new(code).parse` (validates/normalizes ISBN) → `OpenLibrary.request_api(isbn)` (HTTP to openlibrary.org) → `Parser.parse(json)` (builds `ItemData`)
|
|
21
|
+
|
|
22
|
+
`get` also takes a parsed `Pubid::Isbn::Identifier`: its digits (`raw`, 10 or 13) go through the same `Isbn#parse`.
|
|
23
|
+
|
|
24
|
+
Key modules under `Relaton::Isbn`:
|
|
25
|
+
- **OpenLibrary** — API client, main entry point via `.get`
|
|
26
|
+
- **Isbn** — ISBN-10/13 validation and conversion (always normalizes to ISBN-13)
|
|
27
|
+
- **Parser** — Transforms OpenLibrary JSON into `Relaton::Bib::ItemData` with titles, contributors, dates, etc.
|
|
28
|
+
- **Processor** — Integration hook for relaton-core framework (`from_xml`, `from_yaml`, `get`)
|
|
29
|
+
- **Util** — Logging via `Relaton::Bib::Util`
|
|
30
|
+
|
|
31
|
+
## Data model (relaton-bib 2.0)
|
|
32
|
+
|
|
33
|
+
Uses `Relaton::Bib::` namespace (not the old `RelatonBib::`). Key classes: `ItemData`, `Title`, `Docidentifier` (uses `.content` not `.id`), `Uri`, `Contributor`, `Person`, `Organization`, `Date` (uses `at:` not `on:`), `Place`.
|
|
34
|
+
|
|
35
|
+
`Parser#parse` returns `Bib::ItemData` (not `Bibitem`) — `ItemData` is the plain data class; `Bibitem`/`Item` are lutaml-model serializers that wrap it.
|
|
36
|
+
|
|
37
|
+
## Testing
|
|
38
|
+
|
|
39
|
+
- **VCR** records HTTP interactions in `spec/vcr_cassettes/` (re-records every 7 days)
|
|
40
|
+
- **WebMock** disables real HTTP by default
|
|
41
|
+
- **SimpleCov** tracks coverage
|
|
42
|
+
- Fixtures in `spec/fixtures/`
|
|
43
|
+
|
|
44
|
+
## Style
|
|
45
|
+
|
|
46
|
+
- RuboCop with Ribose OSS style guide, target Ruby 3.1
|
|
47
|
+
- `rubocop-rails` is required but Rails cops are disabled (not a Rails app)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
= Relaton::ISBN: retrieves bibliographic items using OpenLibrary API
|
|
2
|
+
|
|
3
|
+
Relaton::Isbn 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://openlibrary.org, and
|
|
7
|
+
access such metadata through the `Bibitem` object.
|
|
8
|
+
|
|
9
|
+
== Usage
|
|
10
|
+
|
|
11
|
+
=== Retrieving bibliographic items using OpenLibrary API
|
|
12
|
+
|
|
13
|
+
To retrieve bibliographic items, use `Relaton::Isbn::OpenLibrary.get` method with ISBN-10 or ISBN-13 as an argument. Allowed prefixes are `ISBN`, `isbn:`. Prefix and hyphens are optional. The method returns `Relaton::Bib::Bibitem` object.
|
|
14
|
+
|
|
15
|
+
[source,ruby]
|
|
16
|
+
----
|
|
17
|
+
require 'relaton/isbn'
|
|
18
|
+
=> true
|
|
19
|
+
|
|
20
|
+
# get document by ISBN-13
|
|
21
|
+
bibitem = Relaton::Isbn::OpenLibrary.get "ISBN 978-0-12-064481-0"
|
|
22
|
+
[relaton-isbn] INFO: (ISBN 978-0-12-064481-0) Fetching from OpenLibrary ...
|
|
23
|
+
[relaton-isbn] INFO: (ISBN 978-0-12-064481-0) Found: `9780120644810`
|
|
24
|
+
=> #<Relaton::Bib::Bibitem:0x0000000127ed25d8
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
# get document by ISBN-10
|
|
28
|
+
Relaton::Isbn::OpenLibrary.get "isbn:0120644819"
|
|
29
|
+
[relaton-isbn] INFO: (isbn:0120644819) Fetching from OpenLibrary ...
|
|
30
|
+
[relaton-isbn] INFO: (isbn:0120644819) Found: `9780120644810`
|
|
31
|
+
=> #<Relaton::Bib::Bibitem:0x0000000127f11698
|
|
32
|
+
...
|
|
33
|
+
----
|
|
34
|
+
|
|
35
|
+
=== Serializing bibliographic items
|
|
36
|
+
[source,ruby]
|
|
37
|
+
----
|
|
38
|
+
# serialize to XML
|
|
39
|
+
bibitem.to_xml
|
|
40
|
+
=> "<bibitem id="9780120644810" schema-version="v1.4.1">
|
|
41
|
+
<fetched>2026-03-04</fetched>
|
|
42
|
+
<title type="main">Graphics gems II</title>
|
|
43
|
+
<uri type="src">http://openlibrary.org/books/OL21119585M/Graphics_gems_II</uri>
|
|
44
|
+
<docidentifier type="ISBN" primary="true">9780120644810</docidentifier>
|
|
45
|
+
...
|
|
46
|
+
</bibitem>"
|
|
47
|
+
|
|
48
|
+
# serialize to bibdata XML
|
|
49
|
+
bibitem.to_xml bibdata: true
|
|
50
|
+
=> "<bibdata schema-version="v1.4.1">
|
|
51
|
+
<fetched>2026-03-04</fetched>
|
|
52
|
+
<title type="main">Graphics gems II</title>
|
|
53
|
+
<uri type="src">http://openlibrary.org/books/OL21119585M/Graphics_gems_II</uri>
|
|
54
|
+
<docidentifier type="ISBN" primary="true">9780120644810</docidentifier>
|
|
55
|
+
...
|
|
56
|
+
<ext>
|
|
57
|
+
<flavor>isbn</flavor>
|
|
58
|
+
</ext>
|
|
59
|
+
</bibdata>"
|
|
60
|
+
|
|
61
|
+
# serialize to hash
|
|
62
|
+
puts bibitem.to_yaml
|
|
63
|
+
=> "---
|
|
64
|
+
id: '9780120644810'
|
|
65
|
+
schema_version: v1.4.1
|
|
66
|
+
..."
|
|
67
|
+
----
|
|
68
|
+
|
|
69
|
+
=== Logging
|
|
70
|
+
|
|
71
|
+
Relaton::Isbn 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.
|
|
72
|
+
|
|
73
|
+
== Development
|
|
74
|
+
|
|
75
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
76
|
+
`rake spec` to run the tests. You can also run `bin/console` for an interactive
|
|
77
|
+
prompt that will allow you to experiment.
|
|
78
|
+
|
|
79
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
80
|
+
release a new version, update the version number in `version.rb`, and then run
|
|
81
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
|
82
|
+
git commits and the created tag, and push the `.gem` file to
|
|
83
|
+
https://rubygems.org[rubygems.org].
|
|
84
|
+
|
|
85
|
+
== Contributing
|
|
86
|
+
|
|
87
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/relaton/relaton-isbn.
|
|
88
|
+
|
|
89
|
+
== License
|
|
90
|
+
|
|
91
|
+
The gem is available as open source under the terms of the https://opensource.org/licenses/MIT[MIT License].
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Development
|
|
6
|
+
|
|
7
|
+
- `bin/setup` — install dependencies
|
|
8
|
+
- `rake spec` — run tests
|
|
9
|
+
- `rspec spec/relaton/iso/bibliography_spec.rb` — run a single test file
|
|
10
|
+
- `rspec spec/relaton/iso/bibliography_spec.rb -e "some description"` — run a specific example
|
|
11
|
+
- `rake spec:update_index` — download latest ISO index fixture (`spec/fixtures/index-v1.zip`) from relaton-data-iso
|
|
12
|
+
- `bin/console` — interactive prompt with the gem loaded
|
|
13
|
+
- `rubocop` — lint (Ribose OSS style guide, Ruby 3.2 target)
|
|
14
|
+
|
|
15
|
+
## Architecture
|
|
16
|
+
|
|
17
|
+
relaton-iso retrieves ISO standard bibliographic data. The core retrieval flow:
|
|
18
|
+
|
|
19
|
+
1. **Bibliography** (`lib/relaton/iso/bibliography.rb`) — entry point via `search(pubid)` and `get(ref, year, opts)`. Handles year filtering, part matching, and type/stage validation. Both **propagate** `Pubid::Errors::ParseError` when the caller's reference can't be parsed by pubid (they do not swallow it — the CLI turns it into a friendly message; API consumers rescue it themselves). Note `look_up_with_any_types_stages` deliberately rescues `Pubid::Errors::ParseError` internally: it re-parses a *machine-derived* type/stage-stripped probe, so a failure there just means "no alternative-type/stage candidates" and must not abort a lookup whose original reference already parsed.
|
|
20
|
+
2. **HitCollection** (`lib/relaton/iso/hit_collection.rb`) — searches a pre-built YAML index (`index-v1.zip` from relaton-data-iso) using `Relaton::Index`. Matches ids through the parsed pubid (`reference === row`, relaton#109). Returns sorted Hit array.
|
|
21
|
+
3. **Hit** (`lib/relaton/iso/hit.rb`) — wraps an index result. The `item` attribute lazy-loads the full document from GitHub raw content (relaton-data-iso repo). `sort_weight` prioritizes published over withdrawn/deleted.
|
|
22
|
+
4. **ItemData** / **Model::Item** — ISO-specific bibliographic item extending `Relaton::Bib::ItemData`.
|
|
23
|
+
5. **Scraper** (`lib/relaton/iso/scraper.rb`) — parses individual ISO website pages. Used only by `Bibliography.get` as a fallback when an item is missing from the curated index; no longer drives bulk ingest.
|
|
24
|
+
6. **DataFetcher** (`lib/relaton/iso/data_fetcher.rb`) — streams the ISO Open Data programme JSONL feeds (`iso_deliverables_metadata.jsonl` for documents, `iso_technical_committees.jsonl` for committee titles) and writes one YAML per primary docid into `@output`. Short-circuits on upstream `Last-Modified`; falls back to a full pass when `data/` or `index-v1.yaml` is missing. Two source modes:
|
|
25
|
+
- `iso-open-data` (default) — incremental, skip when upstream is unchanged.
|
|
26
|
+
- `iso-open-data-all` — wipe `@output` and re-emit every record.
|
|
27
|
+
|
|
28
|
+
File names come from `Core::DataFetcher#unique_output_file`, not the plain `output_file`: `output_file` is not injective (`ISO/DIS 15869.3` and `ISO/DIS 15869-3` both sanitize to `iso-dis-15869-3.yaml`), so a second distinct docid gets a digest-suffixed path. `rewrite_with_same_or_newer` (edition / substage-98) therefore only ever compares two records of the **same** docid.
|
|
29
|
+
7. **DataParser** (`lib/relaton/iso/data_parser.rb`) — converts one Open Data record (`Hash`) into a `Relaton::Iso::ItemData`. Takes a `ref_index` (id → reference) for resolving `replaces`/`replacedBy` and a `tc_index` (reference → `{ "en"/"fr" => title }`) for resolving committee labels.
|
|
30
|
+
|
|
31
|
+
Key dependency: `pubid-iso` gem handles ISO publication identifier parsing and comparison.
|
|
32
|
+
|
|
33
|
+
## Testing
|
|
34
|
+
|
|
35
|
+
- **Framework:** RSpec with VCR cassettes and WebMock
|
|
36
|
+
- **Network access:** fully blocked via `WebMock.disable_net_connect!`
|
|
37
|
+
- **Index fixture:** `spec/fixtures/index-v1.zip` is pre-loaded into `Relaton::Index` pool in `before(:suite)` (configured in `spec/support/webmock.rb`). Run `rake spec:update_index` to refresh from relaton-data-iso.
|
|
38
|
+
- **VCR:** cassettes in `spec/vcr_cassettes/`, record mode `:once`, re-record interval 7 days. Index download requests are ignored by VCR (handled by fixture instead).
|