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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e650437c35c0d8e6ba019b07ae835ca224bd620415bd9c9d27c20a36a1b82589
4
- data.tar.gz: 3d8d15f420d6cff7dbba69007218d670cde06511fc00df7b7dcfd957493600cb
3
+ metadata.gz: 16698d2e32777261efbf6842754351b03a2a0d272992ca1525ebd3fbedaf648d
4
+ data.tar.gz: 46d2aa4fb40d9f9ab3fcf7f42bdc3c9b2662637653a9e5c98f26baebecaea0cd
5
5
  SHA512:
6
- metadata.gz: f231838d92fd52c9d8fb298899a6f62018c29872d0d67104ea064ea70ea5cd4e115490876e2e83ba9601da87aeb9060d9f28e34535d0495ca680bc93ad6801ec
7
- data.tar.gz: de691a34d6783e9df75f4304315d6791eb30e68b7788177f8e04285aa8f88372e38ce9754527eb8479db0926ef9276ddef7c6fae1ede170380541459c150a823
6
+ metadata.gz: 638b16ccaed8a371fecee3ec8372a9790ae4649d689d0aeafa306b4b1681e0df42365d5ede2da46da743f8b42e7a70ddcc882774a6c6c333a1a2fec8d30255e4
7
+ data.tar.gz: 2d55ed5b29d6121665c808d697832307bcea629345b7782308c5c0fe76ffc9baae849f752a8f75b5d77c7b3aaed782be9e15dcc9e1dc85e7c229ae77cf4431b8
@@ -26,7 +26,9 @@ NIST.CSWP.01162020pt: NIST.CSWP.01162020(por)
26
26
  FIPS PUB: FIPS
27
27
  NBS CRPL c4-4: NBS CRPL 4-4
28
28
  NIST AMS 300-8r1 (February 2021 update): NIST AMS 300-8r1/Upd1-202102
29
+ NIST AMS 300-8r1/upd: NIST AMS 300-8r1/Upd1-202102
29
30
  NIST IR 8115r1-upd: NIST IR 8115r1/Upd1-202103
31
+ NIST IR 8170-upd: NIST IR 8170/Upd1-202003
30
32
  NIST TN 2150-upd: NIST TN 2150/Upd1-202102
31
33
  NBS IR 73-197r: NBS IR 73-197r1
32
34
  NBS.HB.105-1r1990: NIST.HB.105-1r1990
@@ -13,6 +13,45 @@ module Pubid
13
13
  rescue Parslet::ParseFailed => e
14
14
  raise "Failed to parse ACMA identifier '#{identifier}': #{e.message}"
15
15
  end
16
+
17
+ # Factory that builds an AMCA identifier from a hash of primitives.
18
+ #
19
+ # Dispatch on `:type`:
20
+ # * `:standard` (default) → Identifiers::Standard
21
+ # * `:publication` → Identifiers::Publication
22
+ # * `:interpretation` → Identifiers::Interpretation
23
+ #
24
+ # AMCA stores the "AMCA" prefix in the `copublisher` string
25
+ # attribute (matching parsed output); `.create` defaults it to
26
+ # "AMCA" unless the caller supplies one.
27
+ def self.create(type: nil, **opts)
28
+ klass = resolve_create_class(type)
29
+ klass.new(**coerce_create_attrs(opts))
30
+ end
31
+
32
+ def self.resolve_create_class(type)
33
+ case type&.to_sym
34
+ when nil, :standard then Identifiers::Standard
35
+ when :publication then Identifiers::Publication
36
+ when :interpretation then Identifiers::Interpretation
37
+ else
38
+ raise ArgumentError, "Unknown AMCA type: #{type.inspect}"
39
+ end
40
+ end
41
+
42
+ def self.coerce_create_attrs(opts)
43
+ attrs = { copublisher: (opts[:copublisher] || "AMCA").to_s }
44
+ if (v = opts[:code] || opts[:number])
45
+ attrs[:code] = Pubid::Components::Code.new(value: v.to_s)
46
+ end
47
+ if (v = opts[:year])
48
+ attrs[:year] = Pubid::Components::Date.new(year: v.to_s)
49
+ end
50
+ attrs[:suffix] = opts[:suffix].to_s if opts[:suffix]
51
+ attrs[:reaffirmed] = opts[:reaffirmed].to_s if opts[:reaffirmed]
52
+ attrs
53
+ end
54
+ private_class_method :resolve_create_class, :coerce_create_attrs
16
55
  end
