Composition
Alternative composition support for rails applications, for when
ActiveRecord's composed_of is not enough. This gem adds some behavior
into composed objects and ways to interact and send messages between both
the one composing and the one being composed.
Installation
Add this line to your application's Gemfile:
gem 'composition'And then execute:
$ bundle
Or install it yourself as:
$ gem install composition
Usage
Composition will enable a new way of defining composed objects into an
ActiveRecord class. You should have available a compose macro for your
use in your application models.
class User < ActiveRecord::Base
compose :credit_card,
mapping: {
credit_card_name: :name,
credit_card_brand: :brand,
credit_card_expiration: :expiration
}
endThe User class has now available the following methods to manipulate
the credit_card object:
User#credit_cardUser#credit_card=(credit_card)
These methods will operate with a credit_card value object like the one described below:
class CreditCard < Composition::Base
composed_from :user
def expired?
Date.today > expiration
end
endNotice that CreditCard inherits from Composition::Base and that the
composed_from macro is set to :user. This is necessary in order to gain
full access to the user object from the credit_card.
How to interact with the value object
With the previous setup in place, now it should be possible to access attributes from
the database through the value objects instead. You can think of the CreditCard
as a normal ActiveModel::Model class with the attributes that you already
specified in the mapping option.
You would interact with the credit_card object like the following:
user.credit_card_name = 'Jon Snow' # Set the ActiveRecord attribute
user.credit_card_brand = 'Visa' # Set the ActiveRecord attribute
user.credit_card_expiration = Date.yesterday # Set the ActiveRecord attribute
user.credit_card # => CreditCard.new(name: 'Jon Snow', brand: 'Visa', expiration: Thu, 11 May 2017)
user.credit_card.name # => 'Jon Snow'
user.credit_card.brand # => 'Visa'
user.credit_card.expiration # => Thu, 11 May 2017
user.credit_card.user == user # => true
user.credit_card.attributes # => { name: 'Jon Snow', brand: 'Visa', expiration: Thu, 11 May 2017 }
user.credit_card.expired? # => trueModifying the credit_card attributes:
user.credit_card.name # => 'Jon Snow'
user.credit_card.name = 'Arya Stark' # => 'Arya Stark'
user.credit_card_name # => 'Arya Stark'
user.save # => trueWriting to value objects
The value object can be set by either setting attributes individually, by
assigning a new value object, or by using assign_attributes on the parent.
user.credit_card.name = 'Jon Snow'
user.credit_card.brand = 'Visa'
user.credit_card.expiration = Date.today
user.credit_card # => CreditCard.new(name: 'Jon Snow', brand: 'Visa', expiration: Thu, 12 May 2017)
user.credit_card = CreditCard.new(name: 'Jon Snow', brand: 'Visa', expiration: Date.today)
user.credit_card # => CreditCard.new(name: 'Jon Snow', brand: 'Visa', expiration: Thu, 12 May 2017)
user.assign_attributes(credit_card: { name: 'Jon Snow', brand: 'Visa', expiration: Date.today })
user.credit_card # => CreditCard.new(name: 'Jon Snow', brand: 'Visa', expiration: Thu, 12 May 2017)
user.update_attributes(credit_card: { name: 'Jon Snow', brand: 'Visa', expiration: Date.today })
user.credit_card # => CreditCard.new(name: 'Jon Snow', brand: 'Visa', expiration: Thu, 12 May 2017)Validations
If you need to add validations to your value object that should just work.
class CreditCard < Composition::Base
composed_from :user
validates :expiration, presence: true
def expired?
Date.today > expiration
end
end
user.credit_card = CreditCard.new(name: 'Jon Snow', brand: 'Visa', expiration: nil)
user.credit_card.valid? # => falseDetailed macro documentation
Composition will assume some things and use some defaults based on naming
conventions for when you define compose and composed_from macros. However,
there will be cases where you will have to override the naming convention with
something custom. Following you will find the complete reference for the provided
macros.
Options for compose
The compose method will accept the following options:
:mapping
This is required. It will accept a hash of mappings between the attributes in the parent object and their mapping to the new value object being defined.
class User < ActiveRecord::Base
compose :credit_card,
mapping: {
credit_card_name: :name,
credit_card_brand: :brand,
credit_card_expiration: :expiration
}
end:class_name
Optional. If the name of the value object cannot be derived from the composition
name, you can use the :class_name option to supply the class name. If a user has
a credit_card but the name of the class is something like CCard, then you can use:
class User < ActiveRecord::Base
compose :credit_card,
mapping: {
credit_card_name: :name,
credit_card_brand: :brand,
credit_card_expiration: :expiration
}, class_name: 'CCard'
endOptions for composed_from
The composed_from method will accept the following options:
:class_name
Optional. If the name of the value object cannot be derived from the composition
name, you can use the :class_name option to supply the class name. If a user has
a credit_card but the name of the user class is something like AdminUser, then
you can use:
class CreditCard < Composition::Base
compose_from :user, class_name: 'AdminUser'
endContributing
- Fork it ( https://github.com/cedarcode/composition/ )
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
See the Running Tests guide for details on how to run the test suite.
License
This project is licensed under the MIT License - see the LICENSE file for details