light_service-validated_context 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d272960b5f0002c3b640bba8eeca1684477df3c65a6c7031f3169abcd97eb3f2
4
- data.tar.gz: 13f2d1b53652113892ad2d5e93a34aefee19ed38901034e33ac9764590756289
3
+ metadata.gz: c16c17a54c9867f59ccbf92ec6d969760eb396719ecda969e12d2fec2b063623
4
+ data.tar.gz: 62103f8748ab7efc04cad3253d5dc5aabd153ae62e4d022ca0427365dc2f657f
5
5
  SHA512:
6
- metadata.gz: 483f3c71553b51875b1e10d45c113f42ef3d93102658e7df5499bb912fffb810b9555fade2506eaf6e0c1bc4573b06f8b08b179ee1f2d6a336899f8e519d5aad
7
- data.tar.gz: c0b8e60f94ad3dbeda5d3e10377633fb6dc248543176fda3d5e4353034e76c8b48804c855d7f9c0d778fe0a7180fa313ae313624fc8a26c1cc4257529cf67ce9
6
+ metadata.gz: 79c63b97672442aabfdc1215f1ca1e5c92293508da7ab3cea9e23e6453a1b9017f240501af69aa7423780f4b9b77b3ae5eecb53aa4b7d80dbaecb0285b94271e
7
+ data.tar.gz: 003447c4164440525f526cca2859f3151ebaeb7cc7ba4e32d3ef1f2022b3aa9418ab699400975d4bbe659444678c6df1226716959c00d6c2ccad2a944e547d13
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- light_service-validated_context (0.2.2)
4
+ light_service-validated_context (0.3.0)
5
5
  dry-types (~> 1.7.0)
6
6
  light-service (>= 0.18.0)
7
7
  zeitwerk
data/README.md CHANGED
@@ -47,26 +47,57 @@ require 'light_service/validated_context'
47
47
 
48
48
  ## Usage
49
49
 
50
+ The plugin enables you to pass `VK` (`ValidatedKeys`) objects as arguments to built-ins `expects` and
51
+ `promises` macros.
52
+
53
+ This is how you'd usually write an `Action` in LightService:
54
+
55
+ ```ruby
56
+ class ActionOne
57
+ extend LightService::Action
58
+
59
+ expects :age
60
+ promises :text
61
+
62
+ executed do |context|
63
+ validate_age!(context)
64
+
65
+ # Do something...
66
+
67
+ context.text = 'Long live and prosperity'
68
+ end
69
+
70
+ def self.validate_age!(context)
71
+ context.fail_and_return!(':age must be an Integer') unless context.age.is_a? Integer
72
+ context.fail_and_return!('Sorry, you are too young m8') if (context.age <= 30)
73
+ end
74
+ end
75
+ ```
76
+
77
+ and this is how `light_service-validated_context` enables you to write
78
+
50
79
  ```ruby
51
80
  class ActionOne
52
81
  extend LightService::Action
53
82
 
54
- expects VK.new(:email, Types::Strict::String)
55
83
  expects VK.new(:age, Types::Coercible::Integer.constrained(gt: 30))
56
- expects VK.new(:ary, Types::Array.of(Types::Strict::Symbol).constrained(min_size: 1))
57
- promises VK.new(:text, Types::Strict::String.constrained(max_size: 10).default('foobar'))
84
+ promises VK.new(:text, Types::Strict::String.constrained(max_size: 10).default('Long live and prosperity'))
58
85
 
59
86
  executed do |context|
60
- # something happens
87
+ # Do something
61
88
  end
62
89
  end
90
+ ```
91
+
92
+ and you'll get validations for free
63
93
 