17
56
  end
18
57
  end
@@ -8,6 +8,48 @@ module Pubid
8
8
  parsed = Pubid::Ansi::Parser.new.parse(string)
9
9
  Pubid::Ansi::Builder.new(Pubid::Ansi::Scheme).build(parsed)
10
10
  end
11
+
12
+ # Factory that builds an ANSI identifier from a hash of primitives.
13
+ #
14
+ # Defaults to {Identifiers::Standard} (ANSI has Standard and
15
+ # AmericanNationalStandard, both sharing the same type key `:ans`).
16
+ #
17
+ # ANSI quirk: the parser stores the publication year in the `part`
18
+ # attribute, not `date`. `.create` mirrors that — `:year` is coerced
19
+ # into `part` so the rendered output matches a parsed identifier.
20
+ def self.create(type: nil, **opts)
21
+ klass = resolve_create_class(type)
22
+ klass.new(**coerce_create_attrs(opts))
23
+ end
24
+
25
+ def self.resolve_create_class(type)
26
+ case type&.to_sym
27
+ when nil, :ans, :standard
28
+ Identifiers::Standard
29
+ when :american_national_standard
30
+ Identifiers::AmericanNationalStandard
31
+ else
32
+ raise ArgumentError, "Unknown ANSI type: #{type.inspect}"
33
+ end
34
+ end
35
+
36
+ def self.coerce_create_attrs(opts)
37
+ attrs = {
38
+ publisher: Pubid::Components::Publisher.new(
39
+ body: (opts[:publisher] || "ANSI").to_s,
40
+ ),
41
+ }
42
+ if (v = opts[:number])
43
+ attrs[:number] = Pubid::Components::Code.new(value: v.to_s)
44
+ end
45
+ # ANSI parser stores year in part; mirror that here.
46
+ year_or_part = opts[:year] || opts[:part]
47
+ if year_or_part
48
+ attrs[:part] = Pubid::Components::Code.new(value: year_or_part.to_s)
49
+ end
50
+ attrs
51
+ end
52
+ private_class_method :resolve_create_class, :coerce_create_attrs
11
53
  end
12
54
  end
13
55
  end
@@ -3,6 +3,7 @@
3
3
  require_relative "parser"
4
4
  require_relative "builder"
5
5
  require_relative "single_identifier"
6
+ require_relative "scheme"
6
7
 
7
8
  module Pubid
8
9
  module Api
@@ -16,6 +17,52 @@ module Pubid
16
17
  rescue Parslet::ParseFailed => e
17
18
  raise e
18
19
  end
20
+
21
+ # Factory that builds an API identifier from a hash of primitives.
22
+ # Dispatch on `:type` to the matching subclass; default is
23
+ # {Identifiers::Standard}.
24
+ #
25
+ # API identifiers have a hardcoded "API" publisher (via
26
+ # SingleIdentifier#publisher method) so the `:publisher` kwarg is
27
+ # silently dropped.
28
+ TYPE_KEY_TO_KLASS = {
29
+ std: "Standard",
30
+ rp: "RecommendedPractice",
31
+ spec: "Specification",
32
+ tr: "TechnicalReport",
33
+ bull: "Bulletin",
34
+ mpms: "Mpms",
35
+ cos: "ContinuousOperationsStandard",
36
+ publ: "Publication",
37
+ typeless: "TypelessStandard",
38
+ }.freeze
39
+
40
+ def self.create(type: nil, **opts)
41
+ klass = resolve_create_class(type)
42
+ klass.new(**coerce_create_attrs(opts))
43
+ end
44
+
45
+ def self.resolve_create_class(type)
46
+ return Identifiers::Standard if type.nil?
47
+
48
+ klass_name = TYPE_KEY_TO_KLASS[type.to_sym]
49
+ raise ArgumentError, "Unknown API type: #{type.inspect}" unless klass_name
50
+
51
+ Identifiers.const_get(klass_name)
52
+ end
53
+
54
+ def self.coerce_create_attrs(opts)
55
+ attrs = {}
56
+ if (v = opts[:code] || opts[:number])
57
+ attrs[:code] = Pubid::Api::Components::Code.new(value: v.to_s)
58
+ end
59
+ %i[part year reaffirmation].each do |k|
60
+ attrs[k] = opts[k].to_s unless opts[k].nil?
61
+ end
62
+ # TODO(create-shim): :publisher silently dropped (API hardcoded).
63
+ attrs
64
+ end
65
+ private_class_method :resolve_create_class, :coerce_create_attrs
19
66
  end
