pubid 2.0.0.pre.alpha.3 → 2.0.0.pre.alpha.5

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 +168 -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 +23 -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 +72 -12
  136. data/lib/pubid/oiml/renderer.rb +37 -17
  137. data/lib/pubid/oiml/single_identifier.rb +58 -0
  138. data/lib/pubid/oiml/supplement_identifier.rb +34 -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,23 @@ 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
+ plus_marker = parsed_hash[:plus_marker].to_s if parsed_hash[:plus_marker]
82
+
83
+ # Determine supplement type. The trailing word ("Amendment"/"Errata")
84
+ # selects the class; the concrete class then carries the word via
85
+ # #supplement_type, so only the `trailing` flag needs storing.
81
86
  supplement_class = if parsed_hash[:annex_letter] || parsed_hash[:annex_marker]
82
87
  Identifiers::Annex
88
+ elsif marker == "Errata" || plus_marker == "Errata"
89
+ Identifiers::Errata
83
90
  else
84
91
  Identifiers::Amendment
85
92
  end
86
93
 
87
94
  supplement = supplement_class.new
95
+ supplement.trailing = true if marker
96
+ supplement.joined = true if plus_marker
88
97
 
89
98
  # Recursively parse base identifier
90
99
  if parsed_hash[:base_identifier]
@@ -103,6 +112,13 @@ module Pubid
103
112
  supplement.language = extract_language(parsed_hash[:language]) if parsed_hash[:language]
104
113
  supplement.letter = parsed_hash[:annex_letter].to_s if parsed_hash[:annex_letter]
105
114
 
115
+ # Annex with no year of its own but a dated base ("R 60:2017 Annexes"):
116
+ # the year belongs to the base and must render glued to it.
117
+ if supplement.is_a?(Identifiers::Annex) && !year_value &&
118
+ supplement.base_identifier&.date
119
+ supplement.year_on_base = true
120
+ end
121
+
106
122
  # Track if supplement itself was parsed with Edition format
107
123
  if parsed_hash[:edition_format]
108
124
  supplement.parsed_format = "long"
@@ -119,8 +135,8 @@ module Pubid
119
135
 
120
136
  identifier = identifier_class.new
121
137
 
122
- # Handle code (number-part-subpart) specially
123
- if parsed_hash[:number] || parsed_hash[:part] || parsed_hash[:subpart]
138
+ # Handle code (number-part-subpart-suffix) specially
139
+ if parsed_hash[:number] || parsed_hash[:part] || parsed_hash[:subpart] || parsed_hash[:code_suffix]
124
140
  code_attrs = {}
125
141
  if parsed_hash[:number]
126
142
  code_attrs[:number] =
@@ -131,6 +147,10 @@ module Pubid
131
147
  code_attrs[:subpart] =
132
148
  parsed_hash[:subpart].to_s
133
149
  end
150
+ if parsed_hash[:code_suffix]
151
+ code_attrs[:suffix] = parsed_hash[:code_suffix].to_s
152
+ code_attrs[:space_suffix] = true if parsed_hash.key?(:space_suffix)
153
+ end
134
154
  identifier.code = Components::Code.new(**code_attrs)
135
155
  end
136
156
 
@@ -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,9 @@ 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 | plus_supplement_identifier |
23
+ trailing_supplement_identifier | base_identifier
22
24
  end
23
25
 
24
26
  # Publisher - always "OIML"
@@ -30,14 +32,33 @@ module Pubid
30
32
  # Number with optional part and subpart
31
33
  rule(:number_only) { digits.as(:number) }
32
34
 
33
- rule(:part_number) { dash >> digits.as(:part) }
35
+ # Part number, optionally a slashed multi-part continuation captured
36
+ # verbatim, e.g. "1/-2" in "OIML R 46-1/-2:2012".
37
+ rule(:part_number) do
38
+ dash >> (digits >> (slash >> dash >> digits).repeat).as(:part)
39
+ end
34
40
 
35
41
  rule(:subpart_number) { dash >> digits.as(:subpart) }
36
42
 
43
+ # Free-form named/lettered code suffix glued after the numeric part:
44
+ # -GUM 1, -special, -sup, -erratum, -A, -ISO3930, -Amend, -Amended_2012
45
+ # plus the space-separated "Brochure" label. "GUM <n>" is tried first so
46
+ # its trailing number isn't lost to the bare-word alternative. The
47
+ # trailing `(_?digits)*` captures joint codes ("ISO3930") and the
48
+ # "Amended_2012" amendment label (the hand-off treats these as part
49
+ # suffixes, preserved verbatim for round-trip).
50
+ rule(:named_suffix) do
51
+ (dash >> (
52
+ (str("GUM") >> space >> digits) |
53
+ (match("[A-Za-z]").repeat(1) >> (str("_").maybe >> digits).repeat)
54
+ ).as(:code_suffix)) |
55
+ (space >> str("Brochure").as(:code_suffix) >> str("").as(:space_suffix))
56
+ end
57
+
37
58
  rule(:full_number) do
