Module: Pubid::Ieee::Compaction

Defined in:
lib/pubid/ieee/compaction.rb

Overview

Serialization compaction for IEEE hashes — a recursive transform hooked into Ieee::Identifier#to_hash (collapse) and .from_hash (expand). Two compactions, both provably lossless and nested-safe (they descend into a Corrigendum base too):

  1. typed_stage -> stage scalar. The multi-field typed_stage object becomes a single stage key holding its canonical abbreviation, rebuilt from the TYPED_STAGES registry. The abbreviation keys the registry uniquely, and the collapse fires only when the registry's reconstruction is byte-identical to the serialized sub-hash — an unrecognised / non-matching stage is left as the full object (fails closed).

  2. draft slash strip. Components::Draft#to_s renders "/D3"; the leading / is boilerplate, so the serialized form is "D3". No expand step is needed — Draft.parse already accepts the slashless form and reproduces "/D3" (verified across the whole corpus), so from_hash and rendering are unchanged.

A recursive hash transform (rather than lutaml attributes) is deliberate: stage collides with the inherited Components::Stage attribute name (a redefinition would reintroduce the multi-flavor nondeterminism we hit with number), and only a whole-hash walk reaches a nested Corrigendum base, which a per-object to_hash override cannot (the nested-serialization wall). The runtime typed_stage/draft_obj objects are untouched.

Class Method Summary collapse

Class Method Details

.build_sub_hash_mapObject



40
41
42
# File 'lib/pubid/ieee/compaction.rb', line 40

def build_sub_hash_map
  TypedStages::TYPED_STAGES.to_h { |ts| [ts.abbr.first, entry_sub_hash(ts)] }
end

.collapse(node) ⇒ Object

Recursively compact every hash in the tree: collapse a reproducible typed_stage to a stage scalar and strip the leading / off draft.



62
63
64
65
66
# File 'lib/pubid/ieee/compaction.rb', line 62

def collapse(node)
  collapse_hash(node) if node.is_a?(::Hash)
  node.each { |v| collapse(v) } if node.is_a?(::Array)
  node
end

.collapse_hash(node) ⇒ Object



68
69
70
71
72
73
# File 'lib/pubid/ieee/compaction.rb', line 68

def collapse_hash(node)
  collapse_stage(node)
  draft = node["draft"]
  node["draft"] = draft.delete_prefix("/") if draft.is_a?(::String)
  node.each_value { |v| collapse(v) }
end

.collapse_stage(node) ⇒ Object

Collapse only when the registry rebuilds the exact same sub-hash, so no information is ever lost.



77
78
79
80
81
82
83
84
# File 'lib/pubid/ieee/compaction.rb', line 77

def collapse_stage(node)
  ts = node["typed_stage"]
  abbr = ts["abbr"]&.first if ts.is_a?(::Hash)
  return unless abbr && sub_hash_for(abbr) == ts

  node.delete("typed_stage")
  node["stage"] = abbr
end

.deep_dup(node) ⇒ Object



112
113
114
115
116
117
118
# File 'lib/pubid/ieee/compaction.rb', line 112

def deep_dup(node)
  case node
  when ::Hash then node.transform_values { |v| deep_dup(v) }
  when ::Array then node.map { |v| deep_dup(v) }
  else node
  end
end

.entry_sub_hash(typed_stage) ⇒ Object

NB: this must list every Components::TypedStage attribute. If a field is added there but not here, the collapse guard fails closed — affected stages stay full objects (still lossless) but stop compacting; extend this list to keep them compact.



48
49
50
51
52
53
54
55
56
57
58
# File 'lib/pubid/ieee/compaction.rb', line 48

def entry_sub_hash(typed_stage)
  {
    "abbr" => typed_stage.abbr,
    "stage_code" => typed_stage.stage_code,
    "type_code" => typed_stage.type_code,
    "iso_stage_equivalent" => typed_stage.iso_stage_equivalent,
    "ieee_draft_equivalent" => typed_stage.ieee_draft_equivalent,
    "approval_status" => typed_stage.approval_status,
    "project_status" => typed_stage.project_status || nil,
  }.compact
end

.expand(node) ⇒ Object

Inverse of collapse for stage: replace a recognised stage scalar with its registry typed_stage sub-hash so lutaml rebuilds the component (nested bases included). draft needs no expansion — Draft.parse accepts the slashless form. An unrecognised value is left alone.



90
91
92
93
94
# File 'lib/pubid/ieee/compaction.rb', line 90

def expand(node)
  expand_hash(node) if node.is_a?(::Hash)
  node.each { |v| expand(v) } if node.is_a?(::Array)
  node
end

.expand_hash(node) ⇒ Object



96
97
98
99
100
101
102
103
104
# File 'lib/pubid/ieee/compaction.rb', line 96

def expand_hash(node)
  key = stage_key(node)
  sub = sub_hash_for(node[key].to_s) if key
  if sub
    node.delete(key)
    node["typed_stage"] = deep_dup(sub)
  end
  node.each_value { |v| expand(v) }
end

.stage_key(node) ⇒ Object



106
107
108
109
110
# File 'lib/pubid/ieee/compaction.rb', line 106

def stage_key(node)
  return :stage if node.key?(:stage)

  "stage" if node.key?("stage")
end

.sub_hash_for(abbr) ⇒ Object

abbr => canonical serialized typed_stage sub-hash, mirroring what the parent serializer + canonicalizer emit (nils dropped, and a default project_status: false dropped). Memoized.



36
37
38
# File 'lib/pubid/ieee/compaction.rb', line 36

def sub_hash_for(abbr)
  (@sub_hash_for ||= build_sub_hash_map)[abbr]
end