pubid 2.0.0.pre.alpha.1 → 2.0.0.pre.alpha.2

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/data/nist/update_codes.yaml +2 -0
  3. data/lib/pubid/amca/identifier.rb +39 -0
  4. data/lib/pubid/ansi/identifier.rb +42 -0
  5. data/lib/pubid/api/identifier.rb +47 -0
  6. data/lib/pubid/ashrae/identifier.rb +39 -0
  7. data/lib/pubid/asme/identifier.rb +46 -0
  8. data/lib/pubid/astm/identifier.rb +77 -0
  9. data/lib/pubid/bsi/identifier.rb +60 -0
  10. data/lib/pubid/ccsds/identifier.rb +68 -0
  11. data/lib/pubid/ccsds/identifiers/base.rb +11 -0
  12. data/lib/pubid/ccsds/single_identifier.rb +4 -1
  13. data/lib/pubid/cen_cenelec/identifier.rb +37 -0
  14. data/lib/pubid/cie/identifier.rb +53 -0
  15. data/lib/pubid/components/factory.rb +50 -0
  16. data/lib/pubid/components/typed_stage.rb +4 -0
  17. data/lib/pubid/components.rb +1 -0
  18. data/lib/pubid/csa/identifier.rb +56 -0
  19. data/lib/pubid/etsi/identifier.rb +43 -0
  20. data/lib/pubid/identifier.rb +8 -1
  21. data/lib/pubid/idf/identifier.rb +60 -0
  22. data/lib/pubid/iec/builder.rb +2 -1
  23. data/lib/pubid/iec/components/code.rb +2 -1
  24. data/lib/pubid/iec/components/publisher.rb +2 -1
  25. data/lib/pubid/iec/identifier.rb +235 -0
  26. data/lib/pubid/iec/identifiers/base.rb +4 -0
  27. data/lib/pubid/iec/identifiers/consolidated_identifier.rb +0 -4
  28. data/lib/pubid/iec/identifiers/fragment_identifier.rb +0 -4
  29. data/lib/pubid/iec/identifiers/sheet_identifier.rb +0 -4
  30. data/lib/pubid/iec/identifiers/vap_identifier.rb +0 -4
  31. data/lib/pubid/iec/parser.rb +7 -2
  32. data/lib/pubid/iec/urn_generator.rb +57 -171
  33. data/lib/pubid/iec/urn_parser.rb +53 -252
  34. data/lib/pubid/ieee/identifier.rb +41 -0
  35. data/lib/pubid/iho/identifier.rb +42 -0
  36. data/lib/pubid/iho/identifiers/base.rb +1 -1
  37. data/lib/pubid/iho/identifiers/bibliographic.rb +0 -4
  38. data/lib/pubid/iho/identifiers/circular_letter.rb +0 -4
  39. data/lib/pubid/iho/identifiers/miscellaneous.rb +0 -4
  40. data/lib/pubid/iho/identifiers/publication.rb +0 -4
  41. data/lib/pubid/iho/identifiers/standard.rb +0 -4
  42. data/lib/pubid/iho/urn_generator.rb +1 -1
  43. data/lib/pubid/iso/builder.rb +5 -1
  44. data/lib/pubid/iso/identifier.rb +261 -0
  45. data/lib/pubid/iso/parser.rb +4 -2
  46. data/lib/pubid/iso/scheme.rb +6 -0
  47. data/lib/pubid/iso/single_identifier.rb +6 -3
  48. data/lib/pubid/iso/urn_generator.rb +17 -3
  49. data/lib/pubid/iso/urn_parser.rb +16 -2
  50. data/lib/pubid/itu/identifier.rb +87 -22
  51. data/lib/pubid/jcgm/identifier.rb +43 -0
  52. data/lib/pubid/jis/identifier.rb +43 -0
  53. data/lib/pubid/nist/builder.rb +174 -5
  54. data/lib/pubid/nist/components/edition.rb +16 -0
  55. data/lib/pubid/nist/components/supplement.rb +88 -21
  56. data/lib/pubid/nist/identifier.rb +62 -0
  57. data/lib/pubid/nist/identifiers/base.rb +103 -24
  58. data/lib/pubid/nist/identifiers/circular_supplement.rb +1 -1
  59. data/lib/pubid/nist/identifiers/crpl_report.rb +1 -4
  60. data/lib/pubid/nist/identifiers/federal_information_processing_standards.rb +10 -0
  61. data/lib/pubid/nist/identifiers/report.rb +1 -2
  62. data/lib/pubid/nist/parser.rb +36 -3
  63. data/lib/pubid/nist/supplement_identifier.rb +8 -24
  64. data/lib/pubid/nist/urn_generator.rb +14 -8
  65. data/lib/pubid/nist.rb +1 -0
  66. data/lib/pubid/oiml/identifier.rb +50 -0
  67. data/lib/pubid/plateau/identifier.rb +57 -0
  68. data/lib/pubid/plateau.rb +1 -0
  69. data/lib/pubid/renderers/base.rb +34 -0
  70. data/lib/pubid/renderers/directives_renderer.rb +13 -14
  71. data/lib/pubid/renderers/guide_renderer.rb +5 -1
  72. data/lib/pubid/renderers/human_readable.rb +20 -8
  73. data/lib/pubid/renderers/iwa_renderer.rb +5 -1
  74. data/lib/pubid/renderers/supplement_renderer.rb +4 -1
  75. data/lib/pubid/rendering/context.rb +5 -2
  76. data/lib/pubid/sae/identifier.rb +23 -0
  77. data/lib/pubid/scheme.rb +12 -0
  78. data/lib/pubid/version.rb +1 -1
  79. metadata +5 -2
