pubid 2.0.0.pre.alpha.11 → 2.0.0.pre.alpha.12

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pubid/bsi/components/date.rb +10 -4
  3. data/lib/pubid/bsi/identifiers/british_industrial_practice.rb +1 -1
  4. data/lib/pubid/bsi/identifiers/handbook.rb +1 -1
  5. data/lib/pubid/bsi/identifiers/practice_guide.rb +1 -1
  6. data/lib/pubid/bsi/single_identifier.rb +11 -2
  7. data/lib/pubid/ccsds/identifier.rb +4 -0
  8. data/lib/pubid/ccsds/single_identifier.rb +14 -10
  9. data/lib/pubid/csa/builder.rb +3 -3
  10. data/lib/pubid/csa/identifiers/bundled.rb +2 -2
  11. data/lib/pubid/csa/identifiers/cec.rb +1 -1
  12. data/lib/pubid/csa/single_identifier.rb +1 -1
  13. data/lib/pubid/csa/urn_generator.rb +1 -1
  14. data/lib/pubid/evs/builder.rb +1 -1
  15. data/lib/pubid/evs/identifier.rb +6 -6
  16. data/lib/pubid/evs/identifiers/national_adoption.rb +7 -2
  17. data/lib/pubid/evs/renderer.rb +1 -1
  18. data/lib/pubid/evs/urn_generator.rb +1 -1
  19. data/lib/pubid/evs/urn_parser.rb +1 -1
  20. data/lib/pubid/gb/builder.rb +14 -9
  21. data/lib/pubid/gb/identifier.rb +23 -4
  22. data/lib/pubid/gb/parser.rb +4 -0
  23. data/lib/pubid/gb/renderer.rb +4 -3
  24. data/lib/pubid/gb.rb +1 -0
  25. data/lib/pubid/identifier.rb +11 -3
  26. data/lib/pubid/idf/identifier.rb +7 -0
  27. data/lib/pubid/idf/identifiers/amendment.rb +1 -1
  28. data/lib/pubid/idf/identifiers/corrigendum.rb +1 -1
  29. data/lib/pubid/idf/identifiers/international_standard.rb +1 -1
  30. data/lib/pubid/idf/identifiers/reviewed_method.rb +1 -1
  31. data/lib/pubid/idf/single_identifier.rb +1 -1
  32. data/lib/pubid/ieee/builder.rb +42 -2
  33. data/lib/pubid/ieee/identifiers/si_standard.rb +5 -2
  34. data/lib/pubid/ieee/ire/parser.rb +19 -1
  35. data/lib/pubid/ieee/parser.rb +207 -32
  36. data/lib/pubid/ieee/project_renderer.rb +46 -0
  37. data/lib/pubid/ieee/renderer.rb +6 -7
  38. data/lib/pubid/ieee.rb +2 -0
  39. data/lib/pubid/iso/builder.rb +10 -11
  40. data/lib/pubid/iso/components.rb +0 -1
  41. data/lib/pubid/iso/identifier.rb +11 -4
  42. data/lib/pubid/iso/identifiers/directives.rb +7 -4
  43. data/lib/pubid/iso/identifiers/tc_document.rb +31 -46
  44. data/lib/pubid/iso/normalizer.rb +1 -1
  45. data/lib/pubid/iso/urn_generator.rb +10 -10
  46. data/lib/pubid/iso.rb +5 -4
  47. data/lib/pubid/itu/builder.rb +10 -0
  48. data/lib/pubid/itu/identifiers/base.rb +10 -0
  49. data/lib/pubid/itu/identifiers/contribution.rb +31 -0
  50. data/lib/pubid/itu/identifiers.rb +1 -0
  51. data/lib/pubid/itu/parser.rb +19 -2
  52. data/lib/pubid/nist/builder.rb +12 -12
  53. data/lib/pubid/nist/identifiers/base.rb +10 -3
  54. data/lib/pubid/nist/identifiers/commercial_standard_emergency.rb +1 -1
  55. data/lib/pubid/nist/identifiers/crpl_report.rb +5 -6
  56. data/lib/pubid/nist/identifiers/federal_information_processing_standards.rb +2 -2
  57. data/lib/pubid/nist/identifiers/miscellaneous_publication.rb +2 -2
  58. data/lib/pubid/nist/series/ir.rb +3 -7
  59. data/lib/pubid/oiml/builder.rb +2 -2
  60. data/lib/pubid/oiml/identifiers/bulletin.rb +30 -36
  61. data/lib/pubid/oiml/identifiers/code_number.rb +2 -2
  62. data/lib/pubid/oiml/parser.rb +7 -2
  63. data/lib/pubid/oiml/renderer.rb +3 -3
  64. data/lib/pubid/oiml/single_identifier.rb +1 -1
  65. data/lib/pubid/oiml/urn_generator.rb +4 -4
  66. data/lib/pubid/renderers/directives_renderer.rb +3 -1
  67. data/lib/pubid/sae/builder.rb +1 -1
  68. data/lib/pubid/sae/identifiers/base.rb +4 -1
  69. data/lib/pubid/sae/urn_generator.rb +1 -1
  70. data/lib/pubid/version.rb +1 -1
  71. data/lib/tasks/docs.rake +13 -11
  72. metadata +4 -4
  73. data/lib/pubid/ccsds/identifiers/base_BASE_88929.rb +0 -70
  74. data/lib/pubid/iso/components/code.rb +0 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 18402e85b2e19640920f3e216f62f704c6770a840f654d92f96d799b03fbda71
