Code Monkey home page Code Monkey logo

u-attributes's Introduction

Create "immutable" objects. No setters, just getters!

Create "immutable" objects with no setters, just getters.


Ruby Gem Build Status Maintainability Test Coverage

This gem allows you to define "immutable" objects, when using it your objects will only have getters and no setters. So, if you change [1] [2] an attribute of the object, you’ll have a new object instance. That is, you transform the object instead of modifying it.

Documentation

Version Documentation
unreleased https://github.com/serradura/u-case/blob/main/README.md
2.8.0 https://github.com/serradura/u-case/blob/v2.x/README.md
1.2.0 https://github.com/serradura/u-case/blob/v1.x/README.md

Table of contents

Installation

Add this line to your application's Gemfile and bundle install:

gem 'u-attributes'

Compatibility

u-attributes branch ruby activemodel
unreleased main >= 2.2.0 >= 3.2, < 7
2.8.0 v2.x >= 2.2.0 >= 3.2, < 7
1.2.0 v1.x >= 2.2.0 >= 3.2, < 6.1

Note: The activemodel is an optional dependency, this module can be enabled to validate the attributes.

⬆️ Back to Top

Usage

How to define attributes?

By default, you must define the class constructor.

class Person
  include Micro::Attributes

  attribute :age
  attribute :name

  def initialize(name: 'John Doe', age:)
    @name, @age = name, age
  end
end

person = Person.new(age: 21)

person.age  # 21
person.name # John Doe

# By design the attributes are always exposed as reader methods (getters).
# If you try to call a setter you will see a NoMethodError.
#
# person.name = 'Rodrigo'
# NoMethodError (undefined method `name=' for #<Person:0x0000... @name='John Doe', @age=21>)

⬆️ Back to Top

Micro::Attributes#attributes=

This is a protected method to make easier the assignment in a constructor. e.g.

class Person
  include Micro::Attributes

  attribute :age
  attribute :name, default: 'John Doe'

  def initialize(options)
    self.attributes = options
  end
end

person = Person.new(age: 20)

person.age  # 20
person.name # John Doe

How to extract attributes from an object or hash?

You can extract attributes using the extract_attributes_from method, it will try to fetch attributes from the object using either the object[attribute_key] accessor or the reader method object.attribute_key.

class Person
  include Micro::Attributes

  attribute :age
  attribute :name, default: 'John Doe'

  def initialize(user:)
    self.attributes = extract_attributes_from(user)
  end
end

# extracting from an object

class User
  attr_accessor :age, :name
end

user = User.new
user.age = 20

person = Person.new(user: user)

person.age  # 20
person.name # John Doe

# extracting from a hash

another_person = Person.new(user: { age: 55, name: 'Julia Not Roberts' })

another_person.age  # 55
another_person.name # Julia Not Roberts

Is it possible to define an attribute as required?

You only need to use the required: true option.

But to this work, you need to assign the attributes using the #attributes= method or the extensions: initialize, activemodel_validations.

class Person
  include Micro::Attributes

  attribute :age
  attribute :name, required: true

  def initialize(attributes)
    self.attributes = attributes
  end
end

Person.new(age: 32) # ArgumentError (missing keyword: :name)

⬆️ Back to Top

Micro::Attributes#attribute

Use this method with a valid attribute name to get its value.

person = Person.new(age: 20)

person.attribute('age') # 20
person.attribute(:name) # John Doe
person.attribute('foo') # nil

If you pass a block, it will be executed only if the attribute was valid.

person.attribute(:name) { |value| puts value } # John Doe
person.attribute('age') { |value| puts value } # 20
person.attribute('foo') { |value| puts value } # !! Nothing happened, because of the attribute doesn't exist.

⬆️ Back to Top

Micro::Attributes#attribute!

Works like the #attribute method, but it will raise an exception when the attribute doesn't exist.

person.attribute!('foo')                   # NameError (undefined attribute `foo)

person.attribute!('foo') { |value| value } # NameError (undefined attribute `foo)

⬆️ Back to Top

How to define multiple attributes?

Use .attributes with a list of attribute names.

class Person
  include Micro::Attributes

  attributes :age, :name

  def initialize(options)
    self.attributes = options
  end
end

person = Person.new(age: 32)

person.name # nil
person.age  # 32

Note: This method can't define default values. To do this, use the #attribute() method.

⬆️ Back to Top

Micro::Attributes.with(:initialize)