@@ -26,7 +26,7 @@ module Pubid
26
26
  # Render the identifier as a string in canonical IHO form.
27
27
  # @return [String]
28
28
  def to_s
29
- letter = type.is_a?(Hash) ? type[:short].to_s : type.to_s
29
+ letter = self.class.type[:short]
30
30
  rendered = "#{publisher} #{letter}-#{code}"
31
31
  rendered << " Ap. #{appendix}" if appendix
32
32
  rendered << " Part #{part}" if part
@@ -10,10 +10,6 @@ module Pubid
10
10
  { key: :bibliographic, title: "Bibliographic Publication",
11
11
  short: "B" }
12
12
  end
13
-
14
- def type
15
- self.class.type
16
- end
17
13
  end
18
14
  end
19
15
  end
@@ -9,10 +9,6 @@ module Pubid
9
9
  def self.type
10
10
  { key: :circular_letter, title: "Circular Letter", short: "C" }
11
11
  end
12
-
13
- def type
14
- self.class.type
15
- end
16
12
  end
17
13
  end
18
14
  end
@@ -10,10 +10,6 @@ module Pubid
10
10
  { key: :miscellaneous, title: "Miscellaneous Publication",
11
11
  short: "M" }
12
12
  end
13
-
14
- def type
15
- self.class.type
16
- end
17
13
  end
18
14
  end
19
15
  end
@@ -9,10 +9,6 @@ module Pubid
9
9
  def self.type
10
10
  { key: :publication, title: "Publication", short: "P" }
11
11
  end
12
-
13
- def type
14
- self.class.type
15
- end
16
12
  end
17
13
  end
18
14
  end
@@ -9,10 +9,6 @@ module Pubid
9
9
  def self.type
10
10
  { key: :standard, title: "Standards and Specifications", short: "S" }
11
11
  end
12
-
13
- def type
14
- self.class.type
15
- end
16
12
  end
17
13
  end
18
14
  end
@@ -15,7 +15,7 @@ module Pubid
15
15
 
16
16
  def generate
17
17
  parts = ["urn", "iho"]
18
- parts << identifier.type[:short].to_s.downcase
18
+ parts << identifier.class.type[:short].to_s.downcase
19
19
  parts << identifier.code.to_s
20
20
  parts << "ap.#{identifier.appendix}" if identifier.appendix
21
21
  parts << "part.#{identifier.part}" if identifier.part
@@ -125,7 +125,11 @@ module Pubid
125
125
  copublisher: value[:copublisher],
126
126
  )
127
127
  else
128
- Pubid::Iso::Components::Publisher.new(publisher: value)
128
+ # Default copublisher to [] (not nil) so a bare-string publisher
129
+ # (e.g. a TC document, which skips the copublisher merge) matches
130
+ # the [] convention used by copublisher-merged parses and by
131
+ # Identifier.create. Otherwise equality fails on [] vs nil.
132
+ Pubid::Iso::Components::Publisher.new(publisher: value, copublisher: [])
129
133
  end
130
134
 
131
135
  when :copublishers
@@ -64,6 +64,267 @@ module Pubid
64
64
  Pubid::Iso::Builder.new(Pubid::Iso::Scheme).build(parsed)
65
65
  end
66
66
  end