4
- data.tar.gz: e9b1735e843fe5eb20574749d2581c19192799f86fe3f31a8e72b3d0a00ab047
3
+ metadata.gz: b4e0a5a8d4b5c1410b934712637a882a703f34ded60d92ed1d8f0a6465974396
4
+ data.tar.gz: 35bf6a1a075f68f35c682a3cfdde6bd5166523535491a74f1cb3473abcfaacec
5
5
  SHA512:
6
- metadata.gz: 1cbcaa48b3fb036aca2a62aabe9cdfc4733ad030dc833aa31b5be2dbba433ef6459f87dc22c0b811e707687ce18ee966a2882f928a44d2d0df89e159df5ea837
7
- data.tar.gz: 0ea8984849aa9f56a400948fb5ab7b29f5de122f70ab036d35fe344655abe70fbe9e34d9cdc04b80673593190b1479d8ac47b31bd21e056ad9b64fa6743e18e6
6
+ metadata.gz: 7dcd7c3d8f91d1ef5fa1ab296e4f922b435b04b70a06d184e6d2449d32a270bf31b14f352523686921072aac37bd0f91e0b6f62a6d44c28349b04ca7079092ed
7
+ data.tar.gz: ff8bc5531169fa3dace613d7369e656fdc2418f559adf3b527722e80144fd5a7aaa2db28da23979b2e3b8bc5af31132dca631e955a454d6262ad6a3ddf459b77
@@ -1,11 +1,17 @@
1
- # frozen_string_literal: true
1
+ # frozen_string: true
2
2
 
3
3
  module Pubid
4
4
  module Bsi
5
5
  module Components
6
- # Date component for BSI flavor
7
- class Date < Pubid::Components::Date
8
- end
6
+ # BSI has no date semantics of its own, so this is the shared
7
+ # component, not a subclass: an empty `class Date <
8
+ # Pubid::Components::Date` rejected the shared instances a nested
9
+ # cross-flavor adopted identifier carries (pubid#379 — the adoption
10
+ # wrappers delegate `date` to the foreign object), and made `==`
11
+ # compare the parsed instance against the deserialized one by class.
12
+ # Aliased so the builder's `Components::Date.new` call sites construct
13
+ # the shared class.
14
+ Date = ::Pubid::Components::Date
9
15
  end
10
16
  end
11
17
  end
@@ -6,7 +6,7 @@ module Pubid
6
6
  # British Industrial Practice (BIP)
7
7
  # Examples: "BIP 2225:2022", "BIP 0142:2014", "BIP 0009:2020"
8
8
  class BritishIndustrialPractice < SingleIdentifier
9
- attribute :date, Bsi::Components::Date
9
+
10
10
 
11
11
  def self.type
