Module: ObjectForge::Molds

Defined in:
lib/object_forge/molds.rb,
lib/object_forge/molds/hash_mold.rb,
lib/object_forge/molds/array_mold.rb,
lib/object_forge/molds/struct_mold.rb,
lib/object_forge/molds/wrapped_mold.rb,
lib/object_forge/molds/keywords_mold.rb,
lib/object_forge/molds/single_argument_mold.rb

Overview

This module provides a collection of predefined molds to be used in common cases.

Mold is an object that knows how to take a hash of attributes and create an object from them. Molds are callable objects responsible for actually building objects produced by factories (or doing other, interesting things with them (truly, only the code review is the limit!)). They are supposed to be immutable, shareable, and persistent: initialize once, use for the whole runtime.

A simple mold can easily be just a Proc. All molds must have the following #call signature: call(forge_target:, attributes:, **). The extra keywords are ignored for possibility of future extensions.

Examples:

A very basic FactoryBot replacement

creator = ->(forge_target:, attributes:, **) do
  instance = forge_target.new
  attributes.each_pair { instance.public_send(:"#{_1}=", _2) }
  instance.save!
end

creator.call(forge_target: User, attributes: { name: "John", age: 30 })
  # => <User name="John" age=30>

Using a mold to serialize collection of objects (contrivedly)

dumpy = ->(forge_target:, attributes:, **) do
  Enumerator.new(attributes.size) do |y|
    attributes.each_pair { y << forge_target.dump(_1 => _2) }
  end
end

dumpy.call(forge_target: JSON, attributes: {a:1, b:2}).to_a
  # => ["{\"a\":1}", "{\"b\":2}"]
dumpy.call(forge_target: YAML, attributes: {a:1, b:2}).to_a
  # => ["---\n:a: 1\n", "---\n:b: 2\n"]

Abstract factory pattern (kind of)

class FurnitureFactory
  def call(forge_target:, attributes:, **)
    concrete_factory = concrete_factory(forge_target)
    attributes[:pieces].map do |piece|
      concrete_factory.public_send(piece, attributes.dig(:color, piece))
    end
  end

  private def concrete_factory(style)
    case style
    when :hitech
      HiTechFactory.new
    when :retro
      RetroFactory.new
    end
  end
end

FurnitureFactory.new.call(forge_target: :hitech, attributes: {
  pieces: [:chair, :table], color: { chair: :black, table: :white }
})
  # => [<#HiTech::Chair color=:black>, <#HiTech::Table color=:white>]

Abusing molds

printer = ->(forge_target:, attributes:, **) { PP.pp(attributes, forge_target) }
printer.call(forge_target: $stderr, attributes: {a:1, b:2})
  # outputs "{:a=>1, :b=>2}" to $stderr

Abuse above is just not enough, we need something even better

class Character
  attr_reader :hp

  def initialize(hp)
    @hp = hp
    @damage_factory = ObjectForge::Forge.new(self, DamageParameters.new)
  end

  def hit(damage)
    if damage <= 0
      @damage_factory.call(:shielded, apply: ->(damage) { @hp -= damage })
    else
      @damage_factory.call(amount: damage, apply: ->(damage) { @hp -= damage })
    end
  end

  def heal(amount)
    @damage_factory.call(amount: amount, apply: ->(amount) { @hp += amount }) if amount >= 0
  end

  def die!
    puts "Character died!"
  end
end

class DamageParameters
  def attributes = {}
  def traits = { shielded: { amount: 1 } }
  def options
    {
      crucible: lambda(&:itself),
      mold: ->(forge_target:, attributes:, **) {
        attributes[:apply].call(attributes[:amount])
        forge_target
      },
      after_build: ->(forge_target) { forge_target.die! if forge_target.hp <= 0 }
    }
  end
end

mc = Character.new(100)
mc.hit(50)
mc.heal(25)
mc.hit(-10)
mc.hit(75)
  # outputs "Character died!"

Since:

  • 0.2.0

Defined Under Namespace

Classes: ArrayMold, HashMold, KeywordsMold, SingleArgumentMold, StructMold, WrappedMold

Class Method Summary collapse

Class Method Details

.mold_for(forge_target) ⇒ #call

Get maybe appropriate mold for the given forge target.

Currently provides specific recognition for:

Other objects just get SingleArgumentMold.

Parameters:

  • forge_target (Class, Any)

Returns:

  • (#call)

    an instance of a mold

Since:

  • 0.3.0



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/object_forge/molds.rb', line 134

def self.mold_for(forge_target)
  return SingleArgumentMold.new unless ::Class === forge_target

  mold_class =
    if forge_target < ::Struct
      StructMold
    elsif defined?(::Data) && forge_target < ::Data
      KeywordsMold
    elsif forge_target <= ::Hash
      HashMold
    elsif forge_target <= ::Array
      ArrayMold
    else
      SingleArgumentMold
    end

  mold_class.new
end

.wrap_mold(mold) ⇒ #call?

Wrap mold if needed.

If mold is nil or a callable object, returns it. If it is a Class with #call, wraps it in WrappedMold. Otherwise, raises an error.

Parameters:

  • mold (Class, #call, nil)

Returns:

Raises:

Since:

  • 0.3.0



166
167
168
169
170
171
172
173
174
# File 'lib/object_forge/molds.rb', line 166

def self.wrap_mold(mold)
  if nil == mold || mold.respond_to?(:call) # rubocop:disable Style/YodaCondition
    mold # : ObjectForge::mold?
  elsif ::Class === mold && mold.public_method_defined?(:call)
    WrappedMold.new(mold)
  else
    raise ObjectInterfaceError, "mold must respond to or implement #call"
  end
end