Use Micro::Attributes.with(:initialize) to define a constructor to assign the attributes. e.g.

class Person
  include Micro::Attributes.with(:initialize)

  attribute :age, required: true
  attribute :name, default: 'John Doe'
end

person = Person.new(age: 18)

person.age  # 18
person.name # John Doe

This extension enables two methods for your objects. The #with_attribute() and #with_attributes().

#with_attribute()

another_person = person.with_attribute(:age, 21)

another_person.age            # 21
another_person.name           # John Doe
another_person.equal?(person) # false

#with_attributes()

Use it to assign multiple attributes

other_person = person.with_attributes(name: 'Serradura', age: 32)

other_person.age            # 32
other_person.name           # Serradura
other_person.equal?(person) # false

If you pass a value different of a Hash, a Kind::Error will be raised.

Person.new(1) # Kind::Error (1 expected to be a kind of Hash)

⬆️ Back to Top

Defining default values to the attributes

To do this, you only need make use of the default: keyword. e.g.

class Person
  include Micro::Attributes.with(:initialize)

  attribute :age
  attribute :name, default: 'John Doe'
end

There are two different strategies to define default values.

  1. Pass a regular object, like in the previous example.
  2. Pass a proc/lambda, and if it has an argument you will receive the attribute value to do something before assign it.
class Person
  include Micro::Attributes.with(:initialize)

  attribute :age, default: -> age { age&.to_i }
  attribute :name, default: -> name { String(name || 'John Doe').strip }
end

⬆️ Back to Top

The strict initializer

Use .with(initialize: :strict) to forbids an instantiation without all the attribute keywords.

In other words, it is equivalent to you define all the attributes using the required: true option.

class StrictPerson
  include Micro::Attributes.with(initialize: :strict)

  attribute :age
  attribute :name, default: 'John Doe'
end

StrictPerson.new({}) # ArgumentError (missing keyword: :age)

An attribute with a default value can be omitted.

person_without_age = StrictPerson.new(age: nil)

person_without_age.age  # nil
person_without_age.name # 'John Doe'

Note: Except for this validation the .with(initialize: :strict) method will works in the same ways of .with(:initialize).

⬆️ Back to Top

Is it possible to inherit the attributes?

Yes. e.g.

class Person
  include Micro::Attributes.with(:initialize)

  attribute :age
  attribute :name, default: 'John Doe'
end

class Subclass < Person # Will preserve the parent class attributes
  attribute :foo
end

instance = Subclass.new({})

instance.name              # John Doe
instance.respond_to?(:age) # true
instance.respond_to?(:foo) # true

⬆️ Back to Top

.attribute!()

This method allows us to redefine the attributes default data that was defined in the parent class. e.g.

class AnotherSubclass < Person
  attribute! :name, default: 'Alfa'
end

alfa_person = AnotherSubclass.new({})

alfa_person.name # 'Alfa'
alfa_person.age  # nil

class SubSubclass < Subclass
  attribute! :age, default: 0
  attribute! :name, default: 'Beta'
end

beta_person = SubSubclass.new({})

beta_person.name # 'Beta'
beta_person.age  # 0

⬆️ Back to Top

How to query the attributes?

All of the methods that will be explained can be used with any of the built-in extensions.

PS: We will use the class below for all of the next examples.

class Person
  include Micro::Attributes

  attribute :age
  attribute :first_name, default: 'John'
  attribute :last_name, default: 'Doe'

  def initialize(options)
    self.attributes = options
  end

  def name
    "#{first_name} #{last_name}"
  end
end

.attributes

Listing all the class attributes.

Person.attributes # ["age", "first_name", "last_name"]

.attribute?()

Checking the existence of some attribute.

Person.attribute?(:first_name)  # true
Person.attribute?('first_name') # true

Person.attribute?('foo') # false
Person.attribute?(:foo)  # false

#attribute?()

Checking the existence of some attribute in an instance.

person = Person.new(age: 20)

person.attribute?(:name)  # true
person.attribute?('name') # true

person.attribute?('foo') # false
person.attribute?(:foo)  # false

#attributes()

Fetching all the attributes with their values.

person1 = Person.new(age: 20)
person1.attributes # {"age"=>20, "first_name"=>"John", "last_name"=>"Doe"}

person2 = Person.new(first_name: 'Rodrigo', last_name: 'Rodrigues')
person2.attributes # {"age"=>nil, "first_name"=>"Rodrigo", "last_name"=>"Rodrigues"}

