service_actor 3.9.3 → 4.0.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: e0f34532a18ec537f1bfed394344d917b7692ae8b7c8711cf91e6f840c501cd9
4
- data.tar.gz: e8b42b94b3f20c6fa671d9e3e36686f36c1961d505046c11df025648e1cd04c9
3
+ metadata.gz: 86aa1f5411838fa24961e50eff4138d414c86aa99c7bdc5907953f7643406431
4
+ data.tar.gz: 1e0c82c0ba0e3b3c8278fa7d286454f891266972d34144b2ee3aa18a2bddd20d
5
5
  SHA512:
6
- metadata.gz: 39f81ccfa9ad2e5e6b493e7654b3a979cea9935ab11a8c333d8272e160df2e6463e14e5ccf5b6637b993feb4ff8b5bdcf262ddb8da3be9ca6ac7b712f4f92014
7
- data.tar.gz: 12c34d599551e6260fbb84a4ebb9930dddc2c46d24e77eace106c652456c145256543000de36c613801acb3f8f3f6db44ab070b457db015fbd738a304cba649d
6
+ metadata.gz: ed8bfce6061647dee123bfdecc2a89f92cb7b4fc09664bef3671d722e59605787acfe3ceb9822b785a4a0ed651b315a9c27a38366516cbeadef9403d92c49709
7
+ data.tar.gz: 9dec2bb8b9d5815c1c8d39108432003575f2f5a9a9431c93c69228c2c13f776eea60794696328e2d9ebc6e13849e2fa3e6db3e6e3ac337c03a39d29b24c578ea
data/README.md CHANGED
@@ -314,20 +314,20 @@ end
314
314
 
315
315
  ### Defaults
316
316
 
317
- Inputs can be optional by providing a `default` value or lambda.
317
+ Inputs can be optional by providing a `default` value in a lambda.
318
318
 
319
319
  ```rb
320
320
  class BuildGreeting < Actor
321
321
  input :name
322
- input :adjective, default: "wonderful"
322
+ input :adjective, default: -> { "wonderful" }
323
323
  input :length_of_time, default: -> { ["day", "week", "month"].sample }
324
324
  input :article,
325
- default: -> context { context.adjective.match?(/^aeiou/) ? "an" : "a" }
325
+ default: -> actor { actor.adjective.match?(/^[aeiou]/) ? "an" : "a" }
326
326
 
327
327
  output :greeting
328
328
 
329
329
  def call
330
- self.greeting = "Have #{article} #{length_of_time}, #{name}!"
330
+ self.greeting = "Have #{article} #{adjective} #{length_of_time}, #{name}!"
331
331
  end
332
332
  end
333
333
 
@@ -338,6 +338,33 @@ actor = BuildGreeting.call(name: "Siobhan", adjective: "elegant")
338
338
  actor.greeting # => "Have an elegant week, Siobhan!"
339
339
  ```
340
340
 
341
+ While lambdas are the preferred way to specify defaults, you can also provide
342
+ a default value without using lambdas by using an immutable object.
343
+
344
+ ```rb
345
+ # frozen_string_literal: true
346
+
347
+ class ExampleActor < Actor
348
+ input :options, default: {
349
+ names: {male: "Iaroslav", female: "Anna"}.freeze,
350
+ country_codes: %w[gb ru].freeze
351
+ }.freeze
352
+ end
353
+ ```
354
+
355
+ Note that default values might be mutated if the values returned by the lambda
356
+ are references to mutable objects, e.g.
357
+
358
+ ```rb
359
+ class ExampleActor < Actor
360
+ input :options, default: -> { Registry::DEFAULT_OPTIONS } # `Registry::DEFAULT_OPTIONS` is not frozen
361
+
362
+ def call
363
+ options[:names] = nil
364
+ end
365
+ end
366
+ ```
367
+
341
368
  ### Allow nil
342
369
 
343
370
  By default inputs accept `nil` values. To raise an error instead:
@@ -471,20 +498,6 @@ end
471
498
  end
