pubid 2.0.0.pre.alpha.3 → 2.0.0.pre.alpha.4

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 (151) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pubid/amca/identifier.rb +4 -23
  3. data/lib/pubid/amca/identifiers/base.rb +67 -30
  4. data/lib/pubid/amca/identifiers/interpretation.rb +0 -4
  5. data/lib/pubid/amca/identifiers/publication.rb +0 -4
  6. data/lib/pubid/amca/identifiers/standard.rb +0 -4
  7. data/lib/pubid/amca/renderer.rb +1 -1
  8. data/lib/pubid/amca/urn_generator.rb +2 -1
  9. data/lib/pubid/api/builder.rb +4 -0
  10. data/lib/pubid/api/identifier.rb +5 -1
  11. data/lib/pubid/api/identifiers/base.rb +2 -12
  12. data/lib/pubid/api/identifiers/mpms.rb +0 -4
  13. data/lib/pubid/api/identifiers/typeless_standard.rb +0 -3
  14. data/lib/pubid/api/single_identifier.rb +5 -8
  15. data/lib/pubid/ashrae/identifier.rb +7 -22
  16. data/lib/pubid/ashrae/identifiers/base.rb +28 -17
  17. data/lib/pubid/ashrae/parser.rb +4 -0
  18. data/lib/pubid/ashrae.rb +4 -0
  19. data/lib/pubid/asme/single_identifier.rb +3 -1
  20. data/lib/pubid/astm/single_identifier.rb +3 -1
  21. data/lib/pubid/bsi/identifier.rb +5 -31
  22. data/lib/pubid/bsi/identifiers/addendum_document.rb +0 -4
  23. data/lib/pubid/bsi/identifiers/adopted_european_norm.rb +0 -4
  24. data/lib/pubid/bsi/identifiers/adopted_international_standard.rb +0 -4
  25. data/lib/pubid/bsi/identifiers/aerospace_standard.rb +0 -3
  26. data/lib/pubid/bsi/identifiers/amendment.rb +0 -4
  27. data/lib/pubid/bsi/identifiers/british_industrial_practice.rb +0 -3
  28. data/lib/pubid/bsi/identifiers/bundled_identifier.rb +2 -5
  29. data/lib/pubid/bsi/identifiers/committee_document.rb +0 -3
  30. data/lib/pubid/bsi/identifiers/consolidated_identifier.rb +2 -5
  31. data/lib/pubid/bsi/identifiers/corrigendum.rb +0 -4
  32. data/lib/pubid/bsi/identifiers/detailed_specification.rb +0 -3
  33. data/lib/pubid/bsi/identifiers/disc.rb +0 -3
  34. data/lib/pubid/bsi/identifiers/draft_document.rb +0 -3
  35. data/lib/pubid/bsi/identifiers/electronic_book.rb +0 -3
  36. data/lib/pubid/bsi/identifiers/expert_commentary.rb +0 -4
  37. data/lib/pubid/bsi/identifiers/explanatory_supplement.rb +0 -3
  38. data/lib/pubid/bsi/identifiers/flex.rb +0 -3
  39. data/lib/pubid/bsi/identifiers/handbook.rb +0 -3
  40. data/lib/pubid/bsi/identifiers/index.rb +0 -3
  41. data/lib/pubid/bsi/identifiers/method.rb +0 -3
  42. data/lib/pubid/bsi/identifiers/national_annex.rb +0 -3
  43. data/lib/pubid/bsi/identifiers/practice_guide.rb +0 -3
  44. data/lib/pubid/bsi/identifiers/publicly_available_specification.rb +0 -3
  45. data/lib/pubid/bsi/identifiers/published_document.rb +0 -3
  46. data/lib/pubid/bsi/identifiers/section.rb +0 -3
  47. data/lib/pubid/bsi/identifiers/set.rb +2 -5
  48. data/lib/pubid/bsi/identifiers/standalone_amendment.rb +0 -3
  49. data/lib/pubid/bsi/identifiers/supplement_document.rb +0 -4
  50. data/lib/pubid/bsi/identifiers/supplementary_index.rb +0 -3
  51. data/lib/pubid/bsi/identifiers/technical_specification.rb +0 -3
  52. data/lib/pubid/bsi/identifiers/test_method.rb +0 -3
  53. data/lib/pubid/bsi/identifiers/value_added_publication.rb +0 -4
  54. data/lib/pubid/bsi/single_identifier.rb +32 -13
  55. data/lib/pubid/bsi.rb +8 -1
  56. data/lib/pubid/ccsds/identifier.rb +2 -14
  57. data/lib/pubid/cen_cenelec/identifier.rb +10 -8
  58. data/lib/pubid/cen_cenelec/identifiers/adopted_european_norm.rb +0 -4
  59. data/lib/pubid/cen_cenelec/identifiers/amendment.rb +0 -4
  60. data/lib/pubid/cen_cenelec/identifiers/base.rb +2 -9
  61. data/lib/pubid/cen_cenelec/identifiers/consolidated_identifier.rb +0 -4
  62. data/lib/pubid/cen_cenelec/identifiers/corrigendum.rb +0 -4
  63. data/lib/pubid/cen_cenelec/identifiers/european_prestandard.rb +0 -3
  64. data/lib/pubid/cen_cenelec/identifiers/fragment.rb +0 -4
  65. data/lib/pubid/cen_cenelec/single_identifier.rb +4 -7
  66. data/lib/pubid/cen_cenelec.rb +4 -0
  67. data/lib/pubid/cie/identifier.rb +4 -0
  68. data/lib/pubid/cie/single_identifier.rb +4 -4
  69. data/lib/pubid/cie.rb +4 -0
  70. data/lib/pubid/csa/identifier.rb +10 -1
  71. data/lib/pubid/csa/identifiers/base.rb +0 -3
  72. data/lib/pubid/csa/identifiers/cec.rb +0 -4
  73. data/lib/pubid/csa/identifiers/series.rb +0 -4
  74. data/lib/pubid/csa/single_identifier.rb +1 -1
  75. data/lib/pubid/csa.rb +4 -0
  76. data/lib/pubid/etsi/components/code.rb +1 -7
  77. data/lib/pubid/etsi/components/version.rb +0 -5
  78. data/lib/pubid/etsi/identifier.rb +4 -12
  79. data/lib/pubid/etsi/identifiers/base.rb +31 -24
  80. data/lib/pubid/etsi/identifiers/supplement_identifier.rb +0 -3
  81. data/lib/pubid/etsi.rb +3 -1
  82. data/lib/pubid/identifier.rb +90 -1
  83. data/lib/pubid/idf/identifier.rb +4 -0
  84. data/lib/pubid/idf.rb +4 -0
  85. data/lib/pubid/iec/identifier.rb +14 -14
  86. data/lib/pubid/iec/identifiers/base.rb +0 -4
  87. data/lib/pubid/iec/identifiers/consolidated_identifier.rb +0 -4
  88. data/lib/pubid/iec/identifiers/fragment_identifier.rb +1 -5
  89. data/lib/pubid/iec/identifiers/sheet_identifier.rb +1 -5
  90. data/lib/pubid/iec/identifiers/test_report_form.rb +0 -3
  91. data/lib/pubid/iec/identifiers/vap_identifier.rb +1 -5
  92. data/lib/pubid/iec/identifiers/working_document.rb +0 -4
  93. data/lib/pubid/iec.rb +4 -0
  94. data/lib/pubid/ieee/identifier.rb +5 -17
  95. data/lib/pubid/ieee/identifiers/adopted_standard.rb +0 -4
  96. data/lib/pubid/ieee/identifiers/base.rb +223 -201
  97. data/lib/pubid/ieee/identifiers/conformance_identifier.rb +0 -4
  98. data/lib/pubid/ieee/identifiers/corrigendum.rb +0 -4
  99. data/lib/pubid/ieee/identifiers/csa_dual_published.rb +0 -4
  100. data/lib/pubid/ieee/identifiers/dual_identifier.rb +0 -4
  101. data/lib/pubid/ieee/identifiers/dual_published.rb +0 -4
  102. data/lib/pubid/ieee/identifiers/iec_ieee_copublished.rb +0 -4
  103. data/lib/pubid/ieee/identifiers/interpretation_identifier.rb +0 -4
  104. data/lib/pubid/ieee/identifiers/multi_numbered_identifier.rb +0 -4
  105. data/lib/pubid/ieee/identifiers/parenthetical_identifier.rb +0 -4
  106. data/lib/pubid/ieee/identifiers/redlined_standard.rb +0 -4
  107. data/lib/pubid/ieee/identifiers/si_standard.rb +0 -4
  108. data/lib/pubid/ieee.rb +4 -0
  109. data/lib/pubid/iho/identifier.rb +7 -25
  110. data/lib/pubid/iho/identifiers/base.rb +62 -68
  111. data/lib/pubid/iho.rb +3 -1
  112. data/lib/pubid/iso/identifier.rb +15 -14
  113. data/lib/pubid/iso/single_identifier.rb +0 -4
  114. data/lib/pubid/iso.rb +4 -0
  115. data/lib/pubid/itu/components/code.rb +1 -7
  116. data/lib/pubid/itu/components/sector.rb +15 -4
  117. data/lib/pubid/itu/components/series.rb +0 -4
  118. data/lib/pubid/itu/identifier.rb +4 -18
  119. data/lib/pubid/itu/identifiers/base.rb +124 -122
  120. data/lib/pubid/itu.rb +3 -1
  121. data/lib/pubid/jcgm/identifiers/amendment.rb +0 -4
  122. data/lib/pubid/jcgm/identifiers/gum_guide.rb +0 -4
  123. data/lib/pubid/jcgm/single_identifier.rb +0 -4
  124. data/lib/pubid/jis/identifier.rb +2 -14
  125. data/lib/pubid/nist/identifier.rb +8 -23
  126. data/lib/pubid/nist/identifiers/base.rb +553 -544
  127. data/lib/pubid/nist/preprocessor.rb +12 -0
  128. data/lib/pubid/nist.rb +4 -0
  129. data/lib/pubid/oiml/builder.rb +21 -3
  130. data/lib/pubid/oiml/components/code.rb +7 -0
  131. data/lib/pubid/oiml/identifier.rb +44 -0
  132. data/lib/pubid/oiml/identifiers/annex.rb +10 -1
  133. data/lib/pubid/oiml/identifiers/errata.rb +15 -0
  134. data/lib/pubid/oiml/identifiers.rb +1 -1
  135. data/lib/pubid/oiml/parser.rb +59 -12
  136. data/lib/pubid/oiml/renderer.rb +27 -17
  137. data/lib/pubid/oiml/single_identifier.rb +58 -0
  138. data/lib/pubid/oiml/supplement_identifier.rb +30 -1
  139. data/lib/pubid/plateau/identifier.rb +4 -21
  140. data/lib/pubid/plateau/identifiers/base.rb +45 -36
  141. data/lib/pubid/plateau/identifiers/handbook.rb +0 -4
  142. data/lib/pubid/plateau/identifiers/technical_report.rb +0 -4
  143. data/lib/pubid/plateau/supplement_identifier.rb +0 -4
  144. data/lib/pubid/sae/identifier.rb +4 -12
  145. data/lib/pubid/sae/identifiers/base.rb +26 -17
  146. data/lib/pubid/version.rb +1 -1
  147. data/lib/pubid.rb +18 -2
  148. data/lib/tasks/export.rake +6 -5
  149. metadata +3 -4
  150. data/lib/pubid/identifier_facade.rb +0 -114
  151. data/lib/pubid/oiml/identifiers/base.rb +0 -21
