decanter 1.1.10 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/bin/console +4 -3
  3. data/lib/decanter.rb +10 -18
  4. data/lib/decanter/base.rb +2 -0
  5. data/lib/decanter/configuration.rb +2 -1
  6. data/lib/decanter/core.rb +121 -140
  7. data/lib/decanter/decant.rb +11 -0
  8. data/lib/decanter/exceptions.rb +2 -0
  9. data/lib/decanter/extensions.rb +11 -11
  10. data/lib/decanter/parser.rb +5 -3
  11. data/lib/decanter/parser/base.rb +2 -1
  12. data/lib/decanter/parser/boolean_parser.rb +3 -2
  13. data/lib/decanter/parser/core.rb +9 -10
  14. data/lib/decanter/parser/date_parser.rb +7 -4
  15. data/lib/decanter/parser/date_time_parser.rb +21 -0
  16. data/lib/decanter/parser/float_parser.rb +5 -5
  17. data/lib/decanter/parser/hash_parser.rb +9 -6
  18. data/lib/decanter/parser/integer_parser.rb +4 -6
  19. data/lib/decanter/parser/join_parser.rb +4 -3
  20. data/lib/decanter/parser/key_value_splitter_parser.rb +6 -3
  21. data/lib/decanter/parser/pass_parser.rb +2 -1
  22. data/lib/decanter/parser/phone_parser.rb +3 -1
  23. data/lib/decanter/parser/string_parser.rb +3 -2
  24. data/lib/decanter/parser/time_parser.rb +19 -0
  25. data/lib/decanter/parser/utils.rb +3 -1
  26. data/lib/decanter/parser/value_parser.rb +4 -3
  27. data/lib/decanter/railtie.rb +11 -15
  28. data/lib/decanter/version.rb +3 -1
  29. data/lib/generators/decanter/install_generator.rb +5 -3
  30. data/lib/generators/decanter/templates/initializer.rb +2 -0
  31. data/lib/generators/rails/decanter_generator.rb +7 -5
  32. data/lib/generators/rails/parser_generator.rb +5 -3
  33. data/lib/generators/rails/resource_override.rb +2 -0
  34. metadata +19 -38
  35. data/.codeclimate.yml +0 -38
  36. data/.gitignore +0 -3
  37. data/.rspec +0 -2
  38. data/.ruby-version +0 -1
  39. data/Gemfile +0 -4
  40. data/Gemfile.lock +0 -102
  41. data/LICENSE.txt +0 -21
  42. data/README.md +0 -516
  43. data/Rakefile +0 -1
  44. data/decanter.gemspec +0 -39
  45. data/lib/decanter/parser/datetime_parser.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90171d776162cd88c420fa49fe3110e6abfb2526814f797758eba3657d09264a
4
- data.tar.gz: 57fa05b9c2d917b8082d70085c873f91619fa4a9b6142a88d0d715dd5fbbef61
3
+ metadata.gz: 881a76a5958f93883b6fe71571d149e0cd477a449484bc4d61df9fa7d49b7be0
4
+ data.tar.gz: d3cb5f4ac6eb4a1d576904112ba85721164ed987ef7285d9ab7258c0a85062cc
5
5
  SHA512:
6
- metadata.gz: 5a6f20987ae62f6a580ede57dc29e7bc70d96e352e796f065a394cbcb754683fdf472a80ef2e529606278935f4ee5162d91d9009034202d098384fcea0ad8342
7
- data.tar.gz: 3db467af7278eef616121b81b66dc09fa3d0780b62a53ed383cedca78fe5be9d83283b097fc63ace2ac7715459dd0c406ae81d2370151c82fa14e325e05bffab
6
+ metadata.gz: 430b80997c899726116d97b067d8c42f30efffb44c160b7e91238ad714b6bbc6b15527ee372637be5eb41c77c44ef87fec943b460bb80a61cfc88eecf38899c1
7
+ data.tar.gz: 5ce3bea3ce908809a6a233fa1d903128e37e039f7022969590cc9f6868f6ce6d3218aaa00618a8bd9b67acd53b722c679d5b0e665d6da593e8c3652f750737dc
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require "bundler/setup"
4
- require "decanter"
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 "irb"
14
+ require 'irb'
14
15
  IRB.start