64
- result = ActionOne.execute(email: 'foo@example.com', age: '37', ary: [:foo])
65
- # => {:email=>"foo@example.com", :age=>37, :ary=>[:foo], :text=>"foobar"}
66
- result = ActionOne.execute(age: '37', ary: [:foo])
67
- # expected :email to be in the context during ActionOne (LightService::ExpectedKeysNotInContextError)
68
- result = ActionOne.execute(email: 'foo@example.com', age: '37', ary: [])
69
- # [] violates constraints (min_size?(1, []) failed) (LightService::ExpectedKeysNotInContextError)
94
+ ```ruby
95
+ ActionOne.execute(age: '19')
96
+ # [App::ActionOne][:age] "19" violates constraints (gt?(30, 19) failed) (LightService::ExpectedKeysNotInContextError)
97
+ ActionOne.execute(age: 37)
98
+ # LightService::Context({:age=>37, :text=>"Long live and prosperity"}, success: true, message: '', error_code: nil, skip_remaining: false, aliases: {})
99
+ ActionOne.execute(age: 37, text: 'Too long too pass the constrain')
100
+ # [App::ActionOne][:text] "Too long too pass the constrain" violates constraints (max_size?(24, "Too long too pass the constrain") failed) (LightService::PromisedKeysNotInContextError)
70
101
  ```
71
102
 
72
103
  Since all the validation and coercion logic is delegated to `dry-types`, you can
@@ -75,7 +106,7 @@ read more about what you can achieve at https://dry-rb.org/gems/dry-types/1.2/
75
106
  `VK` objects needs to be created with 2 positional arguments:
76
107
 
77
108
  - key name as a symbol
78
- - A type
109
+ - A type declaration from `dry-types` (`Tyeps` namespace is already setup for you)
79
110
 
80
111
  `VK` and `ValidatedKey` (equivalent) are short aliases for `LightService::Context::ValidatedKey`.
81
112
  They are created only if not already defined in the global space. You're free to use the namespaced
@@ -83,20 +114,53 @@ form to avoid name collisions.
83
114
 
84
115
  You can find more usage example in `spec/support/test_doubles.rb`
85
116
 
117
+ ### Custom validation error message
118
+
119
+ You can set a custom validation error message when instantiating a `VK`
120
+
121
+ ```ruby
122
+ VK.new(:my_integer, Types::Strict::Integer, message: 'Custom validatio message for :my_integer key')
123
+ ```
124
+
125
+ Messages translated via `I18n` are supported too, following stardard `light-service`'s configuration
126
+
127
+ ```ruby
128
+ VK.new(:my_integer, Types::Strict::Integer, message: :my_integer_error_message)
129
+ ```
130
+
131
+ ### Raise vs fail
132
+
133
+ By default, following original `light-service` implementation, a validation error will raise
134
+ and error.
135
+
136
+ May you prefere to fail the action populating outcome's message with error message:
137
+
138
+ ```ruby
139
+ class ActionFailInsteadOfRaise
140
+ extend LightService::Action
141
+ extend LightService::Context::FailOnValidationError
142
+
143
+ expects VK.new(:foo, Types::String)
144
+
145
+ executed do |context|
146
+ # do something
147
+ end
148
+ end
149
+
150
+ result = ActionFailInsteadOfRaise.execute(foo: 12)
151
+ result.message # Here you'll find the validation(s) message(s)
152
+ ```
153
+
86
154
  ## Why validation matters?
87
155
 
88
- In OO programming there's a rule (strict or "of thumb", IDK) that says to "never" instantiate an
89
- invalid object whenever the object self has the concept of _validity_ for itself. This rule takes
90
- sense to my eyes whenever I'm working with an object already initialized and in memory, but I cannot
91
- trust its internal status.
156
+ In OO programming there's a rule (strict or "of thumb", IDK) that says to never instantiate an
157
+ invalid object - given the object itself has the concept of _validity_.
92
158
 
93
- Taken that `light-service` doesn't work on instances, but it works on classes and class methods
94
- having a more functional and stateless approach, side effects of having invalid state in the context
95
- (which is The state of an Action/Organizer) are mostly the same.
159
+ How many times do you find yourself working with an object already initialized and in memory, but
160
+ you cannot trust its internal status?
96
161
 
