├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .projections.json
├── .rspec
├── .rubocop.yml
├── CHANGELOG.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── lib
├── mini_form.rb
└── mini_form
│ ├── errors.rb
│ ├── model.rb
│ ├── nested_validator.rb
│ └── version.rb
├── mini_form.gemspec
└── spec
├── mini_form
└── nested_validator_spec.rb
├── mini_form_spec.rb
└── spec_helper.rb
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.projections.json:
--------------------------------------------------------------------------------
1 | {
2 | "lib/*.rb": {
3 | "alternate": "spec/{}_spec.rb"
4 | },
5 | "spec/*_spec.rb": {
6 | "alternate": "lib/{}.rb"
7 | },
8 | }
9 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --colour
2 | --format=documentation
3 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 |
5 | gemspec
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
240 |
241 | Method |
242 | Description |
243 |
244 |
245 | .model |
246 | Defines a sub object for the form |
247 |
248 |
249 | .attributes |
250 | Defines an attribute, it can delegate to sub object |
251 |
252 |
253 | .attribute_names |
254 | Returns list of attribute names |
255 |
256 |
257 | #initialize |
258 | Meant to be overwritten. By defaults calls `attributes=` |
259 |
260 |
261 | #attributes= |
262 | Sets values of all attributes |
263 |
264 |
265 | #attributes |
266 | Returns all attributes of the form |
267 |
268 |
269 | #update |
270 | Sets attributes, calls validations, saves models and `perform` |
271 |
272 |
273 | #update! |
274 | Calls `update`. If validation fails, it raises an error |
275 |
276 |
277 | #perform |
278 | Meant to be overwritten. Doesn't do anything by default |
279 |
280 |
281 | #before_update |
282 | Meant to be overwritten. |
283 |
284 |
285 | #after_update |
286 | Meant to be overwritten. |
287 |
288 |
289 | #before_assignment |
290 | Meant to be overwritten. |
291 |
292 |
293 | #after_assignment |
294 | Meant to be overwritten. |
295 |
296 |
297 | #transaction |
298 | If ActiveRecord is available, wraps `perform` in transaction. |
299 |
300 |
301 |
302 | ## Contributing
303 |
304 | 1. Fork it
305 | 2. Create your feature branch (`git checkout -b my-new-feature`)
306 | 3. Commit your changes (`git commit -am 'Add some feature'`)
307 | 4. Push to the branch (`git push origin my-new-feature`)
308 | 5. Run the tests (`rake`)
309 | 6. Create new Pull Request
310 |
311 | ## License
312 |
313 | **[MIT License](https://github.com/RStankov/MiniForm/blob/master/LICENSE.txt)**
314 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/mini_form/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module MiniForm
4 | VERSION = '0.2.5'
5 | end
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/spec/mini_form_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | module SpecSupport
6 | class User
7 | include ActiveModel::Model
8 |
9 | attr_accessor :id, :name, :age
10 |
11 | validates :name, presence: true
12 |
13 | def to_param
14 | "user-#{id}"
15 | end
16 |
17 | def persisted?
18 | id.present?
19 | end
20 |
21 | def new_record?
22 | !persisted?
23 | end
24 | end
25 |
26 | class Example
27 | include MiniForm::Model
28 |
29 | attributes :name, :price
30 | end
31 |
32 | class ExampleWithDelegate
33 | include MiniForm::Model
34 |
35 | attr_reader :user
36 |
37 | attributes :name, delegate: :user
38 | attributes :id, delegate: :user, prefix: true
39 | attributes :name, delegate: :user, prefix: 'full'
40 |
41 | def initialize(user)
42 | @user = user
43 | end
44 | end
45 |
46 | class ExampleWithModel
47 | include MiniForm::Model
48 |
49 | model :user, attributes: %i(name), read: %i(id)
50 |
51 | def initialize(user)
52 | self.user = user
53 | end
54 | end
55 |
56 | class ExampleForUpdate
57 | include MiniForm::Model
58 |
59 | attributes :name
60 |
61 | validates :name, presence: true
62 | end
63 |
64 | class ExampleForSave
65 | include MiniForm::Model
66 |
67 | model :user, attributes: %i(name), save: true
68 |
69 | def initialize(user:)
70 | self.user = user
71 | end
72 | end
73 |
74 | class ExampleFormModel
75 | include MiniForm::Model
76 |
77 | main_model :user
78 |
79 | def initialize(user:)
80 | self.user = user
81 | end
82 | end
83 | end
84 |
85 | module MiniForm
86 | describe Model do
87 | let(:user) { SpecSupport::User.new id: 1, name: 'name', age: 28 }
88 |
89 | describe 'acts as ActiveModel' do
90 | include ActiveModel::Lint::Tests
91 |
92 | before do
93 | @model = SpecSupport::Example.new
94 | end
95 |
96 | def assert(condition, message = nil)
97 | expect(condition).to be_truthy, message
98 | end
99 |
100 | def assert_kind_of(expected_kind, object, message = nil)
101 | expect(object).to be_kind_of(expected_kind), message
102 | end
103 |
104 | def assert_equal(expected_value, value, message = nil)
105 | expect(value).to eq(expected_value), message
106 | end
107 |
108 | def assert_respond_to(klass, method, message = nil)
109 | expect(klass).to respond_to(method), message
110 | end
111 |
112 | ActiveModel::Lint::Tests.public_instance_methods.map(&:to_s).grep(/^test/).each do |method|
113 | example(method.gsub('_', ' ')) { send method }
114 | end
115 | end
116 |
117 | describe 'inheritance' do
118 | it 'can be inherited' do
119 | parent_class = Class.new do
120 | include Model
121 |
122 | attributes :name
123 | end
124 |
125 | child_class = Class.new(parent_class) do
126 | attributes :age
127 | end
128 |
129 | expect(parent_class.attribute_names).to eq %i(name)
130 | expect(child_class.attribute_names).to eq %i(name age)
131 | end
132 | end
133 |
134 | describe '.attributes' do
135 | it 'generates getters' do
136 | object = SpecSupport::Example.new name: 'value'
137 | expect(object.name).to eq 'value'
138 | end
139 |
140 | it 'generates setters' do
141 | object = SpecSupport::Example.new
142 | object.name = 'value'
143 |
144 | expect(object.name).to eq 'value'
145 | end
146 |
147 | it 'can delegate getter' do
148 | object = SpecSupport::ExampleWithDelegate.new user
149 | expect(object.name).to eq user.name
150 | end
151 |
152 | it 'can delegate setter' do
153 | object = SpecSupport::ExampleWithDelegate.new user
154 |
155 | object.name = 'New Name'
156 |
157 | expect(object.name).to eq 'New Name'
158 | expect(user.name).to eq 'New Name'
159 | end
160 | end
161 |
162 | describe '.model' do
163 | it 'generates model accessors' do
164 | object = SpecSupport::ExampleWithModel.new user
165 | expect(object.user).to eq user
166 | end
167 |
168 | it 'can delegate only a reader' do
169 | object = SpecSupport::ExampleWithModel.new user
170 |
171 | expect(object).not_to respond_to :id=
172 | expect(object.id).to eq user.id
173 | end
174 |
175 | it 'can delegate model attributes' do
176 | object = SpecSupport::ExampleWithModel.new user
177 | expect(object.name).to eq user.name
178 | end
179 |
180 | it 'performs nested validation for model' do
181 | user = SpecSupport::User.new
182 | object = SpecSupport::ExampleWithModel.new user
183 |
184 | expect(object).not_to be_valid
185 | expect(object.errors[:name]).to be_present
186 | end
187 | end
188 |
189 | describe '.main_model' do
190 | it 'delegates Rails form attributes to the model' do
191 | user = SpecSupport::User.new
192 | object = SpecSupport::ExampleFormModel.new(user: user)
193 |
194 | expect(object).to have_attributes(
195 | id: user.id,
196 | to_param: user.to_param,
197 | persisted?: user.persisted?,
198 | new_record?: user.new_record?
199 | )
200 | end
201 | end
202 |
203 | describe '.attributes_names' do
204 | it 'returns attribute names' do
205 | expect(SpecSupport::Example.attribute_names).to eq %i(name price)
206 | end
207 |
208 | it 'can handle prefixes' do
209 | expect(SpecSupport::ExampleWithDelegate.attribute_names).to include :user_id
210 | expect(SpecSupport::ExampleWithDelegate.attribute_names).to include :full_name
211 | end
212 | end
213 |
214 | describe '#initialize' do
215 | it 'can be called with no arguments' do
216 | expect { SpecSupport::Example.new }.not_to raise_error
217 | end
218 |
219 | it 'assign the passed attributes' do
220 | object = SpecSupport::Example.new price: '$5'
221 |
222 | expect(object.price).to eq '$5'
223 | end
224 |
225 | it 'ignores invalid attributes' do
226 | expect { SpecSupport::Example.new invalid: 'attribute' }.not_to raise_error
227 | end
228 |
229 | it 'handles HashWithIndifferentAccess hashes' do
230 | hash = ActiveSupport::HashWithIndifferentAccess.new 'price' => '$5'
231 | object = SpecSupport::Example.new hash
232 |
233 | expect(object.price).to eq '$5'
234 | end
235 | end
236 |
237 | describe '#attributes' do
238 | it 'returns attributes' do
239 | object = SpecSupport::Example.new name: 'iPhone', price: '$5'
240 | expect(object.attributes).to eq name: 'iPhone', price: '$5'
241 | end
242 | end
243 |
244 | ['attributes=', 'assign_attributes'].each do |method_name|
245 | describe "##{method_name}" do
246 | it 'sets attributes' do
247 | object = SpecSupport::Example.new
248 | object.public_send method_name, name: 'iPhone', price: '$5'
249 |
250 | expect(object.attributes).to eq name: 'iPhone', price: '$5'
251 | end
252 |
253 | it 'ignores not listed attributes' do
254 | object = SpecSupport::Example.new
255 | object.public_send method_name, invalid: 'value'
256 |
257 | expect(object.attributes).to eq name: nil, price: nil
258 | end
259 | end
260 | end
261 |
262 | describe '#update' do
263 | it 'updates attributes' do
264 | object = SpecSupport::ExampleForUpdate.new name: 'value'
265 |
266 | expect { object.update(name: 'new value') }.to change { object.name }.to 'new value'
267 | end
268 |
269 | it 'returns true when validations pass' do
270 | object = SpecSupport::ExampleForUpdate.new name: 'value'
271 |
272 | expect(object.update).to eq true
273 | end
274 |
275 | it 'calls "perfom" method when validation pass' do
276 | object = SpecSupport::ExampleForUpdate.new name: 'value'
277 |
278 | allow(object).to receive(:perform)
279 |
280 | object.update
281 |
282 | expect(object).to have_received(:perform)
283 | end
284 |
285 | it 'calls "save" for the model' do
286 | object = SpecSupport::ExampleForSave.new user: user
287 |
288 | allow(user).to receive(:save!)
289 |
290 | object.update
291 |
292 | expect(user).to have_received(:save!)
293 | end
294 |
295 | it 'supports update callbacks' do
296 | object = SpecSupport::ExampleForUpdate.new name: 'value'
297 |
298 | allow(object).to receive(:before_update)
299 | allow(object).to receive(:after_update)
300 |
301 | object.update
302 |
303 | expect(object).to have_received(:before_update)
304 | expect(object).to have_received(:after_update)
305 | end
306 |
307 | it 'supports legacy assig callbacks' do
308 | object = SpecSupport::ExampleForUpdate.new
309 |
310 | allow(object).to receive(:before_assigment)
311 | allow(object).to receive(:after_assigment)
312 |
313 | object.update name: 'value'
314 |
315 | expect(object).to have_received(:before_assigment)
316 | expect(object).to have_received(:after_assigment)
317 | end
318 |
319 | it 'supports assign callbacks' do
320 | object = SpecSupport::ExampleForUpdate.new
321 |
322 | allow(object).to receive(:before_assignment)
323 | allow(object).to receive(:after_assignment)
324 |
325 | object.update name: 'value'
326 |
327 | expect(object).to have_received(:before_assignment)
328 | expect(object).to have_received(:after_assignment)
329 | end
330 |
331 | it 'returns false when validations fail' do
332 | object = SpecSupport::ExampleForUpdate.new name: nil
333 |
334 | expect(object.update).to eq false
335 | end
336 |
337 | it 'does not call "perfom" method when validation fail' do
338 | object = SpecSupport::ExampleForUpdate.new name: nil
339 |
340 | allow(object).to receive(:perform)
341 |
342 | object.update
343 |
344 | expect(object).not_to have_received(:perform)
345 | end
346 | end
347 |
348 | describe '#update!' do
349 | it 'returns self' do
350 | object = SpecSupport::Example.new
351 | expect(object.update!).to eq object
352 | end
353 |
354 | it 'calls update with given arguments' do
355 | object = SpecSupport::Example.new
356 |
357 | allow(object).to receive(:update).and_return true
358 |
359 | object.update! :attributes
360 |
361 | expect(object).to have_received(:update).with(:attributes)
362 | end
363 |
364 | it 'raises error when update fails' do
365 | object = SpecSupport::Example.new
366 |
367 | allow(object).to receive(:update).and_return false
368 |
369 | expect { object.update! }.to raise_error InvalidForm
370 | end
371 | end
372 | end
373 | end
374 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------