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
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "code"
4
+ require_relative "date"
5
+ require_relative "edition"
6
+ require_relative "language"
7
+ require_relative "publisher"
8
+
9
+ module Pubid
10
+ module Components
11
+ # Coerces loose primitive kwargs (matching pubid 1.x's `Identifier.create`
12
+ # signature) into the structured Component objects pubid 2.x expects.
13
+ #
14
+ # Used by per-flavor `.create` factories.
15
+ module Factory
16
+ # Per-kwarg coercer. Returns a Component, or an Array<Component> for
17
+ # collection attributes such as `languages`.
18
+ COERCERS = {
19
+ publisher: ->(v) { Publisher.new(body: v.to_s) },
20
+ number: ->(v) { Code.new(value: v.to_s) },
21
+ part: ->(v) { Code.new(value: v.to_s) },
22
+ subpart: ->(v) { Code.new(value: v.to_s) },
23
+ year: ->(v) { Date.new(year: v.to_s) },
24
+ edition: ->(v) { Edition.new(number: v) },
25
+ language: ->(v) { [Language.new(code: v.to_s)] },
26
+ }.freeze
27
+
28
+ # 1.x kwarg name → 2.x attribute name.
29
+ # Applied after coercion.
30
+ KEY_RENAMES = {
31
+ year: :date,
32
+ language: :languages,
33
+ }.freeze
34
+
35
+ # Convert a hash of 1.x-style primitive kwargs into a hash of 2.x-style
36
+ # Component-valued attributes ready for `<IdentifierClass>.new(...)`.
37
+ #
38
+ # Unknown keys pass through unchanged; nil values are dropped.
39
+ def self.from_hash(opts)
40
+ opts.each_with_object({}) do |(k, v), out|
41
+ next if v.nil?
42
+
43
+ target = KEY_RENAMES.fetch(k, k)
44
+ coercer = COERCERS[k]
45
+ out[target] = coercer ? coercer.call(v) : v
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -6,6 +6,10 @@ module Pubid
6
6
  module Components
7
7
  class TypedStage < Lutaml::Model::Serializable
8
8
  attribute :name, :string
9
+ # Unique per-typed-stage code (e.g. :dtr, :fdisp). Distinct from the
10
+ # generic stage_code (e.g. :draft, :fdis) which is shared across types.
11
+ # Some index data serializes this code, so it must be resolvable.
12
+ attribute :code, :string
9
13
  attribute :type_code, :string
10
14
  attribute :stage_code, :string
11
15
  attribute :abbr, :string, collection: true
@@ -5,6 +5,7 @@ module Pubid
5
5
  autoload :Code, "pubid/components/code"
6
6
  autoload :Date, "pubid/components/date"
7
7
  autoload :Edition, "pubid/components/edition"
8
+ autoload :Factory, "pubid/components/factory"
8
9
  autoload :Language, "pubid/components/language"
9
10
  autoload :Locality, "pubid/components/locality"
10
11
  autoload :Publisher, "pubid/components/publisher"
@@ -3,6 +3,62 @@
3
3
  module Pubid
4
4
  module Csa
5
5
  class Identifier
