pubid 2.0.0.pre.alpha.4 → 2.0.0.pre.alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd077b48356a46dfb52d7b95e932c29e91e5fe1a7859c1128ddda22d768b4a96
4
- data.tar.gz: 4e8059d20bed9297393f54bb894958a6c0ab8d92bef895fcb7528c291b3ff349
3
+ metadata.gz: 1b1fa5f38da07bc9a507c0c507ba9ddfc8f6541ab9f4b6793b09abe302ab8849
4
+ data.tar.gz: 63b7eba9c9942c177e95b9c78a09462679a11acde4fd6a2c2e486848fb43ca93
5
5
  SHA512:
6
- metadata.gz: 329f3776c53f4021bfd2d25bd8698fb9c01309a66f56e9334d815aee3fde8ce25b509637f97ffc4ef91f0dfb67d10768a33505a939109a305d34b2e917af8f6c
7
- data.tar.gz: 79c43b4e3ca83fe379a588f20490850e6a88757e382f0f9b9fc88d9f31923aff8fd8a5d48670c33a0d0705747820c6b3548260334ebf4e63a09b60dab82ad4ec
6
+ metadata.gz: 915034be3882a43991ce3c0f934c4097f216f1fdac35dcb59ed6a0decb639dd4a19882de4f28940d4f29e6e9bb351de3383b07bd95cc0dc2a53bb4ad875f072d
7
+ data.tar.gz: 1cb7e935accc6e66bed8b599d25605b26cd415547c1ddec9ba2ecf1b7183883102c99078b830b2de9da436bc17613db042b5fc05e0d7d5515786fae71592aeb8
@@ -114,19 +114,11 @@ module Pubid
114
114
  if data[:na_prefix]
115
115
  return build_national_annex(data, supplements_data)
116
116
  elsif data[:adopted_string]
117
- # Check for multi-level adoptions
118
- identifier = build_adopted_identifier(data)
119
-
120
- # Wrap with consolidated if supplements present
121
- if supplements_data.any?
122
- identifier = wrap_with_consolidated(identifier,
123
- supplements_data)
124
- end
125
-
126
- # Wrap with ExpertCommentary if needed
127
- identifier = wrap_with_expert_commentary(identifier) if data[:expert_commentary]
128
-
129
- return identifier
117
+ # Adopted identifiers carry their supplements and Expert-commentary
118
+ # suffix *inside* the adopted string, so build_adopted_identifier does
119
+ # all the wrapping. Wrapping again here would double-wrap (a second
120
+ # ExpertCommentary around the first) and break #base_document.
121
+ return build_adopted_identifier(data)
130
122
  end
131
123
 
132
124
  # Determine identifier class using the module's lookup helpers
@@ -139,8 +131,11 @@ module Pubid
139
131
  supplements_data)
140
132
  end
141
133
 
142
- # Wrap with ExpertCommentary if needed
143
- identifier = wrap_with_expert_commentary(identifier) if data[:expert_commentary]
134
+ # Wrap with ExpertCommentary if needed (full "Expert Commentary" form
135
+ # arrives as the standalone :expert_commentary_full token)
136
+ if data[:expert_commentary] || data[:expert_commentary_full]
137
+ identifier = wrap_with_expert_commentary(identifier)
138
+ end
144
139
 
145
140
  identifier
146
141
  end
@@ -922,6 +917,7 @@ module Pubid
922
917
  number: (supp_data[:amd_number] || supp_data[:cor_number])&.to_s,
923
918
  year: (supp_data[:amd_year] || supp_data[:cor_year])&.to_s,
924
919
  separator: separator,
920
+ amd_suffix_form: !(supp_data[:amd_sep_plus] || supp_data[:amd_sep_slash]),
925
921
  }
926
922
  end
927
923
  end
@@ -1157,8 +1153,10 @@ module Pubid
1157
1153
  # Extract ExComm suffix before parsing (it should be preserved in data[:expert_commentary])
1158
1154
  # Remove it from adopted_string so ISO/IEC parsers don't choke on it
1159
1155
  # Handle all three formats: "Expert Commentary", "ExComm", "ExComm (Fire)"
