light_service-validated_context 0.3.0 → 0.3.1
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 +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +55 -14
- data/lib/light_service/validated_context/context.rb +6 -4
- data/lib/light_service/validated_context/expected_key_verifier.rb +19 -17
- data/lib/light_service/validated_context/fail_on_validation_error.rb +6 -4
- data/lib/light_service/validated_context/key_verifier.rb +37 -35
- data/lib/light_service/validated_context/promised_key_verifier.rb +19 -17
- data/lib/light_service/validated_context/validated_key.rb +15 -13
- data/lib/light_service/validated_context/version.rb +1 -1
- data/lib/light_service/validated_context.rb +14 -9
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 859cf8786c692bedc1d764b0656629bc7f9e0ddb741a10874d290c723b708e3d
|
4
|
+
data.tar.gz: 18059f28fcbaaa2d54373bdf2cea6c6f8387fc68b07f83f338631e445ba29e5f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7d7a6568947199bf4e96296d03825a65b7485227083d02b72b450598db273e72bae5547b19dd9d32d5ca9a85e9b5e676cc5af4f257f974ec4d4d9e59a8c3ed7f
|
7
|
+
data.tar.gz: eb44c15324105179fb1426fc201d2f34d8bb92e5ccc5e1ed2a265e8c8d85eaf66625b287717c6ec452a8789afa240b69b395fa3050c0a1661ed2026a0fb6e5d0
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -23,7 +23,7 @@ AFAIK this is the only way to achieve the goal. Because of this fact I consider
|
|
23
23
|
## Goals
|
24
24
|
|
25
25
|
- implement an advanced and flexible interface to declare,
|
26
|
-
type-check, coerce and describe action's arguments without reinventing the wheel (the wheel we use under the wood is [`dry-types`](https://dry-rb.org/gems/dry-types))
|
26
|
+
type-check, coerce and describe action's arguments without reinventing the wheel (the wheel we use under the wood is [`dry-types`](https://dry-rb.org/gems/dry-types/main/custom-types/))
|
27
27
|
- testing DX and interfaces
|
28
28
|
- study what parts of code are involved into this area of `light-service`'s code base
|
29
29
|
|
@@ -101,7 +101,7 @@ ActionOne.execute(age: 37, text: 'Too long too pass the constrain')
|
|
101
101
|
```
|
102
102
|
|
103
103
|
Since all the validation and coercion logic is delegated to `dry-types`, you can
|
104
|
-
read more about what you can achieve at https://dry-rb.org/gems/dry-types/
|
104
|
+
read more about what you can achieve at https://dry-rb.org/gems/dry-types/main/custom-types/
|
105
105
|
|
106
106
|
`VK` objects needs to be created with 2 positional arguments:
|
107
107
|
|
@@ -116,13 +116,14 @@ You can find more usage example in `spec/support/test_doubles.rb`
|
|
116
116
|
|
117
117
|
### Custom validation error message
|
118
118
|
|
119
|
-
You can set a custom validation error message when instantiating a `VK`
|
119
|
+
You can set a custom validation error message when instantiating a `VK` object
|
120
120
|
|
121
121
|
```ruby
|
122
|
-
VK.new(:my_integer, Types::Strict::Integer, message: 'Custom
|
122
|
+
VK.new(:my_integer, Types::Strict::Integer, message: 'Custom validation message for :my_integer key')
|
123
123
|
```
|
124
124
|
|
125
|
-
Messages translated via `I18n` are supported too, following
|
125
|
+
Messages translated via `I18n` are supported too, following standard `light-service`'s
|
126
|
+
[configuration](https://github.com/adomokos/light-service/#localizing-messages)
|
126
127
|
|
127
128
|
```ruby
|
128
129
|
VK.new(:my_integer, Types::Strict::Integer, message: :my_integer_error_message)
|
@@ -130,10 +131,15 @@ VK.new(:my_integer, Types::Strict::Integer, message: :my_integer_error_message)
|
|
130
131
|
|
131
132
|
### Raise vs fail
|
132
133
|
|
133
|
-
By default, following original `light-service` implementation, a validation error will raise
|
134
|
-
|
134
|
+
By default, following original `light-service` implementation, a validation error will raise a
|
135
|
+
`LightService::ExpectedKeysNotInContextError` or `LightService::PromisedKeysNotInContextError`.
|
135
136
|
|
136
|
-
|
137
|
+
> NOTE: I know that raised exceptions do not express the concept of "invalid", but I opted
|
138
|
+
to preserve the original one in order to make this plugin more droppable-in as possible, thus
|
139
|
+
w/o breaking code relying on, for example, rescueing those specific excpetions.
|
140
|
+
|
141
|
+
May you prefere to fail the action, populating outcome's message with error message, just do
|
142
|
+
`extend LightService::Context::FailOnValidationError` into you action:
|
137
143
|
|
138
144
|
```ruby
|
139
145
|
class ActionFailInsteadOfRaise
|
@@ -151,13 +157,45 @@ result = ActionFailInsteadOfRaise.execute(foo: 12)
|
|
151
157
|
result.message # Here you'll find the validation(s) message(s)
|
152
158
|
```
|
153
159
|
|
154
|
-
|
160
|
+
### Custom types
|
161
|
+
|
162
|
+
As documented in [dry-types doc](https://dry-rb.org/gems/dry-types/main/getting-started/#creating-your-first-type),
|
163
|
+
you can be more expressive defining custom types; you can define them reopening the already defined `LightService::Types` module
|
164
|
+
(or simply `Types` in the global namespace if it does not conflict with your domain's namespace), e.g.:
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
module LightService::Types
|
168
|
+
MyExpressiveThing = Hash.schema(
|
169
|
+
name: String,
|
170
|
+
age: Coercible::Integer,
|
171
|
+
foo: Symbol.constrained(included_in: %i[bar baz])
|
172
|
+
)
|
173
|
+
end
|
174
|
+
|
175
|
+
class ActionOne
|
176
|
+
extend LightService::Action
|
177
|
+
extend LightService::Context::FailOnValidationError
|
155
178
|
|
156
|
-
|
157
|
-
|
179
|
+
expects VK.new(:foo, Types::MyBusinessHash)
|
180
|
+
|
181
|
+
executed do |context|
|
182
|
+
# do something...
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
result = App::ActionOne.execute(foo: {
|
187
|
+
name: 'Alessandro',
|
188
|
+
age: '37',
|
189
|
+
foo: :bar
|
190
|
+
})
|
191
|
+
```
|
192
|
+
|
193
|
+
Custom types will be reusable, more expressive and moreover will clean your action up a bit.
|
194
|
+
|
195
|
+
## Why validation matters?
|
158
196
|
|
159
|
-
|
160
|
-
|
197
|
+
In OO programming there's a rule that says to never instantiate an
|
198
|
+
invalid object.
|
161
199
|
|
162
200
|
If you cannot trust the state, given the state is internal or delegated to a context object,
|
163
201
|
you'll have to do a bunch of validation-oriented logical branches into your logic. E.g.:
|
@@ -211,8 +249,11 @@ some features, feel free to drop me a line on Mastodon [@alessandrofazzi@mastodo
|
|
211
249
|
| type check | ❌ | ✅ | ❌ | ✅ | ✅ |
|
212
250
|
| data structure type check | ❌ | ❌ | ❌ | ❌ | ✅ |
|
213
251
|
| optional | ⚠️ through `default` | ✅ through `allow_nil` (which defaults to `true` 🤔 ❓) | ❌ | ⚠️ through `default` | ✅ |
|
214
|
-
| 1st party code | ✅ | ✅ | ✅ |
|
252
|
+
| 1st party code | ✅ | ✅ | ✅ | ⚠️ ActiveModel::Validation | ❌ Dry::Types |
|
215
253
|
|
254
|
+
> NOTE: in `active_interaction` the fact that validation code isn't first party isn't an issue, since
|
255
|
+
> the gem is a Rails-only gem and validation is delegated to Rails, thus no additional dependencies
|
256
|
+
> are required. `light_service-validated_context` depends on additional gems from the dry-rb ecosystem
|
216
257
|
|
217
258
|
## Development
|
218
259
|
|
@@ -1,9 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module
|
4
|
-
module
|
5
|
-
|
6
|
-
|
3
|
+
module LightService
|
4
|
+
module ValidatedContext
|
5
|
+
module Context
|
6
|
+
def define_accessor_methods_for_keys(keys)
|
7
|
+
super keys.map(&:to_sym)
|
8
|
+
end
|
7
9
|
end
|
8
10
|
end
|
9
11
|
end
|
@@ -1,29 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module
|
4
|
-
module
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
module LightService
|
4
|
+
module ValidatedContext
|
5
|
+
module ExpectedKeyVerifier
|
6
|
+
def keys
|
7
|
+
keys_as_symbols
|
8
|
+
end
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
10
|
+
def keys_as_symbols
|
11
|
+
action.expected_keys.map do |key|
|
12
|
+
next key unless key.is_a?(LightService::Context::ValidatedKey)
|
12
13
|
|
13
|
-
|
14
|
+
key.to_sym
|
15
|
+
end
|
14
16
|
end
|
15
|
-
end
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
18
|
+
def raw_keys
|
19
|
+
action.expected_keys
|
20
|
+
end
|
20
21
|
|
21
|
-
|
22
|
-
|
22
|
+
def throw_error_predicate(_keys)
|
23
|
+
type_check_and_coerce_keys!(raw_keys)
|
23
24
|
|
24
|
-
|
25
|
+
return false if are_all_keys_valid?
|
25
26
|
|
26
|
-
|
27
|
+
should_throw_on_validation_error?
|
28
|
+
end
|
27
29
|
end
|
28
30
|
end
|
29
31
|
end
|
@@ -1,8 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module
|
4
|
-
module
|
5
|
-
|
6
|
-
|
3
|
+
module LightService
|
4
|
+
module ValidatedContext
|
5
|
+
module FailOnValidationError
|
6
|
+
def fail_on_validation_error? = true
|
7
|
+
def throw_on_validation_error? = !fail_on_validation_error?
|
8
|
+
end
|
7
9
|
end
|
8
10
|
end
|
@@ -1,48 +1,50 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module
|
4
|
-
module
|
5
|
-
|
6
|
-
|
3
|
+
module LightService
|
4
|
+
module ValidatedContext
|
5
|
+
module KeyVerifier
|
6
|
+
def initialize(context, action)
|
7
|
+
@validation_errors = []
|
7
8
|
|
8
|
-
|
9
|
-
|
9
|
+
super(context, action)
|
10
|
+
end
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
12
|
+
# rubocop:disable Metrics/AbcSize
|
13
|
+
# Refactoring this is out of my scope ATM
|
14
|
+
def type_check_and_coerce_keys!(keys)
|
15
|
+
errors = []
|
16
|
+
|
17
|
+
keys.each do |key|
|
18
|
+
next unless key.is_a?(LightService::Context::ValidatedKey)
|
19
|
+
|
20
|
+
begin
|
21
|
+
context[key.label] = key.type[context[key.label] || Dry::Types::Undefined]
|
22
|
+
rescue Dry::Types::CoercionError => e
|
23
|
+
errors << (
|
24
|
+
LightService::Configuration.localization_adapter.failure(key.message, action) ||
|
25
|
+
"[#{action}][:#{key.label}] #{e.message}"
|
26
|
+
)
|
27
|
+
end
|
26
28
|
end
|
27
|
-
end
|
28
29
|
|
29
|
-
|
30
|
-
|
31
|
-
|
30
|
+
@validation_errors = errors
|
31
|
+
end
|
32
|
+
# rubocop:enable Metrics/AbcSize
|
32
33
|
|
33
|
-
|
34
|
-
|
35
|
-
|
34
|
+
def are_all_keys_valid?
|
35
|
+
@validation_errors.none?
|
36
|
+
end
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
38
|
+
def error_message
|
39
|
+
@validation_errors.join(', ')
|
40
|
+
end
|
40
41
|
|
41
|
-
|
42
|
-
|
42
|
+
def should_throw_on_validation_error?
|
43
|
+
return true unless action.respond_to?(:fail_on_validation_error?) && action.fail_on_validation_error?
|
43
44
|
|
44
|
-
|
45
|
-
|
45
|
+
context.fail!(error_message)
|
46
|
+
false
|
47
|
+
end
|
46
48
|
end
|
47
49
|
end
|
48
50
|
end
|
@@ -1,29 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module
|
4
|
-
module
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
module LightService
|
4
|
+
module ValidatedContext
|
5
|
+
module PromisedKeyVerifier
|
6
|
+
def keys
|
7
|
+
keys_as_symbols
|
8
|
+
end
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
10
|
+
def keys_as_symbols
|
11
|
+
raw_keys.map do |key|
|
12
|
+
next key unless key.is_a?(LightService::Context::ValidatedKey)
|
12
13
|
|
13
|
-
|
14
|
+
key.to_sym
|
15
|
+
end
|
14
16
|
end
|
15
|
-
end
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
18
|
+
def raw_keys
|
19
|
+
action.promised_keys
|
20
|
+
end
|
20
21
|
|
21
|
-
|
22
|
-
|
22
|
+
def throw_error_predicate(_keys)
|
23
|
+
type_check_and_coerce_keys!(raw_keys)
|
23
24
|
|
24
|
-
|
25
|
+
return false if are_all_keys_valid?
|
25
26
|
|
26
|
-
|
27
|
+
should_throw_on_validation_error?
|
28
|
+
end
|
27
29
|
end
|
28
30
|
end
|
29
31
|
end
|
@@ -1,21 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module
|
4
|
-
|
5
|
-
|
3
|
+
module LightService
|
4
|
+
module ValidatedContext
|
5
|
+
class ValidatedKey
|
6
|
+
attr_reader :label, :type, :message
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
def initialize(label, type, message: nil)
|
9
|
+
@label = label
|
10
|
+
@type = type
|
11
|
+
@message = message
|
12
|
+
end
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
def to_sym
|
15
|
+
label.to_sym
|
16
|
+
end
|
16
17
|
|
17
|
-
|
18
|
-
|
18
|
+
def to_s
|
19
|
+
label.to_s
|
20
|
+
end
|
19
21
|
end
|
20
22
|
end
|
21
23
|
end
|
@@ -1,30 +1,35 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "zeitwerk"
|
4
|
-
loader = Zeitwerk::Loader.
|
4
|
+
loader = Zeitwerk::Loader.new
|
5
|
+
loader.tag = 'LightService::I18n'
|
6
|
+
loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
|
7
|
+
loader.push_dir(__dir__, :namespace => LightService)
|
5
8
|
loader.setup
|
6
9
|
|
7
10
|
require 'light-service'
|
8
11
|
require 'dry-types'
|
9
12
|
|
10
|
-
module
|
13
|
+
module LightService
|
14
|
+
module ValidatedContext; end
|
15
|
+
end
|
11
16
|
|
12
17
|
module LightService
|
13
18
|
class Context
|
14
|
-
ValidatedKey = ::ValidatedContext::ValidatedKey
|
15
|
-
FailOnValidationError = ::ValidatedContext::FailOnValidationError
|
16
|
-
prepend ::ValidatedContext::Context
|
19
|
+
ValidatedKey = LightService::ValidatedContext::ValidatedKey
|
20
|
+
FailOnValidationError = LightService::ValidatedContext::FailOnValidationError
|
21
|
+
prepend LightService::ValidatedContext::Context
|
17
22
|
|
18
23
|
class KeyVerifier
|
19
|
-
prepend ::ValidatedContext::KeyVerifier
|
24
|
+
prepend LightService::ValidatedContext::KeyVerifier
|
20
25
|
end
|
21
26
|
|
22
27
|
class ExpectedKeyVerifier
|
23
|
-
prepend ::ValidatedContext::ExpectedKeyVerifier
|
28
|
+
prepend LightService::ValidatedContext::ExpectedKeyVerifier
|
24
29
|
end
|
25
30
|
|
26
31
|
class PromisedKeyVerifier
|
27
|
-
prepend ::ValidatedContext::PromisedKeyVerifier
|
32
|
+
prepend LightService::ValidatedContext::PromisedKeyVerifier
|
28
33
|
end
|
29
34
|
end
|
30
35
|
|
@@ -33,7 +38,7 @@ module LightService
|
|
33
38
|
end
|
34
39
|
end
|
35
40
|
|
36
|
-
# Convenience
|
41
|
+
# Convenience alias for implementor
|
37
42
|
Types = LightService::Types unless Module.const_defined?('Types')
|
38
43
|
|
39
44
|
# Convenience alias for implementor
|
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.3.
|
4
|
+
version: 0.3.1
|
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-12-
|
11
|
+
date: 2022-12-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-types
|