67
+
68
+ # Factory mirroring pubid 1.x's `Pubid::Iso::Identifier.create` API.
69
+ #
70
+ # Accepts 1.x-style primitive kwargs and dispatches to the correct
71
+ # 2.x `Identifiers::*` subclass via {Pubid::Iso::Scheme}. Coerces
72
+ # primitives into ISO-specific Component objects.
73
+ #
74
+ # Dispatch rules:
75
+ # * `type:` (e.g. `:tr`, `:amd`) → lookup via Scheme
76
+ # * else `stage:` (e.g. `"DIS"`, `"AMD"`) → lookup via Scheme
77
+ # * else → InternationalStandard
78
+ #
79
+ # @param type [Symbol, String, nil] type key (`:is`, `:tr`, `:amd`, …)
80
+ # @param stage [String, Symbol, nil] typed-stage abbreviation
81
+ # @param opts [Hash] remaining attribute primitives:
82
+ # :publisher (String), :number, :part, :subpart, :year, :edition,
83
+ # :language
84
+ # @return [Pubid::Iso::Identifier]
85
+ def self.create(type: nil, stage: nil, base: nil, **opts)
86
+ # A bundled directive (e.g. "ISO/IEC DIR 1 + IEC SUP") is stored by the
87
+ # 1.x index as the base document's fields plus a nested joint_document
88
+ # (or a supplements array). The 2.x model is a separate
89
+ # BundledIdentifier; build that so .create round-trips parse.
90
+ if opts[:joint_document] || opts[:supplements]
91
+ return build_bundled(type: type, stage: stage, base: base, **opts)
92
+ end
93
+
94
+ klass = resolve_create_class(type: type, stage: stage, base: base)
95
+ attrs = coerce_create_attrs(opts)
96
+ ts = resolve_create_typed_stage(klass, stage)
97
+ if ts
98
+ # dup the (shared) TYPED_STAGES element before tweaking, and set
99
+ # original_abbr to the canonical abbr so rendering matches a
100
+ # parsed identifier (parse records the spelled abbr, e.g. "Amd"
101
+ # not the upcased short_abbr "AMD").
102
+ ts = ts.dup
103
+ ts.original_abbr ||= Array(ts.abbr).first&.to_s
104
+ attrs[:typed_stage] = ts
105
+ # Parse fills `type` and `stage` Components derived from
106
+ # typed_stage; mirror that here so .create round-trips through
107
+ # Pubid::Identifier#== with a parsed identifier.
108
+ attrs[:type] ||= ::Pubid::Components::Type.new(
109
+ name: ts.name,
110
+ abbr: Array(ts.abbr).first.to_s,
111
+ type_code: ts.type_code&.to_s,
112
+ )
113
+ attrs[:stage] ||= ::Pubid::Components::Stage.new(
114
+ name: ts.name,
115
+ stage_code: ts.stage_code&.to_s,
116
+ abbr: Array(ts.abbr).first.to_s,
117
+ harmonized_stages: Array(ts.harmonized_stages),
118
+ )
119
+ end
120
+ # Build the base_identifier whenever a `base:` is supplied, regardless
121
+ # of whether the resolved class is registered as a supplement (e.g.
122
+ # DirectivesSupplement holds a base but is not in
123
+ # Scheme#supplement_identifiers). Only classes that *require* a base
124
+ # and were given none raise.
125
+ if base
126
+ attrs[:base_identifier] = build_base_identifier(base)
127
+ elsif supplement_klass?(klass)
128
+ raise ArgumentError, "#{klass} requires a base: identifier"
129
+ end
130
+ # For a DirectivesSupplement the top-level `publisher:` names the
131
+ # supplement's own publisher ("… ISO SUP"), not the document's — the
132
+ # document publisher lives on the base. Mirror parse, which records it
133
+ # as `supplement_publisher`.
134
+ if klass <= Identifiers::DirectivesSupplement && attrs.key?(:publisher)
135
+ attrs[:supplement_publisher] = attrs.delete(:publisher)
136
+ attrs.delete(:copublishers)
137
+ end
138
+ klass.new(**attrs)
139
+ end
140
+
141
+ # Build a BundledIdentifier (base document + supplements) from the 1.x
142
+ # index shape: the top-level fields are the base document, and each
143
+ # joint_document/supplements entry is a joined supplement.
144
+ def self.build_bundled(type:, stage:, base:, **opts)
145
+ raw = opts.delete(:joint_document) || opts.delete(:supplements)
146
+ entries = raw.is_a?(Array) ? raw : [raw]
147
+ base_document = create(type: type, stage: stage, base: base, **opts)
148
+ supplements = entries.map { |j| build_joint_supplement(j) }
149
+ BundledIdentifier.new(base_document: base_document,
150
+ supplements: supplements)
151
+ end
152
+
153
+ # Map a 1.x joint_document hash to a 2.x supplement matching parse. The
154
+ # index nests the supplement's publisher under joint_document.base.publisher
155
+ # (e.g. "IEC" for "+ IEC SUP"), while parse records it as the supplement's
156
+ # own publisher; move it and drop the nested base + empty top publisher.
157
+ def self.build_joint_supplement(joint)
158
+ j = joint.transform_keys(&:to_sym)
159
+ b = (j[:base] || {}).transform_keys(&:to_sym)
160
+ publisher = b[:publisher].to_s.empty? ? j[:publisher] : b[:publisher]
161
+ create(
162
+ type: j[:type],
163
+ publisher: publisher,
164
+ copublisher: b[:copublisher] || j[:copublisher],
165
+ number: (j[:number] unless j[:number].to_s.empty?),
166
+ year: j[:year],
167
+ )
168
+ end
169
+
170
+ # Build the base_identifier for a supplement from either an already
171
+ # constructed identifier or a 1.x-style attribute hash (the nested
172
+ # `:base` entry in a structured index). Recurses so supplement-of-
173
+ # supplement chains (e.g. a Corrigendum to an Amendment) build cleanly.
174
+ def self.build_base_identifier(base)
175
+ return base if base.is_a?(::Pubid::Identifier)
176
+
177
+ create(**base.transform_keys(&:to_sym))
178
+ end
179
+
180
+ # When `stage:` is explicit, look up the matching TypedStage via
181
+ # Scheme. Otherwise default to the chosen class's "published"
182
+ # TypedStage (so e.g. TR renders the "/TR" prefix). Returns nil if
183
+ # neither is available; the renderer then omits the stage prefix.
184
+ def self.resolve_create_typed_stage(klass, stage)
185
+ if stage
186
+ ts = locate_create_typed_stage(stage)
187
+ ts && retype_stage_for_class(klass, ts)
188
+ elsif klass.const_defined?(:TYPED_STAGES)
189
+ klass.const_get(:TYPED_STAGES).find do |ts|
190
+ ts.stage_code.to_sym == :published
191
+ end
192
+ end
193
+ end
194
+
195
+ # Indexes store a supplement's stage as the bare review-stage abbr
196
+ # ("CD", "WD", "AWI") plus a separate type ("AMD"), so the global lookup
197
+ # resolves the stage to the IS-typed variant (cdis) rather than the
198
+ # amendment-typed one (committee_draft_amd). When the resolved stage's
199
+ # type differs from the class chosen via `type:`, re-pick the equivalent
200
+ # stage from the class's own TYPED_STAGES. harmonized_stages is the
201
+ # stable cross-type key (stage_code/abbr diverge between IS and amd:
202
+ # IS "WD" is :working_draft, the amendment is :wd_amd).
203
+ def self.retype_stage_for_class(klass, ts)
204
+ return ts unless klass.const_defined?(:TYPED_STAGES)
205
+ return ts unless klass.respond_to?(:type) && klass.type
206
+ return ts if ts.type_code.to_s == klass.type[:key].to_s
207
+
208
+ harmonized = Array(ts.harmonized_stages)
209
+ return ts if harmonized.empty?
210
+
211
+ klass.const_get(:TYPED_STAGES).find do |s|
212
+ (Array(s.harmonized_stages) & harmonized).any?
213
+ end || ts
214
+ end
215
+
216
+ # Resolve a TypedStage from a create() :stage value. The index may
217
+ # supply it as an abbreviation ("DIS"), a generic stage_code (:dis),
218
+ # or a unique per-typed-stage code (:dtr, :fdisp). Try each in turn.
219
+ def self.locate_create_typed_stage(stage)
220
+ Scheme.locate_typed_stage_by_abbr(stage.to_s) ||
221
+ Scheme.locate_typed_stage_by_stage_code(stage) ||
222
+ Scheme.locate_typed_stage_by_code(stage)
223
+ end
224
+
225
+ def self.resolve_create_class(type:, stage:, base: nil)
226
+ klass =
227
+ if type
228
+ located = locate_klass_by_type_or_short(type)
229
+ raise ArgumentError, "Unknown ISO type: #{type.inspect}" unless located
230
+
231
+ located
232
+ elsif stage
233
+ ts = locate_create_typed_stage(stage)
234
+ ts && Scheme.locate_identifier_klass_by_type_code(ts.type_code)
235
+ end
236
+ # A bare `base:` with no type/stage is still a supplement; fall back to
237
+ # the generic Supplement (which can hold a base) rather than
238
+ # InternationalStandard (which cannot).
239
+ klass ||= Identifiers::Supplement if base
240
+ klass || Identifiers::InternationalStandard
241
+ end
242
+
243
+ # Try direct key lookup, then a case-insensitive key lookup (indexes
244
+ # store e.g. "DATA" but the registry key is :data), then fall back to
245
+ # matching the class's :short letter (e.g. type "R" → Recommendation,
246
+ # whose key is :rec and short is "R"). Indexes and legacy data carry
247
+ # either the key, an upper-cased key, or the short form.
248
+ def self.locate_klass_by_type_or_short(type)
249
+ Scheme.locate_identifier_klass_by_type_code(type) ||
250
+ Scheme.locate_identifier_klass_by_type_code(type.to_s.downcase) ||
251
+ Scheme.identifiers.detect { |k| k.type&.dig(:short)&.to_s == type.to_s }
252
+ end
253
+
254
+ def self.supplement_klass?(klass)
255
+ Array(Scheme.instance.supplement_identifiers).include?(klass)
256
+ end
257
+
258
+ def self.coerce_create_attrs(opts)
259
+ out = {}
260
+ if (v = opts[:publisher])
261
+ # Mirror parse: the publisher carries its copublishers in its own
262
+ # copublisher list, and each copublisher is also a standalone entry
263
+ # in the copublishers collection. No copublisher → [] (parity with
264
+ # parsed plain-ISO identifiers).
265
+ cops = Array(opts[:copublisher]).map(&:to_s)
266
+ out[:publisher] = Components::Publisher.new(
267
+ publisher: v.to_s, copublisher: cops,
268
+ )
269
+ end
270
+ if opts[:copublisher]
271
+ out[:copublishers] = Array(opts[:copublisher]).map do |c|
272
+ Components::Publisher.new(publisher: c.to_s)
273
+ end
274
+ end
275
+ %i[number part subpart].each do |k|
276
+ v = opts[k]
277
+ out[k] = Components::Code.new(number: v.to_s) unless v.nil?
278
+ end
279
+ if (v = opts[:year])
280
+ out[:date] = ::Pubid::Components::Date.new(year: v.to_s)
281
+ end
282
+ if (v = opts[:edition])
283
+ if v.is_a?(Hash)
284
+ # 1.x stored a directive org-variant ("ISO/IEC DIR 2 ISO" /
285
+ # "ISO/IEC DIR 1 IEC:2023") as edition: {publisher:, year:}; parse
286
+ # models the org as `part` and the year as `date`. Map it so create
287
+ # round-trips parse.
288
+ eh = v.transform_keys(&:to_sym)
289
+ out[:part] ||= Components::Code.new(number: eh[:publisher].to_s) if eh[:publisher]
290
+ out[:date] ||= ::Pubid::Components::Date.new(year: eh[:year].to_s) if eh[:year]
291
+ else
292
+ out[:edition] = ::Pubid::Components::Edition.new(number: v)
293
+ end
294
+ end
295
+ if (v = opts[:language])
296
+ out[:languages] =
297
+ [::Pubid::Components::Language.new(code: v.to_s)]
298
+ end
299
+ # TcDocument committee fields: the 1.x/index shape is the flat
300
+ # tctype/tcnumber/… keys, but the 2.x model stores underscored Code
301
+ # components (mirrors the parser's builder). Without this mapping a
302
+ # TC document round-trips to a bare "ISO N <num>".
303
+ { tctype: :tc_type, tcnumber: :tc_number,
304
+ sctype: :sc_type, scnumber: :sc_number,
305
+ wgtype: :wg_type, wgnumber: :wg_number }.each do |src, dest|
306
+ v = opts[src]
307
+ out[dest] = Components::Code.new(number: v.to_s) unless v.nil?
308
+ end
309
+ # Directives subgroup (e.g. "ISO/IEC JTC 1 DIR"): the 1.x/index shape
310
+ # is dirtype: "JTC", jtc_dir: "DIR", number: "1"; the 2.x model folds
311
+ # the subgroup into a single `subgroup` Code ("JTC 1") with no
312
+ # directive number, mirroring parse. Without this, .create drops the
313
+ # subgroup and collapses the id into a plain "DIR 1".
314
+ if opts[:dirtype]
315
+ out[:subgroup] = Components::Code.new(
316
+ number: [opts[:dirtype], opts[:number]].compact.join(" "),
317
+ )
318
+ out.delete(:number)
319
+ end
320
+ # TODO(create-shim): 1.x also accepted iteration, amendments,
321
+ # corrigendums, addendum, month, dir. Add as relaton call sites
322
+ # require them.
323
+ out
324
+ end
325
+ private_class_method :resolve_create_class, :supplement_klass?,
326
+ :resolve_create_typed_stage, :coerce_create_attrs,
327
+ :build_bundled, :build_joint_supplement
67
328
  end