1160
- if adopted_str.end_with?("Expert Commentary")
1161
- adopted_str = adopted_str.sub(/Expert Commentary$/, "")
1156
+ # Expert-commentary suffix matching is case-insensitive; output is always
1157
+ # canonicalised to "Expert Commentary" / "ExComm".
1158
+ if adopted_str.match?(/expert commentary$/i)
1159
+ adopted_str = adopted_str.sub(/expert commentary$/i, "")
1162
1160
  data[:expert_commentary_full] = "Expert Commentary"
1163
1161
  data[:expert_commentary] = true unless data.key?(:expert_commentary)
1164
1162
  # Update @original_data so wrap_with_expert_commentary can access it
@@ -1167,8 +1165,8 @@ module Pubid
1167
1165
  @original_data[:expert_commentary] =
1168
1166
  true
1169
1167
  end
1170
- elsif adopted_str.end_with?("ExComm (")
1171
- adopted_str = adopted_str.sub(/ExComm \(.*\)$/, "")
1168
+ elsif adopted_str.match?(/excomm \($/i)
1169
+ adopted_str = adopted_str.sub(/excomm \(.*\)$/i, "")
1172
1170
  data[:expert_commentary_full] = "Expert Commentary"
1173
1171
  data[:expert_commentary] = true unless data.key?(:expert_commentary)
1174
1172
  @original_data[:expert_commentary_full] = "Expert Commentary"
@@ -1176,10 +1174,10 @@ module Pubid
1176
1174
  @original_data[:expert_commentary] =
1177
1175
  true
1178
1176
  end
1179
- elsif adopted_str.include?("ExComm (")
1177
+ elsif adopted_str.match?(/excomm \(/i)
1180
1178
  # Extract topic from "ExComm (Fire)"
1181
- topic_match = adopted_str.match(/ExComm\s*\(([^)]+)\)/)
1182
- adopted_str = adopted_str.sub(/ExComm\s*\(.*\)$/, "")
1179
+ topic_match = adopted_str.match(/excomm\s*\(([^)]+)\)/i)
1180
+ adopted_str = adopted_str.sub(/excomm\s*\(.*\)$/i, "")
1183
1181
  data[:expert_commentary_topic] = topic_match[1] if topic_match
1184
1182
  data[:expert_commentary] = true unless data.key?(:expert_commentary)
1185
1183
  if topic_match
@@ -1190,9 +1188,9 @@ module Pubid
1190
1188
  @original_data[:expert_commentary] =
1191
1189
  true
1192
1190
  end
1193
- elsif adopted_str.match?(/ExComm\s*$/)
1191
+ elsif adopted_str.match?(/excomm\s*$/i)
1194
1192
  # Abbreviated form "ExComm" at the end
1195
- adopted_str = adopted_str.sub(/ExComm\s*$/, "")
1193
+ adopted_str = adopted_str.sub(/excomm\s*$/i, "")
1196
1194
  data[:expert_commentary] = true unless data.key?(:expert_commentary)
1197
1195
  unless @original_data.key?(:expert_commentary)
1198
1196
  @original_data[:expert_commentary] =
@@ -1258,9 +1256,10 @@ module Pubid
1258
1256
  # BSI uses +A1:2020 format, but ISO/IEC parsers can't handle this
1259
1257
  # Patterns: +A1:2020, +A11:2021, +AMD1:2001, +C1:2020
1260
1258
  extracted_supplements = []
1261
- # Match patterns like: +A1:2020, +A11:2021, +AMD1:2001, +C1:2020, +COR1:2020
1259
+ # Match patterns like: +A1:2020, +A11:2021, +AMD1:2001, +C1:2020, +COR1:2020,
1260
+ # and the year-less compact forms +A2 / +C1 (the ":YYYY" is optional).
1262
1261
  # Must be at end of string or followed by space (not colon, which is part of date)
1263
- adopted_str_clean = adopted_str_clean.gsub(/([+])(A(\d+)|AMD(\d+)|C(\d+)|COR(\d+)):(\d{4})(?:\s|$)/) do |_match|
1262
+ adopted_str_clean = adopted_str_clean.gsub(/([+])(A(\d+)|AMD(\d+)|C(\d+)|COR(\d+))(?::(\d{4}))?(?:\s|$)/) do |_match|
1264
1263
  separator = $1 # "+" or "/"
1265
1264
  type_code = $2 || $3 || $4 || $5 || $6
