pubid 2.0.0.pre.alpha.6 → 2.0.0.pre.alpha.8

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +49 -50
  3. data/lib/pubid/amca.rb +6 -0
  4. data/lib/pubid/ansi.rb +7 -0
  5. data/lib/pubid/api.rb +5 -0
  6. data/lib/pubid/ashrae.rb +6 -0
  7. data/lib/pubid/asme.rb +5 -0
  8. data/lib/pubid/astm.rb +5 -0
  9. data/lib/pubid/bsi.rb +15 -0
  10. data/lib/pubid/ccsds.rb +5 -0
  11. data/lib/pubid/cen_cenelec.rb +7 -0
  12. data/lib/pubid/cie.rb +5 -0
  13. data/lib/pubid/csa.rb +5 -0
  14. data/lib/pubid/etsi.rb +7 -0
  15. data/lib/pubid/iala/builder.rb +62 -6
  16. data/lib/pubid/iala/identifier.rb +11 -7
  17. data/lib/pubid/iala/identifiers/advice.rb +15 -0
  18. data/lib/pubid/iala/identifiers/annex.rb +57 -0
  19. data/lib/pubid/iala/identifiers/general_assembly.rb +16 -0
  20. data/lib/pubid/iala/identifiers/letter.rb +16 -0
  21. data/lib/pubid/iala/identifiers.rb +12 -8
  22. data/lib/pubid/iala/parser.rb +39 -8
  23. data/lib/pubid/iala/renderer.rb +14 -0
  24. data/lib/pubid/iala/urn_generator.rb +17 -0
  25. data/lib/pubid/iala.rb +5 -0
  26. data/lib/pubid/idf.rb +5 -0
  27. data/lib/pubid/iec.rb +8 -0
  28. data/lib/pubid/ieee.rb +8 -0
  29. data/lib/pubid/iho.rb +5 -0
  30. data/lib/pubid/iso.rb +6 -0
  31. data/lib/pubid/itu.rb +5 -0
  32. data/lib/pubid/jcgm.rb +5 -0
  33. data/lib/pubid/jis.rb +6 -0
  34. data/lib/pubid/nist/parser.rb +3 -1
  35. data/lib/pubid/nist/router.rb +1 -0
  36. data/lib/pubid/nist.rb +14 -0
  37. data/lib/pubid/oiml/builder.rb +33 -0
  38. data/lib/pubid/oiml/identifier.rb +1 -0
  39. data/lib/pubid/oiml/identifiers/bulletin.rb +101 -0
  40. data/lib/pubid/oiml/identifiers.rb +1 -0
  41. data/lib/pubid/oiml/parser.rb +43 -1
  42. data/lib/pubid/oiml/renderer.rb +51 -0
  43. data/lib/pubid/oiml/urn_generator.rb +22 -0
  44. data/lib/pubid/oiml.rb +5 -0
  45. data/lib/pubid/plateau.rb +5 -0
  46. data/lib/pubid/prefixes_support.rb +51 -0
  47. data/lib/pubid/sae.rb +6 -0
  48. data/lib/pubid/version.rb +1 -1
  49. data/lib/pubid.rb +68 -0
  50. metadata +8 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1b1fa5f38da07bc9a507c0c507ba9ddfc8f6541ab9f4b6793b09abe302ab8849
4
- data.tar.gz: 63b7eba9c9942c177e95b9c78a09462679a11acde4fd6a2c2e486848fb43ca93
3
+ metadata.gz: 5987bc5bdc86f9fdf02244f7b5e1abbff308a0d7a5ae88c9ea6e26d20f316ec4
4
+ data.tar.gz: d768df87e4d1b06d0be0dbcc72dab8d5e5eda5476b4157bbf754c5863a2ad1f0
5
5
  SHA512:
6
- metadata.gz: 915034be3882a43991ce3c0f934c4097f216f1fdac35dcb59ed6a0decb639dd4a19882de4f28940d4f29e6e9bb351de3383b07bd95cc0dc2a53bb4ad875f072d
7
- data.tar.gz: 1cb7e935accc6e66bed8b599d25605b26cd415547c1ddec9ba2ecf1b7183883102c99078b830b2de9da436bc17613db042b5fc05e0d7d5515786fae71592aeb8
6
+ metadata.gz: 7214ebd2c2eac905def41dd1b4304ef299224378e703ab3d9883022b8fd2770adafd08c8e697d759a9ae6e2a984ff54927051090039c8b20cd56f98ae21bc3b8
7
+ data.tar.gz: 32227b91be942f7f59c758685ea7cbe105845eaa758c29984d43f81adeaf33321e68f6b4f7a8d9d71709f3395acf7011d008e1a2983ae43a61610b267c785d0e
data/README.adoc CHANGED
@@ -1,54 +1,15 @@
1
1
  = PubID: Interoperable identifiers for information resources
