Class: ObjectForge::ForgeDSL

Inherits:
UnBasicObject show all
Defined in:
lib/object_forge/forge_dsl.rb

Overview

Note:

This class is not intended to be used directly, but it’s not a private API.

DSL for defining a forge.

Since:

  • 0.1.0

Thread safety:

  • DSL is not thread-safe. Take care not to introduce side effects, especially in attribute definitions. The instance itself is frozen after initialization, so it should be safe to share.

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from UnBasicObject

#class, #eql?, #frozen?, #hash, #is_a?, #pretty_print, #pretty_print_cycle, #respond_to?, #to_s

Constructor Details

#initialize {|f| ... } ⇒ ForgeDSL

Define forge’s parameters through DSL.

If the block has a parameter, an object will be yielded, and self context will be preserved. Otherwise, DSL will change self context inside the block, without ability to call methods available outside.

Examples:

with block parameter

ForgeDSL.new do |f|
  f.attribute(:name) { "Name" }
  f[:description] { name.upcase }
  f.duration { rand(1000) }
end

without block parameter

ForgeDSL.new do
  attribute(:name) { "Name" }
  self[:description] { name.upcase }
  duration { rand(1000) }
end

Yield Parameters:

Yield Returns:

  • (void)

Since:

  • 0.1.0



53
54
55
56
57
58
59
60
61
62
# File 'lib/object_forge/forge_dsl.rb', line 53

def initialize(&dsl)
  super
  @attributes = {}
  @sequences = {}
  @traits = {}

  dsl.arity.zero? ? instance_exec(&dsl) : yield(self)

  freeze
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name) ⇒ Symbol (private)

Define an attribute using a shorthand.

Can not be used to define attributes with reserved names. Trying to use a conflicting name will lead to usual issues with calling random methods. When in doubt, use #attribute or #[] instead.

Reserved names are:

  • all names ending in ?, ! or =

  • all names starting with a non-word ASCII character (operators, , [], []=)

  • rand

Parameters:

  • name (Symbol)

    attribute name

Yield Returns:

  • (Any)

    attribute value

Returns:

  • (Symbol)

    attribute name

Raises:

  • (DSLError)

    if a reserved name is used

Since:

  • 0.1.0



280
281
282
283
284
285
# File 'lib/object_forge/forge_dsl.rb', line 280

def method_missing(name, **nil, &)
  return super if frozen?
  return attribute(name, &) if respond_to_missing?(name, false)

  raise DSLError, "#{name.inspect} is a reserved name (in #{name.inspect})"
end

Instance Attribute Details

#attributesHash{Symbol => Proc} (readonly)

Returns attribute definitions.

Returns:

  • (Hash{Symbol => Proc})

    attribute definitions

Since:

  • 0.1.0



22
23
24
# File 'lib/object_forge/forge_dsl.rb', line 22

def attributes
  @attributes
end

#sequencesHash{Symbol => Sequence} (readonly)

Returns used sequences.

Returns:

  • (Hash{Symbol => Sequence})

    used sequences

Since:

  • 0.1.0



25
26
27
# File 'lib/object_forge/forge_dsl.rb', line 25

def sequences
  @sequences
end

#traitsHash{Symbol => Hash{Symbol => Proc}} (readonly)

Returns trait definitions.

Returns:

  • (Hash{Symbol => Hash{Symbol => Proc}})

    trait definitions

Since:

  • 0.1.0



28
29
30
# File 'lib/object_forge/forge_dsl.rb', line 28

def traits
  @traits
end

Instance Method Details

#attribute(name, &definition) ⇒ Symbol Also known as: []

Define an attribute, possibly transient.

DSL does not know or care what attributes the forged class has, so the only difference between “real” and “transient” attributes is how the class itself treats them.

It is also possible to define attributes using method_missing shortcut, except for conflicting or reserved names.

You can refer to any other attribute inside the attribute definition block. self[:name] can be used to refer to an attribute with a conflicting or reserved name.

Examples:

f.attribute(:name) { "Name" }
f[:description] { name.downcase }
f.duration { rand(1000) }

using conflicting and reserved names

f.attribute(:[]) { "Brackets" }
f.attribute(:[]=) { "#{self[:[]]} are brackets" }
f.attribute(:!) { "#{self[:[]=]}!" }

Parameters:

  • name (Symbol)

    attribute name

Yield Returns:

  • (Any)

    attribute value

Returns:

  • (Symbol)

    attribute name

Raises:

  • (ArgumentError)

    if name is not a Symbol

  • (DSLError)

    if no block is given

Since:

  • 0.1.0



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/object_forge/forge_dsl.rb', line 137

def attribute(name, &definition)
  unless ::Symbol === name
    raise ::ArgumentError,
          "attribute name must be a Symbol, #{name.class} given (in #{name.inspect})"
  end
  unless block_given?
    raise DSLError, "attribute definition requires a block (in #{name.inspect})"
  end

  if @current_trait
    @traits[@current_trait][name] = definition
  else
    @attributes[name] = definition
  end

  name
end

#freezeself

Note:

Called automatically in #initialize.

Freezes the instance, including attributes, sequences and traits. Prevents further responses through #method_missing.

