Class: Domainic::Attributer::DSL::AttributeBuilder

Inherits:
Object
  • Object
show all
Defined in:
lib/domainic/attributer/dsl/attribute_builder.rb,
lib/domainic/attributer/dsl/attribute_builder/option_parser.rb

Overview

This class provides a rich DSL for configuring attributes with support for default values, coercion, validation, visibility controls, and change tracking. It uses method chaining to allow natural, declarative attribute definitions

Author:

Since:

  • 0.1.0

Instance Method Summary collapse

Constructor Details

#initialize(base, attribute_name, attribute_type, type_validator = Undefined, **options, &block) ⇒ AttributeBuilder

Initialize a new AttributeBuilder

Parameters:

  • base (Class, Module)

    the class or module to build the attribute in

  • attribute_name (String, Symbol)

    the name of the attribute

  • attribute_type (String, Symbol)

    the type of attribute

  • type_validator (Proc, Object, nil) (defaults to: Undefined)

    optional type validator

  • options (Hash{Symbol => Object})

    additional options for attribute configuration. See Domainic::Attributer::DSL::AttributeBuilder::OptionParser#initialize for details

Since:

  • 0.1.0



37
38
39
40
41
42
43
# File 'lib/domainic/attributer/dsl/attribute_builder.rb', line 37

def initialize(base, attribute_name, attribute_type, type_validator = Undefined, **options, &block)
  @base = base
  # @type var options: OptionParser::options
  @options = OptionParser.parse!(attribute_name, attribute_type, options)
  @options[:validators] << type_validator if type_validator != Undefined
  instance_exec(&block) if block
end

Instance Method Details

#coerce_with(proc_symbol = Undefined) {|value| ... } ⇒ self Also known as: coerce

Note:

When coercion is used with nilable attributes, handlers should account for nil values appropriately.

Provides a way to automatically transform attribute values into the desired format or type. Coercion ensures input values conform to the expected structure by applying one or more handlers. Handlers can be Procs, lambdas, or method symbols.

Coercions are applied during initialization or whenever the attribute value is updated.

Examples:

Simple coercion

class Superhero
  include Domainic::Attributer

  argument :code_name do
    coerce_with ->(val) { val.to_s.upcase }
  end
end

hero = Superhero.new("spiderman")
hero.code_name # => "SPIDERMAN"

Multiple coercions

class Superhero
  include Domainic::Attributer

  option :power_level do
    coerce_with ->(val) { val.to_s }         # Convert to string
    coerce_with do |val|                     # Remove non-digits
      val.gsub(/\D/, '')
    end
    coerce_with ->(val) { val.to_i }         # Convert to integer
  end
end

hero = Superhero.new(power_level: "over 9000!")
hero.power_level # => 9000

Coercion with an instance method

class Superhero
  include Domainic::Attributer

  option :alias_name do
    coerce_with :format_alias
  end

  private

  def format_alias(value)
    value.to_s.downcase.split.map(&:capitalize).join(' ')
  end
end

hero = Superhero.new(alias_name: "ironMAN")
hero.alias_name # => "Ironman"

Parameters:

  • proc_symbol (Proc, Symbol, nil) (defaults to: Undefined)

    optional coercion handler

Yields:

  • optional coercion block

Yield Parameters:

  • value (Object)

    the value to coerce

Yield Returns:

  • (Object)

    the coerced value

Returns:

  • (self)

    the builder for method chaining

Since:

  • 0.1.0



119
120
121
122
123
# File 'lib/domainic/attributer/dsl/attribute_builder.rb', line 119

def coerce_with(proc_symbol = Undefined, &block)
  handler = proc_symbol == Undefined ? block : proc_symbol #: Attribute::Coercer::handler
  @options[:coercers] << handler
  self
end

#default(value_or_proc = Undefined) { ... } ⇒ self Also known as: default_generator, default_value

Provides a way to assign default values to attributes. These values can be static or dynamically generated using a block. The default value is only applied when no explicit value is provided for the attribute

Examples:

Static default values

class RPGCharacter
  include Domainic::Attributer

  option :level, Integer do
    default 1
  end

  option :health_max do
    default 100
  end
end

hero = RPGCharacter.new
hero.level           # => 1
hero.health_max      # => 100

Dynamic default values

class RPGCharacter
  include Domainic::Attributer

  option :created_at do
    default { Time.now }
  end

  option :health_current do
    default { health_max }
  end
