├── .rspec ├── Gemfile ├── lib ├── mini_form │ ├── version.rb │ ├── errors.rb │ ├── nested_validator.rb │ └── model.rb └── mini_form.rb ├── .projections.json ├── .gitignore ├── Rakefile ├── spec ├── spec_helper.rb ├── mini_form │ └── nested_validator_spec.rb └── mini_form_spec.rb ├── .github └── workflows │ └── ci.yml ├── LICENSE.txt ├── mini_form.gemspec ├── CHANGELOG.md ├── .rubocop.yml └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --format=documentation 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /lib/mini_form/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MiniForm 4 | VERSION = '0.2.5' 5 | end 6 | -------------------------------------------------------------------------------- /.projections.json: -------------------------------------------------------------------------------- 1 | { 2 | "lib/*.rb": { 3 | "alternate": "spec/{}_spec.rb" 4 | }, 5 | "spec/*_spec.rb": { 6 | "alternate": "lib/{}.rb" 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /lib/mini_form.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'mini_form/version' 4 | require 'mini_form/model' 5 | require 'mini_form/errors' 6 | require 'mini_form/nested_validator' 7 | 8 | module MiniForm 9 | end 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | .vimrc.local 19 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | require 'rubocop/rake_task' 6 | 7 | RSpec::Core::RakeTask.new(:spec) 8 | RuboCop::RakeTask.new(:rubocop) 9 | 10 | task default: %i(rubocop spec) 11 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | 5 | if ENV['TRAVIS'] 6 | require 'coveralls' 7 | Coveralls.wear! 8 | end 9 | 10 | require 'mini_form' 11 | 12 | RSpec.configure do |config| 13 | config.expect_with(:rspec) { |c| c.syntax = :expect } 14 | end 15 | -------------------------------------------------------------------------------- /lib/mini_form/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MiniForm 4 | class InvalidForm < StandardError 5 | attr_reader :errors 6 | 7 | def initialize(object) 8 | @errors = object.errors 9 | 10 | arr_obj = errors.respond_to?(:attribute_names) ? errors.attribute_names : errors.keys 11 | super "Form validation failed for: #{arr_obj.join(', ')}" 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | ruby-version: ['2.6', '2.7', '3.0', '3.1'] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up Ruby ${{ matrix.ruby-version }} 21 | uses: ruby/setup-ruby@0a29871fe2b0200a17a4497bae54fe5df0d973aa # v1.115.3 22 | with: 23 | ruby-version: ${{ matrix.ruby-version }} 24 | 25 | - name: Install dependencies 26 | run: bundle install 27 | 28 | - name: Run linters 29 | run: bundle exec rubocop 30 | 31 | - name: Run tests 32 | run: bundle exec rspec spec 33 | 34 | -------------------------------------------------------------------------------- /lib/mini_form/nested_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_model' 4 | 5 | module MiniForm 6 | class NestedValidator < ActiveModel::EachValidator 7 | def validate_each(record, _, relation) 8 | return if relation.valid? 9 | 10 | if record.errors.respond_to?(:merge!) 11 | # Rails 6.1+ where accessing ActiveModel::Errors as a hash has been 12 | # deprecated and the errors array is frozen. For this reason we use the new 13 | # method merge!() which appends the errors as NestedErrors to the array. "This is the way." 14 | record.errors.merge!(relation.errors) 15 | return 16 | end 17 | 18 | # Rails < 6.1 19 | relation.errors.each do |name, value| 20 | record.errors.add name, value 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/mini_form/nested_validator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | module SpecSupport 6 | class Person 7 | include ActiveModel::Model 8 | 9 | attr_accessor :name 10 | 11 | validates :name, presence: true 12 | end 13 | 14 | class Record 15 | include ActiveModel::Validations 16 | 17 | attr_accessor :user 18 | 19 | def initialize(user) 20 | @user = user 21 | end 22 | end 23 | end 24 | 25 | module MiniForm 26 | describe NestedValidator do 27 | let(:validator) { described_class.new(attributes: [:user]) } 28 | let(:user) { SpecSupport::Person.new } 29 | let(:record) { SpecSupport::Record.new(user) } 30 | 31 | it 'copies errors from submodel to model' do 32 | validator.validate(record) 33 | 34 | expect(record.errors[:name]).not_to be_blank 35 | end 36 | 37 | it 'does not copy errors when there are not any' do 38 | user.name = 'valid name' 39 | 40 | validator.validate(record) 41 | 42 | expect(record.errors[:name]).to be_blank 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2015 Radoslav Stankov 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /mini_form.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'mini_form/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'mini_form' 8 | spec.version = MiniForm::VERSION 9 | spec.authors = ['Radoslav Stankov'] 10 | spec.email = ['rstankov@gmail.com'] 11 | spec.description = 'Sugar around ActiveModel::Model' 12 | spec.summary = 'Easy to use form objects in Rails projects' 13 | spec.homepage = 'https://github.com/RStankov/MiniForm' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) 17 | spec.executables = spec.files.grep(%r{^bin\/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(spec)\/}) 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_dependency 'activemodel', '>= 4.0' 22 | 23 | spec.add_development_dependency 'bundler', '>= 2.1.4' 24 | spec.add_development_dependency 'rake', '>= 12.3.3' 25 | spec.add_development_dependency 'rspec', '3.12.0' 26 | spec.add_development_dependency 'rspec-mocks', '3.12.0' 27 | spec.add_development_dependency 'coveralls' 28 | spec.add_development_dependency 'rubocop' 29 | spec.add_development_dependency 'rubocop-rspec' 30 | end 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 0.2.5 4 | 5 | * Added `main_model` (@rstankov) 6 | 7 | ## Version 0.2.4 8 | 9 | * Rails 6+ support (@emilov) 10 | 11 | ## Version 0.2.3 12 | 13 | * Fix handling delegated attributes prefixes in `attribute_names` (@rstankov) 14 | 15 | ## Version 0.2.2 16 | 17 | * Bump activemodel requirements (@vestimir) 18 | 19 | ## Version 0.2.1 20 | 21 | * Fix typo in `assignment` callbaks (@ayrton) 22 | * Alias `assignment` to `assigment` for backwards compatibility (@ayrton) 23 | 24 | ## Version 0.2.0 25 | 26 | * Don't expose model name on `model`. _(security fix)_ 27 | 28 | * Included `ActiveModel::Validations::Callbacks` to `MiniForm::Model` 29 | 30 | * Added read option to `model`: 31 | 32 | ```ruby 33 | class EditProfile 34 | include MiniForm::Model 35 | 36 | model :user, attributes: %i(email name), read:%(id) 37 | end 38 | 39 | profile = EditProfile.new(user: user) 40 | profile.id 41 | profile.id = 1 # raises NoMethodError 42 | ``` 43 | 44 | 45 | * MiniForm::Model can be inherited 46 | * Added `assigment` callbacks, called when attributes are assigned 47 | * Added `assign_attributes` alias to `attributes=` 48 | * Exposed `errors` on MiniForm::InvalidForm 49 | * Added descriptive message MiniForm::InvalidForm 50 | * Made `.attribute_names` public 51 | 52 | ## Version 0.1.0 53 | 54 | * Initial release 55 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: rubocop-rspec 2 | 3 | AllCops: 4 | Exclude: 5 | - mini_form.gemspec 6 | 7 | SuggestExtensions: false 8 | NewCops: disable 9 | 10 | # Disables "Module definition is too long" 11 | Metrics/ModuleLength: 12 | Enabled: false 13 | 14 | # Disables "Line is too long" 15 | Layout/LineLength: 16 | Enabled: false 17 | 18 | # Disables "Method is too long" 19 | Metrics/MethodLength: 20 | Enabled: false 21 | 22 | # Disables "Assignment Branch Condition size for included is too high" 23 | Metrics/AbcSize: 24 | Enabled: false 25 | 26 | # Disables "Block has too many lines" 27 | Metrics/BlockLength: 28 | Enabled: false 29 | 30 | # Disables "Missing top-level class documentation comment" 31 | Style/Documentation: 32 | Enabled: false 33 | 34 | # Disables "Use each_with_object instead of inject" 35 | Style/EachWithObject: 36 | Enabled: false 37 | 38 | # Disables "Prefer reduce over inject." 39 | Style/CollectionMethods: 40 | Enabled: false 41 | 42 | # Disables "%i-literals should be delimited by [ and ]." 43 | Style/PercentLiteralDelimiters: 44 | Enabled: false 45 | 46 | # Disables "Example has too many expectations" 47 | RSpec/MultipleExpectations: 48 | Enabled: false 49 | 50 | # Disables "Example has too many lines" 51 | RSpec/ExampleLength: 52 | Enabled: false 53 | 54 | Style/HashSyntax: 55 | Enabled: false 56 | 57 | Lint/MissingSuper: 58 | Enabled: false 59 | -------------------------------------------------------------------------------- /lib/mini_form/model.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/all' 4 | require 'active_model' 5 | 6 | module MiniForm 7 | module Model 8 | def self.included(base) 9 | base.class_eval do 10 | include ActiveModel::Validations 11 | include ActiveModel::Validations::Callbacks 12 | include ActiveModel::Conversion 13 | 14 | extend ActiveModel::Naming 15 | extend ActiveModel::Callbacks 16 | 17 | extend ClassMethods 18 | 19 | define_model_callbacks :update 20 | define_model_callbacks :assignment 21 | 22 | # For backwards compatibility purpose 23 | define_model_callbacks :assigment 24 | 25 | before_update :before_update 26 | after_update :after_update 27 | 28 | before_assignment :before_assignment 29 | after_assignment :after_assignment 30 | 31 | # For backwards compatibility purpose 32 | before_assigment :before_assigment 33 | after_assigment :after_assigment 34 | end 35 | end 36 | 37 | def initialize(attributes = {}) 38 | self.attributes = attributes 39 | end 40 | 41 | def persisted? 42 | false 43 | end 44 | 45 | def attributes=(attributes) 46 | run_callbacks :assignment do 47 | run_callbacks :assigment do 48 | attributes.slice(*self.class.attribute_names).each do |name, value| 49 | public_send "#{name}=", value 50 | end 51 | end 52 | end 53 | end 54 | 55 | alias assign_attributes attributes= 56 | 57 | def attributes 58 | Hash[self.class.attribute_names.map { |name| [name, public_send(name)] }] 59 | end 60 | 61 | def update(attributes = {}) 62 | self.attributes = attributes unless attributes.empty? 63 | 64 | return false unless valid? 65 | 66 | run_callbacks :update do 67 | transaction do 68 | save_models 69 | perform 70 | end 71 | end 72 | 73 | true 74 | end 75 | 76 | def update!(attributes = {}) 77 | raise InvalidForm, self unless update attributes 78 | 79 | self 80 | end 81 | 82 | private 83 | 84 | def transaction(&block) 85 | if defined? ActiveRecord 86 | ActiveRecord::Base.transaction(&block) 87 | else 88 | yield 89 | end 90 | end 91 | 92 | # :api: private 93 | def save_models 94 | self.class.models_to_save.each { |model_name| public_send(model_name).save! } 95 | end 96 | 97 | def perform 98 | # noop 99 | end 100 | 101 | def before_update 102 | # noop 103 | end 104 | 105 | def after_update 106 | # noop 107 | end 108 | 109 | def before_assignment 110 | # noop 111 | end 112 | 113 | def after_assignment 114 | # noop 115 | end 116 | 117 | def before_assigment 118 | # noop 119 | end 120 | 121 | def after_assigment 122 | # noop 123 | end 124 | 125 | module ClassMethods 126 | def inherited(base) 127 | new_attribute_names = attribute_names.dup 128 | new_models_to_save = models_to_save.dup 129 | 130 | base.instance_eval do 131 | @attribute_names = new_attribute_names 132 | @models_to_save = new_models_to_save 133 | end 134 | end 135 | 136 | def attribute_names 137 | @attribute_names ||= [] 138 | end 139 | 140 | # :api: private 141 | def models_to_save 142 | @models_to_save ||= [] 143 | end 144 | 145 | def attributes(*attributes, delegate: nil, prefix: nil, allow_nil: nil) 146 | if prefix 147 | attribute_names.push(*attributes.map do |name| 148 | :"#{prefix == true ? delegate : prefix}_#{name}" 149 | end) 150 | else 151 | attribute_names.push(*attributes) 152 | end 153 | 154 | if delegate.nil? 155 | attr_accessor(*attributes) 156 | else 157 | delegate(*attributes, to: delegate, prefix: prefix, allow_nil: allow_nil) 158 | delegate(*attributes.map { |attr| "#{attr}=" }, to: delegate, prefix: prefix, allow_nil: allow_nil) 159 | end 160 | end 161 | 162 | def model(name, attributes: [], read: [], prefix: nil, allow_nil: nil, save: false) # rubocop:disable Metrics/ParameterLists 163 | attr_accessor name 164 | 165 | attributes(*attributes, delegate: name, prefix: prefix, allow_nil: allow_nil) unless attributes.empty? 166 | 167 | delegate(*read, to: name, prefix: prefix, allow_nil: nil) 168 | 169 | validates name, 'mini_form/nested' => true 170 | 171 | models_to_save << name if save 172 | end 173 | 174 | def main_model(model_name, **args) 175 | delegate :id, :persisted?, :to_param, :new_record?, to: model_name 176 | 177 | model model_name, **args 178 | end 179 | end 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](http://badge.fury.io/rb/mini_form) 2 | [](https://codeclimate.com/github/RStankov/MiniForm) 3 | [](https://coveralls.io/r/RStankov/MiniForm) 4 | 5 | # MiniForm 6 | 7 | Helpers for dealing with form objects and nested forms. 8 | 9 | ## Installation 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | ```ruby 14 | gem 'mini_form' 15 | ``` 16 | 17 | And then execute: 18 | 19 | $ bundle 20 | 21 | Or install it yourself as: 22 | 23 | $ gem install mini_form 24 | 25 | ## Usage 26 | 27 | ```ruby 28 | class ProductForm 29 | include MiniForm::Model 30 | 31 | attributes :id, :name, :price, :description 32 | 33 | validates :name, :price, :description, presence: true 34 | 35 | # called after successful validations in update 36 | def perform 37 | @id = ExternalService.create(attributes) 38 | end 39 | end 40 | ``` 41 | 42 | ```ruby 43 | class ProductsController < ApplicationController 44 | def create 45 | @product = ProductForm.new 46 | 47 | if @product.update(product_params) 48 | redirect_to product_path(@product.id) 49 | else 50 | render :edit 51 | end 52 | end 53 | 54 | private 55 | 56 | def product_params 57 | params.require(:product).permit(:name, :price, :description) 58 | end 59 | end 60 | ``` 61 | 62 | ### Delegated attributes 63 | 64 | Attributes can be delegated to a sub object. 65 | 66 | ```ruby 67 | class SignUpForm 68 | include MiniForm::Model 69 | 70 | attr_reader :account, :user 71 | 72 | attributes :name, :email, delegate: :user 73 | attributes :company_name, :plan, delegate: :account 74 | 75 | validates :name, :email, :company_name, :plan, presence: true 76 | 77 | def initialize 78 | @account = Account.new 79 | @user = User.new account: @account 80 | end 81 | 82 | def perform 83 | user.save! 84 | account.save! 85 | end 86 | end 87 | ``` 88 | 89 | ```ruby 90 | form = SignUpForm.new 91 | form.name = 'name' # => form.user.name = 'name' 92 | form.name # => form.user.name 93 | form.plan = 'free' # => form.account.plan = 'free' 94 | form.plan # => form.account.plan 95 | ``` 96 | 97 | ### Nested validator 98 | 99 | `mini_form/nested` validator runs validations on the given model and copies errors to the form object. 100 | 101 | ```ruby 102 | class SignUpForm 103 | include MiniForm::Model 104 | 105 | attr_reader :account, :user 106 | 107 | attributes :name, :email, delegate: :user 108 | attributes :company_name, :plan, delegate: :account 109 | 110 | validates :account, :user, 'mini_form/nested' => true 111 | 112 | def initialize 113 | @account = Account.new 114 | @user = User.new account: @account 115 | end 116 | 117 | def perform 118 | account.save! 119 | user.save! 120 | end 121 | end 122 | ``` 123 | 124 | ### Nested models 125 | 126 | Combines delegated attributes and nested validation into a single call. 127 | 128 | ```ruby 129 | class SignUpForm 130 | include MiniForm::Model 131 | 132 | model :user, attributes: %i(name email) 133 | model :account, attributes: %i(company_name plan) 134 | 135 | def initialize 136 | @account = Account.new 137 | @user = User.new account: @account 138 | end 139 | 140 | def perform 141 | account.save! 142 | user.save! 143 | end 144 | end 145 | ``` 146 | 147 | ### Auto saving nested models 148 | 149 | Most of the time `perform` is just calling `save!`. We can avoid this by using `model`'s `save` option. 150 | 151 | ```ruby 152 | class SignUpForm 153 | include MiniForm::Model 154 | 155 | model :user, attributes: %i(name email), save: true 156 | model :account, attributes: %i(company_name plan), save: true 157 | 158 | def initialize 159 | @account = Account.new 160 | @user = User.new account: @account 161 | end 162 | end 163 | ``` 164 | 165 | ### Before/after callbacks 166 | 167 | ```ruby 168 | class SignUpForm 169 | include MiniForm::Model 170 | 171 | # ... code 172 | 173 | before_update :run_before_update 174 | after_update :run_after_update 175 | 176 | private 177 | 178 | def run_before_update 179 | # ... 180 | end 181 | 182 | def run_after_update 183 | # ... 184 | end 185 | 186 | # alternatively you can overwrite "before_update" 187 | def before_update 188 | end 189 | 190 | # alternatively you can overwrite "after_update" 191 | def after_update 192 | end 193 | end 194 | ``` 195 | 196 | ### Using in forms 197 | 198 | Using `main_model` will delegate `id`, `to_param`, `persisted?` and `new_record?` to the model. Allowing you to use it in forms. 199 | 200 | ```ruby 201 | class SignUpForm 202 | include MiniForm::Model 203 | 204 | main_model :user 205 | 206 | def initialize 207 | @user = User.new(account: @account) 208 | end 209 | end 210 | ``` 211 | 212 | ```eruby 213 | <% form_for SignUpForm.new %> 214 | ``` 215 | 216 | ### Delegating model attributes 217 | 218 | ```ruby 219 | class SignUpForm 220 | include MiniForm::Model 221 | 222 | model :user, attributes: %i(name email), read: %i(id) 223 | 224 | def initialize 225 | @user = User.new(account: @account) 226 | end 227 | end 228 | ``` 229 | 230 | ``` 231 | form = SignUpForm.new 232 | form.update! form_params 233 | form.id # => delegates to `user.id` 234 | form.id = 42 # => raises `NoMethodError` 235 | ``` 236 | 237 | ### Methods 238 | 239 |
| Method | 242 |Description | 243 |
|---|---|
| .model | 246 |Defines a sub object for the form | 247 |
| .attributes | 250 |Defines an attribute, it can delegate to sub object | 251 |
| .attribute_names | 254 |Returns list of attribute names | 255 |
| #initialize | 258 |Meant to be overwritten. By defaults calls `attributes=` | 259 |
| #attributes= | 262 |Sets values of all attributes | 263 |
| #attributes | 266 |Returns all attributes of the form | 267 |
| #update | 270 |Sets attributes, calls validations, saves models and `perform` | 271 |
| #update! | 274 |Calls `update`. If validation fails, it raises an error | 275 |
| #perform | 278 |Meant to be overwritten. Doesn't do anything by default | 279 |
| #before_update | 282 |Meant to be overwritten. | 283 |
| #after_update | 286 |Meant to be overwritten. | 287 |
| #before_assignment | 290 |Meant to be overwritten. | 291 |
| #after_assignment | 294 |Meant to be overwritten. | 295 |
| #transaction | 298 |If ActiveRecord is available, wraps `perform` in transaction. | 299 |