97
- Rewording: if I cannot trust the state, given
98
- the state is internal or delegated to a context object, I'll have to to a bunch of validation-oriented
99
- logical branches into my logic. E.g.:
162
+ If you cannot trust the state, given the state is internal or delegated to a context object,
163
+ you'll have to do a bunch of validation-oriented logical branches into your logic. E.g.:
100
164
 
101
165
  ```ruby
102
166
  class HugAFriend
@@ -110,7 +174,7 @@ class HugAFriend
110
174
  end
111
175
  ```
112
176
 
113
- The `if` in this uber-trivial example exists just due to lack of trust on the state.
177
+ The `if` in this uber-trivial example exists just due to untrusted state.
114
178
 
115
179
  Let's re-imagine the code given an `executed` block that totally trusts the context:
116
180
 
@@ -122,6 +186,7 @@ class HugAFriend
122
186
  expects VK.new(:friend, Types.Instance(Friend))
123
187
  # Or a less usual approach could be to trust duck typing
124
188
  # expects VK.new(:friend, Types::Interface(:hug))
189
+ # Actually not all friends do appreciate hugs nor other forms of physical contact :P
125
190
 
126
191
  executed do |context|
127
192
  context.friend.hug
@@ -131,6 +196,8 @@ end
131
196
 
132
197
  ## Comparison with similar gems
133
198
 
199
+ A brief comparison about what similar gems offer to work with validation.
200
+
134
201
  This is a comparison table I've done using my own limited experience w/ other solutions
135
202
  and/or reading projects' READMEs. Don't take my word for it. And if I was wrong understanding