12
12
  {
@@ -6,7 +6,7 @@ module Pubid
6
6
  # BSI Handbook
7
7
  # Examples: "Handbook 17:1963", "HB 10146:1998"
8
8
  class Handbook < SingleIdentifier
9
- attribute :date, Bsi::Components::Date
9
+
10
10
  attribute :original_abbr, :string # Preserve "Handbook" or "HB"
11
11
 
12
12
  def self.type
@@ -6,7 +6,7 @@ module Pubid
6
6
  # BSI Practice Guide (PP - Published Practice)
7
7
  # Examples: "PP 888:1982", "PP 7307:1986", "PP 7722:2006"
8
8
  class PracticeGuide < SingleIdentifier
9
- attribute :date, Bsi::Components::Date
9
+
10
10
 
11
11
  def self.type
12
12
  {
@@ -50,9 +50,18 @@ module Pubid
50
50
  attribute :part, :string
51
51
  attribute :subpart, :string
52
52
  attribute :second_number, Bsi::Components::Code # For collections like PAS 2035/2030
53
- attribute :date, Bsi::Components::Date
53
+ # The shared component, not Bsi::Components::Date: the adoption wrappers
54
+ # (AdoptedEuropeanNorm, AdoptedInternationalStandard) delegate `date` to
55
+ # the foreign identifier they wrap, whose date is a plain
56
+ # ::Pubid::Components::Date — the empty BSI subclass rejected it and
57
+ # to_hash/from_hash raised on every "BS EN …"/"BS ISO …" (pubid#379,
58
+ # the same shape as the bsi-set-cross-flavor-type fix).
59
+ attribute :date, ::Pubid::Components::Date
54
60
  attribute :stage, Pubid::Components::Stage
55
- attribute :type, Bsi::Components::Type
61
+ # Shared Type too: the leaf builders reach for ::Pubid::Components::Type
62
+ # (e.g. a default materialized from self.type), which the empty BSI
63
+ # subclass rejected — "BIP 2225:2022" et al. failed to_hash on `type`.
64
+ attribute :type, ::Pubid::Components::Type
56
65
  attribute :typed_stage, Pubid::Components::TypedStage
57
66
  attribute :edition, :string
58
67
  attribute :month, :integer
@@ -22,6 +22,10 @@ module Pubid
22
22
  # are likewise CCSDS-specific plain strings.
23
23
  attribute :number, :string
24
24
  attribute :part, :string
25
+ # CCSDS never populates `subpart`, but the inherited declaration is a
26
+ # Components::Code, and a parent that disagrees with its leaves is the
27
+ # determinism landmine. Declare it here, like every converted flavor.
28
+ attribute :subpart, :string
25
29
  attribute :edition, :string
26
30
  attribute :type, :string
27
31
  attribute :suffix, :string
@@ -9,10 +9,14 @@ module Pubid
9
9
  attribute :publisher, Components::Publisher, default: -> {
10
10
  Components::Publisher.new(body: "CCSDS")
11
11
  }
12
- attribute :series, Components::Code
13
- attribute :number, Components::Code
14
- attribute :part, Components::Code
15
- attribute :book_color, Components::Code
12
+ # Plain :string, like the live Pubid::Ccsds::Identifier hierarchy, which
13
+ # already declared these as strings. This class is otherwise
14
+ # unreferenced — nothing inherits it and no builder instantiates it — but
15
+ # it is public API, so it is retyped rather than deleted.
16
+ attribute :series, :string
17
+ attribute :number, :string
18
+ attribute :part, :string
19
+ attribute :book_color, :string
16
20
  attribute :edition, Components::Edition
17
21
  attribute :retired, :boolean, default: -> { false }
18
22
  attribute :language, Components::Language
@@ -29,14 +33,14 @@ module Pubid
29
33
  result += " "
30
34
 
31
35
  # Series (optional single letter before number)
32
- result += series.value if series
36
+ result += series.to_s if series
33
37
 
34
38
  # Number with part (using dot notation)
35
- result += number.value
36
- result += ".#{part.value}" if part
39
+ result += number.to_s
40
+ result += ".#{part}" if part
37
41
 
38
42
  # Book color (required) - no space before dash
39
- result += "-#{book_color.value}" if book_color
43
+ result += "-#{book_color}" if book_color
40
44
 
41
45
  # Edition (optional)
42
46
  result += "-#{edition.number}" if edition
@@ -54,11 +58,11 @@ module Pubid
54
58
  return nil unless other.is_a?(SingleIdentifier)
55
59
 
56
60
  # Compare by number first
57
- num_cmp = number.value.to_i <=> other.number.value.to_i
61
+ num_cmp = number.to_i <=> other.number.to_i
58
62
  return num_cmp unless num_cmp.zero?
59
63
 
60
64
  # Then by part
61
- part_cmp = (part ? part.value.to_i : 0) <=> (other.part ? other.part.value.to_i : 0)
65
+ part_cmp = part.to_i <=> other.part.to_i
62
66
  return part_cmp unless part_cmp.zero?
63
67
 
64
68
  # Then by edition
@@ -253,7 +253,7 @@ module Pubid
253
253
 
254
254
  # Code
255
255
  if parsed_hash[:code]
256
- series.number = Components::Code.new(value: parsed_hash[:code].to_s)
256
+ series.number = parsed_hash[:code].to_s
257
257
  end
258
258
 
259
259
  # Year format and year
@@ -520,13 +520,13 @@ module Pubid
520
520
  # Pattern: "C22.1-15" should become code="C22.1", year="2015"
521
521
  if !data[:year] && code_value =~ /^(.+)-(\d{2})$/
522
522
  # Split code and year
523
- identifier.number = Components::Code.new(value: $1)
523
+ identifier.number = $1
524
524
  # Convert 2-digit year to 4-digit
525
525
  year_2digit = $2
526
526
  identifier.year = "20#{year_2digit}"
527
527
  identifier.year_format = "dash"
528
528
  else
529
- identifier.number = Components::Code.new(value: code_value)
529
+ identifier.number = code_value
530
530
  end
531
531
  end
532
532
 
@@ -47,7 +47,7 @@ module Pubid
47
47
  end
48
48
  end
49
49
  base_str = (needs_space ? "#{prefix} " : prefix) +
50
- base.number.value.to_s + # Normalized code (e.g. "C22.2-1")
50
+ base.number.to_s + # Normalized code (e.g. "C22.2-1")
51
51
  ":#{year_display}"
52
52
  parts = [base_str]
53
53
  else
@@ -60,7 +60,7 @@ module Pubid
60
60
  bundled_with.each do |bundled|
61
61
  # For Cec identifiers, use normalized code format
62
62
  if bundled.is_a?(Cec)
63
- bundled_part = bundled.number.value.to_s # e.g. "C22.2-2"
63
+ bundled_part = bundled.number.to_s # e.g. "C22.2-2"
64
64
  if bundled.year
65
65
  # Use dash if year_format is dash, otherwise colon
66
66
  separator = bundled.year_format == "dash" ? "-" : ":"
@@ -58,7 +58,7 @@ module Pubid
58
58
  def number
59
59
  return nil unless cec_part && no_number
60
60
 
61
- Components::Code.new(value: "#{cec_part.value}-#{no_number.value}")
61
+ "#{cec_part.value}-#{no_number.value}"
62
62
  end
63
63
  end
64
64
  end
@@ -86,7 +86,7 @@ module Pubid
86
86
  end
87
87
 
88
88
  def number_from_kv(model, value)
89
- model.number = Components::Code.new(value: value.to_s)
89
+ model.number = value.to_s
90
90
  end
91
91
 
92
92
  # `no_number` is a plain :string here but a Components::Code on Cec, so
@@ -19,7 +19,7 @@ module Pubid
19
19
  def urn_number
20
20
  return nil unless identifier.number
21
21
 
22
- identifier.number.render(context: URN_CONTEXT)
22
+ identifier.number.to_s
23
23
  end
24
24
 
25
25
  def urn_no_number
@@ -17,7 +17,7 @@ module Pubid
17
17
  adopted = Pubid::CenCenelec::Builder.build(data[:adopted])
18
18
 
19
19
  Identifiers::NationalAdoption.new(
20
- adopted_identifier: adopted,
20
+ base: adopted,
21
21
  separator: data[:evs_separator] || "-",
22
22
  )
23
23
  end
@@ -3,16 +3,16 @@
3
3
  module Pubid
4
4
  module Evs
5
5
  # An EVS national adoption wraps a CEN identifier the same way BSI's
6
- # AdoptedEuropeanNorm does — polymorphic `adopted_identifier` so the
6
+ # AdoptedEuropeanNorm does — polymorphic `base` so the
7
7
  # wrapped object stays a real Pubid::CenCenelec identifier.
8
8
  #
9
9
  # Examples:
10
- # "EVS-EN 18216:2026" (adopted_identifier: EN 18216:2026)
11
- # "EVS-EN ISO 14001:2026" (adopted_identifier: EN ISO 14001:2026)
12
- # "EVS-EN ISO/IEC 27017:2026" (adopted_identifier: EN ISO/IEC 27017:2026)
13
- # "EVS-EN ISO 9001:2015/A1:2024" (adopted_identifier: EN ISO 9001:2015/A1:2024)
10
+ # "EVS-EN 18216:2026" (base: EN 18216:2026)
11
+ # "EVS-EN ISO 14001:2026" (base: EN ISO 14001:2026)
12
+ # "EVS-EN ISO/IEC 27017:2026" (base: EN ISO/IEC 27017:2026)
13
+ # "EVS-EN ISO 9001:2015/A1:2024" (base: EN ISO 9001:2015/A1:2024)
14
14
  class Identifier < ::Pubid::Identifier
15
- attribute :adopted_identifier, ::Pubid::Identifier, polymorphic: true
15
+ attribute :base, ::Pubid::Identifier, polymorphic: true
16
16
  # "-" or " " — preserves the printed separator ("EVS-EN" vs "EVS EN")
17
17
  attribute :separator, :string, default: -> { "-" }
18
18
 
@@ -6,10 +6,15 @@ module Pubid
6
6
  # The EVS national adoption of a European Standard.
7
7
  # Example: "EVS-EN 18216:2026"
8
8
  class NationalAdoption < ::Pubid::Evs::Identifier
9
- attribute :type, Components::Type, default: -> { self.class.type[:key] }
9
+ attribute :type, Components::Type, default: -> { self.class.default_type }
10
+
11
+ def self.default_type
12
+ Components::Type.new(abbr: type[:short])
13
+ end
10
14
 
11
15
  def self.type
12
- { key: :evs_en, title: "EVS National Adoption of a European Standard" }
16
+ { key: :evs_en, short: "EN",
17
+ title: "EVS National Adoption of a European Standard" }
13
18
  end
14
19
  end
15
20
  end
@@ -23,7 +23,7 @@ module Pubid
23
23
  end
24
24
 
25
25
  def adopted
26
- @id.adopted_identifier.to_s
26
+ @id.base.to_s
27
27
  end
28
28
  end
29
29
  end
@@ -17,7 +17,7 @@ module Pubid
17
17
  EVS_NAMESPACE = "urn:evs:"
18
18
 
19
19
  def generate
20
- adopted_urn = identifier.adopted_identifier.to_urn
20
+ adopted_urn = identifier.base.to_urn
21
21
  unless adopted_urn.start_with?(CEN_NAMESPACE)
22
22
  raise Errors::ParseError,
23
23
  "expected adopted CEN URN, got #{adopted_urn.inspect}"
@@ -56,7 +56,7 @@ module Pubid
56
56
 
57
57
  adopted = Pubid::CenCenelec.parse(text)
58
58
  ::Pubid::Evs::Identifiers::NationalAdoption.new(
59
- adopted_identifier: adopted,
59
+ base: adopted,
60
60
  )
61
61
  end
62
62
 
@@ -6,31 +6,36 @@ module Pubid
6
6
  #
7
7
  # The parser captures the publisher code verbatim (which may already
8
8
  # include the "/T" or "/Z" suffix). The builder normalizes: if the
9
- # suffix is in the publisher_code string, it's split out into the
10
- # separate +mandate+ attribute so the renderer can recompose either
11
- # the inline or split form.
9
+ # suffix is in the code, it is split out into the separate +mandate+
10
+ # attribute so the renderer can recompose either the inline or split
11
+ # form. The code itself goes into the inherited +publisher+ component.
12
12
  class Builder
13
13
  def self.build(parsed_data)
14
14
  new.build(parsed_data)
15
15
  end
16
16
 
17
17
  def build(data)
18
- publisher_code, mandate = split_mandate(data[:publisher_code].to_s)
19
-
20
- mandate ||= data[:mandate]&.to_s
18
+ code, mandate = split_mandate(data[:publisher_code].to_s)
21
19
 
22
20
  Identifiers::Standard.new(
23
- publisher_code: publisher_code,
24
- mandate: mandate,
21
+ publisher: ::Pubid::Components::Publisher.new(body: code),
22
+ mandate: mandate || data[:mandate]&.to_s,
25
23
  number: data[:number].to_s,
26
24
  part: data[:part]&.to_s,
27
- date: data[:year] ? ::Pubid::Components::Date.new(year: data[:year].to_s) : nil,
25
+ date: date_for(data[:year]),
28
26
  all_parts: !data[:all_parts].to_s.empty?,
29
27
  )
30
28
  end
31
29
 
32
30
  private
33
31
 
32
+ # The publication year, or nil for a partial reference.
33
+ def date_for(year)
34
+ return nil unless year
35
+
36
+ ::Pubid::Components::Date.new(year: year.to_s)
37
+ end
38
+
34
39
  # If the publisher code carries an inline /T or /Z suffix, split it off
35
40
  # and return the cleaned code + extracted mandate.
36
41
  def split_mandate(code)
@@ -7,9 +7,11 @@ module Pubid
7
7
  # descend from this class, so a parsed GB id is always an instance of
8
8
  # Pubid::Gb::Identifier.
9
9
  class Identifier < ::Pubid::Identifier
10
- # The issuing body code as printed, e.g. "GB", "JB", "T/GZAEPI".
11
- # Always non-empty for a valid identifier.
12
- attribute :publisher_code, :string
10
+ # The issuing body code as printed "GB", "JB", "GBn", "T/GZAEPI"
11
+ # lives in the `publisher` attribute inherited from ::Pubid::Identifier.
12
+ # The flat-scalar hooks below serialize it as a bare string, and the
13
+ # shared URN generator renders it lowercase, so the series is part of
14
+ # the URN identity (GB 20223 and GBn 20223 are two documents).
13
15
 
14
16
  # Mandate category: "T" (recommended), "Z" (guideline), or nil
15
17
  # (mandatory). Carried after the "/" in the printed form.
@@ -31,17 +33,34 @@ module Pubid
31
33
  "pubid:gb:standard" => "Pubid::Gb::Identifiers::Standard",
32
34
  }.freeze
33
35
 
36
+ # This block REPLACES the maps of ::Pubid::Identifier, so every
37
+ # attribute the flavor uses must appear here — "date" included, or the
38
+ # publication year is lost in to_hash. The shared flat-scalar rules
39
+ # (Identifier#to_hash / .from_hash) write it as a bare "year" scalar.
34
40
  key_value do
35
41
  map "_type", to: :_type, polymorphic_map: GB_TYPE_MAP
36
- map "publisher_code", to: :publisher_code
42
+ map "publisher", to: :publisher
37
43
  map "mandate", to: :mandate
38
44
  map "number", to: :number
39
45
  map "part", to: :part
46
+ map "date", to: :date
40
47
  map "all_parts", to: :all_parts, render_default: false
41
48
  end
42
49
 
43
50
  PUBLISHER = "CN"
44
51
 
52
+ # Serialize `publisher` as a bare string instead of a nested
53
+ # {"body" => "GB"}, the CEN/CENELEC precedent. The entry is added for
54
+ # GB's own classes only: a shared entry would change the wire format of
55
+ # every flavor that publishes an index.
56
+ def self.flat_scalar_components
57
+ super.merge(publisher: "publisher")
58
+ end
59
+
60
+ def self.flat_scalar_fields
61
+ super.merge(publisher: :body)
62
+ end
63
+
45
64
  def to_s(**opts)
46
65
  render(format: :human, **opts)
47
66
  end
@@ -25,6 +25,10 @@ module Pubid
25
25
  rule(:publisher_code) do
26
26
  # Social-group form: "T/" then 2+ uppercase letters/digits.
27
27
  (str("T/") >> match("[A-Z0-9]").repeat(1)).as(:publisher_code) |
28
+ # Confidential national series. The lowercase "n" is part of the
29
+ # code, so it comes before the uppercase-only rule below: a PEG
30
+ # takes the first branch that matches and does not backtrack.
31
+ (str("GBn") >> (str("/T") | str("/Z")).maybe).as(:publisher_code) |
28
32
  # Standard form: 1-3 letters, optional slash-T-or-Z.
29
33
  (match("[A-Z]").repeat(1, 3) >>
30
34
  (str("/T") | str("/Z")).maybe).as(:publisher_code)
@@ -8,7 +8,8 @@ module Pubid
8
8
  # "GB/T 20223-2006"
9
9
  # "GB/T 5606.1-2004"
10
10
  # "GB/T 5606 (all parts)"
11
- # "T/GZAEPI 0012018" (social-group form, em-dash year)
11
+ # "T/GZAEPI 001-2018" (social-group form; an em-dash year
12
+ # separator in the input renders as a hyphen)
12
13
  class Renderer < ::Pubid::Renderers::Base
13
14
  def render(**_opts)
14
15
  parts = [publisher_portion, " #{number_portion}"]
@@ -19,8 +20,8 @@ module Pubid
19
20
  private
20
21
 
21
22
  def publisher_portion
22
- code = @id.publisher_code.to_s
23
- code += "/#{@id.mandate}" if @id.mandate && !@id.publisher_code.include?("/")
23
+ code = @id.publisher.to_s
24
+ code += "/#{@id.mandate}" if @id.mandate && !code.include?("/")
24
25
  code
25
26
  end
26
27
 
data/lib/pubid/gb.rb CHANGED
@@ -17,6 +17,7 @@ module Pubid
17
17
  # "GB" comes first so PrefixesSupport's longest-match wins.
18
18
  PREFIXES = %w[
19
19
  GB GB/T GB/Z
20
+ GBn GBn/T GBn/Z
20
21
  DB DB/T DB/Z
21
22
  QB QB/T
22
23
  ZB ZB/T
@@ -498,9 +498,17 @@ module Pubid
498
498
  end
499
499
 
500
500
  attribute :_type, :string, polymorphic_class: true
501
- attribute :number, Components::Code
502
- attribute :part, Components::Code
503
- attribute :subpart, Components::Code
501
+ # Plain strings, for every flavor. A Components::Code here held nothing
502
+ # but its `value` on the identifier path, and the disagreement between
503
+ # this declaration and the leaves that redeclared a scalar was the
504
+ # determinism landmine: a redeclaration on an inherited-from class
505
+ # resolves nondeterministically under multi-flavor load. With the base
506
+ # declaring the scalar, no flavor redeclares and the bug class is gone.
507
+ # A flavor that needs structure keeps its own attributes (ISO's committee
508
+ # codes, NIST's series/subseries, ITU's code), never a boxed `number`.
509
+ attribute :number, :string
510
+ attribute :part, :string
511
+ attribute :subpart, :string
504
512
  attribute :stage_iteration, Components::Iteration
505
513
  attribute :date, Components::Date
506
514
  attribute :edition, Components::Edition
@@ -5,6 +5,13 @@ module Pubid
5
5
  module Idf
6
6
  # An IDF identifier has no copublisher
7
7
  class Identifier < ::Pubid::Identifier
8
+ # A Type INSTANCE, never the raw :key symbol - lutaml materializes
9
+ # defaults during from_hash; casting a Symbol into the
10
+ # component raises InvalidFormatError (pubid#383).
11
+ def self.default_type
12
+ Components::Type.new(abbr: type[:short])
13
+ end
14
+
8
15
  attribute :typed_stage, Components::TypedStage
9
16
 
10
17
  # `number`/`part`/`subpart` are declared here rather than inherited as a
@@ -4,7 +4,7 @@ module Pubid
4
4
  module Idf
5
5
  module Identifiers
6
6
  class Amendment < SupplementIdentifier
7
- attribute :type, Components::Type, default: -> { self.class.type[:key] }
7
+ attribute :type, Components::Type, default: -> { self.class.default_type }
8
8
 
9
9
  TYPED_STAGES = [
10
10
  Components::TypedStage.new(
@@ -4,7 +4,7 @@ module Pubid
4
4
  module Idf
5
5
  module Identifiers
6
6
  class Corrigendum < SupplementIdentifier
7
- attribute :type, Components::Type, default: -> { self.class.type[:key] }
7
+ attribute :type, Components::Type, default: -> { self.class.default_type }
8
8
 
9
9
  TYPED_STAGES = [
10
10
  Components::TypedStage.new(
@@ -5,7 +5,7 @@ module Pubid
5
5
  module Identifiers
6
6
  # International Standard Identifier
7
7
  class InternationalStandard < SingleIdentifier
8
- attribute :type, Components::Type, default: -> { self.class.type[:key] }
8
+ attribute :type, Components::Type, default: -> { self.class.default_type }
9
9
 
10
10
  TYPED_STAGES = [
11
11
  Components::TypedStage.new(
@@ -4,7 +4,7 @@ module Pubid
4
4
  module Idf
5
5
  module Identifiers
6
6
  class ReviewedMethod < SingleIdentifier
7
- attribute :type, Components::Type, default: -> { self.class.type[:key] }
7
+ attribute :type, Components::Type, default: -> { self.class.default_type }
8
8
  TYPED_STAGES = [
9
9
  Components::TypedStage.new(
10
10
  code: :pwirm,
@@ -12,7 +12,7 @@ module Pubid
12
12
  #
13
13
  # Note: The type attribute is inherited from the base Identifier class.
14
14
  # Concrete classes should add their own default value like:
15
- # attribute :type, Components::Type, default: -> { self.class.type[:key] }
15
+ # attribute :type, Components::Type, default: -> { self.class.default_type }
16
16
  class SingleIdentifier < Identifier
17
17
  end
18
18
  end
@@ -669,7 +669,14 @@ module Pubid
669
669
  # Extract publishers from joint_publishers
670
670
  if parsed[:joint_publishers]
671
671
  joint_pub_str = extract_value(parsed[:joint_publishers])
672
- attributes[:publishers] = joint_pub_str.split("/")
672
+ pubs = joint_pub_str.split("/")
673
+ attributes[:publishers] = pubs
674
+ # lutaml materializes attribute defaults during from_hash, so the
675
+ # deserialized joint carries publisher/copublisher alongside
676
+ # publishers; set the same shape on the parse path or the
677
+ # amendment-wrapped round-trip diverges on exactly those keys.
678
+ attributes[:publisher] = pubs.first
679
+ attributes[:copublisher] = pubs.drop(1)
673
680
  end
674
681
 
675
682
  # Build code with parts if present
@@ -693,6 +700,15 @@ module Pubid
693
700
  end
694
701
  attributes[:month] = extract_value(parsed[:month]) if parsed[:month]
695
702
 
703
+ # A text date trailing the DRAFT (pubid#216: "CD P26515/D1, March
704
+ # 2017") reaches the builder under :draft_month/:draft_year — the
705
+ # grammar captures them separately to avoid the :month/:year
706
+ # duplicate-subtree collision with the date clause.
707
+ if parsed[:draft_year]
708
+ attributes[:year] ||= extract_value(parsed[:draft_year])
709
+ attributes[:month] ||= extract_value(parsed[:draft_month])
710
+ end
711
+
696
712
  # Extract edition, from relaton's "/E-<n>" suffix (normalized to
697
713
  # "Edition <n>.0"). nil-residue hand-off item 1.
698
714
  attributes[:edition] = extract_value(parsed[:edition]) if parsed[:edition]
@@ -722,6 +738,10 @@ module Pubid
722
738
  attributes[:typed_stage] =
723
739
  Pubid::Ieee.locate_stage(stage_abbr)
724
740
  end
741
+ elsif parsed[:iso_published]
742
+ # Stage-less PUBLISHED joint form (pubid#317): ISO-led spelling,
743
+ # no project marker, no typed stage - renders as printed.
744
+ attributes[:lead_party] = "ISO"
725
745
  else
726
746
  # IEEE format - lead party is IEEE
727
747
  attributes[:lead_party] = "IEEE"
@@ -734,7 +754,19 @@ module Pubid
734
754
  Pubid::Ieee.locate_stage("P")
735
755
  end
736
756
 
737
- Identifiers::JointDevelopment.new(**attributes)
757
+ joint = Identifiers::JointDevelopment.new(**attributes)
758
+
759
+ # Amendment tail on the joint ISO-format form (pubid#317:
760
+ # "8802-11:2012/Amd.1:2014(E)"): wrap the joint id, mirroring
761
+ # build_flat_amendment.
762
+ return joint unless parsed[:amd_number]
763
+
764
+ amd_year = extract_value(parsed[:amd_year]) if parsed[:amd_year]
765
+ Identifiers::Amendment.new(
766
+ base: joint,
767
+ number: extract_value(parsed[:amd_number]),
768
+ year: amd_year,
769
+ )
738
770
  end
739
771
 
740
772
  # Build SI/PSI identifier from parsed data
@@ -1189,6 +1221,14 @@ module Pubid
1189
1221
  return "P"
1190
1222
  elsif type_value == "Std"
1191
1223
  return "Std"
1224
+ elsif type_value == "Draft Std" && parsed[:draft].nil? && parsed[:digit_draft].nil?
1225
+ # A version-less "Draft Std" fabricates /D1 (issue #205);
1226
+ # deriving the stage from that fabricated version renders the
1227
+ # same P-prefixed form as an explicit "/D1" spelling, so the
1228
+ # render reaches its fixed point in one round (pubid#318).
1229
+ # Gated on no-draft: a dotted version ("D2.0") fails the registry
1230
+ # lookup above and must fall through, keeping "Draft Std".
1231
+ return "D1"
1192
1232
  elsif type_value.match?(/^No\.?$/)
1193
1233
  return type_value
1194
1234
  end
@@ -30,8 +30,11 @@ module Pubid
30
30
  # Format: "IEEE/ASTM SI 10-1997" (published)
31
31
  # or: "IEEE/ASTM PSI 10/D2, October 2015" (draft)
32
32
 
33
- # Use proper Draft component (Lutaml::Model object)
34
- attr_accessor :draft_obj
33
+ # NOTE: no `attr_accessor :draft_obj` here — it would shadow the
34
+ # base's lazy `draft_obj` reader (base.rb), which rebuilds the
35
+ # component from the `draft` attribute after from_hash. The plain
36
+ # accessor made every deserialized PSI draft render (and re-emit
37
+ # in to_hash) without its "/D<n>".
35
38
  end
36
39
  end
37
40
  end