1266
1265
  $2 || $3 || $4 || $5 || $6
@@ -1285,6 +1284,8 @@ module Pubid
1285
1284
  number: supp_number,
1286
1285
  year: year,
1287
1286
  separator: separator,
1287
+ # compact "+A"/"/A" join form (not the trailing " AMD5" suffix form)
1288
+ amd_suffix_form: false,
1288
1289
  }
1289
1290
 
1290
1291
  "" # Remove from adopted_string
@@ -1300,6 +1301,7 @@ module Pubid
1300
1301
  number: amd_number,
1301
1302
  year: nil,
1302
1303
  separator: nil, # No separator for AMD without year
1304
+ amd_suffix_form: true, # trailing " AMD5" suffix form
1303
1305
  }
1304
1306
  "" # Remove from adopted_string
1305
1307
  end.strip
@@ -1365,6 +1367,9 @@ module Pubid
1365
1367
  # Return appropriate wrapper based on adoption type
1366
1368
  if adopted_id
1367
1369
  # If adopted_id is a CEN identifier (in Cen module), use AdoptedEuropeanNorm
1370
+ # The Expert-commentary suffix is represented uniformly by an outer
1371
+ # ExpertCommentary wrapper (below), never a boolean on the inner norm,
1372
+ # so that #base_document peels cleanly to the bare standard.
1368
1373
  identifier = if adopted_id.class.name.start_with?("Pubid::CenCenelec::")
1369
1374
  Identifiers::AdoptedEuropeanNorm.new(
1370
1375
  publisher: Components::Publisher.new(body: bsi_prefix),
@@ -1372,10 +1377,6 @@ module Pubid
1372
1377
  edition: final_edition&.to_s,
1373
1378
  translation_lang: data[:translation_lang]&.to_s,
1374
1379
  translation_upper: data[:translation_upper]&.to_s,
1375
- # Pass expert_commentary data so ConsolidatedIdentifier can render it
1376
- expert_commentary: data[:expert_commentary],
1377
- # Pass expert_commentary_topic if present
1378
- expert_commentary_topic: data[:expert_commentary_topic],
1379
1380
  # Pass translation_suffix_type for rendering
1380
1381
  translation_suffix_type: data[:translation_suffix_type]&.to_s,
1381
1382
  # Pass reaffirmation_year for rendering
@@ -1389,11 +1390,6 @@ module Pubid
1389
1390
  edition: final_edition&.to_s,
1390
1391
  translation_lang: data[:translation_lang]&.to_s,
1391
1392
  translation_upper: data[:translation_upper]&.to_s,
1392
- # Pass expert_commentary data ONLY if no supplements
1393
- # When supplements are present, ConsolidatedIdentifier will add ExComm later
1394
- expert_commentary: data[:expert_commentary] && extracted_supplements.none?,
1395
- # Pass expert_commentary_topic if present and no supplements
1396
- expert_commentary_topic: (data[:expert_commentary_topic] if extracted_supplements.none?),
1397
1393
  # Pass translation_suffix_type for rendering
1398
1394
  translation_suffix_type: data[:translation_suffix_type]&.to_s,
1399
1395
  # Pass reaffirmation_year for rendering
@@ -1401,20 +1397,13 @@ module Pubid
1401
1397
  )
1402
1398
  end
1403
1399
 
1404
- # NEW: Wrap with supplements if any were extracted from adopted_string
1405
- # This must happen BEFORE wrapping with ExpertCommentary so that supplements
1406
- # render before ExComm suffix
1400
+ # Wrap supplements first (they render before the ExComm suffix), then
1401
+ # wrap the whole thing in a single ExpertCommentary same shape as the
1402
+ # pure-BSI path, keeping #base_document uniform.
1407
1403
  if extracted_supplements.any?
1408
- # When supplements are present, wrap with ConsolidatedIdentifier
1409
- # Pass expert_commentary data so ConsolidatedIdentifier can render it later
1410
- identifier = wrap_with_consolidated(
1411
- identifier,
1412
- extracted_supplements,
1413
- expert_commentary: data[:expert_commentary],
1414
- expert_commentary_topic: data[:expert_commentary_topic],
1415
- )
1416
- elsif data[:expert_commentary]
1417
- # When no supplements but ExComm present, wrap with ExpertCommentary
1404
+ identifier = wrap_with_consolidated(identifier, extracted_supplements)
1405
+ end
1406
+ if data[:expert_commentary] || data[:expert_commentary_full]
1418
1407
  identifier = wrap_with_expert_commentary(identifier)