136
203
  some features, feel free to drop me a line on Mastodon [@alessandrofazzi@mastodon.uno](https://mastodon.uno/@alessandrofazzi)
@@ -140,11 +207,11 @@ some features, feel free to drop me a line on Mastodon [@alessandrofazzi@mastodo
140
207
  | presence | ✅ | ✅ | ❌ | ⚠️ Only input, not output | ✅ |
141
208
  | static default | ✅ | ✅ | ❌ | ✅ | ✅ |
142
209
  | dynamic default | ✅ | ✅ | ❌ | ✅ | ✅ |
143
- | raise or fail control | ❌ | ✅ | ❌ | ❓ | |
210
+ | raise or fail control | ❌ | ✅ | ❌ | ❓ | |
144
211
  | type check | ❌ | ✅ | ❌ | ✅ | ✅ |
145
212
  | data structure type check | ❌ | ❌ | ❌ | ❌ | ✅ |
146
213
  | optional | ⚠️ through `default` | ✅ through `allow_nil` (which defaults to `true` 🤔 ❓) | ❌ | ⚠️ through `default` | ✅ |
147
- | built-in | ✅ | ✅ | | ❌ ActiveModel::Validation | ❌ Dry::Types |
214
+ | 1st party code | ✅ | ✅ | | ❌ ActiveModel::Validation | ❌ Dry::Types |
148
215
 
149
216
 
150
217
  ## Development
@@ -18,14 +18,12 @@ module ValidatedContext
18
18
  action.expected_keys
19
19
  end
20
20
 
21
- def throw_error_predicate(keys)
21
+ def throw_error_predicate(_keys)
22
22
  type_check_and_coerce_keys!(raw_keys)
23
23
 
24
- keys_are_all_present = are_all_keys_in_context?(keys)
24
+ return false if are_all_keys_valid?
25
25
 
26
- return false if are_all_keys_valid? && keys_are_all_present
27
-
28
- true
26
+ should_throw_on_validation_error?
29
27
  end
30
28
  end
31
29
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ValidatedContext
4
+ module FailOnValidationError
5
+ def fail_on_validation_error? = true
6
+ def throw_on_validation_error? = !fail_on_validation_error?
7
+ end
8
+ end
@@ -2,6 +2,14 @@
2
2
 
3
3
  module ValidatedContext
4
4
  module KeyVerifier
5
+ def initialize(context, action)
6
+ @validation_errors = []
7
+
8
+ super(context, action)
9
+ end
10
+
11
+ # rubocop:disable Metrics/AbcSize
12
+ # Refactoring this is out of my scope ATM
5
13
  def type_check_and_coerce_keys!(keys)
6
14
  errors = []
7
15
 
@@ -11,12 +19,16 @@ module ValidatedContext
11
19
  begin
12
20
  context[key.label] = key.type[context[key.label] || Dry::Types::Undefined]
13
21
  rescue Dry::Types::CoercionError => e
14
- errors << "[#{action}][:#{key.label}] #{e.message}"
22
+ errors << (
23
+ LightService::Configuration.localization_adapter.failure(key.message, action) ||
24
+ "[#{action}][:#{key.label}] #{e.message}"
25
+ )
15
26
  end
16
27
  end
17
- # debugger
28
+
18
29
  @validation_errors = errors
19
30
  end
31
+ # rubocop:enable Metrics/AbcSize
20
32
 
21
33
  def are_all_keys_valid?
22
34
  @validation_errors.none?
@@ -25,5 +37,12 @@ module ValidatedContext
25
37
  def error_message
26
38
  @validation_errors.join(', ')
27
39
  end
40
+
41
+ def should_throw_on_validation_error?
42
+ return true unless action.respond_to?(:fail_on_validation_error?) && action.fail_on_validation_error?
43
+
44
+ context.fail!(error_message)
45
+ false
46
+ end
28
47
  end
29
48
  end
@@ -18,14 +18,12 @@ module ValidatedContext
18
18
  action.promised_keys
19
19
  end
20
20
 
21
- def throw_error_predicate(keys)
21
+ def throw_error_predicate(_keys)
22
22
  type_check_and_coerce_keys!(raw_keys)
23
23
 
24
- keys_are_all_present = are_all_keys_in_context?(keys)
24
+ return false if are_all_keys_valid?
25
25
 
26
- return false if are_all_keys_valid? && keys_are_all_present
27
-
28
- true
26
+ should_throw_on_validation_error?
29
27
  end
30
28
  end
31
29
  end
@@ -1,7 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ValidatedContext
4
- ValidatedKey = Struct.new(:label, :type) do
4
+ class ValidatedKey
5
+ attr_reader :label, :type, :message
6
+
7
+ def initialize(label, type, message: nil)
8
+ @label = label
9
+ @type = type
10
+ @message = message
11
+ end
12
+
5
13
  def to_sym
6
14
  label.to_sym
7
15
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module LightService
4
4
  module ValidatedContext
5
- VERSION = "0.2.2"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
@@ -12,6 +12,7 @@ module ValidatedContext; end
12
12
  module LightService
13
13
  class Context
14
14
  ValidatedKey = ::ValidatedContext::ValidatedKey
15
+ FailOnValidationError = ::ValidatedContext::FailOnValidationError
15
16
  prepend ::ValidatedContext::Context
16
17
 
17
18
  class KeyVerifier
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: light_service-validated_context
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - "'Alessandro Fazzi'"
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-11-30 00:00:00.000000000 Z
11
+ date: 2022-12-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-types
@@ -69,6 +69,7 @@ files:
69
69
  - lib/light_service/validated_context.rb
70
70
  - lib/light_service/validated_context/context.rb
71
71
  - lib/light_service/validated_context/expected_key_verifier.rb
72
+ - lib/light_service/validated_context/fail_on_validation_error.rb
72
73
  - lib/light_service/validated_context/key_verifier.rb
73
74
  - lib/light_service/validated_context/promised_key_verifier.rb
74
75
  - lib/light_service/validated_context/validated_key.rb