@@ -53,6 +53,7 @@ module Pubid
53
53
  # Extracted so rubocop can scope length/ABC metrics narrowly.
54
54
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
55
55
  def run_stages
56
+ normalize_spurious_u_suffix!
56
57
  normalize_publisher_and_series!
57
58
  normalize_lcirc_supplement_contexts!
58
59
  normalize_revision_spacing!
@@ -84,6 +85,17 @@ module Pubid
84
85
 
85
86
  private
86
87
 
88
+ # Strip the spurious "U" a bad v2 data migration prefixed onto real
89
+ # letter/revision suffixes (e.g. "800-38a" → "800-38Ua",
90
+ # "73-197r" → "73-197Ur"). NIST's authoritative records
91
+ # (allrecords.xml, DOIs) carry no such "U"; it exists only in the
92
+ # migrated corpus. Removing it here lets the remainder parse through
93
+ # the normal letter-suffix / revision path — "U<letter>" is never a
94
+ # real NIST suffix, so this only ever undoes the corruption.
95
+ def normalize_spurious_u_suffix!
96
+ @cleaned = @cleaned.gsub(/(\d)U([a-z])/, '\1\2')
97
+ end
98
+
87
99
  # Lowercase publishers, publisher+series concatenations, lowercase
88
100
  # series codes, and the lone "LC" → "LCIRC" expansion.