end

hero = RPGCharacter.new
hero.created_at      # => Current timestamp
hero.health_current  # => Defaults to the value of `health_max`

Complex dynamic default values

class RPGCharacter
  include Domainic::Attributer

  option :inventory do
    default do
      base_items = ['Health Potion', 'Map']
      base_items << 'Lucky Coin' if Random.rand < 0.1
      base_items
    end
  end
end

hero = RPGCharacter.new
hero.inventory       # => ["Health Potion", "Map"] or ["Health Potion", "Map", "Lucky Coin"]

Parameters:

  • value_or_proc (Object, Proc, nil) (defaults to: Undefined)

    optional default value or generator

Yields:

  • optional default value generator block

Yield Returns:

  • (Object)

    the default value

Returns:

  • (self)

    the builder for method chaining

Since:

  • 0.1.0



185
186
187
188
# File 'lib/domainic/attributer/dsl/attribute_builder.rb', line 185

def default(value_or_proc = Undefined, &block)
  @options[:default] = value_or_proc == Undefined ? block : value_or_proc
  self
end

#description(text) ⇒ self Also known as: desc

Note:

Descriptions are optional but highly recommended for improving readability and maintainability of the code.

Provides a way to add descriptive metadata to attributes. Descriptions improve code clarity by documenting the purpose or behavior of an attribute. These descriptions can be short or detailed, depending on the context.

Examples:

Adding a short description

class MagicItem
  include Domainic::Attributer

  argument :name do
    desc 'The name of the magic item, must be unique'
  end
end

Adding a detailed description

class MagicItem
  include Domainic::Attributer

  option :power_level do
    description 'The magical power level of the item, ranging from 0 to 100.
                 Higher power levels increase effectiveness but may come with
                 increased risks during use.'
    validate_with ->(val) { val.between?(0, 100) }
  end
end

Parameters:

  • text (String)

    the description text

Returns:

  • (self)

    the builder for method chaining

Since:

  • 0.1.0



224
225
226
227
# File 'lib/domainic/attributer/dsl/attribute_builder.rb', line 224

def description(text)
  @options[:description] = text
  self
end

#non_nilableself Also known as: non_nil, non_null, non_nullable, not_nil, not_nilable, not_null, not_nullable

Ensures that an attribute defined in the block DSL cannot have a nil value. This validation is enforced during initialization and when modifying the attribute value at runtime. Use non_nilable for attributes that must always have a value.

Examples:

Preventing nil values for an attribute

class Ninja
  include Domainic::Attributer

  argument :code_name do
    non_nilable
  end
end

Ninja.new(nil)  # Raises ArgumentError: nil value is not allowed

Combining non_nilable with other features

class Ninja
  include Domainic::Attributer

  argument :rank do
    desc 'The rank of the ninja, such as Genin or Chunin'
    non_nilable
  end
end

ninja = Ninja.new('Genin') # => Works
ninja.rank = nil # Raises ArgumentError: nil value is not allowed

Returns:

  • (self)

    the builder for method chaining

Since:

  • 0.1.0



260
261
262
263
# File 'lib/domainic/attributer/dsl/attribute_builder.rb', line 260

def non_nilable
  @options[:nilable] = false
  self
end

#on_change(proc = Undefined) {|old_value, new_value| ... } ⇒ self

Allows defining a callback to be triggered whenever the attribute's value changes. The callback receives the old value and the new value as arguments, enabling custom logic to be executed on changes. Use on_change to react to changes in attribute values, such as updating dependent attributes or triggering side effects.

Examples:

Reacting to changes in an attribute

class VideoGame
  include Domainic::Attributer

  option :health do
    default 100
    on_change ->(old_value, new_value) {
      puts "Health changed from #{old_value} to #{new_value}"
    }
  end
end

game = VideoGame.new
game.health = 50  # Outputs: Health changed from 100 to 50

Performing complex logic on change

class VideoGame
  include Domainic::Attributer

  option :power_ups do
    default []
    on_change do |old_value, new_value|
      new_items = new_value - old_value
      lost_items = old_value - new_value

      new_items.each { |item| activate_power_up(item) }
      lost_items.each { |item| deactivate_power_up(item) }
    end
  end

  private

  def activate_power_up(item)
    puts "Activated power-up: #{item}"
  end

  def deactivate_power_up(item)
    puts "Deactivated power-up: #{item}"
  end