472
499
  ```
473
500
 
474
- #### Default
475
-
476
- ```ruby
477
- class MultiplyThing < Actor
478
- input :multiplier,
479
- default: {
480
- is: -> { rand(1..10) },
481
- message: (lambda do |input_key:, **|
482
- "Input \"#{input_key}\" is required"
483
- end)
484
- }
485
- end
486
- ```
487
-
488
501
  #### Type
489
502
 
490
503
  ```ruby
@@ -16,4 +16,13 @@ module ServiceActor::ArgumentsValidator
16
16
 
17
17
  raise ArgumentError, "Expected #{value} to be a subclass of Exception"
18
18
  end
19
+
20
+ def validate_default_value(value, origin_type:, origin_name:, actor:)
21
+ return if value.is_a?(Proc) || !defined?(Ractor.shareable?) || Ractor.shareable?(value)
22
+
23
+ ::Kernel.warn(
24
+ "DEPRECATED: Actor `#{actor}` has #{origin_type} `#{origin_name}` with default " \
25
+ "which is not a Proc or an immutable object.",
26
+ )
27
+ end
19
28
  end
@@ -26,6 +26,10 @@ module ServiceActor::Attributable
26
26
  name, origin: :input
27
27
  )
28
28
 
29
+ ServiceActor::ArgumentsValidator.validate_default_value(
30
+ arguments[:default], actor: self, origin_type: :input, origin_name: name
31
+ )
32
+
29
33
  inputs[name] = arguments
30
34
 
31
35
  define_method(name) do
@@ -47,6 +51,10 @@ module ServiceActor::Attributable
47
51
  name, origin: :output
48
52
  )
49
53
 
54
+ ServiceActor::ArgumentsValidator.validate_default_value(
55
+ arguments[:default], actor: self, origin_type: :output, origin_name: name
56
+ )
57
+
50
58
  outputs[name] = arguments
51
59
 
52
60
  define_method(name) do
@@ -31,10 +31,9 @@ module ServiceActor::Checkable
31
31
 
32
32
  # rubocop:disable Metrics/MethodLength
33
33
  def service_actor_checks_for(origin)
34
- check_classes = CHECK_CLASSES.select { _1.applicable_to_origin?(origin) }
35
34
  self.class.public_send(:"#{origin}s").each do |input_key, input_options|
36
35
  input_options.each do |check_name, check_conditions|
37
- check_classes.each do |check_class|
36
+ CHECK_CLASSES.each do |check_class|
38
37
  argument_errors = check_class.check(
39
38
  check_name: check_name,
40
39
  origin: origin,
@@ -1,12 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ServiceActor::Checks::Base
4
- class << self
5
- def applicable_to_origin?(_origin)
6
- true
7
- end
8
- end
9
-
10
4
  def initialize
11
5
  @argument_errors = []
12
6
  end
@@ -72,14 +72,14 @@ class ServiceActor::Checks::TypeCheck < ServiceActor::Checks::Base
72
72
 
73
73
  types, message = define_types_and_message
74
74
 
75
- return if types.any? { |type| @given_type.is_a?(type) }
75
+ return if types.any? { |type| type === @given_type }
76
76
 
77
77
  add_argument_error(
78
78
  message,
79
79
  origin: @origin,
80
80
  input_key: @input_key,
81
81
  actor: @actor,
82
- expected_type: types.join(", "),
82
+ expected_type: types.map(&:name).join(", "),
83
83
  given_type: @given_type.class,
84
84
  )
85
85
  end
@@ -9,22 +9,6 @@
9
9
  # input :counter, default: 1
10
10
  # input :multiplier, default: -> { rand(1..10) }
11
11
  # end
12
- #
13
- # class MultiplyThing < Actor
14
- # input :counter,
15
- # default: {
16
- # is: 1,
17
- # message: "Counter is required"
18
- # }
19
- #
20
- # input :multiplier,
21
- # default: {
22
- # is: -> { rand(1..10) },
23
- # message: (lambda do |input_key:, actor:|
24
- # "Input \"#{input_key}\" is required"
25
- # end)
26
- # }
27
- # end
28
12
  module ServiceActor::Defaultable
29
13
  class << self
30
14
  def included(base)
@@ -43,19 +27,13 @@ module ServiceActor::Defaultable
43
27
  )