1419
1408
  end
1420
1409
 
@@ -1451,6 +1440,7 @@ expert_commentary: nil, expert_commentary_topic: nil)
1451
1440
  amendment_number: supp[:number],
1452
1441
  amendment_year: year_val&.to_i,
1453
1442
  separator: supp[:separator] || "+",
1443
+ amd_suffix_form: supp[:amd_suffix_form] ? true : false,
1454
1444
  )
1455
1445
  else
1456
1446
  Identifiers::Corrigendum.new(
@@ -1484,6 +1474,9 @@ expert_commentary: nil, expert_commentary_topic: nil)
1484
1474
  number: (supp_data[:amd_number] || supp_data[:cor_number])&.to_s,
1485
1475
  year: (supp_data[:amd_year] || supp_data[:cor_year])&.to_s,
1486
1476
  separator: separator,
1477
+ # The compact "+A"/"/A" amendment rule captures an explicit separator;
1478
+ # the trailing " AMD5" suffix form does not.
1479
+ amd_suffix_form: !(supp_data[:amd_sep_plus] || supp_data[:amd_sep_slash]),
1487
1480
  }
1488
1481
  end
1489
1482
  end
@@ -10,10 +10,38 @@ module Pubid
10
10
  attribute :amendment_number, :string
11
11
  attribute :amendment_year, :integer
12
12
  attribute :separator, :string, default: -> { "+" }
13
+ # true for the trailing " AMD5" / " AMD AA" suffix form, false for the
14
+ # compact "+A5" / "/A5" join form. Distinguishes the two when no year is
15
+ # present (previously the presence of a year was a reliable proxy).
16
+ attribute :amd_suffix_form, :boolean, default: -> { false }
13
17
 
14
18
  def publisher
15
19
  base_identifier&.publisher
16
20
  end
21
+
22
+ # Base document = the standard this amendment applies to, fully peeled.
23
+ def base_document
24
+ base_identifier&.base_document || self
25
+ end
26
+
27
+ # Dropping the supplement layer yields the base standard.
28
+ def drop_supplements
29
+ base_identifier || self
30
+ end
31
+
32
+ # Uniform supplement interface (shared with Corrigendum) so callers need
33
+ # not special-case the class.
34
+ def supplement_type
35
+ :amendment
36
+ end
37
+
38
+ def supplement_number
39
+ amendment_number
40
+ end
41
+
42
+ def supplement_year
43
+ amendment_year
44
+ end
17
45
  end
18
46
  end
19
47
  end
@@ -6,7 +6,7 @@ module Pubid
6
6
  # Consolidated Identifier - contains base document plus supplements
7
7
  # Example: "BS 4592-0:2006+A1:2012" = [BS 4592-0:2006, Amendment 1:2012]
8
8
  class ConsolidatedIdentifier < SingleIdentifier
9
- attribute :identifiers, ::Pubid::Identifier, polymorphic: true,
9
+ attribute :identifiers, ::Pubid::Identifier, polymorphic: true,
10
10
  collection: true
11
11
 
12
12
  def to_urn
@@ -62,6 +62,16 @@ module Pubid
62
62
  base = identifiers&.first
63
63
  base.type if base&.class&.attributes&.key?(:type)
64
64
  end
65
+
66
+ # Base document = the wrapped standard (first entry), fully peeled.
67
+ def base_document
68
+ identifiers&.first&.base_document || self
69
+ end
70
+
71
+ # Dropping the supplement layer yields the base standard alone.
72
+ def drop_supplements
73
+ identifiers&.first || self
74
+ end
65
75
  end
66
76
  end
67
77
  end
@@ -14,6 +14,30 @@ module Pubid
14
14
  def publisher
15
15
  base_identifier&.publisher
16
16
  end
