on_form 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +237 -0
- data/Rakefile +11 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/on_form/attributes.rb +64 -0
- data/lib/on_form/errors.rb +22 -0
- data/lib/on_form/form.rb +48 -0
- data/lib/on_form/multiparameter_attributes.rb +67 -0
- data/lib/on_form/saving.rb +79 -0
- data/lib/on_form/version.rb +3 -0
- data/lib/on_form.rb +8 -0
- data/on_form.gemspec +27 -0
- metadata +131 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b6aada85f9b13d5b28592d880d3cfebd1f5ef8e4
|
4
|
+
data.tar.gz: 9d15cc5152cb460307365fe91dadb63c8ca72370
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ab8c256758e5eebecf4e3f2fd2f93ab85c763fed8924e61629cd3e7765a12527f911c22341d484bacdacc0347de62804f06c8ff20ecc3a52aac85b0e6f539d4e
|
7
|
+
data.tar.gz: 43ee6c65289f1194bb92076c517a28fcdd10733517c8dc0d4e3dd702f625d8bbe629659d047f3b887b4e25154c64880db85a3b29e619e517a8dd751a09b02022
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Powershop New Zealand Limited
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,237 @@
|
|
1
|
+
# OnForm
|
2
|
+
|
3
|
+
A pragmatism-first library to help Rails applications migrate from complex nested attribute models to tidy form objects.
|
4
|
+
|
5
|
+
Our goal is that you can migrate large forms to OnForm incrementally, without having to refactor large amounts of code in a single release.
|
6
|
+
|
7
|
+
Data and validations flow back and forward from the model layer automatically once you've defined which model attributes should be exposed.
|
8
|
+
|
9
|
+
Forms backed by multiple models are supported natively, with no concept of a single main model.
|
10
|
+
|
11
|
+
ActiveModel/ActiveRecord idioms such as validations and callbacks can be used directly in the form object.
|
12
|
+
|
13
|
+
Whereever possible, the terminology and experience should be familiar to Rails developers, to minimize relearning time.
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem 'on_form', '~> 1.0'
|
21
|
+
```
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
$ bundle
|
26
|
+
|
27
|
+
Or install it yourself as:
|
28
|
+
|
29
|
+
$ gem install on_form
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
This version of OnForm should work with Rails 5.0 and 4.2.
|
34
|
+
|
35
|
+
This version of OnForm depends on both the `activemodel` and `activerecord` gems. Rails 5.0 has refactored some of the necessary ActiveRecord code across to ActiveModel, so the `activerecord` dependency may be dropped once Rails 4.2 support is dropped.
|
36
|
+
|
37
|
+
### Simple example of wrapping a model
|
38
|
+
|
39
|
+
Let's say you have a big fat legacy model called `Customer`, and you have a preferences controller:
|
40
|
+
|
41
|
+
class PreferencesController
|
42
|
+
def show
|
43
|
+
@customer = Customer.find(params[:id])
|
44
|
+
end
|
45
|
+
|
46
|
+
def update
|
47
|
+
@customer = Customer.find(params[:id])
|
48
|
+
@customer.update!(params[:customer].permit(:name, :email, :phone_number)
|
49
|
+
redirect_to preferences_path(@customer)
|
50
|
+
rescue ActiveRecord::RecordInvalid
|
51
|
+
render :show
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
Let's wrap the customer object in a form object. Ideally we'd call this `@customer_form`, but you may not feel you have time to go and update all your view code, so in this example we'll keep calling it `@customer`.
|
56
|
+
|
57
|
+
class PreferencesController
|
58
|
+
def show
|
59
|
+
@customer = PreferencesForm.new(Customer.find(params[:id]))
|
60
|
+
end
|
61
|
+
|
62
|
+
def update
|
63
|
+
@customer = PreferencesForm.new(Customer.find(params[:id]))
|
64
|
+
@customer.update!(params[:customer])
|
65
|
+
rescue ActiveRecord::RecordInvalid
|
66
|
+
render :show
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
Now we need to make our form object. At this point we need to tell the form object which attributes on the model we want to expose. (In this example we have just one model and a couple of attributes, but you wouldn't bother using this library if this was all you had.)
|
71
|
+
|
72
|
+
class PreferencesForm < OnForm::Form
|
73
|
+
expose :customer => %i(name email phone_number)
|
74
|
+
|
75
|
+
def initialize(customer)
|
76
|
+
@customer = customer
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
The form object responds to the usual persistance methods like `email`, `email=`, `save`, `save!`, `update`, and `update!`.
|
81
|
+
|
82
|
+
It will automatically write those exposed attributes back onto the models, and *it exposes any validation errors from those fields on the form object itself* - you don't have to copy them back manually or move your field validation code over to get started. It'll also expose any errors on base on the models whose attributes you exposed.
|
83
|
+
|
84
|
+
### A multi-model form
|
85
|
+
|
86
|
+
You aren't limited to having one primary model - if your form is made up of multiple models pass more than one key to `expose`, or call it multiple times if you prefer. They'll automatically be saved in the same order you declared them.
|
87
|
+
|
88
|
+
In this example, the new models we're exposing are associated with the first one, so we don't need to pass them in to the constructor.
|
89
|
+
|
90
|
+
class HouseListingForm < OnForm::Form
|
91
|
+
expose :house => %i(street_number street_name city),
|
92
|
+
:vendor => %i(name phone_number)
|
93
|
+
|
94
|
+
def initialize(house)
|
95
|
+
@house = house
|
96
|
+
@vendor = house.vendor
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
Transactions will automatically be started so that _all_ database updates will be rolled back if _any_ record fails to save (for example, due to a validation error).
|
101
|
+
|
102
|
+
Note that the keys are the name of the methods on the form object which return the records, not the class names. In this example, vendor might actually be an instance of our `Customer` model from the earlier examples.
|
103
|
+
|
104
|
+
### Model accessor methods
|
105
|
+
|
106
|
+
In the previous example, the constructor set `@house` and `@vendor` because these variables correspond to the names passed to `expose`. `expose` will automatically add an `attr_reader` for each key it's given, meaning you only need to set the instance variables.
|
107
|
+
|
108
|
+
But if you prefer, you can define a method with the same name yourself, for example using delegation. `expose` won't run `attr_reader` if you've already defined the method, and there's no requirement to set an instance variable.
|
109
|
+
|
110
|
+
class HouseListingForm < OnForm::Form
|
111
|
+
delegate :vendor, :to => :house
|
112
|
+
|
113
|
+
expose :house => %i(street_number street_name city),
|
114
|
+
:vendor => %i(name phone_number)
|
115
|
+
|
116
|
+
def initialize(house)
|
117
|
+
@house = house
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
You can also define your own method over the top of the `attr_reader`. Just remember it will be called more than once, so it should be idempotent.
|
122
|
+
|
123
|
+
### Validations
|
124
|
+
|
125
|
+
Validations on the underlying models not only get used, but their validation errors show up on the form's `errors` object directly when you call `valid?` or any of the save/update methods.
|
126
|
+
|
127
|
+
But you can also declare validations on the form object itself, which is useful when you have business rules applicable to this form that aren't intrinsic to the domain model.
|
128
|
+
|
129
|
+
class AddEmergencyContactForm < OnForm::Form
|
130
|
+
expose :customer => %i(next_of_kin_name next_of_kin_phone_number)
|
131
|
+
|
132
|
+
validates_presence_of :next_of_kin_name, :next_of_kin_phone_number
|
133
|
+
|
134
|
+
def initialize(customer)
|
135
|
+
@customer = customer
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
Note that when you call `save!`, `update!`, or `update_attributes!` on the form object, validation errors from records will still raise `ActiveRecord::RecordInvalid`, but validation errors from validations defined on the form itself will raise `ActiveModel::ValidationError`. You will usually want to rescue both.
|
140
|
+
|
141
|
+
### Callbacks
|
142
|
+
|
143
|
+
You can also use the `before_validation`, `before_save`, `after_save`, and `around_save` validations. Like ActiveRecord, these will run inside the database transaction when you're calling one of the save or update methods, which is especially useful if you need to take locks on parent records.
|
144
|
+
|
145
|
+
class NewBranchForm < OnForm::Form
|
146
|
+
expose :branch => %w(bank_id branch_number branch_name)
|
147
|
+
|
148
|
+
before_save :lock_bank
|
149
|
+
|
150
|
+
protected
|
151
|
+
def lock_bank
|
152
|
+
branch.bank.lock!
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
Note that model save calls are nested inside the form save calls, which means that although form validation takes place before form save starts, model validation takes place after form saving begins.
|
157
|
+
|
158
|
+
form before_validation
|
159
|
+
form validate (validations defined on the form itself)
|
160
|
+
form before_save
|
161
|
+
form around_save begins
|
162
|
+
model before_validation
|
163
|
+
model validate (validations defined on the model)
|
164
|
+
model before_save
|
165
|
+
model around_save begins
|
166
|
+
model saved
|
167
|
+
model around_save ends
|
168
|
+
model after_save
|
169
|
+
form around_save ends
|
170
|
+
form after_save
|
171
|
+
|
172
|
+
### Reusing and extending forms
|
173
|
+
|
174
|
+
You can descend form classes from other form classes and expose additional models or additional attributes on existing models.
|
175
|
+
|
176
|
+
class AdminHouseListingForm < HouseListingForm
|
177
|
+
expose :house => %i(listing_approved)
|
178
|
+
end
|
179
|
+
|
180
|
+
This works well for some use cases, but can quickly become cumbersome if you have a lot of partial form reuse, and it may not be obvious to other developers that the parent form is also used to derive the other forms. Consider breaking your form parts into reuseable modules, and defining each form separately.
|
181
|
+
|
182
|
+
You can use standard Ruby hooks for this:
|
183
|
+
|
184
|
+
module AccountFormComponent
|
185
|
+
def self.included(form)
|
186
|
+
form.expose :customer => %i(email phone_number)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
class NewAccountForm < OnForm::Form
|
191
|
+
include AccountFormComponent
|
192
|
+
|
193
|
+
expose :customer => %i(name)
|
194
|
+
|
195
|
+
def initialize(customer)
|
196
|
+
@customer = customer
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
class EditAccountForm < OnForm::Form
|
201
|
+
include AccountFormComponent
|
202
|
+
|
203
|
+
delegate :name, to: :customer
|
204
|
+
|
205
|
+
def initialize(customer)
|
206
|
+
@customer = customer
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
In this example the initialize method could actually be moved to the module as well, but that makes it harder to compose forms from multiple modules.
|
211
|
+
|
212
|
+
If you prefer, you can use the Rails `included` block syntax in the module instead of `def self.included`.
|
213
|
+
|
214
|
+
## Development
|
215
|
+
|
216
|
+
After checking out the repo, pick the rails version you'd like to run tests against, and run:
|
217
|
+
|
218
|
+
RAILS_VERSION=5.0.0.1 bundle update
|
219
|
+
|
220
|
+
You should then be able to run the test suite:
|
221
|
+
|
222
|
+
bundle exec rake
|
223
|
+
|
224
|
+
## Contributing
|
225
|
+
|
226
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/powershop/on_form.
|
227
|
+
|
228
|
+
## Roadmap
|
229
|
+
|
230
|
+
* For version 2, the author is looking into support for declaring attributes on the form. (You can use plain old Ruby object `attr_accessor` for untyped attributes in the meantime.)
|
231
|
+
* After that we'll need to tackle the other use cases for ActiveRecord nested attributes, such as one-to-many associations and auto-building/deleting associated records.
|
232
|
+
|
233
|
+
## License
|
234
|
+
|
235
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
236
|
+
|
237
|
+
Copyright © Powershop New Zealand Limited, 2016
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "on_form"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
module OnForm
|
2
|
+
module Attributes
|
3
|
+
# the individual attribute methods are introduced by the expose_attribute class method.
|
4
|
+
# here we introduce some methods used for the attribute set as a whole.
|
5
|
+
|
6
|
+
def [](attribute_name)
|
7
|
+
send(attribute_name)
|
8
|
+
end
|
9
|
+
|
10
|
+
def []=(attribute_name, attribute_value)
|
11
|
+
send("#{attribute_name}=", attribute_value)
|
12
|
+
end
|
13
|
+
|
14
|
+
def read_attribute_for_validation(attribute_name)
|
15
|
+
send(attribute_name)
|
16
|
+
end
|
17
|
+
|
18
|
+
def write_attribute(attribute_name, attribute_value)
|
19
|
+
send("#{attribute_name}=", attribute_value)
|
20
|
+
end
|
21
|
+
|
22
|
+
def attribute_names
|
23
|
+
self.class.exposed_attributes.values.reduce(:+).collect(&:to_s)
|
24
|
+
end
|
25
|
+
|
26
|
+
def attributes
|
27
|
+
attribute_names.each_with_object({}) do |attribute_name, results|
|
28
|
+
results[attribute_name] = self[attribute_name]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def attributes=(attributes)
|
33
|
+
# match ActiveRecord #attributes= behavior on nil, scalars, etc.
|
34
|
+
raise ArgumentError, "When assigning attributes, you must pass a hash as an argument." unless attributes.is_a?(Hash)
|
35
|
+
|
36
|
+
multiparameter_attributes = {}
|
37
|
+
attributes.each do |attribute_name, attribute_value|
|
38
|
+
attribute_name = attribute_name.to_s
|
39
|
+
if attribute_name.include?('(')
|
40
|
+
multiparameter_attributes[attribute_name] = attribute_value
|
41
|
+
else
|
42
|
+
write_attribute(attribute_name, attribute_value)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
assign_multiparameter_attributes(multiparameter_attributes)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def backing_model(backing_model_name)
|
50
|
+
send(backing_model_name)
|
51
|
+
end
|
52
|
+
|
53
|
+
def backing_models
|
54
|
+
self.class.exposed_attributes.keys.collect { |backing_model_name| backing_model(backing_model_name) }
|
55
|
+
end
|
56
|
+
|
57
|
+
def backing_object_for_attribute(attribute_name)
|
58
|
+
self.class.exposed_attributes.each do |backing_model_name, attribute_names|
|
59
|
+
return backing_model(backing_model_name) if attribute_names.include?(attribute_name.to_sym)
|
60
|
+
end
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module OnForm
|
2
|
+
module Errors
|
3
|
+
def errors
|
4
|
+
@errors ||= ActiveModel::Errors.new(self)
|
5
|
+
end
|
6
|
+
|
7
|
+
private
|
8
|
+
def reset_errors
|
9
|
+
@errors = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def collect_errors
|
13
|
+
self.class.exposed_attributes.each do |backing_model_name, exposed_attributes_on_backing_model|
|
14
|
+
backing_model(backing_model_name).errors.each do |backing_attribute, attribute_errors|
|
15
|
+
if backing_attribute == :base || exposed_attributes_on_backing_model.include?(backing_attribute)
|
16
|
+
Array(attribute_errors).each { |error| errors[backing_attribute] << error }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/on_form/form.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module OnForm
|
2
|
+
class Form
|
3
|
+
include ActiveModel::Validations
|
4
|
+
include ActiveModel::Validations::Callbacks
|
5
|
+
|
6
|
+
include Attributes
|
7
|
+
include MultiparameterAttributes
|
8
|
+
include Errors
|
9
|
+
include Saving
|
10
|
+
|
11
|
+
def self.exposed_attributes
|
12
|
+
@exposed_attributes ||= Hash.new { |h, k| h[k] = [] }
|
13
|
+
end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def inherited(child)
|
17
|
+
exposed_attributes.each { |k, v| child.exposed_attributes[k].concat(v) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.expose(backing_models_and_attribute_names)
|
22
|
+
backing_models_and_attribute_names.each do |backing_model_name, attribute_names|
|
23
|
+
backing_model_name = backing_model_name.to_sym
|
24
|
+
expose_backing_model(backing_model_name)
|
25
|
+
attribute_names.each do |attribute_name|
|
26
|
+
expose_attribute(backing_model_name, attribute_name)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.expose_backing_model(backing_model_name)
|
32
|
+
unless instance_methods.include?(backing_model_name)
|
33
|
+
attr_reader backing_model_name
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.expose_attribute(backing_model_name, attribute_name)
|
38
|
+
exposed_attributes[backing_model_name] << attribute_name.to_sym
|
39
|
+
|
40
|
+
[attribute_name, "#{attribute_name}_before_type_cast", "#{attribute_name}?"].each do |attribute_method|
|
41
|
+
define_method(attribute_method) { backing_model(backing_model_name).send(attribute_method) }
|
42
|
+
end
|
43
|
+
["#{attribute_name}="].each do |attribute_method|
|
44
|
+
define_method(attribute_method) { |arg| backing_model(backing_model_name).send(attribute_method, arg) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# unlike the rest of this library, which is new code, the code in this source file is from ActiveRecord, and
|
2
|
+
# is used to provide compatibility wrappers with different versions of ActiveRecord. please keep it separate
|
3
|
+
# so we can see where everything came from and what may need to be kept in sync with ActiveRecord refactors.
|
4
|
+
module OnForm
|
5
|
+
module MultiparameterAttributes
|
6
|
+
# Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
|
7
|
+
# by calling new on the column type or aggregation type (through composed_of) object with these parameters.
|
8
|
+
# So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
|
9
|
+
# written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
|
10
|
+
# parentheses to have the parameters typecasted before they're used in the constructor. Use i for Integer and
|
11
|
+
# f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+.
|
12
|
+
def assign_multiparameter_attributes(pairs)
|
13
|
+
execute_callstack_for_multiparameter_attributes(
|
14
|
+
extract_callstack_for_multiparameter_attributes(pairs)
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def execute_callstack_for_multiparameter_attributes(callstack)
|
19
|
+
errors = []
|
20
|
+
callstack.each do |name, values_with_empty_parameters|
|
21
|
+
begin
|
22
|
+
if defined?(ActiveRecord::AttributeAssignment::MultiparameterAttribute)
|
23
|
+
# ActiveRecord 4.2 and below: you must use MultiparameterAttribute to construct the attribute value.
|
24
|
+
# we therefore have to look up which model the attribute actually lives on.
|
25
|
+
send("#{name}=", ActiveRecord::AttributeAssignment::MultiparameterAttribute.new(backing_object_for_attribute(name), name, values_with_empty_parameters).read_value)
|
26
|
+
else
|
27
|
+
# ActiveRecord 5.0+: you can assign the indexed hash to the column and it will construct the value for you.
|
28
|
+
if values_with_empty_parameters.each_value.all?(&:nil?)
|
29
|
+
values = nil
|
30
|
+
else
|
31
|
+
values = values_with_empty_parameters
|
32
|
+
end
|
33
|
+
send("#{name}=", values)
|
34
|
+
end
|
35
|
+
rescue => ex
|
36
|
+
errors << ActiveRecord::AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
unless errors.empty?
|
40
|
+
error_descriptions = errors.map(&:message).join(",")
|
41
|
+
raise ActiveRecord::MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def extract_callstack_for_multiparameter_attributes(pairs)
|
46
|
+
attributes = {}
|
47
|
+
|
48
|
+
pairs.each do |(multiparameter_name, value)|
|
49
|
+
attribute_name = multiparameter_name.split("(").first
|
50
|
+
attributes[attribute_name] ||= {}
|
51
|
+
|
52
|
+
parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
|
53
|
+
attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
|
54
|
+
end
|
55
|
+
|
56
|
+
attributes
|
57
|
+
end
|
58
|
+
|
59
|
+
def type_cast_attribute_value(multiparameter_name, value)
|
60
|
+
multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
|
61
|
+
end
|
62
|
+
|
63
|
+
def find_parameter_position(multiparameter_name)
|
64
|
+
multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module OnForm
|
2
|
+
module Saving
|
3
|
+
def self.included(base)
|
4
|
+
base.define_model_callbacks :save
|
5
|
+
end
|
6
|
+
|
7
|
+
def transaction(&block)
|
8
|
+
with_transactions(backing_models, &block)
|
9
|
+
end
|
10
|
+
|
11
|
+
def invalid?
|
12
|
+
!valid?
|
13
|
+
end
|
14
|
+
|
15
|
+
def save!
|
16
|
+
reset_errors
|
17
|
+
transaction do
|
18
|
+
reset_errors
|
19
|
+
unless run_validations!(backing_model_validations: false)
|
20
|
+
raise ActiveModel::ValidationError, self
|
21
|
+
end
|
22
|
+
run_callbacks :save do
|
23
|
+
begin
|
24
|
+
backing_models.each { |backing_model| backing_model.save! }
|
25
|
+
rescue ActiveRecord::RecordInvalid, ActiveModel::ValidationError
|
26
|
+
collect_errors
|
27
|
+
raise
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
def save
|
35
|
+
save!
|
36
|
+
rescue ActiveRecord::RecordInvalid, ActiveModel::ValidationError
|
37
|
+
false
|
38
|
+
end
|
39
|
+
|
40
|
+
def update(attributes)
|
41
|
+
transaction do
|
42
|
+
self.attributes = attributes
|
43
|
+
save
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def update!(attributes)
|
48
|
+
transaction do
|
49
|
+
self.attributes = attributes
|
50
|
+
save!
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
alias :update_attributes :update
|
55
|
+
alias :update_attributes! :update!
|
56
|
+
|
57
|
+
private
|
58
|
+
def with_transactions(models, &block)
|
59
|
+
if models.empty?
|
60
|
+
block.call
|
61
|
+
else
|
62
|
+
models.shift.transaction do
|
63
|
+
with_transactions(models, &block)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def run_validations!(backing_model_validations: true)
|
69
|
+
super()
|
70
|
+
run_backing_model_validations if backing_model_validations
|
71
|
+
errors.empty?
|
72
|
+
end
|
73
|
+
|
74
|
+
def run_backing_model_validations
|
75
|
+
backing_models.collect { |backing_model| backing_model.valid? }
|
76
|
+
collect_errors
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/on_form.rb
ADDED
data/on_form.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'on_form/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "on_form"
|
8
|
+
spec.version = OnForm::VERSION
|
9
|
+
spec.authors = ["Will Bryant"]
|
10
|
+
spec.email = ["will.bryant@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{A pragmatism-first library to help Rails applications migrate from complex nested attribute models to tidy form objects.}
|
13
|
+
spec.description = %q{Our goal is that you can migrate large forms to OnForm incrementally, without having to refactor large amounts of code in a single release.}
|
14
|
+
spec.homepage = "https://github.com/powershop/on_form"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_dependency "activemodel", ENV["RAILS_VERSION"]
|
23
|
+
spec.add_dependency "activerecord", ENV["RAILS_VERSION"]
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.12"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "sqlite3"
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: on_form
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Will Bryant
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-09-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activemodel
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activerecord
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.12'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.12'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Our goal is that you can migrate large forms to OnForm incrementally,
|
84
|
+
without having to refactor large amounts of code in a single release.
|
85
|
+
email:
|
86
|
+
- will.bryant@gmail.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- Gemfile
|
93
|
+
- LICENSE.txt
|
94
|
+
- README.md
|
95
|
+
- Rakefile
|
96
|
+
- bin/console
|
97
|
+
- bin/setup
|
98
|
+
- lib/on_form.rb
|
99
|
+
- lib/on_form/attributes.rb
|
100
|
+
- lib/on_form/errors.rb
|
101
|
+
- lib/on_form/form.rb
|
102
|
+
- lib/on_form/multiparameter_attributes.rb
|
103
|
+
- lib/on_form/saving.rb
|
104
|
+
- lib/on_form/version.rb
|
105
|
+
- on_form.gemspec
|
106
|
+
homepage: https://github.com/powershop/on_form
|
107
|
+
licenses:
|
108
|
+
- MIT
|
109
|
+
metadata: {}
|
110
|
+
post_install_message:
|
111
|
+
rdoc_options: []
|
112
|
+
require_paths:
|
113
|
+
- lib
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
requirements: []
|
125
|
+
rubyforge_project:
|
126
|
+
rubygems_version: 2.5.1
|
127
|
+
signing_key:
|
128
|
+
specification_version: 4
|
129
|
+
summary: A pragmatism-first library to help Rails applications migrate from complex
|
130
|
+
nested attribute models to tidy form objects.
|
131
|
+
test_files: []
|