light_service-validated_context 0.2.2 → 0.3.1

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: 859cf8786c692bedc1d764b0656629bc7f9e0ddb741a10874d290c723b708e3d
4
+ data.tar.gz: 18059f28fcbaaa2d54373bdf2cea6c6f8387fc68b07f83f338631e445ba29e5f
5
5
  SHA512:
6
- metadata.gz: 483f3c71553b51875b1e10d45c113f42ef3d93102658e7df5499bb912fffb810b9555fade2506eaf6e0c1bc4573b06f8b08b179ee1f2d6a336899f8e519d5aad
7
- data.tar.gz: c0b8e60f94ad3dbeda5d3e10377633fb6dc248543176fda3d5e4353034e76c8b48804c855d7f9c0d778fe0a7180fa313ae313624fc8a26c1cc4257529cf67ce9
6
+ metadata.gz: 7d7a6568947199bf4e96296d03825a65b7485227083d02b72b450598db273e72bae5547b19dd9d32d5ca9a85e9b5e676cc5af4f257f974ec4d4d9e59a8c3ed7f
7
+ data.tar.gz: eb44c15324105179fb1426fc201d2f34d8bb92e5ccc5e1ed2a265e8c8d85eaf66625b287717c6ec452a8789afa240b69b395fa3050c0a1661ed2026a0fb6e5d0
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.1)
5
5
  dry-types (~> 1.7.0)
6
6
  light-service (>= 0.18.0)
7
7
  zeitwerk
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
 
@@ -47,35 +47,66 @@ 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
+ ```
63
91
 
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)
92
+ and you'll get validations for free
93
+
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
73
- read more about what you can achieve at https://dry-rb.org/gems/dry-types/1.2/
104
+ read more about what you can achieve at https://dry-rb.org/gems/dry-types/main/custom-types/
74
105
 
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,91 @@ form to avoid name collisions.
83
114
 
84
115
  You can find more usage example in `spec/support/test_doubles.rb`
85
116
 
86
- ## Why validation matters?
117
+ ### Custom validation error message
87
118
 
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.
119
+ You can set a custom validation error message when instantiating a `VK` object
92
120
 
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.
121
+ ```ruby
122
+ VK.new(:my_integer, Types::Strict::Integer, message: 'Custom validation message for :my_integer key')
123
+ ```
96
124
 
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.:
125
+ Messages translated via `I18n` are supported too, following standard `light-service`'s
126
+ [configuration](https://github.com/adomokos/light-service/#localizing-messages)
127
+
128
+ ```ruby
129
+ VK.new(:my_integer, Types::Strict::Integer, message: :my_integer_error_message)
130
+ ```
131
+
132
+ ### Raise vs fail
133
+
134
+ By default, following original `light-service` implementation, a validation error will raise a
135
+ `LightService::ExpectedKeysNotInContextError` or `LightService::PromisedKeysNotInContextError`.
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:
143
+
144
+ ```ruby
145
+ class ActionFailInsteadOfRaise
146
+ extend LightService::Action
147
+ extend LightService::Context::FailOnValidationError
148
+
149
+ expects VK.new(:foo, Types::String)
150
+
151
+ executed do |context|
152
+ # do something
153
+ end
154
+ end
155
+
156
+ result = ActionFailInsteadOfRaise.execute(foo: 12)
157
+ result.message # Here you'll find the validation(s) message(s)
158
+ ```
159
+
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
178
+
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?
196
+
197
+ In OO programming there's a rule that says to never instantiate an
198
+ invalid object.
199
+
200
+ If you cannot trust the state, given the state is internal or delegated to a context object,
201
+ you'll have to do a bunch of validation-oriented logical branches into your logic. E.g.:
100
202
 
101
203
  ```ruby
102
204
  class HugAFriend
@@ -110,7 +212,7 @@ class HugAFriend
110
212
  end