20
67
  end
21
68
  end
@@ -13,6 +13,45 @@ module Pubid
13
13
  rescue Parslet::ParseFailed => e
14
14
  raise "Failed to parse ASHRAE identifier '#{identifier}': #{e.message}"
15
15
  end
16
+
17
+ # Factory that builds an ASHRAE identifier from a hash of primitives.
18
+ # Dispatch on `:type` to the matching subclass; default is
19
+ # {Identifiers::Standard}.
20
+ TYPE_KEY_TO_KLASS = {
21
+ standard: "Standard",
22
+ guideline: "Guideline",
23
+ addendum: "Addendum",
24
+ addenda_package: "AddendaPackage",
25
+ combined_addenda: "CombinedAddenda",
26
+ errata: "Errata",
27
+ interpretation: "Interpretation",
28
+ }.freeze
29
+
30
+ def self.create(type: nil, **opts)
31
+ klass = resolve_create_class(type)
32
+ klass.new(**coerce_create_attrs(opts))
33
+ end
34
+
35
+ def self.resolve_create_class(type)
36
+ return Identifiers::Standard if type.nil?
37
+
38
+ klass_name = TYPE_KEY_TO_KLASS[type.to_sym]
39
+ raise ArgumentError, "Unknown ASHRAE type: #{type.inspect}" unless klass_name
40
+
41
+ Identifiers.const_get(klass_name)
42
+ end
43
+
44
+ def self.coerce_create_attrs(opts)
45
+ attrs = { publisher: (opts[:publisher] || "ASHRAE").to_s }
46
+ if (v = opts[:code] || opts[:number])
47
+ attrs[:code] = v.to_s
48
+ end
49
+ %i[year suffix amendment reaffirmed copublisher].each do |k|
50
+ attrs[k] = opts[k].to_s unless opts[k].nil?
51
+ end
52
+ attrs
53
+ end
54
+ private_class_method :resolve_create_class, :coerce_create_attrs
16
55
  end
17
56
  end
18
57
  end
@@ -10,6 +10,52 @@ module Pubid
10
10
  parsed = parser.parse(str)
11
11
  builder.build(parsed)
12
12
  end
13
+
14
+ # Factory that builds an ASME identifier from a hash of primitives.
15
+ # Default is {Identifiers::Standard}. ASME's Code Component has
16
+ # `:designator` (leading letters, e.g. "B") and `:number` (rest,
17
+ # e.g. "16.34"); `.create` accepts either an already-split pair or
18
+ # a combined `:code` string which it splits at the first digit.
19
+ def self.create(**opts)
20
+ Identifiers::Standard.new(**coerce_create_attrs(opts))
21
+ end
22
+
23
+ def self.coerce_create_attrs(opts)
24
+ attrs = { publisher: (opts[:publisher] || "ASME").to_s }
25
+
26
+ if opts[:designator] || opts[:number]
27
+ attrs[:code] = Pubid::Asme::Components::Code.new(
28
+ designator: opts[:designator]&.to_s,
29
+ number: opts[:number]&.to_s,
30
+ )
31
+ elsif (combined = opts[:code])
32
+ designator, number = split_asme_code(combined.to_s)
33
+ attrs[:code] = Pubid::Asme::Components::Code.new(
34
+ designator: designator,
35
+ number: number,
36
+ )
37
+ end
38
+
39
+ %i[year reaffirmation language csa_number draft_year
40
+ revision_note parenthetical_revision ptc_suffix
41
+ joint_publisher first_publisher first_code].each do |k|
42
+ attrs[k] = opts[k].to_s unless opts[k].nil?
43
+ end
44
+ attrs[:handbook] = opts[:handbook] if opts.key?(:handbook)
45
+ attrs
46
+ end
47
+
48
+ # ASME codes pair a letter designator with a numeric/dotted number.
49
+ # Split at the first digit. "B16.34" → ["B", "16.34"]; "Y14.5" →
50
+ # ["Y", "14.5"]; "BPVC.III.1.NB" → ["BPVC.III.1.NB", nil] (no
51
+ # leading all-letters before the first digit — pass through).
52
+ def self.split_asme_code(str)
53
+ match = str.match(/\A([A-Za-z]+)(\d.*)\z/)
54
+ return match.captures if match
55
+
56
+ [str, nil]
57
+ end
58
+ private_class_method :coerce_create_attrs, :split_asme_code
13
59
  end