68
329
  end
69
330
  end
@@ -45,7 +45,9 @@ module Pubid
45
45
  # ISO/JTC 1 N 456
46
46
  # ISO/TC 184/SC 4 N 789 (TC and SC only, no WG)
47
47
  rule(:tc_document) do
48
- prefix_sole_publisher >> str("/") >> tc_type >> space >> tc_number.as(:tc_number) >>
48
+ # Accept either "ISO/TC 184…" or the lenient space form "ISO TC 184…"
49
+ # (the latter was valid in pubid 1.x via an optional slash).
50
+ prefix_sole_publisher >> (str("/") | space) >> tc_type >> space >> tc_number.as(:tc_number) >>
49
51
  tc_subcommittee_part.maybe >>
50
52
  space >> str("N") >> space? >> digits.as(:number) >>
51
53
  (str(":") >> year_digits.as(:year)).maybe
@@ -378,7 +380,7 @@ module Pubid
378
380
 
379
381
  DIRECTIVES_TYPED_STAGES = Identifiers::Directives::TYPED_STAGES.map(&:abbr).flatten.sort_by(&:length).reverse
380
382
  rule(:directives_identifier_no_third) do
381
- prefix_with_copublishers >> space >>
383
+ prefix_with_copublishers >> space? >>
382
384
  (directives_publisher_subgroup >> space).maybe >>