44
28
  end
45
29
 
46
- default = input[:default]
47
-
48
- if default.is_a?(Hash) && default[:is]
49
- default_for_advanced_mode_with(result, key, default)
50
- else
51
- default_for_normal_mode_with(result, key, default)
52
- end
30
+ apply_default_for_origin(key, input)
53
31
  end
54
32
 
55
- self.class.outputs.each do |key, options|
56
- unless result.key?(key)
57
- result.send(:"#{key}=", options[:default])
58
- end
33
+ self.class.outputs.each do |key, output|
34
+ next if result.key?(key)
35
+
36
+ apply_default_for_origin(key, output)
59
37
  end
60
38
 
61
39
  super
@@ -63,26 +41,13 @@ module ServiceActor::Defaultable
63
41
 
64
42
  private
65
43
 
66
- def default_for_normal_mode_with(result, key, default)
67
- result[key] = reify_default(result, default)
68
- end
69
-
70
- def default_for_advanced_mode_with(result, key, content)
71
- default, message = content.values_at(:is, :message)
44
+ def apply_default_for_origin(origin_name, origin_options)
45
+ default = origin_options[:default]
72
46
 
73
- unless default
74
- raise_error_with(message, input_key: key, actor: self.class)
75
- end
76
-
77
- result[key] = reify_default(result, default)
78
-
79
- message.call(key, self.class)
47
+ result[origin_name] = reify_default(result, default)
80
48
  end
81
49
 
82
- # Raises an error depending on the mode
83
- def raise_error_with(message, **arguments)
84
- message = message.call(**arguments) if message.is_a?(Proc)
85
-
50
+ def raise_error_with(message)
86
51
  raise self.class.argument_error_class, message
87
52
  end
88
53
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Error raised when using `fail!` inside an actor.
3
+ # Default error raised when using `fail!` inside an actor.
4
4
  class ServiceActor::Failure < ServiceActor::Error
5
5
  def initialize(result)
6
6
  @result = result
@@ -90,7 +90,15 @@ module ServiceActor::Playable
90
90
  end
91
91
 
92
92
  def play_actor(actor)
93
- if actor.is_a?(Symbol)
93
+ actor_includes_kernel = begin
94
+ actor.respond_to?(:is_a?)
95
+ rescue NoMethodError
96
+ false
97
+ end
98
+
99
+ if !actor_includes_kernel
100
+ actor.call(result)
101
+ elsif actor.is_a?(Symbol)
94
102
  send(actor)
95
103
  elsif actor.is_a?(Class) && actor.ancestors.include?(ServiceActor::Core)
96
104
  play_service_actor(actor)
@@ -14,9 +14,12 @@ class ServiceActor::Result < BasicObject
14
14
  %i[
15
15
  block_given?
16
16
  class
17
+ hash
18
+ instance_of?
17
19
  instance_variables
18
20
  is_a?
19
21
  kind_of?
22
+ methods
20
23
  nil?
21
24
  object_id
22
25
  private_methods
@@ -24,7 +27,6 @@ class ServiceActor::Result < BasicObject
24
27
  send
25
28
  tap
26
29
  then
27
- methods
28
30
  ].each do |method_name|
29
31
  define_method(method_name, ::Kernel.instance_method(method_name))
30
32
  end
@@ -33,7 +35,7 @@ class ServiceActor::Result < BasicObject
33
35
  alias_method :blank?, :nil?
34
36
 
35
37
  def initialize(data = {})
36
- @data = data.to_h
38
+ @data = data.to_h.transform_keys(&:to_sym)
37
39
  end
38
40
 
39
41
  def to_h
@@ -44,7 +46,11 @@ class ServiceActor::Result < BasicObject
44
46
  "<#{self.class.name} #{to_h}>"
45
47
  end
46
48
 
47
- alias_method :pretty_print, :inspect
49
+ def pretty_print(pp)
50
+ pp.text "#<#{self.class.name} "
51
+ pp.pp to_h
52
+ pp.text ">"
53
+ end
48
54
 