89
101
  def normalize_publisher_and_series!
data/lib/pubid/nist.rb CHANGED
@@ -53,6 +53,10 @@ module Pubid
53
53
  # @param identifier [String] the identifier string to parse
54
54
  # @return [Identifiers::Base] the parsed identifier
55
55
  def self.parse(identifier)
56
+ if identifier.length > Pubid::MAX_INPUT_LENGTH
57
+ raise ArgumentError, Pubid::INPUT_TOO_LONG_MESSAGE
58
+ end
59
+
56
60
  # Use the Parser class's preprocessing method
57
61
  # Note: We call the class method directly to ensure preprocessing is applied
58
62
  parsed = Parser.class_parse_with_preprocessing(identifier)
@@ -77,14 +77,21 @@ module Pubid
77
77
  end
78
78
 
79
79
  def build_supplement(parsed_hash)
80
- # Determine supplement type
80
+ marker = parsed_hash[:trailing_marker].to_s if parsed_hash[:trailing_marker]
81
+
82
+ # Determine supplement type. The trailing word ("Amendment"/"Errata")
83
+ # selects the class; the concrete class then carries the word via
84
+ # #supplement_type, so only the `trailing` flag needs storing.
81
85
  supplement_class = if parsed_hash[:annex_letter] || parsed_hash[:annex_marker]
82
86
  Identifiers::Annex
87
+ elsif marker == "Errata"
88
+ Identifiers::Errata
83
89
  else
84
90
  Identifiers::Amendment
85
91
  end
86
92
 
87
93
  supplement = supplement_class.new
94
+ supplement.trailing = true if marker
88
95
 
89
96
  # Recursively parse base identifier
90
97
  if parsed_hash[:base_identifier]
@@ -103,6 +110,13 @@ module Pubid
103
110
  supplement.language = extract_language(parsed_hash[:language]) if parsed_hash[:language]
104
111
  supplement.letter = parsed_hash[:annex_letter].to_s if parsed_hash[:annex_letter]
105
112
 
113
+ # Annex with no year of its own but a dated base ("R 60:2017 Annexes"):
114
+ # the year belongs to the base and must render glued to it.
115
+ if supplement.is_a?(Identifiers::Annex) && !year_value &&
116
+ supplement.base_identifier&.date
117
+ supplement.year_on_base = true
118
+ end
119
+
106
120
  # Track if supplement itself was parsed with Edition format
107
121
  if parsed_hash[:edition_format]
108
122
  supplement.parsed_format = "long"
@@ -119,8 +133,8 @@ module Pubid
119
133
 
120
134
  identifier = identifier_class.new
121
135
 