17
+
18
+ # Base document = the standard this corrigendum applies to, fully peeled.
19
+ def base_document
20
+ base_identifier&.base_document || self
21
+ end
22
+
23
+ # Dropping the supplement layer yields the base standard.
24
+ def drop_supplements
25
+ base_identifier || self
26
+ end
27
+
28
+ # Uniform supplement interface (shared with Amendment) so callers need
29
+ # not special-case the class.
30
+ def supplement_type
31
+ :corrigendum
32
+ end
33
+
34
+ def supplement_number
35
+ corrigendum_number
36
+ end
37
+
38
+ def supplement_year
39
+ corrigendum_year
40
+ end
17
41
  end
18
42
  end
19
43
  end
@@ -25,6 +25,11 @@ module Pubid
25
25
  def year
26
26
  base_identifier&.year
27
27
  end
28
+
29
+ # Base document = the commented standard, fully peeled.
30
+ def base_document
31
+ base_identifier&.base_document || self
32
+ end
28
33
  end
29
34
  end
30
35
  end
@@ -20,6 +20,10 @@ module Pubid
20
20
  { key: :flex, title: "BSI Flex", short: "BSI Flex" }
21
21
  end
22
22
 
23
+ # Base document = the Flex standard without its version (edition).
24
+ def base_document
25
+ exclude(:edition)
26
+ end
23
27
  end
24
28
  end
25
29
  end
@@ -5,6 +5,19 @@ require "parslet"
5
5
  module Pubid
6
6
  module Bsi
7
7
  class Parser < Parslet::Parser
8
+ # Case-insensitive string match (Parslet 2.0 has no built-in `stri`).
9
+ # Each character becomes a two-case character class; other characters
10
+ # (spaces, punctuation) match verbatim.
11
+ def stri(string)
12
+ string.each_char.map do |char|
13
+ if char =~ /[a-z]/i
14
+ match["#{char.upcase}#{char.downcase}"]
15
+ else
16
+ str(char)
17
+ end
18
+ end.reduce(:>>)
19
+ end
20
+
8
21
  rule(:digit) { match["0-9"] }
9
22
  rule(:digits) { digit.repeat(1) }
10
23
  rule(:space) { str(" ") }
@@ -296,14 +309,15 @@ module Pubid
296
309
  # Single letter (a-z or A-Z) that comes immediately after number or part
297
310
  rule(:letter_edition) { match["a-zA-Z"].as(:letter_edition) }
298
311
 
299
- # Amendment (+A1:2008 or /A2:2019 or /Amd 1:2020 or +A1:15 for short year)
312
+ # Amendment (+A1:2008 or /A2:2019 or /Amd 1:2020 or +A1:15 for short year,
313
+ # or +A2 / /A2 without a year — mirrors the optional-year corrigendum rule)
300
314
  rule(:amendment) do
301
315
  ((plus.as(:amd_sep_plus) | slash.as(:amd_sep_slash)) >>
302
316
  str("A") >> digits.as(:amd_number) >>
303
- colon >> digit.repeat(2, 4).as(:amd_year)) |
317
+ (colon >> digit.repeat(2, 4).as(:amd_year)).maybe) |
304
318
  ((plus.as(:amd_sep_plus) | slash.as(:amd_sep_slash)) >>
305
319
  str("Amd") >> space >> digits.as(:amd_number) >>
306
- colon >> digit.repeat(4, 4).as(:amd_year))
320
+ (colon >> digit.repeat(4, 4).as(:amd_year)).maybe)
307
321
  end
308
322
 
309
323
  # Corrigendum (+AC:2009 or +AC1:2008 or /AC1:2005 or +C1:2018 or +C1 without year)
@@ -518,8 +532,8 @@ module Pubid
518
532
  # 3. "ExComm (Fire)" (with optional topic suffix)
519
533
  rule(:expert_commentary) do
520
534
  space >> (
521
- str("Expert Commentary").as(:expert_commentary_full) |
522
- (str("ExComm") >> (space >> str("(") >> match["A-Za-z"].repeat(1).as(:expert_commentary_topic) >> str(")")).maybe).as(:expert_commentary)
535
+ stri("Expert Commentary").as(:expert_commentary_full) |
536
+ (stri("ExComm") >> (space >> str("(") >> match["A-Za-z"].repeat(1).as(:expert_commentary_topic) >> str(")")).maybe).as(:expert_commentary)
523
537
  )
524
538
  end
525
539
 
@@ -289,20 +289,21 @@ module Pubid
289
289
  end