38
- (number_only >> part_number >> subpart_number) |
39
- (number_only >> part_number) |
40
- number_only
59
+ (number_only >> part_number >> subpart_number >> named_suffix.maybe) |
60
+ (number_only >> part_number >> named_suffix.maybe) |
61
+ (number_only >> named_suffix.maybe)
41
62
  end
42
63
 
43
64
  # Edition number - ordinal numbers
@@ -82,8 +103,18 @@ module Pubid
82
103
  end
83
104
 
84
105
  # Language codes
106
+ #
107
+ # relaton-data-oiml encodes the publication language with its own code
108
+ # set (lib/oiml_fetcher.rb DOCID_LANG_CODE): single uppercase letters
109
+ # E F D R S C A U X, plus two-letter PO/PT/PE/SR. The two-letter OIML
110
+ # codes must be tried before the single-letter rule so e.g. "PE" isn't
111
+ # left with a dangling "E".
85
112
  rule(:lang_single) do
86
- match("[EFRX]") # Single letter: E, F, R, X
113
+ match("[EFRXDSCAU]")
114
+ end
115
+
116
+ rule(:lang_multi_oiml) do
117
+ str("PO") | str("PT") | str("PE") | str("SR")
87
118
  end
88
119
 
89
120
  rule(:lang_multi) do
@@ -93,8 +124,9 @@ module Pubid
93
124
  rule(:language_code) do
94
125
  (
95
126
  (lang_single >> slash >> lang_single) | # E/F
96
- lang_single | # E, F
97
- lang_multi # en, fr
127
+ lang_multi_oiml | # PO, PT, PE, SR
128
+ lang_single | # E, F, D, R, S, C, A, U, X
129
+ lang_multi # en, fr
98
130
  ).as(:language)
99
131
  end
100
132
 
@@ -131,21 +163,49 @@ module Pubid
131
163
  language_portion.maybe.as(:language)
132
164
  end
133
165
 
134
- # Annex identifier - "BASE Annexes Edition YYYY" or "BASE Annexes:YYYY"
166
+ # Trailing supplement word - "BASE Amendment" / "BASE Errata" where the
167
+ # publication year stays on the base (e.g. "OIML R 138:2009 Amendment").
168
+ # amendment_short is tried first; it only matches when a year follows the
169
+ # word, so the no-year trailing form falls through to here.
170
+ rule(:trailing_supplement_identifier) do
171
+ base_without_language.as(:base_identifier) >>
172
+ space >> (str("Amendment") | str("Errata")).as(:trailing_marker) >>
173
+ language_portion.maybe.as(:language)
174
+ end
175
+
176
+ # Plus-joined supplement - "BASE:YEAR+Supplement:YEAR" form where both
177
+ # the base and the supplement carry their own year. Used for amendments
178
+ # and errata to dated bases (e.g. "OIML B 10:2011+Amendment:2012").
179
+ # Annexes already encode the year-on-base intent via their own model.
180
+ rule(:plus_supplement_identifier) do
181
+ base_without_language.as(:base_identifier) >>
182
+ str("+") >>
183
+ (str("Amendment") | str("Errata")).as(:plus_marker) >>
184
+ (colon >> year_digits.as(:year)).maybe >>
185
+ language_portion.maybe.as(:language)
186
+ end
187
+
188
+ # Annex identifier - "BASE Annexes Edition YYYY" / "BASE Annexes:YYYY" /
189
+ # "BASE:YYYY Annexes" (year on the base, no annex year).
135
190
  rule(:annex_identifier) do
136
191
  base_without_language.as(:base_identifier) >>
137
192
  space >> str("Annexes").as(:annex_marker) >>
138
193
  (
139
194
  (space >> edition_text >> space >> year_digits.as(:year)).as(:edition_format) |
140
195
  (colon >> year_digits.as(:year))
141
- ) >>
196
+ ).maybe >>
142
197
  language_portion.maybe.as(:language)
143
198
  end
144
199
 
145
- # Annex with letter - "BASE Annex A Edition YYYY"
200
+ # Annex letter or letter range, e.g. "A" or "B-C".
201
+ rule(:annex_letter_value) do
202
+ (match("[A-Z]") >> (dash >> match("[A-Z]")).maybe).as(:annex_letter)
203
+ end
204
+
205
+ # Annex with letter - "BASE Annex A Edition YYYY" / "BASE:YYYY Annex B-C"
146
206
  rule(:annex_letter_identifier) do
