decanter 3.2.1 → 3.5.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/CONTRIBUTING.md +1 -1
- data/Gemfile.lock +2 -2
- data/README.md +37 -1
- data/decanter.gemspec +1 -1
- data/lib/decanter.rb +2 -2
- data/lib/decanter/core.rb +31 -17
- data/lib/decanter/extensions.rb +16 -1
- data/lib/decanter/version.rb +1 -1
- data/lib/generators/rails/decanter_generator.rb +3 -3
- data/lib/generators/rails/parser_generator.rb +2 -2
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 47761a57539196c1be8d4de3b4f18a78a0e133bdace720e4363dfc2a4ab5776c
|
4
|
+
data.tar.gz: caccc39b0926d5f39a29673a912fab356079fb67357b868cc4f803b325988890
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d7ee0004606f84e64e0b94697d1ff035f54b96ceb3dddc1f0a77c716ad17c566b9b959f78a67cfa6c40598c337db4464c62c26d2d394a749183bec915cad3d44
|
7
|
+
data.tar.gz: 6aecb9eb64b1bd282de2d29a3828fe131fa01227c50b8b5ff69049cb08184497435d9f67ab95f71dacf8120e072ff813b00dac497e81772396f249b0e8a74327
|
data/CONTRIBUTING.md
CHANGED
@@ -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.lock
CHANGED
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
|
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
|
data/decanter.gemspec
CHANGED
@@ -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'
|
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'
|
data/lib/decanter.rb
CHANGED
@@ -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
|
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)
|
data/lib/decanter/core.rb
CHANGED
@@ -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
|
-
|
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[
|
20
|
-
key: options.fetch(:key,
|
21
|
-
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
|
-
|
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(
|
63
|
-
.merge( handled_keys(
|
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
|
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
|
-
|
123
|
-
|
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
|
data/lib/decanter/extensions.rb
CHANGED
@@ -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
|
data/lib/decanter/version.rb
CHANGED
@@ -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 :
|
5
|
+
check_class_collision suffix: 'Decanter'
|
6
6
|
ASSOCIATION_TYPES = [:has_many, :has_one, :belongs_to]
|
7
7
|
|
8
|
-
argument :attributes, :
|
8
|
+
argument :attributes, type: :array, default: [], banner: 'field:type field:type'
|
9
9
|
|
10
|
-
class_option :parent, :
|
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 :
|
5
|
+
check_class_collision suffix: 'Parser'
|
6
6
|
|
7
|
-
class_option :parent, :
|
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.
|
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:
|
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: '
|
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: '
|
41
|
+
version: '0'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
43
|
name: rails-html-sanitizer
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|