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