#attributes(keys_as:)

Use the keys_as: option with Symbol/:symbol or String/:string to transform the attributes hash keys.

person1 = Person.new(age: 20)
person2 = Person.new(first_name: 'Rodrigo', last_name: 'Rodrigues')

person1.attributes(keys_as: Symbol) # {:age=>20, :first_name=>"John", :last_name=>"Doe"}
person2.attributes(keys_as: String) # {"age"=>nil, "first_name"=>"Rodrigo", "last_name"=>"Rodrigues"}

person1.attributes(keys_as: :symbol) # {:age=>20, :first_name=>"John", :last_name=>"Doe"}
person2.attributes(keys_as: :string) # {"age"=>nil, "first_name"=>"Rodrigo", "last_name"=>"Rodrigues"}

#attributes(*names)

Slices the attributes to include only the given keys (in their types).

person = Person.new(age: 20)

person.attributes(:age)               # {:age => 20}
person.attributes(:age, :first_name)  # {:age => 20, :first_name => "John"}
person.attributes('age', 'last_name') # {"age" => 20, "last_name" => "Doe"}

person.attributes(:age, 'last_name') # {:age => 20, "last_name" => "Doe"}

# You could also use the keys_as: option to ensure the same type for all of the hash keys.

person.attributes(:age, 'last_name', keys_as: Symbol) # {:age=>20, :last_name=>"Doe"}

#attributes([names])

As the previous example, this methods accepts a list of keys to slice the attributes.

person = Person.new(age: 20)

person.attributes([:age])               # {:age => 20}
person.attributes([:age, :first_name])  # {:age => 20, :first_name => "John"}
person.attributes(['age', 'last_name']) # {"age" => 20, "last_name" => "Doe"}

person.attributes([:age, 'last_name']) # {:age => 20, "last_name" => "Doe"}

# You could also use the keys_as: option to ensure the same type for all of the hash keys.

person.attributes([:age, 'last_name'], keys_as: Symbol) # {:age=>20, :last_name=>"Doe"}

#attributes(with:, without:)

Use the with: option to include any method value of the instance inside of the hash, and, you can use the without: option to exclude one or more attribute keys from the final hash.

person = Person.new(age: 20)

person.attributes(without: :age)               # {"first_name"=>"John", "last_name"=>"Doe"}
person.attributes(without: [:age, :last_name]) # {"first_name"=>"John"}

person.attributes(with: [:name], without: [:first_name, :last_name]) # {"age"=>20, "name"=>"John Doe"}

# To achieves the same output of the previous example, use the attribute names to slice only them.

person.attributes(:age, with: [:name]) # {:age=>20, "name"=>"John Doe"}

# You could also use the keys_as: option to ensure the same type for all of the hash keys.

person.attributes(:age, with: [:name], keys_as: Symbol) # {:age=>20, :name=>"John Doe"}

#defined_attributes

Listing all the available attributes.

person = Person.new(age: 20)

person.defined_attributes # ["age", "first_name", "last_name"]

⬆️ Back to Top

Built-in extensions

You can use the method Micro::Attributes.with() to combine and require only the features that better fit your needs.

But, if you desire except one or more features, use the Micro::Attributes.without() method.

Picking specific features

Micro::Attributes.with

Micro::Attributes.with(:initialize)

Micro::Attributes.with(:initialize, :keys_as_symbol)

Micro::Attributes.with(:keys_as_symbol, initialize: :strict)

Micro::Attributes.with(:diff, :initialize)

Micro::Attributes.with(:diff, initialize: :strict)

Micro::Attributes.with(:diff, :keys_as_symbol, initialize: :strict)

Micro::Attributes.with(:activemodel_validations)

Micro::Attributes.with(:activemodel_validations, :diff)

Micro::Attributes.with(:activemodel_validations, :diff, initialize: :strict)

Micro::Attributes.with(:activemodel_validations, :diff, :keys_as_symbol, initialize: :strict)

The method Micro::Attributes.with() will raise an exception if no arguments/features were declared.

class Job
  include Micro::Attributes.with() # ArgumentError (Invalid feature name! Available options: :accept, :activemodel_validations, :diff, :initialize, :keys_as_symbol)
end

Micro::Attributes.without

Picking except one or more features

Micro::Attributes.without(:diff) # will load :activemodel_validations, :keys_as_symbol and initialize: :strict

Micro::Attributes.without(initialize: :strict) # will load :activemodel_validations, :diff and :keys_as_symbol

