relaton 3.0.0.pre.alpha.17 → 3.0.0.pre.alpha.19
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 +59 -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/item_shared.rb +5 -1
- 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 +142 -3
- data/lib/relaton/db/CLAUDE.md +373 -0
- data/lib/relaton/db.rb +21 -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 +117 -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/ogc/data_fetcher.rb +9 -3
- 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 +85 -4
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
= Relaton::Logger
|
|
2
|
+
|
|
3
|
+
Relaton::Logger is a Ruby gem that implements a logger for Relaton gems. It is also possible to use this gem as a standalone logger.
|
|
4
|
+
|
|
5
|
+
== Usage
|
|
6
|
+
|
|
7
|
+
The Relaton::Logger gem provides a logger pool that can be used to log messages. The logger pool is a singleton and can be accessed by calling `Relaton.logger_pool`. The logger pool is an Array that contains loggers. By default, the logger pool contains a single logger, which is an instance of `Relaton::Logger::Log` class, which inherits the `Logger` class from the Ruby standard library.
|
|
8
|
+
|
|
9
|
+
The logger pool can be used to log messages by calling the methods of the loggers in the pool. These methods are the same as the methods of the `Logger` class from the Ruby standard library (`debug`, `info`, `warn`, `error`, `fatal`, `unknown`).
|
|
10
|
+
|
|
11
|
+
[source, ruby]
|
|
12
|
+
----
|
|
13
|
+
require "relaton/logger"
|
|
14
|
+
|
|
15
|
+
Relaton.logger_pool.info "message"
|
|
16
|
+
INFO: message
|
|
17
|
+
----
|
|
18
|
+
|
|
19
|
+
The program name and the key can be passed as arguments to the log methods. The program name is a string that is added to the log message. The key is a string that is added to the log message in parentheses.
|
|
20
|
+
|
|
21
|
+
[source, ruby]
|
|
22
|
+
----
|
|
23
|
+
Relaton.logger_pool.warn "message", "progname", key: "KEY"
|
|
24
|
+
[progname] WARN: (KEY) message
|
|
25
|
+
----
|
|
26
|
+
|
|
27
|
+
The block form of the log methods can be used to log messages that are expensive to generate. The block is called only if the message is logged.
|
|
28
|
+
|
|
29
|
+
[source, ruby]
|
|
30
|
+
----
|
|
31
|
+
Relaton.logger_pool.error("progname", key: "KEY") { "message" }
|
|
32
|
+
[progname] ERROR: (KEY) message
|
|
33
|
+
----
|
|
34
|
+
|
|
35
|
+
The logger pool can be configured to contain multiple loggers. Initially, there is one `:default` logger. Any instance of loggers in the logger pool are accessible with `[]` method. To add or replace a logger use `[]=` method of the logger pool and pass the logger as an argument. The loggers in the pool are called in the order they are added to the pool.
|
|
36
|
+
|
|
37
|
+
[source, ruby]
|
|
38
|
+
----
|
|
39
|
+
# Access logger
|
|
40
|
+
Relaton.logger_pool[:default]
|
|
41
|
+
=>
|
|
42
|
+
#<Relaton::Logger::Log:0x00000001212068c8
|
|
43
|
+
|
|
44
|
+
# Add a new logger
|
|
45
|
+
Relaton.logger_pool[:file] = Relaton::Logger::Log.new("relaton.log", levels: [:info, :warn])
|
|
46
|
+
----
|
|
47
|
+
|
|
48
|
+
To change the log level of a logger, call the `level=` method of the logger.
|
|
49
|
+
|
|
50
|
+
[source, ruby]
|
|
51
|
+
----
|
|
52
|
+
Relaton.logger_pool[:default].levels = [:info, :warn, :error]
|
|
53
|
+
Relaton.logger_pool[:default].add_level :fatal
|
|
54
|
+
Relaton.logger_pool[:default].remove_level :warn
|
|
55
|
+
----
|
|
56
|
+
|
|
57
|
+
To create a new logger, call the `new` method of the `Relaton::Logger::Log` class and pass arguments:
|
|
58
|
+
|
|
59
|
+
- `logdev` - file name or an IO object. If nil or File::NULL, messages are not logged. If it is a file name, the file is opened in append mode. If it is an IO object, messages are written to it.
|
|
60
|
+
- `shift_age` - the number of old log files to keep. If 0, no old log files are kept. If it is a number, old log files are kept and the log file is rotated when it reaches the maximum size. If it is a string, old log files are kept and the log file is rotated when it reaches the maximum size. The string is a time unit and a number, for example, "daily 7" or "weekly 4". Default is 0.
|
|
61
|
+
- `shift_size` - the maximum size of the log file. If it is a number, the log file is rotated when it reaches the maximum size. If it is a string, the log file is rotated when it reaches the maximum size. The string is a number and a time unit, for example, "1048576" or "1M". Default is 1048576.
|
|
62
|
+
- `levels` - an array of symbols that represent the log levels The log levels are `:debug`, `:info`, `:warn`, `:error`, `:fatal`, `:unknown`. Default is `[:info, :warn, :error, :fatal, :unknown]`.
|
|
63
|
+
- `formatter` - a formatter object that is used to format the log messages. Two formatters are available at this moment `Relaton::Logger::FormatterString`, `Relaton::Logger::FormatterJson`, and `Proc`. The default formater is `Relaton::Logger::FormatterString`.
|
|
64
|
+
- `progname` - a string that is added to the log message. Default is nil.
|
|
65
|
+
|
|
66
|
+
[source, ruby]
|
|
67
|
+
----
|
|
68
|
+
Relaton::Logger::Log.new("relaton.log", levels: [:info, :warn], formatter: Relaton::Logger::FormatterJSON, progname: "progname")
|
|
69
|
+
----
|
|
70
|
+
|
|
71
|
+
To create a custom formatter implement a class with the `call` method which takes agruments:
|
|
72
|
+
|
|
73
|
+
- `severity` - the log level
|
|
74
|
+
- `time` - the time the message was logged
|
|
75
|
+
- `progname` - the program name
|
|
76
|
+
- `msg` - the message
|
|
77
|
+
- any other keyword arguments that are passed to the log method, for example, `key`
|
|
78
|
+
|
|
79
|
+
[source, ruby]
|
|
80
|
+
----
|
|
81
|
+
class CustomFormatter
|
|
82
|
+
def call(severity, time, progname, msg, key: "Key 1")
|
|
83
|
+
"#{time} [#{severity}] #{progname} (#{key}): #{msg}\n"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
Relaton::Logger::Log.new("relaton.log", formatter: CustomFormatter)
|
|
88
|
+
----
|
|
89
|
+
|
|
90
|
+
It's possible to use `Proc` as a formatter. The `Proc` object is called with the same arguments as the `call` method of the custom formatter.
|
|
91
|
+
|
|
92
|
+
[source, ruby]
|
|
93
|
+
----
|
|
94
|
+
log = Relaton::Logger::Log.new("relaton.log")
|
|
95
|
+
|
|
96
|
+
log.formatter = Proc.new do |severity, time, progname, msg, key: "Key 1"|
|
|
97
|
+
"#{time} [#{severity}] #{progname} (#{key}): #{msg}\n"
|
|
98
|
+
end
|
|
99
|
+
----
|
|
100
|
+
|
|
101
|
+
A log file can be cleared by calling the `truncate` method of the logger pool.
|
|
102
|
+
|
|
103
|
+
[source, ruby]
|
|
104
|
+
----
|
|
105
|
+
Relaton.logger_pool.truncate
|
|
106
|
+
----
|
|
107
|
+
|
|
108
|
+
== Development
|
|
109
|
+
|
|
110
|
+
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.
|
|
111
|
+
|
|
112
|
+
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).
|
|
113
|
+
|
|
114
|
+
== Contributing
|
|
115
|
+
|
|
116
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/relaton/relaton-logger.
|
|
117
|
+
|
|
118
|
+
== License
|
|
119
|
+
|
|
120
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,180 @@
|
|
|
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 Overview
|
|
6
|
+
|
|
7
|
+
relaton-nist is a Ruby gem that retrieves and manages NIST (National Institute of Standards and Technology) bibliographic metadata as part of the Relaton ecosystem. Data sources include the NIST Cybersecurity Resource Center (CSRC) and the NIST Library (MARC21/MODS XML).
|
|
8
|
+
|
|
9
|
+
## Development Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Install dependencies
|
|
13
|
+
bundle install
|
|
14
|
+
|
|
15
|
+
# Run all tests (any of these)
|
|
16
|
+
rake
|
|
17
|
+
rake spec
|
|
18
|
+
bundle exec rspec
|
|
19
|
+
|
|
20
|
+
# Run a single test file
|
|
21
|
+
bundle exec rspec spec/relaton/nist/item_spec.rb
|
|
22
|
+
|
|
23
|
+
# Run a specific test by line number
|
|
24
|
+
bundle exec rspec spec/relaton/nist/item_spec.rb:10
|
|
25
|
+
|
|
26
|
+
# Interactive console with gem loaded
|
|
27
|
+
bin/console
|
|
28
|
+
|
|
29
|
+
# Lint (uses Ribose OSS rubocop config)
|
|
30
|
+
bundle exec rubocop
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Architecture
|
|
34
|
+
|
|
35
|
+
### Module Structure
|
|
36
|
+
|
|
37
|
+
All classes live under `Relaton::Nist` (entry point: `require 'relaton/nist'`).
|
|
38
|
+
|
|
39
|
+
### Lutaml-Model Serialization
|
|
40
|
+
|
|
41
|
+
All data models inherit from `Lutaml::Model::Serializable` and use declarative attribute/mapping DSL for XML and YAML serialization. The base bibliographic types come from `relaton-bib` (aliased as `Bib`).
|
|
42
|
+
|
|
43
|
+
**Core model hierarchy**:
|
|
44
|
+
- `Bib::Item` → `Relaton::Nist::Item` (`model ItemData`)
|
|
45
|
+
- `Relaton::Nist::Bibitem` (includes `Bib::BibitemShared`)
|
|
46
|
+
- `Relaton::Nist::Bibdata` (includes `Bib::BibdataShared`)
|
|
47
|
+
- `Bib::ItemData` → `Relaton::Nist::ItemData`
|
|
48
|
+
|
|
49
|
+
**NIST-specific extensions** (under `lib/relaton/nist/`):
|
|
50
|
+
- `Ext` — extension block holding doctype, comment period (extends `Bib::Ext`)
|
|
51
|
+
- `Date` — adds `abandoned` and `superseded` date types (extends `Bib::Date`)
|
|
52
|
+
- `Relation` — adds `obsoletedBy`, `supersedes`, `supersededBy` relation types (extends `Bib::Relation`)
|
|
53
|
+
- `CommentPeriod` — from/to/extended date ranges (extends `Lutaml::Model::Serializable`)
|
|
54
|
+
- `Doctype` — currently only `standard` (extends `Bib::Doctype`)
|
|
55
|
+
|
|
56
|
+
**Search/retrieval layer** (extends `relaton-core`):
|
|
57
|
+
- `Bibliography` — class methods `search(text, year, opts)` and `get(code, year, opts)`
|
|
58
|
+
- `HitCollection` < `Core::HitCollection` — searches GitHub index + CSRC JSON
|
|
59
|
+
- `Hit` < `Core::Hit` — lazily resolves items via `Scraper`
|
|
60
|
+
- `Processor` < `Core::Processor` — Relaton plugin interface (`get`, `fetch_data`, `from_xml`, `from_yaml`)
|
|
61
|
+
|
|
62
|
+
**Data fetching/parsing**:
|
|
63
|
+
- `DataFetcher` < `Core::DataFetcher` — fetches NIST Tech Pubs MODS XML from GitHub releases. `#source_url(source)` builds the download URL: with no tag (nil/blank/`"latest"`) it uses GitHub's `releases/latest/download/allrecords-MODS.xml` redirect, so the `relaton-data-nist` crawler auto-picks up new releases without a gem release; passing a concrete tag (e.g. `"June2026"`) pins that release. The `source` flows from `Core::DataFetcher.fetch(source)` → `#fetch(source)` → `#fetch_tech_pubs(source)` (the crawler workflow's `args` input can supply it).
|
|
64
|
+
- **Per-record resilience (mirrors the ISO flavor).** A single bad record must never abort the crawl. `#write_file` skips (never `id.content`-crashes on) a record whose MODS entry yields **no docidentifier** (e.g. a DOI stored only in `<identifier type="doi">`, which `ModsParser#parse_doi` doesn't read), and records ids that `#pubid` can't parse (new series like `NIST RB`/`NIST CHIPS`) as "not indexed" — the YAML is still written, only the index-v2 entry is skipped. Both classes accumulate in `#failures`; the `#report_errors` override emits each at `:error` (via `#log_error` → `Util.error`), which the shared `Core::DataFetcher` gh_issue machinery turns into one "Error fetching documents" GitHub issue in CI (`GITHUB_REPOSITORY` + `GITHUB_TOKEN`). `#pubid` no longer warns per-failure; reporting is centralized in `#report_errors`.
|
|
65
|
+
- **Draft-stage rows come from CSRC (relaton#313).** The MODS collection has no draft records, so `#fetch_csrc` adds the CSRC pubs-export feed (`PubsExport.instance.data`) after the MODS pass. It builds each row **exactly as the runtime builds a CSRC hit** — `HitCollection#hit_data` (which calls `#pubs_export_id`) → `Scraper.parse_page` — so the crawled id and the live id are one string, with the stage, revision and update from the row's uri and DOI (`NIST FIPS 197/Upd1 ipd`, `NIST SP 800-108r1 ipd`, `NIST SP 1800-12 2pd`). Only a row whose id carries a **stage** is written; finals come from MODS, so a CSRC row never shadows a base document. On the 914-row fixture feed this writes 302 staged records. Don't go back to a hand-built `"NIST #{series} #{docnumber} #{stage}"` id: it dropped the numbered drafts (`2pd`…), the `/UpdN`, and spelled revisions `Rev. 1`. A stage that CSRC does not list (e.g. `NIST FIPS 197 fpd`) still answers through the consumer's #303 family fallback. Three more rules: `#csrc_bib` clears the `fetched` date the runtime stamps, or every draft changes each day and `Core#force_write` rewrites ~300 files per incremental crawl; CSRC lists a republished draft twice (`…/ipd` and `…/ipd-(1)`, one DOI, so one id), so each id is written once and the plain uri wins; and only the build step is rescued (to a `#failures` entry) — a write error stops the crawl, as in the MODS pass. `#pubs_export_id` must not mutate its row (`sub`, not `sub!`): the crawler and the runtime share the `PubsExport` data.
|
|
66
|
+
- **A CSRC relation names the related document (relaton#314).** `Scraper#related_id` finds the related feed row by its uri and derives its id with `HitCollection.pubs_export_id` (the class method the instance method delegates to). The entry's own `docidentifier` drops the update and the part, and the hit's code is the record's own id, so neither is the related id. `HitCollection#search_filter` ends in an `ensure` that returns `arr`: every branch must set `arr`, because an earlier `return` is overridden there.
|
|
67
|
+
- **`#write_file` serializes before it indexes**, so a record that fails to serialize leaves no index row naming a file that was never written. That is how a `Bib::Date` in `Scraper.fetch_dates` hid: `Nist::ItemData` takes `Nist::Date`, and YAML serialization rejects the parent class (the same strictness as the docidentifier note below).
|
|
68
|
+
- `ModsParser` — maps MODS XML (via `loc_mods`) to `ItemData`
|
|
69
|
+
- `Scraper` — fetches items from GitHub YAML or CSRC JSON
|
|
70
|
+
- `PubsExport` — singleton; caches CSRC pubs-export zip with thread-safe daily updates
|
|
71
|
+
|
|
72
|
+
### Pubid-backed docidentifier
|
|
73
|
+
|
|
74
|
+
`Relaton::Nist::Docidentifier` (`docidentifier.rb`, `< Bib::Docidentifier`,
|
|
75
|
+
declared flat like the rest of the NIST models) parses its `content` into a
|
|
76
|
+
`Pubid::Nist::Identifier` kept in `@pubid`, while the lutaml `content` attribute
|
|
77
|
+
stays a plain string for serialization. It implements the base class's abstract
|
|
78
|
+
`remove_part!` / `remove_date!` / `to_all_parts!` (plus a NIST `remove_stage!`)
|
|
79
|
+
by mutating the pubid graph and re-rendering via `refresh_content!`, so NIST
|
|
80
|
+
items no longer raise `NotImplementedError` on the `Bib::ItemData#to_all_parts!`
|
|
81
|
+
/ `#remove_date!` paths. It is wired into `item.rb`
|
|
82
|
+
(`attribute :docidentifier, Docidentifier, collection: true`) and into
|
|
83
|
+
`item_base.rb` (`Nist::ItemBase`, the nested bibitem used inside `Relation`), so
|
|
84
|
+
relation cross-reference ids are pubid-backed too — mirroring
|
|
85
|
+
`iso/model/item_base.rb` / `iec/model/item_base.rb`. `ModsParser` and `Scraper`
|
|
86
|
+
build **every** docid (primary NIST id, DOI, and relation cross-refs) as this
|
|
87
|
+
class. Mirrors `lib/relaton/iec/model/docidentifier.rb` and
|
|
88
|
+
`lib/relaton/ccsds/model/docidentifier.rb`.
|
|
89
|
+
|
|
90
|
+
NIST-specific gotchas (why this isn't a straight copy of IEC/CCSDS):
|
|
91
|
+
|
|
92
|
+
- **Adopt-on-round-trip, not a `type` gate.** A NIST DOI such as
|
|
93
|
+
`NIST.SP.800-162` is itself a valid NIST pubid in dotted MR form whose `:human`
|
|
94
|
+
render is `NIST SP 800-162` — so blindly parsing it would rewrite the DOI.
|
|
95
|
+
`content=` therefore keeps the parsed pubid **only when
|
|
96
|
+
`pubid.to_s(:human) == content`**; otherwise the raw string is preserved.
|
|
97
|
+
A `type == "NIST"` gate looks tempting but breaks: lutaml applies setters in
|
|
98
|
+
attribute-declaration order (`content` before `type`), so on `from_yaml` the
|
|
99
|
+
gate sees `type == nil`, drops the pubid, and the mutators silently no-op on
|
|
100
|
+
YAML-loaded items. The round-trip test is type/order-independent.
|
|
101
|
+
- **Uniform collection type.** The `docidentifier` collection is typed
|
|
102
|
+
`Nist::Docidentifier`, and lutaml's **YAML/JSON** serializer strictly rejects a
|
|
103
|
+
parent `Bib::Docidentifier` instance in that slot (`IncorrectModelError`) —
|
|
104
|
+
which would crash the YAML-default `DataFetcher` crawler on nearly every
|
|
105
|
+
DOI-bearing record (XML tolerates it, which is why it hides in XML specs). So
|
|
106
|
+
the DOI is a `Nist::Docidentifier` too (kept raw via the round-trip rule), not
|
|
107
|
+
a `Bib::Docidentifier`.
|
|
108
|
+
- **The date lives in the `edition` component.** NIST has no `:date` attribute;
|
|
109
|
+
a year is carried as the edition id (`FIPS 46e1977`) or its trailing
|
|
110
|
+
`additional_text` (`NBS CIRC 11e2.1915`), and the scalar `year`/`edition_year`
|
|
111
|
+
fields are nil on the common parse paths. `remove_date!` clears the scalar
|
|
112
|
+
fields *and* strips a 4-digit-year edition/`additional_text`, while preserving
|
|
113
|
+
numbered editions/revisions (`e2`, `r5`). Dates that ride the `update`
|
|
114
|
+
component (some NBS supplements) are left intact — `update` also encodes
|
|
115
|
+
non-date update codes (`/Upd2`), so stripping it would corrupt identity.
|
|
116
|
+
- **No `(all parts)` marker in `content`.** `content` is live-derived from
|
|
117
|
+
`@pubid`, so `to_all_parts!` freezes the already-stripped rendering into
|
|
118
|
+
`@raw_content` before wrapping `@pubid` in pubid's `AllParts` — matching
|
|
119
|
+
IEC/CCSDS. `#pubid` itself, read directly, now answers `all_parts? ==
|
|
120
|
+
true` and renders WITH pubid's generic "(all parts)" marker.
|
|
121
|
+
|
|
122
|
+
### Serialization Round-Trip Pattern
|
|
123
|
+
|
|
124
|
+
Models support `from_xml`/`to_xml` and `from_yaml`/`to_yaml`. Tests verify round-trip fidelity by parsing a fixture, re-serializing, and comparing output to input.
|
|
125
|
+
|
|
126
|
+
### XML Schema Validation
|
|
127
|
+
|
|
128
|
+
RNG (Relax NG) schemas in `grammars/` validate XML output. Tests use the `jing` gem:
|
|
129
|
+
```ruby
|
|
130
|
+
schema = Jing.new("grammars/relaton-nist-compile.rng")
|
|
131
|
+
errors = schema.validate(file)
|
|
132
|
+
```
|
|
133
|
+
`relaton-nist-compile.rng` is the top-level schema that includes `relaton-nist.rng` and `basicdoc.rng`.
|
|
134
|
+
|
|
135
|
+
### HTTP Recording
|
|
136
|
+
|
|
137
|
+
Tests use VCR with WebMock. Cassettes are stored in `spec/vcr_cassettes/` and
|
|
138
|
+
`spec/support/vcr.rb` sets `record: :once` **with `re_record_interval: 7 days`**.
|
|
139
|
+
|
|
140
|
+
**Gotcha (this bites periodically):** once a cassette is older than 7 days, the
|
|
141
|
+
next run **re-records it from the live network** — so the suite is not truly
|
|
142
|
+
deterministic. Two known failure modes when that fires: the NIST GitHub source
|
|
143
|
+
data can drift (an author renamed, a `loc.gov` `identifier` dropped) so the
|
|
144
|
+
round-tripped XML no longer matches the on-disk `spec/fixtures/*.xml` fixtures;
|
|
145
|
+
and any flavor whose upstream throttles the recorder records a bad response.
|
|
146
|
+
When cassettes show up mass-modified in `git status` for no code reason, that's
|
|
147
|
+
the 7-day re-record firing, not a manual change. To recover: re-record cleanly
|
|
148
|
+
(delete the stale cassette, run the spec) and, for the integration fixtures that
|
|
149
|
+
compare full XML (`get.xml`, `hit.xml`, `hit_bibitem.xml`), regenerate them
|
|
150
|
+
(delete → the spec rewrites them via `File.write … unless File.exist?`).
|
|
151
|
+
|
|
152
|
+
### Test Data Stubbing
|
|
153
|
+
|
|
154
|
+
Tests pre-load both the NIST index and CSRC pubs-export data from local fixtures in `before(:suite)` (see `spec/support/webmock.rb`), avoiding all HTTP requests for these data sources. VCR is configured to ignore both `index-v2.zip` and `pubs-export` requests (`spec/support/vcr.rb`).
|
|
155
|
+
|
|
156
|
+
The gem consumes the **index-v2** index (`INDEXFILE = "index-v2"`): each row's `:id` is a `Pubid::Nist::Identifier` hash (lean `to_hash` form), so `HitCollection#from_ga` narrows candidates by number via binary search before the substring block filter, and stringifies `row[:id].to_s` at the Hit boundary. `spec/fixtures/index-v2.zip` is required for the suite to run (`rake spec:update_index` downloads it from `relaton-data-nist/v2`).
|
|
157
|
+
|
|
158
|
+
- **Index**: The YAML inside `spec/fixtures/index-v2.zip` is written to a temp file and loaded through `Relaton::Index::Type.new(:nist, file:, pubid_class: ::Pubid::Nist::Identifier)`; calling `type.index` forces the offline read + `pubid_class` deserialization (and sort) before net is blocked. The instance is injected into `Relaton::Index.pool`, with `actual?` overridden to match only the remote (`url:`) lookup so the producer-side `find_or_create(:nist, file:, pubid_class:)` still gets a fresh instance. Run `rake spec:update_index` to refresh.
|
|
159
|
+
- **PubsExport**: The `PubsExport` singleton's `@data` is set directly from `spec/fixtures/pubs-export.zip`. Run `rake spec:update_pubs_export` to refresh.
|
|
160
|
+
|
|
161
|
+
To apply the index stubbing pattern to other relaton gems:
|
|
162
|
+
|
|
163
|
+
1. Add a `spec:update_index` rake task (downloads `index-v2.zip` from the gem's GitHub data repo)
|
|
164
|
+
2. Run `rake spec:update_index` to create `spec/fixtures/index-v2.zip`
|
|
165
|
+
3. In `spec/support/webmock.rb`: extract the zip's YAML, write it to a temp file, create a `Type` with the flavor's `pubid_class`, call `type.index` to deserialize + sort, override `actual?` to match the remote `url:` lookup, and inject into `Relaton::Index.pool`
|
|
166
|
+
4. In `spec/support/vcr.rb`: add `ignore_request` for `index-v2.zip`
|
|
167
|
+
5. Remove any `allow_any_instance_of(Relaton::Index::Type)` workarounds from specs
|
|
168
|
+
|
|
169
|
+
## Key Dependencies
|
|
170
|
+
|
|
171
|
+
- `relaton-bib` — base bibliographic models and shared mixins
|
|
172
|
+
- `relaton-core` — base classes for Processor, HitCollection, Hit, DataFetcher
|
|
173
|
+
- `loc_mods` — MODS (Metadata Object Description Schema) XML parsing
|
|
174
|
+
- `pubid` — NIST publication ID parsing
|
|
175
|
+
- `relaton-index` — index/search utilities
|
|
176
|
+
- `mechanize` — HTTP fetching for data sources
|
|
177
|
+
|
|
178
|
+
## Code Style
|
|
179
|
+
|
|
180
|
+
RuboCop config inherits from the Ribose OSS style guide. Target Ruby version is 3.1.
|