 [](https://rubygems.org/gems/u-attributes) [](https://travis-ci.com/serradura/u-attributes) [](https://codeclimate.com/github/serradura/u-attributes/maintainability) [](https://codeclimate.com/github/serradura/u-attributes/test_coverage) μ-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 <!-- 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 and `bundle install`: ```ruby gem 'u-attributes' ``` ## Compatibility | 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 | ## Usage ### How to define attributes? ```ruby # By default you must to define the class constructor. class Person include Micro::Attributes attribute :name attribute :age def initialize(name: 'John Doe', age:) @name, @age = name, age end end person = Person.new(age: 21) person.name # John Doe person.age # 21 # 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>) ``` #### `Micro::Attributes#attributes=` This is a protected method to make easier the assignment in a constructor. e.g. ```ruby class Person include Micro::Attributes attribute :name, default: 'John Doe' attribute :age def initialize(options) self.attributes = options end end person = Person.new(age: 20) person.name # John Doe person.age # 20 ``` #### `Micro::Attributes#attribute` Use this method with a valid attribute name to get its value. ```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 doesn't exist. ``` #### `Micro::Attributes#attribute!` 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? Use `.attributes` with a list of attribute names. ```ruby 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. ### `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.with(:initialize) attribute :age attribute :name, default: 'John Doe' end person = Person.new(age: 18) person.name # John Doe person.age # 18 ``` This extension enables two methods for your objects. The `#with_attribute()` and `#with_attributes()`. #### `#with_attribute()` ```ruby another_person = person.with_attribute(:age, 21) another_person.name # John Doe another_person.age # 21 another_person.equal?(person) # false ``` #### `#with_attributes()` Use it to assign multiple attributes ```ruby other_person = person.with_attributes(name: 'Serradura', age: 32) other_person.name # Serradura other_person.age # 32 other_person.equal?(person) # false ``` If you pass a value different of a Hash, a Kind::Error will be raised. ```ruby Person.new(1) # Kind::Error (1 must be a Hash) ``` ### 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.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. ``` ruby person_without_age = StrictPerson.new(age: nil) person_without_age.name # 'John Doe' person_without_age.age # nil ``` > **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.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 ``` #### .attribute!() 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, 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 ``` ### How to query the attributes? ```ruby class Person include Micro::Attributes attribute :age attribute :name, default: 'John Doe' def initialize(options) self.attributes = options end end #---------------# # .attributes() # #---------------# Person.attributes # ['name', 'age'] #---------------# # .attribute?() # #---------------# Person.attribute?(:name) # true Person.attribute?('name') # true Person.attribute?('foo') # false Person.attribute?(:foo) # false # --- person = Person.new(age: 20) #---------------# # #attribute?() # #---------------# person.attribute?(:name) # true person.attribute?('name') # true person.attribute?('foo') # false person.attribute?(:foo) # false #---------------# # #attributes() # #---------------# 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). 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.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. ```ruby #===========================# # Loading specific features # #===========================# class Job include Micro::Attributes.with(:diff) attribute :id attribute :state, default: 'sleeping' def initialize(options) self.attributes = options end end #======================# # Loading all features # # --- # #======================# class Job include Micro::Attributes.with_all_features attribute :id attribute :state, default: 'sleeping' end #----------------------------------------------------------------------------# # Using the .with() method alias and adding the strict initialize extension. # #----------------------------------------------------------------------------# class Job include Micro::Attributes.with(:diff, initialize: :strict) 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 #=====================================# # Loading except one or more features # # ----- # #=====================================# class Job include Micro::Attributes.without(:diff) attribute :id attribute :state, default: 'sleeping' end # Note: # The method `Micro::Attributes.without()` returns `Micro::Attributes` if all features extensions were used. ``` ### ActiveModel::Validations extension 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(: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. ```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) 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'}} ``` ### Initialize extension 1. Creates a constructor to assign the attributes. 2. Add methods to build new instances when some data was assigned. ```ruby 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 ``` ### 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(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. ## 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](https://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](http://contributor-covenant.org) code of conduct. ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). ## 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](https://github.com/serradura/u-attributes/blob/master/CODE_OF_CONDUCT.md).