ActiveType 
Make any Ruby object quack like ActiveRecord
ActiveType is our take on "presenter models" (or "form models") in Rails. We want to have controllers (and forms) talk to models that are either not backed by a database table, or have additional functionality that should not be shared to the rest of the application.
However, we do not want to lose ActiveRecord's amenities, like validations, callbacks, etc.
Examples for use cases are models to support sign in:
class SignIn < ActiveType::Object
# this is not backed by a db table
attribute :username, :string
attribute :password, :string
validates :username, presence: true
validates :password, presence: true
# ...
end
Or models to support sign up:
class User < ActiveRecord::Base
# ...
end
class SignUp < ActiveType::Record[User]
# this inherits from User
validates :password, confirmation: true
after_create :send_confirmation_email
def send_confirmation_email
# this should happen on sign-up, but not when creating a user in tests etc.
end
# ...
end
ActiveType::Object
Inherit from ActiveType::Object if you want an ActiveRecord-kind class that is not backed by a database table.
You can define "columns" by saying attribute:
class SignIn < ActiveType::Object
attribute :email, :string
attribute :date_of_birth, :date
attribute :accepted_terms, :boolean
attribute :account_type
end
These attributes can be assigned via constructor, mass-assignment, and are automatically typecast:
sign_in = SignIn.new(date_of_birth: "1980-01-01", accepted_terms: "1", account_type: AccountType::Trial.new)
sign_in.date_of_birth.class # Date
sign_in.accepted_terms? # true
ActiveType knows all the types that are allowed in migrations (i.e. :string, :integer, :float, :decimal, :datetime, :time, :date, :boolean). You can also skip the type to have a virtual attribute without typecasting.
ActiveType::Object actually inherits from ActiveRecord::Base, but simply skips all database access, inspired by ActiveRecord Tableless.
This means your object has all usual ActiveRecord::Base methods. Some of those might not work properly, however. What does work:
- validations
- callbacks (use
before_save,after_save, notbefore_create, orbefore_update) - "saving" (returning
trueorfalse, without actually persisting) - belongs_to (after saying
attribute :child_id, :integer)
ActiveType::Record
If you have a database backed record (that inherits from ActiveRecord::Base), but also want to declare virtual attributes, simply inherit from ActiveType::Record.
Virtual attributes will not be persisted.
ActiveType::Record[BaseClass]
ActiveType::Record[BaseClass] is used to extend a given BaseClass (that itself has to be an ActiveRecord model) with additional functionality, that is not meant to be shared to the rest of the application.
You class will inherit from BaseClass. You can add additional methods, validations, callbacks, as well as use (virtual) attributes like an ActiveType::Object:
class SignUp < ActiveType::Record[User]
# ...
end
Inheriting from ActiveType:: objects
If you want to inherit from an ActiveType class, simply do
class SignUp < ActiveType::Record[User]
# ...
end
class SpecialSignUp < SignUp
# ...
end
Defaults
Attributes can have defaults. Those are lazily evaluated on the first read, if no value has been set.
class SignIn < ActiveType::Object
attribute :created_at, :datetime, default: proc { Time.now }
end
The proc is evaluated in the context of the object, so you can do
class SignIn < ActiveType::Object
attribute :email, :string
attribute :nickname, :string, default: proc { email.split('@').first }
end
SignIn.new(email: "[email protected]").nickname # "tobias"
SignIn.new(email: "[email protected]", :nickname => "kratob").nickname # "kratob"
Nested attributes
ActiveType supports its own variant of nested attributes via the nests_one /
nests_many macros. The intention is to be mostly compatible with
ActiveRecord's accepts_nested_attributes functionality.
Assume you have a list of records, say representing holidays, and you want to support bulk editing. Then you could do something like:
class Holiday < ActiveRecord::Base
validates :date, presence: true
end
class HolidaysForm < ActiveType::Object
nests_many :holidays, reject_if: :all_blank, default: proc { Holiday.all }
end
class HolidaysController < ApplicationController
def edit
@holidays_form = HolidaysForm.new
end
def update
@holidays_form = HolidaysForm.new(params[:holidays_form])
if @holidays_form.save
redirect_to root_url, notice: "Success!"
else
render :edit
end
end
end
# and in the view
<%= form_for @holidays_form, url: '/holidays', method: :put do |form| %>
<ul>
<%= form.fields_for :holidays do |holiday_form| %>
<li><%= holiday_form.text_field :date %></li>
<% end %>
</ul>
<% end %>
- You have to say
nests_many :records recordswill be validated and saved automaticallyThe generated
.records_attributes =expects parameters likeActiveRecord's nested attributes, and words together with thefields_forhelper:- either as a hash (where the keys are meaningless)
{ '1' => { date: "new record's date" }, '2' => { id: '3', date: "existing record's date" } }- or as an array
{ [ date: "new record's date" ], [ id: '3', date: "existing record's date" ] }
To use it with single records, use nests_one. It works like accept_nested_attributes does for has_one. Use .record_attributes = to build the child record.
Supported options for nests_many / nests_one are:
build_scope
Used to build new records, for example:
nests_many :documents, build_scope: proc { Document.where(:state => "fresh") }
find_scope
Used to find existing records (in order to update them).
scope
Sets find_scope and build_scope together.
If you don't supply a scope, ActiveType will guess from the association name, i.e. saying
nests_many :documents
is the same as saying
nests_many :documents, scope: proc { Document }
which is identical to
nests_many :documents, build_scope: proc { Document }, find_scope: proc { Document }
All ...scope options are evaled in the context of the record on first use, and cached.
allow_destroy
Allow to destroy records if the attributes contain _destroy => '1'
reject_if
Pass either a proc of the form proc { |attributes| ... }, or a symbol indicating a method, or :all_blank.
Will reject attributes for which the proc or the method returns true, or with only blank values (for :all_blank).
default
Initializes the association on first access with the given proc:
nests_many :documents, default: proc { Documents.all }
Supported Rails versions
ActiveType is tested against ActiveRecord 3.2, 4.0, 4.1 and 4.2.
Later versions might work, earlier will not.
Supported Ruby versions
ActiveType is tested against MRI 1.8.7 (for 3.2 only), 1.9.3, 2.0.0, 2.1.2.
Installation
In your Gemfile say:
gem 'active_type'
Now run bundle install and restart your server.
Development
- We run tests against several ActiveRecord versions.
- You can bundle all versions with
rake all:bundle. - You can run specs against all versions with
rake. - You can run specs against a single version with
VERSION=4.0 rake.
If you would like to contribute:
- Fork the repository.
- Push your changes with passing specs.
- Send us a pull request.
I'm very eager to keep this gem leightweight and on topic. If you're unsure whether a change would make it into the gem, talk to me beforehand.
Credits
Tobias Kraze from makandra
Henning Koch from makandra