2
2
 
3
- image:https://img.shields.io/gem/v/pubid-core.svg["pubid-core Gem Version", link="https://rubygems.org/gems/pubid-core"]
3
+ image:https://img.shields.io/gem/v/pubid.svg["pubid Gem Version", link="https://rubygems.org/gems/pubid"]
4
4
  image:https://github.com/metanorma/pubid/actions/workflows/rake.yml/badge.svg["Build Status", link="https://github.com/metanorma/pubid/actions/workflows/rake.yml"]
5
5
  image:https://img.shields.io/github/issues-pr-raw/metanorma/pubid.svg["Pull Requests", link="https://github.com/metanorma/pubid/pulls"]
6
6
  image:https://img.shields.io/github/commits-since/metanorma/pubid/latest.svg["Commits since latest",link="https://github.com/metanorma/pubid/releases"]
7
7
 
8
- == Badges
9
-
10
- [cols="3*", options="header"]
11
- |===
12
- | Core & Meta | Standards Organizations | Regional Standards
13
-
14
- | pubid image:https://img.shields.io/gem/v/pubid.svg["pubid", link="https://rubygems.org/gems/pubid"]
15
- | pubid-iso image:https://img.shields.io/gem/v/pubid-iso.svg["pubid-iso", link="https://rubygems.org/gems/pubid-iso"]
16
- | pubid-cen image:https://img.shields.io/gem/v/pubid-cen.svg["pubid-cen", link="https://rubygems.org/gems/pubid-cen"]
17
-
18
- | pubid-core image:https://img.shields.io/gem/v/pubid-core.svg["pubid-core", link="https://rubygems.org/gems/pubid-core"]
19
- | pubid-iec image:https://img.shields.io/gem/v/pubid-iec.svg["pubid-iec", link="https://rubygems.org/gems/pubid-iec"]
20
- | pubid-bsi image:https://img.shields.io/gem/v/pubid-bsi.svg["pubid-bsi", link="https://rubygems.org/gems/pubid-bsi"]
21
-
22
- |
23
- | pubid-ieee image:https://img.shields.io/gem/v/pubid-ieee.svg["pubid-ieee", link="https://rubygems.org/gems/pubid-ieee"]
24
- | pubid-jis image:https://img.shields.io/gem/v/pubid-jis.svg["pubid-jis", link="https://rubygems.org/gems/pubid-jis"]
25
-
26
- |
27
- | pubid-nist image:https://img.shields.io/gem/v/pubid-nist.svg["pubid-nist", link="https://rubygems.org/gems/pubid-nist"]
28
- |
29
-
30
- |
31
- | pubid-etsi image:https://img.shields.io/gem/v/pubid-etsi.svg["pubid-etsi", link="https://rubygems.org/gems/pubid-etsi"]
32
- |
33
-
34
- |
35
- | pubid-itu image:https://img.shields.io/gem/v/pubid-itu.svg["pubid-itu", link="https://rubygems.org/gems/pubid-itu"]
36
- |
37
-
38
- |
39
- | pubid-ccsds image:https://img.shields.io/gem/v/pubid-ccsds.svg["pubid-ccsds", link="https://rubygems.org/gems/pubid-ccsds"]
40
- |
41
-
42
- |
43
- | pubid-plateau image:https://img.shields.io/gem/v/pubid-plateau.svg["pubid-plateau", link="https://rubygems.org/gems/pubid-plateau"]
44
- |
45
- |===
46
-
47
8
  == Quick Start
48
9
 
49
10
  [source,ruby]
50
11
  ----
51
- gem install pubid-core
12
+ gem install pubid
52
13
 
53
14
  require 'pubid/iso'
54
15
 
@@ -292,6 +253,46 @@ base.root.equal?(base) # => true
292
253
  * ✅ **Works with supplements** - Traverses supplement chains correctly
293
254
 
294
255
 