111
213
  ```
112
214
 
113
- The `if` in this uber-trivial example exists just due to lack of trust on the state.
215
+ The `if` in this uber-trivial example exists just due to untrusted state.
114
216
 
115
217
  Let's re-imagine the code given an `executed` block that totally trusts the context:
116
218
 
@@ -122,6 +224,7 @@ class HugAFriend
122
224
  expects VK.new(:friend, Types.Instance(Friend))
123
225
  # Or a less usual approach could be to trust duck typing
124
226
  # expects VK.new(:friend, Types::Interface(:hug))
227
+ # Actually not all friends do appreciate hugs nor other forms of physical contact :P
125
228
 
126
229
  executed do |context|
127
230
  context.friend.hug
@@ -131,6 +234,8 @@ end
131
234
 
132
235
  ## Comparison with similar gems
133
236
 
237
+ A brief comparison about what similar gems offer to work with validation.
238
+
134
239
  This is a comparison table I've done using my own limited experience w/ other solutions
135
240
  and/or reading projects' READMEs. Don't take my word for it. And if I was wrong understanding
136
241
  some features, feel free to drop me a line on Mastodon [@alessandrofazzi@mastodon.uno](https://mastodon.uno/@alessandrofazzi)
@@ -140,12 +245,15 @@ some features, feel free to drop me a line on Mastodon [@alessandrofazzi@mastodo
140
245
  | presence | ✅ | ✅ | ❌ | ⚠️ Only input, not output | ✅ |
141
246
  | static default | ✅ | ✅ | ❌ | ✅ | ✅ |
142
247
  | dynamic default | ✅ | ✅ | ❌ | ✅ | ✅ |
143
- | raise or fail control | ❌ | ✅ | ❌ | ❓ | |
248
+ | raise or fail control | ❌ | ✅ | ❌ | ❓ | |
144
249
  | type check | ❌ | ✅ | ❌ | ✅ | ✅ |
145
250
  | data structure type check | ❌ | ❌ | ❌ | ❌ | ✅ |
146
251
  | optional | ⚠️ through `default` | ✅ through `allow_nil` (which defaults to `true` 🤔 ❓) | ❌ | ⚠️ through `default` | ✅ |
147
- | built-in | ✅ | ✅ | | ActiveModel::Validation | ❌ Dry::Types |
252
+ | 1st party code | ✅ | ✅ | | ⚠️ ActiveModel::Validation | ❌ Dry::Types |
148
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
149
257
 
150
258
  ## Development
151
259
 
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ValidatedContext
4
- module Context
5
- def define_accessor_methods_for_keys(keys)
6
- super keys.map(&:to_sym)
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,31 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ValidatedContext
4
- module ExpectedKeyVerifier
5
- def keys
6
- keys_as_symbols
7
- end
3
+ module LightService
4
+ module ValidatedContext
5
+ module ExpectedKeyVerifier
6
+ def keys
7
+ keys_as_symbols
8
+ end
8
9
 
9
- def keys_as_symbols
10
- action.expected_keys.map do |key|
11
- next key unless key.is_a?(LightService::Context::ValidatedKey)
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
- key.to_sym
14
+ key.to_sym
15
+ end
14
16
  end
15
- end
16
17
 
17
- def raw_keys
18
- action.expected_keys
19
- end
20
-
21
- def throw_error_predicate(keys)
22
- type_check_and_coerce_keys!(raw_keys)
18
+ def raw_keys
19
+ action.expected_keys
20
+ end
23
21
 
24
- keys_are_all_present = are_all_keys_in_context?(keys)
22
+ def throw_error_predicate(_keys)
23
+ type_check_and_coerce_keys!(raw_keys)
25
24
 
26
- return false if are_all_keys_valid? && keys_are_all_present
25
+ return false if are_all_keys_valid?
27
26
 
28
- true
27
+ should_throw_on_validation_error?
28
+ end
29
29
  end
30
30
  end
31
31
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
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
9
+ end
10
+ end
@@ -1,29 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ValidatedContext
4
- module KeyVerifier
5
- def type_check_and_coerce_keys!(keys)
6
- errors = []
7
-
8
- keys.each do |key|
9
- next unless key.is_a?(LightService::Context::ValidatedKey)
10
-
11
- begin
12
- context[key.label] = key.type[context[key.label] || Dry::Types::Undefined]
13
- rescue Dry::Types::CoercionError => e
14
- errors << "[#{action}][:#{key.label}] #{e.message}"
3
+ module LightService
4
+ module ValidatedContext
5
+ module KeyVerifier
6
+ def initialize(context, action)
7
+ @validation_errors = []
8
+
9
+ super(context, action)
10
+ end
11
+
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
15
28
  end
29
+
30
+ @validation_errors = errors
16
31
  end
17
- # debugger
18
- @validation_errors = errors
19
- end
32
+ # rubocop:enable Metrics/AbcSize
20
33
 
21
- def are_all_keys_valid?
22
- @validation_errors.none?
23
- end
34
+ def are_all_keys_valid?
35
+ @validation_errors.none?
36
+ end
37
+
38
+ def error_message
39
+ @validation_errors.join(', ')
40
+ end
41
+
42
+ def should_throw_on_validation_error?
43
+ return true unless action.respond_to?(:fail_on_validation_error?) && action.fail_on_validation_error?
24
44
 
25
- def error_message
26
- @validation_errors.join(', ')
45
+ context.fail!(error_message)
46
+ false
47
+ end
27
48
  end
28
49
  end
29
50
  end
@@ -1,31 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ValidatedContext
4
- module PromisedKeyVerifier
5
- def keys
6
- keys_as_symbols
7
- end
3
+ module LightService
4
+ module ValidatedContext
5
+ module PromisedKeyVerifier
6
+ def keys
7
+ keys_as_symbols
8
+ end
8
9
 
9
- def keys_as_symbols
10
- raw_keys.map do |key|
11
- next key unless key.is_a?(LightService::Context::ValidatedKey)
10
+ def keys_as_symbols
11
+ raw_keys.map do |key|
12
+ next key unless key.is_a?(LightService::Context::ValidatedKey)
12
13
 
13
- key.to_sym
14
+ key.to_sym
15
+ end
14
16
  end
15
- end
16
17
 
17
- def raw_keys
18
- action.promised_keys
19
- end
20
-
21
- def throw_error_predicate(keys)
22
- type_check_and_coerce_keys!(raw_keys)
18
+ def raw_keys
19
+ action.promised_keys
20
+ end
23
21
 
24
- keys_are_all_present = are_all_keys_in_context?(keys)
22
+ def throw_error_predicate(_keys)
23
+ type_check_and_coerce_keys!(raw_keys)
25
24
 
26
- return false if are_all_keys_valid? && keys_are_all_present
25
+ return false if are_all_keys_valid?
27
26
 
28
- true
27
+ should_throw_on_validation_error?
28
+ end
29
29
  end
30
30
  end
31
31
  end
@@ -1,13 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ValidatedContext
4
- ValidatedKey = Struct.new(:label, :type) do
5
- def to_sym
6
- label.to_sym
7
- end
3
+ module LightService
4
+ module ValidatedContext
5
+ class ValidatedKey
6
+ attr_reader :label, :type, :message
7
+
8
+ def initialize(label, type, message: nil)
9
+ @label = label
10
+ @type = type
11
+ @message = message
12
+ end
13
+
14
+ def to_sym
15
+ label.to_sym
16
+ end
8
17
 
9
- def to_s
10
- label.to_s
18
+ def to_s
19
+ label.to_s
20
+ end
11
21
  end
12
22
  end
13
23
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module LightService
4
4
  module ValidatedContext
5
- VERSION = "0.2.2"
5
+ VERSION = "0.3.1"
6
6
  end
7
7
  end
@@ -1,29 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "zeitwerk"
4
- loader = Zeitwerk::Loader.for_gem
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 ValidatedContext; end
13
+ module LightService
14
+ module ValidatedContext; end
15
+ end
11
16
 
12
17
  module LightService
13
18
  class Context
14
- ValidatedKey = ::ValidatedContext::ValidatedKey
15
- prepend ::ValidatedContext::Context
19
+ ValidatedKey = LightService::ValidatedContext::ValidatedKey
20
+ FailOnValidationError = LightService::ValidatedContext::FailOnValidationError
21
+ prepend LightService::ValidatedContext::Context
16
22
 
17
23
  class KeyVerifier
18
- prepend ::ValidatedContext::KeyVerifier
24
+ prepend LightService::ValidatedContext::KeyVerifier
19
25
  end
20
26
 
21
27
  class ExpectedKeyVerifier
22
- prepend ::ValidatedContext::ExpectedKeyVerifier
28
+ prepend LightService::ValidatedContext::ExpectedKeyVerifier
23
29
  end
24
30
 
25
31
  class PromisedKeyVerifier
26
- prepend ::ValidatedContext::PromisedKeyVerifier
32
+ prepend LightService::ValidatedContext::PromisedKeyVerifier
27
33
  end
28
34
  end
29
35
 
@@ -32,7 +38,7 @@ module LightService
32
38
  end
33
39
  end
34
40
 
35
- # Convenience namespace for implementor
41
+ # Convenience alias for implementor
36
42
  Types = LightService::Types unless Module.const_defined?('Types')
37
43
 
38
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.2.2
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-11-30 00:00:00.000000000 Z
11
+ date: 2022-12-08 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