14
60
  end
15
61
  end
@@ -10,6 +10,83 @@ module Pubid
10
10
  parsed = parser.parse(str)
11
11
  builder.build(parsed)
12
12
  end
13
+
14
+ # Factory that builds an ASTM identifier from a hash of primitives.
15
+ # Default is {Identifiers::Standard}.
16
+ #
17
+ # ASTM's Code Component has :letter (single letter A–Z) + :number
18
+ # (rest); `.create` accepts either an already-split pair or a
19
+ # combined `:code` string which it splits at the first digit.
20
+ TYPE_KEY_TO_KLASS = {
21
+ standard: "Standard",
22
+ adjunct: "Adjunct",
23
+ manual: "Manual",
24
+ monograph: "Monograph",
25
+ research_report: "ResearchReport",
26
+ technical_report: "TechnicalReport",
27
+ data_series: "DataSeries",
28
+ iso_dual_published: "IsoDualPublished",
29
+ work_in_progress: "WorkInProgress",
30
+ }.freeze
31
+
32
+ def self.create(type: nil, **opts)
33
+ klass = resolve_create_class(type)
34
+ klass.new(**coerce_create_attrs(opts, klass: klass))
35
+ end
36
+
37
+ def self.resolve_create_class(type)
38
+ return Identifiers::Standard if type.nil?
39
+
40
+ klass_name = TYPE_KEY_TO_KLASS[type.to_sym]
41
+ raise ArgumentError, "Unknown ASTM type: #{type.inspect}" unless klass_name
42
+
43
+ Identifiers.const_get(klass_name)
44
+ end
45
+
46
+ def self.coerce_create_attrs(opts, klass:)
47
+ attrs = { publisher: (opts[:publisher] || "ASTM").to_s }
48
+
49
+ if opts[:letter] || opts[:number]
50
+ attrs[:code] = Pubid::Astm::Components::Code.new(
51
+ letter: opts[:letter]&.to_s,
52
+ number: opts[:number]&.to_s,
53
+ suffix: opts[:suffix]&.to_s,
54
+ subseries: opts[:subseries]&.to_s,
55
+ dual_m: opts[:dual_m],
56
+ )
57
+ elsif (combined = opts[:code])
58
+ letter, number = split_astm_code(combined.to_s)
59
+ attrs[:code] = Pubid::Astm::Components::Code.new(
60
+ letter: letter, number: number,
61
+ )
62
+ end
63
+
64
+ attrs[:year] = opts[:year].to_s if opts[:year]
65
+ attrs[:format_suffix] = opts[:format_suffix].to_s if opts[:format_suffix]
66
+
67
+ # Pass through subclass-specific kwargs (designation, edition,
68
+ # committee, …) when the class declares them.
69
+ opts.each do |k, v|
70
+ next if attrs.key?(k)
71
+ next if %i[publisher code letter number suffix subseries
72
+ dual_m year format_suffix].include?(k)
73
+ attrs[k] = v if klass.attributes.key?(k)
74
+ end
75
+ attrs
76
+ end
77
+
78
+ # ASTM codes use a single letter prefix (A–Z) followed by digits.
79
+ # "A36" → ["A", "36"]; "E1444" → ["E", "1444"]. Codes without a
80
+ # leading letter (data series numbers, etc.) pass through with
81
+ # letter: nil.
82
+ def self.split_astm_code(str)
83
+ match = str.match(/\A([A-Za-z])(\d.*)\z/)
84
+ return match.captures if match
85
+
86
+ [nil, str]
87
+ end
88
+ private_class_method :resolve_create_class, :coerce_create_attrs,
89
+ :split_astm_code
13
90
  end