6
+ # Factory that builds a CSA identifier from a hash of primitives.
7
+ # Default is {Identifiers::Standard}.
8
+ TYPE_KEY_TO_KLASS = {
9
+ standard: "Standard",
10
+ bundled: "Bundled",
11
+ canadian_adopted: "CanadianAdopted",
12
+ cec: "Cec",
13
+ combined: "Combined",
14
+ csa_adopted: "CsaAdopted",
15
+ package: "Package",
16
+ series: "Series",
17
+ }.freeze
18
+
19
+ def self.create(type: nil, **opts)
20
+ klass = resolve_create_class(type)
21
+ klass.new(**coerce_create_attrs(opts, klass: klass))
22
+ end
23
+
24
+ def self.resolve_create_class(type)
25
+ return Identifiers::Standard if type.nil?
26
+
27
+ klass_name = TYPE_KEY_TO_KLASS[type.to_sym]
28
+ raise ArgumentError, "Unknown CSA type: #{type.inspect}" unless klass_name
29
+
30
+ Identifiers.const_get(klass_name)
31
+ end
32
+
33
+ def self.coerce_create_attrs(opts, klass:)
34
+ attrs = {}
35
+
36
+ if (v = opts[:code] || opts[:number])
37
+ attrs[:code] = Pubid::Components::Code.new(value: v.to_s)
38
+ end
39
+
40
+ if opts[:year]
41
+ year_str = opts[:year].to_s
42
+ attrs[:year] = year_str
43
+ # Preserve 4-digit year rendering (e.g. "B51:2024") rather than
44
+ # collapsing to "B51:24".
45
+ attrs[:original_year_4digit] = year_str.length == 4 unless opts.key?(:original_year_4digit)
46
+ end
47
+ attrs[:original_year_4digit] = opts[:original_year_4digit] if opts.key?(:original_year_4digit)
48
+ attrs[:year_format] = opts[:year_format].to_s if opts[:year_format]
49
+ attrs[:year_prefix] = opts[:year_prefix].to_s if opts[:year_prefix]
50
+ attrs[:reaffirmation] = opts[:reaffirmation].to_s if opts[:reaffirmation]
51
+ attrs[:french] = opts[:french] if opts.key?(:french)
52
+ attrs[:has_publisher] = opts.fetch(:has_publisher, true)
53
+ attrs[:publisher_prefix] = opts[:publisher_prefix].to_s if opts[:publisher_prefix]
54
+ attrs[:series_prefix] = opts[:series_prefix].to_s if opts[:series_prefix]
55
+ attrs[:series] = opts[:series] if opts.key?(:series) && [true, false].include?(opts[:series])
56
+ attrs[:package] = opts[:package].to_s if opts[:package]
57
+ attrs[:no_number] = opts[:no_number].to_s if opts[:no_number]
58
+ attrs
59
+ end
60
+ private_class_method :resolve_create_class, :coerce_create_attrs
61
+
6
62
  def self.parse(input)
7
63
  # Filter out comments
8
64
  return nil if input.start_with?("#")
@@ -9,6 +9,49 @@ module Pubid
9
9
  rescue Parslet::ParseFailed => e
10
10
  raise "Failed to parse ETSI identifier '#{identifier}': #{e.message}"
11
11
  end
12
+
13
+ # Factory mirroring pubid 1.x's `Pubid::Etsi::Identifier.create` API.
14
+ #
15
+ # ETSI's `type` kwarg (EN, ES, EG, TS, TR, GS, GR, GTS, …) is data
16
+ # stored on the identifier instance, not a class-dispatch key — all
17
+ # ETSI standards share the {Identifiers::EtsiStandard} class.
18
+ #
19
+ # @param opts [Hash] :type, :code/:number, :parts, :version, :year,
20
+ # :month, :date
21
+ def self.create(**opts)
22
+ Identifiers::EtsiStandard.new(**coerce_create_attrs(opts))
23
+ end
24
+
25
+ def self.coerce_create_attrs(opts)
26
+ attrs = {}
27
+ attrs[:type] = opts[:type].to_s if opts[:type]
28
+
29
+ code_value = opts[:code] || opts[:number]
30
+ if code_value
31
+ attrs[:code] = Pubid::Etsi::Components::Code.new(
32
+ number: code_value.to_s,
33
+ parts: opts[:parts] ? Array(opts[:parts]).map(&:to_s) : nil,
34
+ )
35
+ end
36
+
37
+ if (v = opts[:version])
38
+ attrs[:version] = Pubid::Etsi::Components::Version.new(
39
+ version: v.to_s,
40
+ )
41
+ end
42
+
43
+ if opts[:year] || opts[:month]
44
+ attrs[:date] = ::Pubid::Components::Date.new(
45
+ year: opts[:year]&.to_s,
46
+ month: opts[:month]&.to_s,
47
+ )
48
+ end
49
+
50
+ # TODO(create-shim): Amendment and Corrigendum supplements need a
51
+ # base_identifier and aren't yet wired through.
52
+ attrs
53
+ end
54
+ private_class_method :coerce_create_attrs
12
55
  end
