decanter 3.2.1 → 3.5.0

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: 8bcc235910fd86ed830c20f5c07f53b3154911cb26aec56d45fece483207d4bd
4
- data.tar.gz: a9db5721c34fdd0c9857ce8647ad84b2a4bbfdb6b1b76f67fe4e5a8b1650d9d2
3
+ metadata.gz: 47761a57539196c1be8d4de3b4f18a78a0e133bdace720e4363dfc2a4ab5776c
4
+ data.tar.gz: caccc39b0926d5f39a29673a912fab356079fb67357b868cc4f803b325988890
5
5
  SHA512:
6
- metadata.gz: 170e5256eda3f2b36f259f6f475420218c1a04aa360f553a0fb0f74479c28469a521e32fbfb852521d8c84e5dc22d754f7455e66e18702493b303b285cb1f7a6
7
- data.tar.gz: 35a01b51460f2f831a40e52fc5f11a8e800b5e50eeb08d0623f8241d72f00b5f20e1543235904083e793b0be76a02c1378c4f31edbb4b3934fa155b01f409b92
6
+ metadata.gz: d7ee0004606f84e64e0b94697d1ff035f54b96ceb3dddc1f0a77c716ad17c566b9b959f78a67cfa6c40598c337db4464c62c26d2d394a749183bec915cad3d44
7
+ data.tar.gz: 6aecb9eb64b1bd282de2d29a3828fe131fa01227c50b8b5ff69049cb08184497435d9f67ab95f71dacf8120e072ff813b00dac497e81772396f249b0e8a74327
@@ -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.
@@ -1,9 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- decanter (3.2.1)
4
+ decanter (3.5.0)
5
5
  actionpack (>= 4.2.10)
6
- activesupport (~> 5.2)
6
+ activesupport
7
7
  rails-html-sanitizer (>= 1.0.4)
8
8
 
9
9
  GEM
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:
@@ -112,7 +144,11 @@ input :start_date, :date, parse_format: '%Y-%m-%d'
112
144
 
113
145
  ### Exceptions
114
146
 
115
- By default, `Decanter#decant` will raise an exception when unexpected parameters are passed. To override this behavior, you can disable strict mode:
147
+ By default, `Decanter#decant` will raise an exception when unexpected parameters are passed. To override this behavior, you can change the strict mode option to one of:
148
+
149
+ - `true` (default): unhandled keys will raise an unexpected parameters exception
150
+ - `false`: all parameter key-value pairs will be included in the result
151
+ - `:ignore`: unhandled keys will be excluded from the decanted result
116
152
 