Picking all the features

Micro::Attributes.with_all_features

# This method returns the same of:

Micro::Attributes.with(:activemodel_validations, :diff, :keys_as_symbol, initialize: :strict)

⬆️ Back to Top

Extensions

ActiveModel::Validation extension

If your application uses ActiveModel as a dependency (like a regular Rails app). You will be enabled to use the activemodel_validations extension.

class Job
  include Micro::Attributes.with(:activemodel_validations)

  attribute :id
  attribute :state, default: 'sleeping'

  validates! :id, :state, presence: true
end

Job.new({}) # ActiveModel::StrictValidationFailed (Id can't be blank)

job = Job.new(id: 1)

job.id    # 1
job.state # 'sleeping'

.attribute() options

You can use the validate or validates options to define your attributes. e.g.

class Job
  include Micro::Attributes.with(:activemodel_validations)

  attribute :id, validates: { presence: true }
  attribute :state, validate: :must_be_a_filled_string

  def must_be_a_filled_string
    return if state.is_a?(String) && state.present?

    errors.add(:state, 'must be a filled string')
  end
end

⬆️ Back to Top

Diff extension

Provides a way to track changes in your object attributes.

require 'securerandom'

class Job
  include Micro::Attributes.with(:initialize, :diff)

  attribute :id
  attribute :state, default: 'sleeping'
end

job = Job.new(id: SecureRandom.uuid())

job.id    # A random UUID generated from SecureRandom.uuid(). e.g: 'e68bcc74-b91c-45c2-a904-12f1298cc60e'
job.state # 'sleeping'

job_running = job.with_attribute(:state, 'running')

job_running.state # 'running'

job_changes = job.diff_attributes(job_running)

#-----------------------------#
# #present?, #blank?, #empty? #
#-----------------------------#

job_changes.present? # true
job_changes.blank?   # false
job_changes.empty?   # false

#-----------#
# #changed? #
#-----------#
job_changes.changed? # true

job_changes.changed?(:id)    # false

job_changes.changed?(:state) # true
job_changes.changed?(:state, from: 'sleeping', to: 'running') # true

#----------------#
# #differences() #
#----------------#
job_changes.differences # {'state'=> {'from' => 'sleeping', 'to' => 'running'}}

⬆️ Back to Top

Initialize extension

  1. Creates a constructor to assign the attributes.
  2. Add methods to build new instances when some data was assigned.
class Job
  include Micro::Attributes.with(:initialize)

  attributes :id, :state
end

job_null = Job.new({})

job.id    # nil
job.state # nil

job = Job.new(id: 1, state: 'sleeping')

job.id    # 1
job.state # 'sleeping'

##############################################
# Assigning new values to get a new instance #
##############################################

#-------------------#
# #with_attribute() #
#-------------------#

new_job = job.with_attribute(:state, 'running')

new_job.id          # 1
new_job.state       # running
new_job.equal?(job) # false

#--------------------#
# #with_attributes() #
#--------------------#
#
# Use it to assign multiple attributes

other_job = job.with_attributes(id: 2, state: 'killed')

other_job.id          # 2
other_job.state       # killed
other_job.equal?(job) # false

⬆️ Back to Top

Strict mode

  1. Creates a constructor to assign the attributes.
  2. Adds methods to build new instances when some data was assigned.
  3. Forbids missing keywords.
class Job
  include Micro::Attributes.with(initialize: :strict)

  attributes :id, :state
end
#-----------------------------------------------------------------------#
# The strict initialize mode will require all the keys when initialize. #
#-----------------------------------------------------------------------#

Job.new({})

# The code above will raise:
# ArgumentError (missing keywords: :id, :state)

#---------------------------#
# Samples passing some data #
#---------------------------#

job_null = Job.new(id: nil, state: nil)

job.id    # nil
job.state # nil

job = Job.new(id: 1, state: 'sleeping')

job.id    # 1
job.state # 'sleeping'

Note: This extension works like the initialize extension. So, look at its section to understand all of the other features.

⬆️ Back to Top

Keys as symbol extension

Disables the indifferent access requiring the declaration/usage of the attributes as symbols.

The advantage of this extension over the default behavior is because it avoids an unnecessary allocation in memory of strings. All the keys are transformed into strings in the indifferent access mode, but, with this extension, this typecasting will be avoided. So, it has a better performance and reduces the usage of memory/Garbage collector, but gives for you the responsibility to always use symbols to set/access the attributes.