122
- # Handle code (number-part-subpart) specially
123
- if parsed_hash[:number] || parsed_hash[:part] || parsed_hash[:subpart]
136
+ # Handle code (number-part-subpart-suffix) specially
137
+ if parsed_hash[:number] || parsed_hash[:part] || parsed_hash[:subpart] || parsed_hash[:code_suffix]
124
138
  code_attrs = {}
125
139
  if parsed_hash[:number]
126
140
  code_attrs[:number] =
@@ -131,6 +145,10 @@ module Pubid
131
145
  code_attrs[:subpart] =
132
146
  parsed_hash[:subpart].to_s
133
147
  end
148
+ if parsed_hash[:code_suffix]
149
+ code_attrs[:suffix] = parsed_hash[:code_suffix].to_s
150
+ code_attrs[:space_suffix] = true if parsed_hash.key?(:space_suffix)
151
+ end
134
152
  identifier.code = Components::Code.new(**code_attrs)
135
153
  end
136
154
 
@@ -13,11 +13,18 @@ module Pubid
13
13
  attribute :number, :string
14
14
  attribute :part, :string
15
15
  attribute :subpart, :string
16
+ # Free-form trailing suffix glued to the code, e.g. "sup", "A",
17
+ # "erratum", "GUM 1", "ISO3930". Preserved verbatim for round-trip.
18
+ attribute :suffix, :string
19
+ # When true the suffix is space-separated ("D 1 Brochure") rather than
20
+ # the default dash ("R 60-sup").
21
+ attribute :space_suffix, :boolean, default: false
16
22
 
17
23
  def to_s
18
24
  result = number.to_s
19
25
  result += "-#{part}" if part
20
26
  result += "-#{subpart}" if subpart
27
+ result += "#{space_suffix ? ' ' : '-'}#{suffix}" if suffix
21
28
  result
22
29
  end
23
30
 
@@ -3,6 +3,50 @@
3
3
  module Pubid
4
4
  module Oiml
5
5
  class Identifier < Pubid::Identifier
6
+ # Maps each concrete identifier's polymorphic_name => class name, so
7
+ # key_value (de)serialization can re-instantiate the correct subclass
8
+ # from the `_type` discriminator. Kept in sync with the Identifiers
9
+ # namespace by a spec assertion (see spec/pubid/oiml/to_hash_spec.rb).
10
+ OIML_TYPE_MAP = {
11
+ "pubid:oiml:recommendation" => "Pubid::Oiml::Identifiers::Recommendation",
12
+ "pubid:oiml:basic-publication" => "Pubid::Oiml::Identifiers::BasicPublication",
13
+ "pubid:oiml:document" => "Pubid::Oiml::Identifiers::Document",
14
+ "pubid:oiml:guide" => "Pubid::Oiml::Identifiers::Guide",
15
+ "pubid:oiml:vocabulary" => "Pubid::Oiml::Identifiers::Vocabulary",
16
+ "pubid:oiml:expert-report" => "Pubid::Oiml::Identifiers::ExpertReport",
17
+ "pubid:oiml:seminar-report" => "Pubid::Oiml::Identifiers::SeminarReport",
18
+ "pubid:oiml:amendment" => "Pubid::Oiml::Identifiers::Amendment",
19
+ "pubid:oiml:errata" => "Pubid::Oiml::Identifiers::Errata",
20
+ "pubid:oiml:annex" => "Pubid::Oiml::Identifiers::Annex",
21
+ }.freeze
22
+
23
+ # The base Pubid::Identifier no longer auto-maps attributes, so OIML must
24
+ # declare its own key_value mapping. SingleIdentifier and
25
+ # SupplementIdentifier (siblings) add their disjoint attribute deltas on
26
+ # top of this shared block. `type` is deliberately NOT mapped: OIML's
27
+ # `type` is a String-returning method (e.g. "R"), incompatible with the
28
+ # inherited Components::Type attribute — and the `_type` discriminator
29
+ # already pins the concrete subclass (and thus the type letter).
30
+ key_value do
31
+ map "_type", to: :_type, polymorphic_map: OIML_TYPE_MAP
32
+ map "language", to: :language
33
+ map "parsed_format", to: :parsed_format
34
+ end
35
+
36
+ # lutaml's polymorphic_map only validates `_type` on deserialization; it
37
+ # does not re-instantiate the concrete subclass. Route by `_type` so
38
+ # `Pubid::Oiml::Identifier.from_hash(h)` returns the right class (and its
39
+ # nested base_identifier), mirroring Pubid::Iso::Identifier.from_hash.
40
+ def self.from_hash(data, options = {})
41
+ type = data["_type"] || data[:_type]
42
+ klass_name = OIML_TYPE_MAP[type]
43
+ if klass_name
44
+ klass = Object.const_get(klass_name)
45
+ return klass.from_hash(data, options) unless klass == self
46
+ end
47
+ super
48
+ end
49
+
6
50
  def to_urn
7
51
  UrnGenerator.new(self).generate
8
52
  end
@@ -4,7 +4,16 @@ module Pubid
4
4
  module Oiml
5
5
  module Identifiers
6
6
  class Annex < SupplementIdentifier
7
- attribute :letter, :string # For "Annex A", "Annex B", etc.
7
+ attribute :letter, :string # For "Annex A", "Annex B", "Annex B-C"
8
+ # True when the publication year is glued to the base ("OIML R 60:2017
9
+ # Annexes") rather than the marker ("OIML R 60 Annexes:2017"); drives
10
+ # exact round-trip in the renderer.
11
+ attribute :year_on_base, :boolean, default: false
12
+
13
+ key_value do
14
+ map "letter", to: :letter
15
+ map "year_on_base", to: :year_on_base
16
+ end
8
17
 
