light_service-validated_context 0.2.2 → 0.3.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.
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