49
55
  def fail!(failure_class = nil, result = {})
50
56
  if failure_class.nil? || failure_class.is_a?(::Hash)
@@ -71,24 +77,32 @@ class ServiceActor::Result < BasicObject
71
77
  end
72
78
 
73
79
  def merge!(result)
74
- data.merge!(result)
80
+ data.merge!(result.transform_keys(&:to_sym))
75
81
 
76
82
  self
77
83
  end
78
84
 
79
85
  def key?(name)
86
+ name = name.to_sym
87
+
80
88
  to_h.key?(name)
81
89
  end
82
90
 
83
91
  def [](name)
92
+ name = name.to_sym
93
+
84
94
  data[name]
85
95
  end
86
96
 
87
97
  def []=(key, value)
98
+ key = key.to_sym
99
+
88
100
  data[key] = value
89
101
  end
90
102
 
91
103
  def delete!(key)
104
+ key = key.to_sym
105
+
92
106
  data.delete(key)
93
107
  end
94
108
 
@@ -110,9 +124,7 @@ class ServiceActor::Result < BasicObject
110
124
  # Key `_default_output` is an internal datum used by actor class
111
125
  # method `.valuable`. Don't expose it with the rest of the result.
112
126
  def filter_default_output(h)
113
- # using `filter` instead of `except` to maintain Ruby 2.7 compatibility
114
- # update once support for 2.7 is dropped
115
- h.filter { |k| k != :_default_output }
127
+ h.except(:_default_output)
116
128
  end
117
129
 
118
130
  def respond_to_missing?(method_name, _include_private = false)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ServiceActor
4
- VERSION = "3.9.3"
4
+ VERSION = "4.0.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: service_actor
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.9.3
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sunny Ripert
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-07-15 00:00:00.000000000 Z
10
+ date: 2025-02-24 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: zeitwerk
@@ -150,6 +149,20 @@ dependencies:
150
149
  - - ">="
151
150
  - !ruby/object:Gem::Version
152
151
  version: '0.1'
152
+ - !ruby/object:Gem::Dependency
153
+ name: rubocop-thread_safety
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0.1'
159
+ type: :development
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0.1'
153
166
  - !ruby/object:Gem::Dependency
154
167
  name: code-scanning-rubocop
155
168
  requirement: !ruby/object:Gem::Requirement
@@ -192,6 +205,20 @@ dependencies:
192
205
  - - ">="
193
206
  - !ruby/object:Gem::Version
194
207
  version: '0.0'
208
+ - !ruby/object:Gem::Dependency
209
+ name: ostruct
210
+ requirement: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - ">="
213
+ - !ruby/object:Gem::Version
214
+ version: '0.0'
215
+ type: :development
216
+ prerelease: false
217
+ version_requirements: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - ">="
220
+ - !ruby/object:Gem::Version
221
+ version: '0.0'
195
222
  description: Service objects for your application logic
196
223
  email:
197
224
  - sunny@sunfox.org
@@ -233,7 +260,6 @@ metadata:
233
260
  source_code_uri: https://github.com/sunny/actor
234
261
  changelog_uri: https://github.com/sunny/actor/blob/main/CHANGELOG.md
235
262
  rubygems_mfa_required: 'true'
236
- post_install_message:
237
263
  rdoc_options: []
238
264
  require_paths:
239
265
  - lib
@@ -241,15 +267,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
241
267
  requirements:
242
268
  - - ">="
243
269
  - !ruby/object:Gem::Version
244
- version: '2.7'
270
+ version: '3.0'
245
271
  required_rubygems_version: !ruby/object:Gem::Requirement
246
272
  requirements:
247
273
  - - ">="
248
274
  - !ruby/object:Gem::Version
249
275
  version: '0'
250
276
  requirements: []
251
- rubygems_version: 3.3.26
252
- signing_key:
277
+ rubygems_version: 3.6.5
253
278
  specification_version: 4
254
279
  summary: Service objects for your application logic
255
280
  test_files: []