14
91
  end
15
92
  end
@@ -22,6 +22,66 @@ module Pubid
22
22
  rescue Parslet::ParseFailed => e
23
23
  raise StandardError, "Failed to parse '#{string}': #{e.message}"
24
24
  end
25
+
26
+ # Factory mirroring pubid 1.x's `Pubid::Bsi::Identifier.create` API.
27
+ # Dispatches via {Pubid::Bsi::Scheme}'s IDENTIFIER_CLASS_MAP and
28
+ # TYPED_STAGES_REGISTRY; default subclass is BritishStandard.
29
+ #
30
+ # BSI's renderer requires BSI-namespaced Component subclasses
31
+ # (`Bsi::Components::*`), so coercion is inlined rather than reusing
32
+ # {Pubid::Components::Factory}.
33
+ def self.create(type: nil, stage: nil, **opts)
34
+ klass = resolve_create_class(type: type, stage: stage)
35
+ attrs = coerce_create_attrs(opts)
36
+ ts = resolve_create_typed_stage(klass, stage)
37
+ attrs[:typed_stage] = ts if ts
38
+ klass.new(**attrs)
39
+ end
40
+
41
+ def self.resolve_create_class(type:, stage:)
42
+ scheme = Scheme.new
43
+ klass = nil
44
+ if type
45
+ klass = scheme.locate_identifier_klass_by_type_code(type)
46
+ elsif stage
47
+ ts = scheme.locate_typed_stage_by_abbr(stage.to_s)
48
+ klass = scheme.locate_identifier_klass_by_type_code(ts.type_code) if ts
49
+ end
50
+ klass || Identifiers::BritishStandard
51
+ end
52
+
53
+ def self.resolve_create_typed_stage(klass, stage)
54
+ if stage
55
+ Scheme.new.locate_typed_stage_by_abbr(stage.to_s)
56
+ elsif klass.const_defined?(:TYPED_STAGES)
57
+ klass.const_get(:TYPED_STAGES).find do |ts|
58
+ ts.stage_code.to_sym == :published
59
+ end
60
+ end
61
+ end
62
+
63
+ def self.coerce_create_attrs(opts)
64
+ attrs = {}
65
+ if (v = opts[:publisher])
66
+ attrs[:publisher] = Components::Publisher.new(body: v.to_s)
67
+ end
68
+ %i[number part subpart].each do |k|
69
+ v = opts[k]
70
+ attrs[k] = Components::Code.new(value: v.to_s) unless v.nil?
71
+ end
72
+ if (v = opts[:year])
73
+ attrs[:date] = Components::Date.new(year: v.to_s)
74
+ end
75
+ # BSI :edition is a plain string attribute (not a Component).
76
+ attrs[:edition] = opts[:edition].to_s if opts[:edition]
77
+ # TODO(create-shim): BSI also has prefix, flex_prefix, iteration,
78
+ # second_number, month, translation_lang/upper attributes that
79
+ # aren't yet wired through. Add as call sites require them.
80
+ attrs
81
+ end
82
+ private_class_method :resolve_create_class,
83
+ :resolve_create_typed_stage,
84
+ :coerce_create_attrs
25
85
  end
26
86
  end
27
87
  end
@@ -11,6 +11,74 @@ module Pubid
11
11
  rescue Parslet::ParseFailed => e
12
12
  raise "Failed to parse CCSDS identifier '#{identifier}': #{e.message}"
13
13
  end