@@ -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.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
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
- begin
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.new("cannot find decanter from #{klass_or_string} with class #{klass_or_string.class}")
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.new("#{constant.name} is not a decanter")
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
- @config ||= Decanter::Configuration.new
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)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'decanter/core'
2
4
 
3
5
  module Decanter
@@ -1,6 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Decanter
2
4
  class Configuration
3
-
4
5
  attr_accessor :strict
5
6
 
6
7
  def initialize
@@ -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
- _name = [name].flatten
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
- if _name.length > 1 && parsers.blank?
15
- raise ArgumentError.new("#{self.name} no parser specified for input with multiple values.")
16
- end
17
+ # rubocop:disable Naming/PredicateName
17
18
 
18
- handlers[_name] = {
19
- key: options.fetch(:key, _name.first),
20
- name: _name,
21
- options: options,
22
- parsers: parsers,
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
- 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
- }
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
- def has_one(assoc, **options)
38
- handlers[assoc] = {
39
- assoc: assoc,
40
- key: options.fetch(:key, assoc),
41
- name: assoc,
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
- type: :has_one
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, "#{self.name}: Unknown strict value #{mode}") unless [:with_exception, true, false].include? mode
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
- def handle_empty_args
65
- any_inputs_required? ? empty_args_error : {}
66
- end
83
+ if args.is_a?(ActionController::Parameters)
84
+ args.permit!
85
+ args = args.to_h
86
+ end
67
87
 
68
- def any_inputs_required?
69
- required_inputs.any?
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 |h|
74
- options = h.last[:options]
75
- h.first.first if options && options[:required]
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 any_inputs_required?
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(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')
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
- 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 }
104
-
105
- if unhandled_keys.any?
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
- arg_keys = args.keys.map(&:to_sym)
122
- inputs, assocs = handlers.values.partition { |handler| handler[:type] == :input }
123
-
124
- {}.merge(
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
- def handle(handler, args)
135
- values = args.values_at(*handler[:name])
136
- values = values.length == 1 ? values.first : values
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
- def handle_input(handler, args)
141
- values = args.values_at(*handler[:name])
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 handle_association(handler, args)
147
- assoc_handlers = [
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
- assoc_handler_names = assoc_handlers.map { |_handler| _handler[:name] }
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
- case args.values_at(*assoc_handler_names).compact.length
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 handle_has_many(handler, values)
169
- decanter = decanter_for_handler(handler)
170
- if values.is_a?(Hash)
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 handle_has_one(handler, values)
184
- { handler[:key] => decanter_for_handler(handler).decant(values) }
168
+ def handle_empty_args
169
+ required_inputs.any? ? empty_required_input_error : {}
185
170
  end
186
171
 
187
- def decanter_for_handler(handler)
188
- if specified_decanter = handler[:options][:decanter]
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
- def parse(key, parsers, values, options)
196
- case
197
- when !parsers
198
- { key => values }
199
- when options[:required] == true && Array.wrap(values).all? { |value| value.nil? || value == "" }
200
- raise ArgumentError.new("No value for required argument: #{key}")
201
- else
202
- Parser.parsers_for(parsers)
203
- .reduce({key => values}) do |vals_hash, parser|
204
- vals_hash.keys.reduce({}) { |acc, k| acc.merge(parser.parse(k, vals_hash[k], options)) }
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
 
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decanter
4
+ module Decant
5
+ extend ActiveSupport::Concern
6
+
7
+ def decant(decanter, params)
8
+ Decanter.decanter_for(decanter).decant(params)
9
+ end
10
+ end
11
+ end