Module: Minitwin::ClassMethods::Dsl

Included in:
Minitwin::ClassMethods
Defined in:
lib/minitwin/class_methods/dsl.rb,
sig/generated/minitwin/class_methods/dsl.rbs

Instance Method Summary collapse

Instance Method Details

#add_block_property(name:) ⇒ Object

Parameters:

  • name: (Object)

Returns:

  • (Object)


382
383
384
# File 'lib/minitwin/class_methods/dsl.rb', line 382

def add_block_property(name:)
  block_properties << name.to_sym
end

#add_collection_property(name:) ⇒ Object

Parameters:

  • name: (Object)

Returns:

  • (Object)


386
387
388
# File 'lib/minitwin/class_methods/dsl.rb', line 386

def add_collection_property(name:)
  collection_properties << name.to_sym
end

#add_to_property_order(name) ⇒ Object

Parameters:

  • name (Object)

Returns:

  • (Object)


390
391
392
393
# File 'lib/minitwin/class_methods/dsl.rb', line 390

def add_to_property_order(name)
  key = name.to_sym
  property_order << key unless property_order.include?(key)
end

#add_unexposed_property(name:, expose:) ⇒ Object

Parameters:

  • name: (Object)
  • expose: (Object)

Returns:

  • (Object)


378
379
380
# File 'lib/minitwin/class_methods/dsl.rb', line 378

def add_unexposed_property(name:, expose:)
  unexposed_properties << name unless expose
end

#add_validation(name:, validates:) ⇒ Object

Parameters:

  • name: (Object)
  • validates: (Object)

Returns:

  • (Object)


362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/minitwin/class_methods/dsl.rb', line 362

def add_validation(name:, validates:)
  return if validates.nil?
  return if validates.respond_to?(:empty?) && validates.empty?

  raise "Validation is not possible, because activemodel is not available" unless respond_to?(:validates)

  if validates.is_a?(Proc)
    validate do
      value = send(name)
      errors.add(name, "is invalid") unless validates.call(value)
    end
  else
    validates(name, **validates)
  end
end

#apply_alias_to_getter(name:, as:) ⇒ Object

Parameters:

  • name: (Object)
  • as: (Object)

Returns:

  • (Object)


349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/minitwin/class_methods/dsl.rb', line 349

def apply_alias_to_getter(name:, as:)
  return if as.nil?

  if as.is_a?(Proc)
    # Dynamic alias: protect original reader and let instances
    # compute and define the alias method at runtime.
    protected name
  elsif name != as
    alias_method as, name
    protected name
  end
end

#block_propertiesArray[Symbol]

: () -> Array

Returns:

  • (Array[Symbol])


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

def block_properties
  @block_properties ||= []
end

#build_composition_getter(name:, on:, default:, type:) ⇒ Object

Parameters:

  • name: (Object)
  • on: (Object)
  • default: (Object)
  • type: (Object)

Returns:

  • (Object)


277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/minitwin/class_methods/dsl.rb', line 277

def build_composition_getter(name:, on:, default:, type:)
  # Resolve model ivar name at definition time when on is a symbol
  model_ivar = on.is_a?(Proc) ? nil : internal_model_name(on)
  # Collection metadata will be resolved after property registration
  # via a lazy lookup on first access, then cached in the closure.
  col_meta = nil
  col_meta_resolved = false

  -> { # rubocop: disable Metrics/BlockLength
    # Get composition model - handle both symbol and proc cases
    model = if on.is_a?(Proc)
              instance_exec(&on)
            else
              instance_variable_get(model_ivar) || begin
                send(on)
              rescue NoMethodError
                nil
              end
            end

    # Validate model
    if model.nil?
      raise(
        "Property '#{name}' refers to unknown composition source '#{on}' in #{self.class}. " \
          "Ensure the model is provided via from_objects or a reader exists."
      )
    end
    unless model.respond_to?(name)
      raise "The instance of '#{model.class}' does not respond to '#{name}'."
    end

    raw = model.send(name)

    # Return default if raw is nil
    return self.class.send(:resolve_default_value, default, type) if raw.nil?

    # Resolve collection metadata once and cache in closure
    unless col_meta_resolved
      col_meta = begin
        self.class.collections[name.to_sym]
      rescue StandardError
        nil
      end
      col_meta_resolved = true
    end

    if col_meta && (raw.is_a?(Array) || raw.respond_to?(:to_a))
      elem_klass = col_meta[:element_twin]
      arr = self.class.send(:coerce_collection_array, raw)
      arr.map { |v| self.class.send(:coerce_value_to_twin, v, elem_klass) }
    else
      type ? self.class.send(:coerce_with_type, raw, type) : raw
    end
  }
end

#build_getter_proc(name:, on:, default:, getter:, type:) ⇒ Object

Parameters:

  • name: (Object)
  • on: (Object)
  • default: (Object)
  • getter: (Object)
  • type: (Object)

Returns:

  • (Object)


252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/minitwin/class_methods/dsl.rb', line 252

