levee 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +328 -0
- data/Rakefile +2 -0
- data/levee.gemspec +27 -0
- data/lib/levee/builder.rb +230 -0
- data/lib/levee/version.rb +9 -0
- data/lib/levee.rb +3 -0
- metadata +103 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ac51414d35dc2d20439b9b06d0d7f82b84594fc2
|
4
|
+
data.tar.gz: 5a8172b7e1bd55767a34ebbb498cb832469d7a1f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6493cba2bbe24d3b5364e0c784aeeb4c789ba7bf329fba3622c2d0e5d84fc5f2e664b8f2f22f5465549ca269a964f548dd69d709c25f6c84452c36c4f38e00f4
|
7
|
+
data.tar.gz: ac03887aac3e66fbf540e6a6121323e39bbe30b78316b5b7b6b9a7066dafcb009e1ef1c46c6131940af19972e35830bd46e179131bce968bd97532cf657131dc
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Mike Martinson
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,328 @@
|
|
1
|
+
# Levee
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'levee'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install levee
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Overview
|
24
|
+
------------------------
|
25
|
+
|
26
|
+
The abstract builder and validator classes are abstract classes for concrete builder classes to inherit from. They will be extracted to a gem once they are imporved and generalized through this project.
|
27
|
+
|
28
|
+
The purpose of the builder object is to create a layer of abstraction between the controller and models in a Rails application. The builder is particularly useful for receiving complex post and put requests with multiple parameters, but is lightweight enough to use for simple writes when some filtering or parameter combination validation might be useful before writing to the database. Since it wraps the entire write action to mulitple models in a single transaction, any failure in the builder will result in the entire request being rolled back.
|
29
|
+
|
30
|
+
|
31
|
+
Features
|
32
|
+
----------------------
|
33
|
+
|
34
|
+
|
35
|
+
- Entire build action wrapped in transaction by default
|
36
|
+
- Automatic rollback if errors present
|
37
|
+
- Lightwight, flexible DSL that can be easily overridden or extended
|
38
|
+
- Macro-style validators
|
39
|
+
- Transaction-level callbacks
|
40
|
+
- Automatic generation of errors object
|
41
|
+
- Mass-assignment protection, parameter whitelisting outside of controller
|
42
|
+
- Find-or-instantiate based on class name inference
|
43
|
+
|
44
|
+
|
45
|
+
|
46
|
+
The TL;DR copy & paste version:
|
47
|
+
---------------------------------
|
48
|
+
|
49
|
+
|
50
|
+
Save this somewhere in your app directory. Maybe in a builders folder?
|
51
|
+
|
52
|
+
|
53
|
+
class PostBuilder < BaseBuilder
|
54
|
+
#matches the class name
|
55
|
+
|
56
|
+
#make sure you list all params that are passed in (you can skip id if you want)
|
57
|
+
attributes :title,
|
58
|
+
:content,
|
59
|
+
:tweet_required,
|
60
|
+
:author,
|
61
|
+
:comment
|
62
|
+
|
63
|
+
#list as many before_save and after_save callbacks as you want
|
64
|
+
after_save :tweet_post
|
65
|
+
|
66
|
+
#choose a validator class to run automatically (must be a legit base validator)
|
67
|
+
validator PostParamsValidator
|
68
|
+
|
69
|
+
#access the model using #object
|
70
|
+
#this method is completely redundant
|
71
|
+
#it is what is called if you don't define a method for a listed attribute
|
72
|
+
def title(data)
|
73
|
+
object.title = data
|
74
|
+
end
|
75
|
+
|
76
|
+
#override the automatic writer so it doesn't try to set it on object
|
77
|
+
#needs to take exactly one parameter
|
78
|
+
def tweet_required(data); end
|
79
|
+
|
80
|
+
|
81
|
+
#use #delayed_save be be sure the object is saved after the parent object
|
82
|
+
def comment(data)
|
83
|
+
comment = Comment.new(content: data, author: current_user)
|
84
|
+
object.comments << comment
|
85
|
+
delayed_save!(comment)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
#get data that you passed in in the builder_options hash
|
91
|
+
def current_user
|
92
|
+
@current_user ||= User.find(builder_options[:user_id])
|
93
|
+
end
|
94
|
+
|
95
|
+
#access the params you passed in anywhere you need to
|
96
|
+
def tweet_post
|
97
|
+
TwitterMachine.spam(object) if params[:tweet_required]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
######These are the methods you have access to inside the builder:
|
103
|
+
|
104
|
+
:params
|
105
|
+
:errors #Array
|
106
|
+
:object #ActiveRecord::Base
|
107
|
+
:permitted_attributes #Array
|
108
|
+
:requires_save #Boolean
|
109
|
+
:builder_options #Hash
|
110
|
+
|
111
|
+
|
112
|
+
|
113
|
+
|
114
|
+
The Full Deets on What is Going on Here:
|
115
|
+
------------------------------------
|
116
|
+
|
117
|
+
|
118
|
+
The API is for the builder is intended to closely resemble that of ActiveModel::Serializers, as the builder is used in a way similar way but for parsing data into instead of serializing data out of Rails models.
|
119
|
+
|
120
|
+
|
121
|
+
|
122
|
+
Creating a Builder
|
123
|
+
==================================
|
124
|
+
|
125
|
+
|
126
|
+
Each builder class inherits from BaseBuilder and maps directly onto one ActiveRecord model. The model name is inferred from the builder name, so for a Post model builder must be named PostBuilder. Class inference works with namespaced builders as well, leveraging the magic of regex voodoo. Currently there is no way to use a builder with a non-matching model.
|
127
|
+
|
128
|
+
|
129
|
+
###Attribute Mapping and Whitelisting
|
130
|
+
|
131
|
+
|
132
|
+
All parameters that are used within the builder must be explicitly whitelisted as attributes. This can either provide super-reduntant mass-assignment protection on top of the Rails strong params or can be used in place of them to stop param whitelisting from ruining your short controller zen. Because the builder iteratively assigns each parmeter using the attribute writers you can pass paremters from the controller without whitelisting them there if you prefer. See 'Using the Builder' below for more on how to work with the rails params hash.
|
133
|
+
|
134
|
+
By default, the builder will attempt to map every parameter onto the attribute writer on the target model with the same name. If parameters are passed that are not whitelisted an exception will be raised. In order to implement custom behaviour for certain parmeters, methods written inside the class will be called when they match the name of whitelisted attribute that receives data from the params. These overwriter methods must take one parameter, which will hold the value of the submitted parameter. When an automatic attribute method is overridden by a custom method, the return value of the method does not map to the matching attribute writer
|
135
|
+
|
136
|
+
|
137
|
+
Let me say that again. The return value of custom methods does not automatically map to the matchiang attribute writer. Anytime a default mapping is overridden, you are left with full flexibility to do what you want with the parameter value, including throw it away. This allows to builder to utilize parameters that were never intended to be written straight as model data. For parameter values that you want to throw away or handle elsewhere inside the builder, simply define a for the parameter key name that does nothing. To make it explicit that it is intentionally empty, write in like this: ``def parameter_key_name(value); end``
|
138
|
+
|
139
|
+
|
140
|
+
#####A note on :id
|
141
|
+
|
142
|
+
The id key is the only key in the params hash that does not need to be whitelisted and will not be mapped by default. It can still be overridden if you want to catch the id and do someting with it, or if you want to cue some other action just before the attribute mapping executes.
|
143
|
+
|
144
|
+
|
145
|
+
###The object Object
|
146
|
+
|
147
|
+
|
148
|
+
As in ActiveModel::Serializers, the builder makes use of the object object within the class. This allows for code that easily reusable between create and update requests. Behind the scenes the base builder uses the id the the params hash to look up the existing object form the inferred model and represents it as object. If no :id parameter exists a new empty object of that class is instantiated and assigned to object. Buidler methods then don't need to know or care about whether object is new or existing and can treat it the same. In the cases where this might be important, you are of course free to dig into object and ask it all sorts of ActiveRecord questions about its state inside your overwriter methods.
|
149
|
+
|
150
|
+
|
151
|
+
Just like you use object in your serializer methods to query things about the object. Use object in your builder methods to call methods on objects or set values. This can be super useful when you want to so something like initiate changes to a state machine while maintaining a RESTful web interface. Changes to state can be included in an update action, then the builder can check for the paremeters used for initiating a state change and call methods on object (or anywhere else really). Once again, the return values of the builder methods are thrown away and are completely unimportant.
|
152
|
+
|
153
|
+
|
154
|
+
Using the Builder
|
155
|
+
======================================
|
156
|
+
|
157
|
+
|
158
|
+
Currently the builder name is not inferred from the controller name and it msut be called explicitly in the controller. On initialization the builder takes the params as a mandatory first argument for parmas, a number of optional keyword arguments and an optional block. The block is used to quickly assign a single :after_save callback from data that is available in the controller.
|
159
|
+
|
160
|
+
The builder does not expect to get the entire params hash in the controller but instead wants a hash or array with no root node. When making a new post, for example, you would call ``PostBuilder.new(params[:post])``. This means that you have the choice of whether you whitelist in the controller using Rails strong params or pass the raw params in. The builder will force you to whitelist them inside regardless.
|
161
|
+
|
162
|
+
The builder will throw an exception on initialization if you pass it something besides an array or hash as the params, or if the params have a root node matching the class name. It won't creating the regular errors hash, because the client shouldn't be exposed to this error message. Any extra stuff in the params should be filtered out by the controller.
|
163
|
+
|
164
|
+
|
165
|
+
|
166
|
+
###The Builder#build method
|
167
|
+
|
168
|
+
|
169
|
+
The build method fires the the entire builder for both create and update. Two other public methods are also available. #build_nested will perform the build without saving. In doing so it completely skips the callbacks so use it carefully. #update is used for explicitly passing in :object_id at initialization that will take precedence over any id value that is included in the parameters. This can be used if you watch to ensure that an id from the query string params is used instead of the request body.
|
170
|
+
|
171
|
+
|
172
|
+
The build method returns either the ActiveRecord object or an errors hash. Expected errors will be rescued and added to the errors hash, where unexpected errors will be raised. In both cases the entire build transaction will be rolled back.
|
173
|
+
|
174
|
+
|
175
|
+
|
176
|
+
|
177
|
+
Moar Features
|
178
|
+
======================================
|
179
|
+
|
180
|
+
|
181
|
+
###Callbacks
|
182
|
+
|
183
|
+
|
184
|
+
You can list macro-style callbacks inside the class just like in your ActiveRecord models.
|
185
|
+
|
186
|
+
class PostBuilder
|
187
|
+
|
188
|
+
attributes :title,
|
189
|
+
:content,
|
190
|
+
:author
|
191
|
+
|
192
|
+
before_save :update_state
|
193
|
+
|
194
|
+
after_save :add_email_job,
|
195
|
+
:count_records
|
196
|
+
|
197
|
+
def update_state
|
198
|
+
end
|
199
|
+
|
200
|
+
def add_email_job
|
201
|
+
end
|
202
|
+
|
203
|
+
#you get the idea
|
204
|
+
|
205
|
+
end
|
206
|
+
|
207
|
+
Just be sure you define the methods you list somewhere within the class, otherwise you get an explosion. An important point to remember is that callbacks ARE NOT CALLED EVER if you use the #build_nested instead of the #build method.
|
208
|
+
|
209
|
+
|
210
|
+
### #delayed_save(object)
|
211
|
+
|
212
|
+
|
213
|
+
|
214
|
+
The builder implements #delayed_saved(object) to be used inside a builder class. This is a shortcut for calling save! on any object as part of the after save callbacks. It is useful for when you are creating nested objects inside a builder and you want to be sure that the parent object is saved first, as is the case with associations where you need to be sure the parent has all attributes in place to pass validattion before the child forces a save. Note that delayed_save callbacks fire before the rest of the after_save callbacks.
|
215
|
+
|
216
|
+
|
217
|
+
class PostBuilder
|
218
|
+
|
219
|
+
attributes :title,
|
220
|
+
:content,
|
221
|
+
:author
|
222
|
+
:comment
|
223
|
+
|
224
|
+
def comment(data)
|
225
|
+
new_comment = Comment.new(content: data)
|
226
|
+
object.comments < new_comment
|
227
|
+
delayed_save!(new_comment)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
|
232
|
+
###Builder Options
|
233
|
+
|
234
|
+
|
235
|
+
You probably have situations where you want to get stuff from the controller inside the builder that doesn't come in the params. You can include any number of keyword arguments after the parameters argument at initialization that will be available inside the builder in the ``builder options`` hash.
|
236
|
+
|
237
|
+
|
238
|
+
When you make the builder like this in the contoller: ``PostBuilder.new(params[:post], user: current_user).build``
|
239
|
+
|
240
|
+
|
241
|
+
Inside the builder you can get the current user by calling ``builder_options[:current_user]``
|
242
|
+
|
243
|
+
|
244
|
+
This way you can easily pass in any extra query string stuff you get in the params. It's recommended that you perform any lookups inside a method in the builder instead of in the controller when you don't need to reuse them for anything out there. For example, if you only need the current user in the builder, pass in the user_id from the query string params and define a method like this in your builder:
|
245
|
+
|
246
|
+
|
247
|
+
def user
|
248
|
+
@user ||= User.find(builder_options[:user_id])
|
249
|
+
end
|
250
|
+
|
251
|
+
|
252
|
+
That way the controller stays nice and snappy and the builder has a reusable method that's not 30 chars long.
|
253
|
+
|
254
|
+
|
255
|
+
###Validator
|
256
|
+
|
257
|
+
|
258
|
+
You can make a validator class to be used with your builder. It's a good place to do validations that check for certain combinations of parameters, especially if they affect multiple models and therefore don't really make sense to have in one model class or the other. The validator uses only a tiny bit of custom syntax, and it's really just there to make your life easier.
|
259
|
+
|
260
|
+
|
261
|
+
#####Make the validator:
|
262
|
+
|
263
|
+
|
264
|
+
Make the class like this. You can put it anywhere in the app directory, but it feels very at home in a folder called validations. It doesn't use class inference so you can call it whatever
|
265
|
+
|
266
|
+
|
267
|
+
class UltraSweetValidator < BaseValidator
|
268
|
+
|
269
|
+
validations :first_method,
|
270
|
+
:other_method
|
271
|
+
|
272
|
+
def first_method
|
273
|
+
if params[:a_thing]
|
274
|
+
return true if params[:that_other_thing_that_has_to_be_there]
|
275
|
+
else
|
276
|
+
message "You can't do that"
|
277
|
+
add_invalid_request_error(message)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def other_method
|
282
|
+
#return value doesn't matter
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
|
287
|
+
Just list all the validation methods you want at the top and then define them below. The builder knows how to call them. The validator has access to two useful objects and has one useful custom method. You can, obviously, access the params that you passed into the builder (not the ones in the controller), and you can also access the errors hash.
|
288
|
+
|
289
|
+
|
290
|
+
When you find something you don't like, you can either use the super userful #add_invalid_request_error(message) method, or you can just add whatever you want to the errors hash. If the errors hash is anyting but completely empty the buidler transaction will roll back and return whatever is in there to the controller. The method just adds your message there is a nice formatted way and includes a 400 'bad request' status code. If you want to skip the the rest of the validations as soon as one fails just add a bang (!) to the end of the method. Don't add it after the message argument, that would obviously be wrong.
|
291
|
+
|
292
|
+
|
293
|
+
#####Use the validator:
|
294
|
+
|
295
|
+
|
296
|
+
Call the validator inside the builder by listing the class name
|
297
|
+
|
298
|
+
|
299
|
+
class PostBuilder < BaseBuilder
|
300
|
+
|
301
|
+
attributes :title,
|
302
|
+
:content,
|
303
|
+
:author
|
304
|
+
:comment
|
305
|
+
|
306
|
+
validator UltraSweetValidator
|
307
|
+
|
308
|
+
end
|
309
|
+
|
310
|
+
|
311
|
+
That's it. The validator is called at the very beginning of the build method (and the other similar ones) and if it snags any errors in the errors hash it will return them instead of trying to run the rest of the build action. If all goes well, this will give you a way to stop your builder from exploding because it's missing data it expects to be there. The validators are there to keep messy error handling out of the builders. You should definitely use them.
|
312
|
+
|
313
|
+
|
314
|
+
Don't bother validating simple params in your validator that map straight onto the model attributes. The builder always calls #save! instead of #save and then catches the errors so you can jsut use your regular old ActiveRecord validators and still have a errors hash at the end.
|
315
|
+
|
316
|
+
|
317
|
+
|
318
|
+
|
319
|
+
|
320
|
+
TODO: Write usage instructions here
|
321
|
+
|
322
|
+
## Contributing
|
323
|
+
|
324
|
+
1. Fork it ( https://github.com/[my-github-username]/levee/fork )
|
325
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
326
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
327
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
328
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/levee.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 'levee/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "levee"
|
8
|
+
spec.version = Levee::VERSION
|
9
|
+
spec.authors = ["Mike Martinson"]
|
10
|
+
spec.email = ["mike.j.martinson@gmail.com"]
|
11
|
+
spec.summary = %q{Flexible builder template for mapping complex rails post params onto ActiveRecord models}
|
12
|
+
spec.description = Levee.gem_description
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_runtime_dependency 'activerecord', '~> 4.0'
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
end
|
26
|
+
|
27
|
+
|
@@ -0,0 +1,230 @@
|
|
1
|
+
module Levee
|
2
|
+
class Builder
|
3
|
+
|
4
|
+
attr_accessor :params,
|
5
|
+
:errors,
|
6
|
+
:object,
|
7
|
+
:nested_objects_to_save,
|
8
|
+
:permitted_attributes,
|
9
|
+
:requires_save,
|
10
|
+
:builder_options
|
11
|
+
|
12
|
+
def initialize(params, options={}, &blk)
|
13
|
+
self.params = params
|
14
|
+
validate_params
|
15
|
+
self.errors = []
|
16
|
+
self.nested_objects_to_save = []
|
17
|
+
self.permitted_attributes = [:id]
|
18
|
+
self.requires_save = true
|
19
|
+
self.builder_options = options
|
20
|
+
@callback_blocks = [*blk]
|
21
|
+
end
|
22
|
+
|
23
|
+
def build
|
24
|
+
unless params.is_a? Array
|
25
|
+
self.object = object_class.find_by(id: params[:id]) || object_class.new
|
26
|
+
end
|
27
|
+
assign_parameters_in_transaction
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_nested(parent_object: nil, parent_builder: nil)
|
31
|
+
self.requires_save = false
|
32
|
+
build
|
33
|
+
end
|
34
|
+
|
35
|
+
def update(object_id:)
|
36
|
+
self.object = object_class.find_by_id(object_id)
|
37
|
+
return {error_status: 404, errors:[{status: 404, code: 'record_not_found'}]} unless object
|
38
|
+
assign_parameters_in_transaction
|
39
|
+
end
|
40
|
+
|
41
|
+
def permitted_attributes
|
42
|
+
self.class._permitted_attributes || []
|
43
|
+
end
|
44
|
+
|
45
|
+
def validator
|
46
|
+
return nil unless self.class._validator
|
47
|
+
@validator ||= self.class._validator.new(params)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def assign_parameters_in_transaction
|
53
|
+
ActiveRecord::Base.transaction do
|
54
|
+
begin
|
55
|
+
perform_in_transaction
|
56
|
+
rescue => e
|
57
|
+
raise_error = -> { raise e }
|
58
|
+
rescue_errors(e) || raise_error.call
|
59
|
+
ensure
|
60
|
+
self.errors = (errors << validator.errors).flatten if validator
|
61
|
+
raise ActiveRecord::Rollback unless errors.flatten.empty?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
Rails.logger.error "Builder Errors:"
|
65
|
+
Rails.logger.error errors.to_yaml
|
66
|
+
self.object = object.reload if errors.empty? && object.try(:persisted?)
|
67
|
+
errors.empty? ? object : {errors: errors, error_status: errors.first[:status]}
|
68
|
+
end
|
69
|
+
|
70
|
+
def perform_in_transaction
|
71
|
+
self.errors += validator.validate_params.errors if validator
|
72
|
+
return false if errors.any?
|
73
|
+
self.object = top_level_array || call_setter_for_each_param_key
|
74
|
+
return true unless requires_save && !top_level_array
|
75
|
+
before_save_callbacks.each { |callback| send(callback) }
|
76
|
+
[*object].each(&:save!)
|
77
|
+
nested_objects_to_save.flatten.each(&:save!)
|
78
|
+
@callback_blocks.each_with_object(object, &:call)
|
79
|
+
after_save_callbacks.each { |callback| send(callback) }
|
80
|
+
end
|
81
|
+
|
82
|
+
def call_setter_for_each_param_key
|
83
|
+
params.each do |key, value|
|
84
|
+
send_if_key_included_in_attributes(key.to_sym, value)
|
85
|
+
end
|
86
|
+
object
|
87
|
+
end
|
88
|
+
|
89
|
+
def send_if_key_included_in_attributes(key, value)
|
90
|
+
if permitted_attributes.include?(key)
|
91
|
+
self.send(key, value)
|
92
|
+
else
|
93
|
+
errors << {status: 400, message: "Unpermitted parameter key #{key}"}
|
94
|
+
raise ActiveRecord::Rollback
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def top_level_array
|
99
|
+
return false unless params.is_a?(Array)
|
100
|
+
@top_level_array ||= params.map do |param_object|
|
101
|
+
self.class.new(param_object, builder_options).build_nested
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
#need to use attributes to handle errors that aren't meant to be model methods
|
106
|
+
def method_missing(method_name, *args)
|
107
|
+
return super unless permitted_attributes.include?(method_name)
|
108
|
+
begin
|
109
|
+
object.send(:"#{method_name}=", args.first)
|
110
|
+
rescue => e
|
111
|
+
if params.has_key?(method_name)
|
112
|
+
message = "Unable to process value for :#{method_name}, no attribute writer. Be sure to override the automatic setters for all params that do not map straight to a model attribute."
|
113
|
+
self.errors << {status: 422, message: message}
|
114
|
+
else
|
115
|
+
raise e
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def delayed_save!(nested_object)
|
121
|
+
self.nested_objects_to_save = (nested_objects_to_save << nested_object).uniq
|
122
|
+
end
|
123
|
+
|
124
|
+
def object_class
|
125
|
+
klass = self.class.to_s
|
126
|
+
start_position = (/::\w+$/ =~ klass).try(:+,2)
|
127
|
+
klass = klass[start_position..-1] if start_position
|
128
|
+
suffix_position = klass =~ /Builder$/
|
129
|
+
if suffix_position
|
130
|
+
try_constant = klass[0...suffix_position]
|
131
|
+
begin
|
132
|
+
try_constant.constantize
|
133
|
+
rescue
|
134
|
+
raise NameError.new "#{try_constant} does not exist. Builder class name must map the the name of an existing class"
|
135
|
+
end
|
136
|
+
else
|
137
|
+
raise "#{self.class} must be named ModelNameBuilder to be used as a builder class"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def rescue_errors(rescued_error)
|
142
|
+
raise_if_validation_error(rescued_error)
|
143
|
+
raise_if_argument_error(rescued_error)
|
144
|
+
raise_if_unknown_attribute_error(rescued_error)
|
145
|
+
end
|
146
|
+
|
147
|
+
def raise_if_validation_error(rescued_error)
|
148
|
+
if rescued_error.is_a? ActiveRecord::RecordInvalid
|
149
|
+
self.errors << { status: 422, code: 'validation_error', message: object.errors.full_messages, record: rescued_error.record }
|
150
|
+
raise ActiveRecord::Rollback
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def raise_if_argument_error(rescued_error)
|
155
|
+
if rescued_error.is_a? ArgumentError
|
156
|
+
message = "All methods on the builder that override attribute setters must accept one argument to catch the parameter value"
|
157
|
+
self.errors << { status: 500, code: 'builder_error', message: message }
|
158
|
+
raise ArgumentError.new message
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def raise_if_unknown_attribute_error(rescued_error)
|
163
|
+
if rescued_error.is_a? ActiveRecord::UnknownAttributeError
|
164
|
+
self.errors << { status: 400, code: 'unknown_attribute_error', message: rescued_error.message, record: rescued_error.record }
|
165
|
+
raise ActiveRecord::Rollback
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.attributes(*args)
|
170
|
+
@permitted_attributes ||= []
|
171
|
+
args.each { |a| @permitted_attributes << a unless @permitted_attributes.include?(a) }
|
172
|
+
end
|
173
|
+
|
174
|
+
def self._permitted_attributes
|
175
|
+
@permitted_attributes
|
176
|
+
end
|
177
|
+
|
178
|
+
def validate_params
|
179
|
+
message = "Params passed to builder must be a hash or top level array"
|
180
|
+
raise message unless params.respond_to?(:fetch)
|
181
|
+
return true if params.is_a? Array
|
182
|
+
message = "Params passed to builder must not have a root node"
|
183
|
+
key = params.keys.first
|
184
|
+
raise message if key && key.to_s.camelize == object_class.to_s
|
185
|
+
end
|
186
|
+
|
187
|
+
#used so that id setter is not called by default
|
188
|
+
def id(data)
|
189
|
+
true
|
190
|
+
end
|
191
|
+
|
192
|
+
#######################
|
193
|
+
## callbacks
|
194
|
+
###########################
|
195
|
+
|
196
|
+
def self.before_save(*args)
|
197
|
+
@before_save_callbacks ||= []
|
198
|
+
args.each { |a| @before_save_callbacks << a unless @before_save_callbacks.include?(a) }
|
199
|
+
end
|
200
|
+
|
201
|
+
def self._before_save_callbacks
|
202
|
+
@before_save_callbacks
|
203
|
+
end
|
204
|
+
|
205
|
+
def self._after_save_callbacks
|
206
|
+
@after_save_callbacks
|
207
|
+
end
|
208
|
+
|
209
|
+
def self.after_save(*args)
|
210
|
+
@after_save_callbacks ||= []
|
211
|
+
args.each { |a| @after_save_callbacks << a unless @after_save_callbacks.include?(a) }
|
212
|
+
end
|
213
|
+
|
214
|
+
def before_save_callbacks
|
215
|
+
self.class._before_save_callbacks || []
|
216
|
+
end
|
217
|
+
|
218
|
+
def after_save_callbacks
|
219
|
+
self.class._after_save_callbacks || []
|
220
|
+
end
|
221
|
+
|
222
|
+
def self.validator(validator)
|
223
|
+
@validator = validator
|
224
|
+
end
|
225
|
+
|
226
|
+
def self._validator
|
227
|
+
@validator
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Levee
|
2
|
+
extend self
|
3
|
+
|
4
|
+
VERSION = "0.0.1"
|
5
|
+
|
6
|
+
def gem_description
|
7
|
+
%Q(The purpose of the builder object is to create a layer of abstraction between the controller and models in a Rails application. The builder is particularly useful for receiving complex post and put requests with multiple parameters, but is lightweight enough to use for simple writes when some filtering or parameter combination validation might be useful before writing to the database. Since it wraps the entire write action to mulitple models in a single transaction, any failure in the builder will result in the entire request being rolled back.)
|
8
|
+
end
|
9
|
+
end
|
data/lib/levee.rb
ADDED
metadata
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: levee
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mike Martinson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-03-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
description: The purpose of the builder object is to create a layer of abstraction
|
56
|
+
between the controller and models in a Rails application. The builder is particularly
|
57
|
+
useful for receiving complex post and put requests with multiple parameters, but
|
58
|
+
is lightweight enough to use for simple writes when some filtering or parameter
|
59
|
+
combination validation might be useful before writing to the database. Since it
|
60
|
+
wraps the entire write action to mulitple models in a single transaction, any failure
|
61
|
+
in the builder will result in the entire request being rolled back.
|
62
|
+
email:
|
63
|
+
- mike.j.martinson@gmail.com
|
64
|
+
executables: []
|
65
|
+
extensions: []
|
66
|
+
extra_rdoc_files: []
|
67
|
+
files:
|
68
|
+
- ".gitignore"
|
69
|
+
- Gemfile
|
70
|
+
- LICENSE.txt
|
71
|
+
- README.md
|
72
|
+
- Rakefile
|
73
|
+
- levee.gemspec
|
74
|
+
- lib/levee.rb
|
75
|
+
- lib/levee/builder.rb
|
76
|
+
- lib/levee/version.rb
|
77
|
+
homepage: ''
|
78
|
+
licenses:
|
79
|
+
- MIT
|
80
|
+
metadata: {}
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
requirements: []
|
96
|
+
rubyforge_project:
|
97
|
+
rubygems_version: 2.4.5
|
98
|
+
signing_key:
|
99
|
+
specification_version: 4
|
100
|
+
summary: Flexible builder template for mapping complex rails post params onto ActiveRecord
|
101
|
+
models
|
102
|
+
test_files: []
|
103
|
+
has_rdoc:
|