383
385
  array_to_str(DIRECTIVES_TYPED_STAGES).as(:type_with_stage) >>
384
386
  (
@@ -36,6 +36,12 @@ module Pubid
36
36
  instance.locate_typed_stage_by_stage_code(stage_code)
37
37
  end
38
38
 
39
+ # @param code [String, Symbol] the per-typed-stage code to find
40
+ # @return [TypedStage, nil] the matching typed stage
41
+ def locate_typed_stage_by_code(code)
42
+ instance.locate_typed_stage_by_code(code)
43
+ end
44
+
39
45
  # @param harmonized_code [String] the harmonized stage code to find
40
46
  # @return [TypedStage, nil] the matching typed stage
41
47
  def locate_typed_stage_by_harmonized_code(harmonized_code)
@@ -11,7 +11,8 @@ module Pubid
11
11
 
12
12
  def build_rendering_context(_renderer, format:, with_edition: false,
13
13
  lang: :en, lang_single: false,
14
- stage_format_long: nil, with_date: nil)
14
+ stage_format_long: nil, with_date: nil,
15
+ annotated: false)
15
16
  if format == :mr_string
16
17
  nil
17
18
  elsif lang_single || stage_format_long || !with_date.nil?
@@ -19,17 +20,19 @@ module Pubid
19
20
  with_language_code: lang_single ? :single : :none,