290
290
 
291
291
  def render_amendment(id)
292
- if id.base_identifier
293
- if id.amendment_year
294
- "#{id.base_identifier}#{id.separator}A#{id.amendment_number}:#{id.amendment_year}"
295
- elsif id.amendment_number&.match?(/^[A-Z]+$/)
296
- "#{id.base_identifier} AMD #{id.amendment_number}"
297
- else
298
- "#{id.base_identifier} AMD#{id.amendment_number}"
299
- end
300
- elsif id.amendment_year
301
- "#{id.separator}A#{id.amendment_number}:#{id.amendment_year}"
302
- elsif id.amendment_number&.match?(/^[A-Z]+$/)
303
- "AMD #{id.amendment_number}"
292
+ base = id.base_identifier ? id.base_identifier.to_s : ""
293
+
294
+ if id.amd_suffix_form
295
+ # Trailing " AMD5" / " AMD AA" suffix form
296
+ suffix = if id.amendment_number&.match?(/^[A-Z]+$/)
297
+ " AMD #{id.amendment_number}"
298
+ else
299
+ " AMD#{id.amendment_number}"
300
+ end
301
+ base.empty? ? suffix.lstrip : "#{base}#{suffix}"
304
302
  else
305
- "AMD#{id.amendment_number}"
303
+ # Compact "+A5" / "/A5" join form, with an optional ":year"
304
+ compact = "#{id.separator}A#{id.amendment_number}"
305
+ compact += ":#{id.amendment_year}" if id.amendment_year
306
+ "#{base}#{compact}"
306
307
  end
307
308
  end
308
309
 
@@ -408,15 +409,16 @@ module Pubid
408
409
 
409
410
  id.identifiers[1..].each do |idd|
410
411
  if idd.is_a?(Identifiers::Amendment)
411
- if idd.amendment_year
412
- sep = idd.separator || "+"
413
- result += "#{sep}A#{idd.amendment_number}:#{idd.amendment_year}"
414
- else
412
+ if idd.amd_suffix_form
415
413
  result += if idd.amendment_number&.match?(/^[A-Z]+$/)
416
414
  " AMD #{idd.amendment_number}"
417
415
  else
418
416
  " AMD#{idd.amendment_number}"
419
417
  end
418
+ else
419
+ sep = idd.separator || "+"
420
+ result += "#{sep}A#{idd.amendment_number}"
421
+ result += ":#{idd.amendment_year}" if idd.amendment_year
420
422
  end
421
423
  elsif idd.is_a?(Identifiers::Corrigendum)
