DataComposer
DataComposer provides a set of helpers to elegantly compose native Ruby Data objects into your ActiveRecord models.
This gem lets you turn this:
# db/migrate/xxxx.rb
add_column :products, :price_amount, :decimal
add_column :products, :price_currency, :string
# app/models/product.rb
Money = Data.define(:amount, :currency)
composed_of :price, class_name: "Money", mapping: [%w[price_amount amount], %w[price_currency currency]]
Into this:
# db/migrate/xxxx.rb
t.data_object :price, members: { amount: :decimal, currency: :string }
# app/models/product.rb
Money = Data.define(:amount, :currency)
compose_data :price, Money
Product.first.price.amount #=> 100.0
Core Concept
The philosophy is simple:
- Define Value Objects with
Data.define: Use pure, native RubyDataobjects to represent concepts in your domain. - Declare Schema in Migrations: Use the
t.data_objecthelper to explicitly define the database columns required to store your value object. - Compose in the Model: Use the
compose_datahelper in your model to map the database columns to yourDataobject.# DataComposer
Installation
Add this line to your application's Gemfile:
gem 'data_composer'
And then execute:
$ bundle install
Usage
Let's walk through an example of storing a Height value object, composed of an amount and a unit, on a User model.
Step 1: Define Your Value Object
Create a file for your value object. It's just a plain Ruby Data object—no special wrappers are needed.
# app/models/height.rb
Height = Data.define(:amount, :unit) do
# You can add any relevant domain logic here
def to_s
"#{amount} #{unit}"
end
def in_inches
unit == 'cm' ? (amount * 0.393701).round(2) : amount
end
end
Step 2: Create a Migration
Generate a migration to add the columns to your table.
rails g migration AddHeightToUsers
Inside the migration file, use the t.data_object helper. You must explicitly define the member names and their corresponding database types.
# db/migrate/xxxx_add_height_to_users.rb
class AddHeightToUsers < ActiveRecord::Migration[7.0]
def change
change_table :users do |t|
t.data_object :height,
members: { amount: :decimal, unit: :string },
null: false,
default: { amount: 0, unit: 'cm' }
end
end
end
When you run rails db:migrate, this will create two columns: height_amount (DECIMAL) and height_unit (STRING).
Step 3: Compose it in Your Model
In your ActiveRecord model, use compose_data to tie everything together. This method automatically infers that :height will be mapped from the height_amount and height_unit columns.
# app/models/user.rb
class User < ApplicationRecord
compose_data :height, Height
end
Step 4: Use It!
Your User model now has a rich height attribute.
# Create a user with a Height object
user = User.create!(height: Height.new(amount: 180, unit: 'cm'))
# Access the object and its members
puts user.height
#=> #<data Height amount=180, unit="cm">
puts user.height.amount
#=> 180
puts user.height.to_s
#=> "180 cm"
puts user.height.in_inches
#=> 70.87
# The object is transparently saved to the database columns
user.reload
puts user.read_attribute(:height_amount) #=> 180
puts user.read_attribute(:height_unit) #=> "cm"
# Assigning nil or invalid data also works as expected
user.update!(height: nil)
puts user.height #=> nil
Advanced Features
Default Values
You can specify default values for individual members:
t.data_object :price,
members: { amount: :decimal, currency: :string },
default: { amount: 0, currency: "USD" }
Multiple Data Objects
You can have multiple data objects on the same model:
class Product < ApplicationRecord
compose_data :price, Money
compose_data :dimensions, Dimensions
compose_data :weight, Weight
end
Custom Validation
Data objects support standard ActiveRecord validations:
class User < ApplicationRecord
compose_data :height, Height
validates :height, presence: true
validate :height_must_be_reasonable
private
def height_must_be_reasonable
return unless height
errors.add(:height, "must be positive") if height.amount <= 0
errors.add(:height, "seems too tall") if height.amount > 300
end
end
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/your-username/data_composer.
License
The gem is available as open source under the terms of the MIT License.