20
21
  stage_format_long: stage_format_long || false,
21
22
  with_date: with_date.nil? || with_date,
23
+ annotated: annotated,
22
24
  )
23
25
  else
24
- detect_rendering_context
26
+ detect_rendering_context(annotated: annotated)
25
27
  end
26
28
  end
27
29
 
28
- def detect_rendering_context
30
+ def detect_rendering_context(annotated: false)
29
31
  Rendering::RenderingContext.new(
30
32
  with_language_code: detect_language_code_format,
31
33
  stage_format_long: detect_stage_format_long,
32
34
  with_date: true,
35
+ annotated: annotated,
33
36
  )
34
37
  end
35
38
 
@@ -78,9 +78,10 @@ module Pubid
78
78
  part_comp = part_component
79
79
  parts << part_comp if part_comp
80
80
 
81
- # Stage (only for non-published documents)
81
+ # Stage (only for non-published documents); an all-parts series
82
+ # reference carries no specific stage.
82
83
  stage_comp = stage_component
83
- parts << stage_comp if stage_comp
84
+ parts << stage_comp if stage_comp && !identifier.all_parts
84
85
 
85
86
  # Year (for published documents and when edition is present)
86
87
  year_comp = year_component
@@ -94,6 +95,9 @@ module Pubid
94
95
  lang_comp = language_component
