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 {|dsl| ... } ⇒ 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.mold = ObjectForge::Molds::KeywordsMold.new
  f.attribute(:name) { "Name" }
  f[:description] { name.upcase }
  f.duration { rand(1000) }
end

without block parameter

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

Yield Parameters:

Yield Returns:

  • (void)

Since:

  • 0.1.0



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

def initialize(&dsl)
  super
  @attributes = {}
  @sequences = {}
  @traits = {}
  @options = {}
  @transient_attributes = []

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

  shape_attribute_list!
  freeze
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, value = nil) ⇒ Symbol (private)

Define an attribute (like name) or set a option (like name=) using a shorthand.

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

Reserved names are:

  • all names ending in ?, !

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

  • rand and yard

Parameters:

  • name (Symbol)

    attribute or option name

  • value (Any) (defaults to: nil)

    value for option

Yield Returns:

  • (Any)

    attribute value

Returns:

  • (Symbol)

    attribute or option name

Raises:

  • (DSLError)

    if a reserved name is used

Since:

  • 0.1.0



310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/object_forge/forge_dsl.rb', line 310

def method_missing(name, value = nil, **nil, &)
  return super(name) if frozen?

  if valid_option_method?(name)
    # Intentionally passing block to `option` to trigger DSLError if it is present.
    return option(name[...-1].to_sym, value, &) # steep:ignore NoMethod, UnexpectedBlockGiven
  end
  # Block can be missing, but `attribute` will raise if it is.
  return attribute(name, &) if respond_to_missing?(name, false) # steep:ignore BlockTypeMismatch

  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



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

def attributes
  @attributes
end

#optionsHash{Symbol => Any} (readonly)

Returns options for forge, such as mold.

Returns:

  • (Hash{Symbol => Any})

    options for forge, such as mold

Since:

  • 0.1.0



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

def options
  @options
end

#sequencesHash{Symbol => Sequence} (readonly)

Returns used sequences.

Returns:

  • (Hash{Symbol => Sequence})

    used sequences

Since:

  • 0.1.0



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

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



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

def traits
  @traits
end

Instance Method Details

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

Define an attribute, possibly transient.

DSL does not know or care what attributes the target 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[:[]=]}!" }

using transient attributes

f.attribute(:range) { (base - delta)..(base + delta) }
f.attribute(:base, transient: true) { 1 }
f.transient(:delta) { 0.05 }

Parameters:

  • name (Symbol)

    attribute name

  • transient (Boolean) (defaults to: false)

    whether the attribute is transient (automatically excluded from attribute list)

Yield Returns:

  • (Any)

    attribute value

Returns:

  • (Symbol)

    attribute name

Raises:

  • (TypeError)

    if name is not a Symbol

  • (DSLError)

    if no block is given

Since:

  • 0.1.0



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/object_forge/forge_dsl.rb', line 152

def attribute(name, transient: false, &definition)
  unless ::Symbol === name
    raise ::TypeError,
          "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
  @transient_attributes << name if transient

  name
end

#freezeself

Note:

Called automatically in #initialize.

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

Returns:

  • (self)

Since:

  • 0.1.0



77
78
79
80
81
82
83
84
85
# File 'lib/object_forge/forge_dsl.rb', line 77

def freeze
  ::Kernel.instance_method(:freeze).bind_call(self)
  @attributes.freeze
  @sequences.freeze
  @traits.freeze
  @options.freeze
  @options[:attribute_list].freeze
  self
end

#inspectString

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

Returns:

  • (String)

Since:

  • 0.1.0



282
283
284
285
286
287
# File 'lib/object_forge/forge_dsl.rb', line 282

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

#option(name, value) ⇒ Symbol

Set a value for a forge’s option.

Possible options depend on used forge, but for default ObjectForge::Forge a :mold is expected. Check its documentation for full list of available options.

It is also possible to set options through method_missing, using name with a = suffix.

Examples:

f.option(:mold, ->(forge_target:, attributes:, **) { forge.new(**attributes) })
f.mold = ObjectForge::Molds::SingleArgumentMold.new

Parameters:

  • name (Symbol)

    option name

  • value (Any)

    value for the option

Returns:

  • (Symbol)

    option name

Raises:

  • (TypeError)

    if name is not a Symbol

See Also:

Since:

  • 0.1.0



106
107
108
109
110
111
112
113
114
115
# File 'lib/object_forge/forge_dsl.rb', line 106

def option(name, value)
  unless ::Symbol === name
    raise ::TypeError, "option name must be a Symbol, #{name.class} given (in #{name.inspect})"
  end
  raise DSLError, "option definition does not take a block (in #{name.inspect})" if block_given?

  @options[name] = value

  name
end

#sequence(name, initial = 1, transient: false) {|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

  • transient (Boolean) (defaults to: false)

    whether the attribute is transient (automatically excluded from attribute list)

Yield Parameters:

  • value (#succ)

    current value of the sequence to calculate attribute value

Yield Returns:

  • (Any)

    attribute value

Returns:

  • (Symbol)

    attribute name

Raises:

Since:

  • 0.1.0



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/object_forge/forge_dsl.rb', line 211

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

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

  if block_given?
    attribute(name, transient: transient) do
      instance_exec(seq.next, &) # steep:ignore BlockTypeMismatch
    end
  else
    attribute(name, transient: transient) { 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:

  • (TypeError)

    if name is not a Symbol

  • (DSLError)

    if no block is given

  • (DSLError)

    if called inside of another trait definition

Since:

  • 0.1.0



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/object_forge/forge_dsl.rb', line 261

def trait(name, **nil)
  unless ::Symbol === name
    raise ::TypeError, "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

#transient(name) ⇒ Object

Define a transient attribute.

See Also:

Since:

  • 0.1.0



176
177
178
# File 'lib/object_forge/forge_dsl.rb', line 176

def transient(name, &)
  attribute(name, transient: true, &)
end