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 +4 -4
- data/.codeclimate.yml +38 -0
- data/.gitignore +2 -0
- data/Gemfile.lock +12 -1
- data/README.md +212 -236
- data/decanter.gemspec +3 -1
- data/lib/decanter/core.rb +93 -69
- data/lib/decanter/extensions.rb +17 -14
- data/lib/decanter/railtie.rb +6 -0
- data/lib/decanter/value_parser.rb +2 -1
- data/lib/decanter/value_parser/core.rb +24 -8
- data/lib/decanter/value_parser/join_parser.rb +13 -0
- data/lib/decanter/value_parser/key_value_splitter_parser.rb +17 -0
- data/lib/decanter/version.rb +1 -1
- data/lib/generators/rails/decanter_generator.rb +31 -0
- data/lib/generators/rails/parser_generator.rb +20 -0
- data/lib/generators/rails/resource_override.rb +10 -0
- data/lib/generators/rails/templates/decanter.rb.erb +10 -0
- data/lib/generators/rails/templates/parser.rb.erb +7 -0
- metadata +40 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9ac86e971bab4b5e95eae3ada3b1fe992c6cbc83
|
4
|
+
data.tar.gz: 79a8ec074351d0947a5c28a426c8981c1006bc76
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
decanter (0.5.
|
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
|
-
|
1
|
+
Decanter
|
2
2
|
===
|
3
3
|
|
4
|
-
|
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
|
-
|
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
|
-
|
38
|
+
```
|
39
|
+
rails g decanter Trip name:string start_date:date end_date:date
|
40
|
+
```
|
22
41
|
|
23
|
-
**app/
|
42
|
+
**app/decanter/decanters/trip_decanter.rb**
|
24
43
|
|
25
44
|
```ruby
|
26
|
-
class
|
27
|
-
|
28
|
-
input :
|
29
|
-
input :
|
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
|
-
@
|
56
|
+
@trip = Trip.decant_new(params[:trip])
|
42
57
|
|
43
|
-
if @
|
58
|
+
if @trip.save
|
44
59
|
redirect_to trips_path
|
45
60
|
else
|
46
|
-
|
47
|
-
|
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
|
-
|
76
|
+
Basic Example
|
53
77
|
---
|
54
78
|
|
55
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
74
|
-
class Forms::Property::Create < Forms::Property
|
103
|
+
With Decanter installed, here is what the same controller action would look like:
|
75
104
|
|
76
|
-
|
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
|
-
|
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
|
-
|
121
|
+
From terminal we ran:
|
84
122
|
|
85
|
-
|
123
|
+
```
|
124
|
+
rails g decanter Trip name:string start_date:date end_date:date
|
86
125
|
```
|
87
126
|
|
88
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
98
|
-
|
139
|
+
```ruby
|
140
|
+
{
|
141
|
+
name: "My Trip",
|
142
|
+
start_date: "01/15/2015",
|
143
|
+
end_date: "01/20/2015"
|
144
|
+
}
|
145
|
+
```
|
99
146
|
|
100
|
-
|
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
|
-
|
159
|
+
Adding Custom Parsers
|
105
160
|
---
|
106
161
|
|
107
|
-
In
|
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
|
-
|
165
|
+
class DateParser < Decanter::ValueParser::Base
|
121
166
|
|
122
|
-
|
123
|
-
@trip = Trip.new(trip_params)
|
167
|
+
allow Date
|
124
168
|
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
167
|
-
|
168
|
-
|
192
|
+
```
|
193
|
+
rails g parser Date
|
194
|
+
```
|
169
195
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
-
|
206
|
+
Nested Example
|
207
|
+
---
|
179
208
|
|
180
|
-
|
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
|
-
|
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
|
-
|
196
|
-
# same as above
|
221
|
+
# app/models/destination.rb
|
197
222
|
|
198
|
-
|
199
|
-
|
200
|
-
|
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
|
-
|
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
|
-
|
213
|
-
|
214
|
-
|
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
|
-
|
235
|
+
Which produces app/decanter/decanters/trip and app/decanter/decanters/destination:
|
219
236
|
|
220
237
|
```ruby
|
221
|
-
class
|
222
|
-
|
223
|
-
|
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
|
256
|
-
|
257
|
-
input :
|
258
|
-
input :
|
259
|
-
input :
|
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
|
-
|
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
|
287
|
-
|
256
|
+
class TripsController < ApplicationController
|
257
|
+
def create
|
258
|
+
@trip = Trip.decant_new(params[:trip])
|
288
259
|
|
289
|
-
|
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
|
-
|
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
|
-
|
271
|
+
Non Database-Backed Objects
|
272
|
+
---
|
301
273
|
|
302
|
-
|
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
|
-
|
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
|
-
|
279
|
+
rails g decanter SearchFilter start_date:date end_date:date city:string state:string
|
280
|
+
```
|
318
281
|
|
319
282
|
```ruby
|
320
|
-
|
283
|
+
# app/decanter/decanters/search_filter_decanter.rb
|
321
284
|
|
322
|
-
|
323
|
-
trip
|
324
|
-
end
|
285
|
+
class SearchFilterDecanter < Decanter::Base
|
325
286
|
|
326
|
-
# ... above code here
|
327
287
|
end
|
288
|
+
```
|
328
289
|
|
329
|
-
|
290
|
+
```ruby
|
291
|
+
# app/controllers/search_controller.rb
|
330
292
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
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
|
-
|
300
|
+
Default Parsers
|
301
|
+
---
|
343
302
|
|
344
|
-
|
345
|
-
|
303
|
+
Decanter comes with the following parsers:
|
304
|
+
- boolean
|
305
|
+
- date
|
306
|
+
- datetime
|
307
|
+
- float
|
308
|
+
- integer
|
309
|
+
- phone
|
310
|
+
- string
|
346
311
|
|
347
|
-
|
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
|
-
|
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
|
-
|
316
|
+
```
|
317
|
+
rails g parser Zip
|
318
|
+
```
|
355
319
|
|
356
|
-
|
320
|
+
Squashing Inputs
|
321
|
+
---
|
357
322
|
|
358
|
-
|
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
|
-
|
325
|
+
```ruby
|
326
|
+
class TripDecanter < Decanter::Base
|
327
|
+
input [:day, :month, :year], :squash_date, key: :start_date
|
328
|
+
end
|
329
|
+
```
|
363
330
|
|
364
|
-
|
365
|
-
|
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", "
|
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
|
11
|
-
@associations ||= {}.with_indifferent_access
|
12
|
-
end
|
10
|
+
def input(name, parser=nil, **options)
|
13
11
|
|
14
|
-
|
15
|
-
@inputs ||= {}.with_indifferent_access
|
16
|
-
end
|
12
|
+
_name = [name].flatten
|
17
13
|
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
31
|
-
|
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(
|
35
|
-
|
36
|
-
|
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
|
33
|
+
options: options,
|
39
34
|
type: :has_many
|
40
35
|
}
|
41
36
|
end
|
42
37
|
|
43
|
-
def has_one(
|
44
|
-
|
45
|
-
|
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
|
44
|
+
options: options,
|
48
45
|
type: :has_one
|
49
46
|
}
|
50
47
|
end
|
51
48
|
|
52
|
-
def
|
53
|
-
(
|
54
|
-
|
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
|
58
|
-
|
59
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
73
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
end
|
93
|
+
def handle_input(handler, values)
|
94
|
+
parse(handler[:key], handler[:parser], values, handler[:options])
|
95
|
+
end
|
79
96
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
data/lib/decanter/extensions.rb
CHANGED
@@ -5,32 +5,35 @@ module Decanter
|
|
5
5
|
base.extend(ClassMethods)
|
6
6
|
end
|
7
7
|
|
8
|
-
def decant_update(args
|
9
|
-
self.attributes = self.class.decant(args,
|
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
|
14
|
-
self.attributes = self.class.decant(args,
|
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
|
21
|
-
self.new(decant(args,
|
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
|
25
|
-
self.new(decant(args,
|
25
|
+
def decant_new(args, **options)
|
26
|
+
self.new(decant(args, options))
|
26
27
|
end
|
27
28
|
|
28
|
-
def decant_create!(args
|
29
|
-
self.new(decant(args,
|
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,
|
33
|
-
Decanter.decanter_for(self)
|
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
|
data/lib/decanter/railtie.rb
CHANGED
@@ -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,
|
11
|
+
def parse(name, values, options={})
|
13
12
|
|
14
|
-
|
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
|
20
|
+
return { name => nil }
|
19
21
|
end
|
20
22
|
end
|
21
23
|
|
22
|
-
if @allowed && @allowed.include?(
|
23
|
-
return
|
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
|
29
|
+
raise ArgumentError.new("No parser for argument: #{name} with types: #{value_ary.map(&:class).join(', ')}")
|
28
30
|
end
|
29
31
|
|
30
|
-
@
|
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,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
|
+
|
data/lib/decanter/version.rb
CHANGED
@@ -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
|
+
<% 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 -%>
|
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
|
+
version: 0.5.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Francis
|
8
|
-
-
|
8
|
+
- David Corwin
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2016-02-
|
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
|