class Job
  include Micro::Attributes.with(:initialize, :keys_as_symbol)

  attribute :id
  attribute :state, default: 'sleeping'
end

job = Job.new(id: 1)

job.attributes # {:id => 1, :state => "sleeping"}

job.attribute?(:id) # true
job.attribute?('id') # false

job.attribute(:id) # 1
job.attribute('id') # nil

job.attribute!(:id) # 1
job.attribute!('id') # NameError (undefined attribute `id)

As you could see in the previous example only symbols will work to do something with the attributes.

This extension also changes the diff extension making everything (arguments, outputs) working only with symbols.

⬆️ Back to Top

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/serradura/u-attributes. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

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

Code of Conduct

Everyone interacting in the Micro::Attributes project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

u-attributes's People

Contributors

dependabot[bot] avatar gogainda avatar mrbongiolo avatar olleolleolle avatar serradura avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

u-attributes's Issues

#extract_attributes_from should prefer reader methods

Currently #extract_attributes_from will favor data retrieved with #[] instead of using the readers, this can be cumbersome, especially if working with ActiveRecord models, since the #[attribute] would return the "raw" data, and sometimes the .attribute can enrich the same data.

To fix this, the extraction should favor data retrieved with reader (.attribute).

Add private:/protected: attribute options

class User::SignUpParams
  include Micro::Attributes.with(:initialize, :accept)

  TrimString = -> value { String(value).strip }

  attribute :email                , default: TrimString
  attribute :password             , default: TrimString, refute: :empty?, private: true
  attribute :password_confirmation, default: TrimString, refute: :empty?, private: true

  def valid_password?
    accepted_attributes? && password == password_confirmation
  end

  def password_digest
    Digest::SHA256.hexdigest(password) if valid_password?
  end
end

User::SignUpParams.attributes               # ["email", "password", "password_confirmation"]
User::SignUpParams.attributes_by_visibility # { public: "email", private: ["password", "password_confirmation"] }

user = User::SignUpParams.new(email: '[email protected]', password: 'password', password_confirmation: 'password')

user.defined_attributes                 # ["email", "password", "password_confirmation"]
user.defined_attributes(:by_visibility) # { public: "email", private: ["password", "password_confirmation"] }

user.attributes # { "email"=> "[email protected]" }

user.attribute?('email')          # true
user.attribute?('password')       # false
user.attribute?('password', true) # true

user.attribute('email')   # "[email protected]"
user.attribute('password')   # nil
user.attribute!('password')  # NameError, tried to access a private attribute `password'

user.password              # NoMethodError: private method `password' called for #<User:0x007fb74c3a01c8>
user.password_confirmation # NoMethodError: private method `password_confirmation' called for #<User:0x007fb74c3a01c8>

Declaring multiple attributes:

class User::SignUpParams
  include Micro::Attributes

  attribute :email
  attributes :password, :password_confirmation, private: true
end
  • Allow defining private attributes
  • Allow defining protected attributes
  • Ensure these options using indifferent_access and keys_as_symbol
  • Ensure that the visibility definition of the attributes are kept with inheritance

attribute :name, validates: {}

  • It will be available when the active model validation feature is enabled.
  • attribute :foo, validate: {}
  • attribute :foo, validates: :must_have_something
  • Update README examples.

Add freeze: option

Allow freeze an attribute value.

attribute :name, freeze: true

attribute :name, freeze: :after_dup

attribute :name, freeze: :after_clone

Add new API to include the Micro::Attributes features

  • Implement the new API

V1:

class User
  include Micro::Attributes
    .with(:initialize, :activemodel_validations, :diff)
end

########################################
# Requiring the strict initialize mode #
########################################

class User
  include Micro::Attributes
    .with(:strict_initialize, :activemodel_validations, :diff)
end

V2:

class User
  include Micro::Attributes
    .with(:initialize, :activemodel_validations, :diff)
end

########################################
# Requiring the strict initialize mode #
########################################

class User
  include Micro::Attributes
    .with(:activemodel_validations, :diff, initialize: :strict)
end
  • Remove the variants to fetch the features:
    include Micro::Attributes.to_initialize! diff: true
    include Micro::Attributes.feature(:diff)

  • Replace include Micro::Attributes.features by include Micro::Attributes.with_all_features

  • Update README

Micro::Attributes.with(mode:)

Instead of doing:

