Module: Minitwin::Serialization

Included in:
Minitwin
Defined in:
lib/minitwin/serialization.rb,
sig/generated/minitwin/serialization.rbs

Overview

Serialization to Hash/JSON and ActiveModel validation aggregation. Converts nested twins recursively and preserves array items (dropping only nil). When ActiveModel validations are available, nested errors are surfaced on the parent using dot/bracket notation.

Constant Summary collapse

ALIASES_VAR =

Cache constant references for JIT optimization

Returns:

  • (Object)
Minitwin::DYNAMIC_ALIASES_VAR
NESTED_PREFIX =

Returns:

  • (Object)
Minitwin::NESTED_READER_PREFIX

Instance Method Summary collapse

Instance Method Details

#attributesHash[Symbol, untyped]

: () -> Hash[Symbol, untyped]

Returns:

  • (Hash[Symbol, untyped])


58
59
60
61
62
63
64
# File 'lib/minitwin/serialization.rb', line 58

def attributes
  # Use setter-based attribute names and read via `send` to allow
  # accessing protected original readers when aliases (`as:`) are used.
  attribute_methods.each_with_object({}) do |m, h|
    h[m] = send(m) if respond_to?(m, true)
  end
end

#display_name_for(method) ⇒ Object

Parameters:

  • method (Object)

Returns:

  • (Object)


153
154
155
156
# File 'lib/minitwin/serialization.rb', line 153

def display_name_for(method)
  meta = self.class.properties[method] || self.class.collections[method]
  meta && meta[:as] && !meta[:as].is_a?(Proc) ? meta[:as] : method
end

#dynamic_aliases_for_ppObject

Returns:

  • (Object)


158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/minitwin/serialization.rb', line 158

def dynamic_aliases_for_pp
  return [] unless instance_variable_defined?(ALIASES_VAR)

  aliases = instance_variable_get(ALIASES_VAR)
  return [] unless aliases && !aliases.empty?

  aliases.filter_map do |target_method, alias_method|
    next if target_method.is_a?(Symbol) && target_method.to_s.start_with?(NESTED_PREFIX)

    [alias_method, send(target_method)]
  end
end

#inspectString

: () -> String

Returns:

  • (String)


102
103
104
105
# File 'lib/minitwin/serialization.rb', line 102

def inspect
  attrs = to_hash.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
  "#<#{self.class.name} #{attrs}>"
end

#ordered_attributes_for_ppObject

Returns:

  • (Object)


141
142
143
144
145
# File 'lib/minitwin/serialization.rb', line 141

def ordered_attributes_for_pp
  methods = ordered_methods_for_pp
  attrs = methods.map { |m| [display_name_for(m), send(m)] }
  attrs + dynamic_aliases_for_pp
end

#ordered_methods_for_ppObject

Returns:

  • (Object)


147
148
149
150
151
# File 'lib/minitwin/serialization.rb', line 147

def ordered_methods_for_pp
  ordered = self.class.send(:property_order)
  all_methods = self.class.send(:serializable_getters)
  ordered.select { |m| all_methods.include?(m) } + (all_methods - ordered)
end

#pretty_print(pretty_printer) ⇒ void

This method returns an undefined value.

Internal helper for PrettyPrint. Do not call this on your own. : (PP) -> void

Parameters:

  • (PP)


109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/minitwin/serialization.rb', line 109

def pretty_print(pretty_printer)
  pretty_printer.object_group(self) do
    pretty_printer.breakable
    pretty_printer.seplist(
      ordered_attributes_for_pp,
      -> {
        pretty_printer.text(",")
        pretty_printer.breakable
      }
    ) do |(name, value)|
      pretty_printer.group do
        pretty_printer.text name.to_s
        pretty_printer.text ": "
        pretty_printer.pp value
      end
    end
  end
end

#to_hash(render_nil: false) ⇒ Hash[Symbol, untyped] Also known as: to_h

: (render_nil: bool) -> Hash[Symbol, untyped]

Parameters:

  • render_nil: (Boolean) (defaults to: false)

Returns:

  • (Hash[Symbol, untyped])


15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/minitwin/serialization.rb', line 15

def to_hash(render_nil: false)
  hash = Minitwin.hash_klass.new

  methods_to_serialize = self.class.send(:serializable_getters)

  methods_to_serialize.each do |method|
    value = send(method)
    next if value.nil? && !render_nil

    hash[method] = transform_value_for_serialization(value)
  end

  # Include dynamic alias keys (defined per instance via `as: -> { ... }`).
  if instance_variable_defined?(ALIASES_VAR)
    aliases = instance_variable_get(ALIASES_VAR)
    if aliases && !aliases.empty?
      aliases.each do |target_method, alias_method|
        # Skip nested proxy aliases at the top level; nested groups
        # serialize under their container key only.
        next if target_method.is_a?(Symbol) && target_method.to_s.start_with?(NESTED_PREFIX)

        # Read the value from the original target method to avoid issues if
        # the alias method is overridden. Apply the same transformation rules
        # as above for nested twins and arrays.
        value = send(target_method)
        next if value.nil? && !render_nil

        hash[alias_method] = transform_value_for_serialization(value)
      end
    end
  end

  hash
end

#to_jsonString

: (**untyped) -> String

Parameters:

  • (Object)

Returns:

  • (String)


53
54
55
# File 'lib/minitwin/serialization.rb', line 53

def to_json(**)
  to_hash(**).to_json
end

#transform_value_for_serialization(value) ⇒ Object

Parameters:

  • value (Object)

Returns:

  • (Object)


130
131
132
133
134
135
136
137
138
139
# File 'lib/minitwin/serialization.rb', line 130

def transform_value_for_serialization(value)
  case value
  when Minitwin
    value.to_hash
  when Array
    value.filter_map { |item| item.respond_to?(:to_hash) ? item.to_hash : item }
  else
    value
  end
end

#valid?Boolean

: () -> bool

Returns:

  • (Boolean)


67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/minitwin/serialization.rb', line 67

def valid?
  # If ActiveModel validations are available and included, run them and
  # aggregate nested errors. Otherwise, consider the twin valid.
  if defined?(ActiveModel::Validations) && self.class.ancestors.include?(ActiveModel::Validations)
    super

    self.class.block_properties.each do |property|
      child = send(property)
      next unless child.respond_to?(:valid?)

      child.valid?
      child.errors.each do |attribute|
        errors.add("#{property}.#{attribute.attribute}", attribute.message)
      end
    end

    self.class.collection_properties.each do |property|
      send(property).each_with_index do |value, index|
        next unless value.respond_to?(:valid?)

        value.valid?
        value.errors.each do |attribute|
          errors.add("#{property}[#{index}].#{attribute.attribute}", attribute.message)
        end
      end
    end

    errors.empty?
  else
    # When ActiveModel is not available, consider the twin valid by default.
    true
  end
end