decanter 3.1.2 → 3.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb694b13aa7696d65c4608fcf10a1d1d23af16f00be59d8728596c126d97bf31
4
- data.tar.gz: 233c7c16256e602b0cb1e9307f2ca4b9fadc91539ddb69d71ce1e68ba71f90c3
3
+ metadata.gz: 91c0d17331090df0653fd6750102bddf1506bd74f73920e8a4281d61cf3190b0
4
+ data.tar.gz: 620711a2e9819afe1a66055b41e70afdfb113b637362053784d53cc79df4f541
5
5
  SHA512:
6
- metadata.gz: 9509797f7e4c1c9000c5d0bb1c500ad91bfefd51d2c8e06975bace2010bd944b21c02eedeccd9d3d76b9deb0a55843875473478ed5f188ab439b5778f29df605
7
- data.tar.gz: 83372fc430c9fad63051ad53405bb922c9f8f451e4902b095c2a3feaa1440069400154064c5da8f0b331a494d89baed54e2de0c7ed122338ab1f6d7721095c48
6
+ metadata.gz: 2da563150e6a55e066f08200245175b69e8d404e4a56a99af1ea9a211ff4d3f140feec4658d97c0e09ea8f7e73f42e08d819f36e2fbac0815852fb629142bce1
7
+ data.tar.gz: a03440c46bb573f6e42692b2eea31d576e57495bdb0d54f6cc1b1722b973496c251de053e92c0ed56614fa35d912c5b14f2e1782ffe879c3b17c0891bbc4dbf0
@@ -33,4 +33,4 @@ For any non-trivial change, we prefer an issue to be created first. This helps u
33
33
  ### Sending a Pull Request
34
34
  If this is your first Pull Request, we recommend learning how to navigate GitHub via this free video tutorial: [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github).
35
35
 
36
- The LaunchPad Lab team monitors this repository for pull requests. Once a pull request has been created from a forked repository that **meets all the guidelines** outlined in the [Pull Request Checklist](.github/PULL_REQUEST_TEMPLATE.md), we will review the request and either merge it, request changes, or close it with an explanation. We aim to respond to pull requests within 48 hours.
36
+ The LaunchPad Lab team monitors this repository for pull requests. Once a pull request has been created from a forked repository that **meets all the guidelines** outlined in the [Pull Request Checklist](.github/PULL_REQUEST_TEMPLATE.md), we will review the request and either merge it, request changes, or close it with an explanation. We aim to respond to pull requests within 48 hours.
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in goaltender.gemspec
3
+ # Specify your gem's dependencies in decanter.gemspec
4
4
  gemspec
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- decanter (3.1.2)
4
+ decanter (3.4.1)
5
5
  actionpack (>= 4.2.10)
6
6
  activesupport
7
7
  rails-html-sanitizer (>= 1.0.4)
@@ -9,82 +9,82 @@ PATH
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- actionpack (5.1.4)
13
- actionview (= 5.1.4)
14
- activesupport (= 5.1.4)
15
- rack (~> 2.0)
12
+ actionpack (5.2.4.4)
13
+ actionview (= 5.2.4.4)
14
+ activesupport (= 5.2.4.4)
15
+ rack (~> 2.0, >= 2.0.8)
16
16
  rack-test (>= 0.6.3)
17
17
  rails-dom-testing (~> 2.0)
18
18
  rails-html-sanitizer (~> 1.0, >= 1.0.2)
19
- actionview (5.1.4)
20
- activesupport (= 5.1.4)
19
+ actionview (5.2.4.4)
20
+ activesupport (= 5.2.4.4)
21
21
  builder (~> 3.1)
22
22
  erubi (~> 1.4)
23
23
  rails-dom-testing (~> 2.0)
24
24
  rails-html-sanitizer (~> 1.0, >= 1.0.3)
25
- activesupport (5.1.4)
25
+ activesupport (5.2.4.4)
26
26
  concurrent-ruby (~> 1.0, >= 1.0.2)
27
- i18n (~> 0.7)
27
+ i18n (>= 0.7, < 2)
28
28
  minitest (~> 5.1)
29
29
  tzinfo (~> 1.1)
30
- builder (3.2.3)
31
- concurrent-ruby (1.0.5)
32
- crass (1.0.4)
33
- diff-lcs (1.3)
30
+ builder (3.2.4)
31
+ concurrent-ruby (1.1.7)
32
+ crass (1.0.6)
33
+ diff-lcs (1.4.4)
34
34
  docile (1.1.5)