14
+
15
+ # Factory that builds a CCSDS identifier from a hash of primitives.
16
+ #
17
+ # Accepts the field shape used by relaton-data-ccsds index entries:
18
+ # `:publisher`, `:number`, `:series`, `:part`, `:book_color`,
19
+ # `:retired`, `:edition`, plus the parsed-shape fields `:type`,
20
+ # `:suffix`, `:language`. CCSDS 2.x maps these as:
21
+ #
22
+ # * `:series` + `:number` → combined number ("A" + "20" → "A20")
23
+ # * `:book_color` → `:type` (B/G/M/R/Y/O)
24
+ # * `:publisher` → ignored (hardcoded "CCSDS")
25
+ # * `:retired` → ignored (no 2.x field)
26
+ #
27
+ # Supplement subclasses (currently just Corrigendum, via `type: :cor`)
28
+ # raise ArgumentError until `base:` kwarg is wired through.
29
+ def self.create(type: nil, **opts)
30
+ if type
31
+ type_sym = type.to_sym
32
+ base_type_key = Identifiers::Base.type[:key].to_sym
33
+
34
+ if type_sym != base_type_key
35
+ supplement = supplement_klass_for(type_sym)
36
+ if supplement
37
+ # TODO(create-shim): supplement subclasses (Corrigendum)
38
+ # require a base_identifier. Wire `base:` kwarg through once
39
+ # a caller needs it.
40
+ raise ArgumentError, "#{supplement} requires a " \
41
+ "base_identifier; Identifier.create " \
42
+ "cannot build supplements yet"
43
+ end
44
+
45
+ # Not a class-dispatch key — treat as book_color data
46
+ # (e.g. `type: "G"` for Green Book) and merge back into opts.
47
+ opts = opts.merge(type: type)
48
+ end
49
+ end
50
+
51
+ Identifiers::Base.new(**coerce_create_attrs(opts))
52
+ end
53
+
54
+ def self.supplement_klass_for(type_sym)
55
+ Scheme.supplement_identifiers.find do |k|
56
+ k.type[:key].to_sym == type_sym
57
+ end
58
+ end
59
+
60
+ def self.coerce_create_attrs(opts)
61
+ attrs = {}
62
+
63
+ if opts[:number]
64
+ num = opts[:number].to_s
65
+ attrs[:number] =
66
+ opts[:series] ? "#{opts[:series]}#{num}" : num
67
+ end
68
+
69
+ attrs[:type] = opts[:book_color].to_s if opts[:book_color]
70
+ # Allow caller to pass :type as data when no :book_color given
71
+ # (parsed identifiers expose it via Base#type attribute).
72
+ attrs[:type] = opts[:type].to_s if opts[:type] && !attrs[:type]
73
+
74
+ %i[part edition suffix language].each do |k|
75
+ v = opts[k]
76
+ attrs[k] = v.to_s unless v.nil?
77
+ end
78
+
79
+ attrs
80
+ end
81
+ private_class_method :supplement_klass_for, :coerce_create_attrs
14
82
  end
15
83
  end
16
84
  end
@@ -24,6 +24,17 @@ module Pubid
24
24
  { key: :base, title: "Base Standard", short: nil }
25
25
  end
26
26
 
27
+ # Return a copy of this identifier with the named attributes
28
+ # nil'd. Mirrors the {Pubid::Identifier#exclude} convenience.
29
+ # CCSDS Base extends Lutaml::Model::Serializable directly (not
30
+ # Pubid::Identifier), so the method is defined here too.
31
+ def exclude(*args)
32
+ attrs = self.class.attributes.each_with_object({}) do |(name, _), h|
33
+ h[name] = args.include?(name) ? nil : send(name)
34
+ end
35
+ self.class.new(attrs)
36
+ end
37
+
27
38
  # Generate URN for this identifier
28
39
  #
29
40
  # @return [String] URN representation
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Pubid
4
4
  module Ccsds
5
- class SingleIdentifier < Identifier
5
+ # Qualified as ::Pubid::Identifier to avoid resolving to the local
6
+ # Pubid::Ccsds::Identifier module, which is a parser entry point
7
+ # (`self.parse`) — not the base class.
8
+ class SingleIdentifier < ::Pubid::Identifier
6
9
  attribute :publisher, Components::Publisher, default: -> {
7
10
  Components::Publisher.new(body: "CCSDS")
8
11
  }
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../components/factory"
4
+
3
5
  module Pubid
4
6
  module CenCenelec
5
7
  module Identifier
@@ -10,6 +12,41 @@ module Pubid
10
12
  rescue Parslet::ParseFailed => e
11
13
  raise "Failed to parse CEN identifier '#{identifier}': #{e.message}"
12
14
  end