end

game = VideoGame.new
game.power_ups = ['Shield', 'Speed Boost']
# Outputs: Activated power-up: Shield
#          Activated power-up: Speed Boost
game.power_ups = ['Shield']
# Outputs: Deactivated power-up: Speed Boost

Parameters:

  • proc (Proc, nil) (defaults to: Undefined)

    optional callback handler

Yields:

  • optional callback block

Yield Parameters:

  • old_value (Object)

    the previous value of the attribute

  • new_value (Object)

    the new value of the attribute

Yield Returns:

  • (void)

Returns:

  • (self)

    the builder for method chaining

Since:

  • 0.1.0



332
333
334
335
336
# File 'lib/domainic/attributer/dsl/attribute_builder.rb', line 332

def on_change(proc = Undefined, &block)
  handler = proc == Undefined ? block : proc #: Attribute::Callback::handler
  @options[:callbacks] << handler
  self
end

#privateself

Sets both the read and write visibility of an attribute to private. This ensures the attribute can only be accessed or modified within the class itself.

Examples:

Making an attribute private

class SecretAgent
  include Domainic::Attributer

  option :real_name do
    desc 'The real name of the agent, hidden from external access.'
    private
  end
end

agent = SecretAgent.new(real_name: 'James Bond')
agent.real_name  # Raises NoMethodError: private method `real_name' called for #<SecretAgent>
agent.real_name = 'John Doe'  # Raises NoMethodError: private method `real_name=' called for #<SecretAgent>

Returns:

  • (self)

    the builder for method chaining

Since:

  • 0.1.0



357
358
359
360
# File 'lib/domainic/attributer/dsl/attribute_builder.rb', line 357

def private
  private_read
  private_write
end

#private_readself

Sets the read visibility of an attribute to private, allowing the attribute to be read only within the class itself. The write visibility remains unchanged unless explicitly modified. Use private_read when the value of an attribute should be hidden from external consumers but writable by external code if needed.

Examples:

Making the reader private

class SecretAgent
  include Domainic::Attributer

  option :mission_code do
    desc 'The secret mission code, readable only within the class.'
    private_read
    default { generate_code }
  end

  private

  def generate_code
    "M-#{rand(1000..9999)}"
  end
end

agent = SecretAgent.new
agent.mission_code  # Raises NoMethodError: private method `mission_code' called for #<SecretAgent>
agent.mission_code = 'Override Code'  # Works, as write visibility is still public

Returns:

  • (self)

    the builder for method chaining

Since:

  • 0.1.0



389
390
391
392
# File 'lib/domainic/attributer/dsl/attribute_builder.rb', line 389

def private_read
  @options[:read] = :private
  self
end

#private_writeself

Sets the write visibility of an attribute to private, allowing the attribute to be modified only within the class. The read visibility remains unchanged unless explicitly modified. Use private_write to ensure that an attribute's value can only be updated internally, while still allowing external code to read its value if needed.

Examples:

Making the writer private

class SecretAgent
  include Domainic::Attributer

  option :mission_code do
    desc 'The secret mission code, writable only within the class.'
    private_write
    default { generate_code }
  end

  private

  def generate_code
    "M-#{rand(1000..9999)}"
  end
end

agent = SecretAgent.new
agent.mission_code          # => "M-1234"
agent.mission_code = '007'  # Raises NoMethodError: private method `mission_code=' called for #<SecretAgent>

Returns:

  • (self)

    the builder for method chaining

Since:

  • 0.1.0



422
423
424
425
# File 'lib/domainic/attributer/dsl/attribute_builder.rb', line 422

def private_write
  @options[:write] = :private
  self
end

#protectedself

Sets both the read and write visibility of an attribute to protected, allowing access only within the class and its subclasses. This visibility restricts external access entirely. Use protected to share attributes within a class hierarchy while keeping them hidden from external consumers.

Examples:

Defining a protected attribute

class SecretAgent
  include Domainic::Attributer

  option :mission_code do
    protected
    description 'The mission code, accessible only within the class and its subclasses.'
  end
end

class DoubleAgent < SecretAgent
  def reveal_code
    self.mission_code
  end
end

agent = SecretAgent.new(mission_code: '007')
agent.mission_code          # Raises NoMethodError
DoubleAgent.new.reveal_code # => '007'

