μ-attributes (Micro::Attributes)
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)
- Table of contents
- Required Ruby version
- Installation
- Usage
- How to require?
- How to define attributes?
- How to define multiple attributes?
- How to define attributes with a constructor to assign them?
- How to inherit the attributes?
- How to query the attributes?
- Built-in extensions
- ActiveModel::Validations extension
- Diff extension
- Initialize extension
- Strict initialize extension
- Development
- Contributing
- License
- Code of Conduct
Required Ruby version
>= 2.2.0
Installation
Add this line to your application's Gemfile:
gem 'u-attributes'
And then execute:
$ bundle
Or install it yourself as:
$ gem install u-attributes
Usage
How to require?
# 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?
# 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)
puts person.name # John Doe
puts person.age # 21
# By design, the attributes expose only 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>)
#------------------#
# self.attributes= #
#------------------#
# This protected method is added to make easier the assignment in a constructor.
class Person
include Micro::Attributes
attribute :name, 'John Doe' # .attribute() accepts a second arg as its default value
attribute :age
def initialize()
self.attributes =
end
end
person = Person.new(age: 20)
puts person.name # John Doe
puts person.age # 20
#--------------#
# #attribute() #
#--------------#
#
# Use the #attribute() method with a valid attribute name to get its value
puts person.attribute(:name) # John Doe
puts person.attribute('age') # 20
puts person.attribute('foo') # nil
#
# If you pass a block, it will be executed only if the attribute is 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 not exists.
#---------------#
# #attribute!() #
#---------------#
#
# Works like the #attribute() method, but will raise an exception when the attribute not exist.
puts person.attribute!('foo') # NameError (undefined attribute `foo)
person.attribute!('foo') { |value| puts value } # NameError (undefined attribute `foo)
How to define multiple attributes?
# Use .attributes with a list of attribute names.
class Person
include Micro::Attributes
attributes :age, name: 'John Doe' # Use a hash to define attributes with default values
def initialize()
self.attributes =
end
end
person = Person.new(age: 32)
puts person.name # 'John Doe'
puts person.age # 32
How to define attributes with a constructor to assign them?
A: Use Micro::Attributes.to_initialize
class Person
include Micro::Attributes.to_initialize
attributes :age, name: 'John Doe'
end
person = Person.new(age: 18)
puts person.name # John Doe
puts person.age # 18
##############################################
# Assigning new values to get a new instance #
##############################################
#-------------------#
# #with_attribute() #
#-------------------#
another_person = person.with_attribute(:age, 21)
puts another_person.name # John Doe
puts another_person.age # 21
puts another_person.equal?(person) # false
#--------------------#
# #with_attributes() #
#--------------------#
#
# Use it to assign multiple attributes
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
# If you pass a value different of a Hash, an ArgumentError will be raised.
#
# Person.new(1)
# ArgumentError (argument must be a Hash)
#--------------------#
# Strict initializer #
#--------------------#
# Use .to_initialize! to forbids an instantiation without all keywords.
class StrictPerson
include Micro::Attributes.to_initialize!
attributes :age, name: 'John Doe'
end
StrictPerson.new({})
# The code above will raise:
# ArgumentError (missing keyword: :age)
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`.
How to inherit the attributes?
class Person
include Micro::Attributes.to_initialize
attributes :age, name: '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
#---------------------------------#
# .attribute!() or .attributes!() #
#---------------------------------#
# The methods above allow redefining the attributes default data
class AnotherSubclass < Person
attribute! :name, 'Alfa'
end
alfa_person = AnotherSubclass.new({})
p alfa_person.name # "Alfa"
p alfa_person.age # nil
class SubSubclass < Subclass
attributes! name: 'Beta', age: 0
end
beta_person = SubSubclass.new({})
p beta_person.name # "Beta"
p beta_person.age # 0
How to query the attributes?
class Person
include Micro::Attributes
attributes :age, name: 'John Doe'
def initialize()
self.attributes =
end
end
#---------------#
# .attributes() #
#---------------#
p 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 = 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
#---------------#
# #attributes() #
#---------------#
p person.attributes # {"age"=>20, "name"=>"John Doe"}
p 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"}
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.
But, if you desire...
- only one feature, use the
Micro::Attributes.feature()
method. - except one or more features, use the
Micro::Attributes.without()
method.
#===========================#
# Loading specific features #
#===========================#
class Job
include Micro::Attributes.feature(:diff)
attribute :id
attribute :state, 'sleeping'
def initialize()
self.attributes =
end
end
#======================#
# Loading all features #
# --- #
#======================#
class Job
include Micro::Attributes.features
attributes :id, state: '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)
attributes :id, state: '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'
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.
class Job
# include Micro::Attributes.with(:initialize, :activemodel_validations)
# include Micro::Attributes.features(:initialize, :activemodel_validations)
include Micro::Attributes.to_initialize(activemodel_validations: true)
attributes :id, state: '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"
Diff extension
Provides a way to track changes in your object attributes.
require 'securerandom'
class Job
# include Micro::Attributes.with(:initialize, :diff)
# include Micro::Attributes.to_initialize(diff: true)
include Micro::Attributes.features(:initialize, :diff)
attributes :id, state: '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_running = job.with_attribute(:state, 'running')
p 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
#-----------#
# #changed? #
#-----------#
p job_changes.changed? # true
p job_changes.changed?(:id) # false
p job_changes.changed?(:state) # true
p job_changes.changed?(:state, from: 'sleeping', to: 'running') # true
#----------------#
# #differences() #
#----------------#
p job_changes.differences # {"state"=> {"from" => "sleeping", "to" => "running"}}
Initialize extension
- Creates a constructor to assign the attributes.
- Adds methods to build new instances when some data was assigned.
class Job
# include Micro::Attributes.with(:initialize)
# include Micro::Attributes.feature(:initialize)
# include Micro::Attributes.features(:initialize)
include Micro::Attributes.to_initialize
attributes :id, :state
end
job_null = Job.new({})
p job.id # nil
p job.state # nil
job = Job.new(id: 1, state: 'sleeping')
p job.id # 1
p job.state # "sleeping"
##############################################
# Assigning new values to get a new instance #
##############################################
#-------------------#
# #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
#--------------------#
# #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
Strict initialize extension
- Creates a constructor to assign the attributes.
- Adds methods to build new instances when some data was assigned.
- Forbids missing keywords.
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!
attributes :id, :state
end
#----------------------------------------------------------------------------#
# The strict_initialize extension 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)
p job.id # nil
p 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.
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.