256
+ == Flavor Prefix Routing
257
+
258
+ Every flavor exposes a static `prefixes` method returning the *leading
259
+ identifier prefix tokens* it recognizes — the tokens a printed identifier can
260
+ start with such that the string belongs to that SDO. This is the routing key a
261
+ consumer (such as relaton) uses to dispatch a reference string to the right
262
+ backend, without parsing.
263
+
264
+ [source,ruby]
265
+ ----
266
+ require 'pubid'
267
+
268
+ Pubid::Iso.prefixes
269
+ # => ["ISO", "ISO/IEC", "IEC/ISO", "ISO/IEC/IEEE"]
270
+
271
+ Pubid::Bsi.prefixes
272
+ # => ["BS", "BSI", "DD", "PD", "PAS", "NA", "TS", "HB", ... , "BS EN ISO"]
273
+
274
+ # Look up a single flavor by name
275
+ Pubid.prefixes(:iec)
276
+ # => ["IEC", "CISPR", "IECEE", "IECEx", "IECQ", "ISO/IEC", "IEC/ISO", "ISO/IEC/IEEE"]
277
+
278
+ # Reverse index: prefix token => owning flavor(s)
279
+ Pubid.prefix_flavors["ISO/IEC"] # => [:iec, :iso]
280
+ Pubid.prefix_flavors["ISO/IEC/IEEE"] # => [:iec, :ieee, :iso]
281
+ Pubid.prefix_flavors["DD"] # => [:bsi]
282
+ ----
283
+
284
+ **Semantics** (see `Pubid::PrefixesSupport` for the full contract):
285
+
286
+ * **Static** — no identifier string is parsed; callable at startup.
287
+ * **Uniform** — every registered flavor returns a plain `Array<String>`.
288
+ * **Symmetric joints** — a co-published prefix is listed by every co-publisher
289
+ (`"ISO/IEC"` is in both `Pubid::Iso.prefixes` and `Pubid::Iec.prefixes`).
290
+ * **Inclusion** — publisher prefixes (`ISO`, `NIST`, `BS`…) and leading
291
+ series/type tokens that can *begin* a reference (BSI `DD`/`PD`, NIST `FIPS`).
292
+ * **Exclusion** — pure sub-series that never lead a reference and
293
+ dangerously-ambiguous single letters (e.g. BSI aerospace `A`/`B`/`C`).
294
+
295
+
295
296
  == Advanced Rendering Styles
296
297
 
297
298
  ISO and IEC identifiers support multiple abbreviation forms for supplements to maintain round-trip fidelity with different official formats.
@@ -638,15 +639,13 @@ lib/tasks/export.rake # Rake tasks: export:website_data, export:a
638
639
 
639
640
  == Repository
640
641
 
641
- This repository is a monorepo for the PubID Ruby gems, which implement the PubID
642
+ This repository provides the single `pubid` Ruby gem, which implements the PubID
642
643
  identifier data model and its various components.
643
644
 
644
- The PubID Ruby gems implement identifiers from multiple standards organizations,
645
- making it easier for developers to work with them in their applications.
646
-
647
- This repository contains all the `pubid-*` gems consolidated into a single
648
- monorepo for easier development and maintenance while preserving individual gem
649
- releases.
645
+ The `pubid` gem implements identifiers from multiple standards organizations,
646
+ making it easier for developers to work with them in their applications. All 22+
647
+ supported flavors (ISO, IEC, NIST, IEEE, …) live under `lib/pubid/{flavor}/` and
648
+ ship together in the one gem.
650
649
 
651
650
  == Structure
652
651
 
@@ -671,11 +670,11 @@ The repository is organized as follows:
671
670
  ├── spec/ # Tests
672
671
  │ ├── pubid/ # Per-flavor tests
673
672
  │ ├── fixtures/ # Bulk test fixtures
674
- │ └── integration/ # Cross-gem tests
673
+ │ └── integration/ # Cross-flavor integration tests
675
674
  ├── docs/ # Documentation
676
675
  ├── .github/workflows/ # CI/CD workflows
677
676
  ├── Gemfile # Root Gemfile for development
678
- ├── Rakefile # Monorepo management tasks
677
+ ├── Rakefile # Test, lint, build, and release tasks
679
678
  ├── .rubocop.yml # Shared RuboCop configuration
680
679
  └── LICENSE.txt # Shared license
681
680
  ----