95
96
  parts << lang_comp if lang_comp
96
97
 
98
+ # Series suffix for all-parts identifiers (compact, no padding)
99
+ parts << "ser" if identifier.all_parts
100
+
97
101
  parts.join(":")
98
102
  end
99
103
 
@@ -228,6 +232,9 @@ module Pubid
228
232
  end
229
233
  end
230
234
 
235
+ # Series suffix for all-parts identifiers (compact, no padding)
236
+ parts << "ser" if identifier.all_parts
237
+
231
238
  parts.join(":")
232
239
  end
233
240
 
@@ -326,7 +333,14 @@ module Pubid
326
333
  end
327
334
 
328
335
  # Format as stage-XX.XX
329
- stage_part = "stage-#{harmonized_code}"
336
+ # An iterated PRF (Proof) stage (e.g. "PRF TR 17716.2") renders as the
337
+ # symbolic "draft" stage in the URN rather than its harmonized numeric
338
+ # code; a plain PRF without an iteration keeps the harmonized code.
339
+ stage_part = if stage_code.to_s == "prf" && identifier.stage_iteration
340
+ "stage-draft"
341
+ else
342
+ "stage-#{harmonized_code}"
343
+ end
330
344
 
331
345
  # For base identifiers (not supplements), include iteration in stage code
332
346
  # For supplements, iteration goes in the version part (v1.2)
@@ -71,6 +71,15 @@ module Pubid
71
71
 
72
72
  parts = urn.sub("urn:iso:std:", "").split(":")
73
73
 
74
+ # Series suffix: a trailing "ser" marks an all-parts identifier. Strip
75
+ # it before component parsing so it is not misread as a language or
76
+ # supplement token (which previously rendered as "(SER)").
77
+ all_parts = false
78
+ if parts.last == "ser"
79
+ parts.pop
80
+ all_parts = true
81
+ end
82
+
74
83
  # Parse publisher(s) - first part
75
84
  publishers = parse_publisher(parts.shift)
76
85
 
@@ -212,7 +221,8 @@ module Pubid
212
221
 
213
222
  # Build the identifier hash
214
223
  build_identifier(publishers, number, part, subpart, type_code, stage_code, stage_iteration,
215
- harmonized_stage_code, stage_from_abbr, year, edition, languages, supplements)
224
+ harmonized_stage_code, stage_from_abbr, year, edition, languages, supplements,
225
+ all_parts)
216
226
  end
217
227
 
218
228
  private
@@ -302,7 +312,8 @@ module Pubid
302
312
 
303
313
  # Build identifier from parsed components
304
314
  def build_identifier(publishers, number, part, subpart, type_code, stage_code, stage_iteration,
305
- harmonized_stage_code, stage_from_abbr, year, edition, languages, supplements)
315
+ harmonized_stage_code, stage_from_abbr, year, edition, languages, supplements,
316
+ all_parts = false)
306
317
  # Start with base document hash
307
318
  base_hash = {
308
319
  publisher: publishers.first,
@@ -414,6 +425,9 @@ typed_stage
414
425
  }
415
426
  end
416
427
 
428
+ # all_parts belongs on the outermost identifier (after any supplement wrapping)
429
+ base_hash[:all_parts] = true if all_parts
430
+
417
431
  # Build the final identifier
418
432
  builder = Pubid::Iso::Builder.new(Pubid::Iso::Scheme.new)
419
433
  builder.build(base_hash)
@@ -17,37 +17,102 @@ module Pubid
17
17
  raise "Failed to parse ITU identifier '#{identifier}': #{e.message}"
18
18
  end
19
19
 
20
- # Factory mirroring v1's Pubid::Itu::Identifier.create API. The v1 form
21
- # accepts a `type:` discriminator plus attribute kwargs; this v2
22
- # implementation supports the subset needed by metanorma-itu PR #497:
23
- # * type: :annex — builds Identifiers::Annex
24
- # * series: "OB" (with no type:) — builds Identifiers::SpecialPublication
20
+ # Factory mirroring pubid 1.x's `Pubid::Itu::Identifier.create` API.
21
+ # Dispatch on `:type`:
22
+ # * nil Recommendation (or SpecialPublication for series "OB")
23
+ # * :recommendation → Identifiers::Recommendation
24
+ # * :annex → Identifiers::Annex
25
+ # * :special_publication → Identifiers::SpecialPublication
26
+ #
27
+ # Component-typed kwargs are accepted as primitives and coerced:
28
+ # * sector: "T" → Components::Sector.new(sector: "T")
29
+ # * series: "X" → Components::Series.new(series: "X")
30
+ # * number: "509" → Components::Code.new(number: "509")
31
+ # * year: "2020" → Pubid::Components::Date.new(year: "2020")
32
+ TYPE_KEY_TO_KLASS = {
33
+ recommendation: "Recommendation",
34
+ annex: "Annex",
35
+ special_publication: "SpecialPublication",
36
+ }.freeze
37
+
25
38
  def self.create(type: nil, **kwargs)
