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

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



56
57
58
59
60
61
62
63
64
65
# File 'lib/object_forge/forge_dsl.rb', line 56

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



276
277
278
279
280
281
# File 'lib/object_forge/forge_dsl.rb', line 276

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

#mold#call?

Returns forge mold.

Returns:

  • (#call, nil)

    forge mold

Since:

  • 0.1.0



31
32
33
# File 'lib/object_forge/forge_dsl.rb', line 31

def mold
  @mold
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



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

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



73
74
75
76
77
78
79
80
# File 'lib/object_forge/forge_dsl.rb', line 73

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



249
250
251
252
253
254
# File 'lib/object_forge/forge_dsl.rb', line 249

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

#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



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/object_forge/forge_dsl.rb', line 180

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



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

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