pubid 2.0.26 → 2.0.27

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.
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module CenPfs
5
+ # The adopted-base dispatch hook (pubid-grammar#121 feature 4, the §4
6
+ # coded escape hatch). The generated builder calls this module at two
7
+ # explicit points — the captured adopted run, and the implicit probe —
8
+ # and this module owns the value-dependent dispatch the declaration
9
+ # cannot carry:
10
+ #
11
+ # * the adopted string parses with the owning scheme's flavor
12
+ # ("ISO/IEC" and "ISO" leads through Pubid::Iso, "IEC" through
13
+ # Pubid::Iec);
14
+ # * an ENV-prefixed adoption switches the class to the prestandard
15
+ # (the coded ENV branch), the publisher staying ENV;
16
+ # * an EN with a bare number in the IEC range (60000-79999) adopts
17
+ # the corresponding IEC standard implicitly — the ISO-only range
18
+ # is intentionally NOT mapped (too many real CEN publications
19
+ # would change type; pubid#249).
20
+ module AdoptedBase
21
+ IMPLICIT_IEC_RANGE = (60_000..79_999)
22
+
23
+ class << self
24
+ # The adoption identifier the captured run builds, or nil when the
25
+ # run names no known flavor (the caller surfaces the parse error).
26
+ def build(builder, data)
27
+ adopted_str = builder.unwrap(data[:adopted])&.to_s&.strip
28
+ return nil unless adopted_str && !adopted_str.empty?
29
+
30
+ adopted_id = parse_adopted(adopted_str)
31
+ publisher = builder.unwrap(data[:publisher])&.to_s
32
+ return env_adopted(adopted_id) if publisher == "ENV"
33
+
34
+ attrs = adoption_attrs(builder, data, adopted_id, publisher)
35
+ Identifiers::AdoptedEuropeanNorm.new(**attrs)
36
+ end
37
+
38
+ # The implicit adoption probe: an EN with a bare IEC-range number,
39
+ # no parts and no supplements, adopts the corresponding IEC
40
+ # standard (the coded builder's own guard). nil otherwise.
41
+ def implicit(builder, data)
42
+ return nil unless implicit_adoption?(builder, data)
43
+
44
+ number = builder.unwrap(data[:number])&.to_s&.strip
45
+ return nil unless number&.match?(/\A\d{1,6}\z/)
46
+ return nil unless IMPLICIT_IEC_RANGE.cover?(number.to_i)
47
+
48
+ Identifiers::AdoptedEuropeanNorm.new(
49
+ adopted: Pubid::Iec.parse("IEC #{number}"),
50
+ )
51
+ end
52
+
53
+ private
54
+
55
+ # The ENV adoption switch: the prestandard wraps the adopted
56
+ # document, its publisher staying ENV.
57
+ def env_adopted(adopted_id)
58
+ Identifiers::EuropeanPrestandard.new(
59
+ publisher: ::Pubid::Components::Publisher.new(body: "ENV"),
60
+ adopted: adopted_id,
61
+ )
62
+ end
63
+
64
+ # The adoption's own columns: the draft stage rides the CEN
65
+ # adoption ("prEN ISO 1234:2020"), the publisher face, and the
66
+ # copublishers.
67
+ def adoption_attrs(builder, data, adopted_id, publisher)
68
+ attrs = { adopted: adopted_id }
69
+ attrs.merge!(stage_attrs(builder, data))
70
+ attrs[:publisher] =
71
+ ::Pubid::Components::Publisher.new(body: publisher) if publisher
72
+ copubs = Array(data[:copublishers]).map do |row|
73
+ value = builder.unwrap(row[:copublisher] || row["copublisher"])
74
+ ::Pubid::Components::Publisher.new(body: value.to_s)
75
+ end
76
+ attrs[:copublishers] = copubs unless copubs.empty?
77
+ attrs
78
+ end
79
+
80
+ def stage_attrs(builder, data)
81
+ stage = builder.unwrap(data[:stage])
82
+ return {} unless stage
83
+
84
+ typed_stage = ::Pubid::CenPfs::Builder.stage_by_code(
85
+ ::Pubid::CenPfs::Builder::STAGE_MAP.fetch(stage.to_s),
86
+ )
87
+ { typed_stage: typed_stage, type: typed_stage.to_type,
88
+ stage: typed_stage.to_stage }
89
+ end
90
+
91
+ # The coded guard: the EN publisher alone, no copublishers, no
92
+ # parts, no supplements — only then may the number imply adoption.
93
+ def implicit_adoption?(builder, data)
94
+ builder.unwrap(data[:publisher])&.to_s == "EN" &&
95
+ Array(data[:copublishers]).none? &&
96
+ builder.unwrap(data[:part]).nil? &&
97
+ Array(data[:supplements]).none?
98
+ end
99
+
100
+ # "EN ISO 13485:2016/Amd 1:2016" parses the ADOPTED string with the
101
+ # ISO flavor (its amendment spelling included); an IEC lead token
102
+ # parses with the IEC flavor. CISPR stays unparsed (the coded
103
+ # builder's own boundary).
104
+ def parse_adopted(adopted_str)
105
+ if adopted_str.start_with?("IEC")
106
+ Pubid::Iec.parse(adopted_str)
107
+ elsif adopted_str.start_with?("ISO")
108
+ Pubid::Iso.parse(adopted_str)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -1,33 +1,169 @@
1
- module Pubid
2
- module CenPfs
3
- class Builder
4
- def self.build(parsed_data)
5
- new.build(parsed_data)
6
- end
1
+ module Pubid
2
+ module CenPfs
3
+ class Builder
4
+ # The type each declared token selects (feature 1); the
5
+ # token-free type is the default class.
6
+ TYPE_TOKENS = {
7
+ "TR" => "TechnicalReport",
8
+ "TS" => "TechnicalSpecification",
9
+ "Guide" => "Guide",
10
+ "CR" => "CenReport",
11
+ "CWA" => "CenWorkshopAgreement",
12
+ "ES" => "EuropeanSpecification",
13
+ "ENV" => "EuropeanPrestandard",
14
+ "HD" => "HarmonizationDocument",
15
+ }.freeze
16
+
17
+ DEFAULT_TYPE = "EuropeanNorm"
18
+
19
+ # The declared draft stages (feature 2): code → the
20
+ # typed-stage row the trio builds from.
21
+ STAGES = {
22
+ "pren" => { code: :pren, stage_code: :proposal, type_code: :en, abbr: ["prEN"] },
23
+ "fpren" => { code: :fpren, stage_code: :final_proposal, type_code: :en, abbr: ["FprEN"] },
24
+ }.freeze
25
+
26
+ # The captured spelling → the serialized stage code.
27
+ STAGE_MAP = {
28
+ "prEN" => "pren",
29
+ "FprEN" => "fpren",
30
+ }.freeze
31
+
32
+ def self.build(parsed_data)
33
+ new.build(parsed_data)
34
+ end
35
+
36
+ def self.stage_by_code(code)
37
+ row = STAGES[code.to_s]
38
+ return nil unless row
39
+
40
+ ::Pubid::Components::TypedStage.new(
41
+ code: row[:code], stage_code: row[:stage_code],
42
+ type_code: row[:type_code], abbr: row[:abbr],
43
+ )
44
+ end
45
+
46
+ def build(data)
47
+ base = build_base_document(data)
48
+ wrapped = wrap_supplements(base, data)
49
+ wrap_fragment(wrapped, data)
50
+ end
51
+
52
+ # The document the supplements apply to. The adopted-base
53
+ # dispatch (feature 4) calls the vendored hook at two explicit
54
+ # points: the captured adopted run, and the hook's own implicit
55
+ # probe — never implicitly.
56
+ def build_base_document(data)
57
+ return AdoptedBase.build(self, data) if data[:adopted]
58
+
59
+ implicit = AdoptedBase.implicit(self, data)
60
+ return implicit if implicit
61
+
62
+ identifier = locate_identifier_klass(data).new
63
+ assign_attributes(identifier, data)
64
+ identifier
65
+ end
66
+
67
+ # The class each reference selects: the type-position token
68
+ # first, then the publisher-position token (the publisher-type
69
+ # faces CR/CWA/…), then the token-free default class.
70
+ def locate_identifier_klass(data)
71
+ token = data[:guide] || data[:type]
72
+ token ||= data[:publisher] if TYPE_TOKENS.key?(data[:publisher].to_s)
73
+ name = token ? TYPE_TOKENS.fetch(token.to_s, DEFAULT_TYPE) : DEFAULT_TYPE
74
+ Identifiers.const_get(name)
75
+ end
7
76
 