117
153
  ```ruby
118
154
  class TripDecanter < Decanter::Base
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
28
28
  spec.require_paths = ['lib']
29
29
 
30
30
  spec.add_dependency 'actionpack', '>= 4.2.10'
31
- spec.add_dependency 'activesupport', '~> 5.2'
31
+ spec.add_dependency 'activesupport'
32
32
  spec.add_dependency 'rails-html-sanitizer', '>= 1.0.4'
33
33
 
34
34
  spec.add_development_dependency 'bundler', '~> 1.9'
@@ -13,7 +13,7 @@ module Decanter
13
13
  klass_or_sym.to_s.singularize.camelize
14
14
  else
15
15
  raise ArgumentError.new("cannot lookup decanter for #{klass_or_sym} with class #{klass_or_sym.class}")
16
- end.concat('Decanter')
16
+ end + 'Decanter'
17
17
  begin
18
18
  decanter_name.constantize
19
19
  rescue
@@ -62,4 +62,4 @@ require 'decanter/base'
62
62
  require 'decanter/extensions'
63
63
  require 'decanter/exceptions'
64
64
  require 'decanter/parser'
65
- require 'decanter/railtie' if defined?(::Rails)
65
+ require 'decanter/railtie' if defined?(::Rails)
@@ -1,24 +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'
4
5
 
5
6
  def self.included(base)
6
7
  base.extend(ClassMethods)
7
8
  end
8
9
 
9
10
  module ClassMethods
10
-
11
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)
12
14
 
13
- _name = [name].flatten
14
-
15
- if _name.length > 1 && parsers.blank?
15
+ if input_names.length > 1 && parsers.blank?
16
16
  raise ArgumentError.new("#{self.name} no parser specified for input with multiple values.")
17
17
  end
18
18
 
19
- handlers[_name] = {
20
- key: options.fetch(:key, _name.first),
21
- name: _name,
19
+ handlers[input_names] = {
20
+ key: options.fetch(:key, input_names.first),
21
+ name: input_names,
22
22
  options: options,
23
23
  parsers: parsers,
24
24
  type: :input
@@ -46,21 +46,23 @@ module Decanter
46
46
  end
47
47
 
48
48
  def ignore(*args)
49
- keys_to_ignore.push(*args)
49
+ keys_to_ignore.push(*args).map!(&:to_sym)
50
50
  end
51
51
 
52
52
  def strict(mode)
53
- raise(ArgumentError, "#{self.name}: Unknown strict value #{mode}") unless [true, false].include? mode
53
+ raise(ArgumentError, "#{self.name}: Unknown strict value #{mode}") unless [:ignore, true, false].include? mode
54
54
  @strict_mode = mode
55
55
  end
56
56
 
57
57
  def decant(args)
58
58
  return handle_empty_args if args.blank?
59
59
  return empty_required_input_error unless required_input_keys_present?(args)
60
- args = args.to_unsafe_h.with_indifferent_access if args.class.name == 'ActionController::Parameters'
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)
61
63
  {}.merge( default_keys )
62
- .merge( unhandled_keys(args) )
63
- .merge( handled_keys(args) )
64
+ .merge( unhandled_keys(accessible_args) )
65
+ .merge( handled_keys(accessible_args) )
64
66
  end
65
67
 
66
68
  def default_keys
@@ -69,8 +71,9 @@ module Decanter
69
71
  .map { |input| [input[:key], input[:options][DEFAULT_VALUE_KEY]] }
70
72
  .to_h
71
73
 
72
- # parse default values
73
- handled_keys(default_result)
74
+ # parse handled default values, including keys
75
+ # with defaults not already managed by handled_keys
76
+ default_result.merge(handled_keys(default_result))
74
77
  end
75
78
 
76
79
  def default_value_inputs
@@ -119,14 +122,21 @@ module Decanter
119
122
  .map { |handler| "#{handler[:name]}_attributes".to_sym }
120
123
 
121
124
  return {} unless unhandled_keys.any?
122
- raise(UnhandledKeysError, "#{self.name} received unhandled keys: #{unhandled_keys.join(', ')}.") if strict_mode
123
- args.select { |key| unhandled_keys.include? key }
125
+
126
+ case strict_mode
127
+ when :ignore
128
+ p "#{self.name} ignoring unhandled keys: #{unhandled_keys.join(', ')}."
129
+ {}
130
+ when true
131
+ raise(UnhandledKeysError, "#{self.name} received unhandled keys: #{unhandled_keys.join(', ')}.")
132
+ else
133
+ args.select { |key| unhandled_keys.include? key.to_sym }
134
+ end
124
135
  end
125
136
 
126
137
  def handled_keys(args)
127
138
  arg_keys = args.keys.map(&:to_sym)
128
139
  inputs, assocs = handlers.values.partition { |handler| handler[:type] == :input }
129
-
130
140
  {}.merge(
131
141
  # Inputs
132
142
  inputs.select { |handler| (arg_keys & handler[:name]).any? }
@@ -227,6 +237,10 @@ module Decanter
227
237
  value.nil? || value == ""
228
238
  end
229
239
 
240
+ def to_indifferent_hash(args)
241
+ return args.to_unsafe_h if args.class.name == ACTION_CONTROLLER_PARAMETERS_CLASS_NAME
242
+ args.to_h.with_indifferent_access
243
+ end
230
244
  end
231
245
  end
232
246
  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.2.1'.freeze
2
+ VERSION = '3.5.0'.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.2.1
4
+ version: 3.5.0
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-11-03 00:00:00.000000000 Z
12
+ date: 2021-01-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: actionpack
@@ -29,16 +29,16 @@ dependencies:
29
29
  name: activesupport
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
- - - "~>"
32
+ - - ">="
33
33
  - !ruby/object:Gem::Version
34
- version: '5.2'
34
+ version: '0'
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
- - - "~>"
39
+ - - ">="
40
40
  - !ruby/object:Gem::Version
41
- version: '5.2'
41
+ version: '0'
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: rails-html-sanitizer
44
44
  requirement: !ruby/object:Gem::Requirement