decanter 0.5.4 → 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 84d61b3aafe9eeae8726af22da2c9fa239321564
4
- data.tar.gz: aa12d102ed3c916f3e6c662502c434f3fb5a7a94
3
+ metadata.gz: 9ac86e971bab4b5e95eae3ada3b1fe992c6cbc83
4
+ data.tar.gz: 79a8ec074351d0947a5c28a426c8981c1006bc76
5
5
  SHA512:
6
- metadata.gz: b5e6756d719f3531fcf28f1cabafda30b41f7f186a83882c406d550ed8897a863af65338f1e652abb9c205bb690cde8676f5395eb3f0fe8892750f5a9e41e8da
7
- data.tar.gz: 934e9c5fde54466b13828d40921816bed7f2cd65efc3585d7f8d40fff2762aad0082aff6c9da733464ade29921b830baa90f90b2ff5eba66e99bb002ec6fcb90
6
+ metadata.gz: 9a99b2791d76756d0d279b123216310e0c230137ac17812934b180dda107cc2950702916442021446a979072b930598b86be19c6f6d0de1486b15683ce2b60a5
7
+ data.tar.gz: 963650185d1e39baffa089e9115cdd72fe217a5a2378221cbdead7f28d5368970158f706731de6f27efad13eb318d664bb5d67151c2426d6d6ae4a92c643b935
data/.codeclimate.yml ADDED
@@ -0,0 +1,38 @@
1
+ engines:
2
+ rubocop:
3
+ enabled: true
4
+ checks:
5
+ Rubocop/Style/Documentation:
6
+ enabled: false
7
+ Rubocop/Metrics/LineLength:
8
+ enabled: false
9
+ Rubocop/Rails/Validation:
10
+ enabled: false
11
+ Rubocop/Style/IndentationConsistency:
12
+ enabled: false
13
+ Rubocop/Style/EmptyLines:
14
+ enabled: false
15
+ Rubocop/Style/ClassAndModuleChildren:
16
+ enabled: false
17
+ Rubocop/Style/AccessorMethodName:
18
+ enabled: false
19
+ golint:
20
+ enabled: true
21
+ gofmt:
22
+ enabled: true
23
+ eslint:
24
+ enabled: true
25
+ csslint:
26
+ enabled: true
27
+ brakeman:
28
+ enabled: true
29
+ bundler-audit:
30
+ enabled: true
31
+ ratings:
32
+ paths:
33
+ - lib/**
34
+ - "**.rb"
35
+ exclude_paths:
36
+ - bin/**/*
37
+ - spec/**/*
38
+ - coverage/**/*
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ /coverage
2
+ .env
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- decanter (0.5.2)
4
+ decanter (0.5.4)
5
5
  activesupport
6
6
 
7
7
  GEM
@@ -27,7 +27,11 @@ GEM
27
27
  thread_safe (~> 0.3, >= 0.3.4)
28
28
  tzinfo (~> 1.1)
29
29
  builder (3.2.2)
30
+ codeclimate-test-reporter (0.4.8)
31
+ simplecov (>= 0.7.1, < 1.0.0)
30
32
  diff-lcs (1.2.5)
33
+ docile (1.1.5)
34
+ dotenv (2.1.0)
31
35
  erubis (2.7.0)
32
36
  i18n (0.7.0)
33
37
  json (1.8.3)
@@ -71,6 +75,11 @@ GEM
71
75
  rspec-mocks (~> 3.3.0)
72
76
  rspec-support (~> 3.3.0)
73
77
  rspec-support (3.3.0)
78
+ simplecov (0.11.2)
79
+ docile (~> 1.1.0)
80
+ json (~> 1.8)
81
+ simplecov-html (~> 0.10.0)
82
+ simplecov-html (0.10.0)
74
83
  thor (0.19.1)
75
84
  thread_safe (0.3.5)
76
85
  tzinfo (1.2.2)
@@ -81,7 +90,9 @@ PLATFORMS
81
90
 
82
91
  DEPENDENCIES
83
92
  bundler (~> 1.9)
93
+ codeclimate-test-reporter
84
94
  decanter!
95
+ dotenv
85
96
  rake (~> 10.0)
86
97
  rspec-rails
87
98
 
data/README.md CHANGED
@@ -1,10 +1,17 @@
1
- ParamsTransformer
1
+ Decanter
2
2
  ===
3
3
 
