pubid 2.0.23 → 2.0.24
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/parg/calconnect_pfs.json +1 -0
- data/data/parg/cen_pfs.json +1 -0
- data/data/parg/etsi_pfs.json +1 -0
- data/lib/pubid/calconnect_pfs/builder.rb +35 -0
- data/lib/pubid/calconnect_pfs/identifier.rb +84 -0
- data/lib/pubid/calconnect_pfs/identifiers/standard.rb +31 -0
- data/lib/pubid/calconnect_pfs/identifiers.rb +7 -0
- data/lib/pubid/calconnect_pfs/parser.rb +10 -0
- data/lib/pubid/calconnect_pfs/pubid_urn_schema.rb +17 -0
- data/lib/pubid/calconnect_pfs/renderer.rb +10 -0
- data/lib/pubid/calconnect_pfs/urn_generator.rb +30 -0
- data/lib/pubid/calconnect_pfs.rb +60 -0
- data/lib/pubid/cen_pfs/builder.rb +33 -0
- data/lib/pubid/cen_pfs/identifier.rb +64 -0
- data/lib/pubid/cen_pfs/identifiers/european_norm.rb +31 -0
- data/lib/pubid/cen_pfs/identifiers.rb +7 -0
- data/lib/pubid/cen_pfs/parser.rb +10 -0
- data/lib/pubid/cen_pfs/pubid_urn_schema.rb +15 -0
- data/lib/pubid/cen_pfs/renderer.rb +10 -0
- data/lib/pubid/cen_pfs/urn_generator.rb +12 -0
- data/lib/pubid/cen_pfs.rb +71 -0
- data/lib/pubid/doi_pfs/identifier.rb +8 -1
- data/lib/pubid/doi_pfs.rb +3 -11
- data/lib/pubid/etsi_pfs/CLAUDE.md +95 -0
- data/lib/pubid/etsi_pfs/builder.rb +25 -0
- data/lib/pubid/etsi_pfs/identifier.rb +50 -0
- data/lib/pubid/etsi_pfs/identifiers/standard.rb +22 -0
- data/lib/pubid/etsi_pfs/identifiers.rb +7 -0
- data/lib/pubid/etsi_pfs/parser.rb +10 -0
- data/lib/pubid/etsi_pfs/pubid_urn_schema.rb +19 -0
- data/lib/pubid/etsi_pfs/renderer.rb +10 -0
- data/lib/pubid/etsi_pfs/urn_generator.rb +12 -0
- data/lib/pubid/etsi_pfs.rb +59 -0
- data/lib/pubid/itu/CLAUDE.md +25 -14
- data/lib/pubid/itu/builder.rb +7 -5
- data/lib/pubid/itu/identifiers/special_publication.rb +20 -17
- data/lib/pubid/itu/urn_generator.rb +3 -4
- data/lib/pubid/version.rb +1 -1
- data/lib/pubid.rb +13 -0
- metadata +33 -2
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# PFS-generated flavor — DO NOT EDIT BY HAND.
|
|
4
|
+
# Source: definitions/cen.pfs (pfs:1) sha256:bc1edd5874f8938b5f946416a2b052d9d9b9df3d99f6c20a3d88fcab9ae1d70c
|
|
5
|
+
# Compiler: pubid/grammar/pfs 0.1.0 (baked with parsanol 1.3.126)
|
|
6
|
+
# Regenerate: ruby scripts/pfs compile definitions/cen.pfs --as cen_pfs --ruby-out <dir>
|
|
7
|
+
|
|
8
|
+
require "pubid"
|
|
9
|
+
|
|
10
|
+
module Pubid
|
|
11
|
+
# CEN/CENELEC European Norms (module composition) flavor, compiled from its PFS definition
|
|
12
|
+
# (pubid/pubid#706). A compiled flavor is a peer of the coded
|
|
13
|
+
# flavors: same registry, same input guards, same error
|
|
14
|
+
# namespace, same URN contracts.
|
|
15
|
+
module CenPfs
|
|
16
|
+
extend Pubid::PrefixesSupport
|
|
17
|
+
|
|
18
|
+
PREFIXES = ["EN", "CEN/", "CLC/"].freeze
|
|
19
|
+
# The module composition the definition binds (issue #706 §4):
|
|
20
|
+
# each entry names the library module, the version it was bound
|
|
21
|
+
# at, and — where the module declares one — its IDENTITY
|
|
22
|
+
# composition (wrapper-with-adopted: the adopted document is
|
|
23
|
+
# the base). Carried so consumers and the wizard can
|
|
24
|
+
# introspect what the flavor is composed of.
|
|
25
|
+
MODULES = [
|
|
26
|
+
{ name: "iso.inner", version: 1, identity: { composition: "wrapper-with-adopted", base_module: "iso.inner", adopted_field: :adopted } },
|
|
27
|
+
{ name: "en.overlay", version: 1, identity: { composition: "wrapper-with-adopted", base_module: "iso.inner", adopted_field: :adopted } },
|
|
28
|
+
{ name: "national.prefix", version: 1, identity: { composition: "wrapper-with-adopted", base_module: "iso.inner", adopted_field: :adopted } },
|
|
29
|
+
{ name: "supplements.core", version: 1, identity: { composition: "wrapper-with-adopted", adopted_field: :base } },
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
autoload :Builder, "#{__dir__}/cen_pfs/builder"
|
|
33
|
+
autoload :Identifier, "#{__dir__}/cen_pfs/identifier"
|
|
34
|
+
autoload :Identifiers, "#{__dir__}/cen_pfs/identifiers"
|
|
35
|
+
autoload :Parser, "#{__dir__}/cen_pfs/parser"
|
|
36
|
+
autoload :Renderer, "#{__dir__}/cen_pfs/renderer"
|
|
37
|
+
autoload :PubidUrnSchema, "#{__dir__}/cen_pfs/pubid_urn_schema"
|
|
38
|
+
autoload :UrnGenerator, "#{__dir__}/cen_pfs/urn_generator"
|
|
39
|
+
|
|
40
|
+
def self.parse(identifier)
|
|
41
|
+
Identifier.parse(identifier)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
Identifier.format_registry = FormatRegistry.new(parent: Identifier.format_registry)
|
|
45
|
+
Identifier.format_registry.register(:human, renderer: CenPfs::Renderer)
|
|
46
|
+
|
|
47
|
+
def self.identifier_types
|
|
48
|
+
@identifier_types ||= Identifiers.constants
|
|
49
|
+
.filter_map { |c| begin; Identifiers.const_get(c); rescue NameError; nil; end }
|
|
50
|
+
.select { |c| c.is_a?(Class) && c.singleton_methods(false).include?(:type) }
|
|
51
|
+
.select { |c| c.type.is_a?(Hash) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.all_typed_stages
|
|
55
|
+
@all_typed_stages ||= identifier_types.flat_map do |klass|
|
|
56
|
+
klass.const_defined?(:TYPED_STAGES) ? klass.const_get(:TYPED_STAGES) : []
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.locate_type(code)
|
|
61
|
+
identifier_types.find { |t| t.type[:key].to_s == code.to_s }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.locate_stage(abbr)
|
|
65
|
+
abbr_str = abbr.to_s.upcase
|
|
66
|
+
all_typed_stages.find { |s| s.abbr.any? { |a| a.to_s.upcase == abbr_str } }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
Pubid::Registry.register(:cen_pfs, Pubid::CenPfs)
|
|
@@ -14,7 +14,14 @@
|
|
|
14
14
|
map "suffix", to: :suffix
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
PUBLISHER = "DOI"
|
|
17
|
+
PUBLISHER = "DOI"
|
|
18
|
+
|
|
19
|
+
# The scheme's declared publisher token: the internal-form
|
|
20
|
+
# cut (to_s(with_publisher: false)) strips it from the
|
|
21
|
+
# rendered face.
|
|
22
|
+
def publisher_prefixes
|
|
23
|
+
[PUBLISHER]
|
|
24
|
+
end
|
|
18
25
|
|
|
19
26
|
def self.parse(identifier)
|
|
20
27
|
unless identifier.is_a?(String)
|
data/lib/pubid/doi_pfs.rb
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
3
2
|
#
|
|
4
3
|
# PFS-generated flavor — DO NOT EDIT BY HAND.
|
|
5
4
|
# Source: definitions/doi.pfs (pfs:1) sha256:60abc3c403c61019d727ee44d9a72591c1549672451e6609a72d36189cae6340
|
|
@@ -17,6 +16,7 @@ module Pubid
|
|
|
17
16
|
extend Pubid::PrefixesSupport
|
|
18
17
|
|
|
19
18
|
PREFIXES = ["doi:", "DOI:", "10."].freeze
|
|
19
|
+
|
|
20
20
|
|
|
21
21
|
autoload :Builder, "#{__dir__}/doi_pfs/builder"
|
|
22
22
|
autoload :Identifier, "#{__dir__}/doi_pfs/identifier"
|
|
@@ -35,11 +35,7 @@ module Pubid
|
|
|
35
35
|
|
|
36
36
|
def self.identifier_types
|
|
37
37
|
@identifier_types ||= Identifiers.constants
|
|
38
|
-
.filter_map
|
|
39
|
-
|
|
40
|
-
Identifiers.const_get(c); rescue NameError; nil
|
|
41
|
-
|
|
42
|
-
end
|
|
38
|
+
.filter_map { |c| begin; Identifiers.const_get(c); rescue NameError; nil; end }
|
|
43
39
|
.select { |c| c.is_a?(Class) && c.singleton_methods(false).include?(:type) }
|
|
44
40
|
.select { |c| c.type.is_a?(Hash) }
|
|
45
41
|
end
|
|
@@ -56,11 +52,7 @@ module Pubid
|
|
|
56
52
|
|
|
57
53
|
def self.locate_stage(abbr)
|
|
58
54
|
abbr_str = abbr.to_s.upcase
|
|
59
|
-
all_typed_stages.find
|
|
60
|
-
s.abbr.any? do |a|
|
|
61
|
-
a.to_s.upcase == abbr_str
|
|
62
|
-
end
|
|
63
|
-
end
|
|
55
|
+
all_typed_stages.find { |s| s.abbr.any? { |a| a.to_s.upcase == abbr_str } }
|
|
64
56
|
end
|
|
65
57
|
end
|
|
66
58
|
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# EtsiPfs — the PFS-compiled etsi dev alias
|
|
2
|
+
|
|
3
|
+
This flavor is **generated**, not hand-written: every file here is compiler
|
|
4
|
+
output from `pubid-grammar`'s `definitions/etsi.pfs` (pubid/pubid#706
|
|
5
|
+
phase 2), reproduce with
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd ../pubid-grammar
|
|
9
|
+
ruby scripts/pfs compile definitions/etsi.pfs --as etsi_pfs \
|
|
10
|
+
--parsanol-version <CI pin> --ruby-out ../pubid/lib
|
|
11
|
+
mv ../pubid/lib/etsi_pfs.rb ../pubid/lib/pubid/etsi_pfs.rb # __dir__ paths assume lib/pubid/
|
|
12
|
+
mv ../pubid/lib/etsi_pfs ../pubid/lib/pubid/etsi_pfs
|
|
13
|
+
cp artifacts/etsi_pfs.json ../pubid/data/parg/etsi_pfs.json
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
DO NOT EDIT these files by hand — a hand edit is silently overwritten by
|
|
17
|
+
the next compile, and the grammar SSOT is the definition, not this module
|
|
18
|
+
(the `data/parg/etsi_pfs.json` artifact's freshness envelope and the
|
|
19
|
+
definition's source sha pin the lineage).
|
|
20
|
+
|
|
21
|
+
## The coexistence semantics (read before touching anything)
|
|
22
|
+
|
|
23
|
+
`etsi_pfs` is the **dev-alias key** of the etsi scheme: it registers beside
|
|
24
|
+
the coded `Pubid::Etsi` so the compiled pipeline can be proven against the
|
|
25
|
+
coded flavor without cutover. Same rules as `lib/pubid/doi_pfs/CLAUDE.md`:
|
|
26
|
+
the definition declares the scheme-canonical wire tag
|
|
27
|
+
(`pubid:etsi:etsi-standard`) and the scheme's `to_urn` namespace (`etsi`);
|
|
28
|
+
`Pubid.parse` keeps routing `ETSI …` to the coded flavor.
|
|
29
|
+
|
|
30
|
+
## The parity gate
|
|
31
|
+
|
|
32
|
+
`spec/pubid/etsi_pfs/parity_spec.rb` is the deliverable: over the FULL
|
|
33
|
+
fixture inventory (24,718 pass fixtures — the largest corpus a compiled
|
|
34
|
+
flavor is bracketed against) plus constructed edge cases and 23 negatives.
|
|
35
|
+
Measured surface (see the spec header for the numbers and their meaning):
|
|
36
|
+
|
|
37
|
+
- **accept 24,718/24,718** and the same `ParseError` on negatives — the
|
|
38
|
+
language bracket: the compiled grammar accepts exactly the coded surface
|
|
39
|
+
(all 15 type tokens; the four number shapes GSM/dotted/complex/simple;
|
|
40
|
+
minor; the parts run; the /A//C supplement runs; `V`-versions and
|
|
41
|
+
`ed.N`; the `(YYYY-MM)` date).
|
|
42
|
+
- **to_hash and to_pubid_urn byte parity: 18,945/24,718** — exactly the
|
|
43
|
+
rows carrying none of the three constructs pfs:1 cannot declare. Each
|
|
44
|
+
gap is pinned per construct in the spec, so compiler progress turns the
|
|
45
|
+
pin red:
|
|
46
|
+
1. **collection bindings** — the coded model stores `["1","0"]` (the
|
|
47
|
+
`part -> parts[].part` binding); the compiled capture is the printed
|
|
48
|
+
dash run `"-1-0"`.
|
|
49
|
+
2. **the edition face** — the coded model folds `ed.N` into
|
|
50
|
+
`Version(version: "N", is_edition: true)`; the compiled hash carries
|
|
51
|
+
`edition: "N"`.
|
|
52
|
+
3. **supplement wrappers** — the coded model wraps `/A1` `/C1` as
|
|
53
|
+
Amendment/Corrigendum classes with an Integer ordinal delegating to
|
|
54
|
+
their base; the compiled row stays a flat etsi-standard carrying the
|
|
55
|
+
printed run `"/A1"`.
|
|
56
|
+
- **to_s** renders the canonical CORE face (`ETSI <type> <number>`) —
|
|
57
|
+
pfs:1 render faces cannot render optionals, so every corpus row (all of
|
|
58
|
+
which carry version/ed + date) renders shorter; bare ids agree
|
|
59
|
+
byte-for-byte.
|
|
60
|
+
- **to_urn** is a compiler gap (the coded generator is hand-written Ruby
|
|
61
|
+
composing `urn:etsi:{type}:{code}:{version}:{date}`; the generated
|
|
62
|
+
generator pins the namespace only). Note the CODED generator raises
|
|
63
|
+
`ArgumentError` on months 08/09 (`format('%02d', "08")` — the zero-padded
|
|
64
|
+
string reads as an invalid octal); pre-existing on main, pinned by the
|
|
65
|
+
parity spec.
|
|
66
|
+
- **to_mr_string** is a compiler gap (the coded slug reads the flavor's
|
|
67
|
+
`mr_*` hooks — type/parts/edition/date segments; the base hooks find
|
|
68
|
+
none of them on the compiled columns).
|
|
69
|
+
|
|
70
|
+
## Non-obvious facts, learned the hard way
|
|
71
|
+
|
|
72
|
+
- The generated `--ruby-out` lands at `lib/etsi_pfs.rb`; the autoload and
|
|
73
|
+
the `#{__dir__}` paths require it at `lib/pubid/etsi_pfs.rb` — move both
|
|
74
|
+
the file and the directory (commands above).
|
|
75
|
+
- `core: ["number"]` in a `.pfs` urn type row would generate
|
|
76
|
+
`core: [:number]`, which crashes `PubidUrn.define` at load time (the
|
|
77
|
+
generator emits `core.map(&:to_sym)`); only `core: []` round-trips.
|
|
78
|
+
The instance's URNs resolve through the CODED schema anyway (the flavor
|
|
79
|
+
segment of the canonical tag is `etsi`), so the alias rows are the
|
|
80
|
+
future-cutover shape.
|
|
81
|
+
- `V1..1` parses in BOTH flavors (`.` is a version_char) and `GS` is a
|
|
82
|
+
real type token — neither is a parity negative.
|
|
83
|
+
- `Pubid::EtsiPfs.parse("ETSI …")` never hijacks `Pubid.parse` (registry
|
|
84
|
+
keys sort `"etsi"` first), and `Pubid.from_hash` of an alias hash
|
|
85
|
+
resolves the coded `EtsiStandard` (one scheme, one `_type`).
|
|
86
|
+
- The corpus inventory (`spec/fixtures/etsi/identifiers/full/`) is the
|
|
87
|
+
parity source; its bucket split is pinned: plain 18,945 / parts 3,772 /
|
|
88
|
+
edition 1,833 / supplement 168.
|
|
89
|
+
|
|
90
|
+
## v1 boundaries
|
|
91
|
+
|
|
92
|
+
Single-type schemes only, `kind: string` fields only, no free-standing
|
|
93
|
+
`stages:`/`supplements:` sections, no semantic decode rules — see
|
|
94
|
+
`pubid-grammar`'s `definitions/README.md` for the language reference and
|
|
95
|
+
the fail-closed validation rules.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Pubid
|
|
2
|
+
module EtsiPfs
|
|
3
|
+
class Builder
|
|
4
|
+
def self.build(parsed_data)
|
|
5
|
+
new.build(parsed_data)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def build(data)
|
|
9
|
+
Identifiers::Standard.new(
|
|
10
|
+
type: data[:type],
|
|
11
|
+
number: data[:number],
|
|
12
|
+
minor: data[:minor],
|
|
13
|
+
parts: data[:parts],
|
|
14
|
+
supplements: data[:supplements],
|
|
15
|
+
version: data[:version],
|
|
16
|
+
edition: data[:edition],
|
|
17
|
+
year: data[:year],
|
|
18
|
+
month: data[:month],
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
Pubid::EtsiPfs::Builder.prepend(Pubid::Builder::AllPartsWrap)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Pubid
|
|
2
|
+
module EtsiPfs
|
|
3
|
+
class Identifier < ::Pubid::Identifier
|
|
4
|
+
attribute :type, :string
|
|
5
|
+
attribute :number, :string
|
|
6
|
+
attribute :minor, :string
|
|
7
|
+
attribute :parts, :string
|
|
8
|
+
attribute :supplements, :string
|
|
9
|
+
attribute :version, :string
|
|
10
|
+
attribute :edition, :string
|
|
11
|
+
attribute :year, :string
|
|
12
|
+
attribute :month, :string
|
|
13
|
+
|
|
14
|
+
ETSI_TYPE_MAP = {
|
|
15
|
+
"pubid:etsi:etsi-standard" => "Pubid::EtsiPfs::Identifiers::Standard",
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
key_value do
|
|
19
|
+
map "_type", to: :_type, polymorphic_map: ETSI_TYPE_MAP
|
|
20
|
+
map "type", to: :type
|
|
21
|
+
map "number", to: :number
|
|
22
|
+
map "minor", to: :minor
|
|
23
|
+
map "parts", to: :parts
|
|
24
|
+
map "supplements", to: :supplements
|
|
25
|
+
map "version", to: :version
|
|
26
|
+
map "edition", to: :edition
|
|
27
|
+
map "year", to: :year
|
|
28
|
+
map "month", to: :month
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
PUBLISHER = "ETSI"
|
|
32
|
+
|
|
33
|
+
def self.parse(identifier)
|
|
34
|
+
unless identifier.is_a?(String)
|
|
35
|
+
raise Pubid::Errors::InvalidInputError,
|
|
36
|
+
Pubid::INPUT_NOT_A_STRING_MESSAGE
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if identifier.length > Pubid::MAX_INPUT_LENGTH
|
|
40
|
+
raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
identifier = ::Pubid.utf8_input(identifier)
|
|
44
|
+
|
|
45
|
+
parsed = Pubid::Parg::Model.bound(:etsi_pfs, identifier)
|
|
46
|
+
Builder.build(parsed)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Pubid
|
|
2
|
+
module EtsiPfs
|
|
3
|
+
module Identifiers
|
|
4
|
+
# Concrete ETSI Standard identifier type (PFS type `standard`).
|
|
5
|
+
class Standard < Identifier
|
|
6
|
+
TYPED_STAGES = [].freeze
|
|
7
|
+
|
|
8
|
+
def self.type
|
|
9
|
+
{ key: :etsi, web: :etsi, title: "ETSI Standard", short: "ETSI" }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# The scheme-canonical wire tag declared in the definition:
|
|
13
|
+
# to_hash stays byte-identical with the scheme's coded
|
|
14
|
+
# flavor, and this flavor's own from_hash routes the tag
|
|
15
|
+
# to this class.
|
|
16
|
+
def self.polymorphic_name
|
|
17
|
+
"pubid:etsi:etsi-standard"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Pubid
|
|
2
|
+
module EtsiPfs
|
|
3
|
+
# PFS-generated parser shell. Parsing runs through the baked
|
|
4
|
+
# PARG artifact (data/parg/etsi_pfs.json) via Pubid::Parg::Model.bound;
|
|
5
|
+
# the grammar source lives in pubid-grammar
|
|
6
|
+
# (grammars/etsi_pfs.parg, compiled from definitions/etsi.pfs).
|
|
7
|
+
class Parser
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Pubid
|
|
2
|
+
module EtsiPfs
|
|
3
|
+
# The pubid-scheme URN schema, registered under the compiled
|
|
4
|
+
# flavor key. The definition's declared namespace
|
|
5
|
+
# ("etsi") 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("etsi_pfs") do
|
|
9
|
+
atom "type", "type"
|
|
10
|
+
atom "parts", "part"
|
|
11
|
+
atom "minor", "minor"
|
|
12
|
+
atom "year", "year"
|
|
13
|
+
atom "month", "month"
|
|
14
|
+
atom "version", "v"
|
|
15
|
+
type "pubid:etsi_pfs:identifier", "id", core: []
|
|
16
|
+
type "pubid:etsi_pfs:standard", "es", core: []
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Pubid
|
|
2
|
+
module EtsiPfs
|
|
3
|
+
# The official URN generator, pinned to the scheme's declared
|
|
4
|
+
# namespace ("etsi") so to_urn stays
|
|
5
|
+
# scheme-canonical under the compiled flavor key.
|
|
6
|
+
class UrnGenerator < ::Pubid::UrnGenerator::Base
|
|
7
|
+
def urn_namespace
|
|
8
|
+
"etsi"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# PFS-generated flavor — DO NOT EDIT BY HAND.
|
|
4
|
+
# Source: definitions/etsi.pfs (pfs:1) sha256:9343088bf087b55ea790057e6be0bbebe2272407ee81f035b52e5f9a4d5c7b76
|
|
5
|
+
# Compiler: pubid/grammar/pfs 0.1.0 (baked with parsanol 1.3.126)
|
|
6
|
+
# Regenerate: ruby scripts/pfs compile definitions/etsi.pfs --as etsi_pfs --ruby-out <dir>
|
|
7
|
+
|
|
8
|
+
require "pubid"
|
|
9
|
+
|
|
10
|
+
module Pubid
|
|
11
|
+
# ETSI flavor, compiled from its PFS definition
|
|
12
|
+
# (pubid/pubid#706). A compiled flavor is a peer of the coded
|
|
13
|
+
# flavors: same registry, same input guards, same error
|
|
14
|
+
# namespace, same URN contracts.
|
|
15
|
+
module EtsiPfs
|
|
16
|
+
extend Pubid::PrefixesSupport
|
|
17
|
+
|
|
18
|
+
PREFIXES = ["ETSI"].freeze
|
|
19
|
+
|
|
20
|
+
autoload :Builder, "#{__dir__}/etsi_pfs/builder"
|
|
21
|
+
autoload :Identifier, "#{__dir__}/etsi_pfs/identifier"
|
|
22
|
+
autoload :Identifiers, "#{__dir__}/etsi_pfs/identifiers"
|
|
23
|
+
autoload :Parser, "#{__dir__}/etsi_pfs/parser"
|
|
24
|
+
autoload :Renderer, "#{__dir__}/etsi_pfs/renderer"
|
|
25
|
+
autoload :PubidUrnSchema, "#{__dir__}/etsi_pfs/pubid_urn_schema"
|
|
26
|
+
autoload :UrnGenerator, "#{__dir__}/etsi_pfs/urn_generator"
|
|
27
|
+
|
|
28
|
+
def self.parse(identifier)
|
|
29
|
+
Identifier.parse(identifier)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
Identifier.format_registry = FormatRegistry.new(parent: Identifier.format_registry)
|
|
33
|
+
Identifier.format_registry.register(:human, renderer: EtsiPfs::Renderer)
|
|
34
|
+
|
|
35
|
+
def self.identifier_types
|
|
36
|
+
@identifier_types ||= Identifiers.constants
|
|
37
|
+
.filter_map { |c| begin; Identifiers.const_get(c); rescue NameError; nil; end }
|
|
38
|
+
.select { |c| c.is_a?(Class) && c.singleton_methods(false).include?(:type) }
|
|
39
|
+
.select { |c| c.type.is_a?(Hash) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.all_typed_stages
|
|
43
|
+
@all_typed_stages ||= identifier_types.flat_map do |klass|
|
|
44
|
+
klass.const_defined?(:TYPED_STAGES) ? klass.const_get(:TYPED_STAGES) : []
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.locate_type(code)
|
|
49
|
+
identifier_types.find { |t| t.type[:key].to_s == code.to_s }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.locate_stage(abbr)
|
|
53
|
+
abbr_str = abbr.to_s.upcase
|
|
54
|
+
all_typed_stages.find { |s| s.abbr.any? { |a| a.to_s.upcase == abbr_str } }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
Pubid::Registry.register(:etsi_pfs, Pubid::EtsiPfs)
|
data/lib/pubid/itu/CLAUDE.md
CHANGED
|
@@ -82,20 +82,31 @@ a replay of all 24,382 rows and every ITU pass fixture showed 0 changes.
|
|
|
82
82
|
code, so `#number` returns the series and `root.number` is `"RR"`. The rule
|
|
83
83
|
ends in `any.absent?`: it sits before `with_series`, and PEG never re-enters
|
|
84
84
|
the alternation, so a partial match on `ITU-R RR.1` must fail inside it.
|
|
85
|
-
- **Operational
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
85
|
+
- **An Operational Bulletin never carries a sector (#696).** OB is an
|
|
86
|
+
org-level publication: "ITU OB", never "ITU-T OB". This reverses the
|
|
87
|
+
"Operational Bulletins keep their sector" rule of `27454393`, which kept the
|
|
88
|
+
sector as a spelling but left it in `to_hash`, so `ITU OB No. 1096` and
|
|
89
|
+
`ITU-T OB.1096` were `==` and had two pubid-scheme URNs (two relaton slugs).
|
|
90
|
+
The TSB spellings (`ITU-T OB.1096`, `ITU-T OB No. 1096`, `ITU-T Operational
|
|
91
|
+
Bulletin No. 1096`, also as an `Annex to …` base) still **parse** — the
|
|
92
|
+
grammar is unchanged — but `build_special_publication` passes no sector and
|
|
93
|
+
`SpecialPublication` drops one given to `new` or to `sector=` (the path a
|
|
94
|
+
legacy `from_hash` row with `"sector" => "T"` takes). So every surface,
|
|
95
|
+
`to_s` included, is sector-free: `ITU-T OB.1096 (2016)` renders `ITU OB
|
|
96
|
+
No. 1096 (2016)`. The `ITU-T OB…` lines in `pass/special_publication.txt`
|
|
97
|
+
are therefore `!input!rendered`. `No.` stays in the default render — it is
|
|
98
|
+
how ITU's bulletin site and pubid v1 write it — and metanorma-itu's `ITU OB
|
|
99
|
+
1000` / `Annex to ITU OB 1000` (its i18n template omits `No.`) is accepted
|
|
100
|
+
on parse (`ob_bare_body`) and rendered with it. The printed date
|
|
101
|
+
`- 15.III.2016` sets `date.day`, which only this form does, so
|
|
102
|
+
`render_ob_date` uses the day as the spelling marker; `day_to_kv` emits only
|
|
103
|
+
then. `build_supplement` does not take the sector of a TSB-spelled OB base
|
|
104
|
+
either, so `ITU-T OB.1096 Amd. 1` stores no sector and slugs
|
|
105
|
+
`itu.ob-1096…`. **Known gap:** the grammar has no sector-less OB
|
|
106
|
+
supplement, so that id renders `ITU OB No. 1096 Amd. 1`, which does not
|
|
107
|
+
parse back. No corpus holds an OB supplement; a grammar rule would be a
|
|
108
|
+
pubid-grammar change. Sources that cite `ITU-T OB` (relaton's
|
|
109
|
+
`publication_code`, the pubid-testsuite rows) are fixed in their own repos.
|
|
99
110
|
- **`-YYYYMM` is a date, never a part.** `part` refuses a six-digit run with a
|
|
100
111
|
19xx/20xx year and a 01–12 month (`yyyymm_shape`), and `id_date` reads it as
|
|
101
112
|
year+month. `-200313`, `-180001` stay parts. `ITU-T REC T.4` drops the
|
data/lib/pubid/itu/builder.rb
CHANGED
|
@@ -170,12 +170,10 @@ module Pubid
|
|
|
170
170
|
nil
|
|
171
171
|
end
|
|
172
172
|
|
|
173
|
-
# Build Special Publication (OB).
|
|
174
|
-
# ("ITU-T OB.1096") is
|
|
175
|
-
# SpecialPublication#== ignores it, since OB is cross-bureau.
|
|
173
|
+
# Build Special Publication (OB). An OB never carries a sector, so the
|
|
174
|
+
# sector of the TSB spelling ("ITU-T OB.1096") is discarded (#696).
|
|
176
175
|
def build_special_publication(data)
|
|
177
176
|
Identifiers::SpecialPublication.new(
|
|
178
|
-
sector: (Components::Sector.new(sector: data[:sector].to_s) if data[:sector]),
|
|
179
177
|
series: Components::Series.new(series: "OB"),
|
|
180
178
|
code: data[:number] ? build_code(data) : nil,
|
|
181
179
|
date: data[:year] ? build_date(data) : nil,
|
|
@@ -289,8 +287,12 @@ module Pubid
|
|
|
289
287
|
# it via #root; otherwise a supplement of an annex would carry none of
|
|
290
288
|
# them and its MR string would collapse to a bare "itu.<date>".
|
|
291
289
|
base_identity = base&.sector ? base : base&.root
|
|
290
|
+
# An Operational Bulletin never carries a sector (#696), so a
|
|
291
|
+
# supplement of one does not take the sector of its TSB spelling.
|
|
292
|
+
own_sector = data[:sector] &&
|
|
293
|
+
!base_identity.is_a?(Identifiers::SpecialPublication)
|
|
292
294
|
sector = base_identity&.sector ||
|
|
293
|
-
(Components::Sector.new(sector: data[:sector].to_s) if
|
|
295
|
+
(Components::Sector.new(sector: data[:sector].to_s) if own_sector)
|
|
294
296
|
series = base_identity&.series ||
|
|
295
297
|
(Components::Series.new(series: data[:series].to_s) if data[:series])
|
|
296
298
|
|
|
@@ -4,29 +4,33 @@ module Pubid
|
|
|
4
4
|
module Itu
|
|
5
5
|
module Identifiers
|
|
6
6
|
# ITU Special Publication — currently models the Operational Bulletin (OB).
|
|
7
|
-
# OB is
|
|
8
|
-
# ("ITU OB No. 1283 (01/2024)")
|
|
9
|
-
# ("ITU-T OB.1096 (2016)"),
|
|
10
|
-
#
|
|
11
|
-
#
|
|
7
|
+
# OB is an org-level publication: it never carries a sector
|
|
8
|
+
# ("ITU OB No. 1283 (01/2024)", never "ITU-T OB"). The TSB spelling
|
|
9
|
+
# ("ITU-T OB.1096 (2016)") parses, but the sector is discarded here, so
|
|
10
|
+
# both spellings are one value on every surface — `to_s`, `to_hash`,
|
|
11
|
+
# `==`, the URN and the pubid-scheme URN (issue #696).
|
|
12
12
|
class SpecialPublication < Identifier
|
|
13
13
|
include StandardSerialization
|
|
14
14
|
|
|
15
15
|
ROMAN_MONTHS = %w[I II III IV V VI VII VIII IX X XI XII].freeze
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"#{publisher} #{series} No. #{number}"
|
|
23
|
-
end
|
|
17
|
+
# Drop a sector given on construction (the parse path never passes
|
|
18
|
+
# one; a caller or a legacy hash row may).
|
|
19
|
+
def initialize(**kwargs)
|
|
20
|
+
super(**kwargs.except(:sector))
|
|
21
|
+
end
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
# Drop a sector set later — `from_hash` of a legacy row with
|
|
24
|
+
# `"sector" => "T"` comes through this setter.
|
|
25
|
+
def sector=(_value)
|
|
26
|
+
super(nil)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def render_base(**_opts)
|
|
30
|
+
"#{publisher} #{series} No. #{code&.number}#{render_ob_date}"
|
|
26
31
|
end
|
|
27
32
|
|
|
28
|
-
#
|
|
29
|
-
# which bulletin it is.
|
|
33
|
+
# Only the fields a bulletin carries. The sector is always nil now.
|
|
30
34
|
def ==(other)
|
|
31
35
|
return false unless other.instance_of?(self.class)
|
|
32
36
|
|
|
@@ -37,8 +41,7 @@ module Pubid
|
|
|
37
41
|
common_text_twin == other.common_text_twin
|
|
38
42
|
end
|
|
39
43
|
|
|
40
|
-
#
|
|
41
|
-
# the sector used to be dropped).
|
|
44
|
+
# A bulletin has no type token in its MR slug.
|
|
42
45
|
def mr_type
|
|
43
46
|
nil
|
|
44
47
|
end
|
|
@@ -16,10 +16,9 @@ module Pubid
|
|
|
16
16
|
def generate_base_urn
|
|
17
17
|
parts = ["urn", "itu"]
|
|
18
18
|
|
|
19
|
-
# An Operational Bulletin
|
|
20
|
-
#
|
|
21
|
-
if identifier.sector
|
|
22
|
-
!identifier.is_a?(Identifiers::SpecialPublication)
|
|
19
|
+
# An Operational Bulletin never carries a sector (#696), so it gets
|
|
20
|
+
# the `itu` segment here.
|
|
21
|
+
if identifier.sector
|
|
23
22
|
sector = identifier.sector.to_s
|
|
24
23
|
parts << sector.to_s.downcase
|
|
25
24
|
else
|
data/lib/pubid/version.rb
CHANGED
data/lib/pubid.rb
CHANGED
|
@@ -233,12 +233,25 @@ module Pubid
|
|
|
233
233
|
autoload :Bipm, "pubid/bipm"
|
|
234
234
|
autoload :Bsi, "pubid/bsi"
|
|
235
235
|
autoload :Calconnect, "pubid/calconnect"
|
|
236
|
+
# The PFS-compiled dev alias of calconnect (pubid/pubid#706 §8): the
|
|
237
|
+
# acceptance port, registered beside the coded flavor, never routed by
|
|
238
|
+
# Pubid.parse (registry keys sort the coded key first).
|
|
239
|
+
autoload :CalconnectPfs, "pubid/calconnect_pfs"
|
|
240
|
+
# The PFS-compiled module-composition surface of cen_cenelec
|
|
241
|
+
# (pubid/pubid#706 §4): the EN face composed from the module library —
|
|
242
|
+
# the measured boundary of what the modules absorb
|
|
243
|
+
# (spec/pubid/cen_pfs/parity_spec.rb's ledger).
|
|
244
|
+
autoload :CenPfs, "pubid/cen_pfs"
|
|
236
245
|
autoload :Ccsds, "pubid/ccsds"
|
|
237
246
|
autoload :CenCenelec, "pubid/cen_cenelec"
|
|
238
247
|
autoload :Cie, "pubid/cie"
|
|
239
248
|
autoload :Csa, "pubid/csa"
|
|
240
249
|
autoload :Ecma, "pubid/ecma"
|
|
241
250
|
autoload :Etsi, "pubid/etsi"
|
|
251
|
+
# The PFS-compiled dev alias of etsi (pubid/pubid#706): registered
|
|
252
|
+
# beside the coded flavor, never routed by Pubid.parse (registry keys
|
|
253
|
+
# sort "etsi" first).
|
|
254
|
+
autoload :EtsiPfs, "pubid/etsi_pfs"
|
|
242
255
|
autoload :Evs, "pubid/evs"
|
|
243
256
|
autoload :Gost, "pubid/gost"
|
|
244
257
|
autoload :Gb, "pubid/gb"
|