15
+
16
+ # Factory mirroring pubid 1.x's `Pubid::Cen::Identifier.create` API.
17
+ # Dispatches via {Pubid::CenCenelec::Scheme}'s IDENTIFIER_CLASS_MAP
18
+ # and TYPED_STAGES_REGISTRY. Default is EuropeanNorm.
19
+ def self.create(type: nil, stage: nil, **opts)
20
+ klass = resolve_create_class(type: type, stage: stage)
21
+ attrs = ::Pubid::Components::Factory.from_hash(opts)
22
+ ts = resolve_create_typed_stage(klass, stage)
23
+ attrs[:typed_stage] = ts if ts
24
+ klass.new(**attrs)
25
+ end
26
+
27
+ def self.resolve_create_class(type:, stage:)
28
+ scheme = Scheme.new
29
+ klass = nil
30
+ if type
31
+ klass = scheme.locate_identifier_klass_by_type_code(type)
32
+ elsif stage
33
+ ts = scheme.locate_typed_stage_by_abbr(stage.to_s)
34
+ klass = scheme.locate_identifier_klass_by_type_code(ts.type_code) if ts
35
+ end
36
+ klass || Identifiers::EuropeanNorm
37
+ end
38
+
39
+ def self.resolve_create_typed_stage(klass, stage)
40
+ if stage
41
+ Scheme.new.locate_typed_stage_by_abbr(stage.to_s)
42
+ elsif klass.const_defined?(:TYPED_STAGES)
43
+ klass.const_get(:TYPED_STAGES).find do |ts|
44
+ ts.stage_code.to_sym == :published
45
+ end
46
+ end
47
+ end
48
+ private_class_method :resolve_create_class,
49
+ :resolve_create_typed_stage
13
50
  end
14
51
  end
15
52
  end
@@ -13,6 +13,59 @@ module Pubid
13
13
  builder = Builder.new
14
14
  builder.build(parsed, original_string: input)
15
15
  end
16
+
17
+ # Factory that builds a CIE identifier from a hash of primitives.
18
+ # Dispatch on `:type` to a SingleIdentifier subclass; default is
19
+ # {Identifiers::Standard}.
20
+ TYPE_KEY_TO_KLASS = {
21
+ standard: "Standard",
22
+ conference: "Conference",
23
+ bundle: "Bundle",
24
+ joint_published: "JointPublished",
25
+ dual_published: "DualPublished",
26
+ identical: "Identical",
27
+ tutorial_bundle: "TutorialBundle",
28
+ }.freeze
29
+
30
+ def self.create(type: nil, **opts)
31
+ klass = resolve_create_class(type)
32
+ klass.new(**coerce_create_attrs(opts, klass: klass))
33
+ end
34
+
35
+ def self.resolve_create_class(type)
36
+ return Identifiers::Standard if type.nil?
37
+
38
+ klass_name = TYPE_KEY_TO_KLASS[type.to_sym]
39
+ raise ArgumentError, "Unknown CIE type: #{type.inspect}" unless klass_name
40
+
41
+ Identifiers.const_get(klass_name)
42
+ end
43
+
44
+ def self.coerce_create_attrs(opts, klass:)
45
+ attrs = {}
46
+ attrs[:year] = opts[:year].to_s if opts[:year]
47
+ attrs[:date_separator] = opts[:date_separator].to_s if opts[:date_separator]
48
+
49
+ if (v = opts[:code] || opts[:number])
50
+ attrs[:code] = Pubid::Cie::Components::Code.new(
51
+ number: v.to_s,
52
+ part: opts[:part]&.to_s,
53
+ iteration: opts[:iteration]&.to_s,
54
+ style: opts[:style]&.to_s,
55
+ part_separator: opts[:part_separator]&.to_s,
56
+ )
57
+ end
58
+
59
+ # Standard-only attributes
60
+ if klass.attributes.key?(:s_prefix) && opts.key?(:s_prefix)
61
+ attrs[:s_prefix] = opts[:s_prefix]
62
+ end
63
+ if klass.attributes.key?(:stage) && opts[:stage]
64
+ attrs[:stage] = opts[:stage].to_s
65
+ end
66
+ attrs
67
+ end
68
+ private_class_method :resolve_create_class, :coerce_create_attrs
16
69
  end
17
70
  end
18
71
  end