9
18
  def supplement_type
10
19
  letter ? "Annex #{letter}" : "Annexes"
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Oiml
5
+ module Identifiers
6
+ # Errata supplement, used by the trailing-word shorthand
7
+ # "OIML R 126:2015 Errata" (the publication year lives on the base).
8
+ class Errata < SupplementIdentifier
9
+ def supplement_type
10
+ "Errata"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -5,9 +5,9 @@ module Pubid
5
5
  module Identifiers
6
6
  autoload :Amendment, "#{__dir__}/identifiers/amendment"
7
7
  autoload :Annex, "#{__dir__}/identifiers/annex"
8
- autoload :Base, "#{__dir__}/identifiers/base"
9
8
  autoload :BasicPublication, "#{__dir__}/identifiers/basic_publication"
10
9
  autoload :Document, "#{__dir__}/identifiers/document"
10
+ autoload :Errata, "#{__dir__}/identifiers/errata"
11
11
  autoload :ExpertReport, "#{__dir__}/identifiers/expert_report"
12
12
  autoload :Guide, "#{__dir__}/identifiers/guide"
13
13
  autoload :Recommendation, "#{__dir__}/identifiers/recommendation"
@@ -18,7 +18,8 @@ module Pubid
18
18
 
19
19
  # Main identifier pattern - check supplements first
20
20
  rule(:identifier) do
21
- amendment_identifier | amendment_short | annex_letter_identifier | annex_identifier | base_identifier
21
+ amendment_identifier | amendment_short | annex_letter_identifier |
22
+ annex_identifier | trailing_supplement_identifier | base_identifier
22
23
  end
23
24
 
24
25
  # Publisher - always "OIML"
@@ -30,14 +31,33 @@ module Pubid
30
31
  # Number with optional part and subpart
31
32
  rule(:number_only) { digits.as(:number) }
32
33
 
33
- rule(:part_number) { dash >> digits.as(:part) }
34
+ # Part number, optionally a slashed multi-part continuation captured
35
+ # verbatim, e.g. "1/-2" in "OIML R 46-1/-2:2012".
36
+ rule(:part_number) do
37
+ dash >> (digits >> (slash >> dash >> digits).repeat).as(:part)
38
+ end
34
39
 
35
40
  rule(:subpart_number) { dash >> digits.as(:subpart) }
36
41
 
42
+ # Free-form named/lettered code suffix glued after the numeric part:
43
+ # -GUM 1, -special, -sup, -erratum, -A, -ISO3930, -Amend, -Amended_2012
44
+ # plus the space-separated "Brochure" label. "GUM <n>" is tried first so
45
+ # its trailing number isn't lost to the bare-word alternative. The
46
+ # trailing `(_?digits)*` captures joint codes ("ISO3930") and the
47
+ # "Amended_2012" amendment label (the hand-off treats these as part
48
+ # suffixes, preserved verbatim for round-trip).
49
+ rule(:named_suffix) do
50
+ (dash >> (
51
+ (str("GUM") >> space >> digits) |
52
+ (match("[A-Za-z]").repeat(1) >> (str("_").maybe >> digits).repeat)
53
+ ).as(:code_suffix)) |
54
+ (space >> str("Brochure").as(:code_suffix) >> str("").as(:space_suffix))
55
+ end
56
+
37
57
  rule(:full_number) do
38
- (number_only >> part_number >> subpart_number) |
39
- (number_only >> part_number) |
40
- number_only
58
+ (number_only >> part_number >> subpart_number >> named_suffix.maybe) |
59
+ (number_only >> part_number >> named_suffix.maybe) |
60
+ (number_only >> named_suffix.maybe)
41
61
  end
42
62
 
43
63
  # Edition number - ordinal numbers
@@ -82,8 +102,18 @@ module Pubid
82
102
  end
83
103
 
84
104
  # Language codes
105
+ #
106
+ # relaton-data-oiml encodes the publication language with its own code
107
+ # set (lib/oiml_fetcher.rb DOCID_LANG_CODE): single uppercase letters
108
+ # E F D R S C A U X, plus two-letter PO/PT/PE/SR. The two-letter OIML
109
+ # codes must be tried before the single-letter rule so e.g. "PE" isn't
110
+ # left with a dangling "E".
85
111
  rule(:lang_single) do
86
- match("[EFRX]") # Single letter: E, F, R, X
112
+ match("[EFRXDSCAU]")
113
+ end
114
+
115
+ rule(:lang_multi_oiml) do
116
+ str("PO") | str("PT") | str("PE") | str("SR")
87
117
  end
88
118
 
89
119
  rule(:lang_multi) do
@@ -93,8 +123,9 @@ module Pubid
93
123
  rule(:language_code) do
94
124
  (
95
125
  (lang_single >> slash >> lang_single) | # E/F
96
- lang_single | # E, F
97
- lang_multi # en, fr
126
+ lang_multi_oiml | # PO, PT, PE, SR
127
+ lang_single | # E, F, D, R, S, C, A, U, X
128
+ lang_multi # en, fr
98
129
  ).as(:language)
