param_param 0.0.2

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: 61d152a83cea632bd4e660508c5dc62d26231e9bb60a35626e1991548171e205
4
+ data.tar.gz: 28d02503d7fc9a374d1e555c3e9328aac9418194ed6520989165b66fe5d4a3cd
5
+ SHA512:
6
+ metadata.gz: bdef7f585fc8f1050327bcdbe0946cc3255eca321c2f43b17fd42035cdc4d029f5d85011f91e01e2813af50b9e47ae70593f676078e483611efeed2e2a3f583b
7
+ data.tar.gz: '004692833ec9cefe0d4ebdc6ec02ffa1c4de71ef38995e03aa62eecf2cbe4e8fa839f75a9119ca4adcab5576742909d69955f332f5e833c01965544fdd54c504'
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Mr Dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ # param_param
2
+ Params parser built on lambdas.
3
+
4
+ Inspired by Martin Chabot's [Simple Functional Strong Parameters In Ruby](https://blog.martinosis.com/blog/simple-functional-strong-params-in-ruby) article.
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ParamParam
4
+ # Defines operation result pattern.
5
+ # A result can be a success or a failure and has some value.
6
+ class Result
7
+ # Returns +false+.
8
+ def success?
9
+ false
10
+ end
11
+
12
+ # Returns +false+.
13
+ def failure?
14
+ false
15
+ end
16
+ end
17
+
18
+ # Describes successful result.
19
+ class Success < Result
20
+ # A value related to the success.
21
+ attr_reader :value
22
+
23
+ def initialize(value)
24
+ @value = value
25
+ end
26
+
27
+ # Returns +true+.
28
+ def success?
29
+ true
30
+ end
31
+ end
32
+
33
+ # Describes failed result.
34
+ class Failure < Result
35
+ # An error related to the failure.
36
+ attr_reader :error
37
+
38
+ def initialize(error)
39
+ @error = error
40
+ end
41
+
42
+ # Returns +true+.
43
+ def failure?
44
+ true
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ # It contains a collection of some useful rules.
4
+ module ParamParam
5
+ # Some string values that can be considered as +true+ (thank you dry-rb for inspiration).
6
+ TRUE_VALUES = %w[1 on On ON t true True TRUE T y yes Yes YES Y].freeze
7
+ # Some string values that can be considered as +false+ (thank you dry-rb for inspiration).
8
+ FALSE_VALUES = %w[0 off Off OFF f false False FALSE F n no No NO N].freeze
9
+
10
+ NOT_GTE = :not_gte
11
+ NOT_GT = :not_gt
12
+ NOT_LTE = :not_lte
13
+ NOT_LT = :not_lt
14
+ NOT_INCLUDED = :not_included
15
+ TOO_LONG = :too_long
16
+ TOO_SHORT = :too_short
17
+
18
+ NON_BOOL = :non_bool
19
+ NON_DECIMAL = :non_decimal
20
+ NON_INTEGER = :non_integer
21
+ NON_STRING = :non_string
22
+
23
+ # Verifies inclusion of a value in a collection.
24
+ #
25
+ # Returns
26
+ # lambda { |collection, option| ... }.
27
+ #
28
+ # Verifies if value of the +option+ is included in the provided +collection+.
29
+ def self.included_in
30
+ lambda { |collection, option|
31
+ collection.include?(option.value) ? Success.new(option) : Failure.new(NOT_INCLUDED)
32
+ }.curry
33
+ end
34
+
35
+ # Returns
36
+ # lambda { |limit, option| ... }.
37
+ #
38
+ # Checks if the +option+'s value is greater than or equal to the provided +limit+.
39
+ def self.gte
40
+ ->(limit, option) { option.value >= limit ? Success.new(option) : Failure.new(NOT_GTE) }.curry
41
+ end
42
+
43
+ # Returns
44
+ # lambda { |limit, option| ... }.
45
+ #
46
+ # Checks if the +option+'s value is greater than the provided +limit+.
47
+ def self.gt
48
+ ->(limit, option) { option.value > limit ? Success.new(option) : Failure.new(NOT_GT) }.curry
49
+ end
50
+
51
+ # Returns
52
+ # lambda { |limit, option| ... }.
53
+ #
54
+ # Checks if the +option+'s value is less than or equal to the provided +limit+.
55
+ def self.lte
56
+ ->(limit, option) { option.value <= limit ? Success.new(option) : Failure.new(NOT_LTE) }.curry
57
+ end
58
+
59
+ # Returns
60
+ # lambda { |limit, option| ... }.
61
+ #
62
+ # Checks if the +option+'s value is less than the provided +limit+.
63
+ def self.lt
64
+ ->(limit, option) { option.value < limit ? Success.new(option) : Failure.new(NOT_LT) }.curry
65
+ end
66
+
67
+ # Returns
68
+ # lambda { |limit, option| ... }.
69
+ #
70
+ # Checks if the size of the value in +option+ does not exceed provided +limit+.
71
+ def self.max_size
72
+ lambda { |limit, option|
73
+ option.value.size <= limit ? Success.new(option) : Failure.new(TOO_LONG)
74
+ }.curry
75
+ end
76
+
77
+ # Returns
78
+ # lambda { |limit, option| ... }.
79
+ #
80
+ # Checks if the size of the value in +option+ is not lower than the provided +limit+.
81
+ def self.min_size
82
+ lambda { |limit, option|
83
+ option.value.size >= limit ? Success.new(option) : Failure.new(TOO_SHORT)
84
+ }.curry
85
+ end
86
+
87
+ # Returns
88
+ # lambda { |option| ... }.
89
+ #
90
+ # Removes leading and trailing spaces from string provided in +option+'s value.
91
+ def self.stripped
92
+ ->(option) { Success.new(Optiomist.some(option.value.strip)) }
93
+ end
94
+
95
+ # Returns
96
+ # lambda { |fn, option| ... }.
97
+ #
98
+ # Converts provided +option+'s value to integer.
99
+ # If the conversion is not possible it fails, otherwise executes the provider function +fn+
100
+ # for the converted integer value.
101
+ def self.integer
102
+ lambda { |fn, option|
103
+ begin
104
+ integer_value = Integer(option.value)
105
+ rescue StandardError
106
+ return Failure.new(NON_INTEGER)
107
+ end
108
+ fn.call(Optiomist.some(integer_value))
109
+ }.curry
110
+ end
111
+
112
+ # Returns
113
+ # lambda { |fn, option| ... }.
114
+ #
115
+ # Converts provided +option+'s value to float.
116
+ # If the conversion is not possible it fails, otherwise executes the provider function +fn+
117
+ # for the converted float value.
118
+ def self.decimal
119
+ lambda { |fn, option|
120
+ begin
121
+ float_value = Float(option.value)
122
+ rescue StandardError
123
+ return Failure.new(NON_DECIMAL)
124
+ end
125
+ fn.call(Optiomist.some(float_value))
126
+ }.curry
127
+ end
128
+
129
+ # Returns
130
+ # lambda { |fn, option| ... }.
131
+ #
132
+ # Converts provided +option+'s value to boolean.
133
+ # If the conversion is not possible it fails, otherwise executes the provider function +fn+
134
+ # for the converted boolean value.
135
+ def self.bool
136
+ lambda { |fn, option|
137
+ case option
138
+ in Optiomist::Some
139
+ if [true, *TRUE_VALUES].include?(option.value)
140
+ fn.call(Optiomist.some(true))
141
+ elsif [false, *FALSE_VALUES].include?(option.value)
142
+ fn.call(Optiomist.some(false))
143
+ else
144
+ Failure.new(NON_BOOL)
145
+ end
146
+ in Optiomist::None
147
+ Failure.new(NON_BOOL)
148
+ end
149
+ }.curry
150
+ end
151
+
152
+ # Returns
153
+ # lambda { |fn, option| ... }.
154
+ #
155
+ # Converts provided +option+'s value to string.
156
+ # If the conversion is not possible it fails, otherwise executes the provider function +fn+
157
+ # for the converted string value.
158
+ def self.string
159
+ lambda { |fn, option|
160
+ case option
161
+ in Optiomist::Some
162
+ fn.call(Optiomist.some(option.value.to_s))
163
+ in Optiomist::None
164
+ Failure.new(NON_STRING)
165
+ end
166
+ }.curry
167
+ end
168
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optiomist'
4
+ require 'param_param/result'
5
+ require 'param_param/std'
6
+
7
+ # The main purpose of this module is to convert hash data
8
+ # applying a chain of rules to values in the provided hash.
9
+ #
10
+ # It can be used to process form data in a web application converting
11
+ # user provided data to values understood by the application and validate
12
+ # it the data fulfills constraints required by the application.
13
+ #
14
+ # Example:
15
+ #
16
+ # class UserOperation
17
+ # Rules = ParamParam.define.(
18
+ # name: ParamParam.required.(
19
+ # ParamParam.string.(ParamParam.all_of.([ParamParam.not_nil, ParamParam.stripped, ParamParam.max_size.(50)]))
20
+ # ),
21
+ # admin: ParamParam.required.(ParamParam.bool.(ParamParam.any)),
22
+ # age: ParamParam.optional.(ParamParam.integer.(ParamParam.gt.(0))),
23
+ # )
24
+ #
25
+ # def create(name:, age:)
26
+ # params, errors = Rules.(name: name, age: age)
27
+ # throw errors unless errors.empty?
28
+ #
29
+ # # do something with params
30
+ # end
31
+ # end
32
+ module ParamParam
33
+ MISSING = :missing
34
+ BLANK = :blank
35
+
36
+ # Converts provided value to +Optiomist::Some+.
37
+ # If the value is already +Optiomist::Some+ or +Optiomist::None+ returns it untouched.
38
+ def self.optionize(value)
39
+ if value.is_a?(Optiomist::Some) || value.is_a?(Optiomist::None)
40
+ value
41
+ else
42
+ Optiomist.some(value)
43
+ end
44
+ end
45
+
46
+ # Verifies if provided value is nil, empty string or string consisting only from spaces.
47
+ def self.blank?(value)
48
+ value.nil? || (value.is_a?(String) && value.strip.empty?)
49
+ end
50
+
51
+ # Returns lambda that allows defining a set of rules and bind them to symbols.
52
+ # Later those rules can be applied to parameters provided in a for of a hash.
53
+ # Each rule defined for a given key processes a value related to the same key in provided parameters.
54
+ # lambda { |rules, params| ... }
55
+ #
56
+ # The lambda returns two hashes:
57
+ # - if a value related to a key can be procesed by the rules,
58
+ # the result is bound to the key and added to the first hash
59
+ # - if a rule can't be applied to a value,
60
+ # the error is bound to the key and added to the second hash
61
+ #
62
+ # Each rule needs to be a lambda taking +Optiomist+ as the only or the last parameter and returning either:
63
+ # - +ParamParam::Success+ with processed option
64
+ # - +ParamParam::Failure+ with an error
65
+ def self.define
66
+ lambda { |rules, params|
67
+ results = rules.to_h do |key, fn|
68
+ option = params.key?(key) ? optionize(params[key]) : Optiomist.none
69
+ [key, fn.call(option)]
70
+ end
71
+
72
+ errors = results.select { |_, result| result.failure? }
73
+ .transform_values(&:error)
74
+ params = results.select { |_, result| result.success? && result.value.some? }
75
+ .transform_values { |result| result.value.value }
76
+ [params, errors]
77
+ }.curry
78
+ end
79
+
80
+ # It return lambda that allows defining a chain of rules that will be applied one by one
81
+ # to value processed by a previous rule.
82
+ #
83
+ # Returns:
84
+ # lambda { |fns, option| ... }
85
+ # If some rule fails the chain is broken and value stops being processed.
86
+ def self.all_of
87
+ lambda { |fns, option|
88
+ fns.reduce(Success.new(option)) { |result, fn| result.failure? ? result : fn.call(result.value) }
89
+ }.curry
90
+ end
91
+
92
+ # Returns
93
+ # lambda { |option| ... }.
94
+ #
95
+ # Always succeeds with the provided +option+.
96
+ def self.any
97
+ ->(option) { Success.new(option) }
98
+ end
99
+
100
+ # Describes an optional value.
101
+ #
102
+ # Returns
103
+ # lambda { |fn, option| ... }.
104
+ #
105
+ # If +option+ is the +Optiomist::None+ it succeeds causing the parameter not to be included in the final result.
106
+ # Otherwise executes the funciton +fn+ for the option.
107
+ def self.optional
108
+ lambda { |fn, option|
109
+ case option
110
+ in Optiomist::None
111
+ Success.new(option)
112
+ in Optiomist::Some
113
+ fn.call(option)
114
+ end
115
+ }.curry
116
+ end
117
+
118
+ # Describes a required value.
119
+ #
120
+ # Returns
121
+ # lambda { |fn, option| ... }.
122
+ #
123
+ # If +option+ is a +Optiomist::None+ it fails otherwise executes the funciton +fn+ for the option.
124
+ def self.required
125
+ lambda { |fn, option|
126
+ case option
127
+ in Optiomist::None
128
+ Failure.new(MISSING)
129
+ in Optiomist::Some
130
+ fn.call(option)
131
+ end
132
+ }.curry
133
+ end
134
+
135
+ # Converts blank value to nil or passes non blank value to next rule.
136
+ #
137
+ # Returns
138
+ # lambda { |fn, option| ... }.
139
+ #
140
+ # If provided +option+'s value is blank it succeeds with +nil+
141
+ # otherwise executes provided function for the +option+.
142
+ def self.blank_to_nil_or
143
+ lambda { |fn, option|
144
+ blank?(option.value) ? Success.new(Optiomist.some(nil)) : fn.call(option)
145
+ }.curry
146
+ end
147
+
148
+ # Verifies if value is not blank.
149
+ #
150
+ # Returns
151
+ # lambda { |option| ... }.
152
+ #
153
+ # It fails if provided +option+ is blank, otherwise succeeds with the +option+.
154
+ def self.not_blank
155
+ ->(option) { blank?(option.value) ? Failure.new(BLANK) : Success.new(option) }
156
+ end
157
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: param_param
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Michał Radmacher
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-09-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: optiomist
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.3
27
+ description:
28
+ email: michal@radmacher.pl
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files:
32
+ - README.md
33
+ - LICENSE
34
+ files:
35
+ - LICENSE
36
+ - README.md
37
+ - lib/param_param.rb
38
+ - lib/param_param/result.rb
39
+ - lib/param_param/std.rb
40
+ homepage: https://github.com/mradmacher/param_param
41
+ licenses:
42
+ - MIT
43
+ metadata:
44
+ rubygems_mfa_required: 'true'
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 2.7.0
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.3.7
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: Params parser built on lambdas
64
+ test_files: []