Micro::Attributes.with(initialize: :strict, acceptance: :strict)

We could use the mode: option to enable a specific behavior in different modules.

Micro::Attributes.with(:initialize, :acceptance, mode: [:strict])

Micro::Entity

Micro::Entity a class to define readonly attributes with an initializer.

class UserData < Micro::Entity
  # The strict options will affect the initializer and types
  attributes with: [:acceptance], strict: true do
    field :name, accept: String
  end
end

attribute :name, required: true

class User
  include Micro::Attributes

  attribute :password, required: true
  attribute :password_confirmation, required: true
end
  • Allow defining required attributes, if some required key was missing it will raise the same error of the strict initializer.

Add Micro::Attributes.with(:accept)

Motivation:

To have a validation mechanism that does not depend on external dependencies (like ActiveModel::Validation).

Ways to declare attributes validations with their rejection messages

EmptyStr = -> value { !value.is_a?(String) || value.blank? }
FilledStr = -> value { value.is_a?(String) && value.present? }

                                    # Default rejection messages:
                                    #
attribute :name, accept: :present?  # { name: "expected to be present?"}

attribute :name, reject: :empty?    # { name: "expected to not be empty?"}

attribute :name, accept: String     # { name: "expected to be a kind of String" }

attribute :name, accept: FilledStr  # { name: "is invalid"}

attribute :name, reject: EmptyStr   # { name: "is invalid"}

Default behavior

class User
  include Micro::Attributes.with(:accept)

  attribute :age, accept: Integer, allow_nil: true
  attribute :name, accept: -> value { value.is_a?(String) && !value.empty? }, default: 'John doe'
  attribute :email

  def initialize(hash)
    self.attributes = hash
  end
end

user = User.new({})

user.attributes_errors? # true

user.attributes_errors # { 'name' => 'is invalid' }

user.accepted_attributes? # false

user.rejected_attributes? # true

Using the strict mode

class User
  include Micro::Attributes.with(accept: :strict)

  attribute :age, accept: Integer
  attribute :name, accept:  -> value { value.is_a?(String) && !value.empty? }, default: 'John doe'

  def initialize(hash)
    self.attributes = hash
  end
end

user = User.new(name: nil)
# ArgumentError, 'One or more attributed were rejected. Errors.
# * age expected to be a kind of Integer
# * name is invalid 

Defining multiple attributes

class User
  include Micro::Attributes.with(:accept)

  attributes :email, :password, accept: String

  def initialize(hash)
    self.attributes = hash
  end
end

List of all features after this addition

include Micro::Attributes
  .with(:accept, :activemodel_validations, :diff, :initialize)

# --

include Micro::Attributes
  .with(:accept, :activemodel_validations, :diff, :initialize, :keys_as_symbol)

# --

include Micro::Attributes
  .with(:activemodel_validations, :diff, initialize: :strict, accept: :strict)

# --

include Micro::Attributes
  .with(:activemodel_validations, :diff, :keys_as_symbol, initialize: :strict, accept: :strict)

Checklist

  • Add accept option
    • If the accept receives a class/module it will verify value.kind_of?(expected)
    • If the accept receives a predicate symbol (:integer?) it will be called with the given value.
    • If the accept receives a callable, it will perform it passing the value as its argument. If the callable responds to the method rejection_message, it will be used to compose the values of the attributes_errors method.
    • If the accept receives the Proc class and the default is a proc/lambda so it won't be performed in the default value resolution.
  • Add reject option
    • If the reject receives a class/module it will verify !value.kind_of?(expected)
    • If the reject receives a predicate symbol (:empty?) it will be called with the given value.
    • If the reject receives a callable, it will perform it passing the value as its argument and negate the output. If the callable responds to the method rejection_message, it will be used to compose the values of the attributes_errors method.
  • Add allow_nil option
    • Will avoid validation if the given value was nil.
  • Add rejection_message option
    • Accepts a String or a proc/lambda (I18n could be performed inside of one). A proc will receive the attribute name as its first argument.
  • The validation will be invoked only after the default value resolution.
  • Add strict mode (will raise an exception if the attributes receive the wrong data type)
  • The ActiveModel validation must be executed after the accept validation and only if all the attributes were accepted.
  • Update README

Thanks, @lunks for reviewing this issue.

Add :setter feature

class Use
  include Micro::Attributes.with(:setter)
end
  • Allow modifying the attributes.
  • Use this new feature to create a way to enable easy integration with shoulda-matchers.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.