pubid 2.0.22 → 2.0.23

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.
@@ -0,0 +1,72 @@
1
+ # DoiPfs — the PFS-compiled doi dev alias
2
+
3
+ This flavor is **generated**, not hand-written: every file here is compiler
4
+ output from `pubid-grammar`'s `definitions/doi.pfs` (pubid/pubid#706),
5
+ reproduce with
6
+
7
+ ```bash
8
+ cd ../pubid-grammar
9
+ ruby scripts/pfs compile definitions/doi.pfs --as doi_pfs \
10
+ --parsanol-version <CI pin> --ruby-out ../pubid/lib
11
+ cp artifacts/doi_pfs.json ../pubid/data/parg/doi_pfs.json
12
+ ```
13
+
14
+ DO NOT EDIT these files by hand — a hand edit is silently overwritten by
15
+ the next compile, and the grammar SSOT is the definition, not this module
16
+ (the `data/parg/doi_pfs.json` artifact's freshness envelope and the
17
+ definition's source sha pin the lineage).
18
+
19
+ ## The coexistence semantics (read before touching anything)
20
+
21
+ `doi_pfs` is the **dev-alias key** of the doi scheme: it registers beside
22
+ the coded `Pubid::Doi` so the compiled pipeline can be proven against the
23
+ coded flavor without cutover (cutover — replacing `doi` with the compiled
24
+ flavor — is a separate, later decision).
25
+
26
+ - **Wire tag**: the definition declares `tag: "pubid:doi:resource"` — the
27
+ SCHEME's canonical tag — so `to_hash` is byte-identical with the coded
28
+ flavor. `Resource.polymorphic_name` overrides the class-name-derived
29
+ tag for exactly this reason. Consequence: a GLOBAL `Pubid.from_hash` of
30
+ a doi_pfs hash resolves the coded class (one scheme, one `_type`, one
31
+ global resolution); the alias's own `DoiPfs::Identifier.from_hash`
32
+ routes the tag back to `DoiPfs::Identifiers::Resource` through its
33
+ `DOI_TYPE_MAP`.
34
+ - **URN namespace**: the definition declares `urn.namespace: doi`, so the
35
+ generated `UrnGenerator` pins `urn_namespace` to `"doi"` and `to_urn`
36
+ is byte-identical (`"urn:doi"`). The pubid-scheme schema registers
37
+ under the FLAVOR key (`PubidUrn.define("doi_pfs")`) — never re-register
38
+ under `"doi"`, that would overwrite the coded flavor's registration.
39
+ - **Routing**: `Pubid.parse` keeps routing `doi:`-faced strings to the
40
+ coded flavor (registry keys sort `"doi"` first; both flavors claim the
41
+ same prefixes). The alias is reached through `Pubid::DoiPfs.parse`.
42
+
43
+ ## The parity gate
44
+
45
+ `spec/pubid/doi_pfs/parity_spec.rb` is the spike's whole point: over the
46
+ full doi inventory (coded fixtures + corpus rows + aliases + constructed
47
+ edge cases), both flavors must agree byte-for-byte on `to_s`, `to_hash`,
48
+ `to_urn`, `to_mr_string`, `to_pubid_urn` and fail on the same inputs with
49
+ `Pubid::Errors::ParseError`. Drift is a defect in the definition or the
50
+ compiler — never relax an expectation here.
51
+
52
+ Non-obvious inventory facts, learned the hard way:
53
+
54
+ - The combined faces (`https://doi.org/doi:10.1000/182`) are rejected by
55
+ the CODED flavor too (the optional faces compose non-backtracking), so
56
+ they are negatives.
57
+ - `Pubid.parse("10.1000/182")` routes to **UN** on main, pre-existing:
58
+ the bare `10.` form is not a doi prefix-owner claim at a token
59
+ boundary (`10.` followed by a digit), and un's token run accepts it.
60
+ Only the scheme-faced form routes to Doi.
61
+ - The coded builder re-adds `"10."` to the registrar digits its grammar
62
+ captures; the doi.pfs pattern captures the whole `10.` face, so the
63
+ doi_pfs builder binds `prefix` directly. Model-level outputs are
64
+ identical; only the bound wire map differs (`prefix: "1000"` vs
65
+ `"10.1000"` in the artifact's corpus).
66
+
67
+ ## v1 boundaries
68
+
69
+ Single-type schemes only, `kind: string` fields only, no free-standing
70
+ `stages:`/`supplements:` sections, no semantic decode rules (the
71
+ DAM-family problem) — see `pubid-grammar`'s `definitions/README.md` for
72
+ the language reference and the fail-closed validation rules.
@@ -0,0 +1,18 @@
1
+ module Pubid
2
+ module DoiPfs
3
+ class Builder
4
+ def self.build(parsed_data)
5
+ new.build(parsed_data)
6
+ end
7
+
8
+ def build(data)
9
+ Identifiers::Resource.new(
10
+ prefix: data[:prefix],
11
+ suffix: data[:suffix],
12
+ )
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ Pubid::DoiPfs::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -0,0 +1,36 @@
1
+ module Pubid
2
+ module DoiPfs
3
+ class Identifier < ::Pubid::Identifier
4
+ attribute :prefix, :string
5
+ attribute :suffix, :string
6
+
7
+ DOI_TYPE_MAP = {
8
+ "pubid:doi:resource" => "Pubid::DoiPfs::Identifiers::Resource",
9
+ }.freeze
10
+
11
+ key_value do
12
+ map "_type", to: :_type, polymorphic_map: DOI_TYPE_MAP
13
+ map "prefix", to: :prefix
14
+ map "suffix", to: :suffix
15
+ end
16
+
17
+ PUBLISHER = "DOI".freeze
18
+
19
+ def self.parse(identifier)
20
+ unless identifier.is_a?(String)
21
+ raise Pubid::Errors::InvalidInputError,
22
+ Pubid::INPUT_NOT_A_STRING_MESSAGE
23
+ end
24
+
25
+ if identifier.length > Pubid::MAX_INPUT_LENGTH
26
+ raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
27
+ end
28
+
29
+ identifier = ::Pubid.utf8_input(identifier)
30
+
31
+ parsed = Pubid::Parg::Model.bound(:doi_pfs, identifier)
32
+ Builder.build(parsed)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,31 @@
1
+ module Pubid
2
+ module DoiPfs
3
+ module Identifiers
4
+ # Concrete DOI identifier type (PFS type `resource`).
5
+ class Resource < Identifier
6
+ TYPED_STAGES = [
7
+ Pubid::Components::TypedStage.new(
8
+ code: :pubdoi,
9
+ stage_code: :published,
10
+ type_code: :doi,
11
+ abbr: ["DOI"],
12
+ name: "DOI",
13
+ harmonized_stages: [],
14
+ ),
15
+ ].freeze
16
+
17
+ def self.type
18
+ { key: :doi, web: :doi, title: "DOI", short: "DOI" }
19
+ end
20
+
21
+ # The scheme-canonical wire tag declared in the definition:
22
+ # to_hash stays byte-identical with the scheme's coded
23
+ # flavor, and this flavor's own from_hash routes the tag
24
+ # to this class.
25
+ def self.polymorphic_name
26
+ "pubid:doi:resource"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ module Pubid
2
+ module DoiPfs
3
+ module Identifiers
4
+ autoload :Resource, "#{__dir__}/identifiers/resource"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ module Pubid
2
+ module DoiPfs
3
+ # PFS-generated parser shell. Parsing runs through the baked
4
+ # PARG artifact (data/parg/doi_pfs.json) via Pubid::Parg::Model.bound;
5
+ # the grammar source lives in pubid-grammar
6
+ # (grammars/doi_pfs.parg, compiled from definitions/doi.pfs).
7
+ class Parser
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ module Pubid
2
+ module DoiPfs
3
+ # The pubid-scheme URN schema, registered under the compiled
4
+ # flavor key. The definition's declared namespace
5
+ # ("doi") is the scheme's canonical
6
+ # namespace; identifiers carry the scheme's wire tag, so their
7
+ # pubid URNs encode through the scheme's own schema.
8
+ PubidUrnSchema = Pubid::PubidUrn.define("doi_pfs") do
9
+ atom "prefix", "prefix"
10
+ atom "suffix", "suffix"
11
+ type "pubid:doi_pfs:identifier", "id", core: []
12
+ type "pubid:doi_pfs:resource", "resource", core: []
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ module Pubid
2
+ module DoiPfs
3
+ # Human renderer, generated from the canonical template.
4
+ class Renderer < ::Pubid::Renderers::Base
5
+ def render(**_opts)
6
+ "doi:#{@id.prefix}/#{@id.suffix}"
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,12 @@
1
+ module Pubid
2
+ module DoiPfs
3
+ # The official URN generator, pinned to the scheme's declared
4
+ # namespace ("doi") so to_urn stays
5
+ # scheme-canonical under the compiled flavor key.
6
+ class UrnGenerator < ::Pubid::UrnGenerator::Base
7
+ def urn_namespace
8
+ "doi"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # PFS-generated flavor — DO NOT EDIT BY HAND.
5
+ # Source: definitions/doi.pfs (pfs:1) sha256:60abc3c403c61019d727ee44d9a72591c1549672451e6609a72d36189cae6340
6
+ # Compiler: pubid/grammar/pfs 0.1.0 (baked with parsanol 1.3.126)
7
+ # Regenerate: ruby scripts/pfs compile definitions/doi.pfs --as doi_pfs --ruby-out <dir>
8
+
9
+ require "pubid"
10
+
11
+ module Pubid
12
+ # Digital Object Identifier flavor, compiled from its PFS definition
13
+ # (pubid/pubid#706). A compiled flavor is a peer of the coded
14
+ # flavors: same registry, same input guards, same error
15
+ # namespace, same URN contracts.
16
+ module DoiPfs
17
+ extend Pubid::PrefixesSupport
18
+
19
+ PREFIXES = ["doi:", "DOI:", "10."].freeze
20
+
21
+ autoload :Builder, "#{__dir__}/doi_pfs/builder"
22
+ autoload :Identifier, "#{__dir__}/doi_pfs/identifier"
23
+ autoload :Identifiers, "#{__dir__}/doi_pfs/identifiers"
24
+ autoload :Parser, "#{__dir__}/doi_pfs/parser"
25
+ autoload :Renderer, "#{__dir__}/doi_pfs/renderer"
26
+ autoload :PubidUrnSchema, "#{__dir__}/doi_pfs/pubid_urn_schema"
27
+ autoload :UrnGenerator, "#{__dir__}/doi_pfs/urn_generator"
28
+
29
+ def self.parse(identifier)
30
+ Identifier.parse(identifier)
31
+ end
32
+
33
+ Identifier.format_registry = FormatRegistry.new(parent: Identifier.format_registry)
34
+ Identifier.format_registry.register(:human, renderer: DoiPfs::Renderer)
35
+
36
+ def self.identifier_types
37
+ @identifier_types ||= Identifiers.constants
38
+ .filter_map do |c|
39
+
40
+ Identifiers.const_get(c); rescue NameError; nil
41
+
42
+ end
43
+ .select { |c| c.is_a?(Class) && c.singleton_methods(false).include?(:type) }
44
+ .select { |c| c.type.is_a?(Hash) }
45
+ end
46
+
47
+ def self.all_typed_stages
48
+ @all_typed_stages ||= identifier_types.flat_map do |klass|
49
+ klass.const_defined?(:TYPED_STAGES) ? klass.const_get(:TYPED_STAGES) : []
50
+ end
51
+ end
52
+
53
+ def self.locate_type(code)
54
+ identifier_types.find { |t| t.type[:key].to_s == code.to_s }
55
+ end
56
+
57
+ def self.locate_stage(abbr)
58
+ abbr_str = abbr.to_s.upcase
59
+ all_typed_stages.find do |s|
60
+ s.abbr.any? do |a|
61
+ a.to_s.upcase == abbr_str
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ Pubid::Registry.register(:doi_pfs, Pubid::DoiPfs)
@@ -184,3 +184,39 @@ Read this before you change `lib/pubid/nist/` or `spec/pubid/nist/`. The root
184
184
  nothing (its flag defaults to true, so it means a spelled form anyway).
185
185
  Locked by
186
186
  `spec/pubid/nist/bare_fips_publisher_spec.rb`.
187
+
188
+ - **The default-series plain-number branch (pubid#711).** A citation
189
+ shaped like the docnumber — "800-90B", the docid-derived citation face
190
+ of NIST.SP.800-90B — and its publisher-led spelling "NIST 800-90B"
191
+ carry NO series token, and the grammar's series alternatives never
192
+ reached them: every branch demanded a series token (or, after a bare
193
+ publisher, only the dated ISO-date digits). The grammar (nist.parg,
194
+ fleet compiler 1.3.126) gained two trailing `identifier` alternatives
195
+ that ride the SAME report-number tail as the series-bearing branches —
196
+ `(publisher) (space) (report_number) …` and bare `(report_number) …` —
197
+ placed LAST so PEG ordering leaves every previously accepted form on
198
+ its original alternative (they only catch inputs that previously
199
+ failed outright). The identity defaults onto the SP series: the Router
200
+ dispatches `SpecialPublication` when the parse carries no series and
201
+ no dated date, and the Builder fills `series` from the new
202
+ `Identifier.default_series` class method (SP is the default — the
203
+ docid of the real document is NIST.SP.800-90B). The publisher rides
204
+ the #704 machinery: `SpecialPublication.series_default_publisher` is
205
+ now "NIST", so the bare parse fills the publisher after
206
+ `publisher_was_parsed: false` and is `==` to the spelled form (URN-,
207
+ MR- and hash-equal but for that equality-ignored flag; the MR face is
208
+ the exact "NIST.SP.800-90B" DOI surface relaton-data-nist stores).
209
+ The SHORT face keeps the bare spelling (publisher suppressed);
210
+ the MR face always carries the publisher (`to_mr_style` prints
211
+ `(publisher || "NIST")`), so the bare parse lands on the DOI-style
212
+ face directly. The dated alternative keeps priority
213
+ ("NIST 2023-04-15 001" stays `DatedDocument`), and the routing probe
214
+ ("zzqx-nonsense-9714") still fails, so NIST is not disqualified as a
215
+ `Pubid.parse` fallback. `to_hash` of the two bare-series probes
216
+ ("SP 123", "SP 800-90B") gained `publisher: NIST` — the #704 ruling
217
+ generalized to the SP series; no corpus row moved. Locked by
218
+ `spec/pubid/nist/default_series_plain_number_spec.rb`; the corpus
219
+ round froze the four faces (the two publisher-led spellings as
220
+ non-normalized aliases of the canonical cases, the two bare ones as
221
+ canonical `publisher_was_parsed: false` cases) with the gem twin in
222
+ lockstep.
@@ -99,6 +99,16 @@ module Pubid
99
99
  # Locate the appropriate identifier class via Router
100
100
  identifier = @router.locate_identifier_klass(parsed_hash).new
101
101
 
102
+ # The default-series plain-number branch (pubid#711): the grammar's
103
+ # trailing alternatives carry no series token ("800-90B" /
104
+ # "NIST 800-90B"), so the identity gains the class's default series
105
+ # and parses == to the spelled "NIST SP 800-90B" form. The bare
106
+ # publisher rides the #704 rule below (apply_series_default_publisher).
107
+ if parsed_hash[:series].nil? &&
108
+ (default = identifier.class.default_series)
109
+ identifier.series = Components::Code.new(value: default)
110
+ end
111
+
102
112
  # Resolve the series policy once — every series-specific decision
103
113
  # flows through this object instead of branching on the parsed shape.
104
114
  series = Series.for(parsed_hash)
@@ -302,6 +302,16 @@ module Pubid
302
302
  nil
303
303
  end
304
304
 
305
+ # The series an input implies when it spells none — the grammar's
306
+ # default-series plain-number branch (pubid#711: "800-90B" /
307
+ # "NIST 800-90B" carry no series token). The Router dispatches the
308
+ # class from this default and the Builder fills the attribute, so a
309
+ # docnumber-shaped citation parses onto the same identity as the
310
+ # spelled form. nil: no default.
311
+ def self.default_series
312
+ nil
313
+ end
314
+
305
315
  # Fill the series default publisher on an id whose input spelled
306
316
  # none. Shared by the Builder and from_hash, so both construction
307
317
  # paths agree.
@@ -35,6 +35,21 @@ module Pubid
35
35
  { key: :sp,
36
36
  web: :special_publication, title: "NIST Special Publication", short: "SP" }
37
37
  end
38
+
39
+ # The default-series plain-number branch (pubid#711): a citation
40
+ # shaped like the docnumber ("800-90B", publisher-led
41
+ # "NIST 800-90B") names an SP document — the docid-derived
42
+ # citation of NIST.SP.800-90B is exactly "800-90B".
43
+ def default_series
44
+ "SP"
45
+ end
46
+
47
+ # A bare plain-number parse carries the series' NIST publisher
48
+ # (the #704 shape), so it is == to the spelled
49
+ # "NIST SP 800-90B" form a lookup compares against.
50
+ def series_default_publisher
51
+ "NIST"
52
+ end
38
53
  end
39
54
 
40
55
  def series_code
@@ -46,6 +46,17 @@ module Pubid
46
46
 
47
47
  series = parsed_hash[:series]&.to_s
48
48
 
49
+ # The default-series plain-number branch (pubid#711): the grammar's
50
+ # trailing alternatives accept a bare report number ("800-90B") and
51
+ # its publisher-led spelling ("NIST 800-90B") with NO series token.
52
+ # The SP series is the default: the docid-derived citation of
53
+ # NIST.SP.800-90B is exactly the bare docnumber "800-90B", and the
54
+ # Builder fills the same SP series the spelled form carries, so the
55
+ # two spellings are one identity to a lookup. No other branch can
56
+ # arrive here series-less (dated shapes returned above; every
57
+ # series-bearing branch captures :series).
58
+ return Identifiers::SpecialPublication if series.nil?
59
+
49
60
  # Handle compound series that include publisher (both space and dot separated)
50
61
  if series&.start_with?("NBS ")
51
62
  simple_series = series.sub("NBS ", "")
data/lib/pubid/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pubid
4
- VERSION = "2.0.22"
4
+ VERSION = "2.0.23"
5
5
  end
data/lib/pubid.rb CHANGED
@@ -224,6 +224,10 @@ module Pubid
224
224
  autoload :Ashrae, "pubid/ashrae"
225
225
  autoload :Asme, "pubid/asme"
226
226
  autoload :Doi, "pubid/doi"
227
+ # The PFS-compiled dev alias of doi (pubid/pubid#706): registered
228
+ # beside the coded flavor, never routed by Pubid.parse (registry keys
229
+ # sort "doi" first).
230
+ autoload :DoiPfs, "pubid/doi_pfs"
227
231
  autoload :Easc, "pubid/easc"
228
232
  autoload :Astm, "pubid/astm"
229
233
  autoload :Bipm, "pubid/bipm"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pubid
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.22
4
+ version: 2.0.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -97,6 +97,7 @@ files:
97
97
  - data/parg/cie.json
98
98
  - data/parg/csa.json
99
99
  - data/parg/doi.json
100
+ - data/parg/doi_pfs.json
100
101
  - data/parg/easc.json
101
102
  - data/parg/ecma.json
102
103
  - data/parg/etsi.json
@@ -470,6 +471,16 @@ files:
470
471
  - lib/pubid/doi/parser.rb
471
472
  - lib/pubid/doi/pubid_urn_schema.rb
472
473
  - lib/pubid/doi/renderer.rb
474
+ - lib/pubid/doi_pfs.rb
475
+ - lib/pubid/doi_pfs/CLAUDE.md
476
+ - lib/pubid/doi_pfs/builder.rb
477
+ - lib/pubid/doi_pfs/identifier.rb
478
+ - lib/pubid/doi_pfs/identifiers.rb
479
+ - lib/pubid/doi_pfs/identifiers/resource.rb
480
+ - lib/pubid/doi_pfs/parser.rb
481
+ - lib/pubid/doi_pfs/pubid_urn_schema.rb
482
+ - lib/pubid/doi_pfs/renderer.rb
483
+ - lib/pubid/doi_pfs/urn_generator.rb
473
484
  - lib/pubid/easc.rb
474
485
  - lib/pubid/easc/builder.rb
475
486
  - lib/pubid/easc/identifier.rb