Module: Minitwin::ClassMethods::Dsl

Included in:
Minitwin::ClassMethods
Defined in:
lib/minitwin/class_methods/dsl.rb

Instance Method Summary collapse

Instance Method Details

#block_propertiesObject

: () -> Array



9
10
11
# File 'lib/minitwin/class_methods/dsl.rb', line 9

def block_properties
  @block_properties ||= []
end

#collection(name, validates: {}, default: [], as: nil, getter: nil, twin: nil, on: nil, **_opts, &block) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/minitwin/class_methods/dsl.rb', line 41

def collection(name, validates: {}, default: [], as: nil, getter: nil, twin: nil, on: nil, **_opts, &block)
  nested_class = block ? create_nested_class(name:, &block) : nil
  element_klass = twin || nested_class

  define_method("#{name}=") do |values|
    arr = self.class.send(:coerce_collection_array, values)
    coerced_values = arr.map { |v| self.class.send(:coerce_value_to_twin, v, element_klass) }
    define_instance_variable(name:, value: coerced_values)
    # :nocov:
    if !@__skip_alias_recompute__ && self.class.dynamic_aliases?
      __recompute_dynamic_aliases__
    end
    # :nocov:
  end
  alias_method "#{name}_attributes=", "#{name}="

  define_getter_method(name:, on:, as:, default:, getter:, type: nil)
  alias_method "#{name}_attributes", name
  add_validation(name:, validates:)
  add_collection_property(name:)
  invalidate_caches

  collections[name.to_sym] = { element_twin: element_klass, as: as }
  add_to_property_order(name)
end

#collection_propertiesObject

: () -> Array



14
15
16
# File 'lib/minitwin/class_methods/dsl.rb', line 14

def collection_properties
  @collection_properties ||= []
end

#dynamic_nested_aliasesObject

: () -> Array



29
30
31
# File 'lib/minitwin/class_methods/dsl.rb', line 29

def dynamic_nested_aliases
  @dynamic_nested_aliases ||= []
end

#nested(name, as: nil, &block) ⇒ Object

Raises:

  • (ArgumentError)


139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/minitwin/class_methods/dsl.rb', line 139

def nested(name, as: nil, &block)
  raise ArgumentError, "nested requires a block" unless block_given?

  property(name, as: as, &block)

  # Pull the nested class directly from the registration `property`
  # just performed instead of round-tripping through `const_get`.
  nested_klass = properties[name.to_sym]&.[](:nested_class)

  # Registry for dynamic nested aliases (as: -> { ... }) on leafs.
  # Reader defined once in the module body above.
  leafs = []
  if nested_klass.respond_to?(:properties)
    extract_leaf_properties = ->(klass, path) do
      klass.properties.each do |prop, meta|
        if meta[:nested_class]
          extract_leaf_properties.call(meta[:nested_class], path + [prop])
        else
          leafs << { path: (path + [prop]), as: (meta[:as] if meta[:as] && meta[:as] != prop) }
        end
      end
    end
    extract_leaf_properties.call(nested_klass, [])
  end

  leafs.each do |leaf| # rubocop: disable Metrics/BlockLength
    path = leaf[:path]
    prop = path.last
    as_meta = leaf[:as]

    # Define a stable internal reader for this leaf to support dynamic aliasing
    target_reader = "#{Minitwin::NESTED_READER_PREFIX}#{([name] + path).join("__")}"
    define_method(target_reader) do
      obj = send(name)
      obj = Minitwin::Utils.traverse_path(obj, path[0..-2])
      if as_meta.is_a?(Proc)
        # When inner property has a dynamic alias, original reader may be protected.
        obj.send(prop)
      else
        inner_read = as_meta || prop
        # Use send to allow accessing protected original readers
        obj.send(inner_read)
      end
    end

    # Setter uses original base name to call the nested twin's writer.
    define_method("#{prop}=") do |value|
      obj = send(name)
      obj = Minitwin::Utils.traverse_path(obj, path[0..-2])
      obj.public_send("#{prop}=", value)
      if !@__skip_alias_recompute__ && self.class.dynamic_aliases?
        __recompute_dynamic_aliases__
      end
    end

    # Static alias: define a public getter method with the alias name
    if as_meta && !as_meta.is_a?(Proc)
      alias_name = as_meta
      define_method(alias_name) do
        send(target_reader)
      end
      unexposed_properties << alias_name

      # Define protected getter with original name so sync can read the value,
      # and register in properties so sync resolves the as: alias.
      define_method(prop) { send(target_reader) }
      protected prop
      properties[prop.to_sym] = { type: nil, as: as_meta, expose: true, nested_proxy: true }
    else
      # Dynamic alias: register for instance-level aliasing and rely on
      # __recompute_dynamic_aliases__ to create the per-instance method.
      dynamic_nested_aliases << { target: target_reader.to_sym, as: as_meta || prop, group: name, path: path }
    end

    # Always hide the internal reader from serialization
    unexposed_properties << target_reader.to_sym
  end

  invalidate_caches
end

#property(name, validates: {}, default: nil, as: nil, expose: true, readonly: false, type: nil, getter: nil, setter: nil, twin: nil, on: nil, **_opts, &block) ⇒ Object



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/minitwin/class_methods/dsl.rb', line 79

def property(
  name, validates: {}, default: nil, as: nil, expose: true, readonly: false, type: nil, getter: nil, setter: nil,
  twin: nil, on: nil, **_opts, &block
)
  nested_class = nil

  if block_given?
    raise "setters are not possible in blocks" if setter

    nested_class = create_nested_class(name:, &block)

    define_method("#{name}=") do |value|
      coerced = self.class.send(:coerce_value_to_twin, value, nested_class)
      raise "Unprocessable input for property '#{name}'." unless coerced.nil? || coerced.is_a?(nested_class)

      define_instance_variable(name:, value: coerced)
      if !@__skip_alias_recompute__ && self.class.dynamic_aliases?
        __recompute_dynamic_aliases__
      end
    end

    add_block_property(name:)
  else
    define_method("#{name}=") do |value|
      coerced_value =
        if twin
          self.class.send(:coerce_value_to_twin, value, twin)
        elsif setter
          setter.call(value)
        elsif type && !value.nil?
          self.class.send(:coerce_with_type, value, type)
        else
          value
        end
      define_instance_variable(name:, value: coerced_value)
      if !@__skip_alias_recompute__ && self.class.dynamic_aliases?
        __recompute_dynamic_aliases__
      end
    end
  end

  define_getter_method(name:, as:, on:, default:, getter:, type: type)
  add_validation(name:, validates:)
  add_unexposed_property(name:, expose:)
  invalidate_caches

  properties[name.to_sym] = {
    type: type,
    as: as,
    expose: expose,
    readonly: readonly
  }
  properties[name.to_sym][:twin] = twin if twin
  properties[name.to_sym][:nested_class] = nested_class if nested_class
  add_to_property_order(name)
end

#property_orderObject

: () -> Array



24
25
26
# File 'lib/minitwin/class_methods/dsl.rb', line 24

def property_order
  @property_order ||= []
end

#unexposed_propertiesObject

: () -> Array



19
20
21
# File 'lib/minitwin/class_methods/dsl.rb', line 19

def unexposed_properties
  @unexposed_properties ||= []
end