4
- Installation
4
+ [![Code Climate](https://codeclimate.com/github/LaunchPadLab/decanter/badges/gpa.svg)](https://codeclimate.com/github/LaunchPadLab/decanter) [![Test Coverage](https://codeclimate.com/github/LaunchPadLab/decanter/badges/coverage.svg)](https://codeclimate.com/github/LaunchPadLab/decanter/coverage)
5
5
  ---
6
6
 
7
- Add Gem:
7
+
8
+ What is Decanter?
9
+ ---
10
+
11
+ Decanter is a Rails gem that makes it easy to manipulate form data before it hits the model. The basic idea is that form data entered by a user often needs to be processed before it is stored into the database. A typical example of this is a datepicker. A user selects January 15th, 2015 as the date, but this is going to come into our controller as a string like "01/15/2015", so we need to convert this string to a Ruby Date object before it is stored in our database. Many developers perform this conversion right in the controller, which results in errors and unnecessary complexity, especially as the application grows.
12
+
13
+ Installation
14
+ ---
8
15
 
9
16
  ```ruby
10
17
  gem "decanter"
@@ -14,23 +21,31 @@ gem "decanter"
14
21
  bundle
15
22
  ```
16
23
 
24
+ Add the following to application.rb so we can load your decanters properly:
25
+
26
+ ```
27
+ config.paths.add "app/decanter", eager_load: true
28
+ config.to_prepare do
29
+ Dir[ File.expand_path(Rails.root.join("app/decanter/**/*.rb")) ].each do |file|
30
+ require_dependency file
31
+ end
32
+ end
33
+ ```
17
34
 
18
35
  Basic Usage
19
36
  ---
20
37
 
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
38
+ ```
39
+ rails g decanter Trip name:string start_date:date end_date:date
40
+ ```
22
41
 
23
- **app/classes/forms/property.rb**
42
+ **app/decanter/decanters/trip_decanter.rb**
24
43
 
25
44
  ```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
-
45
+ class TripDecanter < Decanter::Base
46
+ input :name, :string
47
+ input :start_date, :date
48
+ input :end_date, :date
34
49
  end
35
50
  ```
36
51
 
@@ -38,333 +53,294 @@ In your controller:
38
53
 
39
54
  ```ruby
40
55
  def create
41
- @form = Forms::Property.new(params)
56
+ @trip = Trip.decant_new(params[:trip])
42
57
 
43
- if @form.save
58
+ if @trip.save
44
59
  redirect_to trips_path
45
60
  else
46
- set_index_variables
47
- render "index"
61
+ render "new"
62
+ end
63
+ end
64
+
65
+ def update
66
+ @trip = Trip.find(params[:id])
67
+
68
+ if @trip.decant_update(params[:trip])
69
+ redirect_to trips_path
70
+ else
71
+ render "new"
48
72
  end
49
73
  end
50
74
  ```
51
75
 
52
- Inheritance
76
+ Basic Example
53
77
  ---
54
78
 
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)
79
+ We have a form where users can create a new Trip, which has the following attributes: name, start_date, and end_date
60
80
 
81
+ Without Decanter, here is what our create action may look like:
61
82
 
62
83
  ```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
84
+ class TripsController < ApplicationController
85
+ def create
86
+ @trip = Trip.new(params[:trip])
87
+ start_date = Date.strptime(params[:trip][:start_date], '%m/%d/%Y')
88
+ end_date = Date.strptime(params[:trip][:end_date], '%m/%d/%Y')
89
+ @trip.start_date = start_date
90
+ @trip.end_date = end_date
69
91
 
92
+ if @trip.save
93
+ redirect_to trips_path
94
+ else
95
+ render 'new'
96
+ end
97
+ end
70
98
  end
99
+ ```
71
100
 
101
+ We can see here that converting start_date and end_date to a Ruby date is creating complexity. Could you imagine the complexity involved with performing similar parsing with a nested resource? If you're curious how ugly it would get, we took the liberty of implementing an example here: [Nested Example (Without Decanter)](https://github.com/LaunchPadLab/decanter_demo/blob/master/app/controllers/nested_example/trips_no_decanter_controller.rb)
72
102
 
73
- # app/classes/forms/property/create.rb
74
- class Forms::Property::Create < Forms::Property
103
+ With Decanter installed, here is what the same controller action would look like:
75
104
 
76
- # override any input, validation, or method here
105
+ ```ruby
106
+ class TripsController < ApplicationController
107
+ def create
108
+ @trip = Trip.decant_new(params[:trip])
77
109
 
110
+ if @trip.save
111
+ redirect_to trips_path
112
+ else
113
+ render 'new'
114
+ end
115
+ end
78
116
  end
79
117
  ```
80
118
 
81
- class Forms::Property::Update < Forms::Property
119
+ As you can see, we no longer need to parse the start and end date. Let's take a look at how we accomplished that.
82
120
 
83
- # override any input, validation, or method here
121
+ From terminal we ran:
84
122
 
85
- end
123
+ ```
124
+ rails g decanter Trip name:string start_date:date end_date:date
86
125
  ```
87
126
 
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.
127
+ Which generates app/decanter/decanters/trip_decanter.rb:
92
128
 
93
- The modeling would look like:
129
+ ```ruby
130
+ class TripDecanter < Decanter::Base
131
+ input :name, :string
132
+ input :start_date, :date
133
+ input :end_date, :date
134
+ end
135
+ ```
94
136
 
95
- class Course < ActiveRecord::Base
137
+ You'll also notice that instead of ```@trip = Trip.new(params[:trip])``` we do ```@trip = Trip.decant_new(params[:trip])```. ```decant_new`` is where the magic happens. It is converting the params from this:
96
138
 
97
- has_many :assignments
98
- has_many :resources
139
+ ```ruby
140
+ {
141
+ name: "My Trip",
142
+ start_date: "01/15/2015",
143
+ end_date: "01/20/2015"
144
+ }
145
+ ```
99
146
 
100
- end
147
+ to this:
101
148
 
149
+ ```ruby
150
+ {
151
+ name: "My Trip",
152
+ start_date: Mon, 15 Jan 2015,
153
+ end_date: Mon, 20 Jan 2015
154
+ }
155
+ ```
102
156
 
157
+ As you can see, the converted params hash has converted start_date and end_date to a Ruby Date object that is ready to be stored in our database.
103
158
 
104
- Why a Library for Parsing Forms?
159
+ Adding Custom Parsers
105
160
  ---
106
161
 
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
- ```
162
+ In the above example, start_date and end_date are ran through a DateParser that lives in Decanter. Let's take a look at the DateParser:
118
163
 
119
164
  ```ruby
120
- # trips_controller
165
+ class DateParser < Decanter::ValueParser::Base
121
166
 
122
- def create
123
- @trip = Trip.new(trip_params)
167
+ allow Date
124
168
 
125
- if @trip.save
126
- redirect_to trips_path, notice: "New trip successfully created."
127
- else
128
- render "new"
129
- end
169
+ parser do |name, value, options|
170
+ parse_format = options.fetch(:parse_format, '%m/%d/%Y')
171
+ ::Date.strptime(value, parse_format)
130
172
  end
173
+ end
131
174
  ```
132
175
 
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.
176
+ ```allow Date``` basically tells Decanter that if the value comes in as a Date object, we don't need to parse it at all. Other than that, the parser is really just doing ```Date.strptime("01/15/2015", '%m/%d/%Y')```, which is just a vanilla date parse.
134
177
 
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:
178
+ You'll notice that the above ```parser do``` block takes a ```:parse_format``` option. This allows you to specify the format your date string will come in. For example, if you expect "2016-01-15" instead of "01/15/2016", you can adjust the TripDecanter like so:
140
179
 
141
180
  ```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
181
+ # app/decanter/decanters/trip_decanter.rb
159
182
 
183
+ class TripDecanter < Decanter::Base
184
+ input :name, :string
185
+ input :start_date, :date, parse_format: '%Y-%m-%d'
186
+ input :end_date, :date, parse_format: '%Y-%m-%d'
160
187
  end
161
188
  ```
162
189
 
163
- ```ruby
164
- # trips_controller.rb
190
+ You can add your own parser if you want more control over the logic, or if you have a peculiar format type we don't support.
165
191
 
166
- def create
167
- form = Forms::Trip.new(trip_params)
168
- @trip = form.trip
192
+ ```
193
+ rails g parser Date
194
+ ```
169
195
 
170
- if form.save
171
- redirect_to trips_path, notice: "New trip successfully created."
172
- else
173
- render "new"
174
- end
196
+ **app/decanter/parsers/date_parser**
197
+
198
+ ```ruby
199
+ class DateParser < Decanter::ValueParser::Base
200
+ parser do |name, value, options|
201
+ # your parsing logic here
175
202
  end
203
+ end
176
204
  ```
177
205
 
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.
206
+ Nested Example
207
+ ---
179
208
 
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:
209
+ Let's say we have two models in our app: a Trip and a Destination. A trip has many destinations, and is prepared to accept nested attributes from the form.
181
210
 
182
211
  ```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
212
+ # app/models/trip.rb
190
213
 
214
+ class Trip < ActiveRecord::Base
215
+ has_many :destinations
216
+ accepts_nested_attributes_for :destinations
191
217
  end
192
218
  ```
193
219
 
194
220
  ```ruby
195
- # trips_controller.rb
196
- # same as above
221
+ # app/models/destination.rb
197
222
 
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
223
+ class Destination < ActiveRecord::Base
224
+ belongs_to :trip
225
+ end
208
226
  ```
209
227
 
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.
228
+ First, let's create our decanters for Trip and Destination. Note: decanters are automatically created whenever you run ```rails g resource```.
211
229
 
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 }
230
+ ```
231
+ rails g decanter Trip name destinations:has_many
232
+ rails g decanter Destination city state arrival_date:date departure_date:date
216
233
  ```
