pubid 2.0.0.pre.alpha.4 → 2.0.0.pre.alpha.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd077b48356a46dfb52d7b95e932c29e91e5fe1a7859c1128ddda22d768b4a96
4
- data.tar.gz: 4e8059d20bed9297393f54bb894958a6c0ab8d92bef895fcb7528c291b3ff349
3
+ metadata.gz: 70b24e82967e02c1a0a641c5adc3ad9a77e563f0b7e6f69a59aa4f02971db3d2
4
+ data.tar.gz: c206002602026012406fa8ebe4339ff6ca0f02db7aa0b00ba49796a034cb697c
5
5
  SHA512:
6
- metadata.gz: 329f3776c53f4021bfd2d25bd8698fb9c01309a66f56e9334d815aee3fde8ce25b509637f97ffc4ef91f0dfb67d10768a33505a939109a305d34b2e917af8f6c
7
- data.tar.gz: 79c43b4e3ca83fe379a588f20490850e6a88757e382f0f9b9fc88d9f31923aff8fd8a5d48670c33a0d0705747820c6b3548260334ebf4e63a09b60dab82ad4ec
6
+ metadata.gz: f74a26e175164470260014a2d492eadfc2f396b0b56bc9f5848fc79168c1ec1d627380bb64c83126bd0eeb2841da40828e336cf263b785b4c2c1959a712b95c5
7
+ data.tar.gz: b450a0f2343231dff1174c01cec84fdacda74279134fed4c913a552fcd3c7a82e662e1fba5e52018462c295de1df9ba5c78af5a62a1ebde09974fbcd8270f7d6
@@ -125,6 +125,84 @@ module Pubid
125
125
  date&.year&.to_s
126
126
  end
127
127
 
128
+ # Canonicalize the serialized hash so it never carries a defaulted attribute
129
+ # still at its default (or empty) value. This makes to_hash a pure function
130
+ # of the identifier's values, independent of how the object was built.
131
+ #
132
+ # Motivation: lutaml materializes each attribute's default as an explicit
133
+ # assignment during deserialization (flipping using_default? to false), so a
134
+ # naive from_hash(x).to_hash re-emits defaults that parse(x).to_hash omits —
135
+ # breaking the exact-equality round-trip relaton-index relies on
136
+ # (from_hash(raw).to_hash == raw). pubid never consults lutaml's unset
137
+ # tracking and defines no render_default: true, so a defaulted attribute at
138
+ # its default/empty value carries no meaning and does not belong in the
139
+ # canonical hash. Dropping it here (rather than repairing from_hash) fixes
140
+ # the round-trip through every construction path — parse, from_hash, manual.
141
+ def to_hash(*args)
142
+ hash = super
143
+ canonicalize_hash(self, hash) if hash.is_a?(::Hash)
144
+ hash
145
+ end
146
+
147
+ # Recursively drop attributes holding only their default (or empty) value
148
+ # from +hash+, the serialization of +model+. Recurses into nested component
149
+ # / identifier values because lutaml serializes those via its own transform
150
+ # — bypassing their public to_hash — so a nested defaulted attribute (e.g.
151
+ # Components::Supplement#has_revision) would otherwise leak into the parent
152
+ # hash and break the idempotent round-trip.
153
+ def canonicalize_hash(model, hash)
154
+ register = model.lutaml_register
155
+ model.class.attributes(register).each do |name, attr|
156
+ canonicalize_attr(model, hash, name, attr) unless name == :_type
157
+ end
158
+ end
159
+
160
+ # Canonicalize a single attribute against its serialized value in +hash+:
161
+ # drop it when it holds only its default/empty value, else recurse into it.
162
+ def canonicalize_attr(model, hash, name, attr)
163
+ key = hash.key?(name.to_s) ? name.to_s : name
164
+ return unless hash.key?(key)
165
+
166
+ value = model.public_send(name)
167
+ if default_valued?(value, attr, model)
168
+ hash.delete(key)
169
+ else
170
+ canonicalize_nested(value, hash[key])
171
+ end
172
+ end
173
+
174
+ # True when +value+ is +attr+'s default (or empty), so it can be dropped.
175
+ def default_valued?(value, attr, model)
176
+ register = model.lutaml_register
177
+ attr.default_set?(register, model) &&
178
+ (Lutaml::Model::Utils.empty?(value) ||
179
+ value == attr.default(register, model))
180
+ end
181
+
182
+ # Recurse into a nested component / identifier (or a collection of them) so
183
+ # its own defaulted attributes are canonicalized against its sub-hash.
184
+ def canonicalize_nested(value, sub)
185
+ if value.is_a?(Lutaml::Model::Serialize)
186
+ canonicalize_hash(value, sub) if sub.is_a?(::Hash)
187
+ elsif value.is_a?(::Array)
188
+ canonicalize_collection(value, sub)
189
+ end
190
+ end
191
+
192
+ # Canonicalize each element of a collection against its serialized sub-hash.
193
+ # Only zip when object and serialized sizes match, so a (never-observed)
194
+ # length mismatch can't pair an element with the wrong sub-hash — worst case
195
+ # it leaves that collection untouched.
196
+ def canonicalize_collection(models, subs)
197
+ return unless subs.is_a?(::Array) && models.size == subs.size
198
+
199
+ models.each_with_index do |model, i|
200
+ canonicalize_nested(model, subs[i])
201
+ end
202
+ end
203
+ private :canonicalize_hash, :canonicalize_attr, :default_valued?,
204
+ :canonicalize_nested, :canonicalize_collection
205
+
128
206
  def initialize(attrs = {}, options = {})