99
130
  end
100
131
 
@@ -131,21 +162,37 @@ module Pubid
131
162
  language_portion.maybe.as(:language)
132
163
  end
133
164
 
134
- # Annex identifier - "BASE Annexes Edition YYYY" or "BASE Annexes:YYYY"
165
+ # Trailing supplement word - "BASE Amendment" / "BASE Errata" where the
166
+ # publication year stays on the base (e.g. "OIML R 138:2009 Amendment").
167
+ # amendment_short is tried first; it only matches when a year follows the
168
+ # word, so the no-year trailing form falls through to here.
169
+ rule(:trailing_supplement_identifier) do
170
+ base_without_language.as(:base_identifier) >>
171
+ space >> (str("Amendment") | str("Errata")).as(:trailing_marker) >>
172
+ language_portion.maybe.as(:language)
173
+ end
174
+
175
+ # Annex identifier - "BASE Annexes Edition YYYY" / "BASE Annexes:YYYY" /
176
+ # "BASE:YYYY Annexes" (year on the base, no annex year).
135
177
  rule(:annex_identifier) do
136
178
  base_without_language.as(:base_identifier) >>
137
179
  space >> str("Annexes").as(:annex_marker) >>
138
180
  (
139
181
  (space >> edition_text >> space >> year_digits.as(:year)).as(:edition_format) |
140
182
  (colon >> year_digits.as(:year))
141
- ) >>
183
+ ).maybe >>
142
184
  language_portion.maybe.as(:language)
143
185
  end
144
186
 
145
- # Annex with letter - "BASE Annex A Edition YYYY"
187
+ # Annex letter or letter range, e.g. "A" or "B-C".
188
+ rule(:annex_letter_value) do
189
+ (match("[A-Z]") >> (dash >> match("[A-Z]")).maybe).as(:annex_letter)
190
+ end
191
+
192
+ # Annex with letter - "BASE Annex A Edition YYYY" / "BASE:YYYY Annex B-C"
146
193
  rule(:annex_letter_identifier) do
147
194
  base_without_language.as(:base_identifier) >>
148
- space >> str("Annex") >> space >> match("[A-Z]").as(:annex_letter) >>
195
+ space >> str("Annex") >> space >> annex_letter_value >>
149
196
  ((space >> edition_text >> space >> year_digits.as(:year)) | (colon >> year_digits.as(:year))).maybe >>
150
197
  language_portion.maybe.as(:language)
151
198
  end
@@ -24,8 +24,6 @@ module Pubid
24
24
  render_supplement(id)
25
25
  when SingleIdentifier
26
26
  render_single(id)
27
- when Identifiers::Base
28
- render_base_identifier(id)
29
27
  else
30
28
  id.to_s
31
29
  end
@@ -38,6 +36,12 @@ module Pubid
38
36
  (id.parsed_format == "long" ? :long : :short)
39
37
  end
40
38
 
39
+ # Strip a trailing language group "(...)" from a rendered base string so
40
+ # the supplement can re-attach its own language at the end.
41
+ def strip_language(str)
42
+ str.sub(/\s*\([^)]+\)\s*$/, "").strip
43
+ end
44
+
41
45
  def render_single(id)
42
46
  format = effective_format(id)
43
47
 
@@ -80,6 +84,16 @@ module Pubid
80
84
  def render_supplement(id)
81
85
  format = effective_format(id)
82
86
 
87
+ # Trailing-word shorthand: "BASE Amendment" / "BASE Errata" with the
88
+ # publication year kept on the base identifier. The word comes from the
89
+ # concrete supplement class.
90
+ if id.trailing
91
+ base_str = strip_language(id.base_identifier.to_s)
92
+ result = "#{base_str} #{id.supplement_type}"
93
+ result += " (#{id.language})" if id.language
94
+ return result
95
+ end
96
+
83
97
  base_format = if format && format != :short
84
98
  format
85
99
  elsif id.base_identifier.class.attributes.key?(:parsed_format) && id.base_identifier.parsed_format == "long"
@@ -93,7 +107,7 @@ module Pubid
93
107
  else
94
108
  id.base_identifier.to_s
95
109
  end
96
- base_str = base_str.sub(/\s*\([^)]+\)\s*$/, "").strip
110
+ base_str = strip_language(base_str)
97
111
 
98
112
  result = "#{id.supplement_type} (#{id.year}) to #{base_str}"
99
113
  result += " (#{id.language})" if id.language
@@ -104,6 +118,16 @@ module Pubid
104
118
  def render_annex(id)
105
119
  format = effective_format(id)
106
120
 
121
+ # "BASE:YYYY Annex(es)" — the year is glued to the base, the marker
122
+ # carries none. Keep the base date instead of stripping it.
123
+ if id.year_on_base
124
+ base_str = strip_language(id.base_identifier.to_s)
125
+ marker = id.letter ? "Annex #{id.letter}" : "Annexes"
126
+ result = "#{base_str} #{marker}"
127
+ result += " (#{id.language})" if id.language
128
+ return result
129
+ end
130
+
107
131
  base_format = if id.base_identifier.class.attributes.key?(:parsed_format) && id.base_identifier.parsed_format == "long"
108
132
  :long
109
133
  else
@@ -142,20 +166,6 @@ module Pubid
142
166
  result += " (#{id.language})" if id.language
