pastore 0.0.4 → 0.2.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.
data/docs/Params.md ADDED
@@ -0,0 +1,122 @@
1
+ # `Pastore::Params`
2
+
3
+ `Pastore::Params` is the module that provides the features for params validation in Rails controllers. It allows you to define the params with their data type (`string`, `number`, `boolean`, `date` and `object`), specify which params are mandatory and cannot be blank.
4
+
5
+ **Table of Contents**
6
+
7
+ - [`Pastore::Params`](#pastoreparams)
8
+ - [Setup](#setup)
9
+ - [Specifying params](#specifying-params)
10
+ - [Available param types](#available-param-types)
11
+ - [Avaliable options](#avaliable-options)
12
+
13
+ ## Setup
14
+
15
+ To start using `Pastore::Params` you just need to include `Pastore::Params` module in your controller. If you plan to use it in all your controllers, just add the following to your `ApplicationController` class:
16
+
17
+ ```ruby
18
+ # app/controllers/application_controller.rb
19
+
20
+ class ApplicationController < ActionController::API
21
+ include Pastore::Params
22
+
23
+ # Specify response status code to use for invalid params (default: unprocessable_entity)
24
+ invalid_params_status :bad_request
25
+
26
+ # Here you can customize the response to return on invalid params
27
+ on_invalid_params do
28
+ render json: { message: 'Invalid params' }
29
+ end
30
+
31
+ # ...
32
+ end
33
+ ```
34
+
35
+ ## Specifying params
36
+
37
+ Once you have configured `Pastore::Params` in your controller you can use the `param` method to define your params.
38
+
39
+ `param` method has the following signature:
40
+
41
+ ```ruby
42
+ param PARAM_NAME, **OPTIONS
43
+ ```
44
+
45
+ `PARAM_NAME` can be a `String` or a `Symbol`, while `OPTIONS` is a `Hash`.
46
+
47
+ Below you can find some examples of params definition using `param` method:
48
+
49
+ ```ruby
50
+ class UsersController < ApplicationController
51
+ # specify that :query param is a string, so that it will automatically be converted to string
52
+ param :query, type: :string
53
+ # specify that :page param is a number, which will be defaulted to 1 and will have 1 as lower limit
54
+ param :page, type: :number, default: 1, min: 1
55
+ # specify that :per_page param is a number, which will be defaulted to 15 and will enforce the value to be in a range between 1 and 200
56
+ param :per_page, type: :number, default: 15, clamp: 1..200
57
+ def index
58
+ # ... your code ...
59
+ end
60
+
61
+ # specify that :id param is a number and cannot be missing or blank
62
+ param :id, type: :number, allow_blank: false
63
+ def show
64
+ # ... your code ...
65
+ end
66
+
67
+ param :id, type: :number, allow_blank: false
68
+ # Sometimes you may want to specify a scope for the parameters, because you might have
69
+ # nested params, like `params[:user][:name]`
70
+ scope :user do
71
+ param :name, type: :string, allow_blank: false
72
+ # For string params you can set a format regexp validation, which will be automatically applied
73
+ param :email, type: :string, required: true, format: URI::MailTo::EMAIL_REGEXP,
74
+ modifier: ->(v) { v.strip.downcase }
75
+ param :birth_date, type: :date, required: true, max: DateTime.now
76
+ end
77
+ # You can also specify the scope inline
78
+ param :preferences, scope: :user, type: :object, default: {}, allow_blank: false
79
+ def update
80
+ # ... your code ...
81
+ end
82
+ end
83
+ ```
84
+
85
+ ### Available param types
86
+
87
+ `Pastore::Params` supports the following param types:
88
+
89
+ | Param type | Aliases | Description |
90
+ |------------|---------|-------------|
91
+ | `:string` | | Accepts string values or converts other value types to string. |
92
+ | `:number` | `integer`, `float` | Accepts integer values or tries to convert string to number. |
93
+ | `:boolean` | | Accepts boolean values or tries to convert string to boolean. |
94
+ | `:date` | | Accepts date values or tries to convert string or number (unix time) to date. |
95
+ | `:object` | | Accepts object (`Hash`) values or tries to convert JSON string to object. |
96
+ | `:any` | | Accepts any value. |
97
+
98
+ ### Avaliable options
99
+
100
+ There're several generic options that can be used with all param types, which are listed below:
101
+
102
+ | Option | Value type | Default | Description |
103
+ |--------|------------|---------|-------------|
104
+ | `:type` | `symbol`, `string`, `Class` | `:any` | Specifies the type of the parameter. |
105
+ | `:scope` | `symbol`, `string`, `symbol[]`, `string[]` | `nil` | Specifies the scope of the parameter, which is necessary for nested params definition like `params[:user][:email]`. |
106
+ | `:required` | `boolean` | `false` | When `true`, requires the parameter to be passed by client. |
107
+ | `:allow_blank` | `boolean` | `true` | When `false`, expects parameter's value not to be `nil` or empty. |
108
+ | `:default` | Depends on param type | `nil` | Allows to specify default value to set on parameter when parameter have not been sent by client. |
109
+ | `:modifier` | Lambda block | `nil` | Allows to specify a modifier lambda block, which will be used to modify parameter value. |
110
+ ||
111
+ | **String** |
112
+ | `format` | `RegExp` | `nil` | Allows to use a custom `RegExp` to validate parameter value. |
113
+ ||
114
+ | **Number** |
115
+ | `min` | `integer`, `float` | `nil` | Allows to specify a minimum value for the parameter. |
116
+ | `max` | `integer`, `float` | `nil` | Allows to specify a maximum value for the parameter. |
117
+ | `clamp` | `Range`, `integer[2]`, `float[2]` | `[-Float::INFINITY, Float::INFINITY]` | Allows to specify a lower and upper bound for the param value, so that a value outside the bounds will be forced to the nearest bound value. |
118
+ ||
119
+ | **Date** |
120
+ | `min` | `Date`, `DateTime`, `Time`, `Integer`, `Float` | `nil` | Allows to specify a minimum value for the parameter. |
121
+ | `max` | `Date`, `DateTime`, `Time`, `Integer`, `Float` | `nil` | Allows to specify a maximum value for the parameter. |
122
+ | `clamp` | `Range`, `Date[2]`, `DateTime[2]`, `Time[2]`, `Integer[2]`, `Float[2]` | `nil` | Allows to specify a lower and upper bound for the param value, so that a value outside the bounds will be forced to the nearest bound value. |
@@ -24,6 +24,10 @@ module Pastore
24
24
  end
25
25
  end
26
26
 
27
+ def current_role
28
+ self.class.pastore_guards.current_role(self)
29
+ end
30
+
27
31
  class_methods do # rubocop:disable Metrics/BlockLength
28
32
  attr_accessor :_pastore_guards
29
33
 
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pastore
4
+ module Params
5
+ # Stores data about action parameters
6
+ class ActionParam
7
+ AVAILABLE_TYPES = %w[string number boolean date object array any].freeze
8
+ TYPE_ALIASES = {
9
+ 'text' => 'string',
10
+ 'integer' => 'number',
11
+ 'float' => 'number',
12
+ 'hash' => 'object'
13
+ }.freeze
14
+
15
+ attr_reader :name, :type, :modifier, :options, :scope
16
+
17
+ def initialize(name, **options)
18
+ @name = name
19
+ @options = options
20
+ @scope = [@options&.fetch(:scope, nil)].flatten.compact
21
+ @array = @options&.fetch(:array, false) == true
22
+ @modifier = @options&.fetch(:modifier, nil)
23
+ @type = @options&.fetch(:type, :any)
24
+
25
+ check_options!
26
+ end
27
+
28
+ def validate(value)
29
+ Pastore::Params::Validation.validate!(name, type, value, modifier, **options)
30
+ end
31
+
32
+ def array?
33
+ @array
34
+ end
35
+
36
+ def name_with_scope
37
+ [@scope, name].flatten.compact.join('.')
38
+ end
39
+
40
+ private
41
+
42
+ def check_options!
43
+ check_type!
44
+ check_modifier!
45
+ check_required!
46
+ check_default!
47
+ check_min_max!
48
+ check_format!
49
+ check_clamp!
50
+ end
51
+
52
+ def check_type!
53
+ @type = @type.to_s.strip.downcase
54
+
55
+ @type = TYPE_ALIASES[@type] unless TYPE_ALIASES[@type].nil?
56
+
57
+ valid_type = AVAILABLE_TYPES.include?(@type)
58
+ raise Pastore::Params::InvalidParamTypeError, "Invalid param type: #{@type.inspect}" unless valid_type
59
+ end
60
+
61
+ def check_modifier!
62
+ return if @modifier.nil? || @modifier.is_a?(Proc)
63
+
64
+ raise "Invalid modifier, lambda or Proc expected, got #{@modifier.class}"
65
+ end
66
+
67
+ def check_required!
68
+ options[:required] = (options[:required] == true)
69
+ end
70
+
71
+ def check_default!
72
+ return if options[:default].nil?
73
+
74
+ validation = Pastore::Params::Validation.validate!(name, type, options[:default], modifier, **options)
75
+ return if validation.valid?
76
+
77
+ raise Pastore::Params::InvalidValueError, "Invalid default value: #{validation.errors.join(",")}"
78
+ end
79
+
80
+ def check_min_max!
81
+ check_min!
82
+ check_max!
83
+
84
+ return if options[:min].nil? || options[:max].nil?
85
+
86
+ raise 'Invalid min-max range' unless options[:min] <= options[:max]
87
+ end
88
+
89
+ def check_min!
90
+ min = options[:min]
91
+ return if min.nil?
92
+
93
+ raise 'Invalid minimum' unless min.is_a?(Integer) || min.is_a?(Float)
94
+ end
95
+
96
+ def check_max!
97
+ max = options[:max]
98
+ return if max.nil?
99
+
100
+ raise 'Invalid maximum' unless max.is_a?(Integer) || max.is_a?(Float)
101
+ end
102
+
103
+ def check_format!
104
+ return if options[:format].nil?
105
+
106
+ raise 'Invalid format' unless options[:format].is_a?(Regexp)
107
+ end
108
+
109
+ def check_clamp!
110
+ return if options[:clamp].nil?
111
+
112
+ check_clamp_type!
113
+ convert_clamp_to_array!
114
+ normalize_datetime_clamp!
115
+ check_clamp_bounds!
116
+ end
117
+
118
+ def check_clamp_type!
119
+ return if options[:clamp].nil?
120
+ return if [Array, Range].include?(options[:clamp].class)
121
+
122
+ raise Pastore::Params::InvalidValueError, "Invalid clamp value: #{options[:clamp].inspect}"
123
+ end
124
+
125
+ def convert_clamp_to_array!
126
+ clamp = options[:clamp]
127
+
128
+ options[:clamp] = clamp.is_a?(Array) ? [clamp.first, clamp.last] : [clamp.begin, clamp.end]
129
+ end
130
+
131
+ def check_clamp_bounds!
132
+ clamp = options[:clamp]
133
+ return if clamp.first.nil? || clamp.last.nil?
134
+
135
+ raise Pastore::Params::InvalidValueError, "Invalid clamp range: #{clamp.inspect}" if clamp.first > clamp.last
136
+ end
137
+
138
+ def normalize_datetime_clamp!
139
+ return unless @type == 'date'
140
+
141
+ options[:clamp] = options[:clamp].map do |d|
142
+ return d if d.nil?
143
+
144
+ case d
145
+ when Date then d
146
+ when String then DateTime.parse(d.to_s)
147
+ when Integer, Float then Time.at(d).to_datetime
148
+ when Time then d.to_datetime
149
+ else
150
+ raise Pastore::Params::InvalidValueError, "Invalid clamp value: #{options[:clamp].inspect}"
151
+ end
152
+ end
153
+ rescue Date::Error
154
+ raise Pastore::Params::InvalidValueError, "Invalid clamp value: #{options[:clamp].inspect}"
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'action_param'
4
+ require_relative 'validation'
5
+
6
+ module Pastore
7
+ module Params
8
+ # Implements the logic for params settings storage for a controller.
9
+ class Settings
10
+ attr_writer :invalid_params_cbk, :response_status
11
+
12
+ def initialize(superklass)
13
+ @super_params = superklass.pastore_params if superklass.respond_to?(:pastore_params)
14
+ reset!
15
+ end
16
+
17
+ def reset!
18
+ @actions = {}
19
+ @invalid_params_cbk = nil
20
+ @response_status = nil
21
+
22
+ reset_buffer!
23
+ reset_scope!
24
+ end
25
+
26
+ def invalid_params_cbk
27
+ @invalid_params_cbk || @super_params&.invalid_params_cbk
28
+ end
29
+
30
+ def response_status
31
+ @response_status || @super_params&.response_status || :unprocessable_entity
32
+ end
33
+
34
+ def reset_buffer!
35
+ @buffer = []
36
+ end
37
+
38
+ def set_scope(*keys)
39
+ @scope = [keys].flatten.compact.map(&:to_sym)
40
+ end
41
+
42
+ def reset_scope!
43
+ @scope = nil
44
+ end
45
+
46
+ def add(name, **options)
47
+ options = { scope: @scope }.merge(options.symbolize_keys)
48
+
49
+ raise ParamAlreadyDefinedError, "Param #{name} already defined" if @buffer.any? { |p| p.name == name }
50
+
51
+ if @scope.present? && options[:scope].present? && @scope != options[:scope]
52
+ error = "Scope overwrite attempt detected (current_scope: #{@scope}, param_scope: #{options[:scope]}) for param #{name}"
53
+ raise ScopeConflictError, error
54
+ end
55
+
56
+ param = ActionParam.new(name, **options)
57
+
58
+ @buffer << param
59
+ end
60
+
61
+ def save_for(action_name)
62
+ @actions[action_name.to_sym] = @buffer
63
+ reset_buffer!
64
+ end
65
+
66
+ def validate(params, action_name)
67
+ action_params = @actions[action_name.to_sym]
68
+ return {} if action_params.blank?
69
+
70
+ action_params.each_with_object({}) do |validator, errors|
71
+ value = safe_dig(params, *validator.scope, validator.name)
72
+ validation = validator.validate(value)
73
+
74
+ if validation.valid?
75
+ update_param_value!(params, validator, validation)
76
+
77
+ next if validation.errors.empty?
78
+ end
79
+
80
+ errors[validator.name_with_scope] = validation.errors
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def update_param_value!(params, validator, validation)
87
+ if validator.scope.empty?
88
+ params[validator.name] = validation.value
89
+ return
90
+ end
91
+
92
+ # Try to create missing scope keys
93
+ key_path = []
94
+ validator.scope.each do |key|
95
+ params[key] ||= {}
96
+ key_path << key
97
+
98
+ if params[key].is_a?(ActionController::Parameters)
99
+ params = params[key]
100
+ next
101
+ end
102
+
103
+ # if for some reason the scope key is not a hash, we need to add the error to validation errors
104
+ return validation.add_error(:bad_schema, "Invalid param schema at #{key_path.join(".").inspect}")
105
+ end
106
+
107
+ params[validator.name] = validation.value
108
+ end
109
+
110
+ def safe_dig(params, *keys)
111
+ [keys].flatten.reduce(params) do |acc, key|
112
+ acc.respond_to?(:key?) ? acc[key] : nil
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pastore
4
+ module Params
5
+ # Implements the logic of a single param validation
6
+ class Validation
7
+ require_relative 'validations/string_validation'
8
+ require_relative 'validations/number_validation'
9
+ require_relative 'validations/boolean_validation'
10
+ require_relative 'validations/object_validation'
11
+ require_relative 'validations/date_validation'
12
+
13
+ # Validates the value based on the given type and values with the appropriate validator.
14
+ def self.validate!(name, type, value, modifier = nil, **options)
15
+ case type
16
+ when 'string' then Pastore::Params::StringValidation.new(name, value, modifier, **options)
17
+ when 'number' then Pastore::Params::NumberValidation.new(name, value, modifier, **options)
18
+ when 'boolean' then Pastore::Params::BooleanValidation.new(name, value, modifier, **options)
19
+ when 'object' then Pastore::Params::ObjectValidation.new(name, value, modifier, **options)
20
+ when 'date' then Pastore::Params::DateValidation.new(name, value, modifier, **options)
21
+ when 'any' then Validation.new(name, 'any', value, modifier, **options)
22
+ else
23
+ raise Pastore::Params::InvalidValidationTypeError, "Invalid validation type: #{type}"
24
+ end
25
+ end
26
+
27
+ attr_reader :value, :errors
28
+
29
+ def initialize(name, type, value, modifier = nil, **options)
30
+ @name = name
31
+ @type = type
32
+ @modifier = modifier
33
+ @value = value.nil? ? options[:default] : value
34
+ @required = (options[:required] == true) # default: false
35
+ @allow_blank = (options[:allow_blank].nil? || options[:allow_blank]) # default: true
36
+ @allowed_values = options[:in]
37
+ @exclude_values = options[:exclude]
38
+
39
+ @errors = []
40
+
41
+ validate!
42
+ end
43
+
44
+ # Returns true if the value is valid, false otherwise.
45
+ def valid?
46
+ @errors.empty?
47
+ end
48
+
49
+ # Returns true if the value is required, false otherwise.
50
+ def required?
51
+ @required
52
+ end
53
+
54
+ # Adds an error to the list of errors.
55
+ def add_error(error_type, message)
56
+ @errors << { type: 'param', name: @name, value: @value, error: error_type, message: message }
57
+ end
58
+
59
+ private
60
+
61
+ # Performs a basic validation of the value and applies the modifier.
62
+ def validate!
63
+ # check for value presence and if it's allowed to be blank
64
+ check_presence!
65
+ apply_modifier!
66
+ end
67
+
68
+ # Checks if the value is present (not nil) and if it's allowed to be blank.
69
+ def check_presence!
70
+ valid = true
71
+
72
+ # required options ensures that value is present (not nil)
73
+ valid = false if required? && value.nil?
74
+
75
+ # allow_blank option ensures that value is not blank (not empty)
76
+ valid = false if !@allow_blank && value.to_s.strip == ''
77
+
78
+ add_error(:is_blank, "#{@name} cannot be blank") unless valid
79
+
80
+ valid
81
+ end
82
+
83
+ # Applies the modifier to the value.
84
+ def apply_modifier!
85
+ return if @modifier.nil?
86
+
87
+ @value = @modifier.call(@value)
88
+ end
89
+
90
+ # check if value is in the list of allowed values
91
+ def check_allowed_values!
92
+ check_inclusion!
93
+ check_exclusion!
94
+ end
95
+
96
+ def check_inclusion!
97
+ return if @allowed_values.nil?
98
+ return if @allowed_values.include?(value)
99
+
100
+ add_error(:not_allowed, "#{@name} has invalid value: #{value}")
101
+ end
102
+
103
+ def check_exclusion!
104
+ return if @exclude_values.nil?
105
+ return unless @exclude_values.include?(value)
106
+
107
+ add_error(:not_allowed, "#{@name} has invalid value: #{value}")
108
+ end
109
+
110
+ # check if value is a number
111
+ def numeric?
112
+ !Float(value).nil?
113
+ rescue ArgumentError
114
+ false
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pastore
4
+ module Params
5
+ # Implements the validation logic for object parameters.
6
+ class BooleanValidation < Validation
7
+ def initialize(name, value, modifier, **options)
8
+ super(name, 'boolean', value, modifier, **options)
9
+ end
10
+
11
+ private
12
+
13
+ def validate!
14
+ # check for value presence and if it's allowed to be blank
15
+ check_presence!
16
+
17
+ # don't go further if value is blank
18
+ return if value.to_s.strip == ''
19
+
20
+ # check if value is a boolean
21
+ return unless check_if_boolean!
22
+
23
+ # check if value is in the list of allowed values
24
+ check_allowed_values!
25
+
26
+ # apply the modifier
27
+ apply_modifier!
28
+ end
29
+
30
+ def check_if_boolean!
31
+ return true if [true, false].any?(value)
32
+
33
+ if value.is_a?(String) && boolean?
34
+ @value = %w[t true y yes].any?(value.strip.downcase)
35
+ return true
36
+ end
37
+
38
+ add_error(:invalid_type, "#{@name} has invalid type: #{@type} expected")
39
+
40
+ false
41
+ end
42
+
43
+ def boolean?
44
+ %w[t true y yes f false n no].any?(value.strip.downcase)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pastore
4
+ module Params
5
+ # Implements the validation logic for date parameters.
6
+ class DateValidation < Validation
7
+ def initialize(name, value, modifier, **options)
8
+ @min = options[:min]
9
+ @max = options[:max]
10
+ @clamp = options[:clamp]
11
+
12
+ super(name, 'date', value, modifier, **options)
13
+ end
14
+
15
+ private
16
+
17
+ def validate!
18
+ # check for value presence and if it's allowed to be blank
19
+ check_presence!
20
+
21
+ # don't go further if value is blank
22
+ return if value.to_s.strip == ''
23
+
24
+ # check if value is a boolean
25
+ return unless check_if_date! && check_min_max! && check_clamp!
26
+
27
+ # check if value is in the list of allowed values
28
+ check_allowed_values!
29
+
30
+ # apply the modifier
31
+ apply_modifier!
32
+ end
33
+
34
+ def check_if_date!
35
+ return true if [Date, Time, DateTime].include?(value.class)
36
+
37
+ if numeric?
38
+ @value = Time.at(value.to_f).to_datetime
39
+ return true
40
+ end
41
+
42
+ # When value is a string, try to parse it as a DateTime object
43
+ if value.is_a?(String)
44
+ begin
45
+ @value = DateTime.parse(value)
46
+ return true
47
+ rescue Date::Error
48
+ # Do nothing
49
+ end
50
+ end
51
+
52
+ add_error(:invalid_type, "#{@name} has invalid type: #{@type} expected")
53
+
54
+ false
55
+ end
56
+
57
+ def check_min_max!
58
+ min_invalid = @min && value < @min
59
+ max_invalid = @max && value > @max
60
+
61
+ add_error(:too_small, "#{@name} should be greater than #{@min}") if min_invalid
62
+ add_error(:too_large, "#{@name} should be smaller than #{@max}") if max_invalid
63
+
64
+ min_invalid || max_invalid ? false : true
65
+ end
66
+
67
+ def check_clamp!
68
+ return true if @clamp.nil?
69
+
70
+ @value = @value.clamp(@clamp.first, @clamp.last)
71
+ end
72
+ end
73
+ end
74
+ end