129
207
  attrs = attrs.dup
130
208
  attrs[:_type] ||= self.class.polymorphic_name
@@ -78,13 +78,14 @@ module Pubid
78
78
 
79
79
  def build_supplement(parsed_hash)
80
80
  marker = parsed_hash[:trailing_marker].to_s if parsed_hash[:trailing_marker]
81
+ plus_marker = parsed_hash[:plus_marker].to_s if parsed_hash[:plus_marker]
81
82
 
82
83
  # Determine supplement type. The trailing word ("Amendment"/"Errata")
83
84
  # selects the class; the concrete class then carries the word via
84
85
  # #supplement_type, so only the `trailing` flag needs storing.
85
86
  supplement_class = if parsed_hash[:annex_letter] || parsed_hash[:annex_marker]
86
87
  Identifiers::Annex
87
- elsif marker == "Errata"
88
+ elsif marker == "Errata" || plus_marker == "Errata"
88
89
  Identifiers::Errata
89
90
  else
90
91
  Identifiers::Amendment
@@ -92,6 +93,7 @@ module Pubid
92
93
 
93
94
  supplement = supplement_class.new
94
95
  supplement.trailing = true if marker
96
+ supplement.joined = true if plus_marker
95
97
 
96
98
  # Recursively parse base identifier
97
99
  if parsed_hash[:base_identifier]
@@ -19,7 +19,8 @@ module Pubid
19
19
  # Main identifier pattern - check supplements first
20
20
  rule(:identifier) do
21
21
  amendment_identifier | amendment_short | annex_letter_identifier |
22
- annex_identifier | trailing_supplement_identifier | base_identifier
22
+ annex_identifier | plus_supplement_identifier |
23
+ trailing_supplement_identifier | base_identifier
23
24
  end
24
25
 
25
26
  # Publisher - always "OIML"
@@ -172,6 +173,18 @@ module Pubid
172
173
  language_portion.maybe.as(:language)
173
174
  end
174
175
 
176
+ # Plus-joined supplement - "BASE:YEAR+Supplement:YEAR" form where both
177
+ # the base and the supplement carry their own year. Used for amendments
178
+ # and errata to dated bases (e.g. "OIML B 10:2011+Amendment:2012").
179
+ # Annexes already encode the year-on-base intent via their own model.
180
+ rule(:plus_supplement_identifier) do
181
+ base_without_language.as(:base_identifier) >>
182
+ str("+") >>
183
+ (str("Amendment") | str("Errata")).as(:plus_marker) >>
184
+ (colon >> year_digits.as(:year)).maybe >>
185
+ language_portion.maybe.as(:language)
186
+ end
187
+
175
188
  # Annex identifier - "BASE Annexes Edition YYYY" / "BASE Annexes:YYYY" /
176
189
  # "BASE:YYYY Annexes" (year on the base, no annex year).
177
190
  rule(:annex_identifier) do
@@ -84,6 +84,16 @@ module Pubid
84
84
  def render_supplement(id)
85
85
  format = effective_format(id)
86
86
 
87
+ # Plus-joined: "BASE+Amendment:YEAR" / "BASE+Errata:YEAR" with both
88
+ # the base and the supplement carrying their own year.
89
+ if id.joined
90
+ base_str = strip_language(id.base_identifier.to_s)
91
+ result = "#{base_str}+#{id.supplement_type}"
92
+ result += ":#{id.year}" if id.year
93
+ result += " (#{id.language})" if id.language
94
+ return result
95
+ end
96
+
87
97
  # Trailing-word shorthand: "BASE Amendment" / "BASE Errata" with the
88
98
  # publication year kept on the base identifier. The word comes from the
89
99
  # concrete supplement class.
@@ -13,6 +13,9 @@ module Pubid
13
13
  # "Amendment (YYYY) to BASE" prose form. The word itself comes from the
14
14
  # concrete class (#supplement_type), so only this flag is stored.
15
15
  attribute :trailing, :boolean, default: false
16
+ # True for the plus-joined form ("OIML B 10:2011+Amendment:2012") where
17
+ # both the base and the supplement carry their own year, joined by "+".
18
+ attribute :joined, :boolean, default: false
16
19
  attribute :parsed_format, :string, default: -> {
17
20
  "short"
18
21
  } # Track supplement's parsed format
@@ -25,6 +28,7 @@ module Pubid
25
28
  with: { to: :base_identifier_to_kv, from: :base_identifier_from_kv }
26
29
  map "year", to: :year
27
30
  map "trailing", to: :trailing
31
+ map "joined", to: :joined
28
32
  end
29
33
 
30
34
  def base_identifier_to_kv(model, doc)
data/lib/pubid/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pubid
4
- VERSION = "2.0.0.pre.alpha.4"
4
+ VERSION = "2.0.0.pre.alpha.5"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pubid
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.pre.alpha.4
4
+ version: 2.0.0.pre.alpha.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-03 00:00:00.000000000 Z
11
+ date: 2026-07-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model