217
234
 
218
- A more complex form would end up with a service class like below:
235
+ Which produces app/decanter/decanters/trip and app/decanter/decanters/destination:
219
236
 
220
237
  ```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
-
238
+ class TripDecanter < Decanter::Base
239
+ input :name, :string
240
+ has_many :destinations
249
241
  end
250
242
  ```
251
243
 
252
- With a framework, it would only involve the following:
253
-
254
244
  ```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
-
245
+ class DestinationDecanter < Decanter::Base
246
+ input :city, :string
247
+ input :state, :string
248
+ input :arrival_date, :date
249
+ input :departure_date, :date
280
250
  end
281
251
  ```
282
252
 
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:
253
+ With that, we can use the same vanilla create action syntax you saw in the basic example above:
284
254
 
285
255
  ```ruby
286
- class FormParse::Base
287
- include ActiveModel::Model
256
+ class TripsController < ApplicationController
257
+ def create
258
+ @trip = Trip.decant_new(params[:trip])
288
259
 
289
- # ... base parsing code
260
+ if @trip.save
261
+ redirect_to trips_path
262
+ else
263
+ render 'new'
264
+ end
265
+ end
290
266
  end
267
+ ```
291
268
 
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
269
+ Each of the destinations in our params[:trip] are automatically parsed according to the DestinationDecanter inputs set above. This means that ```arrival_date``` and ```departure_date``` are converted to Ruby Date objects for each of the destinations passed through the nested params. Yeehaw!
299
270
 
