dry-initializer Join the chat at https://gitter.im/dry-rb/chat

Gem Version Build Status Dependency Status Code Climate Test Coverage Inline docs

DSL for building class initializer with params and options.

Installation

Add this line to your application's Gemfile:

gem 'dry-initializer'

And then execute:

$ bundle

Or install it yourself as:

$ gem install dry-initializer

Synopsis

require 'dry-initializer'

class User
  extend Dry::Initializer::Mixin

  # Params of the initializer along with corresponding readers
  param  :name, type: String
  param  :role, default: proc { 'customer' }
  # Options of the initializer along with corresponding readers
  option :admin, default: proc { false }
end

# Defines the initializer with params and options
user = User.new 'Vladimir', 'admin', admin: true

# Defines readers for single attributes
user.name  # => 'Vladimir'
user.role  # => 'admin'
user.admin # => true

This is pretty the same as:

class User
  attr_reader :name, :type, :admin

  def initializer(name, type = 'customer', admin: false)
    @name  = name
    @type  = type
    @admin = admin

    fail TypeError unless String === @name
  end
end

Container Version

Instead of extending a class with the Dry::Initializer::Mixin, you can include a container with the initializer:

require 'dry-initializer'

class User
  # notice `-> do .. end` syntax
  include Dry::Initializer.define -> do
    param  :name,  type: String
    param  :role,  default: proc { 'customer' }
    option :admin, default: proc { false }
  end
end

Now you do not pollute a class with new variables, but isolate them in a special "container" module with the initializer and attribute readers. This method should be preferred when you don't need subclassing.

If you still need the DSL (param and option) to be inherited, use the direct extension:

require 'dry-initializer'

class BaseService
  extend Dry::Initializer::Mixin
  alias_method :dependency, :param
end

class ShowUser < BaseService
  dependency :user

  def call
    puts user&.name
  end
end

Params and Options

Use param to define plain argument:

class User
  extend Dry::Initializer::Mixin

  param :name
  param :email
end

user = User.new 'Andrew', '[email protected]'
user.name  # => 'Andrew'
user.email # => '[email protected]'

Use option to define named (hash) argument:

class User
  extend Dry::Initializer::Mixin

  option :name
  option :email
end

user = User.new email: '[email protected]', name: 'Andrew'
user.name  # => 'Andrew'
user.email # => '[email protected]'

All names should be unique:

class User
  extend Dry::Initializer::Mixin

  param  :name
  option :name # => raises #<SyntaxError ...>
end

Default Values

By default both params and options are mandatory. Use :default key to make them optional:

class User
  extend Dry::Initializer::Mixin

  param  :name,  default: proc { 'Unknown user' }
  option :email, default: proc { '[email protected]' }
end

user = User.new
user.name  # => 'Unknown user'
user.email # => '[email protected]'

user = User.new 'Vladimir', email: '[email protected]'
user.name  # => 'Vladimir'
user.email # => '[email protected]'

Set nil as a default value explicitly:

class User
  extend Dry::Initializer::Mixin

  param  :name
  option :email, default: proc { nil }
end

user = User.new 'Andrew'
user.email # => nil

user = User.new
# => #<ArgumentError ...>

You must wrap default values into procs.

If you need to assign proc as a default value, wrap it to another one:

class User
  extend Dry::Initializer::Mixin

  param :name_proc, default: proc { proc { 'Unknown user' } }
end

user = User.new
user.name_proc.call # => 'Unknown user'

Proc will be executed in a scope of new instance. You can refer to other arguments:

class User
  extend Dry::Initializer::Mixin

  param :name
  param :email, default: proc { "#{name.downcase}@example.com" }
end

user = User.new 'Andrew'
user.email # => '[email protected]'

Warning: when using lambdas instead of procs, don't forget an argument, required by instance_eval (you can skip in in a proc).

class User
  extend Dry::Initializer::Mixin

  param :name, default: -> (obj) { 'Dude' }
end

Order of Declarations

You cannot define required parameter after optional ones. The following example raises SyntaxError exception:

class User
  extend Dry::Initializer::Mixin

  param :name, default: proc { 'Unknown name' }
  param :email # => #<SyntaxError ...>
end

Type Constraints

To set type constraint use :type key:

class User
  extend Dry::Initializer::Mixin

  param :name, type: String
end

user = User.new 'Andrew'
user.name # => 'Andrew'

user = User.new :andrew
# => #<TypeError ...>

You can use plain Ruby classes and modules as type constraint (see above), or use dry-types:

class User
  extend Dry::Initializer::Mixin

  param :name, type: Dry::Types::Coercion::String
end

Or you can define custom constraint as a proc:

class User
  extend Dry::Initializer::Mixin

  param :name, type: proc { |v| raise TypeError if String === v }
end

user = User.new name: 'Andrew'
# => #<TypeError ...>

Reader

By default attr_reader is defined for every param and option.

To skip the reader, use reader: false:

class User
  extend Dry::Initializer::Mixin

  param :name
  param :email, reader: false
end

user = User.new 'Luke', '[email protected]'
user.name  # => 'Luke'

user.email                         # => #<NoMethodError ...>
user.instance_variable_get :@email # => '[email protected]'

No writers are defined. Define them using pure ruby attr_writer when necessary.

Subclassing

Subclassing preserves all definitions being made inside a superclass:

class User
  extend Dry::Initializer::Mixin

  param :name
end

class Employee < User
  param :position
end

employee = Employee.new('John', 'supercargo')
employee.name     # => 'John'
employee.position # => 'supercargo'

Benchmarks

The dry-initializer is a fastest DSL for rubies 2.2+ except for cases when core Struct is sufficient.

Compatibility

Tested under rubies compatible to MRI 2.2+.

Contributing

  • Fork the project
  • Create your feature branch (git checkout -b my-new-feature)
  • Add tests for it
  • Commit your changes (git commit -am '[UPDATE] Add some feature')
  • Push to the branch (git push origin my-new-feature)
  • Create a new Pull Request

License

The gem is available as open source under the terms of the MIT License.