8
- def build(data)
9
- Identifiers::EuropeanNorm.new(
10
- number: data[:number],
11
- part: data[:part],
12
- date: build_date(data),
77
+ # The columns, as the coded builder's cast writes them.
78
+ def assign_attributes(identifier, data)
79
+ if data[:year]
80
+ identifier.date = ::Pubid::Components::Date.new(
81
+ year: data[:year]&.to_s,
13
82
  )
14
83
  end
84
+ identifier.number = data[:number]&.to_s
85
+ identifier.part = data[:part]&.to_s
86
+ identifier.version = present_string(data[:version])
87
+ if data[:publisher]
88
+ identifier.publisher = ::Pubid::Components::Publisher.new(body: data[:publisher].to_s)
89
+ end
90
+ copubs = Array(data[:copublishers]).map do |row|
91
+ value = row[:copublisher] || row["copublisher"]
92
+ ::Pubid::Components::Publisher.new(body: unwrap(value).to_s)
93
+ end
94
+ identifier.copublishers = copubs unless copubs.empty?
95
+ type_token = data[:guide] || data[:type]
96
+ identifier.type = ::Pubid::Components::Type.new(abbr: type_token.to_s) if type_token
97
+ captured = data[:stage]
98
+ return unless captured
99
+
100
+ typed_stage = Builder.stage_by_code(STAGE_MAP.fetch(captured.to_s))
101
+ identifier.typed_stage = typed_stage
102
+ identifier.type = typed_stage.to_type
103
+ identifier.stage = typed_stage.to_stage
104
+ end
15
105
 
16
- private
106
+ # A capture the grammar bound as an empty string stores as nil:
107
+ # to_hash drops the empty string, so from_hash would give nil and
108
+ # the two identifiers would not be ==.
109
+ def present_string(value)
110
+ str = unwrap(value)&.to_s
111
+ str unless str.nil? || str.empty?
112
+ end
17
113
 
18
- # The publication date as the shared Components::Date, or
19
- # nil when the reference omits the calendar fields (a
20
- # partial reference matches by excluding the date).
21
- def build_date(data)
22
- values = [data[:year]]
23
- return nil if values.compact.empty?
114
+ # The run's internal captures bind only through the item row, so
115
+ # unbound ones arrive as raw leaves — a Parsanol::Slice or a
116
+ # {value:, offset: ...} span. Unwrap the captured value.
117
+ def unwrap(capture)
118
+ return capture.content if capture.is_a?(Parsanol::Slice)
119
+ return capture[:value] if capture.is_a?(Hash) && capture.key?(:value)
24
120
 
25
- ::Pubid::Components::Date.new(
26
- year: data[:year]&.to_s,
27
- )
121
+ capture
122
+ end
123
+
124
+ # The wrapper classes by kind face: the run's dispatch.
125
+ SUPPLEMENT_WRAPPERS = {
126
+ "A" => Identifiers::Amendment,
127
+ "AC" => Identifiers::Corrigendum,
128
+ }.freeze
129
+
130
+ def wrap_supplements(base, data)
131
+ rows = Array(data[:supplements])
132
+ return base if rows.empty?
133
+
134
+ items = rows.map do |row|
135
+ inner = row[:supplement] || row["supplement"] || {}
136
+ inner = inner.values.first if inner.values.all?(Hash)
137
+ inner
28
138
  end
139
+ if items.any? { |item| item[:supplements_plus] || item["supplements_plus"] }
140
+ members = items.map { |item| supplement_from(item) }
141
+ Identifiers::ConsolidatedIdentifier.new(identifiers: [base] + members)
142
+ else
143
+ supplement_from(items.first, base: base)
144
+ end
145
+ end
146
+
147
+ def supplement_from(item, base: nil)
148
+ kind = unwrap(item[:supplement_kind] || item["supplement_kind"])
149
+ number = unwrap(item[:supplement_number] || item["supplement_number"])
150
+ attrs = {}
151
+ attrs[:base] = base if base
152
+ attrs[:number] = present_string(number)
153
+ attrs[:year] = present_string(unwrap(item[:supplements_year] || item["supplements_year"]))
154
+ attrs[:month] = present_string(unwrap(item[:supplements_month] || item["supplements_month"]))
155
+ SUPPLEMENT_WRAPPERS.fetch(kind.to_s).new(**attrs)
156
+ end
157
+
158
+ def wrap_fragment(identifier, data)
159
+ return identifier unless data[:fragment_number]
160
+
161
+ Identifiers::Fragment.new(
162
+ number: unwrap(data[:fragment_number]).to_s, base: identifier,
163
+ )
29
164
  end
30
165
  end
31
166
  end
32
167
 
33
168
  Pubid::CenPfs::Builder.prepend(Pubid::Builder::AllPartsWrap)
169
+ end
@@ -1,64 +1,122 @@
1
- module Pubid
2
- module CenPfs
3
- class Identifier < ::Pubid::Identifier
4
- attribute :number, :string
5
- attribute :part, :string
6
- attribute :date, ::Pubid::Components::Date
7
-
8
- EN_TYPE_MAP = {
9
- "pubid:cencenelec:european-norm" => "Pubid::CenPfs::Identifiers::EuropeanNorm",
10
- }.freeze
11
-
12
- key_value do
13
- map "_type", to: :_type, polymorphic_map: EN_TYPE_MAP
14
- map "number", to: :number
15
- map "part", to: :part
16
- map "year", with: { to: :year_to_kv, from: :year_from_kv }
1
+ module Pubid
2
+ module CenPfs
3
+ # The shared base of every concrete CenPfs class. The
4
+ # serialization is the shared ::Pubid::Identifier's (the coded
5
+ # flavor's own shape): flat publisher/copublishers scalars, the
6
+ # date flattened to its fields, and the draft stage compacted
7
+ # to its code by the class hooks below.
8
+ class Identifier < ::Pubid::Identifier
9
+ # The ETSI-style version suffix ("EN 302 326-2 V2.2.1").
10
+ attribute :version, :string
11
+
12
+ # A published document holds no type, stage or typed stage, so
13
+ # a nil one means "published", not "any stage": `EN 1325` is
14
+ # not `prEN 1325`. The three components repeat one entry of
15
+ # the stage registry, so they move together.
16
+ subset_strict :type, :stage, :typed_stage
17
+
18
+ # The bodies that print as a publisher prefix. The publisher
19
+ # attribute also holds the deliverable types that ARE the
20
+ # designation ("CR 954"); those stay under
21
+ # with_publisher: false, as `BS` does for BSI.
22
+ PUBLISHER_PREFIXES = ["CEN", "CLC", "CENELEC", "EN", "CWA", "HD", "ES", "CR", "ENV"].freeze
23
+
24
+ def publisher_prefixes
25
+ super.select do |prefix|
26
+ PUBLISHER_PREFIXES.include?(prefix.split("/").first)
17
27
  end
28
+ end
29
+
30
+ # The publisher serializes as a bare string, the copublishers
31
+ # as a list of strings (the coded flavor's flat-scalar table).
32
+ def self.flat_scalar_components
33
+ super.merge(publisher: "publisher", copublishers: "copublishers")
34
+ end
18
35
 
19
- # --- date serialized flat (the shared flat-scalar shape) ---
20
- def year_to_kv(model, doc)
21
- emit_date_part(doc, "year", model.date&.year)
22
- end
36
+ def self.flat_scalar_fields
37
+ super.merge(publisher: :body, copublishers: :body)
38
+ end
23
39
 
24
- def year_from_kv(model, value)
25
- date_for(model).year = value.to_s
26
- end
40
+ # The declared draft stages: the serialized code is the key,
41
+ # the official-URN code the declared `urn:` value.
42
+ STAGE_CODES = {
43
+ "pren" => { stage_code: :proposal },
44
+ "fpren" => { stage_code: :final_proposal },
45
+ }.freeze
27
46
 
28
- def emit_date_part(doc, key, val)
29
- return if val.nil? || val.to_s.empty?
47
+ # A draft stage serializes as its typed-stage code
48
+ # ("stage" => "pren"); #inflate_scalar_components rebuilds
49
+ # the three components from it (the coded compact-hash shape).
50
+ def self.compact_hash(model, hash)
51
+ stage = model.typed_stage
52
+ return unless stage && hash.key?("typed_stage")
53
+ return unless model.type == stage.to_type &&
54
+ model.stage == stage.to_stage
30
55
 
31
- doc.add_child(Lutaml::KeyValue::DataModel::Element.new(key, val.to_s))
32
- end
56
+ hash.delete("type")
57
+ hash.delete("typed_stage")
58
+ hash["stage"] = stage.code.to_s
59
+ end
33
60
 
34
- def date_for(model)
35
- model.date ||= ::Pubid::Components::Date.new
36
- end
61
+ # Reads the short stage form back; the nested form still
62
+ # reads as before.
63
+ def self.inflate_scalar_components(data)
64
+ data = super
65
+ return data unless data.is_a?(::Hash) && component_attribute?(:stage)
37
66
 
38
- PUBLISHER = "CEN"
67
+ key = data.key?("stage") ? "stage" : :stage
68
+ value = data[key]
69
+ return data unless value.is_a?(::String) || value.is_a?(::Symbol)
39
70
 
40
- # The scheme's declared publisher token: the internal-form
41
- # cut (to_s(with_publisher: false)) strips it from the
42
- # rendered face.
43
- def publisher_prefixes
44
- [PUBLISHER]
45
- end
71
+ stage = locate_stage_by_code(value)
72
+ return data unless stage
73
+
74
+ data = data.dup
75
+ data.delete(key)
76
+ data["type"] = stage.to_type.to_hash
77
+ data["stage"] = stage.to_stage.to_hash
78
+ data["typed_stage"] = stage.to_hash
79
+ data
80
+ end
46
81
 
47
- def self.parse(identifier)
48
- unless identifier.is_a?(String)
49
- raise Pubid::Errors::InvalidInputError,
50
- Pubid::INPUT_NOT_A_STRING_MESSAGE
51
- end
82
+ # The stage entry by serialized code, or nil.
83
+ def self.locate_stage_by_code(code)
84
+ Builder.stage_by_code(code.to_s)
85
+ end
52
86
 
53
- if identifier.length > Pubid::MAX_INPUT_LENGTH
54
- raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
55
- end
87
+ # The draft stage's own code is the MR type segment
88
+ # ("en.pren.10160"); a published identifier has no typed
89
+ # stage, so the shared hook answers nil.
90
+ def mr_type
91
+ stage = typed_stage
92
+ return super unless stage && stage.stage_code.to_s != "published"
56
93
 
57
- identifier = ::Pubid.utf8_input(identifier)
94
+ stage.code.to_s.downcase
95
+ end
96
+
97
+ def self.parse(identifier)
98
+ unless identifier.is_a?(String)
99
+ raise Pubid::Errors::InvalidInputError,
100
+ Pubid::INPUT_NOT_A_STRING_MESSAGE
101
+ end
58
102
 
59
- parsed = Pubid::Parg::Model.bound(:cen_pfs, identifier)
60
- Builder.build(parsed)
103
+ if identifier.length > Pubid::MAX_INPUT_LENGTH
104
+ raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
61
105
  end
106
+
107
+ identifier = ::Pubid.utf8_input(identifier)
108
+
109
+ parsed = Pubid::Parg::Model.bound(:cen_pfs, identifier)
110
+ Builder.build(parsed)
111
+ end
112
+
113
+ private
114
+
115
+ # "amd.1.2005" from ("amd", "1", 2005). A nil or blank
116
+ # segment is left out.
117
+ def mr_join_segments(*segments)
118
+ segments.map(&:to_s).reject(&:empty?).join(".").downcase
62
119
  end
63
120
  end
64
121
  end
122
+ end
@@ -0,0 +1,55 @@
1
+ module Pubid
2
+ module CenPfs
3
+ module Identifiers
4
+ # Concrete Adopted European Norm identifier type (PFS type `adopted_european_norm`).
5
+ class AdoptedEuropeanNorm < Identifier
6
+ TYPED_STAGES = [
7
+ ].freeze
8
+
9
+ def self.type
10
+ { key: :aen, web: :adopted_european_norm, title: "Adopted European Norm", short: "EN" }
11
+ end
12
+
13
+ # The publisher default (feature 5): the type's own token
14
+ # when the type is its designation, else the scheme default.
15
+ def self.default_publisher
16
+ ::Pubid::Components::Publisher.new(body: "EN")
17
+ end
18
+
19
+ attribute :publisher, ::Pubid::Components::Publisher, default: -> { self.class.default_publisher }
20
+ attribute :type, ::Pubid::Components::Type, default: -> { self.class.default_type }
21
+
22
+ def self.default_type
23
+ ::Pubid::Components::Type.new(abbr: type[:short])
24
+ end
25
+
26
+ # The adopted document (feature 4): the whole identity lives
27
+ # on the nested ISO/IEC identifier.
28
+ attribute :adopted, ::Pubid::Identifier, polymorphic: true
29
+
30
+ def root
31
+ adopted ? adopted.root : self
32
+ end
33
+
34
+ # MR "cen.iso.ts.21003-7": the publisher group fused, then the
35
+ # adopted document's own MR string in place of a number.
36
+ def mr_publisher
37
+ mr_join(*([publisher] + Array(copublishers)).compact
38
+ .map { |p| p.to_s.downcase })
39
+ end
40
+
41
+ def mr_number_with_part
42
+ adopted ? adopted.to_mr_string : super
43
+ end
44
+
45
+ # The scheme-canonical wire tag declared in the definition:
46
+ # to_hash stays byte-identical with the scheme's coded
47
+ # flavor, and this flavor's own from_hash routes the tag
48
+ # to this class.
49
+ def self.polymorphic_name
50
+ "pubid:cencenelec:adopted-european-norm"
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,27 @@
1
+ module Pubid
2
+ module CenPfs
3
+ module Identifiers
4
+ # The synthesized Amendment wrapper (PFS supplements
5
+ # declaration, issue #121 feature 3): the kind face
6
+ # "A" is the run's dispatch token and the
7
+ # printed notation's face.
8
+ class Amendment < Supplement
9
+ FACE = "A".freeze
10
+
11
+ def self.face
12
+ FACE
13
+ end
14
+
15
+ def self.polymorphic_name
16
+ "pubid:cencenelec:amendment"
17
+ end
18
+
19
+ # The MR slug's supplement suffix: the shared MrString
20
+ # recurses into base and appends this.
21
+ def mr_supplement_suffix
22
+ mr_join_segments("amd", number, year)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,36 @@
1
+ module Pubid
2
+ module CenPfs
3
+ module Identifiers
4
+ # Concrete CEN Report identifier type (PFS type `cen_report`).
5
+ class CenReport < Identifier
6
+ TYPED_STAGES = [
7
+ ].freeze
8
+
9
+ def self.type
10
+ { key: :cr, web: :cen_report, title: "CEN Report", short: "CR" }
11
+ end
12
+
13
+ # The publisher default (feature 5): the type's own token
14
+ # when the type is its designation, else the scheme default.
15
+ def self.default_publisher
16
+ ::Pubid::Components::Publisher.new(body: "CR")
17
+ end
18
+
19
+ attribute :publisher, ::Pubid::Components::Publisher, default: -> { self.class.default_publisher }
20
+ attribute :type, ::Pubid::Components::Type, default: -> { self.class.default_type }
21
+
22
+ def self.default_type
23
+ ::Pubid::Components::Type.new(abbr: type[:short])
24
+ end
25
+
26
+ # The scheme-canonical wire tag declared in the definition:
27
+ # to_hash stays byte-identical with the scheme's coded
28
+ # flavor, and this flavor's own from_hash routes the tag
29
+ # to this class.
30
+ def self.polymorphic_name
31
+ "pubid:cencenelec:cen-report"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ module Pubid
2
+ module CenPfs
3
+ module Identifiers
4
+ # Concrete CEN Workshop Agreement identifier type (PFS type `cen_workshop_agreement`).
5
+ class CenWorkshopAgreement < Identifier
6
+ TYPED_STAGES = [
7
+ ].freeze
8
+
9
+ def self.type
10
+ { key: :cwa, web: :cen_workshop_agreement, title: "CEN Workshop Agreement", short: "CWA" }
11
+ end
12
+
13
+ # The publisher default (feature 5): the type's own token
14
+ # when the type is its designation, else the scheme default.
15
+ def self.default_publisher
16
+ ::Pubid::Components::Publisher.new(body: "CWA")
17
+ end
18
+
19
+ attribute :publisher, ::Pubid::Components::Publisher, default: -> { self.class.default_publisher }
20
+ attribute :type, ::Pubid::Components::Type, default: -> { self.class.default_type }
21
+
22
+ def self.default_type
23
+ ::Pubid::Components::Type.new(abbr: type[:short])
24
+ end
25
+
26
+ # The scheme-canonical wire tag declared in the definition:
27
+ # to_hash stays byte-identical with the scheme's coded
28
+ # flavor, and this flavor's own from_hash routes the tag
29
+ # to this class.
30
+ def self.polymorphic_name
31
+ "pubid:cencenelec:cen-workshop-agreement"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,41 @@
1
+ module Pubid
2
+ module CenPfs
3
+ module Identifiers
4
+ # The consolidated (plus-run) bundle: the base document is
5
+ # the first member; the supplements carry no base.
6
+ class ConsolidatedIdentifier < ::Pubid::CenPfs::Identifier
7
+ attribute :identifiers, ::Pubid::CenPfs::Identifier,
8
+ polymorphic: true, collection: true
9
+
10
+ # The origin document. Members live in `identifiers`, so
11
+ # walk the first member to its root.
12
+ def root
13
+ identifiers&.first&.root || self
14
+ end
15
+
16
+ def base_document
17
+ identifiers&.first&.base_document || self
18
+ end
19
+
20
+ def drop_supplements
21
+ identifiers&.first || self
22
+ end
23
+
24
+ # The base document, then each member's own supplement
25
+ # suffix behind the plus marker — the coded shape.
26
+ def to_mr_string
27
+ base, *supplements = identifiers
28
+ return "" unless base
29
+
30
+ ([base.to_mr_string] +
31
+ supplements.map { |s| "plus-#{s.mr_supplement_suffix}" })
32
+ .join(::Pubid::Renderers::MrString::SEPARATOR_SUPPLEMENT)
33
+ end
34
+
35
+ def self.polymorphic_name
36
+ "pubid:cencenelec:consolidated-identifier"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end