![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) <!-- 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).