Class: Kernel::Maplet

Inherits:
Object show all
Includes:
Enumerable
Defined in:
lib/patch/let.rb

Overview

Class that allows for dynamic definition of properties

Instance Method Summary collapse

Constructor Details

#initializeMaplet



126
127
128
# File 'lib/patch/let.rb', line 126

def initialize
  @props = []
end

Instance Method Details

#[](*prop) ⇒ Object

Accesses a prop’s value, allowing for nested access.

Examples:

Accessing top-level and nested properties

settings = let(
  host: "localhost",
  database: { adapter: "sqlite3", pool: 5 }
)

settings[:host]                # => "localhost"
settings[:database]            # => <EmanLib::Maplet ...>
settings[:database, :adapter]  # => "sqlite3"
settings[:database, :pool]     # => 5

Raises:

  • (ArgumentError)

    If no prop is given or does not exist.



260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/patch/let.rb', line 260

def [](*prop)
  raise ArgumentError, "No property specified." if prop.empty?
  value = instance_variable_get("@#{prop.first}")

  if prop.size == 1
    value
  else
    value[*prop[1..]]
  end
rescue NameError
  error = "Property '#{prop.join ?.}' is not defined in this Maplet."
  error += " Available properties: [#{@props.join(", ")}]"
  raise ArgumentError, error
end

#define!(*args, &block) ⇒ self

Dynamically defines properties based on the provided arguments and/or block.

Arguments can be Hashes or “hashy” Arrays (arrays of ‘[key, value]` pairs). If a block is given, its local variables are also used to define methods. Keys are converted to symbols and validated as method names.

If a value is a Hash or a “hashy” Array, it’s recursively used to define nested properties.

Examples:

Defining with a Hash

# let(...) === Maplet.new.define!(...)

person = let(name: "Alice", age: 30)
person.name # => "Alice"
person.age = 31
person.age # => 31

Defining with a “hashy” Array

config = let([[:host, "localhost"], [:port, 8080]])
config.host # => "localhost"

Defining with a block

user = let do
  username = "bob"
  active = true
  binding # Important: makes local variables available
end

user.username # => "bob"
user.active?  # This won't define active? automatically, but user.active

Nested definitions

settings = let(
  database: { adapter: "sqlite3", pool: 5 },
  logging: [[:level, "info"], [:file, "/var/log/app.log"]]
)
settings.database.adapter # => "sqlite3"
settings.logging.level    # => "info"

Combining arguments

complex = let({id: 1}, [[:type, "example"]]) do
  description = "A complex object"
  status = :new
  binding
end

complex.id          # => 1
complex.type        # => "example"
complex.description # => "A complex object"

Raises:

  • (ArgumentError)

    If an argument is not a Hash or a “hashy” Array.

  • (ArgumentError)

    If a key is not a valid method name.



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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/patch/let.rb', line 190

def define!(*args, &block)
  # Stores all key-value pairs to be defined
  variable = {}

  # Process Hashes and "hashy" Arrays first
  args.each do |arg|
    case arg
    when Hash
      variable.merge!(arg)
    when Array
      raise ArgumentError, "Array should be Hash like." unless arg.hashy?
      variable.merge!(arg.to_h)
    else
      raise ArgumentError, "Invalid argument type: #{arg.class}"
    end
  end

  # Process local variables from the block
  if block_given?
    binding = block.call # The block is expected to return its binding.
    raise ArgumentError, "Block must return a Binding object." unless binding.is_a?(Binding)

    variable.merge!(binding.variables)
  end

  # Define getters and setters and store values
  variable.each do |prop, value|
    prop = prop.to_s.to_sym
    raise ArgumentError, "Invalid name: #{prop}" unless prop.to_s.valid_name?

    # Recursively define for nested Hashes or "hashy" Arrays
    if value.is_a? Hash
      value = Maplet.new.define!(value)
    elsif value.is_a?(Array) && value.hashy?
      value = Maplet.new.define!(value.to_h)
    end

    # Store the original value in an instance variable
    instance_variable_set("@#{prop}", value)

    define_singleton_method(prop) do
      instance_variable_get("@#{prop}")
    end

    define_singleton_method("#{prop}=") do |value|
      instance_variable_set("@#{prop}", value)
    end
  end

  @props += variable.keys.map(&:to_sym)
  self
end

#each {|value, path| ... } ⇒ self, Enumerator

Iterates over each leaf property of the Maplet.

Examples:

user = let(name: 'Ian', meta: { role: 'Admin', active: true })
user.each { |value, path| puts "#{path}: #{value}" }
# Prints:
# name: Ian
# meta.role: Admin
# meta.active: true

Yields:

  • (value, path)

    Gives the value and its full access path.

Yield Parameters:

  • value (Object)

    The value of the leaf property.

  • path (String)

    The dotted path to the prop (e.g., ‘“dir.home”`).