143
167
  result
144
168
  end
145
-
146
- def render_base_identifier(id)
147
- result = "#{id.publisher} #{id.type} #{id.code}"
148
- result += ":#{id.date.render(context: @context)}" if id.date
149
-
150
- if id.stage || id.iteration
151
- result += " "
152
- result += id.iteration.to_s if id.iteration
153
- result += id.stage.to_s if id.stage
154
- end
155
-
156
- result += "(#{id.language})" if id.language
157
- result
158
- end
159
169
  end
160
170
  end
161
171
  end
@@ -15,6 +15,64 @@ module Pubid
15
15
  "short"
16
16
  } # Track parsed format
17
17
 
18
+ # Serialization delta on top of Oiml::Identifier's shared block. The
19
+ # `code` (number/part/subpart/suffix) and `date` (year) components are
20
+ # flattened to top-level keys rather than nested hashes, mirroring ISO
21
+ # (lib/pubid/iso/identifier.rb). `type` is intentionally omitted
22
+ # (recomputed from the class on load).
23
+ key_value do
24
+ map "publisher", to: :publisher
25
+ map "number", with: { to: :number_to_kv, from: :number_from_kv }
26
+ map "part", with: { to: :part_to_kv, from: :part_from_kv }
27
+ map "subpart", with: { to: :subpart_to_kv, from: :subpart_from_kv }
28
+ map "suffix", with: { to: :suffix_to_kv, from: :suffix_from_kv }
29
+ map "space_suffix",
30
+ with: { to: :space_suffix_to_kv, from: :space_suffix_from_kv }
31
+ map "year", with: { to: :year_to_kv, from: :year_from_kv }
32
+ map "edition", to: :edition
33
+ map "stage", to: :stage
34
+ map "iteration", to: :iteration
35
+ end
36
+
37
+ # --- code components flattened to top-level keys ---
38
+ def number_to_kv(model, doc) = emit_kv(doc, "number", model.code&.number)
39
+ def number_from_kv(model, value) = code_for(model).number = value.to_s
40
+ def part_to_kv(model, doc) = emit_kv(doc, "part", model.code&.part)
41
+ def part_from_kv(model, value) = code_for(model).part = value.to_s
42
+ def subpart_to_kv(model, doc) = emit_kv(doc, "subpart", model.code&.subpart)
43
+ def subpart_from_kv(model, value) = code_for(model).subpart = value.to_s
44
+ def suffix_to_kv(model, doc) = emit_kv(doc, "suffix", model.code&.suffix)
45
+ def suffix_from_kv(model, value) = code_for(model).suffix = value.to_s
46
+
47
+ def space_suffix_to_kv(model, doc)
48
+ return unless model.code&.space_suffix
49
+
50
+ doc.add_child(
51
+ Lutaml::KeyValue::DataModel::Element.new("space_suffix", true),
52
+ )
53
+ end
54
+
55
+ def space_suffix_from_kv(model, value)
56
+ code_for(model).space_suffix = value
57
+ end
58
+
59
+ # --- date flattened to a top-level year ---
60
+ def year_to_kv(model, doc) = emit_kv(doc, "year", model.date&.year)
61
+
62
+ def year_from_kv(model, value)
63
+ (model.date ||= Pubid::Components::Date.new).year = value.to_s
64
+ end
65
+
66
+ def emit_kv(doc, key, value)
67
+ return if value.nil? || value.to_s.empty?
68
+
69
+ doc.add_child(Lutaml::KeyValue::DataModel::Element.new(key, value.to_s))
70
+ end
71
+
72
+ def code_for(model)
73
+ model.code ||= Components::Code.new
74
+ end
75
+
18
76
  attr_reader :requested_format
19
77
 
20
78
  # Type is determined by the subclass
@@ -5,13 +5,42 @@ module Pubid
5
5
  class SupplementIdentifier < Identifier
6
6
  # Base class for OIML supplements (amendments, annexes)
7
7
  # These wrap a base identifier like ISO amendments
8
- attribute :base_identifier, Oiml::Identifier
8
+ attribute :base_identifier, Oiml::Identifier, polymorphic: true
9
9
  attribute :year, :string
10
10
  attribute :language, :string
11
+ # True for the trailing-word shorthand ("OIML R 138:2009 Amendment"),
12
+ # where the supplement word is appended after a dated base instead of the
13
+ # "Amendment (YYYY) to BASE" prose form. The word itself comes from the
14
+ # concrete class (#supplement_type), so only this flag is stored.
15
+ attribute :trailing, :boolean, default: false
11
16
  attribute :parsed_format, :string, default: -> {
12
17
  "short"
13
18
  } # Track supplement's parsed format
14
19
 
20
+ # Serialization delta on top of Oiml::Identifier's shared block. The
21
+ # nested base_identifier is (de)serialized recursively through the
22
+ # polymorphic router so its own `_type` selects the right subclass.
23
+ key_value do
24
+ map "base_identifier",
25
+ with: { to: :base_identifier_to_kv, from: :base_identifier_from_kv }
26
+ map "year", to: :year
27
+ map "trailing", to: :trailing
28
+ end
29
+
30
+ def base_identifier_to_kv(model, doc)
31
+ base = model.base_identifier
32
+ return unless base
33
+
34
+ doc.add_child(
35
+ Lutaml::KeyValue::DataModel::Element.new("base_identifier",
36
+ base.to_hash),
37
+ )
38
+ end
39
+
40
+ def base_identifier_from_kv(model, value)
41
+ model.base_identifier = ::Pubid::Oiml::Identifier.from_hash(value) if value
42
+ end
43
+
15
44
  attr_reader :requested_format