35
35
  dotenv (2.2.1)
36
- erubi (1.7.0)
37
- i18n (0.9.3)
36
+ erubi (1.9.0)
37
+ i18n (1.8.5)
38
38
  concurrent-ruby (~> 1.0)
39
- json (2.1.0)
40
- loofah (2.2.2)
39
+ json (2.3.0)
40
+ loofah (2.7.0)
41
41
  crass (~> 1.0.2)
42
42
  nokogiri (>= 1.5.9)
43
- method_source (0.9.0)
44
- mini_portile2 (2.3.0)
45
- minitest (5.11.3)
46
- nokogiri (1.8.5)
47
- mini_portile2 (~> 2.3.0)
48
- rack (2.0.4)
49
- rack-test (0.8.2)
43
+ method_source (1.0.0)
44
+ mini_portile2 (2.4.0)
45
+ minitest (5.14.2)
46
+ nokogiri (1.10.10)
47
+ mini_portile2 (~> 2.4.0)
48
+ rack (2.2.3)
49
+ rack-test (1.1.0)
50
50
  rack (>= 1.0, < 3)
51
51
  rails-dom-testing (2.0.3)
52
52
  activesupport (>= 4.2.0)
53
53
  nokogiri (>= 1.6)
54
- rails-html-sanitizer (1.0.4)
55
- loofah (~> 2.2, >= 2.2.2)
56
- railties (5.1.4)
57
- actionpack (= 5.1.4)
58
- activesupport (= 5.1.4)
54
+ rails-html-sanitizer (1.3.0)
55
+ loofah (~> 2.3)
56
+ railties (5.2.4.4)
57
+ actionpack (= 5.2.4.4)
58
+ activesupport (= 5.2.4.4)
59
59
  method_source
60
60
  rake (>= 0.8.7)
61
- thor (>= 0.18.1, < 2.0)
62
- rake (10.5.0)
63
- rspec-core (3.7.1)
64
- rspec-support (~> 3.7.0)
65
- rspec-expectations (3.7.0)
61
+ thor (>= 0.19.0, < 2.0)
62
+ rake (12.3.3)
63
+ rspec-core (3.9.3)
64
+ rspec-support (~> 3.9.3)
65
+ rspec-expectations (3.9.3)
66
66
  diff-lcs (>= 1.2.0, < 2.0)
67
- rspec-support (~> 3.7.0)
68
- rspec-mocks (3.7.0)
67
+ rspec-support (~> 3.9.0)
68
+ rspec-mocks (3.9.1)
69
69
  diff-lcs (>= 1.2.0, < 2.0)
70
- rspec-support (~> 3.7.0)
71
- rspec-rails (3.7.2)
70
+ rspec-support (~> 3.9.0)
71
+ rspec-rails (3.9.1)
72
72
  actionpack (>= 3.0)
73
73
  activesupport (>= 3.0)
74
74
  railties (>= 3.0)
75
- rspec-core (~> 3.7.0)
76
- rspec-expectations (~> 3.7.0)
77
- rspec-mocks (~> 3.7.0)
78
- rspec-support (~> 3.7.0)
79
- rspec-support (3.7.1)
75
+ rspec-core (~> 3.9.0)
76
+ rspec-expectations (~> 3.9.0)
77
+ rspec-mocks (~> 3.9.0)
78
+ rspec-support (~> 3.9.0)
79
+ rspec-support (3.9.4)
80
80
  simplecov (0.15.1)
81
81
  docile (~> 1.1.0)
82
82
  json (>= 1.8, < 3)
83
83
  simplecov-html (~> 0.10.0)
84
84
  simplecov-html (0.10.2)
85
- thor (0.20.0)
85
+ thor (1.0.1)
86
86
  thread_safe (0.3.6)
87
- tzinfo (1.2.5)
87
+ tzinfo (1.2.7)
88
88
  thread_safe (~> 0.1)
89
89
 
90
90
  PLATFORMS
@@ -94,8 +94,8 @@ DEPENDENCIES
94
94
  bundler (~> 1.9)
95
95
  decanter!
96
96
  dotenv
97
- rake (~> 10.0)
98
- rspec-rails
97
+ rake (~> 12.0)
98
+ rspec-rails (~> 3.9)
99
99
  simplecov (~> 0.15.1)
100
100
 
101
101
  BUNDLED WITH