310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/patch/let.rb', line 310

def each(&block)
  return enum_for(:each) unless block_given?

  tap do
    @props.each do |prop|
      value = self[prop]
      if value.is_a?(Maplet)
        value.each do |inner, nested|
          yield inner, "#{prop}.#{nested}"
        end
      else
        yield value, prop.to_s
      end
    end
  end
end

#map(*only) {|value, prop| ... } ⇒ Maplet

Creates a new Maplet by applying a block to each property’s value. You can transform all properties or just a select few.

Examples:

Transforming all numeric values

config = let(port: 80, timeout: 3000, host: "localhost")
doubled = config.map do |value, _|
  value.is_a?(Numeric) ? value * 2 : value
end
doubled.to_h # => { port: 160, timeout: 6000, host: "localhost" }

Transforming only a specific property

config = let(port: 80, host: "localhost")
upcased = config.map(:host) { |val, _| val.upcase }
upcased.to_h # => { port: 80, host: "LOCALHOST" }

Yields:

  • (value, prop)

    The block to apply to each selected property.

Yield Parameters:

  • value (Object)

    The value of the property.

  • prop (String)

    The name of the property.



349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/patch/let.rb', line 349

def map(*only, &block)
  return enum_for(:map) unless block_given?
  hash = {}

  @props.each do |prop|
    value = self[prop]
    if value.is_a?(Maplet)
      hash[prop] = value.map(*only, &block)
    elsif only.empty?
      hash[prop] = yield(value, prop)
    elsif only.any? { |p| p.to_sym == prop }
      hash[prop] = yield(value, prop)
    else
      hash[prop] = value
    end
  end

  Maplet.new.define!(hash)
end

#to_hHash{Symbol => Object}

Converts the Maplet and any nested Maplets back into a Hash. Recursively transforms inner maplets into nested Hashes.

Examples:

settings = let(host: 'localhost', db: { name: 'dev', pool: 5 })
settings.to_h # => { host: "localhost", db: { name: "dev", pool: 5 } }


283
284
285
286
287
288
289
290
291
292
293
# File 'lib/patch/let.rb', line 283

def to_h
  @props.each_with_object({}) do |prop, hash|
    value = self[prop]

    if value.is_a?(Maplet)
      hash[prop] = value.to_h
    else
      hash[prop] = value
    end
  end
end

#without(*props) ⇒ Maplet

Returns a new Maplet that excludes the specified top-level properties. Note: This only works on top-level properties.

Examples:

user = let(id: 1, name: 'Tico', email: '[email protected]')

user_public_data = user.without(:id)
user_public_data.to_h # => { name: "Tico", email: "[email protected]" }


380
381
382
383
384
385
386
387
# File 'lib/patch/let.rb', line 380

def without(*props)
  return self if props.empty?

  remaining = to_h
  props.each { |p| remaining.delete(p.to_sym) }

  Maplet.new.define!(remaining)
end