422
424
  sep = idd.separator || "+"
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Iala
5
+ # Builds an IALA identifier object from the Parslet parse tree.
6
+ class Builder
7
+ def build(parsed)
8
+ hash = parsed
9
+ type_letter = stringify(hash[:type_letter])&.upcase
10
+
11
+ attrs = {
12
+ number: build_number(hash),
13
+ edition: stringify(hash.dig(:edition, :edition_value)),
14
+ language: stringify(hash.dig(:language_group, :language))&.upcase,
15
+ }.compact
16
+
17
+ Iala.identifier_klass_for_type_letter(type_letter).new(**attrs)
18
+ end
19
+
20
+ def self.build(parsed)
21
+ new.build(parsed)
22
+ end
23
+
24
+ private
25
+
26
+ # Compose "1070" or "0126-1" from doc_number + optional subpart.
27
+ def build_number(hash)
28
+ base = stringify(hash[:doc_number])
29
+ return base unless hash[:subpart]
30
+
31
+ subpart_str = subpart_to_s(hash[:subpart])
32
+ subpart_str.empty? ? base : "#{base}-#{subpart_str}"
33
+ end
34
+
35
+ # The subpart capture may be an array of {subpart_number: "1"},
36
+ # {subpart_number: "9"}, … (repeated matches). Join with "-".
37
+ def subpart_to_s(subpart)
38
+ return stringify(subpart).to_s unless subpart.is_a?(Array)
39
+
40
+ subpart.filter_map { |h| h.is_a?(Hash) ? stringify(h[:subpart_number]) : nil }.join("-")
41
+ end
42
+
43
+ def stringify(value)
44
+ return nil if value.nil?
45
+
46
+ str = value.to_s
47
+ str.empty? ? nil : str
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Iala
5
+ # Base class for all IALA identifiers. Canonical name
6
+ # Pubid::Iala::Identifier; every concrete IALA identifier
7
+ # (Identifiers::*) descends from it, and Identifiers::Base — aliased
8
+ # at the foot of identifiers/base.rb — points back to it.
9
+ #
10
+ # IALA identifiers have the form:
11
+ # [IALA ]{S|R|G|M|C|X|P}<4-digit number>[-<subpart>][ Ed <edition>][ (<LangLetter>)]
12
+ #
13
+ # Examples:
14
+ # S1070
15
+ # IALA S1070 Ed 2.0
16
+ # R1016:ed2.0(F)
17
+ # C0103-1 Ed 3.0
18
+ class Identifier < ::Pubid::Identifier
19
+ # Parse an IALA identifier string into an identifier object.
20
+ # @param identifier [String]
21
+ # @return [Pubid::Iala::Identifier]
22
+ # @raise [Parslet::ParseFailed] If parsing fails
23
+ def self.parse(identifier)
24
+ parsed = Parser.parse(identifier)
25
+ Builder.build(parsed)
26
+ rescue Parslet::ParseFailed => e
27
+ raise "Failed to parse IALA identifier '#{identifier}': #{e.message}"
28
+ end
29
+
30
+ attribute :publisher, :string, default: "IALA"
31
+ attribute :number, :string # "1070", "0126", "0103-1"
32
+ attribute :edition, :string # "2.0", "1.3", nil
33
+ attribute :language, :string # "E", "F", "S", "C", "A", "R", nil
34
+
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",
43
+ }.freeze
44
+
45
+ key_value do
46
+ map "_type", to: :_type, polymorphic_map: IALA_TYPE_MAP
47
+ map "number", to: :number
48
+ map "edition", to: :edition
49
+ map "language", to: :language
50
+ end
51
+
52
+ def to_urn
53
+ UrnGenerator.new(self).generate
54
+ end
55
+
56
+ # Single-letter type code (S, R, G, …) — convenience accessor used
57
+ # by Renderer and UrnGenerator.
58
+ def type_letter
59
+ self.class.type[:short]
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Backward-compatible alias: IALA's base class used to be
4
+ # Pubid::Iala::Identifiers::Base. It is now Pubid::Iala::Identifier
5
+ # (defined in identifier.rb), which is loaded by the parent autoload.
6
+ require_relative "../identifier" unless defined?(Pubid::Iala::Identifier)
7
+
8
+ module Pubid
9
+ module Iala
10
+ module Identifiers
11
+ Base = Pubid::Iala::Identifier
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Iala
5
+ module Identifiers
6
+ # IALA Guidelines (G series).
7
+ # Examples: G1015 Ed 2.2, G1078 Ed 2.1.
8
+ class Guideline < Base
9
+ def self.type
10
+ { key: :guideline, title: "Guideline", short: "G" }
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Iala
5
+ module Identifiers
6
+ # IALA Manuals (M prefix, planned).
7
+ # Today manuals carry no code (NAVGUIDE, VTS Manual, …); M is
8
+ # reserved for future numbered manuals.
9
+ class Manual < Base
10
+ def self.type
11
+ { key: :manual, title: "Manual", short: "M" }
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Iala
5
+ module Identifiers
6
+ # IALA Model Courses (C series).
7
+ # Examples: C0103-1 Ed 3.0, C2001-1 Ed 1.0.
8
+ class ModelCourse < Base
9
+ def self.type
10
+ { key: :"model-course", title: "Model Course", short: "C" }
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Iala
5
+ module Identifiers
6
+ # IALA Recommendations (R series).
7
+ # Examples: R0103 Ed 3.1, R0126 Ed 2.0, R1024 Ed 1.0.
8
+ class Recommendation < Base
9
+ def self.type
10
+ { key: :recommendation, title: "Recommendation", short: "R" }
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Iala
5
+ module Identifiers
6
+ # IALA Reports & Proceedings (X prefix, planned).
7
+ # Today reports carry no code; X is reserved for future numbered
8
+ # reports.
9
+ class Report < Base
10
+ def self.type
11
+ { key: :report, title: "Report", short: "X" }
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end