• Stars
    star
    1,036
  • Rank 44,465 (Top 0.9 %)
  • Language
    Ruby
  • License
    MIT License
  • Created over 10 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Make any Ruby object quack like ActiveRecord

ActiveType Tests

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

A note on Rails's own .attribute

Rails 5+ comes with its own implementation of .attribute. This implementation is functionally very similar, but not identical to ActiveType's.

We have decided to continue to use our own implementation. This means that if you use ActiveType, ActiveRecord::Base.attribute will be overriden.

The following behaviours are different than in vanilla Rails 5:

  • Defaults procs are evaluated in instance context, not class context.
  • Defaults are evaluated lazily.
  • You can override attributes with custom methods and use super.
  • Attributes will work on records retrieved via .find.
  • Attributes will be duped if you dup the record.
  • You cannot use attribute :db_column to override the behaviour of an existing database-backed attribute.

If you need to use ActiveRecord's own .attribute method, you can still access is as ar_attribute:

class User < ApplicationRecord
  # use my custom type to serialize to the database
  ar_attribute :password, MyPasswordType.new
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, not before_create, or before_update)
  • "saving" (returning true or false, without actually persisting)
  • belongs_to (after saying attribute :child_id, :integer)

Note on transactions

Since ActiveType::Object is not backed by a database, it does not open a real transaction when saving, so you should not rely on database changes that might have happend in a #save callback to be rolled back when #save fails. If you need this, make sure to wrap those changes in an explicit transaction yourself.

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.

Your 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

If you need to access the extended BaseClass from your presenter model, you may call extended_record_base_class on its class:

SignUp.extended_record_base_class # => "User (...)"

# or
sign_up = SignUp.new
sign_up.class # => "SignUp (...)"
sign_up.class.extended_record_base_class # => "User (...)"

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"

Overriding accessors

You can override attribute getters and setters using super:

class SignIn < ActiveType::Object

  attribute :email, :string
  attribute :nickname, :string

  def email
    super.downcase
  end

  def nickname=(value)
    super(value.titleize)
  end

end

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

  • records will be validated and saved automatically

  • The generated .records_attributes = expects parameters like ActiveRecord's nested attributes, and works together with the fields_for helper:

    • 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 }

Options supported exclusively by nests_many are:

  • index_errors

    Use a boolean to get indexed errors on related records. In Rails 5 you can make it global with config.active_record.index_nested_attribute_errors = true.

Casting records or relations

When working with ActiveType you will often find it useful to cast an ActiveRecord instance to its extended ActiveType::Record variant.

Use ActiveType.cast for this:

class User < ActiveRecord::Base
  ...
end

class SignUp < ActiveType::Record[User]
  ...
end

user = User.find(1)
sign_up = ActiveType.cast(user, SignUp)
sign_up.is_a?(SignUp) # => true

This is basically like ActiveRecord#becomes, but with less bugs and more consistent behavior.

Note that cast is destructive. The originally casted record (user) and the returned record (sign_up) share internal state (such as attributes). To avoid unexpected behavior, the original record will raise an error when trying to change or persist it. Also, casting of a record that has changes in its loaded associations is prevented, because those changes would be lost.
If you know what you are doing and absolutely want that, you may use the option force: true to allow this potentially problematic behaviour, e.g. sign_up = ActiveType.cast(user, SignUp, force: true)

You can also cast an entire relation (scope) to a relation of an ActiveType::Record:

adult_users = User.where('age >= 18')
adult_sign_ups = ActiveType.cast(adult_users, SignUp)
sign_up = adult_sign_ups.find(1)
sign_up.is_a?(SignUp) # => true

If you need to add some special logic after casting you can add an after_cast method:

class SignUp < ActiveType::Record[User]
  def after_cast(user)
    # Called with the original user upon casting.
  end
end

Associations

Sometimes, you have an association, and a form model for that association. Instead of always casting the associations manually, you can use the change_association macro to override an association's options. For example.

class Credential < ActiveRecord::Base
end

class User < ActiveRecord::Base
  has_many :credentials
end

class SignUpCredential < ActiveType::Record[Credential]
end

class SignUp < ActiveType::Record[User]
  change_association :credentials, class_name: 'SignUpCredential'
end

Now, if you load credentials, you will automatically receive records of type SignUpCredential.

Supported Rails versions

ActiveType is tested against ActiveRecord 5.2, 6.0, 6.1, and 7.0.

Later versions might work, earlier will not.

Supported Ruby versions

ActiveType is tested against 2.5, 2.7, 3.0, and 3.1.

Installation

In your Gemfile say:

gem 'active_type'

Now run bundle install and restart your server.

Development

  • We run tests against several ActiveRecord and Ruby versions using gemika.
  • You can bundle all versions with rake matrix:install.
  • You can run specs against all Gemfiles compatible with your current ruby version with rake matrix:spec.
  • You can run specs against a single Gemfile with BUNDLE_GEMFILE=Gemfile<variant> bundle exec rspec spec.
  • When you make a pull request, tests are automatically run against all variants and Rubies on travis.ci.

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 lightweight 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

More Repositories

1

consul