Returns:

  • (self)

Since:

  • 0.1.0



70
71
72
73
74
75
76
77
# File 'lib/object_forge/forge_dsl.rb', line 70

def freeze
  ::Object.instance_method(:freeze).bind_call(self)
  @attributes.freeze
  @sequences.freeze
  @traits.freeze
  @mold.freeze
  self
end

#inspectString

Return a string containing a human-readable representation of the definition.

Returns:

  • (String)

Since:

  • 0.1.0



253
254
255
256
257
258
# File 'lib/object_forge/forge_dsl.rb', line 253

def inspect
  "#<#{self.class.name}:#{__id__} " \
    "attributes=#{@attributes.keys.inspect} " \
    "sequences=#{@sequences.keys.inspect} " \
    "traits={#{@traits.map { |k, v| "#{k.inspect}=#{v.keys.inspect}" }.join(", ")}}>"
end

#mold#call?

Returns forge mold.

Returns:

  • (#call, nil)

    forge mold

Since:

  • 0.2.0



81
82
83
84
# File 'lib/object_forge/forge_dsl.rb', line 81

def mold # rubocop:disable Style/TrivialAccessors
  # Not using attr_reader because YARD eats `#mold=` then.
  @mold
end

#mold=(mold) ⇒ #call?

Set the forge mold.

Mold is an object that knows how to take a hash of attributes and create an object from them. It can also be a class with #call, in which case a new mold will be instantiated automatically for each build through Molds::WrappedMold. If a single instance is enough, please call .new yourself once.

Parameters:

  • mold (Class, #call, nil)

Returns:

  • (#call, nil)

    the set mold

Raises:

  • (DSLError)

    if mold does not respond to or implement #call

Since:

  • 0.2.0



100
101
102
103
104
105
106
107
108
# File 'lib/object_forge/forge_dsl.rb', line 100

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

#sequence(name, initial = 1) {|value| ... } ⇒ Symbol

Define an attribute, using a sequence.

name is used for both attribute and sequence, for the whole forge. If the name was used for a sequence previously, the sequence will not be redefined on subsequent calls.

Examples:

f.sequence(:date, Date.today)
f.sequence(:id) { _1.to_s }
f.sequence(:dated_id, 10) { |n| "#{Date.today}/#{n}-#{id}" }

using external sequence

seq = Sequence.new(1)
f.sequence(:global_id, seq)

sequence reuse

f.sequence(:id, "a") # => "a", "b", ...
f.trait :new_id do
  f.sequence(:id) { |n| n * 2 } # => "aa", "bb", ...
end

Parameters:

  • name (Symbol)

    attribute name

  • initial (Sequence, #succ) (defaults to: 1)

    existing sequence, or initial value for a new sequence

Yield Parameters:

  • value (#succ)

    current value of the sequence to calculate attribute value

Yield Returns:

  • (Any)

    attribute value

Returns:

  • (Symbol)

    attribute name

Raises:

  • (ArgumentError)

    if name is not a Symbol

  • (DSLError)

    if initial does not respond to #succ and is not a Sequence

Since:

  • 0.1.0



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/object_forge/forge_dsl.rb', line 184

def sequence(name, initial = 1, **nil, &)
  unless ::Symbol === name
    raise ::ArgumentError,
          "sequence name must be a Symbol, #{name.class} given (in #{name.inspect})"
  end

  seq = @sequences[name] ||= Sequence.new(initial)

  if block_given?
    attribute(name) { instance_exec(seq.next, &) }
  else
    attribute(name) { seq.next }
  end

  name
end

#trait(name) {|f| ... } ⇒ Symbol

Note:

Traits can not be defined inside of traits.

Define a trait — a group of attributes with non-default values.

DSL yields itself to the block, in case you need to refer to it. This can be used to define traits using a block coming from outside of DSL.

Examples:

f.trait :special do
  f.name { "***xXxSPECIALxXx***" }
  f.sequence(:special_id) { "~~~ SpEcIaL #{_1} ~~~" }
end

externally defined trait

# Variable defined outside of DSL:
success_trait = ->(ft) do
  ft.status { :success }
  ft.error_code { 0 }
end
# Inside the DSL:
f.trait(:success, &success_trait)

Parameters:

  • name (Symbol)

    trait name

Yields:

  • block for trait definition

Yield Parameters:

Yield Returns:

  • (void)

Returns:

  • (Symbol)

    trait name

Raises:

  • (ArgumentError)

    if name is not a Symbol

  • (DSLError)

    if no block is given

  • (DSLError)

    if called inside of another trait definition

Since:

  • 0.1.0



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/object_forge/forge_dsl.rb', line 231

def trait(name, **nil)
  unless ::Symbol === name
    raise ::ArgumentError,
          "trait name must be a Symbol, #{name.class} given (in #{name.inspect})"
  end
  if @current_trait
    raise DSLError, "can not define trait inside of another trait (in #{name.inspect})"
  end
  raise DSLError, "trait definition requires a block (in #{name.inspect})" unless block_given?

  @current_trait = name
  @traits[name] = {}
  yield self
  @traits[name].freeze
  @current_trait = nil

  name
end