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.
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