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

without block parameter

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

Yield Parameters:

Yield Returns:

  • (void)

Since:

  • 0.1.0



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

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

  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, value = nil) ⇒ Symbol (private)

Define an attribute (like name) or set a setting (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 #setting instead.

Reserved names are:

  • all names ending in ?, !

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

  • rand

Parameters:

  • name (Symbol)

    attribute or setting name

  • value (Any) (defaults to: nil)

    value for setting

Yield Returns:

  • (Any)

    attribute value

Returns:

  • (Symbol)

    attribute or setting name

Raises:

  • (DSLError)

    if a reserved name is used

Since:

  • 0.1.0



283
284
285
286
287
288
289
290
291
# File 'lib/object_forge/forge_dsl.rb', line 283

def method_missing(name, value = nil, **nil, &)
  return super(name) if frozen?
  if valid_setting_method?(name)
    return setting(name[...-1].to_sym, value) # steep:ignore NoMethod
  end
  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

#settingsHash{Symbol => Any} (readonly)

Returns settings for forge, such as mold.

Returns:

  • (Hash{Symbol => Any})

    settings for forge, such as mold

Since:

  • 0.1.0



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

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



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

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 settings, attributes, sequences and traits. Prevents further responses through #method_missing.

Returns:

  • (self)

Since:

  • 0.1.0



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

def freeze
  ::Kernel.instance_method(:freeze).bind_call(self)
  @attributes.freeze
  @sequences.freeze
  @traits.freeze
  @settings.freeze
  self
end

#inspectString

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

Returns:

  • (String)

Since:

  • 0.1.0



255
256
257
258
259
260
# File 'lib/object_forge/forge_dsl.rb', line 255

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



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

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

#setting(name, value) ⇒ Symbol

Set a value for a forge’s setting.

Possible settings depend on used forge, but for default ObjectForge::Forge a mold is expected.

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

Examples:

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

Parameters:

  • name (Sumbol)

    setting name

  • value (Any)

    value for the setting

Returns:

  • (Symbol)

    setting name

Raises:

  • (ArgumentError)

    if name is not a Symbol

See Also:

Since:

  • 0.1.0



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

def setting(name, value)
  unless ::Symbol === name
    raise ::ArgumentError, "setting name must be a Symbol, #{name.class} given"
  end

  @settings[name] = value

  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



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

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