26
- case type
27
- when :annex
28
- Identifiers::Annex.new(**kwargs)
29
- when nil
30
- if kwargs[:series].to_s == "OB"
31
- create_special_publication(**kwargs)
32
- else
33
- raise ArgumentError,
34
- "Identifier.create without :type requires series: 'OB'"
35
- end
36
- else
37
- raise ArgumentError,
38
- "Unsupported type for Identifier.create: #{type.inspect}"
39
+ # Backward-compat: nil type + series "OB" → SpecialPublication.
40
+ if type.nil? && kwargs[:series].to_s == "OB"
41
+ return create_special_publication(**kwargs)
39
42
  end
43
+
44
+ klass = resolve_create_class(type)
45
+ klass.new(**coerce_create_attrs(kwargs, klass: klass))
46
+ end
47
+
48
+ def self.resolve_create_class(type)
49
+ return Identifiers::Recommendation if type.nil?
50
+
51
+ klass_name = TYPE_KEY_TO_KLASS[type.to_sym]
52
+ raise ArgumentError, "Unknown ITU type: #{type.inspect}" unless klass_name
53
+
54
+ Identifiers.const_get(klass_name)
40
55
  end
41
56
 
57
+ # Backward-compat helper retained for the OB-series shortcut.
42
58
  def self.create_special_publication(number:, series: "OB", date: nil,
43
- language: nil)
59
+ language: nil)
44
60
  Identifiers::SpecialPublication.new(
45
- series: Components::Series.new(series: series.to_s),
46
- code: Components::Code.new(number: number.to_s),
47
- date: date,
61
+ series: Components::Series.new(series: series.to_s),
62
+ code: Components::Code.new(number: number.to_s),
63
+ date: date,
48
64
  language: language&.to_s,
49
65
  )
50
66
  end
67
+
68
+ def self.coerce_create_attrs(opts, klass: nil)
69
+ attrs = {}
70
+ if (v = opts[:sector])
71
+ attrs[:sector] = if v.is_a?(Components::Sector)
72
+ v
73
+ else
74
+ Components::Sector.new(sector: v.to_s.upcase)
75
+ end
76
+ end
77
+ if (v = opts[:series])
78
+ attrs[:series] = if v.is_a?(Components::Series)
79
+ v
80
+ else
81
+ Components::Series.new(series: v.to_s)
82
+ end
83
+ end
84
+ if opts[:number] || opts[:code]
85
+ code_value = opts[:code]
86
+ if code_value.is_a?(Components::Code)
87
+ attrs[:code] = code_value
88
+ else
89
+ attrs[:code] = Components::Code.new(
90
+ number: (opts[:number] || code_value).to_s,
91
+ subseries: opts[:subseries]&.to_s,
92
+ parts: opts[:parts] ? Array(opts[:parts]).map(&:to_s) : nil,
93
+ )
94
+ end
95
+ end
96
+ if (v = opts[:year])
97
+ attrs[:date] = Pubid::Components::Date.new(year: v.to_s)
98
+ end
99
+ attrs[:date] = opts[:date] if opts[:date].is_a?(Pubid::Components::Date)
100
+ attrs[:language] = opts[:language].to_s if opts[:language]
101
+
102
+ # Pass through any subclass-specific kwarg the chosen class
103
+ # declares (e.g. Annex#base) — preserves callers that already pass
104
+ # an explicit attribute that our coercion table doesn't cover.
105
+ if klass
106
+ consumed = %i[sector series number code subseries parts year date
107
+ language]
108
+ opts.each do |k, v|
109
+ next if consumed.include?(k) || attrs.key?(k)
110
+ attrs[k] = v if klass.attributes.key?(k)
111
+ end
112
+ end
113
+ attrs
114
+ end
115
+ private_class_method :resolve_create_class, :coerce_create_attrs
51
116
  end
52
117
  end
53
118
  end