Returns:

  • (self)

    the builder for method chaining

Since:

  • 0.1.0



453
454
455
456
# File 'lib/domainic/attributer/dsl/attribute_builder.rb', line 453

def protected
  protected_read
  protected_write
end

#protected_readself

Sets both the read and write visibility of an attribute to protected. This allows the attribute to be accessed or modified only within the class and its subclasses. Use protected for attributes that should be accessible to the class and its subclasses but hidden from external consumers.

Examples:

Making an attribute protected

class SecretAgent
  include Domainic::Attributer

  option :mission_code do
    desc 'The mission code, accessible only within the class or subclasses.'
    protected
  end
end

class DoubleAgent < SecretAgent
  def reveal_code
    self.mission_code
  end
end

agent = SecretAgent.new(mission_code: '007')
agent.mission_code          # Raises NoMethodError
DoubleAgent.new.reveal_code # => '007'

Returns:

  • (self)

    the builder for method chaining

Since:

  • 0.1.0



484
485
486
487
# File 'lib/domainic/attributer/dsl/attribute_builder.rb', line 484

def protected_read
  @options[:read] = :protected
  self
end

#protected_writeself

Sets both the read and write visibility of an attribute to protected. This allows the attribute to be accessed or modified only within the class and its subclasses. Use protected for attributes that should be accessible to the class and its subclasses but hidden from external consumers.

Examples:

Making an attribute protected

class SecretAgent
  include Domainic::Attributer

  option :mission_code do
    desc 'The mission code, accessible only within the class or subclasses.'
    protected
  end
end

class DoubleAgent < SecretAgent
  def reveal_code
    self.mission_code
  end
end

agent = SecretAgent.new(mission_code: '007')
agent.mission_code          # Raises NoMethodError
DoubleAgent.new.reveal_code # => '007'

Returns:

  • (self)

    the builder for method chaining

Since:

  • 0.1.0



515
516
517
518
# File 'lib/domainic/attributer/dsl/attribute_builder.rb', line 515

def protected_write
  @options[:write] = :protected
  self
end

#publicself

Note:

Attributes are public by default. Use public explicitly to override inherited or modified visibility.

Explicitly sets both the read and write visibility of an attribute to public, overriding any inherited or previously set visibility. By default, attributes are public, so this is typically used to revert an attribute's visibility if it was changed in a parent class or module.

Examples:

Reverting visibility to public in a subclass

class SecretAgent
  include Domainic::Attributer

  option :mission_code do
    desc 'The mission code, protected in the base class.'
    private
  end
end

class FieldAgent < SecretAgent
  option :mission_code do
    desc 'The mission code, made public in the subclass.'
    public
  end
end

agent = FieldAgent.new(mission_code: '007')
agent.mission_code          # => '007' (now accessible)
agent.mission_code = '008'  # Works, as visibility is public in the subclass

Returns:

  • (self)

    the builder for method chaining

Since:

  • 0.1.0



549
550
551
552
# File 'lib/domainic/attributer/dsl/attribute_builder.rb', line 549

def public
  public_read
  public_write
end

#public_readself

Note:

Attributes are publicly readable by default. Use public_read explicitly to override inherited or modified visibility.

Explicitly sets the read visibility of an attribute to public, overriding any inherited or previously set visibility. By default, attributes are readable publicly, so this is typically used to revert the read visibility of an attribute if it was modified in a parent class or module.

Examples:

Reverting read visibility to public in a subclass

class SecretAgent
  include Domainic::Attributer

  option :mission_code do
    desc 'The mission code, privately readable in the base class.'
    private_read
  end
end

class FieldAgent < SecretAgent
  option :mission_code do
    desc 'The mission code, made publicly readable in the subclass.'
    public_read
  end
end

agent = FieldAgent.new(mission_code: '007')
agent.mission_code          # => '007' (now publicly readable)
agent.mission_code = '008'  # Raises NoMethodError, as write visibility is still private

Returns:

  • (self)

    the builder for method chaining

Since:

  • 0.1.0



584
585
586
587
# File 'lib/domainic/attributer/dsl/attribute_builder.rb', line 584

def public_read
  @options[:read] = :public
  self
end

#public_writeself

Note:

Attributes are publicly writable by default. Use public_write explicitly to override inherited or modified visibility.