def build_getter_proc(name:, on:, default:, getter:, type:)
  if getter
    ivar = Minitwin::Utils.ivar_name(name)
    if getter.is_a?(Symbol)
      return -> {
        if self.class.instance_method(getter).arity.zero?
          send(getter)
        else
          send(getter, instance_variable_get(ivar))
        end
      }
    elsif getter.arity.zero?
      return -> { instance_exec(&getter) }
    else
      return -> { instance_exec(instance_variable_get(ivar), &getter) }
    end
  end

  if on
    build_composition_getter(name:, on:, default:, type:)
  else
    build_regular_getter(name:, default:, type:)
  end
end

#build_regular_getter(name:, default:, type:) ⇒ Object

Parameters:

  • name: (Object)
  • default: (Object)
  • type: (Object)

Returns:

  • (Object)


333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# File 'lib/minitwin/class_methods/dsl.rb', line 333

def build_regular_getter(name:, default:, type:)
  # Compute ivar_name at definition time for JIT optimization.
  # Type coercion happens on assignment (setter) so the getter just reads.
  ivar = Minitwin::Utils.ivar_name(name)
  -> {
    if instance_variable_defined?(ivar)
      val = instance_variable_get(ivar)
      return self.class.send(:resolve_default_value, default, type) if val.nil?

      val
    else
      self.class.send(:resolve_default_value, default, type)
    end
  }
end

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

This method returns an undefined value.

Parameters:

  • name (Symbol)
  • validates: (Hash[Symbol, untyped]) (defaults to: {})
  • default: (Object) (defaults to: [])
  • as: (Object) (defaults to: nil)
  • getter: (Proc) (defaults to: nil)
  • twin: (Object) (defaults to: nil)
  • on: (Symbol) (defaults to: nil)
  • _opts (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_propertiesArray[Symbol]

: () -> Array

Returns:

  • (Array[Symbol])


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

def collection_properties
  @collection_properties ||= []
end

#constantize_name(name) ⇒ Object

Parameters:

  • name (Object)

Returns:

  • (Object)


222
223
224
# File 'lib/minitwin/class_methods/dsl.rb', line 222

def constantize_name(name)
  name.to_s.split("_").map(&:capitalize).join
end

#create_nested_class(name:, &block) ⇒ void

This method returns an undefined value.

Parameters:

  • name: (Object)


226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/minitwin/class_methods/dsl.rb', line 226

def create_nested_class(name:, &block)
  Class.new(Minitwin).tap do |klass|
    if defined?(ActiveModel::Name)
      klass.define_singleton_method(:model_name) do
        ActiveModel::Name.new(self, nil, name.to_s)
      end
    end

    klass.class_eval(&block) if block

    const_name = constantize_name(name)
    begin
      const_set(const_name, klass) unless const_defined?(const_name, false)
    rescue NameError
      # Expected: Constant name may be invalid or already defined in complex scenarios.
      # The nested class is still accessible via the klass variable.
    end
  end
end

#define_getter_method(name:, as:, on:, default:, getter:, type: nil) ⇒ Object

Parameters:

  • name: (Object)
  • as: (Object)
  • on: (Object)
  • default: (Object)
  • getter: (Object)
  • type: (Object) (defaults to: nil)

Returns:

  • (Object)


246
247
248
249
250
# File 'lib/minitwin/class_methods/dsl.rb', line 246

def define_getter_method(name:, as:, on:, default:, getter:, type: nil)
  getter_proc = build_getter_proc(name:, on:, default:, getter:, type:)
  define_method(name, &getter_proc)
  apply_alias_to_getter(name:, as:)
end

#dynamic_nested_aliasesArray[Hash]

: () -> Array

Returns:

  • (Array[Hash])


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) ⇒ void

This method returns an undefined value.

Parameters:

  • name (Symbol)
  • as: (Object) (defaults to: nil)


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) ⇒ void

This method returns an undefined value.

Parameters:

  • name (Symbol)
  • validates: (Hash[Symbol, untyped]) (defaults to: {})
  • default: (Object) (defaults to: nil)
  • as: (Object) (defaults to: nil)
  • expose: (Boolean) (defaults to: true)
  • readonly: (Boolean) (defaults to: false)
  • type: (Object) (defaults to: nil)
  • getter: (Proc) (defaults to: nil)
  • setter: (Proc) (defaults to: nil)
  • twin: (Object) (defaults to: nil)
  • on: (Symbol) (defaults to: nil)
  • _opts (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_orderArray[Symbol]

: () -> Array

Returns:

  • (Array[Symbol])


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

def property_order
  @property_order ||= []
end

#resolve_default_value(default, type) ⇒ Object

Parameters:

  • default (Object)
  • type (Object)

Returns:

  • (Object)


395
396
397
398
399
400
401
# File 'lib/minitwin/class_methods/dsl.rb', line 395

def resolve_default_value(default, type)
  unless default.nil?
    return default.respond_to?(:call) ? default.call : default
  end

  type ? type_default_value(type) : nil
end

#unexposed_propertiesArray[Symbol]

: () -> Array

Returns:

  • (Array[Symbol])


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

def unexposed_properties
  @unexposed_properties ||= []
end