data/README.md CHANGED
@@ -15,6 +15,7 @@ gem 'decanter', '~> 3.0'
15
15
  - [Basic Usage](#basic-usage)
16
16
  - [Decanters](#decanters)
17
17
  - [Generators](#generators)
18
+ - [Decanting Collections](#decanting-collections)
18
19
  - [Nested resources](#nested-resources)
19
20
  - [Default parsers](#default-parsers)
20
21
  - [Parser options](#parser-options)
@@ -70,6 +71,37 @@ rails g decanter Trip name:string start_date:date end_date:date
70
71
  rails g parser TruncatedString
71
72
  ```
72
73
 
74
+ ### Decanting Collections
75
+
76
+ Decanter can decant a collection of a resource, applying the patterns used in the [fast JSON API gem](https://github.com/Netflix/fast_jsonapi#collection-serialization):
77
+
78
+ ```rb
79
+ # app/controllers/trips_controller.rb
80
+
81
+ def create
82
+ trip_params = {
83
+ trips: [
84
+ { name: 'Disney World', start_date: '12/24/2018', end_date: '12/28/2018' },
85
+ { name: 'Yosemite', start_date: '5/1/2017', end_date: '5/4/2017' }
86
+ ]
87
+ }
88
+ decanted_trip_params = TripDecanter.decant(trip_params[:trips])
89
+ Trip.create(decanted_trip_params) # bulk create trips with decanted params
90
+ end
91
+ ```
92
+
93
+ #### Control Over Decanting Collections
94
+
95
+ You can use the `is_collection` option for explicit control over decanting collections.
96
+
97
+ `decanted_trip_params = TripDecanter.decant(trip_params[:trips], is_collection: true)`
98
+
99
+ If this option is not provided, autodetect logic is used to determine if the providing incoming params holds a single object or collection of objects.
100
+
101
+ - `nil` or not provided: will try to autodetect single vs collection
102
+ - `true` will always treat the incoming params args as *collection*
103
+ - `false` will always treat incoming params args as *single object*
104
+
73
105
  ### Nested resources
74
106
 
75
107
  Decanters can declare relationships using `ActiveRecord`-style declarators:
@@ -220,6 +252,25 @@ end
220
252
 
221
253
  _Note: we recommend using [Active Record validations](https://guides.rubyonrails.org/active_record_validations.html) to check for presence of an attribute, rather than using the `required` option. This method is intended for use in non-RESTful routes or cases where Active Record validations are not available._
222
254
 
255
+
256
+ ### Default values
257
+
258
+ If you provide the option `:default_value` for an input in your decanter, the input key will be initialized with the given default value. Input keys not found in the incoming data parameters will be set to the provided default rather than ignoring the missing key. Note: `nil` and empty keys will not be overridden.
259
+
260
+ ```ruby
261
+ class TripDecanter < Decanter::Base
262
+ input :name, :string
263
+ input :destination, :string, default_value: 'Chicago'
264
+ end
265
+
266
+ ```
267
+
268
+ ```
269
+ TripDecanter.decant({ name: 'Vacation 2020' })
270
+ => { name: 'Vacation 2020', destination: 'Chicago' }
271
+
272
+ ```
273
+
223
274
  ### Global configuration
224
275
 
225
276
  You can generate a local copy of the default configuration with `rails generate decanter:install`. This will create an initializer where you can do global configuration:
@@ -33,7 +33,7 @@ Gem::Specification.new do |spec|
33
33
 
34
34
  spec.add_development_dependency 'bundler', '~> 1.9'
35
35
  spec.add_development_dependency 'dotenv'
36
- spec.add_development_dependency 'rake', '~> 10.0'
37
- spec.add_development_dependency 'rspec-rails'
36
+ spec.add_development_dependency 'rake', '~> 12.0'
37
+ spec.add_development_dependency 'rspec-rails', '~> 3.9'
38
38
  spec.add_development_dependency 'simplecov', '~> 0.15.1'
39
39
  end
@@ -1,23 +1,24 @@
1
1
  module Decanter
2
2
  module Core
3
-
3
+ DEFAULT_VALUE_KEY = :default_value
4
+ ACTION_CONTROLLER_PARAMETERS_CLASS_NAME = 'ActionController::Parameters'
5
+
4
6
  def self.included(base)
5
7
  base.extend(ClassMethods)
6
8
  end
7
9
 
8
10
  module ClassMethods
9
-
10
11
  def input(name, parsers=nil, **options)
12
+ # Convert all input names to symbols to correctly calculate handled vs. unhandled keys
13
+ input_names = [name].flatten.map(&:to_sym)
11
14
 
12
- _name = [name].flatten
13
-
14
- if _name.length > 1 && parsers.blank?
15
+ if input_names.length > 1 && parsers.blank?
15
16
  raise ArgumentError.new("#{self.name} no parser specified for input with multiple values.")
16
17
  end
17
18
 
18
- handlers[_name] = {
19
- key: options.fetch(:key, _name.first),
20
- name: _name,
19
+ handlers[input_names] = {
20
+ key: options.fetch(:key, input_names.first),
21
+ name: input_names,
21
22
  options: options,
22
23
  parsers: parsers,
23
24
  type: :input
@@ -45,7 +46,7 @@ module Decanter
45
46
  end
46
47
 
47
48
  def ignore(*args)
48
- keys_to_ignore.push(*args)
49
+ keys_to_ignore.push(*args).map!(&:to_sym)
49
50
  end
50
51
 
51
52
  def strict(mode)
@@ -56,12 +57,29 @@ module Decanter
56
57
  def decant(args)
57
58
  return handle_empty_args if args.blank?
58
59
  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) )
60
+
61
+ # Convert all params passed to a decanter to a hash with indifferent access to mitigate accessor ambiguity
62
+ accessible_args = to_indifferent_hash(args)
63
+ {}.merge( default_keys )
64
+ .merge( unhandled_keys(accessible_args) )
65
+ .merge( handled_keys(accessible_args) )
66
+ end
67
+
68
+ def default_keys
69
+ # return keys with provided default value when key is not defined within incoming args
70
+ default_result = default_value_inputs
71
+ .map { |input| [input[:key], input[:options][DEFAULT_VALUE_KEY]] }
72
+ .to_h
73
+
74
+ # parse default values
75
+ handled_keys(default_result)
76
+ end
77
+
78
+ def default_value_inputs
79
+ handlers.values.select { |input| input[:options].key?(DEFAULT_VALUE_KEY) }
62
80
  end
63
81
 
64
- def handle_empty_args
82
+ def handle_empty_args
65
83
  any_inputs_required? ? empty_args_error : {}
66
84
  end
67
85
 
@@ -73,7 +91,7 @@ module Decanter
73
91
  handlers.map do |h|
74
92
  options = h.last[:options]
75
93
  h.first.first if options && options[:required]
76
- end
94
+ end
77
95
  end
78
96
 
79
97
  def required_input_keys_present?(args={})
@@ -100,11 +118,11 @@ module Decanter
100
118
  keys_to_ignore -
101
119
  handlers.values
102
120
  .select { |handler| handler[:type] != :input }
103
- .map { |handler| "#{handler[:name]}_attributes".to_sym }
121
+ .map { |handler| "#{handler[:name]}_attributes".to_sym }
104
122
 
105
123
  return {} unless unhandled_keys.any?
106
124
  raise(UnhandledKeysError, "#{self.name} received unhandled keys: #{unhandled_keys.join(', ')}.") if strict_mode
107
- args.select { |key| unhandled_keys.include? key }
125
+ args.select { |key| unhandled_keys.include? key.to_sym }
108
126
  end
109
127
 
110
128
  def handled_keys(args)
@@ -185,13 +203,13 @@ module Decanter
185
203
  def parse(key, parsers, value, options)
186
204
  return { key => value } unless parsers
187
205
  if options[:required] && value_missing?(value)
188
- raise ArgumentError.new("No value for required argument: #{key}")
206
+ raise ArgumentError.new("No value for required argument: #{key}")
189
207
  end
190
208
  parser_classes = Parser.parsers_for(parsers)
191
209
  Parser.compose_parsers(parser_classes).parse(key, value, options)
192
210
  end
193
211
 
194
- def handlers
212
+ def handlers
195
213
  @handlers ||= {}
196
214
  end
197
215
 
@@ -211,6 +229,10 @@ module Decanter
211
229
  value.nil? || value == ""
212
230
  end
213
231
 
232
+ def to_indifferent_hash(args)
233
+ return args.to_unsafe_h if args.class.name == ACTION_CONTROLLER_PARAMETERS_CLASS_NAME
234
+ args.to_h.with_indifferent_access
235
+ end
214
236
  end
215
237
  end
216
238
  end
@@ -20,7 +20,6 @@ module Decanter
20
20
  end
21
21
 
22
22
  module ClassMethods
23
-
24
23
  def decant_create(args, **options)
25
24
  self.new(decant(args, options))
26
25
  .save(context: options[:context])
@@ -36,12 +35,28 @@ module Decanter
36
35
  end
37
36
 
38
37
  def decant(args, options={})
38
+ is_collection?(args, options[:is_collection]) ? decant_collection(args, options) : decant_args(args, options)
39
+ end
40
+
41
+ def decant_collection(args, options)
42
+ args.map { |resource| decant_args(resource, options) }
43
+ end
44
+
45
+ def decant_args(args, options)
39
46
  if specified_decanter = options[:decanter]
40
47
  Decanter.decanter_from(specified_decanter)
41
48
  else
42
49
  Decanter.decanter_for(self)
43
50
  end.decant(args)
44
51
  end
52
+
53
+ private
54
+
55
+ # leveraging the approach used in the [fast JSON API gem](https://github.com/Netflix/fast_jsonapi#collection-serialization)
56
+ def is_collection?(args, collection_option=nil)
57
+ return collection_option[:is_collection] unless collection_option.nil?
58
+ args.respond_to?(:size) && !args.respond_to?(:each_pair)
59
+ end
45
60
  end
46
61
 
47
62
  module ActiveRecordExtensions
@@ -1,3 +1,3 @@
1
1
  module Decanter
2
- VERSION = '3.1.2'.freeze
2
+ VERSION = '3.4.1'.freeze
3
3
  end
@@ -2,12 +2,12 @@ module Rails
2
2
  module Generators
3
3
  class DecanterGenerator < NamedBase
4
4
  source_root File.expand_path('../templates', __FILE__)
5
- check_class_collision :suffix => 'Decanter'
5
+ check_class_collision suffix: 'Decanter'
6
6
  ASSOCIATION_TYPES = [:has_many, :has_one, :belongs_to]
7
7
 
8
- argument :attributes, :type => :array, :default => [], :banner => 'field:type field:type'
8
+ argument :attributes, type: :array, default: [], banner: 'field:type field:type'
9
9
 
10
- class_option :parent, :type => :string, :desc => 'The parent class for the generated decanter'
10
+ class_option :parent, type: :string, desc: 'The parent class for the generated decanter'
11
11
 
12
12
  def create_decanter_file
13
13
  template 'decanter.rb.erb', File.join('app/decanters', class_path, "#{file_name}_decanter.rb")
@@ -2,9 +2,9 @@ module Rails
2
2
  module Generators
3
3
  class ParserGenerator < NamedBase
4
4
  source_root File.expand_path('../templates', __FILE__)
5
- check_class_collision :suffix => 'Parser'
5
+ check_class_collision suffix: 'Parser'
6
6
 
7
- class_option :parent, :type => :string, :desc => 'The parent class for the generated parser'
7
+ class_option :parent, type: :string, desc: 'The parent class for the generated parser'
8
8
 
9
9
  def create_parser_file
10
10
  template 'parser.rb.erb', File.join('lib/decanter/parsers', class_path, "#{file_name}_parser.rb")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: decanter
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.2
4
+ version: 3.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Francis
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2020-10-05 00:00:00.000000000 Z
12
+ date: 2020-11-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: actionpack
@@ -87,28 +87,28 @@ dependencies:
87
87
  requirements:
88
88
  - - "~>"
89
89
  - !ruby/object:Gem::Version
90
- version: '10.0'
90
+ version: '12.0'
91
91
  type: :development
92
92
  prerelease: false
93
93
  version_requirements: !ruby/object:Gem::Requirement
94
94
  requirements:
95
95
  - - "~>"
96
96
  - !ruby/object:Gem::Version
97
- version: '10.0'
97
+ version: '12.0'
98
98
  - !ruby/object:Gem::Dependency
99
99
  name: rspec-rails
100
100
  requirement: !ruby/object:Gem::Requirement
101
101
  requirements:
102
- - - ">="
102
+ - - "~>"
103
103
  - !ruby/object:Gem::Version
104
- version: '0'
104
+ version: '3.9'
105
105
  type: :development
106
106
  prerelease: false
107
107
  version_requirements: !ruby/object:Gem::Requirement
108
108
  requirements:
109
- - - ">="
109
+ - - "~>"
110
110
  - !ruby/object:Gem::Version
111
- version: '0'
111
+ version: '3.9'
112
112
  - !ruby/object:Gem::Dependency
113
113
  name: simplecov
114
114
  requirement: !ruby/object:Gem::Requirement