Explicitly sets the write visibility of an attribute to public, overriding any inherited or previously set visibility. By default, attributes are writable publicly, so this is typically used to revert the write visibility of an attribute if it was modified in a parent class or module.

Examples:

Reverting write visibility to public in a subclass

class SecretAgent
  include Domainic::Attributer

  option :mission_code do
    desc 'The mission code, writable only within the class or subclasses in the base class.'
    private_write
  end
end

class FieldAgent < SecretAgent
  option :mission_code do
    desc 'The mission code, now writable publicly in the subclass.'
    public_write
  end
end

agent = FieldAgent.new(mission_code: '007')
agent.mission_code          # Raises NoMethodError, as read visibility remains restricted
agent.mission_code = '008'  # Works, as write visibility is now public

Returns:

  • (self)

    the builder for method chaining

Since:

  • 0.1.0



619
620
621
622
# File 'lib/domainic/attributer/dsl/attribute_builder.rb', line 619

def public_write
  @options[:write] = :public
  self
end

#requiredself

Note:

required options are enforced during initialization; as long as the option is provided (even if it is nil) no error will be raised.

Marks an option attribute as required, ensuring that a value must be provided during initialization. If a required attribute is not supplied, an error is raised. Use required to enforce mandatory attributes.

Examples:

Defining a required attribute

class Superhero
  include Domainic::Attributer

  option :name do
    desc 'The name of the superhero, which must be provided.'
    required
  end
end

Superhero.new          # Raises ArgumentError: missing required attribute: name
Superhero.new(name: 'Spiderman') # Works, as the required attribute is supplied
Superhero.new(name: nil) # Works, as the required attribute is supplied (even if it is nil)

Returns:

  • (self)

    the builder for method chaining

Since:

  • 0.1.0



647
648
649
650
# File 'lib/domainic/attributer/dsl/attribute_builder.rb', line 647

def required
  @options[:required] = true
  self
end

#validate_with(object_or_proc = Undefined) {|value| ... } ⇒ self Also known as: validate, validates

Adds a custom validation to an attribute, allowing you to define specific criteria that the attribute's value must meet. Validators can be Procs, lambdas, or symbols referencing instance methods. Validation occurs during initialization and whenever the attribute value is updated. Use validate_with to enforce rules beyond type or presence, such as ranges, formats, or custom logic.

Examples:

Adding a simple validation

class Superhero
  include Domainic::Attributer

  argument :power_level do
    desc 'The power level of the superhero, which must be an integer between 0 and 100.'
    validate_with ->(val) { val.is_a?(Integer) && val.between?(0, 100) }
  end
end

Superhero.new(150)      # Raises ArgumentError: invalid value for power_level
Superhero.new(85)       # Works, as 85 is within the valid range

Using an instance method as a validator

class Superhero
  include Domainic::Attributer

  argument :alias_name do
    desc 'The alias name of the superhero, validated using an instance method.'
    validate_with :validate_alias_name
  end

  private

  def validate_alias_name(value)
    value.is_a?(String) && value.match?(/\A[A-Z][a-z]+\z/)
  end
end

Superhero.new('Spiderman')  # Works, as the alias name matches the validation criteria
Superhero.new('spiderman')  # Raises ArgumentError: invalid value for alias_name

Combining multiple validators

class Vehicle
  include Domainic::Attributer

  option :speed do
    desc 'The speed of the vehicle, which must be a non-negative number.'
    validate_with ->(val) { val.is_a?(Numeric) }
    validate_with do |val|
      val.zero? || val.positive?
    end
  end
end

Vehicle.new(speed: -10)  # Raises ArgumentError: invalid value for speed
Vehicle.new(speed: 50)   # Works, as 50 meets all validation criteria

Parameters:

  • object_or_proc (Object, Proc, nil) (defaults to: Undefined)

    optional validation handler

Yields:

  • optional validation block

Yield Parameters:

  • value (Object)

    the value to validate

Yield Returns:

  • (Boolean)

    true if the value is valid, false otherwise

Returns:

  • (self)

    the builder for method chaining

Since:

  • 0.1.0



712
713
714
715
716
# File 'lib/domainic/attributer/dsl/attribute_builder.rb', line 712

def validate_with(object_or_proc = Undefined, &block)
  handler = object_or_proc == Undefined ? block : object_or_proc #: Attribute::Validator::handler
  @options[:validators] << handler
  self
end