README.md in u-attributes-1.2.0 vs README.md in u-attributes-2.0.0

- old
+ new

@@ -1,74 +1,69 @@ +![Ruby](https://img.shields.io/badge/ruby-2.2+-ruby.svg?colorA=99004d&colorB=cc0066) [![Gem](https://img.shields.io/gem/v/u-attributes.svg?style=flat-square)](https://rubygems.org/gems/u-attributes) [![Build Status](https://travis-ci.com/serradura/u-attributes.svg?branch=master)](https://travis-ci.com/serradura/u-attributes) [![Maintainability](https://api.codeclimate.com/v1/badges/b562e6b877a9edf4dbf6/maintainability)](https://codeclimate.com/github/serradura/u-attributes/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/b562e6b877a9edf4dbf6/test_coverage)](https://codeclimate.com/github/serradura/u-attributes/test_coverage) -μ-attributes (Micro::Attributes) +μ-attributes (Micro::Attributes) <!-- omit in toc --> ================================ This gem allows defining read-only attributes, that is, your objects will have only getters to access their attributes data. -## Table of contents -- [μ-attributes (Micro::Attributes)](#%ce%bc-attributes-microattributes) - - [Table of contents](#table-of-contents) - - [Required Ruby version](#required-ruby-version) - - [Installation](#installation) - - [Usage](#usage) - - [How to require?](#how-to-require) - - [How to define attributes?](#how-to-define-attributes) - - [How to define multiple attributes?](#how-to-define-multiple-attributes) - - [How to define attributes with a constructor to assign them?](#how-to-define-attributes-with-a-constructor-to-assign-them) - - [How to inherit the attributes?](#how-to-inherit-the-attributes) - - [How to query the attributes?](#how-to-query-the-attributes) - - [Built-in extensions](#built-in-extensions) - - [ActiveModel::Validations extension](#activemodelvalidations-extension) - - [Diff extension](#diff-extension) - - [Initialize extension](#initialize-extension) - - [Strict initialize extension](#strict-initialize-extension) - - [Development](#development) - - [Contributing](#contributing) - - [License](#license) - - [Code of Conduct](#code-of-conduct) +## Table of contents <!-- omit in toc --> +- [Required Ruby version](#required-ruby-version) +- [Installation](#installation) +- [Compatibility](#compatibility) +- [Usage](#usage) + - [How to define attributes?](#how-to-define-attributes) + - [`Micro::Attributes#attributes=`](#microattributesattributes) + - [`Micro::Attributes#attribute`](#microattributesattribute) + - [`Micro::Attributes#attribute!`](#microattributesattribute-1) + - [How to define multiple attributes?](#how-to-define-multiple-attributes) + - [`Micro::Attributes.with(:initialize)`](#microattributeswithinitialize) + - [`#with_attribute()`](#with_attribute) + - [`#with_attributes()`](#with_attributes) + - [Defining default values to the attributes](#defining-default-values-to-the-attributes) + - [Strict initializer](#strict-initializer) + - [Is it possible to inherit the attributes?](#is-it-possible-to-inherit-the-attributes) + - [.attribute!()](#attribute) + - [How to query the attributes?](#how-to-query-the-attributes) +- [Built-in extensions](#built-in-extensions) + - [ActiveModel::Validations extension](#activemodelvalidations-extension) + - [Attribute options](#attribute-options) + - [Diff extension](#diff-extension) + - [Initialize extension](#initialize-extension) + - [Strict initialize mode](#strict-initialize-mode) +- [Development](#development) +- [Contributing](#contributing) +- [License](#license) +- [Code of Conduct](#code-of-conduct) ## Required Ruby version > \>= 2.2.0 ## Installation -Add this line to your application's Gemfile: +Add this line to your application's Gemfile and `bundle install`: ```ruby gem 'u-attributes' ``` -And then execute: +## Compatibility - $ bundle +| u-attributes | branch | ruby | activemodel | +| -------------- | ------- | -------- | ------------- | +| 2.0.0 | master | >= 2.2.0 | >= 3.2, < 6.1 | +| 1.2.0 | v1.x | >= 2.2.0 | >= 3.2, < 6.1 | -Or install it yourself as: - - $ gem install u-attributes - ## Usage -### How to require? -```ruby -# Bundler will do it automatically, but if you desire to do a manual require. -# Use one of the following options: - -require 'micro/attributes' - -# or - -require 'u-attributes' -``` - ### How to define attributes? -```ruby +```ruby # By default you must to define the class constructor. class Person include Micro::Attributes @@ -80,284 +75,321 @@ end end person = Person.new(age: 21) -puts person.name # John Doe -puts person.age # 21 +person.name # John Doe +person.age # 21 -# By design, the attributes expose only reader methods (getters). -# If you try to call a setter, you will see a NoMethodError. +# 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>) +# NoMethodError (undefined method `name=' for #<Person:0x0000... @name='John Doe', @age=21>) +``` -#------------------# -# self.attributes= # -#------------------# +#### `Micro::Attributes#attributes=` -# This protected method is added to make easier the assignment in a constructor. +This is a protected method to make easier the assignment in a constructor. e.g. +```ruby class Person include Micro::Attributes - attribute :name, 'John Doe' # .attribute() accepts a second arg as its default value + attribute :name, default: 'John Doe' attribute :age def initialize(options) self.attributes = options end end person = Person.new(age: 20) -puts person.name # John Doe -puts person.age # 20 +person.name # John Doe +person.age # 20 +``` -#--------------# -# #attribute() # -#--------------# -# -# Use the #attribute() method with a valid attribute name to get its value +#### `Micro::Attributes#attribute` -puts person.attribute(:name) # John Doe -puts person.attribute('age') # 20 -puts person.attribute('foo') # nil +Use this method with a valid attribute name to get its value. -# -# If you pass a block, it will be executed only if the attribute is valid. +```ruby +person = Person.new(age: 20) +person.attribute(:name) # John Doe +person.attribute('age') # 20 +person.attribute('foo') # nil +``` + +If you pass a block, it will be executed only if the attribute was valid. + +```ruby 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 not exists. +person.attribute('foo') { |value| puts value } # !! Nothing happened, because of the attribute doesn't exist. +``` -#---------------# -# #attribute!() # -#---------------# -# -# Works like the #attribute() method, but will raise an exception when the attribute not exist. +#### `Micro::Attributes#attribute!` -puts person.attribute!('foo') # NameError (undefined attribute `foo) -person.attribute!('foo') { |value| puts value } # NameError (undefined attribute `foo) +Works like the `#attribute` method, but it will raise an exception when the attribute doesn't exist. + +```ruby +person.attribute!('foo') # NameError (undefined attribute `foo) + +person.attribute!('foo') { |value| value } # NameError (undefined attribute `foo) ``` ### How to define multiple attributes? -```ruby +Use `.attributes` with a list of attribute names. -# Use .attributes with a list of attribute names. - +```ruby class Person include Micro::Attributes - attributes :age, name: 'John Doe' # Use a hash to define attributes with default values + attributes :age, :name def initialize(options) self.attributes = options end end person = Person.new(age: 32) -puts person.name # 'John Doe' -puts person.age # 32 +person.name # nil +person.age # 32 ``` -### How to define attributes with a constructor to assign them? -A: Use `Micro::Attributes.to_initialize` +> **Note:** This method can't define default values. To do this, use the `#attribute()` method. +### `Micro::Attributes.with(:initialize)` + +Use `Micro::Attributes.with(:initialize)` to define a constructor to assign the attributes. e.g. + ```ruby class Person - include Micro::Attributes.to_initialize + include Micro::Attributes.with(:initialize) - attributes :age, name: 'John Doe' + attribute :age + attribute :name, default: 'John Doe' end person = Person.new(age: 18) -puts person.name # John Doe -puts person.age # 18 +person.name # John Doe +person.age # 18 +``` -############################################## -# Assigning new values to get a new instance # -############################################## +This extension enables two methods for your objects. +The `#with_attribute()` and `#with_attributes()`. -#-------------------# -# #with_attribute() # -#-------------------# +#### `#with_attribute()` +```ruby another_person = person.with_attribute(:age, 21) -puts another_person.name # John Doe -puts another_person.age # 21 -puts another_person.equal?(person) # false +another_person.name # John Doe +another_person.age # 21 +another_person.equal?(person) # false +``` -#--------------------# -# #with_attributes() # -#--------------------# -# -# Use it to assign multiple attributes +#### `#with_attributes()` +Use it to assign multiple attributes +```ruby other_person = person.with_attributes(name: 'Serradura', age: 32) -puts other_person.name # Serradura -puts other_person.age # 32 -puts other_person.equal?(person) # false +other_person.name # Serradura +other_person.age # 32 +other_person.equal?(person) # false +``` -# If you pass a value different of a Hash, an ArgumentError will be raised. -# -# Person.new(1) -# ArgumentError (argument must be a Hash) +If you pass a value different of a Hash, a Kind::Error will be raised. -#--------------------# -# Strict initializer # -#--------------------# +```ruby +Person.new(1) # Kind::Error (1 must be a Hash) +``` -# Use .to_initialize! to forbids an instantiation without all keywords. +### Defining default values to the attributes +To do this, you only need make use of the `default:` keyword. e.g. + +```ruby +class Person + include Micro::Attributes.with(:initialize) + + attribute :age + attribute :name, default: 'John Doe' +end +``` + +There are 3 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. +3. Pass a **callable**, that is, a `class`, `module` or `instance` which responds to the `call` method. The behavior will be like the previous item (`proc`/`lambda`). + +```ruby +class Person + include Micro::Attributes.with(:initialize) + + attribute :age, default: -> age { age&.to_i } + attribute :name, default: -> name { String(name || 'John Doe').strip } +end +``` + +### Strict initializer + +Use `.with(initialize: :strict)` to forbids an instantiation without all the attribute keywords. e.g. + +```ruby class StrictPerson - include Micro::Attributes.to_initialize! + include Micro::Attributes.with(initialize: :strict) - attributes :age, name: 'John Doe' + attribute :age + attribute :name, default: 'John Doe' end -StrictPerson.new({}) +StrictPerson.new({}) # ArgumentError (missing keyword: :age) +``` -# The code above will raise: -# ArgumentError (missing keyword: :age) +An attribute with a default value can be omitted. +``` ruby person_without_age = StrictPerson.new(age: nil) -p person_without_age.name # "John Doe" -p person_without_age.age # nil - -# Except for this validation when initializing, -# the `to_initialize!` method will works in the same ways of `to_initialize`. +person_without_age.name # 'John Doe' +person_without_age.age # nil ``` -### How to inherit the attributes? +> **Note:** Except for this validation the `.with(initialize: :strict)` method will works in the same ways of `.with(:initialize)`. +### Is it possible to inherit the attributes? + +Yes. e.g. + ```ruby class Person - include Micro::Attributes.to_initialize + include Micro::Attributes.with(:initialize) - attributes :age, name: 'John Doe' + attribute :age + attribute :name, default: 'John Doe' end class Subclass < Person # Will preserve the parent class attributes attribute :foo end instance = Subclass.new({}) -puts instance.name # John Doe -puts instance.respond_to?(:age) # true -puts instance.respond_to?(:foo) # true +instance.name # John Doe +instance.respond_to?(:age) # true +instance.respond_to?(:foo) # true +``` -#---------------------------------# -# .attribute!() or .attributes!() # -#---------------------------------# +#### .attribute!() -# The methods above allow redefining the attributes default data +This method allows us to redefine the attributes default data that was defined in the parent class. e.g. +```ruby class AnotherSubclass < Person - attribute! :name, 'Alfa' + attribute! :name, default: 'Alfa' end alfa_person = AnotherSubclass.new({}) -p alfa_person.name # "Alfa" -p alfa_person.age # nil +alfa_person.name # 'Alfa' +alfa_person.age # nil class SubSubclass < Subclass - attributes! name: 'Beta', age: 0 + attribute! :age, default: 0 + attribute! :name, default: 'Beta' end beta_person = SubSubclass.new({}) -p beta_person.name # "Beta" -p beta_person.age # 0 +beta_person.name # 'Beta' +beta_person.age # 0 ``` ### How to query the attributes? ```ruby class Person include Micro::Attributes - attributes :age, name: 'John Doe' + attribute :age + attribute :name, default: 'John Doe' def initialize(options) self.attributes = options end end #---------------# # .attributes() # #---------------# -p Person.attributes # ["name", "age"] +Person.attributes # ['name', 'age'] #---------------# # .attribute?() # #---------------# -puts Person.attribute?(:name) # true -puts Person.attribute?('name') # true -puts Person.attribute?('foo') # false -puts Person.attribute?(:foo) # false +Person.attribute?(:name) # true +Person.attribute?('name') # true +Person.attribute?('foo') # false +Person.attribute?(:foo) # false # --- person = Person.new(age: 20) #---------------# # #attribute?() # #---------------# -puts person.attribute?(:name) # true -puts person.attribute?('name') # true -puts person.attribute?('foo') # false -puts person.attribute?(:foo) # false +person.attribute?(:name) # true +person.attribute?('name') # true +person.attribute?('foo') # false +person.attribute?(:foo) # false #---------------# # #attributes() # #---------------# -p person.attributes # {"age"=>20, "name"=>"John Doe"} -p Person.new(name: 'John').attributes # {"age"=>nil, "name"=>"John"} +person.attributes # {'age'=>20, 'name'=>'John Doe'} +Person.new(name: 'John').attributes # {'age'=>nil, 'name'=>'John'} #---------------------# # #attributes(*names) # #---------------------# # Slices the attributes to include only the given keys. # Returns a hash containing the given keys (in their types). -p person.attributes(:age) # {age: 20} -p person.attributes(:age, :name) # {age: 20, name: "John Doe"} -p person.attributes('age', 'name') # {"age"=>20, "name"=>"John Doe"} +person.attributes(:age) # {age: 20} +person.attributes(:age, :name) # {age: 20, name: 'John Doe'} +person.attributes('age', 'name') # {'age'=>20, 'name'=>'John Doe'} ``` ## Built-in extensions -You can use the method `Micro::Attributes.features()` or `Micro::Attributes.with()` to combine and require only the features that better fit your needs. +You can use the method `Micro::Attributes.with()` to combine and require only the features that better fit your needs. -But, if you desire... -1. only one feature, use the `Micro::Attributes.feature()` method. -2. except one or more features, use the `Micro::Attributes.without()` method. +But, if you desire except one or more features, use the `Micro::Attributes.without()` method. ```ruby #===========================# # Loading specific features # #===========================# class Job - include Micro::Attributes.feature(:diff) + include Micro::Attributes.with(:diff) attribute :id - attribute :state, 'sleeping' + attribute :state, default: 'sleeping' def initialize(options) self.attributes = options end end @@ -366,65 +398,43 @@ # Loading all features # # --- # #======================# class Job - include Micro::Attributes.features + include Micro::Attributes.with_all_features - attributes :id, state: 'sleeping' + attribute :id + attribute :state, default: 'sleeping' end -# Note: -# If `Micro::Attributes.features()` be invoked without arguments, a module with all features will be returned. - #----------------------------------------------------------------------------# # Using the .with() method alias and adding the strict initialize extension. # #----------------------------------------------------------------------------# class Job - include Micro::Attributes.with(:strict_initialize, :diff) + include Micro::Attributes.with(:diff, initialize: :strict) - attributes :id, state: 'sleeping' + attribute :id + attribute :state, default: 'sleeping' end # Note: # 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: diff, initialize, activemodel_validations) # end -#===================================# -# Alternatives to the methods above # -#===================================# - -#---------------------------------------# -# Via Micro::Attributes.to_initialize() # -#---------------------------------------# -class Job - include Micro::Attributes.to_initialize(diff: true, activemodel_validations: true) - - # Same of `include Micro::Attributes.with(:initialize, :diff, :activemodel_validations)` -end - -#----------------------------------------# -# Via Micro::Attributes.to_initialize!() # -#----------------------------------------# -class Job - include Micro::Attributes.to_initialize!(diff: false, activemodel_validations: true) - - # Same of `include Micro::Attributes.with(:strict_initialize, :activemodel_validations)` -end - #=====================================# # Loading except one or more features # # ----- # #=====================================# class Job include Micro::Attributes.without(:diff) - attributes :id, state: 'sleeping' + attribute :id + attribute :state, default: 'sleeping' end # Note: # The method `Micro::Attributes.without()` returns `Micro::Attributes` if all features extensions were used. ``` @@ -433,100 +443,115 @@ If your application uses ActiveModel as a dependency (like a regular Rails app). You will be enabled to use the `actimodel_validations` extension. ```ruby class Job - # include Micro::Attributes.with(:initialize, :activemodel_validations) - # include Micro::Attributes.features(:initialize, :activemodel_validations) - include Micro::Attributes.to_initialize(activemodel_validations: true) + include Micro::Attributes.with(:activemodel_validations) - attributes :id, state: 'sleeping' + 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) -p job.id # 1 -p job.state # "sleeping" +job.id # 1 +job.state # 'sleeping' ``` +#### Attribute options + +You can use the `validate` or `validates` options to define your attributes. e.g. + +```ruby +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_string + return if state.is_a?(String) && state.present? + + errors.add(:state, 'must be a filled string') + end +end +``` + ### Diff extension Provides a way to track changes in your object attributes. ```ruby require 'securerandom' class Job - # include Micro::Attributes.with(:initialize, :diff) - # include Micro::Attributes.to_initialize(diff: true) - include Micro::Attributes.features(:initialize, :diff) + include Micro::Attributes.with(:initialize, :diff) - attributes :id, state: 'sleeping' + attribute :id + attribute :state, default: 'sleeping' end job = Job.new(id: SecureRandom.uuid()) -p job.id # A random UUID generated from SecureRandom.uuid(). e.g: "e68bcc74-b91c-45c2-a904-12f1298cc60e" -p job.state # "sleeping" +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') -p job_running.state # "running" +job_running.state # 'running' job_changes = job.diff_attributes(job_running) #-----------------------------# # #present?, #blank?, #empty? # #-----------------------------# -p job_changes.present? # true -p job_changes.blank? # false -p job_changes.empty? # false +job_changes.present? # true +job_changes.blank? # false +job_changes.empty? # false #-----------# # #changed? # #-----------# -p job_changes.changed? # true +job_changes.changed? # true -p job_changes.changed?(:id) # false +job_changes.changed?(:id) # false -p job_changes.changed?(:state) # true -p job_changes.changed?(:state, from: 'sleeping', to: 'running') # true +job_changes.changed?(:state) # true +job_changes.changed?(:state, from: 'sleeping', to: 'running') # true #----------------# # #differences() # #----------------# -p job_changes.differences # {"state"=> {"from" => "sleeping", "to" => "running"}} +job_changes.differences # {'state'=> {'from' => 'sleeping', 'to' => 'running'}} ``` ### Initialize extension 1. Creates a constructor to assign the attributes. -2. Adds methods to build new instances when some data was assigned. +2. Add methods to build new instances when some data was assigned. ```ruby class Job - # include Micro::Attributes.with(:initialize) - # include Micro::Attributes.feature(:initialize) - # include Micro::Attributes.features(:initialize) - include Micro::Attributes.to_initialize + include Micro::Attributes.with(:initialize) attributes :id, :state end job_null = Job.new({}) -p job.id # nil -p job.state # nil +job.id # nil +job.state # nil job = Job.new(id: 1, state: 'sleeping') -p job.id # 1 -p job.state # "sleeping" +job.id # 1 +job.state # 'sleeping' ############################################## # Assigning new values to get a new instance # ############################################## @@ -534,45 +559,42 @@ # #with_attribute() # #-------------------# new_job = job.with_attribute(:state, 'running') -puts new_job.id # 1 -puts new_job.state # running -puts new_job.equal?(job) # false +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') -puts other_job.id # 2 -puts other_job.state # killed -puts other_job.equal?(job) # false +other_job.id # 2 +other_job.state # killed +other_job.equal?(job) # false ``` -### Strict initialize extension +### Strict initialize 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**. ```ruby class Job - # include Micro::Attributes.with(:strict_initialize) - # include Micro::Attributes.feature(:strict_initialize) - # include Micro::Attributes.features(:strict_initialize) - include Micro::Attributes.to_initialize! + include Micro::Attributes.with(initialize: :strict) attributes :id, :state end -#----------------------------------------------------------------------------# -# The strict_initialize extension will require all the keys when initialize. # -#----------------------------------------------------------------------------# +#-----------------------------------------------------------------------# +# The strict initialize mode will require all the keys when initialize. # +#-----------------------------------------------------------------------# Job.new({}) # The code above will raise: # ArgumentError (missing keywords: :id, :state) @@ -581,22 +603,19 @@ # Samples passing some data # #---------------------------# job_null = Job.new(id: nil, state: nil) -p job.id # nil -p job.state # nil +job.id # nil +job.state # nil job = Job.new(id: 1, state: 'sleeping') -p job.id # 1 -p job.state # "sleeping" - - -# Note: -# This extension works like the `initialize` extension. -# So, look at its section to understand all the other features. +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. ## 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.