16
45
 
17
46
  def to_s(format: nil, **opts)
@@ -1,23 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Pubid
4
- module Plateau
5
- # Plateau factory entry point. `.parse` lives on `Pubid::Plateau`
6
- # itself for historical reasons; this module hosts `.create` for API
7
- # consistency with the other pubid flavors.
8
- #
9
- # NOTE: intentionally does NOT `extend Pubid::IdentifierFacade`. PLATEAU's
10
- # `to_hash` currently raises (publisher stored as a String, not a Publisher
11
- # component), so enabling the facade's identity check would route PLATEAU
12
- # ids through a consumer's `to_hash` (e.g. relaton-index#save) and crash.
13
- # Re-enable once PLATEAU serialization round-trips cleanly. See
14
- # identifier_facade.rb.
15
- module Identifier
16
- # Delegate to the flavor module so callers can use
17
- # `Pubid::Plateau::Identifier.parse` consistently with other flavors.
18
- def self.parse(identifier)
19
- Pubid::Plateau.parse(identifier)
20
- end
21
- end
22
- end
23
- end
3
+ # Pubid::Plateau::Identifier is the PLATEAU base class (a real Pubid::Identifier
4
+ # subclass); its body, `.parse`, and the Identifiers::Base back-compat alias
5
+ # live in identifiers/base.rb. This file just ensures it is loaded.
6
+ require_relative "identifiers/base"
@@ -2,50 +2,59 @@
2
2
 
3
3
  module Pubid
4
4
  module Plateau
5
- module Identifiers
6
- # Base class for all PLATEAU identifiers
7
- class Base < Pubid::Identifier
8
- # Generate URN for this identifier
9
- #
10
- # @return [String] URN representation
11
- attribute :number, :integer
12
- attribute :annex, :integer, default: -> {}
13
-
14
- def publisher
15
- "PLATEAU"
16
- end
5
+ # Base class for all PLATEAU identifiers. Canonical name
6
+ # Pubid::Plateau::Identifier (Identifiers::Base is a back-compat alias).
7
+ class Identifier < ::Pubid::Identifier
8
+ # Delegate to the flavor module so callers can use
9
+ # `Pubid::Plateau::Identifier.parse` consistently with other flavors.
10
+ def self.parse(identifier)
11
+ Pubid::Plateau.parse(identifier)
12
+ end
17
13
 
18
- # Subclasses must implement type_string
19
- def type_string
20
- raise NotImplementedError, "Subclasses must implement type_string"
21
- end
14
+ attribute :number, :integer
15
+ attribute :annex, :integer, default: -> {}
22
16
 
23
- def formatted_number
24
- "#%02d" % number
25
- end
17
+ # Stored as a plain string (always "PLATEAU") so it round-trips through
18
+ # to_hash/from_hash. Was a `def publisher` method, which made lutaml
19
+ # serialize a String against the Components::Publisher attribute.
20
+ attribute :publisher, :string, default: -> { "PLATEAU" }
26
21
 
27
- def formatted_annex
28
- annex ? "-#{annex}" : ""
29
- end
22
+ # Subclasses must implement type_string
23
+ def type_string
24
+ raise NotImplementedError, "Subclasses must implement type_string"
25
+ end
26
+
27
+ def formatted_number
28
+ "#%02d" % number
29
+ end
30
+
31
+ def formatted_annex
32
+ annex ? "-#{annex}" : ""
33
+ end
30
34
 
31
- def ==(other)
32
- return false unless other.class == self.class
35
+ def ==(other)
36
+ return false unless other.class == self.class
33
37
 
34
- number == other.number &&
35
- annex == other.annex
36
- end
38
+ number == other.number &&
39
+ annex == other.annex
40
+ end
37
41
 
38
- # Include type_string and annex in serialization for round-trip compatibility
39
- def base_hash
40
- hash = super
41
- if self.class.attributes.key?(:type_string) && type_string
42
- hash[:type] =
43
- type_string
44
- end
45
- hash[:annex] = annex if annex
46
- hash
42
+ # Include type_string and annex in serialization for round-trip compatibility
43
+ def base_hash
44
+ hash = super
45
+ if self.class.attributes.key?(:type_string) && type_string
46
+ hash[:type] =
47
+ type_string
47
48
  end
49
+ hash[:annex] = annex if annex
50
+ hash
48
51
  end
49
52
  end
53
+
54
+ module Identifiers
55
+ # Backward-compatible alias: PLATEAU's base class used to be
56
+ # Pubid::Plateau::Identifiers::Base. It is now Pubid::Plateau::Identifier.
57
+ Base = Pubid::Plateau::Identifier
58
+ end
50
59
  end
51
60
  end
@@ -13,10 +13,6 @@ module Pubid
13
13
  "Handbook"
14
14
  end
15
15
 
16
- def to_s
17
- render(format: :human)
18
- end
19
-
20
16
  def formatted_edition
21
17
  "第#{edition}版"
22
18
  end