147
207
  base_without_language.as(:base_identifier) >>
148
- space >> str("Annex") >> space >> match("[A-Z]").as(:annex_letter) >>
208
+ space >> str("Annex") >> space >> annex_letter_value >>
149
209
  ((space >> edition_text >> space >> year_digits.as(:year)) | (colon >> year_digits.as(:year))).maybe >>
150
210
  language_portion.maybe.as(:language)
151
211
  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,26 @@ module Pubid
80
84
  def render_supplement(id)
81
85
  format = effective_format(id)
82
86
 
87
+ # Plus-joined: "BASE+Amendment:YEAR" / "BASE+Errata:YEAR" with both
88
+ # the base and the supplement carrying their own year.
89
+ if id.joined
90
+ base_str = strip_language(id.base_identifier.to_s)
91
+ result = "#{base_str}+#{id.supplement_type}"
92
+ result += ":#{id.year}" if id.year
93
+ result += " (#{id.language})" if id.language
94
+ return result
95
+ end
96
+
97
+ # Trailing-word shorthand: "BASE Amendment" / "BASE Errata" with the
98
+ # publication year kept on the base identifier. The word comes from the
99
+ # concrete supplement class.
100
+ if id.trailing
101
+ base_str = strip_language(id.base_identifier.to_s)
102
+ result = "#{base_str} #{id.supplement_type}"
103
+ result += " (#{id.language})" if id.language
104
+ return result
105
+ end
106
+
83
107
  base_format = if format && format != :short
84
108
  format
85
109
  elsif id.base_identifier.class.attributes.key?(:parsed_format) && id.base_identifier.parsed_format == "long"
@@ -93,7 +117,7 @@ module Pubid
93
117
  else
94
118
  id.base_identifier.to_s
95
119
  end
96
- base_str = base_str.sub(/\s*\([^)]+\)\s*$/, "").strip
120
+ base_str = strip_language(base_str)
97
121
 
98
122
  result = "#{id.supplement_type} (#{id.year}) to #{base_str}"
99
123
  result += " (#{id.language})" if id.language
@@ -104,6 +128,16 @@ module Pubid
104
128
  def render_annex(id)
105
129
  format = effective_format(id)
106
130
 
131
+ # "BASE:YYYY Annex(es)" — the year is glued to the base, the marker
132
+ # carries none. Keep the base date instead of stripping it.
133
+ if id.year_on_base
134
+ base_str = strip_language(id.base_identifier.to_s)
135
+ marker = id.letter ? "Annex #{id.letter}" : "Annexes"
136
+ result = "#{base_str} #{marker}"
137
+ result += " (#{id.language})" if id.language
138
+ return result
139
+ end
140
+
107
141
  base_format = if id.base_identifier.class.attributes.key?(:parsed_format) && id.base_identifier.parsed_format == "long"
108
142
  :long
109
143
  else
@@ -142,20 +176,6 @@ module Pubid
142
176
  result += " (#{id.language})" if id.language
143
177
  result
144
178
  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
179
  end
160
180
  end
161
181
  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,46 @@ 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
16
+ # True for the plus-joined form ("OIML B 10:2011+Amendment:2012") where
17
+ # both the base and the supplement carry their own year, joined by "+".
18
+ attribute :joined, :boolean, default: false
11
19
  attribute :parsed_format, :string, default: -> {
12
20
  "short"
13
21
  } # Track supplement's parsed format
14
22
 
23
+ # Serialization delta on top of Oiml::Identifier's shared block. The
24
+ # nested base_identifier is (de)serialized recursively through the
25
+ # polymorphic router so its own `_type` selects the right subclass.
26
+ key_value do
27
+ map "base_identifier",
28
+ with: { to: :base_identifier_to_kv, from: :base_identifier_from_kv }
29
+ map "year", to: :year
30
+ map "trailing", to: :trailing
31
+ map "joined", to: :joined
32
+ end
33
+
34
+ def base_identifier_to_kv(model, doc)
35
+ base = model.base_identifier
36
+ return unless base
37
+
38
+ doc.add_child(
39
+ Lutaml::KeyValue::DataModel::Element.new("base_identifier",
40
+ base.to_hash),
41
+ )
42
+ end
43
+
44
+ def base_identifier_from_kv(model, value)
45
+ model.base_identifier = ::Pubid::Oiml::Identifier.from_hash(value) if value
46
+ end
47
+
15
48
  attr_reader :requested_format
16
49
 
17
50
  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"