paramoid 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f528551c46f70a5d476cb1ddbbeb9e59687a7153112c8962a026ed896b6ba343
4
+ data.tar.gz: ebd4e70f7a7987afcc282db3ef47fbda33841be91e97d98e85874bc039c8047d
5
+ SHA512:
6
+ metadata.gz: d4ab2dfeb44387a4c6bcf7de2aa05832d8efd792e4cefdf867039bc51b1c044554575b005f2b7c10b7fe82d7bfc1e5db0dab6dda4d34fd59256ea52c7721fff8
7
+ data.tar.gz: 43ab66b64849be0b6b5879705f58ed4a337729c15d0aa7313d8db9e6671356fa936b6f06d1e0e955b17d9b4386ef9382aabd386adfd839847aff5df886d2d64a
@@ -0,0 +1,84 @@
1
+ module Paramoid
2
+ class Base
3
+ # @param [ActionController::Parameters] params
4
+ def sanitize(params)
5
+ params = params.permit(*permitted_params)
6
+ scalar_params.transform_params!(params)
7
+ params = default_params.merge(params)
8
+ ensure_required_params!(params)
9
+ end
10
+
11
+ protected
12
+
13
+ # @param [Symbol] name
14
+ # @param [Symbol] as
15
+ # @param [Lambda | NilClass] transformer
16
+ def group!(name, as: nil, transformer: nil)
17
+ key = as || name
18
+ data = Object.new(name, key, nested: List.new, transformer: transformer)
19
+ context << data
20
+ return unless block_given?
21
+
22
+ old_context = context
23
+ @context = data
24
+ yield
25
+ @context = old_context
26
+ end
27
+
28
+ alias list! group!
29
+ alias array! group!
30
+
31
+ # @param [Array<Symbol>] names
32
+ def params!(*names, required: false)
33
+ names.each { |name| param! name, required: required }
34
+ end
35
+
36
+ def param!(name, as: nil, transformer: nil, default: nil, required: false)
37
+ key = as || name
38
+ data = Object.new(name, key, nested: nil, default: default, transformer: transformer, required: required)
39
+ context << data
40
+ end
41
+
42
+ def default!(name, value)
43
+ data = Object.new(name, name, nested: nil, default: value)
44
+ context << data
45
+ end
46
+
47
+ private
48
+
49
+ def context
50
+ @context ||= scalar_params
51
+ end
52
+
53
+ def default_params
54
+ scalar_params.to_defaults
55
+ # @default_params ||= {}.with_indifferent_access
56
+ end
57
+
58
+ def scalar_params
59
+ @scalar_params ||= List.new
60
+ end
61
+
62
+ def ensure_required_params!(params)
63
+ scalar_params.ensure_required_params!(params)
64
+ params
65
+ end
66
+
67
+ def transformers
68
+ @transformers ||= {}.with_indifferent_access
69
+ end
70
+
71
+ def permitted_params
72
+ scalar_params.to_params
73
+ end
74
+
75
+ def transformed_keys
76
+ @transformed_keys ||= scalar_params
77
+ end
78
+
79
+ # @param [ActionController::Parameters] params
80
+ def run_transformers!(params)
81
+ @transformers.each { |key, t| params[key] = t.call(params[key]) }
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,27 @@
1
+ require 'action_controller'
2
+
3
+ module Paramoid
4
+ module Controller
5
+ extend ActiveSupport::Concern
6
+
7
+ def sanitize_params!(&block)
8
+ if block_given?
9
+ sanitized = Paramoid::Base.new
10
+ sanitized.instance_exec(_paramoid_safe_current_user, &block)
11
+ sanitized.sanitize(params)
12
+ else
13
+ base_class_name = self.class.name.demodulize.gsub('Controller', '').singularize
14
+
15
+ "#{base_class_name}ParamsSanitizer".safe_constantize&.new(_paramoid_safe_current_user)&.sanitize(params)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def _paramoid_safe_current_user
22
+ respond_to?(:current_user, true) ? current_user : nil
23
+ end
24
+ end
25
+ end
26
+
27
+ ActionController::Base.include Paramoid::Controller if defined?(ActionController::Base)
@@ -0,0 +1,25 @@
1
+ module Paramoid
2
+ class List < Array
3
+ def to_params
4
+ list = reject(&:nested?).map(&:to_params)
5
+ nested = select(&:nested?).inject({}) { |a, b| a.merge!(b.to_params) }
6
+ nested.present? ? list << nested : list
7
+ end
8
+
9
+ def transform_params!(params)
10
+ each do |params_data|
11
+ params_data.transform_params! params
12
+ end
13
+ end
14
+
15
+ def to_defaults
16
+ inject({}) { |a, b| a.merge!(b.to_defaults) }
17
+ end
18
+
19
+ def ensure_required_params!(params)
20
+ each do |params_data|
21
+ params_data.ensure_required_params! params
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,94 @@
1
+ module Paramoid
2
+ class Object
3
+ # @return [Symbol] the parameter name
4
+ attr_reader :name
5
+ # @return [Symbol] the parameter alias
6
+ attr_reader :alias_name
7
+ # @return [Object] the parameter nested
8
+ attr_reader :nested
9
+ # @return [Object] the parameter default value
10
+ attr_reader :default
11
+ # @return [Lambda, NilClass] the transformer
12
+ attr_reader :transformer
13
+ # @return [TrueClass, FalseClass] required
14
+ attr_reader :required
15
+
16
+ # @param [Symbol] name
17
+ # @param [Symbol] alias_name
18
+ # @param [Object, NilClass] nested
19
+ # @param [Object] default
20
+ # @param [Lambda, NilClass] transformer
21
+ # @param [TrueClass, FalseClass] required
22
+ def initialize(name, alias_name, nested: nil, transformer: nil, default: nil, required: false)
23
+ @name = name
24
+ @alias = alias_name
25
+ @nested = nested
26
+ @default = default
27
+ @transformer = transformer
28
+ @required = required
29
+ end
30
+
31
+ # @param [Array, Hash] params
32
+ def transform_params!(params)
33
+ return if @alias == @name
34
+
35
+ if params.is_a?(Array)
36
+ params.each { |param| transform_params!(param) }
37
+ return
38
+ end
39
+
40
+ return unless params.key?(@name)
41
+
42
+ @nested.transform_params!(params[@name]) if nested?
43
+ params[@alias] = params.delete(@name) unless @alias == @name
44
+ end
45
+
46
+ def to_params
47
+ if nested?
48
+ { @name.to_sym => @nested.to_params }
49
+ else
50
+ @name
51
+ end
52
+ end
53
+
54
+ def to_required_params
55
+ if nested?
56
+ @nested.to_required_params
57
+ else
58
+ @required ? [@name] : []
59
+ end
60
+ end
61
+
62
+ def ensure_required_params!(params)
63
+ if @required
64
+ raise ActionController::ParameterMissing, output_key unless params&.key?(output_key)
65
+ raise ActionController::ParameterMissing, output_key if params[output_key].nil?
66
+ end
67
+
68
+ @nested.ensure_required_params!(params[output_key]) if nested?
69
+
70
+ params
71
+ end
72
+
73
+ def to_defaults
74
+ if nested?
75
+ nested_defaults = @nested.to_defaults
76
+ (nested_defaults.present? ? { output_key => @nested.to_defaults } : {}).with_indifferent_access
77
+ else
78
+ (@default ? { output_key => @default } : {}).with_indifferent_access
79
+ end
80
+ end
81
+
82
+ def output_key
83
+ @output_key ||= (@alias || @name).to_s
84
+ end
85
+
86
+ def <<(value)
87
+ @nested << value
88
+ end
89
+
90
+ def nested?
91
+ !@nested.nil?
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,3 @@
1
+ module Paramoid
2
+ VERSION = '1.0.0'
3
+ end
data/lib/paramoid.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Paramoid
4
+ extend ActiveSupport::Autoload
5
+
6
+ autoload :Object
7
+ autoload :List
8
+ autoload :Base
9
+ # autoload :Controller
10
+ end
11
+
12
+ require 'paramoid/controller'
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ describe PersonParamsSanitizer, type: :controller do
4
+ let(:params) do
5
+ ActionController::Parameters.new(params_hash)
6
+ end
7
+
8
+ subject { described_class.new(user) }
9
+ let(:sanitized) { subject.sanitize(params) }
10
+
11
+ describe 'when params are valid' do
12
+ let(:user) { double(admin?: false) }
13
+ let(:params_hash) do
14
+ {
15
+ current_user_id: 2,
16
+ first_name: 'John',
17
+ last_name: 'Doe',
18
+ # TODO: Implement transformers
19
+ # email: 'Hello@MyCustomMAIL.COM',
20
+ role: 'some_role',
21
+ unwanted: 'hello',
22
+ an_object_filtered: { name: 'value' },
23
+ an_array_filtered: [{ name: 'value' }],
24
+ an_array_unfiltered: [1, 2, 3, 4, 5]
25
+ }
26
+ end
27
+
28
+ it 'filters out unwanted params' do
29
+ expect(sanitized).not_to have_key(:unwanted)
30
+ end
31
+
32
+ it 'filters out object values if it\'s not a group' do
33
+ expect(sanitized).not_to have_key('an_object_filtered')
34
+ end
35
+
36
+ it 'filters out object values if it\'s not an array' do
37
+ expect(sanitized).not_to have_key('an_array_filtered')
38
+ end
39
+
40
+ it 'keeps only allowed params' do
41
+ expect(sanitized).to eq(
42
+ {
43
+ 'current_user_id' => 2,
44
+ 'first_name' => 'John',
45
+ 'last_name' => 'Doe',
46
+ # TODO: Implement transformers
47
+ # 'email' => 'hello@mycustommail.com',
48
+ 'some_default' => 1,
49
+ 'an_array_unfiltered' => [1, 2, 3, 4, 5]
50
+ }
51
+ )
52
+ end
53
+
54
+ context 'when the required value is not set' do
55
+ let(:params_hash) do
56
+ {}
57
+ end
58
+ it 'raises an error' do
59
+ expect { sanitized }.to raise_error(ActionController::ParameterMissing)
60
+ end
61
+ end
62
+
63
+ context 'when the default is set' do
64
+ let(:params_hash) do
65
+ { some_default: 2, current_user_id: 1 }
66
+ end
67
+ let 'it replaces the default value' do
68
+ expect(sanitized['some_default']).to eq(2)
69
+ end
70
+ end
71
+
72
+ context 'when the user is an admin' do
73
+ let(:user) { double(admin?: true) }
74
+
75
+ it 'keeps role param' do
76
+ expect(sanitized).to have_key('role')
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'controllers' do
4
+ let(:params_hash) do
5
+ {
6
+ current_user_id: 2,
7
+ first_name: 'John',
8
+ last_name: 'Doe',
9
+ # TODO: Implement transformers
10
+ # email: 'Hello@MyCustomMAIL.COM',
11
+ role: 'some_role',
12
+ unwanted: 'hello',
13
+ an_object_filtered: { name: 'value' },
14
+ an_array_filtered: [{ name: 'value' }],
15
+ an_array_unfiltered: [1, 2, 3, 4, 5]
16
+ }
17
+ end
18
+
19
+ describe PeopleInlineController, type: :controller do
20
+ let(:params) do
21
+ ActionController::Parameters.new(params_hash)
22
+ end
23
+
24
+ it 'sanitizes params in a block' do
25
+ allow_any_instance_of(described_class).to receive(:params).and_return(params)
26
+ expect_any_instance_of(Paramoid::Base).to receive(:instance_exec).and_call_original
27
+
28
+ subject.index
29
+ end
30
+ end
31
+
32
+ describe PeopleController, type: :controller do
33
+ let(:params) do
34
+ ActionController::Parameters.new(params_hash)
35
+ end
36
+
37
+ it 'sanitizes params in a block' do
38
+ allow_any_instance_of(described_class).to receive(:params).and_return(params)
39
+ expect_any_instance_of(PersonParamsSanitizer).to receive(:sanitize)
40
+ subject.index
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,123 @@
1
+ require 'spec_helper'
2
+
3
+ describe Paramoid::Object do
4
+ let(:name) { :some_param }
5
+ let(:alias_name) { :some_param }
6
+ let(:required) { false }
7
+ let(:default) { nil }
8
+ let(:transformer) { nil }
9
+ let(:nested) { nil }
10
+ subject do
11
+ described_class.new(name, alias_name, nested: nested, transformer: transformer, default: default,
12
+ required: required)
13
+ end
14
+ let(:sanitized) { subject.sanitize(params) }
15
+
16
+ describe '#to_params' do
17
+ it 'returns the name' do
18
+ expect(subject.to_params).to eq(name)
19
+ end
20
+
21
+ context 'when it\'s nested' do
22
+ let(:nested) { described_class.new(:nested, nil) }
23
+
24
+ it 'returns the child' do
25
+ expect(subject.to_params).to eq({ name => nested.to_params })
26
+ end
27
+ end
28
+ end
29
+
30
+ describe '#to_defaults' do
31
+ it 'returns an empty value' do
32
+ expect(subject.to_defaults).to eq({})
33
+ end
34
+
35
+ context 'when there\'s a default' do
36
+ let(:default) { 'some_default' }
37
+
38
+ it 'returns the default value' do
39
+ expect(subject.to_defaults).to eq({ name.to_s => default })
40
+ end
41
+
42
+ context 'when there\'s an alias' do
43
+ let(:alias_name) { :alias_name }
44
+
45
+ it 'returns the default value with the alias' do
46
+ expect(subject.to_defaults).to eq({ alias_name.to_s => default })
47
+ end
48
+ end
49
+ end
50
+
51
+ context 'when it\'s nested' do
52
+ let(:nested) { described_class.new(:nested, :nested, default: 'my_child_default') }
53
+
54
+ it 'returns the child' do
55
+ expect(subject.to_defaults).to eq({ name.to_s => nested.to_defaults })
56
+ end
57
+ end
58
+ end
59
+
60
+ describe '#to_required_params' do
61
+ it 'returns an empty list' do
62
+ expect(subject.to_required_params).to eq([])
63
+ end
64
+
65
+ context 'when it\'s required' do
66
+ let(:required) { true }
67
+
68
+ it 'returns the name' do
69
+ expect(subject.to_required_params).to eq([name])
70
+ end
71
+ end
72
+
73
+ context 'when it\'s nested' do
74
+ let(:nested) { described_class.new(:nested, nil, required: true) }
75
+
76
+ it 'returns the child' do
77
+ expect(subject.to_params).to eq({ name => nested.to_params })
78
+ end
79
+ end
80
+ end
81
+
82
+ describe '#ensure_required_params!' do
83
+ let(:params_hash) { { 'not_this_param' => 'some_value' } }
84
+ let(:params) { ActionController::Parameters.new(params_hash) }
85
+
86
+ it 'returns the params' do
87
+ expect(subject.ensure_required_params!(params)).to eq(params)
88
+ end
89
+
90
+ context 'when it\'s required' do
91
+ let(:required) { true }
92
+
93
+ it 'raises an error' do
94
+ expect do
95
+ subject.ensure_required_params!(params)
96
+ end.to raise_error(ActionController::ParameterMissing,
97
+ 'param is missing or the value is empty: some_param')
98
+ end
99
+ end
100
+
101
+ context 'when it\'s nested' do
102
+ let(:nested) { described_class.new(:nested, nil, required: true) }
103
+
104
+ it 'raises an error' do
105
+ expect do
106
+ subject.ensure_required_params!(params)
107
+ end.to raise_error(ActionController::ParameterMissing,
108
+ 'param is missing or the value is empty: nested')
109
+ end
110
+
111
+ context 'and it\'s required' do
112
+ let(:required) { true }
113
+
114
+ it 'raises an error on the parent param' do
115
+ expect do
116
+ subject.ensure_required_params!(params)
117
+ end.to raise_error(ActionController::ParameterMissing,
118
+ 'param is missing or the value is empty: some_param')
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,11 @@
1
+ require 'action_controller'
2
+ require 'rspec'
3
+ require 'paramoid'
4
+
5
+ I18n.enforce_available_locales = false
6
+ RSpec::Expectations.configuration.warn_about_potential_false_positives = false
7
+
8
+ Dir[File.expand_path('support/*.rb', __dir__)].each { |f| require f }
9
+
10
+ RSpec.configure do |config|
11
+ end
@@ -0,0 +1,32 @@
1
+ require 'ostruct'
2
+
3
+ class PeopleInlineController < ActionController::Base
4
+ def index
5
+ create_params
6
+ end
7
+
8
+ protected
9
+
10
+ def create_params
11
+ sanitize_params! do
12
+ params! :first_name, :last_name
13
+ param! :email, transformer: ->(data) { data&.downcase }
14
+ end
15
+ end
16
+ end
17
+
18
+ class PeopleController < ActionController::Base
19
+ def index
20
+ create_params
21
+ end
22
+
23
+ protected
24
+
25
+ def current_user
26
+ OpenStruct.new(admin?: true)
27
+ end
28
+
29
+ def create_params
30
+ sanitize_params!
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ class PersonParamsSanitizer < Paramoid::Base
2
+ # @param [User] user
3
+ def initialize(user = nil)
4
+ params! :first_name, :last_name, :gender
5
+
6
+ param! :email, transformer: ->(data) { data&.downcase }
7
+
8
+ param! :current_user_id, required: true
9
+
10
+ param! :an_object_filtered
11
+ param! :an_array_filtered
12
+
13
+ array! :an_array_unfiltered
14
+
15
+ param! :role if user.admin?
16
+
17
+ default! :some_default, 1
18
+
19
+ group! :contact, as: :contact_attributes do
20
+ params! :id, :first_name, :last_name, :birth_date, :birth_place, :phone, :role, :fiscal_code
21
+ end
22
+ end
23
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: paramoid
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Mònade
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-05-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: actionpack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '8'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '5'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '8'
33
+ - !ruby/object:Gem::Dependency
34
+ name: activesupport
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '5'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '8'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '5'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '8'
53
+ - !ruby/object:Gem::Dependency
54
+ name: rspec
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '3'
60
+ type: :development
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '3'
67
+ - !ruby/object:Gem::Dependency
68
+ name: rubocop
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ type: :development
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ description: Paramoid is a gem that extends Rails Strong Parameters, allowing to declare
82
+ complex params structures with a super cool DSL, supporting required params, default
83
+ values, groups, arrays and more.
84
+ email: team@monade.io
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - lib/paramoid.rb
90
+ - lib/paramoid/base.rb
91
+ - lib/paramoid/controller.rb
92
+ - lib/paramoid/list.rb
93
+ - lib/paramoid/object.rb
94
+ - lib/paramoid/version.rb
95
+ - spec/paramoid/base_spec.rb
96
+ - spec/paramoid/controller_spec.rb
97
+ - spec/paramoid/object_spec.rb
98
+ - spec/spec_helper.rb
99
+ - spec/support/controllers.rb
100
+ - spec/support/params.rb
101
+ homepage: https://rubygems.org/gems/paramoid
102
+ licenses:
103
+ - MIT
104
+ metadata: {}
105
+ post_install_message:
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: 2.7.0
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ requirements: []
120
+ rubygems_version: 3.2.33
121
+ signing_key:
122
+ specification_version: 4
123
+ summary: Getting paranoid about your Rails application params? Try paramoid!
124
+ test_files:
125
+ - spec/paramoid/base_spec.rb
126
+ - spec/paramoid/controller_spec.rb
127
+ - spec/paramoid/object_spec.rb
128
+ - spec/spec_helper.rb
129
+ - spec/support/controllers.rb
130
+ - spec/support/params.rb