decanter 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +89 -0
- data/LICENSE.txt +21 -0
- data/README.md +370 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/decanter.gemspec +35 -0
- data/lib/decanter.rb +23 -0
- data/lib/decanter/base.rb +7 -0
- data/lib/decanter/core.rb +106 -0
- data/lib/decanter/extensions.rb +34 -0
- data/lib/decanter/value_parser.rb +18 -0
- data/lib/decanter/value_parser/base.rb +8 -0
- data/lib/decanter/value_parser/boolean_parser.rb +16 -0
- data/lib/decanter/value_parser/core.rb +16 -0
- data/lib/decanter/value_parser/date_parser.rb +13 -0
- data/lib/decanter/value_parser/string_parser.rb +9 -0
- data/lib/decanter/version.rb +3 -0
- metadata +122 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a88b2e53932e63a9351db615b274f997a829bf40
|
4
|
+
data.tar.gz: 4deade7c29aa9321d62600ba8b2fa7da8a207ace
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3f7aa0df7b97a57191b74a9c8d46572d35fcbf1e1ab2447bb54fbabcb636e5f8274e32480e478bb21e10be846f82643a416f3e39682ad3eda4e4082f0d11055d
|
7
|
+
data.tar.gz: 6fcdc9bfb48de54cfb710a71137285067144b8d639be4ffb6bf939cfd9c8439aae1c70fb247b26990d5d3ce2621c8301ded78c5dbeb0a74772202b87457a0ed6
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
decanter (0.5.0)
|
5
|
+
activesupport
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
actionpack (4.2.4)
|
11
|
+
actionview (= 4.2.4)
|
12
|
+
activesupport (= 4.2.4)
|
13
|
+
rack (~> 1.6)
|
14
|
+
rack-test (~> 0.6.2)
|
15
|
+
rails-dom-testing (~> 1.0, >= 1.0.5)
|
16
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
17
|
+
actionview (4.2.4)
|
18
|
+
activesupport (= 4.2.4)
|
19
|
+
builder (~> 3.1)
|
20
|
+
erubis (~> 2.7.0)
|
21
|
+
rails-dom-testing (~> 1.0, >= 1.0.5)
|
22
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
23
|
+
activesupport (4.2.4)
|
24
|
+
i18n (~> 0.7)
|
25
|
+
json (~> 1.7, >= 1.7.7)
|
26
|
+
minitest (~> 5.1)
|
27
|
+
thread_safe (~> 0.3, >= 0.3.4)
|
28
|
+
tzinfo (~> 1.1)
|
29
|
+
builder (3.2.2)
|
30
|
+
diff-lcs (1.2.5)
|
31
|
+
erubis (2.7.0)
|
32
|
+
i18n (0.7.0)
|
33
|
+
json (1.8.3)
|
34
|
+
loofah (2.0.3)
|
35
|
+
nokogiri (>= 1.5.9)
|
36
|
+
mini_portile (0.6.2)
|
37
|
+
minitest (5.8.2)
|
38
|
+
nokogiri (1.6.6.2)
|
39
|
+
mini_portile (~> 0.6.0)
|
40
|
+
rack (1.6.4)
|
41
|
+
rack-test (0.6.3)
|
42
|
+
rack (>= 1.0)
|
43
|
+
rails-deprecated_sanitizer (1.0.3)
|
44
|
+
activesupport (>= 4.2.0.alpha)
|
45
|
+
rails-dom-testing (1.0.7)
|
46
|
+
activesupport (>= 4.2.0.beta, < 5.0)
|
47
|
+
nokogiri (~> 1.6.0)
|
48
|
+
rails-deprecated_sanitizer (>= 1.0.1)
|
49
|
+
rails-html-sanitizer (1.0.2)
|
50
|
+
loofah (~> 2.0)
|
51
|
+
railties (4.2.4)
|
52
|
+
actionpack (= 4.2.4)
|
53
|
+
activesupport (= 4.2.4)
|
54
|
+
rake (>= 0.8.7)
|
55
|
+
thor (>= 0.18.1, < 2.0)
|
56
|
+
rake (10.4.2)
|
57
|
+
rspec-core (3.3.2)
|
58
|
+
rspec-support (~> 3.3.0)
|
59
|
+
rspec-expectations (3.3.1)
|
60
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
61
|
+
rspec-support (~> 3.3.0)
|
62
|
+
rspec-mocks (3.3.2)
|
63
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
64
|
+
rspec-support (~> 3.3.0)
|
65
|
+
rspec-rails (3.3.3)
|
66
|
+
actionpack (>= 3.0, < 4.3)
|
67
|
+
activesupport (>= 3.0, < 4.3)
|
68
|
+
railties (>= 3.0, < 4.3)
|
69
|
+
rspec-core (~> 3.3.0)
|
70
|
+
rspec-expectations (~> 3.3.0)
|
71
|
+
rspec-mocks (~> 3.3.0)
|
72
|
+
rspec-support (~> 3.3.0)
|
73
|
+
rspec-support (3.3.0)
|
74
|
+
thor (0.19.1)
|
75
|
+
thread_safe (0.3.5)
|
76
|
+
tzinfo (1.2.2)
|
77
|
+
thread_safe (~> 0.1)
|
78
|
+
|
79
|
+
PLATFORMS
|
80
|
+
ruby
|
81
|
+
|
82
|
+
DEPENDENCIES
|
83
|
+
bundler (~> 1.9)
|
84
|
+
decanter!
|
85
|
+
rake (~> 10.0)
|
86
|
+
rspec-rails
|
87
|
+
|
88
|
+
BUNDLED WITH
|
89
|
+
1.10.6
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 TODO: Write your name
|
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,370 @@
|
|
1
|
+
ParamsTransformer
|
2
|
+
===
|
3
|
+
|
4
|
+
Installation
|
5
|
+
---
|
6
|
+
|
7
|
+
Add Gem:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem "decanter"
|
11
|
+
```
|
12
|
+
|
13
|
+
```
|
14
|
+
bundle
|
15
|
+
```
|
16
|
+
|
17
|
+
|
18
|
+
Basic Usage
|
19
|
+
---
|
20
|
+
|
21
|
+
Create a class to process your form and have it inherit from ParamsTransformer::Base. I typically create the following folder to hold my form classes: app/classes/forms
|
22
|
+
|
23
|
+
**app/classes/forms/property.rb**
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
class Forms::Property < ParamsTransformer::Base
|
27
|
+
|
28
|
+
input :bedrooms, :integer
|
29
|
+
input :price, :float
|
30
|
+
input :has_air_conditioning, :boolean
|
31
|
+
input :has_parking, :boolean
|
32
|
+
input :has_laundry, :boolean
|
33
|
+
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
In your controller:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
def create
|
41
|
+
@form = Forms::Property.new(params)
|
42
|
+
|
43
|
+
if @form.save
|
44
|
+
redirect_to trips_path
|
45
|
+
else
|
46
|
+
set_index_variables
|
47
|
+
render "index"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
Inheritance
|
53
|
+
---
|
54
|
+
|
55
|
+
General pattern:
|
56
|
+
|
57
|
+
- forms/property.rb
|
58
|
+
- forms/property/create.rb (inherits from forms/property.rb)
|
59
|
+
- forms/property/update.rb (inherits from forms/property.rb)
|
60
|
+
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
# app/classes/forms/property.rb
|
64
|
+
class Forms::Property < ParamsTransformer::Base
|
65
|
+
|
66
|
+
input :bedrooms, :integer
|
67
|
+
input :price, :float
|
68
|
+
input :referred_by, :string
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
# app/classes/forms/property/create.rb
|
74
|
+
class Forms::Property::Create < Forms::Property
|
75
|
+
|
76
|
+
# override any input, validation, or method here
|
77
|
+
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
class Forms::Property::Update < Forms::Property
|
82
|
+
|
83
|
+
# override any input, validation, or method here
|
84
|
+
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
Associations
|
89
|
+
---
|
90
|
+
|
91
|
+
Let's say we are creating a web app where teacher's can create Courses with assignments and resources attached to the courses.
|
92
|
+
|
93
|
+
The modeling would look like:
|
94
|
+
|
95
|
+
class Course < ActiveRecord::Base
|
96
|
+
|
97
|
+
has_many :assignments
|
98
|
+
has_many :resources
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
|
104
|
+
Why a Library for Parsing Forms?
|
105
|
+
---
|
106
|
+
|
107
|
+
In my humble opinion, Rails is missing a tool for parsing forms on the backend. Currently the process looks something like this:
|
108
|
+
|
109
|
+
```html
|
110
|
+
# new.html.erb
|
111
|
+
<%= form_for @trip do |trip_builder| %>
|
112
|
+
<div class="field">
|
113
|
+
<%= trip_builder.label :miles %>
|
114
|
+
<%= trip_builder.text_field :miles, placeholder: '50 miles' %>
|
115
|
+
</div>
|
116
|
+
<% end %>
|
117
|
+
```
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
# trips_controller
|
121
|
+
|
122
|
+
def create
|
123
|
+
@trip = Trip.new(trip_params)
|
124
|
+
|
125
|
+
if @trip.save
|
126
|
+
redirect_to trips_path, notice: "New trip successfully created."
|
127
|
+
else
|
128
|
+
render "new"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
The problem with this approach is that there is nothing that processes the user inputs before being saved into the database. For example, if the user types "1,000 miles" into the form, Rails would store 0 instead of 1000 into our "miles" column.
|
134
|
+
|
135
|
+
The "Services" concept that many Rails developers favor is on point. Every form should have a corresponding Ruby class whose responsibility is to process the inputs and get the form ready for storage.
|
136
|
+
|
137
|
+
However, this "Services" concept does not have a supporting framework. It is repetitive for every developer, for example, to write code to parse a decimal field that comes into our Ruby controller as a string (as in the example above).
|
138
|
+
|
139
|
+
Here is how it may be done right now:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
class Forms::Trip
|
143
|
+
|
144
|
+
attr_accessor :trip
|
145
|
+
|
146
|
+
def initialize(args = {})
|
147
|
+
@trip = Trip.new
|
148
|
+
@trip.miles = parse_miles(args[:miles)
|
149
|
+
end
|
150
|
+
|
151
|
+
def parse_miles(miles_input)
|
152
|
+
regex = /(\d|[.])/ # only care about numbers and decimals
|
153
|
+
miles_input.scan(regex).join.try(:to_f)
|
154
|
+
end
|
155
|
+
|
156
|
+
def save
|
157
|
+
@trip.save
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
# trips_controller.rb
|
165
|
+
|
166
|
+
def create
|
167
|
+
form = Forms::Trip.new(trip_params)
|
168
|
+
@trip = form.trip
|
169
|
+
|
170
|
+
if form.save
|
171
|
+
redirect_to trips_path, notice: "New trip successfully created."
|
172
|
+
else
|
173
|
+
render "new"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
```
|
177
|
+
|
178
|
+
While this process is not so bad with only one field and one model, it gets more complex with many different fields, types of inputs, and especially with nested attributes.
|
179
|
+
|
180
|
+
A better solution is to have the form service classes inherit from a base "form parser" class that can handle the common parsing needs of the community. For example:
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
class Forms::Trip < FormParse::Base
|
184
|
+
|
185
|
+
input :miles, :float
|
186
|
+
|
187
|
+
def after_init(args)
|
188
|
+
@trip = Trip.new(to_hash)
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
```
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
# trips_controller.rb
|
196
|
+
# same as above
|
197
|
+
|
198
|
+
def create
|
199
|
+
form = Forms::Trip.new(trip_params)
|
200
|
+
@trip = form.trip
|
201
|
+
|
202
|
+
if form.save
|
203
|
+
redirect_to trips_path, notice: "New trip successfully created."
|
204
|
+
else
|
205
|
+
render "new"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
```
|
209
|
+
|
210
|
+
The FormParse::Base class that Forms::Trip inherits from by default performs the proper regex as seen in the original Forms::Trip service object above. We need only define the input key and the type of input and the base class takes care of the heavy lifting.
|
211
|
+
|
212
|
+
The "to_hash" method takes the inputs and converts the parsed values into a hash, which produces the following in this case:
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
{ miles: 1000.0 }
|
216
|
+
```
|
217
|
+
|
218
|
+
A more complex form would end up with a service class like below:
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
class Forms::Trip
|
222
|
+
|
223
|
+
attr_accessor :trip
|
224
|
+
|
225
|
+
def initialize(args = {})
|
226
|
+
@trip = Trip.new
|
227
|
+
@trip.miles = parse_miles(args[:miles)
|
228
|
+
@trip.origin_city = args[:origin_city]
|
229
|
+
@trip.origin_state = args[:origin_state]
|
230
|
+
@trip.departure_datetime = parse_datetime(args[:departure_datetime])
|
231
|
+
@trip.destination_city = args[:destination_city]
|
232
|
+
@trip.destination_state = args[:destination_state]
|
233
|
+
@trip.arrival_datetime = parse_datetime(args[:arrival_datetime])
|
234
|
+
end
|
235
|
+
|
236
|
+
def parse_miles(miles_input)
|
237
|
+
regex = /(\d|[.])/ # only care about numbers and decimals
|
238
|
+
miles_input.scan(regex).join.try(:to_f)
|
239
|
+
end
|
240
|
+
|
241
|
+
def parse_datetime(datetime_input)
|
242
|
+
Date.strptime(datetime, "%m/%d/%Y")
|
243
|
+
end
|
244
|
+
|
245
|
+
def save
|
246
|
+
@trip.save
|
247
|
+
end
|
248
|
+
|
249
|
+
end
|
250
|
+
```
|
251
|
+
|
252
|
+
With a framework, it would only involve the following:
|
253
|
+
|
254
|
+
```ruby
|
255
|
+
class Forms::Trip < FormParse::Base
|
256
|
+
|
257
|
+
input :miles, :float
|
258
|
+
input :origin_city, :string
|
259
|
+
input :origin_state, :string
|
260
|
+
input :departure_datetime, :datetime
|
261
|
+
input :destination_city, :string
|
262
|
+
input :destination_state, :string
|
263
|
+
input :arrival_datetime, :datetime
|
264
|
+
|
265
|
+
def after_init(args)
|
266
|
+
@trip = Trip.new(to_hash)
|
267
|
+
end
|
268
|
+
|
269
|
+
# to_hash produces:
|
270
|
+
# {
|
271
|
+
# miles: 1000.0,
|
272
|
+
# origin_city: "Chicago",
|
273
|
+
# origin_state: "IL",
|
274
|
+
# departure_datetime: # DateTime object
|
275
|
+
# destination_city: "New York",
|
276
|
+
# destination_state: "NY",
|
277
|
+
# arrival_datetime: # DateTime object
|
278
|
+
# }
|
279
|
+
|
280
|
+
end
|
281
|
+
```
|
282
|
+
|
283
|
+
Taking it a step further, if our form inputs are not database backed, we can even validate within our new service object like so:
|
284
|
+
|
285
|
+
```ruby
|
286
|
+
class FormParse::Base
|
287
|
+
include ActiveModel::Model
|
288
|
+
|
289
|
+
# ... base parsing code
|
290
|
+
end
|
291
|
+
|
292
|
+
class Form::Trip < FormParse::Base
|
293
|
+
|
294
|
+
input :miles, :float
|
295
|
+
input :origin_city, :string
|
296
|
+
input :origin_city, :string
|
297
|
+
input :destination_city, :string
|
298
|
+
input :destination_state, :string
|
299
|
+
|
300
|
+
validates :miles, :origin_city, :origin_state, :destination_city, :destination_state, presence: true
|
301
|
+
|
302
|
+
def after_init(args)
|
303
|
+
@trip = Trip.new(to_hash)
|
304
|
+
end
|
305
|
+
|
306
|
+
def save
|
307
|
+
if valid? && trip.valid?
|
308
|
+
trip.save
|
309
|
+
else
|
310
|
+
return false
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
end
|
315
|
+
```
|
316
|
+
|
317
|
+
Of course, the save method could be abstracted to our base class too, assuming we define which object the Form::Trip class is saving like so:
|
318
|
+
|
319
|
+
```ruby
|
320
|
+
class Form::Trip
|
321
|
+
|
322
|
+
def object
|
323
|
+
trip
|
324
|
+
end
|
325
|
+
|
326
|
+
# ... above code here
|
327
|
+
end
|
328
|
+
|
329
|
+
class FormParse::Base
|
330
|
+
|
331
|
+
def save
|
332
|
+
if valid? && object.valid?
|
333
|
+
object.save
|
334
|
+
else
|
335
|
+
return false
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
```
|
341
|
+
|
342
|
+
But where we really can see benefits from a "framework" for parsing our Rails forms is when we start to use nested attributes. For example, if we abstract the origin and destination fields to a "Location" model, we could build out a separate service class to handle parsing our "Location" form (even if it is in the context of a parent object like Trip).
|
343
|
+
|
344
|
+
```ruby
|
345
|
+
class Forms::Location < FormParse::Base
|
346
|
+
|
347
|
+
input :city, :string
|
348
|
+
input :state, :string
|
349
|
+
input :departure_datetime, :datetime
|
350
|
+
input :arrival_datetime, :datetime
|
351
|
+
|
352
|
+
validates :city, :state :presence => true
|
353
|
+
|
354
|
+
end
|
355
|
+
|
356
|
+
class Forms::Trip < FormParse::Base
|
357
|
+
|
358
|
+
input :miles, :float
|
359
|
+
input :origin_attributes, :belongs_to, class: 'location'
|
360
|
+
input :destination_attributes, :belongs_to, class: 'location'
|
361
|
+
|
362
|
+
validates :miles, presence: true
|
363
|
+
|
364
|
+
def after_init(args)
|
365
|
+
@trip = Trip.new(to_hash)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
```
|
369
|
+
|
370
|
+
The FormParse::Base class will recoginze the :belongs_to relationship and utilize the Location service class to parse the part of the params hash that correspond with our destination and origin models. This is great because if there is anywhere that the user can update just the origin or destination of the trip, we could just reuse that Location service object to parse the form.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "decanter"
|
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
data/decanter.gemspec
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'decanter/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "decanter"
|
8
|
+
spec.version = Decanter::VERSION
|
9
|
+
spec.authors = ["Ryan Francis", "Dave Corwin"]
|
10
|
+
spec.email = ["ryan@launchpadlab.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Form Parser for Rails}
|
13
|
+
spec.description = %q{Decanter aims to reduce complexity in Rails controllers by creating a place for transforming data before it hits the model and database.}
|
14
|
+
spec.homepage = "https://github.com/launchpadlab/decanter"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
|
18
|
+
# delete this section to allow pushing this gem to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org"
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
23
|
+
end
|
24
|
+
|
25
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_dependency "activesupport"
|
31
|
+
|
32
|
+
spec.add_development_dependency "bundler", "~> 1.9"
|
33
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
34
|
+
spec.add_development_dependency "rspec-rails"
|
35
|
+
end
|
data/lib/decanter.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Decanter
|
2
|
+
|
3
|
+
@@decanters = {}
|
4
|
+
|
5
|
+
def self.register(decanter)
|
6
|
+
@@decanters[decanter.name] = decanter
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.decanter_for(klass_or_sym)
|
10
|
+
name = klass_or_sym.is_a?(Class) ?
|
11
|
+
klass_or_sym.name :
|
12
|
+
klass_or_sym.to_s.singularize.capitalize
|
13
|
+
|
14
|
+
@@decanters["#{name}Decanter"] || (raise NameError.new("unknown decanter #{name}Decanter"))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
require 'decanter/version'
|
19
|
+
require 'active_support/inflector'
|
20
|
+
require 'decanter/base'
|
21
|
+
require 'decanter/core'
|
22
|
+
require 'decanter/extensions'
|
23
|
+
require 'decanter/value_parser'
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Decanter
|
2
|
+
module Core
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
Decanter.register(base)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
def associations
|
12
|
+
@associations ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def inputs
|
16
|
+
@inputs ||= {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def input(name, type, **options)
|
20
|
+
set_input options, {
|
21
|
+
name: name,
|
22
|
+
options: options.reject { |k| k == :context },
|
23
|
+
type: type
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_input(options, input_cfg)
|
28
|
+
set_for_context options, input_cfg, inputs
|
29
|
+
end
|
30
|
+
|
31
|
+
def input_for(name, context)
|
32
|
+
(inputs[context || :default] || {})[name]
|
33
|
+
end
|
34
|
+
|
35
|
+
def has_many(name, **options)
|
36
|
+
set_association options, {
|
37
|
+
key: options[:key] || "#{name}_attributes".to_sym,
|
38
|
+
name: name,
|
39
|
+
options: options.reject { |k| k == :context },
|
40
|
+
type: :has_many
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def has_one(name, **options)
|
45
|
+
set_association options, {
|
46
|
+
key: options[:key] || "#{name}_attributes".to_sym,
|
47
|
+
name: name,
|
48
|
+
options: options.reject { |k| k == :context },
|
49
|
+
type: :has_one
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
def has_many_for(key, context)
|
54
|
+
(associations[context || :default] || {})
|
55
|
+
.detect { |name, assoc| assoc[:type] == :has_many && assoc[:key] == key}
|
56
|
+
end
|
57
|
+
|
58
|
+
def has_one_for(key, context)
|
59
|
+
(associations[context || :default] || {})
|
60
|
+
.detect { |name, assoc| assoc[:type] == :has_one && assoc[:key] == key}
|
61
|
+
end
|
62
|
+
|
63
|
+
def set_association(options, assoc)
|
64
|
+
set_for_context options, assoc, associations
|
65
|
+
end
|
66
|
+
|
67
|
+
def set_for_context(options, arg, hash)
|
68
|
+
context = options[:context] || @context || :default
|
69
|
+
hash[context] = {} unless hash.has_key? context
|
70
|
+
hash[context][arg[:name]] = arg
|
71
|
+
end
|
72
|
+
|
73
|
+
def with_context(context, &block)
|
74
|
+
raise NameError.new('no context argument provided to with_context') unless context
|
75
|
+
|
76
|
+
@context = context
|
77
|
+
block.arity.zero? ? instance_eval(&block) : block.call(self)
|
78
|
+
@context = nil
|
79
|
+
end
|
80
|
+
|
81
|
+
def decant(args={}, context=nil)
|
82
|
+
Hash[
|
83
|
+
args.map { |name, value| handle_arg(name, value, context) }.compact
|
84
|
+
]
|
85
|
+
end
|
86
|
+
|
87
|
+
def handle_arg(name, value, context)
|
88
|
+
case
|
89
|
+
when input_cfg = input_for(name, context)
|
90
|
+
[name, parse(input_cfg[:type], value)]
|
91
|
+
when assoc = has_one_for(name, context)
|
92
|
+
[assoc.pop[:key], Decanter::decanter_for(assoc.first).decant(value, context)]
|
93
|
+
when assoc = has_many_for(name, context)
|
94
|
+
decanter = Decanter::decanter_for(assoc.first)
|
95
|
+
[assoc.pop[:key], value.map { |val| decanter.decant(val, context) }]
|
96
|
+
else
|
97
|
+
context ? nil : [name, value]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def parse(type, val)
|
102
|
+
ValueParser.value_parser_for(type).parse(val)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Decanter
|
2
|
+
module Extensions
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
def decant_update(args={}, context=nil)
|
9
|
+
self.attributes = self.decant(args, context)
|
10
|
+
self.save(context: context)
|
11
|
+
end
|
12
|
+
|
13
|
+
def decant_update!(args={}, context=nil)
|
14
|
+
self.attributes = self.decant(args, context)
|
15
|
+
self.save!(context: context)
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
|
20
|
+
def decant_create(args={}, context=nil)
|
21
|
+
self.new(decant(args, context)).save(context: context)
|
22
|
+
end
|
23
|
+
|
24
|
+
def decant_create!(args={}, context=nil)
|
25
|
+
self.new(decant(args, context)).save!(context: context)
|
26
|
+
end
|
27
|
+
|
28
|
+
def decant(args, context)
|
29
|
+
decanter_for(self).decant(args, context)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
ActiveRecord::Base.include(Decanter::Extensions) if defined? ActiveRecord
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Decanter
|
2
|
+
module ValueParser
|
3
|
+
@@value_parsers = {}
|
4
|
+
|
5
|
+
def self.register(value_parser)
|
6
|
+
@@value_parsers[value_parser.name] = value_parser
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.value_parser_for(sym)
|
10
|
+
@@value_parsers["#{sym.to_s.capitalize}Parser"] || (raise NameError.new("unknown value parser #{sym.to_s.capitalize}Parser"))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
require_relative 'value_parser/base'
|
16
|
+
require_relative 'value_parser/core'
|
17
|
+
require_relative 'value_parser/boolean_parser'
|
18
|
+
require_relative 'value_parser/string_parser'
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Decanter
|
2
|
+
module ValueParser
|
3
|
+
class DateParser < Base
|
4
|
+
def self.parse(val, options = {})
|
5
|
+
parse_format = options.fetch(:parse_format, '%m/%d/%Y')
|
6
|
+
return input_value unless input_value.present?
|
7
|
+
return input_value if input_value.is_a?(Date)
|
8
|
+
::Date.strptime(input_value, parse_format)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: decanter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ryan Francis
|
8
|
+
- Dave Corwin
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain: []
|
12
|
+
date: 2016-02-13 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: bundler
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '1.9'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '1.9'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: rake
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '10.0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '10.0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rspec-rails
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
description: Decanter aims to reduce complexity in Rails controllers by creating a
|
71
|
+
place for transforming data before it hits the model and database.
|
72
|
+
email:
|
73
|
+
- ryan@launchpadlab.com
|
74
|
+
executables: []
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files: []
|
77
|
+
files:
|
78
|
+
- Gemfile
|
79
|
+
- Gemfile.lock
|
80
|
+
- LICENSE.txt
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- bin/console
|
84
|
+
- bin/setup
|
85
|
+
- decanter.gemspec
|
86
|
+
- lib/decanter.rb
|
87
|
+
- lib/decanter/base.rb
|
88
|
+
- lib/decanter/core.rb
|
89
|
+
- lib/decanter/extensions.rb
|
90
|
+
- lib/decanter/value_parser.rb
|
91
|
+
- lib/decanter/value_parser/base.rb
|
92
|
+
- lib/decanter/value_parser/boolean_parser.rb
|
93
|
+
- lib/decanter/value_parser/core.rb
|
94
|
+
- lib/decanter/value_parser/date_parser.rb
|
95
|
+
- lib/decanter/value_parser/string_parser.rb
|
96
|
+
- lib/decanter/version.rb
|
97
|
+
homepage: https://github.com/launchpadlab/decanter
|
98
|
+
licenses:
|
99
|
+
- MIT
|
100
|
+
metadata:
|
101
|
+
allowed_push_host: https://rubygems.org
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
requirements: []
|
117
|
+
rubyforge_project:
|
118
|
+
rubygems_version: 2.4.8
|
119
|
+
signing_key:
|
120
|
+
specification_version: 4
|
121
|
+
summary: Form Parser for Rails
|
122
|
+
test_files: []
|