300
- validates :miles, :origin_city, :origin_state, :destination_city, :destination_state, presence: true
271
+ Non Database-Backed Objects
272
+ ---
301
273
 
302
- def after_init(args)
303
- @trip = Trip.new(to_hash)
304
- end
274
+ Decanter will work for you non database-backed objects as well. We just need to call ```decant``` to parse our params according to our decanter logic.
305
275
 
306
- def save
307
- if valid? && trip.valid?
308
- trip.save
309
- else
310
- return false
311
- end
312
- end
276
+ Let's say we have a search filtering object called ```SearchFilter```. We start by generating our decanter:
313
277
 
314
- end
315
278
  ```
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:
279
+ rails g decanter SearchFilter start_date:date end_date:date city:string state:string
280
+ ```
318
281
 
319
282
  ```ruby
320
- class Form::Trip
283
+ # app/decanter/decanters/search_filter_decanter.rb
321
284
 
322
- def object
323
- trip
324
- end
285
+ class SearchFilterDecanter < Decanter::Base
325
286
 
326
- # ... above code here
327
287
  end
288
+ ```
328
289
 
329
- class FormParse::Base
290
+ ```ruby
291
+ # app/controllers/search_controller.rb
330
292
 
331
- def save
332
- if valid? && object.valid?
333
- object.save
334
- else
335
- return false
336
- end
337
- end
293
+ def search
294
+ decanted_params = SearchFilterDecanter.decant(params[:search])
295
+ # decanted_params is now parsed according to the parsers defined
296
+ # in SearchFilterDecanter
338
297
  end
339
-
340
298
  ```
341
299
 
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).
300
+ Default Parsers
301
+ ---
343
302
 
344
- ```ruby
345
- class Forms::Location < FormParse::Base
303
+ Decanter comes with the following parsers:
304
+ - boolean
305
+ - date
306
+ - datetime
307
+ - float
308
+ - integer
309
+ - phone
310
+ - string
346
311
 
347
- input :city, :string
348
- input :state, :string
349
- input :departure_datetime, :datetime
350
- input :arrival_datetime, :datetime
312
+ As an example as to how these parsers differ, let's consider ```float```. The float parser will perform a regex to find only characters that are digits or decimals. By doing that, your users can enter in commas and currency symbols without your backend throwing a hissy fit.
351
313
 
352
- validates :city, :state :presence => true
314
+ We encourage you to create your own parsers for other needs in your app, or generate one of the above listed parsers to override its behavior.
353
315
 
354
- end
316
+ ```
317
+ rails g parser Zip
318
+ ```
355
319
 
356
- class Forms::Trip < FormParse::Base
320
+ Squashing Inputs
321
+ ---
357
322
 
358
- input :miles, :float
359
- input :origin_attributes, :belongs_to, class: 'location'
360
- input :destination_attributes, :belongs_to, class: 'location'
323
+ Sometimes, you may want to take several inputs and combine them into one finished input prior to sending to your model. For example, if day, month, and year come in as separate parameters, but your database really only cares about start_date.
361
324
 
362
- validates :miles, presence: true
325
+ ```ruby
326
+ class TripDecanter < Decanter::Base
327
+ input [:day, :month, :year], :squash_date, key: :start_date
328
+ end
329
+ ```
363
330
 
364
- def after_init(args)
365
- @trip = Trip.new(to_hash)
331
+ ```
332
+ rails g parser SquashDate
333
+ ```
334
+
335
+ ```ruby
336
+ # app/decanter/squashers/date_squasher.rb
337
+
338
+ class SquashDateParser < Decanter::Parser::Base
339
+ parser do |name, values, options|
340
+ day = values[0]
341
+ month = values[1]
342
+ year = values[2]
343
+ Date.new(year, month, day)
366
344
  end