13
56
  end
14
57
  end
@@ -39,6 +39,11 @@ module Pubid
39
39
  nil
40
40
  end
41
41
 
42
+ # @return [String, nil] publication year from the date component
43
+ def year
44
+ date&.year&.to_s
45
+ end
46
+
42
47
  def initialize(attrs = {}, options = {})
43
48
  attrs = attrs.dup
44
49
  attrs[:_type] ||= self.class.polymorphic_name
@@ -207,7 +212,8 @@ module Pubid
207
212
 
208
213
  def build_rendering_context(_renderer, format:, with_edition: false,
209
214
  lang: :en, lang_single: false,
210
- stage_format_long: nil, with_date: nil)
215
+ stage_format_long: nil, with_date: nil,
216
+ annotated: false)
211
217
  if format == :mr_string
212
218
  nil
213
219
  else
@@ -215,6 +221,7 @@ module Pubid
215
221
  with_language_code: lang_single ? :single : :none,
216
222
  stage_format_long: stage_format_long || false,
217
223
  with_date: with_date.nil? || with_date,
224
+ annotated: annotated,
218
225
  )
219
226
  end
220
227
  end
@@ -64,6 +64,66 @@ module Pubid
64
64
 
65
65
  Pubid::Idf::Builder.new.build(parsed)
66
66
  end
67
+
68
+ # Factory mirroring pubid 1.x's `Pubid::Idf::Identifier.create` API.
69
+ # Default subclass is {Identifiers::InternationalStandard}.
70
+ #
71
+ # IDF's renderer requires `typed_stage` to be set (calls
72
+ # `.abbreviation` without a nil check); factory auto-resolves the
73
+ # "published" TypedStage for the chosen subclass.
74
+ TYPE_KEY_TO_KLASS = {
75
+ is: "InternationalStandard",
76
+ reviewed_method: "ReviewedMethod",
77
+ }.freeze
78
+
79
+ def self.create(type: nil, stage: nil, **opts)
80
+ klass = resolve_create_class(type)
81
+ attrs = coerce_create_attrs(opts)
82
+ ts = resolve_create_typed_stage(klass, stage)
83
+ attrs[:typed_stage] = ts if ts
84
+ klass.new(**attrs)
85
+ end
86
+
87
+ def self.resolve_create_class(type)
88
+ return Identifiers::InternationalStandard if type.nil?
89
+
90
+ klass_name = TYPE_KEY_TO_KLASS[type.to_sym]
91
+ raise ArgumentError, "Unknown IDF type: #{type.inspect}" unless klass_name
92
+
93
+ Identifiers.const_get(klass_name)
94
+ end
95
+
96
+ def self.resolve_create_typed_stage(klass, stage)
97
+ return nil unless klass.const_defined?(:TYPED_STAGES)
98
+
99
+ if stage
100
+ klass.const_get(:TYPED_STAGES).find do |ts|
101
+ ts.abbr.include?(stage.to_s)
102
+ end
103
+ else
104
+ klass.const_get(:TYPED_STAGES).find do |ts|
105
+ ts.stage_code&.to_sym == :published
106
+ end
107
+ end
108
+ end
109
+
110
+ def self.coerce_create_attrs(opts)
111
+ attrs = {
112
+ publisher: Pubid::Components::Publisher.new(
113
+ body: (opts[:publisher] || "IDF").to_s,
114
+ ),
115
+ }
116
+ if (v = opts[:number])
117
+ attrs[:number] = Pubid::Components::Code.new(value: v.to_s)
118
+ end
119
+ if (v = opts[:year])
120
+ attrs[:date] = Pubid::Components::Date.new(year: v.to_s)
121
+ end
122
+ attrs
123
+ end
124
+ private_class_method :resolve_create_class,
125
+ :resolve_create_typed_stage,
126
+ :coerce_create_attrs
67
127
  end
68
128
  end
69
129
  end
@@ -434,7 +434,8 @@ edition_data = nil, typed_stage = nil)
434
434
  parse_languages(value)
