decanter 2.1.1 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.codeclimate.yml +38 -0
- data/.github/ISSUE_TEMPLATE/BUG_REPORT.md +33 -0
- data/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md +18 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +8 -0
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +10 -0
- data/CODE_OF_CONDUCT.md +76 -0
- data/CONTRIBUTING.md +36 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +102 -0
- data/LICENSE.txt +21 -0
- data/README.md +241 -0
- data/Rakefile +1 -0
- data/bin/console +3 -4
- data/decanter.gemspec +39 -0
- data/lib/decanter.rb +18 -10
- data/lib/decanter/base.rb +0 -2
- data/lib/decanter/configuration.rb +2 -3
- data/lib/decanter/core.rb +136 -126
- data/lib/decanter/exceptions.rb +4 -7
- data/lib/decanter/extensions.rb +11 -11
- data/lib/decanter/parser.rb +13 -5
- data/lib/decanter/parser/array_parser.rb +28 -0
- data/lib/decanter/parser/base.rb +1 -2
- data/lib/decanter/parser/boolean_parser.rb +4 -3
- data/lib/decanter/parser/compose_parser.rb +27 -0
- data/lib/decanter/parser/core.rb +8 -16
- data/lib/decanter/parser/date_parser.rb +6 -7
- data/lib/decanter/parser/datetime_parser.rb +15 -0
- data/lib/decanter/parser/float_parser.rb +7 -5
- data/lib/decanter/parser/hash_parser.rb +6 -9
- data/lib/decanter/parser/integer_parser.rb +8 -4
- data/lib/decanter/parser/pass_parser.rb +5 -3
- data/lib/decanter/parser/phone_parser.rb +3 -3
- data/lib/decanter/parser/string_parser.rb +4 -5
- data/lib/decanter/parser/utils.rb +1 -3
- data/lib/decanter/parser/value_parser.rb +3 -4
- data/lib/decanter/railtie.rb +15 -11
- data/lib/decanter/version.rb +1 -3
- data/lib/generators/decanter/install_generator.rb +3 -5
- data/lib/generators/decanter/templates/initializer.rb +1 -4
- data/lib/generators/rails/decanter_generator.rb +5 -7
- data/lib/generators/rails/parser_generator.rb +3 -5
- data/lib/generators/rails/resource_override.rb +0 -2
- data/migration-guides/v3.0.0.md +21 -0
- metadata +47 -20
- data/lib/decanter/decant.rb +0 -11
- data/lib/decanter/parser/date_time_parser.rb +0 -21
- data/lib/decanter/parser/join_parser.rb +0 -14
- data/lib/decanter/parser/key_value_splitter_parser.rb +0 -18
- data/lib/decanter/parser/time_parser.rb +0 -19
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/console
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
2
|
|
4
|
-
require
|
5
|
-
require
|
3
|
+
require "bundler/setup"
|
4
|
+
require "decanter"
|
6
5
|
|
7
6
|
# You can add fixtures and/or initialization code here to make experimenting
|
8
7
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -11,5 +10,5 @@ require 'decanter'
|
|
11
10
|
# require "pry"
|
12
11
|
# Pry.start
|
13
12
|
|
14
|
-
require
|
13
|
+
require "irb"
|
15
14
|
IRB.start
|
data/decanter.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
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', 'David 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 'actionpack', '>= 4.2.10'
|
31
|
+
spec.add_dependency 'activesupport'
|
32
|
+
spec.add_dependency 'rails-html-sanitizer', '>= 1.0.4'
|
33
|
+
|
34
|
+
spec.add_development_dependency 'bundler', '~> 1.9'
|
35
|
+
spec.add_development_dependency 'dotenv'
|
36
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
37
|
+
spec.add_development_dependency 'rspec-rails'
|
38
|
+
spec.add_development_dependency 'simplecov', '~> 0.15.1'
|
39
|
+
end
|
data/lib/decanter.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
require 'active_support/all'
|
4
2
|
|
5
3
|
module Decanter
|
4
|
+
|
6
5
|
class << self
|
6
|
+
|
7
7
|
def decanter_for(klass_or_sym)
|
8
8
|
decanter_name =
|
9
9
|
case klass_or_sym
|
@@ -12,9 +12,13 @@ module Decanter
|
|
12
12
|
when Symbol
|
13
13
|
klass_or_sym.to_s.singularize.camelize
|
14
14
|
else
|
15
|
-
raise ArgumentError
|
16
|
-
end
|
17
|
-
|
15
|
+
raise ArgumentError.new("cannot lookup decanter for #{klass_or_sym} with class #{klass_or_sym.class}")
|
16
|
+
end.concat('Decanter')
|
17
|
+
begin
|
18
|
+
decanter_name.constantize
|
19
|
+
rescue
|
20
|
+
raise NameError.new("uninitialized constant #{decanter_name}")
|
21
|
+
end
|
18
22
|
end
|
19
23
|
|
20
24
|
def decanter_from(klass_or_string)
|
@@ -23,20 +27,24 @@ module Decanter
|
|
23
27
|
when Class
|
24
28
|
klass_or_string
|
25
29
|
when String
|
26
|
-
|
30
|
+
begin
|
31
|
+
klass_or_string.constantize
|
32
|
+
rescue
|
33
|
+
raise NameError.new("uninitialized constant #{klass_or_string}")
|
34
|
+
end
|
27
35
|
else
|
28
|
-
raise ArgumentError
|
36
|
+
raise ArgumentError.new("cannot find decanter from #{klass_or_string} with class #{klass_or_string.class}")
|
29
37
|
end
|
30
38
|
|
31
39
|
unless constant.ancestors.include? Decanter::Base
|
32
|
-
raise ArgumentError
|
40
|
+
raise ArgumentError.new("#{constant.name} is not a decanter")
|
33
41
|
end
|
34
42
|
|
35
43
|
constant
|
36
44
|
end
|
37
45
|
|
38
46
|
def configuration
|
39
|
-
@
|
47
|
+
@config ||= Decanter::Configuration.new
|
40
48
|
end
|
41
49
|
|
42
50
|
def config
|
@@ -54,4 +62,4 @@ require 'decanter/base'
|
|
54
62
|
require 'decanter/extensions'
|
55
63
|
require 'decanter/exceptions'
|
56
64
|
require 'decanter/parser'
|
57
|
-
require 'decanter/railtie' if defined?(::Rails)
|
65
|
+
require 'decanter/railtie' if defined?(::Rails)
|
data/lib/decanter/base.rb
CHANGED
data/lib/decanter/core.rb
CHANGED
@@ -1,196 +1,197 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module Decanter
|
4
2
|
module Core
|
3
|
+
|
5
4
|
def self.included(base)
|
6
5
|
base.extend(ClassMethods)
|
7
6
|
end
|
8
7
|
|
9
8
|
module ClassMethods
|
10
|
-
|
11
|
-
def input(name, parsers
|
12
|
-
options[:type] = :input
|
13
|
-
options[:parsers] = parsers
|
14
|
-
handler(name, options)
|
15
|
-
end
|
9
|
+
|
10
|
+
def input(name, parsers=nil, **options)
|
16
11
|
|
17
|
-
|
12
|
+
_name = [name].flatten
|
18
13
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
options[:assoc] = name
|
23
|
-
handler(name, options)
|
24
|
-
end
|
14
|
+
if _name.length > 1 && parsers.blank?
|
15
|
+
raise ArgumentError.new("#{self.name} no parser specified for input with multiple values.")
|
16
|
+
end
|
25
17
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
18
|
+
handlers[_name] = {
|
19
|
+
key: options.fetch(:key, _name.first),
|
20
|
+
name: _name,
|
21
|
+
options: options,
|
22
|
+
parsers: parsers,
|
23
|
+
type: :input
|
24
|
+
}
|
31
25
|
end
|
32
|
-
# rubocop:enable Naming/PredicateName
|
33
26
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
raise ArgumentError,
|
44
|
-
"#{name} no parser specified for input with multiple values."
|
45
|
-
end
|
46
|
-
|
47
|
-
if handlers.key?(name)
|
48
|
-
raise ArgumentError, "Handler for #{name} already defined"
|
49
|
-
end
|
27
|
+
def has_many(assoc, **options)
|
28
|
+
handlers[assoc] = {
|
29
|
+
assoc: assoc,
|
30
|
+
key: options.fetch(:key, assoc),
|
31
|
+
name: assoc,
|
32
|
+
options: options,
|
33
|
+
type: :has_many
|
34
|
+
}
|
35
|
+
end
|
50
36
|
|
51
|
-
|
52
|
-
|
53
|
-
assoc:
|
54
|
-
|
37
|
+
def has_one(assoc, **options)
|
38
|
+
handlers[assoc] = {
|
39
|
+
assoc: assoc,
|
40
|
+
key: options.fetch(:key, assoc),
|
41
|
+
name: assoc,
|
55
42
|
options: options,
|
56
|
-
|
43
|
+
type: :has_one
|
57
44
|
}
|
58
45
|
end
|
59
46
|
|
60
|
-
# List of parameters to ignore.
|
61
47
|
def ignore(*args)
|
62
48
|
keys_to_ignore.push(*args)
|
63
49
|
end
|
64
50
|
|
65
|
-
# Set a level of strictness when dealing with parameters that are present
|
66
|
-
# but not expected.
|
67
|
-
#
|
68
|
-
# with_exception: Raise an exception
|
69
|
-
# true: Delete the parameter
|
70
|
-
# false: Allow the parameter through
|
71
|
-
#
|
72
51
|
def strict(mode)
|
73
|
-
raise(ArgumentError, "#{name}: Unknown strict value #{mode}") unless [
|
52
|
+
raise(ArgumentError, "#{self.name}: Unknown strict value #{mode}") unless [true, false].include? mode
|
74
53
|
@strict_mode = mode
|
75
54
|
end
|
76
55
|
|
77
|
-
# Take a parameter hash, and handle it with the various decanters
|
78
|
-
# defined.
|
79
56
|
def decant(args)
|
80
57
|
return handle_empty_args if args.blank?
|
81
58
|
return empty_required_input_error unless required_input_keys_present?(args)
|
59
|
+
args = args.to_unsafe_h.with_indifferent_access if args.class.name == 'ActionController::Parameters'
|
60
|
+
{}.merge( unhandled_keys(args) )
|
61
|
+
.merge( handled_keys(args) )
|
62
|
+
end
|
82
63
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
end
|
64
|
+
def handle_empty_args
|
65
|
+
any_inputs_required? ? empty_args_error : {}
|
66
|
+
end
|
87
67
|
|
88
|
-
|
89
|
-
|
68
|
+
def any_inputs_required?
|
69
|
+
required_inputs.any?
|
90
70
|
end
|
91
71
|
|
92
72
|
def required_inputs
|
93
|
-
handlers.map do |
|
94
|
-
|
95
|
-
|
73
|
+
handlers.map do |h|
|
74
|
+
options = h.last[:options]
|
75
|
+
h.first.first if options && options[:required]
|
76
|
+
end
|
96
77
|
end
|
97
78
|
|
98
|
-
def required_input_keys_present?(args
|
99
|
-
return true unless
|
79
|
+
def required_input_keys_present?(args={})
|
80
|
+
return true unless any_inputs_required?
|
100
81
|
compact_inputs = required_inputs.compact
|
101
82
|
compact_inputs.all? do |input|
|
102
83
|
args.keys.map(&:to_sym).include?(input) && !args[input].nil?
|
103
84
|
end
|
104
85
|
end
|
105
86
|
|
106
|
-
def empty_required_input_error
|
107
|
-
raise
|
87
|
+
def empty_required_input_error
|
88
|
+
raise(MissingRequiredInputValue, 'Required inputs have been declared, but no values for those inputs were passed.')
|
89
|
+
end
|
90
|
+
|
91
|
+
def empty_args_error
|
92
|
+
raise(ArgumentError, 'Decanter has required inputs but no values were passed')
|
108
93
|
end
|
109
94
|
|
110
95
|
# protected
|
111
96
|
|
112
97
|
def unhandled_keys(args)
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
98
|
+
unhandled_keys = args.keys.map(&:to_sym) -
|
99
|
+
handlers.keys.flatten.uniq -
|
100
|
+
keys_to_ignore -
|
101
|
+
handlers.values
|
102
|
+
.select { |handler| handler[:type] != :input }
|
103
|
+
.map { |handler| "#{handler[:name]}_attributes".to_sym }
|
118
104
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
when :with_exception
|
123
|
-
raise(UnhandledKeysError, "#{name} received unhandled keys: #{unhandled.join(', ')}.")
|
124
|
-
else
|
125
|
-
args.select { |key| unhandled.include? key }
|
126
|
-
end
|
105
|
+
return {} unless unhandled_keys.any?
|
106
|
+
raise(UnhandledKeysError, "#{self.name} received unhandled keys: #{unhandled_keys.join(', ')}.") if strict_mode
|
107
|
+
args.select { |key| unhandled_keys.include? key }
|
127
108
|
end
|
128
109
|
|
129
110
|
def handled_keys(args)
|
130
|
-
|
131
|
-
|
132
|
-
values = args.values_at(*name)
|
133
|
-
values = values.length == 1 ? values.first : values
|
134
|
-
|
135
|
-
is_empty_input = Array(values).all?(&:blank?)
|
136
|
-
if is_empty_input
|
137
|
-
empty_required_input_error(name) if handler[:options][:required]
|
138
|
-
# Skip handling empty inputs
|
139
|
-
next acc
|
140
|
-
end
|
111
|
+
arg_keys = args.keys.map(&:to_sym)
|
112
|
+
inputs, assocs = handlers.values.partition { |handler| handler[:type] == :input }
|
141
113
|
|
142
|
-
|
143
|
-
|
114
|
+
{}.merge(
|
115
|
+
# Inputs
|
116
|
+
inputs.select { |handler| (arg_keys & handler[:name]).any? }
|
117
|
+
.reduce({}) { |memo, handler| memo.merge handle_input(handler, args) }
|
118
|
+
).merge(
|
119
|
+
# Associations
|
120
|
+
assocs.reduce({}) { |memo, handler| memo.merge handle_association(handler, args) }
|
121
|
+
)
|
144
122
|
end
|
145
123
|
|
146
|
-
def handle(handler,
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
parse(handler[:parsers], values, handler[:options])
|
152
|
-
when :has_one
|
153
|
-
decanter.decant(values)
|
154
|
-
when :has_many
|
155
|
-
# should sort here, really.
|
156
|
-
values = values.values if values.is_a?(Hash)
|
157
|
-
values.compact.map { |v| decanter.decant(v) }
|
158
|
-
end
|
124
|
+
def handle(handler, args)
|
125
|
+
values = args.values_at(*handler[:name])
|
126
|
+
values = values.length == 1 ? values.first : values
|
127
|
+
self.send("handle_#{handler[:type]}", handler, values)
|
128
|
+
end
|
159
129
|
|
160
|
-
|
130
|
+
def handle_input(handler, args)
|
131
|
+
values = args.values_at(*handler[:name])
|
132
|
+
values = values.length == 1 ? values.first : values
|
133
|
+
parse(handler[:key], handler[:parsers], values, handler[:options])
|
161
134
|
end
|
162
135
|
|
163
|
-
def
|
164
|
-
|
165
|
-
|
136
|
+
def handle_association(handler, args)
|
137
|
+
assoc_handlers = [
|
138
|
+
handler,
|
139
|
+
handler.merge({
|
140
|
+
key: handler[:options].fetch(:key, "#{handler[:name]}_attributes").to_sym,
|
141
|
+
name: "#{handler[:name]}_attributes".to_sym
|
142
|
+
})
|
143
|
+
]
|
144
|
+
|
145
|
+
assoc_handler_names = assoc_handlers.map { |_handler| _handler[:name] }
|
146
|
+
|
147
|
+
case args.values_at(*assoc_handler_names).compact.length
|
148
|
+
when 0
|
149
|
+
{}
|
150
|
+
when 1
|
151
|
+
_handler = assoc_handlers.detect { |_handler| args.has_key?(_handler[:name]) }
|
152
|
+
self.send("handle_#{_handler[:type]}", _handler, args[_handler[:name]])
|
166
153
|
else
|
167
|
-
|
154
|
+
raise ArgumentError.new("Handler #{handler[:name]} matches multiple keys: #{assoc_handler_names}.")
|
168
155
|
end
|
169
156
|
end
|
170
157
|
|
171
|
-
def
|
172
|
-
|
158
|
+
def handle_has_many(handler, values)
|
159
|
+
decanter = decanter_for_handler(handler)
|
160
|
+
if values.is_a?(Hash)
|
161
|
+
parsed_values = values.map do |index, input_values|
|
162
|
+
next if input_values.nil?
|
163
|
+
decanter.decant(input_values)
|
164
|
+
end
|
165
|
+
return { handler[:key] => parsed_values }
|
166
|
+
else
|
167
|
+
{
|
168
|
+
handler[:key] => values.compact.map { |value| decanter.decant(value) }
|
169
|
+
}
|
170
|
+
end
|
173
171
|
end
|
174
172
|
|
175
|
-
def
|
176
|
-
|
177
|
-
|
178
|
-
Parser.parsers_for(parsers).each do |parser|
|
179
|
-
unless values.is_a?(Hash)
|
180
|
-
values = parser.parse(values, options)
|
181
|
-
next
|
182
|
-
end
|
173
|
+
def handle_has_one(handler, values)
|
174
|
+
{ handler[:key] => decanter_for_handler(handler).decant(values) }
|
175
|
+
end
|
183
176
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
177
|
+
def decanter_for_handler(handler)
|
178
|
+
if specified_decanter = handler[:options][:decanter]
|
179
|
+
Decanter::decanter_from(specified_decanter)
|
180
|
+
else
|
181
|
+
Decanter::decanter_for(handler[:assoc])
|
188
182
|
end
|
183
|
+
end
|
189
184
|
|
190
|
-
|
185
|
+
def parse(key, parsers, value, options)
|
186
|
+
return { key => value } unless parsers
|
187
|
+
if options[:required] && value_missing?(value)
|
188
|
+
raise ArgumentError.new("No value for required argument: #{key}")
|
189
|
+
end
|
190
|
+
parser_classes = Parser.parsers_for(parsers)
|
191
|
+
Parser.compose_parsers(parser_classes).parse(key, value, options)
|
191
192
|
end
|
192
193
|
|
193
|
-
def handlers
|
194
|
+
def handlers
|
194
195
|
@handlers ||= {}
|
195
196
|
end
|
196
197
|
|
@@ -201,6 +202,15 @@ module Decanter
|
|
201
202
|
def strict_mode
|
202
203
|
@strict_mode.nil? ? Decanter.configuration.strict : @strict_mode
|
203
204
|
end
|
205
|
+
|
206
|
+
# Helpers
|
207
|
+
|
208
|
+
private
|
209
|
+
|
210
|
+
def value_missing?(value)
|
211
|
+
value.nil? || value == ""
|
212
|
+
end
|
213
|
+
|
204
214
|
end
|
205
215
|
end
|
206
216
|
end
|