data/lib/pubid/amca.rb CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  module Pubid
4
4
  module Amca
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # AMCA publisher token; the ANSI/AMCA joint form comes from
8
+ # Pubid::JOINT_PREFIXES.
9
+ PREFIXES = ["AMCA"].freeze
10
+
5
11
  autoload :Identifier, "#{__dir__}/amca/identifier"
6
12
  autoload :Identifiers, "#{__dir__}/amca/identifiers"
7
13
  autoload :Builder, "#{__dir__}/amca/builder"
data/lib/pubid/ansi.rb CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  module Pubid
4
4
  module Ansi
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # ANSI publisher token; ANSI/ASHRAE and ANSI/AMCA joint forms come from
8
+ # Pubid::JOINT_PREFIXES. Parser::ORGANIZATIONS are co-publishers, not
9
+ # leading ANSI tokens, so they are excluded.
10
+ PREFIXES = ["ANSI"].freeze
11
+
5
12
  autoload :Builder, "#{__dir__}/ansi/builder"
6
13
  autoload :Identifier, "#{__dir__}/ansi/identifier"
7
14
  autoload :Identifiers, "#{__dir__}/ansi/identifiers"
data/lib/pubid/api.rb CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Pubid
4
4
  module Api
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Sole API publisher token (see the parser's `publisher` rule).
8
+ PREFIXES = ["API"].freeze
9
+
5
10
  autoload :Builder, "#{__dir__}/api/builder"
6
11
  autoload :Identifier, "#{__dir__}/api/identifier"
7
12
  autoload :Identifiers, "#{__dir__}/api/identifiers"
data/lib/pubid/ashrae.rb CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  module Pubid
4
4
  module Ashrae
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # ASHRAE publisher token; the ANSI/ASHRAE joint form comes from
8
+ # Pubid::JOINT_PREFIXES.
9
+ PREFIXES = ["ASHRAE"].freeze
10
+
5
11
  autoload :Builder, "#{__dir__}/ashrae/builder"
6
12
  autoload :Identifier, "#{__dir__}/ashrae/identifier"
7
13
  autoload :Identifiers, "#{__dir__}/ashrae/identifiers"
data/lib/pubid/asme.rb CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Pubid
4
4
  module Asme
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Sole ASME publisher token (see the parser's `asme_publisher` rule).
8
+ PREFIXES = ["ASME"].freeze
9
+
5
10
  autoload :Builder, "#{__dir__}/asme/builder"
6
11
  autoload :Components, "#{__dir__}/asme/components/code"
7
12
  autoload :Identifier, "#{__dir__}/asme/identifier"
data/lib/pubid/astm.rb CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Pubid
4
4
  module Astm
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Sole ASTM publisher token (see the parser's `publisher` rule).
8
+ PREFIXES = ["ASTM"].freeze
9
+
5
10
  autoload :Components, "#{__dir__}/astm/components"
6
11
  autoload :Identifier, "#{__dir__}/astm/identifier"
7
12
  autoload :Identifiers, "#{__dir__}/astm/identifiers"
data/lib/pubid/bsi.rb CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  module Pubid
4
4
  module Bsi
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Leading tokens a printed BSI reference can start with, curated from the
8
+ # parser grammar (lib/pubid/bsi/parser.rb): the publisher (`bs` => BS/BSI),
9
+ # the native series/type rules that begin references (dd, pd, pas, na, ts,
10
+ # handbook => HB, pp, bip, ep_type), and the "BS <adopted-org>" combos the
11
+ # grammar builds from `bs` + `adopted_org_prefix`. Deliberately EXCLUDES the
12
+ # aerospace single/multi-letter prefixes (see
13
+ # identifiers/aerospace_standard.rb SINGLE_LETTER_PREFIXES / the parser's
14
+ # `single_letter_prefix` rule — A/B/C/S/X ...) because they are dangerously
15
+ # ambiguous and never lead a routable reference on their own; and the bare
16
+ # adopted forms (ISO/IEC/EN alone), which route to their own SDOs.
17
+ PREFIXES = (%w[BS BSI DD PD PAS NA TS HB PP BIP EP] +
18
+ ["BS EN", "BS ISO", "BS IEC", "BS EN ISO", "BS EN IEC"]).freeze
19
+
5
20
  module Components
6
21
  autoload :Code, "#{__dir__}/bsi/components/code"
7
22
  autoload :Date, "#{__dir__}/bsi/components/date"
data/lib/pubid/ccsds.rb CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Pubid
4
4
  module Ccsds
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Sole CCSDS publisher token (see the parser's `ccsds_prefix` rule).
8
+ PREFIXES = ["CCSDS"].freeze
9
+
5
10
  autoload :Builder, "#{__dir__}/ccsds/builder"
6
11
  autoload :Identifier, "#{__dir__}/ccsds/identifier"
7
12
  autoload :Identifiers, "#{__dir__}/ccsds/identifiers"
@@ -2,6 +2,13 @@
2
2
 
3
3
  module Pubid
4
4
  module CenCenelec
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # CEN/CENELEC publisher tokens, mirroring the parser's publisher rules
8
+ # (lib/pubid/cen_cenelec/parser.rb). Adopted-org prefixes (ISO/IEC/CISPR)
9
+ # are excluded — they route to their own SDOs, not to CEN.
10
+ PREFIXES = %w[EN CEN CLC CWA HD ES CR ENV].freeze
11
+
5
12
  autoload :Builder, "#{__dir__}/cen_cenelec/builder"
6
13
  autoload :Identifier, "#{__dir__}/cen_cenelec/identifier"
7
14
  autoload :Identifiers, "#{__dir__}/cen_cenelec/identifiers"
data/lib/pubid/cie.rb CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Pubid
4
4
  module Cie
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Sole CIE publisher token (see the parser grammar).
8
+ PREFIXES = ["CIE"].freeze
9
+
5
10
  module Components
6
11
  autoload :Code, "#{__dir__}/cie/components/code"
7
12
  autoload :Language, "#{__dir__}/cie/components/language"
data/lib/pubid/csa.rb CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Pubid
4
4
  module Csa
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Sole CSA publisher token (see the parser's `publisher` rule).
8
+ PREFIXES = ["CSA"].freeze
9
+
5
10
  autoload :Builder, "#{__dir__}/csa/builder"
6
11
  autoload :Identifier, "#{__dir__}/csa/identifier"
7
12
  autoload :Identifiers, "#{__dir__}/csa/identifiers"
data/lib/pubid/etsi.rb CHANGED
@@ -4,6 +4,13 @@ require "lutaml/model"
4
4
 
5
5
  module Pubid
6
6
  module Etsi
7
+ extend Pubid::PrefixesSupport
8
+
9
+ # Sole ETSI publisher token (see the parser's `etsi_prefix` rule). The
10
+ # document-type tokens (EN, TS, TR, ...) are sub-types that follow the ETSI
11
+ # prefix, not standalone routing tokens, so they are excluded.
12
+ PREFIXES = ["ETSI"].freeze
13
+
7
14
  autoload :Builder, "#{__dir__}/etsi/builder"
8
15
  autoload :Components, "#{__dir__}/etsi/components"
9
16
  autoload :Identifier, "#{__dir__}/etsi/identifier"
@@ -6,10 +6,13 @@ module Pubid
6
6
  class Builder
7
7
  def build(parsed)
8
8
  hash = parsed
9
+
10
+ return build_annex(hash) if hash[:annex_marker]
11
+
9
12
  type_letter = stringify(hash[:type_letter])&.upcase
10
13
 
11
14
  attrs = {
12
- number: build_number(hash),
15
+ number: build_number(hash, type_letter),
13
16
  edition: stringify(hash.dig(:edition, :edition_value)),
14
17
  language: stringify(hash.dig(:language_group, :language))&.upcase,
15
18
  }.compact
@@ -23,13 +26,66 @@ module Pubid
23
26
 
24
27
  private
25
28
 
26
- # Compose "1070" or "0126-1" from doc_number + optional subpart.
27
- def build_number(hash)
29
+ # Build the Annex wrapper from the parse tree. The base sub-tree
30
+ # (captured under :base) is recursively built via the same logic as
31
+ # a flat identifier, then wrapped.
32
+ def build_annex(hash)
33
+ base_hash = hash[:base]
34
+ base_type = stringify(base_hash[:type_letter])&.upcase
35
+ base_number = build_number(base_hash, base_type)
36
+ base = Iala.identifier_klass_for_type_letter(base_type).new(number: base_number)
37
+
38
+ attrs = {
39
+ base_identifier: base,
40
+ annex_form: stringify(hash[:annex_marker]),
41
+ letter: stringify(hash[:annex_letter]),
42
+ edition: stringify(hash.dig(:edition, :edition_value)),
43
+ language: stringify(hash.dig(:language_group, :language))&.upcase,
44
+ }.compact
45
+
46
+ Identifiers::Annex.new(**attrs)
47
+ end
48
+
49
+ # Compose the full number string from the leading doc_number plus any
50
+ # dotted continuation (GA01.13 → "01.13", L2.1.11 → "2.1.11") plus
51
+ # the optional dash-separated subpart (C0103-1 → "0103-1", L2.7.1-2
52
+ # → "2.7.1-2"). Padding is type-aware per IALA's canonical form:
53
+ # typed S/R/G/M/C zero-pad to 4 digits; GeneralAssembly (GA) zero-
54
+ # pads each segment to 2; Advices (A) and Letters (L) preserve input
55
+ # (their canonical form is already what the IALA site prints).
56
+ def build_number(hash, type_letter = nil)
28
57
  base = stringify(hash[:doc_number])
29
- return base unless hash[:subpart]
58
+ return base unless base
59
+
60
+ base = canonical_base(base, type_letter)
61
+
62
+ dots = hash[:doc_number_dots]
63
+ dots_str = dots.is_a?(Array) ? "" : canonical_dots(stringify(dots).to_s, type_letter)
64
+
65
+ subpart_str = hash[:subpart] ? "-#{subpart_to_s(hash[:subpart])}" : ""
66
+ subpart_str = "" if subpart_str == "-"
67
+ "#{base}#{dots_str}#{subpart_str}"
68
+ end
69
+
70
+ # Per-type canonical width for the leading numeric run.
71
+ def canonical_base(base, type_letter)
72
+ case type_letter
73
+ when "S", "R", "G", "M", "C", "X", "P" then base.rjust(4, "0")
74
+ when "GA" then base.rjust(2, "0")
75
+ else base
76
+ end
77
+ end
78
+
79
+ # Per-type canonical width for each dotted continuation segment.
80
+ # `str` looks like ".13" or ".1.11" (leading dot, then digits).
81
+ def canonical_dots(str, type_letter)
82
+ return "" if str.empty?
30
83
 
31
- subpart_str = subpart_to_s(hash[:subpart])
32
- subpart_str.empty? ? base : "#{base}-#{subpart_str}"
84
+ if type_letter == "GA"
85
+ str.split(".", -1).map { |seg| seg.empty? ? seg : seg.rjust(2, "0") }.join(".")
86
+ else
87
+ str
88
+ end
33
89
  end
34
90
 
35
91
  # The subpart capture may be an array of {subpart_number: "1"},
@@ -33,13 +33,17 @@ module Pubid
33
33
  attribute :language, :string # "E", "F", "S", "C", "A", "R", nil
34
34
 
35
35
  IALA_TYPE_MAP = {
36
- "pubid:iala:standard" => "Pubid::Iala::Identifiers::Standard",
37
- "pubid:iala:recommendation" => "Pubid::Iala::Identifiers::Recommendation",
38
- "pubid:iala:guideline" => "Pubid::Iala::Identifiers::Guideline",
39
- "pubid:iala:manual" => "Pubid::Iala::Identifiers::Manual",
40
- "pubid:iala:model-course" => "Pubid::Iala::Identifiers::ModelCourse",
41
- "pubid:iala:report" => "Pubid::Iala::Identifiers::Report",
42
- "pubid:iala:resolution" => "Pubid::Iala::Identifiers::Resolution",
36
+ "pubid:iala:standard" => "Pubid::Iala::Identifiers::Standard",
37
+ "pubid:iala:recommendation" => "Pubid::Iala::Identifiers::Recommendation",
38
+ "pubid:iala:guideline" => "Pubid::Iala::Identifiers::Guideline",
39
+ "pubid:iala:manual" => "Pubid::Iala::Identifiers::Manual",
40
+ "pubid:iala:model-course" => "Pubid::Iala::Identifiers::ModelCourse",
41
+ "pubid:iala:advice" => "Pubid::Iala::Identifiers::Advice",
42
+ "pubid:iala:general-assembly" => "Pubid::Iala::Identifiers::GeneralAssembly",
43
+ "pubid:iala:letter" => "Pubid::Iala::Identifiers::Letter",
44
+ "pubid:iala:annex" => "Pubid::Iala::Identifiers::Annex",
45
+ "pubid:iala:report" => "Pubid::Iala::Identifiers::Report",
46
+ "pubid:iala:resolution" => "Pubid::Iala::Identifiers::Resolution",
43
47
  }.freeze
44
48
 
45
49
  key_value do
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Iala
5
+ module Identifiers
6
+ # IALA Advices (A prefix). Numbering is dashed: A12-01, A13-04.
7
+ # Carries no edition in the corpus.
8
+ class Advice < Base
9
+ def self.type
10
+ { key: :advice, title: "Advice", short: "A" }
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Iala
5
+ module Identifiers
6
+ # IALA Annex to a base publication. Two surface forms appear in the
7
+ # corpus, both wrapping a Guideline:
8
+ #
9
+ # "G1045 Annex Ed 1" — bare Annex, no letter
10
+ # "G1128 ANNEX A Ed 1.6" — uppercase + letter (A/B/C/D)
11
+ #
12
+ # The case of the marker is preserved (Annex vs ANNEX) for round-trip
13
+ # fidelity. Edition and language apply to the Annex itself, not the base.
14
+ class Annex < Base
15
+ # The publication this annex supplements. Always present; carries the
16
+ # type letter and number (e.g. G1045).
17
+ attribute :base_identifier, ::Pubid::Iala::Identifier, polymorphic: true
18
+ # "Annex" or "ANNEX" — preserved verbatim for exact round-trip.
19
+ attribute :annex_form, :string
20
+ # "A", "B", "C", "D", or nil for the bare form.
21
+ attribute :letter, :string
22
+
23
+ key_value do
24
+ map "base_identifier",
25
+ with: { to: :base_identifier_to_kv,
26
+ from: :base_identifier_from_kv }
27
+ map "annex_form", to: :annex_form
28
+ map "letter", to: :letter
29
+ end
30
+
31
+ def self.type
32
+ { key: :annex, title: "Annex", short: nil }
33
+ end
34
+
35
+ # Public because lutaml-model invokes the `to:` / `from:` mapping
36
+ # methods via public_send during (de)serialization — a `private`
37
+ # section here raises NoMethodError on to_hash (matches the public
38
+ # visibility of Oiml::SupplementIdentifier's identical helpers).
39
+ def base_identifier_to_kv(model, doc)
40
+ base = model.base_identifier
41
+ return unless base
42
+
43
+ doc.add_child(
44
+ Lutaml::KeyValue::DataModel::Element.new("base_identifier",
45
+ base.to_hash),
46
+ )
47
+ end
48
+
49
+ def base_identifier_from_kv(model, value)
50
+ return unless value
51
+
52
+ model.base_identifier = ::Pubid::Iala::Identifier.from_hash(value)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Iala
5
+ module Identifiers
6
+ # IALA General Assembly resolutions (GA prefix). Numbering is dotted
7
+ # with a series and an index: GA01.01, GA01.13. No edition.
8
+ class GeneralAssembly < Base
9
+ def self.type
10
+ { key: :"general-assembly", title: "General Assembly Resolution",
11
+ short: "GA" }
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Iala
5
+ module Identifiers
6
+ # IALA Letters (L prefix). Numbering is dotted with a series, sub-series,
7
+ # and item: L2.1.11, L2.7.1-2 (the trailing -2 is a sub-part on top of
8
+ # the dotted number).
9
+ class Letter < Base
10
+ def self.type
11
+ { key: :letter, title: "Letter", short: "L" }
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -3,14 +3,18 @@
3
3
  module Pubid
4
4
  module Iala
5
5
  module Identifiers
6
- autoload :Base, "#{__dir__}/identifiers/base"
7
- autoload :Standard, "#{__dir__}/identifiers/standard"
8
- autoload :Recommendation, "#{__dir__}/identifiers/recommendation"
9
- autoload :Guideline, "#{__dir__}/identifiers/guideline"
10
- autoload :Manual, "#{__dir__}/identifiers/manual"
11
- autoload :ModelCourse, "#{__dir__}/identifiers/model_course"
12
- autoload :Report, "#{__dir__}/identifiers/report"
13
- autoload :Resolution, "#{__dir__}/identifiers/resolution"
6
+ autoload :Base, "#{__dir__}/identifiers/base"
7
+ autoload :Advice, "#{__dir__}/identifiers/advice"
8
+ autoload :Annex, "#{__dir__}/identifiers/annex"
9
+ autoload :Standard, "#{__dir__}/identifiers/standard"
10
+ autoload :Recommendation, "#{__dir__}/identifiers/recommendation"
11
+ autoload :Guideline, "#{__dir__}/identifiers/guideline"
12
+ autoload :Manual, "#{__dir__}/identifiers/manual"
13
+ autoload :ModelCourse, "#{__dir__}/identifiers/model_course"
14
+ autoload :GeneralAssembly, "#{__dir__}/identifiers/general_assembly"
15
+ autoload :Letter, "#{__dir__}/identifiers/letter"
16
+ autoload :Report, "#{__dir__}/identifiers/report"
17
+ autoload :Resolution, "#{__dir__}/identifiers/resolution"
14
18
  end
15
19
  end
16
20
  end
@@ -28,16 +28,32 @@ module Pubid
28
28
  rule(:lparen) { str("(") }
29
29
  rule(:rparen) { str(")") }
30
30
 
31
- # Type letterone of IALA's seven document classes. Captured to
32
- # route to the right Identifiers::* subclass.
31
+ # Type prefix — IALA document classes. GA must be tried before G so it
32
+ # isn't left with a dangling "A". Captured to route to the right
33
+ # Identifiers::* subclass.
33
34
  rule(:type_letter) do
34
- (str("S") | str("R") | str("G") | str("M") |
35
- str("C") | str("X") | str("P")).as(:type_letter)
35
+ (str("GA") | str("S") | str("R") | str("G") | str("M") |
36
+ str("C") | str("A") | str("L") | str("X") | str("P")).as(:type_letter)
36
37
  end
37
38
 
38
- # 4-digit document number, e.g. "1070", "0126".
39
+ # Document number — one or more decimal digits. The dataset historically
40
+ # zero-pads to 4 (S1070, R0126, M0001) but the canonical form going
41
+ # forward is unpadded (M1, GA1.1). Accept any length so both shapes
42
+ # round-trip; the number attribute preserves whatever was parsed.
39
43
  rule(:doc_number) do
40
- match("[0-9]").repeat(4, 4).as(:doc_number)
44
+ match("[0-9]").repeat(1).as(:doc_number)
45
+ end
46
+
47
+ # Dotted continuation that some prefixes use as part of the number:
48
+ # GA01.01 (series.index), L2.1.11 (series.sub.item). Captured into the
49
+ # same :doc_number atom so the number attribute stays a single string
50
+ # (e.g. "01.01", "2.1.11") and the existing builder needs no changes.
51
+ rule(:dotted_number) do
52
+ dot >> match("[0-9]").repeat(1)
53
+ end
54
+
55
+ rule(:number_with_dots) do
56
+ doc_number >> dotted_number.repeat.as(:doc_number_dots)
41
57
  end
42
58
 
43
59
  # Numeric sub-part suffix(es): "-1", "-9-10", "-11". The catalogue
@@ -48,7 +64,7 @@ module Pubid
48
64
  end
49
65
 
50
66
  rule(:code) do
51
- type_letter >> doc_number >> subpart.maybe
67
+ type_letter >> number_with_dots >> subpart.maybe
52
68
  end
53
69
 
54
70
  # Edition forms observed on covers and listing pages:
@@ -83,7 +99,22 @@ module Pubid
83
99
  end
84
100
 
85
101
  rule(:identifier) do
86
- publisher >> code >> edition.maybe >> language.maybe
102
+ annex_identifier | (publisher >> code >> edition.maybe >> language.maybe)
103
+ end
104
+
105
+ # Annex to a base publication. Wraps the base code and adds an
106
+ # "Annex" / "ANNEX" marker (case preserved verbatim), an optional
107
+ # single-letter suffix (A/B/C/D), then the usual edition + language.
108
+ # Tried before the flat `identifier` so the annex marker isn't left
109
+ # dangling after a successful base-code match.
110
+ #
111
+ # The letter suffix uses a negative lookahead for a following lowercase
112
+ # letter so the "E" in "Ed 1" isn't mis-captured as annex letter "E".
113
+ rule(:annex_identifier) do
114
+ (publisher >> type_letter >> number_with_dots >> subpart.maybe).as(:base) >>
115
+ space >> (str("Annex") | str("ANNEX")).as(:annex_marker) >>
116
+ (space >> match("[A-Z]").as(:annex_letter) >> match("[a-z]").absent?).maybe >>
117
+ edition.maybe >> language.maybe
87
118
  end
88
119
 
89
120
  # Parse a string and return the raw parslet tree.