param_param 0.0.2

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 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: []