435
435
 
436
436
  when :all_parts
437
- Pubid::Components::Locality.new(all_parts: true)
437
+ # Set all_parts boolean attribute directly on identifier (matches ISO builder)
438
+ true
438
439
 
439
440
  when :database
440
441
  # Database flag - return true if DB suffix present
@@ -1,10 +1,11 @@
1
1
  require "lutaml/model"
2
+ require_relative "../../components/code"
2
3
  # frozen_string_literal: true
3
4
 
4
5
  module Pubid
5
6
  module Iec
6
7
  module Components
7
- class Code < Lutaml::Model::Serializable
8
+ class Code < ::Pubid::Components::Code
8
9
  attribute :prefix, :string, default: -> {}
9
10
  attribute :number, :string
10
11
  attribute :part, :string, default: -> {}
@@ -1,10 +1,11 @@
1
1
  require "lutaml/model"
2
+ require_relative "../../components/publisher"
2
3
  # frozen_string_literal: true
3
4
 
4
5
  module Pubid
5
6
  module Iec
6
7
  module Components
7
- class Publisher < Lutaml::Model::Serializable
8
+ class Publisher < ::Pubid::Components::Publisher
8
9
  PUBLISHERS = {
9
10
  "IEC" => "International Electrotechnical Commission",
10
11
  "ISO/IEC" => "ISO/IEC Joint Technical Committee",
@@ -1,11 +1,25 @@
1
1
  require_relative "../identifier"
2
2
  # frozen_string_literal: true
3
3
  require_relative "../components/typed_stage"
4
+ require_relative "../components/factory"
4
5
 
5
6
  module Pubid
6
7
  module Iec
7
8
  class Identifier < ::Pubid::Identifier
9
+ # Long-tail document types that `create` may build but which are not in
10
+ # Scheme.identifiers (the parse/build candidate set). Loaded lazily (not
11
+ # at require time) to avoid a circular load with identifiers/base.rb.
12
+ EXTRA_CREATE_KLASS_FILES = %w[
13
+ conformity_assessment technology_report white_paper
14
+ societal_technology_trend_report systems_reference_document
15
+ interpretation_sheet
16
+ ].freeze
8
17
  def self.parse(string)
18
+ # Route URN strings to the URN parser (mirrors Iso::Identifier.parse)
19
+ if Pubid::FormatDetector.detect(string) == :urn
20
+ return Pubid::Iec::UrnParser.parse(string)
21
+ end
22
+
9
23
  # Apply legacy update_codes normalization first, before any other preprocessing
10
24
  normalized = Core::UpdateCodes.apply(string, :iec)
11
25
  parsed = Pubid::Iec::Parser.new.parse(normalized)
@@ -16,6 +30,227 @@ module Pubid
16
30
 
17
31
  Pubid::Iec::Builder.new(Pubid::Iec::Scheme).build(parsed)
18
32
  end
33
+
34
+ # Factory mirroring pubid 1.x's `Pubid::Iec::Identifier.create` API.
35
+ # See {Pubid::Iso::Identifier.create} for the shared design. Builds the
36
+ # IEC `Components::*` subclasses (not the base ones) and populates
37
+ # type/stage from the resolved TypedStage, so that a created identifier
38
+ # round-trips `==` against the same identifier produced by `parse`.
39
+ def self.create(type: nil, stage: nil, **opts)
40
+ # A VAP suffix (CSV/RLV/…) is the outermost wrapper around the base
41
+ # document (which may itself be amended); rebuild it first.
42
+ if (vap = opts.delete(:vap))
43
+ base = create(type: type, stage: stage, **opts)
44
+ return Identifiers::VapIdentifier.new(
45
+ base_identifier: base,
46
+ vap_suffix: Components::VapSuffix.new(code: Array(vap).first.to_s),
47
+ )
48
+ end
49
+
50
+ # Structured index rows carry amendments/corrigendums as a flat list
51
+ # alongside the base document's keys; rebuild the supplement wrapping
52
+ # the recursively-created base, mirroring what parse produces.
53
+ if (supp = extract_supplement(opts))
54
+ return build_supplement(supp, type: type, stage: stage, opts: opts)
55
+ end
56
+
57
+ # A nested base: holds the base document of a supplement whose own
58
+ # number/year sit at the top level (e.g. an Interpretation Sheet).
59
+ if (base_hash = opts.delete(:base))
60
+ return build_based_supplement(type: type, base_hash: base_hash,
61
+ opts: opts)
62
+ end
63
+
64
+ klass = resolve_create_class(type: type, stage: stage)
65
+ attrs = coerce_create_attrs(opts)
66
+ ts = resolve_create_typed_stage(klass, stage)
67
+ if ts
68
+ attrs[:typed_stage] = ts
69
+ # Parse derives `type` and `stage` from the TypedStage (see
70
+ # Builder#cast for :type_with_stage); mirror that here.
71
+ attrs[:type] ||= ts.to_type
72
+ attrs[:stage] ||= ts.to_stage
73
+ end
74
+ klass.new(**attrs)
75
+ end
76
+
77
+ # Pop a supplement spec off the flat opts hash, if present. Returns
78
+ # { klass:, entry: } or nil. Amendments take precedence over
79
+ # corrigendums for the (rare) consolidated rows that carry both.
80
+ def self.extract_supplement(opts)
81
+ if (amds = opts.delete(:amendments))
82
+ { klass: Identifiers::Amendment, entry: Array(amds).first }
83
+ elsif (cors = opts.delete(:corrigendums))
84
+ { klass: Identifiers::Corrigendum, entry: Array(cors).first }
85
+ end
86
+ end
87
+
88
+ # Build an Amendment/Corrigendum from { number:, year: } wrapping a base
89
+ # identifier created from the remaining opts (which may themselves carry
90
+ # a type, e.g. an amendment to a TR).
91
+ def self.build_supplement(supp, type:, stage:, opts:)
92
+ base = create(type: type, stage: stage, **opts)
93
+ klass = supp[:klass]
94
+ entry = supp[:entry] || {}
95
+ ts = klass::TYPED_STAGES.find { |t| t.stage_code.to_sym == :published }
96
+ attrs = { base_identifier: base, typed_stage: ts,
97
+ type: ts.to_type, stage: ts.to_stage }
98
+ if (n = entry[:number])
99
+ attrs[:number] = Components::Code.new(number: n.to_s)
100
+ end
101
+ if (y = entry[:year])
102
+ attrs[:date] = ::Pubid::Components::Date.new(year: y.to_s)
103
+ end
104
+ klass.new(**attrs)
105
+ end
106
+
107
+ # Build a supplement whose base document is carried in a nested base:
108
+ # hash (e.g. type: "ISH"); the supplement's own number/year are the
109
+ # remaining top-level keys.
110
+ def self.build_based_supplement(type:, base_hash:, opts:)
111
+ klass = (type && locate_klass_by_type_or_short(type)) ||
112
+ Identifiers::InternationalStandard
113
+ base = create(**base_hash.transform_keys(&:to_sym))
114
+ attrs = coerce_create_attrs(opts)
115
+ .slice(:number, :part, :subpart, :date, :publisher, :copublishers)
116
+ attrs[:base_identifier] = base
117
+ if klass.const_defined?(:TYPED_STAGES) &&
118
+ (ts = klass::TYPED_STAGES.find { |t| t.stage_code.to_sym == :published })
119
+ attrs[:typed_stage] = ts
120
+ attrs[:type] ||= ts.to_type
121
+ attrs[:stage] ||= ts.to_stage
122
+ end
123
+ klass.new(**attrs)
124
+ end
125
+
126
+ # Coerce a 1.x-style attribute hash into IEC Component instances,
127
+ # matching what the parser/builder produces. Unknown keys are dropped.
128
+ def self.coerce_create_attrs(opts)
129
+ out = {}
130
+ if (v = opts[:publisher])
131
+ out[:publisher] = Components::Publisher.new(body: v.to_s)
132
+ end
133
+ if (copubs = opts[:copublishers] || opts[:copublisher])
134
+ out[:copublishers] =
135
+ Array(copubs).map { |c| Components::Publisher.new(body: c.to_s) }
136
+ end
137
+ if (v = opts[:number])
138
+ out[:number] = Components::Code.new(number: v.to_s)
139
+ end
140
+ # Indexes fold the subpart into a single "2-4" part string; parse
141
+ # keeps part and subpart separate, so split to match.
142
+ part, subpart = split_part(opts[:part], opts[:subpart])
143
+ out[:part] = Components::Code.new(number: part.to_s) unless part.nil?
144
+ out[:subpart] = Components::Code.new(number: subpart.to_s) unless subpart.nil?
145
+ if (v = opts[:year])
146
+ out[:date] = ::Pubid::Components::Date.new(year: v.to_s)
147
+ end
148
+ if (v = opts[:edition])
149
+ out[:edition] = ::Pubid::Components::Edition.new(number: v)
150
+ end
151
+ if (v = opts[:language])
152
+ out[:languages] = [::Pubid::Components::Language.new(code: v.to_s)]
153
+ end
154
+ out[:database] = true if opts[:database]
155
+ out
156
+ end
157
+
158
+ # Split a folded "2-4" part into [part, subpart], matching parse. A part
159
+ # without a dash (or an explicit subpart already supplied) is untouched.
160
+ def self.split_part(part, subpart)
161
+ return [nil, subpart] if part.nil?
162
+ if subpart.nil? && part.to_s.include?("-")
163
+ part.to_s.split("-", 2)
164
+ else
165
+ [part, subpart]
166
+ end
167
+ end
168
+
169
+ def self.resolve_create_class(type:, stage:)
170
+ if type && supplement_type?(type)
171
+ # Supplements are built from amendments:/corrigendums: data (which
172
+ # carry the supplement number/year); an explicit supplement `type:`
173
+ # alone has no base to wrap.
174
+ raise ArgumentError,
175
+ "#{type} requires a base_identifier; pass amendments:/" \
176
+ "corrigendums: instead of type:"
177
+ end
178
+
179
+ klass =
180
+ if type
181
+ locate_klass_by_type_or_short(type)
182
+ elsif stage
183
+ ts = safe_locate_typed_stage(stage)
184
+ ts && locate_klass_by_type_or_short(ts.type_code)
185
+ end
186
+ klass || Identifiers::InternationalStandard
187
+ end
188
+
189
+ # True when `type` names a supplement identifier (Amendment/Corrigendum/
190
+ # Fragment) by key, downcased key, or short abbreviation.
191
+ def self.supplement_type?(type)
192
+ t = type.to_s
193
+ Scheme.supplement_identifiers.any? do |k|
194
+ k.type[:key].to_s == t || k.type[:key].to_s == t.downcase ||
195
+ Array(k.type[:short]).map(&:to_s).include?(t)
196
+ end
197
+ end
198
+
199
+ # Structured indexes store `type:` as the registry key (:tr), an
200
+ # upper-cased abbreviation ("TR"), or a title ("Technology Report").
201
+ # Try each spelling, across every IEC identifier class (the create
202
+ # candidate set is wider than Scheme.identifiers).
203
+ def self.locate_klass_by_type_or_short(type)
204
+ t = type.to_s
205
+ all_create_klasses.detect { |k| k.type[:key].to_s == t } ||
206
+ all_create_klasses.detect { |k| k.type[:key].to_s == t.downcase } ||
207
+ all_create_klasses.detect { |k| Array(k.type[:short]).map(&:to_s).include?(t) } ||
208
+ all_create_klasses.detect { |k| k.type[:title].to_s == t }
209
+ end
210
+
211
+ # All IEC identifier classes that `create` may build, including the
212
+ # long-tail document types absent from Scheme.identifiers. Memoized;
213
+ # the extra classes are required here (not at load time) to dodge the
214
+ # circular require with identifiers/base.rb.
215
+ def self.all_create_klasses
216
+ @all_create_klasses ||= begin
217
+ extra = EXTRA_CREATE_KLASS_FILES.map do |f|
218
+ require_relative "identifiers/#{f}"
219
+ Identifiers.const_get(camelize_klass_file(f))
220
+ end
221
+ Scheme.identifiers + extra
222
+ end
223
+ end
224
+
225
+ def self.camelize_klass_file(file)
226
+ file.split("_").map(&:capitalize).join
227
+ end
228
+
229
+ # IEC Scheme raises ArgumentError on miss instead of returning nil;
230
+ # wrap so the same control flow works as in the ISO factory.
231
+ def self.safe_locate_typed_stage(abbr)
232
+ Scheme.locate_typed_stage_by_abbr(abbr.to_s)
233
+ rescue ArgumentError
234
+ nil
235
+ end
236
+
237
+ def self.resolve_create_typed_stage(klass, stage)
238
+ if stage
239
+ safe_locate_typed_stage(stage)
240
+ elsif klass.const_defined?(:TYPED_STAGES)
241
+ klass.const_get(:TYPED_STAGES).find do |ts|
242
+ ts.stage_code.to_sym == :published
243
+ end
244
+ end
245
+ end
246
+ private_class_method :resolve_create_class,
247
+ :safe_locate_typed_stage,
248
+ :resolve_create_typed_stage, :coerce_create_attrs,
249
+ :extract_supplement, :build_supplement,
250
+ :build_based_supplement,
251
+ :locate_klass_by_type_or_short, :all_create_klasses,
252
+ :camelize_klass_file, :supplement_type?,
253
+ :split_part
19
254
  end
20
255
  end
21
256
  end
@@ -38,6 +38,10 @@ module Pubid
38
38
  # Database flag
39
39
  parts << " DB" if database
40
40
 
41
+ # All-parts marker — rendered the same as the generic
42
+ # HumanReadable renderer for parity (see Pubid::Renderers::HumanReadable#render).
43
+ parts << " (all parts)" if all_parts
44
+
41
45
  parts.compact.join
42
46
  end
43
47
 
@@ -59,10 +59,6 @@ module Pubid
59
59
  identifiers&.first&.date
60
60
  end
61
61
 
62
- def type
63
- :consolidated
64
- end
65
-
66
62
  def stage
67
63
  identifiers&.first&.stage
68
64
  end
@@ -128,10 +128,6 @@ module Pubid
128
128
  base_identifier&.date
129
129
  end
130
130
 
131
- def type
132
- :frag
133
- end
134
-
135
131
  def stage
136
132
  typed_stage&.to_stage || base_identifier&.stage
137
133
  end
@@ -49,10 +49,6 @@ module Pubid
49
49
  base_identifier&.date
50
50
  end
51
51
 
52
- def type
53
- :sheet
54
- end
55
-
56
52
  def stage
57
53
  base_identifier&.stage
58
54
  end
@@ -60,10 +60,6 @@ module Pubid
60
60
  base_identifier&.date
61
61
  end
62
62
 
63
- def type
64
- :vap
65
- end
66
-
67
63
  def stage
68
64
  base_identifier&.stage
69
65
  end
@@ -136,8 +136,13 @@ module Pubid
136
136
  end
137
137
 
138
138
  rule(:number) do
139
- # Special case for VIM publication
140
- str("VIM") |
139
+ # ISO/IEC Directives: "DIR", "DIR 1", "DIR 1 IEC SUP", "DIR 2 IEC", "DIR IEC SUP"
140
+ # Captured as a flat number (no part/subpart) so it round-trips to the input
141
+ # and class-matches the IEC index. Must come before the IECEE [A-Z]{2,4} branch,
142
+ # which would otherwise match "DIR" alone and leave the trailing tokens unconsumed.
143
+ (str("DIR") >> (space >> match('[A-Z\d]').repeat(1)).repeat) |
144
+ # Special case for VIM publication
145
+ str("VIM") |
141
146
  # Special case for SYMBOL publication
142
147
  str("SYMBOL") |
143
148
  # IECEx TRF version notation: 62784v1a_ds, 62784v1A