Scope-based authorization for Ruby on Rails.
Ruby
313
star
2

query_diet

A Rails database query counter that stays out of your way
Ruby
244
star
3

aegis

Complete authorization solution for Rails that supports roles and a RESTish, resource-style declaration of permission rules.
Ruby
191
star
4

modularity

Traits and partial classes for Ruby
Ruby
174
star
5

spreewald

Our collection of useful cucumber steps.
Ruby
137
star
6

assignable_values

Restrict the values assignable to ActiveRecord attributes or associations.
Ruby
120
star
7

cucumber_factory

Create records from Cucumber features without writing step definitions.
Ruby
116
star
8

geordi

Collection of command line tools used in our daily work with Ruby, Rails and Linux.
Ruby
105
star
9

edge_rider

Power tools for Active Record relations (scopes)
Ruby
85
star
10

rspec_candy

RSpec helpers and matchers
Ruby
68
star
11

capybara-lockstep

Synchronize Capybara commands with application JavaScript and AJAX requests
Ruby
65
star
12

dusen

Comprehensive search solution for ActiveRecord and MySQL.
Ruby
61
star
13

gemika

Test a Ruby gem against multiple versions of everything
Ruby
61
star
14

minidusen

Low-tech search for ActiveRecord with MySQL or PostgreSQL
Ruby
32
star
15

superclamp

Cross-browser ellipsis on multi-line texts. Optimized for performance, and supports tags inside clamped element. Even looks better than -webkit-clamp. Supports all real browsers and IE11+.
HTML
29
star
16

apify

Apify lets you bolt a JSON-API onto your Rails application. UNMAINTAINED.
Ruby
27
star
17

katapult

Kickstart Rails development!
Ruby
22
star
18

rails_state_machine

Rails State Machine is an ActiveRecord-bound state machine.
Ruby
19
star
19

cucumber_spinner

Progress bar formatter for cucumber. Shows failing scenarios immediately and can automatically show the error page in the browser.
Ruby
16
star
20

mail_magnet

Mail Magnet allows you to override e-mail recipients in ActionMailer so all mails go to a given address. This library is UNMAINTAINED. Use the official ActionMailer::Base.register_interceptor API instead: https://makandracards.com/makandra/46094-rails-how-to-write-custom-email-interceptors
Ruby
15
star
21

safe_cookies

Have cookies as secure as possible
Ruby
11
star
22

validate-hls

Smoke test for HLS stream integrity
Ruby
9
star
23

upjs

Progressive enhancement, reloaded
8
star
24

machinist_callbacks

Callback hooks for machinist blueprints. UNMAINTAINED.
Ruby
7
star
25

pollyanna

Very simple search for your ActiveRecord models. UNMAINTAINED.
Ruby
7
star
26

makandra-rubocop

makandra's default Rubocop configuration
Ruby
6
star
27

rack-steady_etag

Rack Middleware that produces the same ETag for responses that only differ in CSRF tokens or CSP nonces.
Ruby
6
star
28

capistrano-opscomplete

Capistrano tasks for easy deployment to a makandra opscomplete environment.
Ruby
6
star
29

precompiled_assets

Serve assets without Rails doing any processing. Just requires a manifest file to resolve filenames.
Ruby
6
star
30

navy

Comprehensive solution for multi-level horizontal navigation bars. UNMAINTAINED.
Ruby
5
star
31

angular_xss

Patches rails_xss and Haml so AngularJS interpolations are auto-escaped in unsafe strings.
Ruby
5
star
32

memoized

Memoize your methods.
Ruby
5
star
33

ie-css-test

Tests Internet Explorer CSS breakage
HTML
4
star
34

cucumber_priority

Overridable step definitions for Cucumber
Ruby
4
star
35

makandra_sidekiq

Support code for sidekiq.
Ruby
3
star
36

rascal

Spin up CI environments locally
Ruby
3
star
37

esbuild-manifest-plugin

esbuild plugin to generate a manifest file for all digested files
JavaScript
2
star
38

angular-coffee-style-guide

Styleguide for Angular 1 using CoffeeScript
2
star
39

cloud_test

Ruby
2
star
40

akamai_tools

Akamai Tools provide some helpful tools, to deal with CCP and Net Storage.
Ruby
1
star
41

terraform-aws-modules

Collection of some standard modules for deployment in AWS accounts.
HCL
1
star
42

github-actions

1
star
43

webdev101

Examples for lecture "Grundlagen der Webentwicklung" at University of Augsburg
JavaScript
1
star
44

serum-rails

Scans a Rails application for metrics relevant to security audits. UNMAINTAINED.
Ruby
1
star
45

secret_service

Better secrets for Rails. UNMAINTAINED.
Ruby
1
star
46

micro_exiftool

Minimal ruby wrapper around exiftool
Ruby
1
star
47

cards_mvc

Sample project to evaluate JavaScript frameworks in the spirit of TodoMVC
JavaScript
1
star
48

check-config-rule-s3-lifecycle

This lambda function evaluates if an AWS Config rule is compliant. It's written to check if AWS::S3::Bucket resources with versioning enabled have a lifecycle policy. It does not check what is configured in the lifecycle policy.
Python
1
star