decanter 1.1.10 → 2.1.0
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/bin/console +4 -3
- data/lib/decanter.rb +10 -18
- data/lib/decanter/base.rb +2 -0
- data/lib/decanter/configuration.rb +2 -1
- data/lib/decanter/core.rb +121 -140
- data/lib/decanter/decant.rb +11 -0
- data/lib/decanter/exceptions.rb +2 -0
- data/lib/decanter/extensions.rb +11 -11
- data/lib/decanter/parser.rb +5 -3
- data/lib/decanter/parser/base.rb +2 -1
- data/lib/decanter/parser/boolean_parser.rb +3 -2
- data/lib/decanter/parser/core.rb +9 -10
- data/lib/decanter/parser/date_parser.rb +7 -4
- data/lib/decanter/parser/date_time_parser.rb +21 -0
- data/lib/decanter/parser/float_parser.rb +5 -5
- data/lib/decanter/parser/hash_parser.rb +9 -6
- data/lib/decanter/parser/integer_parser.rb +4 -6
- data/lib/decanter/parser/join_parser.rb +4 -3
- data/lib/decanter/parser/key_value_splitter_parser.rb +6 -3
- data/lib/decanter/parser/pass_parser.rb +2 -1
- data/lib/decanter/parser/phone_parser.rb +3 -1
- data/lib/decanter/parser/string_parser.rb +3 -2
- data/lib/decanter/parser/time_parser.rb +19 -0
- data/lib/decanter/parser/utils.rb +3 -1
- data/lib/decanter/parser/value_parser.rb +4 -3
- data/lib/decanter/railtie.rb +11 -15
- data/lib/decanter/version.rb +3 -1
- data/lib/generators/decanter/install_generator.rb +5 -3
- data/lib/generators/decanter/templates/initializer.rb +2 -0
- data/lib/generators/rails/decanter_generator.rb +7 -5
- data/lib/generators/rails/parser_generator.rb +5 -3
- data/lib/generators/rails/resource_override.rb +2 -0
- metadata +19 -38
- data/.codeclimate.yml +0 -38
- data/.gitignore +0 -3
- data/.rspec +0 -2
- data/.ruby-version +0 -1
- data/Gemfile +0 -4
- data/Gemfile.lock +0 -102
- data/LICENSE.txt +0 -21
- data/README.md +0 -516
- data/Rakefile +0 -1
- data/decanter.gemspec +0 -39
- data/lib/decanter/parser/datetime_parser.rb +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 881a76a5958f93883b6fe71571d149e0cd477a449484bc4d61df9fa7d49b7be0
|
4
|
+
data.tar.gz: d3cb5f4ac6eb4a1d576904112ba85721164ed987ef7285d9ab7258c0a85062cc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 430b80997c899726116d97b067d8c42f30efffb44c160b7e91238ad714b6bbc6b15527ee372637be5eb41c77c44ef87fec943b460bb80a61cfc88eecf38899c1
|
7
|
+
data.tar.gz: 5ce3bea3ce908809a6a233fa1d903128e37e039f7022969590cc9f6868f6ce6d3218aaa00618a8bd9b67acd53b722c679d5b0e665d6da593e8c3652f750737dc
|
data/bin/console
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
require
|
4
|
-
require
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'decanter'
|
5
6
|
|
6
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
7
8
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -10,5 +11,5 @@ require "decanter"
|
|
10
11
|
# require "pry"
|
11
12
|
# Pry.start
|
12
13
|
|
13
|
-
require
|
14
|
+
require 'irb'
|
14
15
|
IRB.start
|
data/lib/decanter.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support/all'
|
2
4
|
|
3
5
|
module Decanter
|
4
|
-
|
5
6
|
class << self
|
6
|
-
|
7
7
|
def decanter_for(klass_or_sym)
|
8
8
|
decanter_name =
|
9
9
|
case klass_or_sym
|
@@ -12,13 +12,9 @@ module Decanter
|
|
12
12
|
when Symbol
|
13
13
|
klass_or_sym.to_s.singularize.camelize
|
14
14
|
else
|
15
|
-
raise ArgumentError
|
16
|
-
end
|
17
|
-
|
18
|
-
decanter_name.constantize
|
19
|
-
rescue
|
20
|
-
raise NameError.new("uninitialized constant #{decanter_name}")
|
21
|
-
end
|
15
|
+
raise ArgumentError, "cannot lookup decanter for #{klass_or_sym} with class #{klass_or_sym.class}"
|
16
|
+
end + 'Decanter'
|
17
|
+
decanter_name.constantize
|
22
18
|
end
|
23
19
|
|
24
20
|
def decanter_from(klass_or_string)
|
@@ -27,24 +23,20 @@ module Decanter
|
|
27
23
|
when Class
|
28
24
|
klass_or_string
|
29
25
|
when String
|
30
|
-
|
31
|
-
klass_or_string.constantize
|
32
|
-
rescue
|
33
|
-
raise NameError.new("uninitialized constant #{klass_or_string}")
|
34
|
-
end
|
26
|
+
klass_or_string.constantize
|
35
27
|
else
|
36
|
-
raise ArgumentError
|
28
|
+
raise ArgumentError, "cannot find decanter from #{klass_or_string} with class #{klass_or_string.class}"
|
37
29
|
end
|
38
30
|
|
39
31
|
unless constant.ancestors.include? Decanter::Base
|
40
|
-
raise ArgumentError
|
32
|
+
raise ArgumentError, "#{constant.name} is not a decanter"
|
41
33
|
end
|
42
34
|
|
43
35
|
constant
|
44
36
|
end
|
45
37
|
|
46
38
|
def configuration
|
47
|
-
@
|
39
|
+
@configuration ||= Decanter::Configuration.new
|
48
40
|
end
|
49
41
|
|
50
42
|
def config
|
@@ -62,4 +54,4 @@ require 'decanter/base'
|
|
62
54
|
require 'decanter/extensions'
|
63
55
|
require 'decanter/exceptions'
|
64
56
|
require 'decanter/parser'
|
65
|
-
require 'decanter/railtie' if defined?(::Rails)
|
57
|
+
require 'decanter/railtie' if defined?(::Rails)
|
data/lib/decanter/base.rb
CHANGED
data/lib/decanter/core.rb
CHANGED
@@ -1,212 +1,193 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Decanter
|
2
4
|
module Core
|
3
|
-
|
4
5
|
def self.included(base)
|
5
6
|
base.extend(ClassMethods)
|
6
7
|
end
|
7
8
|
|
8
9
|
module ClassMethods
|
9
|
-
|
10
|
-
def input(name, parsers=nil, **options)
|
11
|
-
|
12
|
-
|
10
|
+
# Declare an ordinary parameter transformation.
|
11
|
+
def input(name, parsers = nil, **options)
|
12
|
+
options[:type] = :input
|
13
|
+
options[:parsers] = parsers
|
14
|
+
handler(name, options)
|
15
|
+
end
|
13
16
|
|
14
|
-
|
15
|
-
raise ArgumentError.new("#{self.name} no parser specified for input with multiple values.")
|
16
|
-
end
|
17
|
+
# rubocop:disable Naming/PredicateName
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
type: :input
|
24
|
-
}
|
19
|
+
# Declare a _has many_ association for a parameter.
|
20
|
+
def has_many(name, **options)
|
21
|
+
options[:type] = :has_many
|
22
|
+
options[:assoc] = name
|
23
|
+
handler(name, options)
|
25
24
|
end
|
26
25
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
options: options,
|
33
|
-
type: :has_many
|
34
|
-
}
|
26
|
+
# Declare a _has one_ association for a parameter.
|
27
|
+
def has_one(name, **options)
|
28
|
+
options[:type] = :has_one
|
29
|
+
options[:assoc] = name
|
30
|
+
handler(name, options)
|
35
31
|
end
|
32
|
+
# rubocop:enable Naming/PredicateName
|
33
|
+
|
34
|
+
# Add a parameter handler to the class. Takes a name, and a set of
|
35
|
+
# options. This is a generic method for any sort of handler, e.g.
|
36
|
+
#
|
37
|
+
# handle :foo, type: :input, parsers: [:string], as: :bar
|
38
|
+
def handler(name, options)
|
39
|
+
name = options.fetch(:as, name)
|
40
|
+
parsers = options.delete(:parsers)
|
41
|
+
|
42
|
+
if Array(name).length > 1 && parsers.blank?
|
43
|
+
raise ArgumentError,
|
44
|
+
"#{name} no parser specified for input with multiple values."
|
45
|
+
end
|
36
46
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
47
|
+
if handlers.key?(name)
|
48
|
+
raise ArgumentError, "Handler for #{name} already defined"
|
49
|
+
end
|
50
|
+
|
51
|
+
handlers[name] = {
|
52
|
+
key: options.fetch(:key, Array(name).first),
|
53
|
+
assoc: options.delete(:assoc),
|
54
|
+
type: options.delete(:type),
|
42
55
|
options: options,
|
43
|
-
|
56
|
+
parsers: Array(parsers)
|
44
57
|
}
|
45
58
|
end
|
46
59
|
|
60
|
+
# List of parameters to ignore.
|
47
61
|
def ignore(*args)
|
48
62
|
keys_to_ignore.push(*args)
|
49
63
|
end
|
50
64
|
|
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
|
+
#
|
51
72
|
def strict(mode)
|
52
|
-
raise(ArgumentError, "#{
|
73
|
+
raise(ArgumentError, "#{name}: Unknown strict value #{mode}") unless [:with_exception, true, false].include? mode
|
53
74
|
@strict_mode = mode
|
54
75
|
end
|
55
76
|
|
77
|
+
# Take a parameter hash, and handle it with the various decanters
|
78
|
+
# defined.
|
56
79
|
def decant(args)
|
57
80
|
return handle_empty_args if args.blank?
|
58
81
|
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
|
63
82
|
|
64
|
-
|
65
|
-
|
66
|
-
|
83
|
+
if args.is_a?(ActionController::Parameters)
|
84
|
+
args.permit!
|
85
|
+
args = args.to_h
|
86
|
+
end
|
67
87
|
|
68
|
-
|
69
|
-
|
88
|
+
args = args.deep_symbolize_keys
|
89
|
+
handled_keys(args).merge(unhandled_keys(args))
|
70
90
|
end
|
71
91
|
|
72
92
|
def required_inputs
|
73
|
-
handlers.map do |
|
74
|
-
|
75
|
-
|
76
|
-
end
|
93
|
+
handlers.map do |name, handler|
|
94
|
+
name if handler[:options][:required]
|
95
|
+
end
|
77
96
|
end
|
78
97
|
|
79
|
-
def required_input_keys_present?(args={})
|
80
|
-
return true unless
|
98
|
+
def required_input_keys_present?(args = {})
|
99
|
+
return true unless required_inputs.any?
|
81
100
|
compact_inputs = required_inputs.compact
|
82
101
|
compact_inputs.all? do |input|
|
83
102
|
args.keys.map(&:to_sym).include?(input) && !args[input].nil?
|
84
103
|
end
|
85
104
|
end
|
86
105
|
|
87
|
-
def empty_required_input_error
|
88
|
-
raise
|
89
|
-
end
|
90
|
-
|
91
|
-
def empty_args_error
|
92
|
-
raise(ArgumentError, 'Decanter has required inputs but no values were passed')
|
106
|
+
def empty_required_input_error(name = nil)
|
107
|
+
raise MissingRequiredInputValue, "No value found for required argument #{name}"
|
93
108
|
end
|
94
109
|
|
95
110
|
# protected
|
96
111
|
|
97
112
|
def unhandled_keys(args)
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
case strict_mode
|
107
|
-
when true
|
108
|
-
p "#{self.name} ignoring unhandled keys: #{unhandled_keys.join(', ')}."
|
109
|
-
{}
|
110
|
-
when :with_exception
|
111
|
-
raise(UnhandledKeysError, "#{self.name} received unhandled keys: #{unhandled_keys.join(', ')}.")
|
112
|
-
else
|
113
|
-
args.select { |key| unhandled_keys.include? key }
|
114
|
-
end
|
115
|
-
else
|
113
|
+
unhandled = args.keys
|
114
|
+
unhandled -= keys_to_ignore
|
115
|
+
unhandled -= handlers.keys.flatten
|
116
|
+
|
117
|
+
return {} unless unhandled.any?
|
118
|
+
|
119
|
+
case strict_mode
|
120
|
+
when true
|
116
121
|
{}
|
122
|
+
when :with_exception
|
123
|
+
raise(UnhandledKeysError, "#{name} received unhandled keys: #{unhandled.join(', ')}.")
|
124
|
+
else
|
125
|
+
args.select { |key| unhandled.include? key }
|
117
126
|
end
|
118
127
|
end
|
119
128
|
|
120
129
|
def handled_keys(args)
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
# Inputs
|
126
|
-
inputs.select { |handler| (arg_keys & handler[:name]).any? }
|
127
|
-
.reduce({}) { |memo, handler| memo.merge handle_input(handler, args) }
|
128
|
-
).merge(
|
129
|
-
# Associations
|
130
|
-
assocs.reduce({}) { |memo, handler| memo.merge handle_association(handler, args) }
|
131
|
-
)
|
132
|
-
end
|
130
|
+
handlers.reduce({}) do |m, h|
|
131
|
+
name, handler = *h
|
132
|
+
values = args.values_at(*name)
|
133
|
+
values = values.length == 1 ? values.first : values
|
133
134
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
self.send("handle_#{handler[:type]}", handler, values)
|
138
|
-
end
|
135
|
+
if handler[:options][:required] && Array(values).all?(&:blank?)
|
136
|
+
empty_required_input_error(name)
|
137
|
+
end
|
139
138
|
|
140
|
-
|
141
|
-
|
142
|
-
values = values.length == 1 ? values.first : values
|
143
|
-
parse(handler[:key], handler[:parsers], values, handler[:options])
|
139
|
+
m.merge handle(handler, values)
|
140
|
+
end
|
144
141
|
end
|
145
142
|
|
146
|
-
def
|
147
|
-
|
148
|
-
handler,
|
149
|
-
handler.merge({
|
150
|
-
key: handler[:options].fetch(:key, "#{handler[:name]}_attributes").to_sym,
|
151
|
-
name: "#{handler[:name]}_attributes".to_sym
|
152
|
-
})
|
153
|
-
]
|
143
|
+
def handle(handler, values)
|
144
|
+
decanter = decanter_for_handler(handler) unless handler[:type] == :input
|
154
145
|
|
155
|
-
|
146
|
+
val = case handler[:type]
|
147
|
+
when :input
|
148
|
+
parse(handler[:parsers], values, handler[:options])
|
149
|
+
when :has_one
|
150
|
+
decanter.decant(values)
|
151
|
+
when :has_many
|
152
|
+
# should sort here, really.
|
153
|
+
values = values.values if values.is_a?(Hash)
|
154
|
+
values.compact.map { |v| decanter.decant(v) }
|
155
|
+
end
|
156
156
|
|
157
|
-
|
158
|
-
when 0
|
159
|
-
{}
|
160
|
-
when 1
|
161
|
-
_handler = assoc_handlers.detect { |_handler| args.has_key?(_handler[:name]) }
|
162
|
-
self.send("handle_#{_handler[:type]}", _handler, args[_handler[:name]])
|
163
|
-
else
|
164
|
-
raise ArgumentError.new("Handler #{handler[:name]} matches multiple keys: #{assoc_handler_names}.")
|
165
|
-
end
|
157
|
+
{ handler[:key] => val }
|
166
158
|
end
|
167
159
|
|
168
|
-
def
|
169
|
-
|
170
|
-
|
171
|
-
parsed_values = values.map do |index, input_values|
|
172
|
-
next if input_values.nil?
|
173
|
-
decanter.decant(input_values)
|
174
|
-
end
|
175
|
-
return { handler[:key] => parsed_values }
|
160
|
+
def decanter_for_handler(handler)
|
161
|
+
if (specified_decanter = handler[:options][:decanter])
|
162
|
+
Decanter.decanter_from(specified_decanter)
|
176
163
|
else
|
177
|
-
|
178
|
-
handler[:key] => values.compact.map { |value| decanter.decant(value) }
|
179
|
-
}
|
164
|
+
Decanter.decanter_for(handler[:assoc])
|
180
165
|
end
|
181
166
|
end
|
182
167
|
|
183
|
-
def
|
184
|
-
|
168
|
+
def handle_empty_args
|
169
|
+
required_inputs.any? ? empty_required_input_error : {}
|
185
170
|
end
|
186
171
|
|
187
|
-
def
|
188
|
-
|
189
|
-
Decanter::decanter_from(specified_decanter)
|
190
|
-
else
|
191
|
-
Decanter::decanter_for(handler[:assoc])
|
192
|
-
end
|
193
|
-
end
|
172
|
+
def parse(parsers, values, options)
|
173
|
+
return values if parsers.nil?
|
194
174
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
end
|
175
|
+
Parser.parsers_for(parsers).each do |parser|
|
176
|
+
unless values.is_a?(Hash)
|
177
|
+
values = parser.parse(values, options)
|
178
|
+
next
|
179
|
+
end
|
180
|
+
|
181
|
+
# For hashes, we operate the parser on each member
|
182
|
+
values.each do |k, v|
|
183
|
+
values[k] = parser.parse(v, options)
|
184
|
+
end
|
206
185
|
end
|
186
|
+
|
187
|
+
values
|
207
188
|
end
|
208
189
|
|
209
|
-
def handlers
|
190
|
+
def handlers
|
210
191
|
@handlers ||= {}
|
211
192
|
end
|
212
193
|
|