367
345
  end
368
346
  ```
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/decanter.gemspec CHANGED
@@ -6,7 +6,7 @@ require 'decanter/version'
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "decanter"
8
8
  spec.version = Decanter::VERSION
9
- spec.authors = ["Ryan Francis", "Dave Corwin"]
9
+ spec.authors = ["Ryan Francis", "David Corwin"]
10
10
  spec.email = ["ryan@launchpadlab.com"]
11
11
 
12
12
  spec.summary = %q{Form Parser for Rails}
@@ -32,4 +32,6 @@ Gem::Specification.new do |spec|
32
32
  spec.add_development_dependency "bundler", "~> 1.9"
33
33
  spec.add_development_dependency "rake", "~> 10.0"
34
34
  spec.add_development_dependency "rspec-rails"
35
+ spec.add_development_dependency "codeclimate-test-reporter"
36
+ spec.add_development_dependency "dotenv"
35
37
  end
data/lib/decanter/core.rb CHANGED
@@ -7,101 +7,125 @@ module Decanter
7
7
 
8
8
  module ClassMethods
9
9
 
10
- def associations
11
- @associations ||= {}.with_indifferent_access
12
- end
10
+ def input(name, parser=nil, **options)
13
11
 
14
- def inputs
15
- @inputs ||= {}.with_indifferent_access
16
- end
12
+ _name = [name].flatten
17
13
 
18
- def input(name=nil, type, **options)
19
- set_input options, {
20
- name: name,
21
- options: options.reject { |k| k == :context },
22
- type: type
23
- }
24
- end
25
-
26
- def set_input(options, input_cfg)
27
- set_for_context options, input_cfg, inputs
28
- end
14
+ if _name.length > 1 && parser.blank?
15
+ raise ArgumentError.new("#{self.name} no parser specified for input with multiple values.")
16
+ end
29
17
 
30
- def input_for(name, context)
31
- (inputs[context || :default] || {})[name]
18
+ handlers[_name] = {
19
+ key: options.fetch(:key, _name.first),
20
+ name: _name,
21
+ options: options,
22
+ parser: parser,
23
+ type: :input
24
+ }
32
25
  end
33
26
 
34
- def has_many(name=nil, **options)
35
- set_association options, {
36
- key: options[:key] || "#{name}_attributes".to_sym,
27
+ def has_many(assoc, **options)
28
+ name = ["#{assoc}_attributes".to_sym]
29
+ handlers[name] = {
30
+ assoc: assoc,
31
+ key: options.fetch(:key, name.first),
37
32
  name: name,
38
- options: options.reject { |k| k == :context },
33
+ options: options,
39
34
  type: :has_many
40
35
  }
41
36
  end
42
37
 
43
- def has_one(name=nil, **options)
44
- set_association options, {
45
- key: options[:key] || "#{name}_attributes".to_sym,
38
+ def has_one(assoc, **options)
39
+ name = ["#{assoc}_attributes".to_sym]
40
+ handlers[name] = {
41
+ assoc: assoc,
42
+ key: options.fetch(:key, name.first),
46
43
  name: name,
47
- options: options.reject { |k| k == :context },
44
+ options: options,
48
45
  type: :has_one
49
46
  }
50
47
  end
51
48
 
52
- def has_many_for(key, context)
53
- (associations[context || :default] || {})
54
- .detect { |name, assoc| assoc[:type] == :has_many && assoc[:key] == key.to_sym}
49
+ def strict(mode)
50
+ raise( ArgumentError.new("#{self.name}: Unknown strict value #{mode}")) unless [:with_exception, true, false].include? mode
51
+ @strict_mode = mode
55
52
  end
56
53
 
57
- def has_one_for(key, context)
58
- (associations[context || :default] || {})
59
- .detect { |name, assoc| assoc[:type] == :has_one && assoc[:key] == key.to_sym}
54
+ def decant(args)
55
+ args = args.to_unsafe_h if args.class.name == 'ActionController::Parameters'
56
+ {}.merge( unhandled_keys(args) )
57
+ .merge( handled_keys(args) )
60
58
  end
61
59
 
62
- def set_association(options, assoc)
63
- set_for_context options, assoc, associations
64
- end
60
+ # protected
61
+
62
+ def unhandled_keys(args)
63
+
64
+ unhandled_keys = args.keys - handlers.keys.flatten.uniq
65
+
66
+ if unhandled_keys.any?
67
+ case strict_mode
68
+ when true
69
+ p "#{self.name} ignoring unhandled keys: #{unhandled_keys.join(', ')}."
70
+ {}
71
+ when :with_exception
72
+ raise ArgumentError.new("#{self.name} received unhandled keys: #{unhandled_keys.join(', ')}.")
73
+ else
74
+ args.select { |key| unhandled_keys.include? key }
75
+ end
76
+ else
77
+ {}
78
+ end
79
+ end
65
80
 
66
- def set_for_context(options, arg, hash)
67
- context = options[:context] || @context || :default
68
- hash[context] = {} unless hash.has_key? context
69
- hash[context][arg[:name]] = arg
70
- end
81
+ def handled_keys(args)
82
+ handlers.values
83
+ .select { |handler| (args.keys.map(&:to_sym) & handler[:name]).any? }
84
+ .reduce({}) { |memo, handler| memo.merge handle(handler, args) }
85
+ end
71
86
 
72
- def with_context(context, &block)
73
- raise NameError.new('no context argument provided to with_context') unless context
87
+ def handle(handler, args)
88
+ values = args.values_at(*handler[:name])
89
+ values = values.length == 1 ? values.first : values
90
+ self.send("handle_#{handler[:type]}", handler, values)
91
+ end
74
92
 
75
- @context = context
76
- block.arity.zero? ? instance_eval(&block) : block.call(self)
77
- @context = nil
78
- end
93
+ def handle_input(handler, values)
94
+ parse(handler[:key], handler[:parser], values, handler[:options])
95
+ end
79
96
 
80
- def decant(args={}, context=nil)
81
- Hash[
82
- args.keys.map { |key| handle_arg(key, args[key], context) }.compact
83
- ]
84
- end
97
+ def handle_has_many(handler, values)
98
+ decanter = decanter_for_handler(handler)
99
+ {
100
+ handler[:key] => values.compact.map { |value| decanter.decant(value) }
101
+ }
102
+ end
85
103
 
86
- def handle_arg(name, value, context)
87
- case
88
- when input_cfg = input_for(name, context)
89
- [name, parse(name, input_cfg[:type], value, input_cfg[:options])]
90
- when assoc = has_one_for(name, context)
91
- [assoc.pop[:key], Decanter::decanter_for(assoc[1][:options][:decanter] || assoc.first).decant(value, context)]
92
- when assoc = has_many_for(name, context)
93
- decanter = Decanter::decanter_for(assoc[1][:options][:decanter] || assoc.first)
94
- [assoc.pop[:key], value.map { |val| decanter.decant(val, context) }]
95
- else
96
- context ? nil : [name, value]
104
+ def handle_has_one(handler, values)
105
+ {
106
+ handler[:key] => decanter_for_handler(handler).decant(values)
107
+ }
97
108
  end
98
- end
99
109
 
100
- def parse(name, type, val, options)
101
- type ?
102
- ValueParser.value_parser_for(type).parse(name, val, options) :
103
- val
104
- end
110
+ def decanter_for_handler(handler)
111
+ Decanter::decanter_for(handler[:options][:decanter] || handler[:assoc])
112
+ end
113
+
114
+ def parse(key, parser, values, options)
115
+ parser ?
116
+ ValueParser.value_parser_for(parser)
117
+ .parse(key, values, options)
118
+ :
119
+ { key => values }
120
+ end
121
+
122
+ def handlers
123
+ @handlers ||= {}
124
+ end
125
+
126
+ def strict_mode
127
+ @strict_mode ||= {}
128
+ end
105
129
  end
106
130
  end
107
131
  end
@@ -5,32 +5,35 @@ module Decanter
5
5
  base.extend(ClassMethods)
6
6
  end
7
7
 
8
- def decant_update(args={}, context=nil)
9
- self.attributes = self.class.decant(args, context)
10
- self.save(context: context)
8
+ def decant_update(args, **options)
9
+ self.attributes = self.class.decant(args, options)
10
+ self.save(context: options[:context])
11
11
  end
12
12
 
13
- def decant_update!(args={}, context=nil)
14
- self.attributes = self.class.decant(args, context)
15
- self.save!(context: context)
13
+ def decant_update!(args, **options)
14
+ self.attributes = self.class.decant(args, options)
15
+ self.save!(context: options[:context])
16
16
  end
17
17
 
18
18
  module ClassMethods
19
19
 
20
- def decant_create(args={}, context=nil)
21
- self.new(decant(args, context)).save(context: context)
20
+ def decant_create(args, **options)
21
+ self.new(decant(args, options))
22
+ .save(context: options[:context])
22
23
  end
23
24
 
24
- def decant_new(args={}, context=nil)
25
- self.new(decant(args, context))
25
+ def decant_new(args, **options)
26
+ self.new(decant(args, options))
26
27
  end
27
28
 
28
- def decant_create!(args={}, context=nil)
29
- self.new(decant(args, context)).save!(context: context)
29
+ def decant_create!(args, **options)
30
+ self.new(decant(args, options))
31
+ .save!(context: options[:context])
30
32
  end
31
33
 
32
- def decant(args, context)
33
- Decanter.decanter_for(self).decant(args, context)
34
+ def decant(args, options)
35
+ options.fetch(:decanter, Decanter.decanter_for(self))
36
+ .decant(args)
34
37
  end
35
38
  end
36
39
  end
@@ -6,4 +6,10 @@ class Decanter::Railtie < Rails::Railtie
6
6
  initializer 'decanter.configure' do
7
7
  ActiveRecord::Base.include(Decanter::Extensions) if defined? ActiveRecord
8
8
  end
9
+
10
+ generators do |app|
11
+ Rails::Generators.configure!(app.config.generators)
12
+ Rails::Generators.hidden_namespaces.uniq!
13
+ require 'generators/rails/resource_override'
14
+ end
9
15
  end
@@ -7,7 +7,6 @@ module Decanter
7
7
  end
8
8
 
9
9
  def self.value_parser_for(sym)
10
- p "find parser for #{sym}"
11
10
  @@value_parsers["#{sym.to_s.camelize}Parser"] || (raise NameError.new("unknown value parser #{sym.to_s.capitalize}Parser"))
12
11
  end
13
12
  end
@@ -22,3 +21,5 @@ require_relative 'value_parser/string_parser'
22
21
  require_relative 'value_parser/phone_parser'
23
22
  require_relative 'value_parser/float_parser'
24
23
  require_relative 'value_parser/integer_parser'
24
+ require_relative 'value_parser/key_value_splitter_parser'
25
+ require_relative 'value_parser/join_parser'
@@ -4,30 +4,42 @@ module Decanter
4
4
 
5
5
  def self.included(base)
6
6
  base.extend(ClassMethods)
7
- ValueParser.register(base)
8
7
  end
9
8
 
10
9
  module ClassMethods
11
10
 
12
- def parse(name, val=nil, options={})
11
+ def parse(name, values, options={})
13
12
 
14
- if val.blank?
13
+ value_ary = values.is_a?(Array) ? values : [values]
14
+
15
+ # want to treat 'false' as an actual value
16
+ if value_ary.all? { |value| value.nil? || value == "" }
15
17
  if options[:required]
16
18
  raise ArgumentError.new("No value for required argument: #{name}")
17
19
  else
18
- return val
20
+ return { name => nil }
19
21
  end
20
22
  end
21
23
 
22
- if @allowed && @allowed.include?(val.class)
23
- return val
24
+ if @allowed && value_ary.all? { |value| @allowed.include?(value.class) }
25
+ return { name => values }
24
26
  end
25
27
 
26
28
  unless @parser
27
- raise ArgumentError.new("No parser for argument: #{name} with type: #{val.class}")
29
+ raise ArgumentError.new("No parser for argument: #{name} with types: #{value_ary.map(&:class).join(', ')}")
28
30
  end
29
31
 
30
- @parser.call(name, val, options)
32
+ case @result
33
+ when :raw
34
+ # Parser result must be a hash
35
+ parsed = @parser.call(name, values, options)
36
+ parsed.is_a?(Hash) ?
37
+ parsed :
38
+ raise(ArgumentError.new("Result of parser #{self.name} with values #{values} was #{parsed} when it must be a hash."))
39
+ else
40
+ # Parser result will be treated as a single value belonging to the name
41
+ { name => @parser.call(name, values, options) }
42
+ end
31
43
  end
32
44
 
33
45
  def parser(&block)
@@ -37,6 +49,10 @@ module Decanter
37
49
  def allow(*args)
38
50
  @allowed = args
39
51
  end
52
+
53
+ def result(opt)
54
+ @result = opt
55
+ end
40
56
  end
41
57
  end
42
58
  end
@@ -0,0 +1,13 @@
1
+ module Decanter
2
+ module ValueParser
3
+ class JoinParser < Base
4
+ DELIM = ','
5
+
6
+ parser do |name, val, options|
7
+ delimiter = options.fetch(:delimiter, ITEM_DELIM)
8
+ val.join(DELIM)
9
+ end
10
+ end
11
+ end
12
+ end
13
+
@@ -0,0 +1,17 @@
1
+ module Decanter
2
+ module ValueParser
3
+ class KeyValueSplitterParser < Base
4
+ ITEM_DELIM = ','
5
+ PAIR_DELIM = ':'
6
+
7
+ result :raw
8
+
9
+ parser do |name, val, options|
10
+ item_delimiter = options.fetch(:item_delimiter, ITEM_DELIM)
11
+ pair_delimiter = options.fetch(:pair_delimiter, PAIR_DELIM)
12
+ val.split(item_delimiter).reduce({}) { |memo, pair| memo.merge( Hash[ *pair.split(pair_delimiter) ] ) }
13
+ end
14
+ end
15
+ end
16
+ end
17
+
@@ -1,3 +1,3 @@
1
1
  module Decanter
2
- VERSION = '0.5.4'
2
+ VERSION = '0.5.5'
3
3
  end
@@ -0,0 +1,31 @@
1
+ module Rails
2
+ module Generators
3
+ class DecanterGenerator < NamedBase
4
+ source_root File.expand_path('../templates', __FILE__)
5
+ check_class_collision :suffix => 'Decanter'
6
+ ASSOCIATION_TYPES = [:has_many, :has_one, :belongs_to]
7
+
8
+ argument :attributes, :type => :array, :default => [], :banner => 'field:type field:type'
9
+
10
+ class_option :parent, :type => :string, :desc => 'The parent class for the generated decanter'
11
+
12
+ def create_decanter_file
13
+ template 'decanter.rb.erb', File.join('app/decanter/decanters', class_path, "#{file_name}_decanter.rb")
14
+ end
15
+
16
+ private
17
+
18
+ def inputs
19
+ attributes.find_all { |attr| ASSOCIATION_TYPES.exclude?(attr.type) }
20
+ end
21
+
22
+ def associations
23
+ attributes - inputs
24
+ end
25
+
26
+ def parent_class_name
27
+ 'Decanter::Base'
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ module Rails
2
+ module Generators
3
+ class ParserGenerator < NamedBase
4
+ source_root File.expand_path('../templates', __FILE__)
5
+ check_class_collision :suffix => 'Parser'
6
+
7
+ class_option :parent, :type => :string, :desc => 'The parent class for the generated parser'
8
+
9
+ def create_parser_file
10
+ template 'parser.rb.erb', File.join('app/decanter/parsers', class_path, "#{file_name}_parser.rb")
11
+ end
12
+
13
+ private
14
+
15
+ def parent_class_name
16
+ 'Decanter::ValueParser::Base'
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,10 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/rails/resource/resource_generator'
3
+
4
+ module Rails
5
+ module Generators
6
+ class ResourceGenerator
7
+ hook_for :decanter, default: true, boolean: true
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ <% module_namespacing do -%>
2
+ class <%= class_name %>Decanter < <%= parent_class_name %>
3
+ <% inputs.each do |input| -%>
4
+ input :<%= input.name %>, :<%= input.type %>
5
+ <% end -%>
6
+ <% associations.each do |assoc| -%>
7
+ <%= assoc.type %> :<%= assoc.name %>
8
+ <% end -%>
9
+ end
10
+ <% end -%>
@@ -0,0 +1,7 @@
1
+ <% module_namespacing do -%>
2
+ class <%= class_name %>Parser < <%= parent_class_name %>
3
+ parser do |name, value, options|
4
+ value
5
+ end
6
+ end
7
+ <% end -%>
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: decanter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.4
4
+ version: 0.5.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Francis
8
- - Dave Corwin
8
+ - David Corwin
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2016-02-15 00:00:00.000000000 Z
12
+ date: 2016-02-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -67,6 +67,34 @@ dependencies:
67
67
  - - ">="
68
68
  - !ruby/object:Gem::Version
69
69
  version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: codeclimate-test-reporter
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: dotenv
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
70
98
  description: Decanter aims to reduce complexity in Rails controllers by creating a
71
99
  place for transforming data before it hits the model and database.
72
100
  email:
@@ -75,6 +103,8 @@ executables: []
75
103
  extensions: []
76
104
  extra_rdoc_files: []
77
105
  files:
106
+ - ".codeclimate.yml"
107
+ - ".gitignore"
78
108
  - ".rspec"
79
109
  - Gemfile
80
110
  - Gemfile.lock
@@ -98,9 +128,16 @@ files:
98
128
  - lib/decanter/value_parser/datetime_parser.rb
99
129
  - lib/decanter/value_parser/float_parser.rb
100
130
  - lib/decanter/value_parser/integer_parser.rb
131
+ - lib/decanter/value_parser/join_parser.rb
132
+ - lib/decanter/value_parser/key_value_splitter_parser.rb
101
133
  - lib/decanter/value_parser/phone_parser.rb
102
134
  - lib/decanter/value_parser/string_parser.rb
103
135
  - lib/decanter/version.rb
136
+ - lib/generators/rails/decanter_generator.rb
137
+ - lib/generators/rails/parser_generator.rb
138
+ - lib/generators/rails/resource_override.rb
139
+ - lib/generators/rails/templates/decanter.rb.erb
140
+ - lib/generators/rails/templates/parser.rb.erb
104
141
  homepage: https://github.com/launchpadlab/decanter
105
142
  licenses:
106
143
  - MIT