typed_operation 1.0.0.pre3 → 1.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +76 -624
  3. data/lib/generators/typed_operation/install/USAGE +1 -0
  4. data/lib/generators/typed_operation/install/install_generator.rb +8 -0
  5. data/lib/generators/typed_operation/install/templates/application_operation.rb +24 -2
  6. data/lib/generators/typed_operation_generator.rb +8 -4
  7. data/lib/typed_operation/action_policy_auth.rb +161 -0
  8. data/lib/typed_operation/base.rb +4 -13
  9. data/lib/typed_operation/callable_resolver.rb +30 -0
  10. data/lib/typed_operation/chains/chained_operation.rb +27 -0
  11. data/lib/typed_operation/chains/fallback_chain.rb +32 -0
  12. data/lib/typed_operation/chains/map_chain.rb +37 -0
  13. data/lib/typed_operation/chains/sequence_chain.rb +54 -0
  14. data/lib/typed_operation/chains/smart_chain.rb +161 -0
  15. data/lib/typed_operation/chains/splat_chain.rb +53 -0
  16. data/lib/typed_operation/configuration.rb +52 -0
  17. data/lib/typed_operation/context.rb +193 -0
  18. data/lib/typed_operation/curried.rb +14 -1
  19. data/lib/typed_operation/explainable.rb +14 -0
  20. data/lib/typed_operation/immutable_base.rb +4 -2
  21. data/lib/typed_operation/instrumentation/trace.rb +71 -0
  22. data/lib/typed_operation/instrumentation/tree_formatter.rb +141 -0
  23. data/lib/typed_operation/instrumentation.rb +214 -0
  24. data/lib/typed_operation/operations/composition.rb +41 -0
  25. data/lib/typed_operation/operations/executable.rb +27 -1
  26. data/lib/typed_operation/operations/introspection.rb +14 -8
  27. data/lib/typed_operation/operations/lifecycle.rb +5 -2
  28. data/lib/typed_operation/operations/parameters.rb +21 -6
  29. data/lib/typed_operation/operations/partial_application.rb +4 -0
  30. data/lib/typed_operation/operations/property_builder.rb +105 -0
  31. data/lib/typed_operation/partially_applied.rb +33 -10
  32. data/lib/typed_operation/pipeline/builder.rb +88 -0
  33. data/lib/typed_operation/pipeline/chainable_wrapper.rb +23 -0
  34. data/lib/typed_operation/pipeline/empty_pipeline_chain.rb +25 -0
  35. data/lib/typed_operation/pipeline/step_wrapper.rb +94 -0
  36. data/lib/typed_operation/pipeline.rb +176 -0
  37. data/lib/typed_operation/prepared.rb +13 -0
  38. data/lib/typed_operation/railtie.rb +4 -0
  39. data/lib/typed_operation/result/adapters/built_in.rb +28 -0
  40. data/lib/typed_operation/result/adapters/dry_monads.rb +36 -0
  41. data/lib/typed_operation/result/failure.rb +78 -0
  42. data/lib/typed_operation/result/mixin.rb +24 -0
  43. data/lib/typed_operation/result/success.rb +75 -0
  44. data/lib/typed_operation/result.rb +39 -0
  45. data/lib/typed_operation/version.rb +5 -1
  46. data/lib/typed_operation.rb +18 -6
  47. metadata +58 -18
  48. data/Rakefile +0 -17
  49. data/lib/tasks/typed_operation_tasks.rake +0 -4
  50. data/lib/typed_operation/operations/attribute_builder.rb +0 -81
  51. data/lib/typed_operation/operations/callable.rb +0 -23
  52. data/lib/typed_operation/operations/deconstruct.rb +0 -16
data/README.md CHANGED
@@ -1,693 +1,145 @@
1
1
  # TypedOperation
2
2
 
3
- An implementation of a Command pattern, which is callable, and can be partially applied.
3
+ ![Coverage](badges/coverage_badge_total.svg)
4
+ ![RubyCritic](badges/rubycritic_badge_score.svg)
4
5
 
5
- Inputs to the operation are specified as typed attributes (uses [`literal`](https://github.com/joeldrapper/literal)).
6
+ A Command pattern implementation for Ruby with typed parameters, partial application, and operation composition.
6
7
 
7
- Type of result of the operation is up to you, eg you could use [`literal` monads](https://github.com/joeldrapper/literal) or [`Dry::Monads`](https://dry-rb.org/gems/dry-monads/1.3/).
8
+ ## Why TypedOperation?
8
9
 
9
- **Note the version described here (~ 1.0.0) is not yet released on Rubygems, it is waiting for a release of `literal`)**
10
+ - **Type-safe parameters** - Runtime validation using the [`literal`](https://github.com/joeldrapper/literal) gem
11
+ - **Partial application & currying** - Build specialized operations from general ones
12
+ - **Operation composition** - Chain operations together with `.then`, pipelines, and railway-oriented error handling
13
+ - **Result types** - Built-in Success/Failure types, or use [`Dry::Monads`](https://dry-rb.org/gems/dry-monads/)
14
+ - **Rails integration** - Generators and Action Policy authorization support
10
15
 
11
- ## Features
12
-
13
- - Operations can be **partially applied** or **curried**
14
- - Operations are **callable**
15
- - Operations can be **pattern matched** on
16
- - Parameters:
17
- - specified with **type constraints** (uses `literal` gem)
18
- - can be **positional** or **named**
19
- - can be **optional**, or have **default** values
20
- - can be **coerced** by providing a block
21
-
22
- ### Example
23
-
24
- ```ruby
25
- class ShelveBookOperation < ::TypedOperation::Base
26
- # Parameters can be specified with `positional_param`/`named_param` or directly with the
27
- # underlying `param` method.
28
-
29
- # Note that you may also like to simply alias the param methods to your own preferred names:
30
- # `positional`/`named` or `arg`/`key` for example.
31
-
32
- # A positional parameter (positional argument passed to the operation when creating it).
33
- positional_param :title, String
34
- # Or if you prefer:
35
- # `param :title, String, positional: true`
36
-
37
- # A named parameter (keyword argument passed to the operation when creating it).
38
- named_param :description, String
39
- # Or if you prefer:
40
- # `param :description, String`
41
-
42
- named_param :author_id, Integer, &:to_i
43
- named_param :isbn, String
44
-
45
- # Optional parameters are specified by wrapping the type constraint in the `optional` method, or using the `optional:` option
46
- named_param :shelf_code, optional(Integer)
47
- # Or if you prefer:
48
- # `named_param :shelf_code, Integer, optional: true`
49
-
50
- named_param :category, String, default: "unknown".freeze
51
-
52
- # optional hook called when the operation is initialized, and after the parameters have been set
53
- def prepare
54
- raise ArgumentError, "ISBN is invalid" unless valid_isbn?
55
- end
56
-
57
- # optionally hook in before execution ... and call super to allow subclasses to hook in too
58
- def before_execute_operation
59
- # ...
60
- super
61
- end
62
-
63
- # The 'work' of the operation, this is the main body of the operation and must be implemented
64
- def perform
65
- "Put away '#{title}' by author ID #{author_id}#{shelf_code ? " on shelf #{shelf_code}" : "" }"
66
- end
67
-
68
- # optionally hook in after execution ... and call super to allow subclasses to hook in too
69
- def after_execute_operation(result)
70
- # ...
71
- super
72
- end
73
-
74
- private
75
-
76
- def valid_isbn?
77
- # ...
78
- true
79
- end
80
- end
81
-
82
- shelve = ShelveBookOperation.new("The Hobbit", description: "A book about a hobbit", author_id: "1", isbn: "978-0261103283")
83
- # => #<ShelveBookOperation:0x0000000108b3e490 @attributes={:title=>"The Hobbit", :description=>"A book about a hobbit", :author_id=>1, :isbn=>"978-0261103283", :shelf_code=>nil, :category=>"unknown"}, ...
84
-
85
- shelve.call
86
- # => "Put away 'The Hobbit' by author ID 1"
87
-
88
- shelve = ShelveBookOperation.with("The Silmarillion", description: "A book about the history of Middle-earth", shelf_code: 1)
89
- # => #<TypedOperation::PartiallyApplied:0x0000000103e6f560 ...
90
-
91
- shelve.call(author_id: "1", isbn: "978-0261102736")
92
- # => "Put away 'The Silmarillion' by author ID 1 on shelf 1"
93
-
94
- curried = shelve.curry
95
- # => #<TypedOperation::Curried:0x0000000108d98a10 ...
96
-
97
- curried.(1).("978-0261102736")
98
- # => "Put away 'The Silmarillion' by author ID 1 on shelf 1"
99
-
100
- shelve.call(author_id: "1", isbn: false)
101
- # => Raises an error because isbn is invalid
102
- # :in `initialize': Expected `false` to be of type: `String`. (Literal::TypeError)
103
- ```
104
-
105
- ### Partially applying parameters
106
-
107
- Operations can also be partially applied and curried:
108
-
109
- ```ruby
110
- class TestOperation < ::TypedOperation::Base
111
- param :foo, String, positional: true
112
- param :bar, String
113
- param :baz, String, &:to_s
114
-
115
- def perform = "It worked! (#{foo}, #{bar}, #{baz})"
116
- end
117
-
118
- # Invoking the operation directly
119
- TestOperation.("1", bar: "2", baz: 3)
120
- # => "It worked! (1, 2, 3)"
121
-
122
- # Partial application of parameters
123
- partially_applied = TestOperation.with("1").with(bar: "2")
124
- # => #<TypedOperation::PartiallyApplied:0x0000000110270248 @keyword_args={:bar=>"2"}, @operation_class=TestOperation, @positional_args=["1"]>
125
-
126
- # You can partially apply more than one parameter at a time, and chain calls to `.with`.
127
- # With all the required parameters set, the operation is 'prepared' and can be instantiated and called
128
- prepared = TestOperation.with("1", bar: "2").with(baz: 3)
129
- # => #<TypedOperation::Prepared:0x0000000110a9df38 @keyword_args={:bar=>"2", :baz=>3}, @operation_class=TestOperation, @positional_args=["1"]>
130
-
131
- # A 'prepared' operation can instantiated & called
132
- prepared.call
133
- # => "It worked! (1, 2, 3)"
134
-
135
- # You can provide additional parameters when calling call on a partially applied operation
136
- partially_applied.call(baz: 3)
137
- # => "It worked! (1, 2, 3)"
138
-
139
- # Partial application can be done using `.with or `.[]`
140
- TestOperation.with("1")[bar: "2", baz: 3].call
141
- # => "It worked! (1, 2, 3)"
142
-
143
- # Currying an operation, note that *all required* parameters must be provided an argument in order
144
- TestOperation.curry.("1").("2").(3)
145
- # => "It worked! (1, 2, 3)"
146
-
147
- # You can also curry from an already partially applied operation, so you can set optional named parameters first.
148
- # Note currying won't let you set optional positional parameters.
149
- partially_applied = TestOperation.with("1")
150
- partially_applied.curry.("2").(3)
151
- # => "It worked! (1, 2, 3)"
152
-
153
- # > TestOperation.with("1").with(bar: "2").call
154
- # => Raises an error because it is PartiallyApplied and so can't be called (it is missing required args)
155
- # "Cannot call PartiallyApplied operation TestOperation (key: test_operation), are you expecting it to be Prepared? (TypedOperation::MissingParameterError)"
156
-
157
- TestOperation.with("1").with(bar: "2").with(baz: 3).operation
158
- # same as > TestOperation.new("1", bar: "2", baz: 3)
159
- # => <TestOperation:0x000000014a0048a8 ...>
160
-
161
- # > TestOperation.with(foo: "1").with(bar: "2").operation
162
- # => Raises an error because it is PartiallyApplied so operation can't be instantiated
163
- # "Cannot instantiate Operation TestOperation (key: test_operation), as it is only partially applied. (TypedOperation::MissingParameterError)"
164
- ```
165
-
166
- ## Documentation
167
-
168
- ### Create an operation (subclass `TypedOperation::Base` or `TypedOperation::ImmutableBase`)
169
-
170
- Create an operation by subclassing `TypedOperation::Base` or `TypedOperation::ImmutableBase` and specifying the parameters the operation requires.
171
-
172
- - `TypedOperation::Base` (uses `Literal::Struct`) is the parent class for an operation where the arguments are potentially mutable (ie not frozen).
173
- No attribute writer methods are defined, so the arguments can not be changed after initialization, but the values passed in are not guaranteed to be frozen.
174
- - `TypedOperation::ImmutableBase` (uses `Literal::Data`) is the parent class for an operation where the arguments are immutable (frozen on initialization),
175
- thus giving a somewhat stronger immutability guarantee (ie that the operation does not mutate its arguments).
176
-
177
- The subclass must implement the `#perform` method which is where the operations main work is done.
16
+ ## Installation
178
17
 
179
- The operation can also implement:
18
+ Add to your Gemfile:
180
19
 
181
- - `#prepare` - called when the operation is initialized, and after the parameters have been set
182
- - `#before_execute_operation` - optionally hook in before execution ... and call super to allow subclasses to hook in too
183
- - `#after_execute_operation` - optionally hook in after execution ... and call super to allow subclasses to hook in too
184
-
185
20
  ```ruby
186
- # optionally hook in before execution...
187
- def before_execute_operation
188
- # Remember to call super
189
- super
190
- end
191
-
192
- def perform
193
- # ... implement me!
194
- end
195
-
196
- # optionally hook in after execution...
197
- def after_execute_operation(result)
198
- # Remember to call super, note the result is passed in and the return value of this method is the result of the operation
199
- # thus allowing you to modify the result if you wish
200
- super
201
- end
21
+ gem "typed_operation"
202
22
  ```
203
23
 
204
- ### Specifying parameters (using `.param`)
205
-
206
- Parameters are specified using the provided class methods (`.positional_param` and `.named_param`),
207
- or using the underlying `.param` method.
208
-
209
- Types are specified using the `literal` gem. In many cases this simply means providing the class of the
210
- expected type, but there are also some other useful types provided by `literal` (eg `Union`).
24
+ For Rails, generate the base operation class:
211
25
 
212
- These can be either accessed via the `Literal` module, eg `Literal::Types::BooleanType`:
213
-
214
- ```ruby
215
- class MyOperation < ::TypedOperation::Base
216
- param :name, String
217
- param :age, Integer, optional: true
218
- param :choices, Literal::Types::ArrayType.new(String)
219
- param :chose, Literal::Types::BooleanType
220
- end
26
+ ```bash
27
+ bin/rails g typed_operation:install
221
28
 
222
- MyOperation.new(name: "bob", choices: ["st"], chose: true)
29
+ # With optional integrations
30
+ bin/rails g typed_operation:install --dry_monads --action_policy
223
31
  ```
224
32
 
225
- or by including the `Literal::Types` module into your operation class, and using the aliases provided:
33
+ ## Quick Start
226
34
 
227
35
  ```ruby
228
- class MyOperation < ::TypedOperation::Base
229
- include Literal::Types
230
-
36
+ class GreetUser < TypedOperation::Base
231
37
  param :name, String
232
- param :age, _Nilable(Integer) # optional can also be specifed using `.optional`
233
- param :choices, _Array(String)
234
- param :chose, _Boolean
235
- end
236
- ```
237
-
238
- Type constraints can be modified to make the parameter optional using `.optional`.
239
-
240
- #### Your own aliases
241
-
242
- Note that you may also like to alias the param methods to your own preferred names in a common base operation class.
243
-
244
- Some possible aliases are:
245
- - `positional`/`named`
246
- - `arg`/`key`
247
-
248
- For example:
249
-
250
- ```ruby
251
- class ApplicationOperation < ::TypedOperation::Base
252
- class << self
253
- alias_method :arg, :positional_param
254
- alias_method :key, :named_param
255
- end
256
- end
257
-
258
- class MyOperation < ApplicationOperation
259
- arg :name, String
260
- key :age, Integer
261
- end
262
-
263
- MyOperation.new("Steve", age: 20)
264
- ```
265
-
266
- #### Positional parameters (`positional: true` or `.positional_param`)
267
-
268
- Defines a positional parameter (positional argument passed to the operation when creating it).
269
-
270
- The following are equivalent:
271
-
272
- - `param <param_name>, <type>, positional: true, <**options>`
273
- - `positional_param <param_name>, <type>, <**options>`
274
-
275
- The `<para_name>` is a symbolic name, used to create the accessor method, and when deconstructing to a hash.
276
-
277
- The `<type>` constraint provides the expected type of the parameter (the type is a type signature compatible with `literal`).
278
-
279
- The `<options>` are:
280
- - `default:` - a default value for the parameter (can be a proc or a frozen value)
281
- - `optional:` - a boolean indicating whether the parameter is optional (default: false). Note you may prefer to use the
282
- `.optional` method instead of this option.
283
-
284
- **Note** when positional arguments are provided to the operation, they are matched in order of definition or positional
285
- params. Also note that you cannot define required positional parameters after optional ones.
286
-
287
- Eg
38
+ param :greeting, String, default: "Hello"
288
39
 
289
- ```ruby
290
- class MyOperation < ::TypedOperation::Base
291
- positional_param :name, String, positional: true
292
- # Or alternatively => `param :name, String, positional: true`
293
- positional_param :age, Integer, default: -> { 0 }
294
-
295
40
  def perform
296
- puts "Hello #{name} (#{age})"
41
+ "#{greeting}, #{name}!"
297
42
  end
298
43
  end
299
44
 
300
- MyOperation.new("Steve").call
301
- # => "Hello Steve (0)"
45
+ # Direct invocation
46
+ GreetUser.call(name: "World")
47
+ # => "Hello, World!"
302
48
 
303
- MyOperation.with("Steve").call(20)
304
- # => "Hello Steve (20)"
49
+ # Partial application
50
+ greeter = GreetUser.with(greeting: "Welcome")
51
+ greeter.call(name: "Alice")
52
+ # => "Welcome, Alice!"
305
53
  ```
306
54
 
307
- #### Named (keyword) parameters
308
-
309
- Defines a named parameter (keyword argument passed to the operation when creating it).
310
-
311
- The following are equivalent:
312
- - `param <param_name>, <type>, <**options>`
313
- - `named_param <param_name>, <type>, <**options>`
314
-
315
- The `<para_name>` is a symbol, used as parameter name for the keyword arguments in the operation constructor, to
316
- create the accessor method and when deconstructing to a hash.
317
-
318
- The type constraint and options are the same as for positional parameters.
55
+ ### Parameters
319
56
 
320
57
  ```ruby
321
- class MyOperation < ::TypedOperation::Base
322
- named_param :name, String
323
- # Or alternatively => `param :name, String`
324
- named_param :age, Integer, default: -> { 0 }
325
-
326
- def perform
327
- puts "Hello #{name} (#{age})"
328
- end
329
- end
330
-
331
- MyOperation.new(name: "Steve").call
332
- # => "Hello Steve (0)"
58
+ class CreatePost < TypedOperation::Base
59
+ # Positional parameter
60
+ positional_param :title, String
333
61
 
334
- MyOperation.with(name: "Steve").call(age: 20)
335
- # => "Hello Steve (20)"
336
- ```
62
+ # Named parameters (keyword arguments)
63
+ param :body, String
64
+ param :author, User
337
65
 
338
- #### Using both positional and named parameters
66
+ # Optional with default
67
+ param :status, String, default: "draft"
339
68
 
340
- You can use both positional and named parameters in the same operation.
69
+ # Type coercion
70
+ param :view_count, Integer, &:to_i
341
71
 
342
- ```ruby
343
- class MyOperation < ::TypedOperation::Base
344
- positional_param :name, String
345
- named_param :age, Integer, default: -> { 0 }
346
-
347
72
  def perform
348
- puts "Hello #{name} (#{age})"
73
+ Post.create!(title: title, body: body, author: author, status: status)
349
74
  end
350
75
  end
351
76
 
352
- MyOperation.new("Steve").call
353
- # => "Hello Steve (0)"
354
-
355
- MyOperation.new("Steve", age: 20).call
356
- # => "Hello Steve (20)"
357
-
358
- MyOperation.with("Steve").call(age: 20)
359
- # => "Hello Steve (20)"
360
- ```
361
-
362
- #### Optional parameters (using `optional:` or `.optional`)
363
-
364
- Optional parameters are ones that do not need to be specified for the operation to be instantiated.
365
-
366
- An optional parameter can be specified by:
367
- - using the `optional:` option
368
- - using the `.optional` method around the type constraint
369
-
370
- ```ruby
371
- class MyOperation < ::TypedOperation::Base
372
- param :name, String
373
- param :age, Integer, optional: true
374
- param :nickname, optional(String)
375
- # ...
376
- end
377
-
378
- MyOperation.new(name: "Steve")
379
- MyOperation.new(name: "Steve", age: 20)
380
- MyOperation.new(name: "Steve", nickname: "Steve-o")
381
- ```
382
-
383
- This `.optional` class method effectively makes the type signature a union of the provided type and `NilClass`.
384
-
385
- #### Coercing parameters
386
-
387
- You can specify a block after a parameter definition to coerce the argument value.
388
-
389
- ```ruby
390
- param :name, String, &:to_s
391
- param :choice, Literal::Types::BooleanType do |v|
392
- v == "y"
393
- end
394
- ```
395
-
396
- #### Default values (with `default:`)
397
-
398
- You can specify a default value for a parameter using the `default:` option.
399
-
400
- The default value can be a proc or a frozen value. If the value is specified as `nil` then the default value is literally nil and the parameter is optional.
401
-
402
- ```ruby
403
- param :name, String, default: "Steve".freeze
404
- param :age, Integer, default: -> { rand(100) }
77
+ CreatePost.call("My Post", body: "Content", author: current_user)
405
78
  ```
406
79
 
407
- If using the directive `# frozen_string_literal: true` then you string values are frozen by default.
80
+ ### Operation Composition
408
81
 
409
- ### Partially applying (fixing parameters) on an operation (using `.with`)
410
-
411
- `.with(...)` creates a partially applied operation with the provided parameters.
412
-
413
- It is aliased to `.[]` for an alternative syntax.
414
-
415
- Note that `.with` can take both positional and keyword arguments, and can be chained.
416
-
417
- **An important caveat about partial application is that type checking is not done until the operation is instantiated**
82
+ Chain operations using `.then`, `.transform`, and `.or_else`:
418
83
 
419
84
  ```ruby
420
- MyOperation.new(123)
421
- # => Raises an error as the type of the first parameter is incorrect:
422
- # Expected `123` to be of type: `String`. (Literal::TypeError)
423
-
424
- op = MyOperation.with(123)
425
- # => #<TypedOperation::Prepared:0x000000010b1d3358 ...
426
- # Does **not raise** an error, as the type of the first parameter is not checked until the operation is instantiated
427
-
428
- op.call # or op.operation
429
- # => Now raises an error as the type of the first parameter is incorrect and operation is instantiated
85
+ ValidateOrder
86
+ .then(ProcessPayment)
87
+ .then(SendConfirmation)
88
+ .or_else { |failure| LogError.call(failure) }
89
+ .call(order_id: 123)
430
90
  ```
431
91
 
432
- ### Calling an operation (using `.call`)
433
-
434
- An operation can be invoked by:
435
-
436
- - instantiating it with at least required params and then calling the `#call` method on the instance
437
- - once a partially applied operation has been prepared (all required parameters have been set), the call
438
- method on `TypedOperation::Prepared` can be used to instantiate and call the operation.
439
- - once an operation is curried, the `#call` method on last TypedOperation::Curried in the chain will invoke the operation
440
- - calling `#call` on a partially applied operation and passing in any remaining required parameters
441
- - calling `#execute_operation` on an operation instance (this is the method that is called by `#call`)
442
-
443
- See the many examples in this document.
444
-
445
- ### Pattern matching on an operation
446
-
447
- `TypedOperation::Base` and `TypedOperation::PartiallyApplied` implement `deconstruct` and `deconstruct_keys` methods,
448
- so they can be pattern matched against.
92
+ Or use the Pipeline DSL for declarative composition:
449
93
 
450
94
  ```ruby
451
- case MyOperation.new("Steve", age: 20)
452
- in MyOperation[name, age]
453
- puts "Hello #{name} (#{age})"
95
+ OrderPipeline = TypedOperation::Pipeline.build do
96
+ step ValidateOrder
97
+ step ProcessPayment
98
+ step SendConfirmation, if: ->(ctx) { ctx[:send_email] }
99
+ on_failure { |error| Logger.error(error) }
454
100
  end
455
101
 
456
- case MyOperation.new("Steve", age: 20)
457
- in MyOperation[name:, age: 20]
458
- puts "Hello #{name} (#{age})"
459
- end
460
- ```
461
-
462
- ### Introspection of parameters & other methods
463
-
464
- #### `.to_proc`
465
-
466
- Get a proc that calls `.call(...)`
467
-
468
-
469
- #### `#to_proc`
470
-
471
- Get a proc that calls the `#call` method on an operation instance
472
-
473
- #### `.prepared?`
474
-
475
- Check if an operation is prepared
476
-
477
- #### `.operation`
478
-
479
- Return an operation instance from a Prepared operation. Will raise if called on a PartiallyApplied operation
480
-
481
- #### `.positional_parameters`
482
-
483
- List of the names of the positional parameters, in order
484
-
485
- #### `.keyword_parameters`
486
-
487
- List of the names of the keyword parameters
488
-
489
- #### `.required_positional_parameters`
490
-
491
- List of the names of the required positional parameters, in order
492
-
493
- #### `.required_keyword_parameters`
494
-
495
- List of the names of the required keyword parameters
496
-
497
- #### `.optional_positional_parameters`
498
-
499
- List of the names of the optional positional parameters, in order
500
-
501
- #### `.optional_keyword_parameters`
502
-
503
- List of the names of the optional keyword parameters
504
-
505
-
506
- ### Using with Rails
507
-
508
- You can use the provided generator to create an `ApplicationOperation` class in your Rails project.
509
-
510
- You can then extend this to add extra functionality to all your operations.
511
-
512
- This is an example of a `ApplicationOperation` in a Rails app that uses `Dry::Monads`:
513
-
514
- ```ruby
515
- # frozen_string_literal: true
516
-
517
- class ApplicationOperation < ::TypedOperation::Base
518
- # We choose to use dry-monads for our operations, so include the required modules
519
- include Dry::Monads[:result, :do]
520
-
521
- class << self
522
- # Setup our own preferred names for the DSL methods
523
- alias_method :positional, :positional_param
524
- alias_method :named, :named_param
525
- end
526
-
527
- # Parameters common to all Operations in this application
528
- named :initiator, optional(::User)
529
-
530
- private
531
-
532
- # We setup some helper methods for our operations to use
533
-
534
- def succeeded(value)
535
- Success(value)
536
- end
537
-
538
- def failed_with_value(value, message: "Operation failed", error_code: nil)
539
- failed(error_code || operation_key, message, value)
540
- end
541
-
542
- def failed_with_message(message, error_code: nil)
543
- failed(error_code || operation_key, message)
544
- end
545
-
546
- def failed(error_code, message = "Operation failed", value = nil)
547
- Failure[error_code, message, value]
548
- end
549
-
550
- def failed_with_code_and_value(error_code, value, message: "Operation failed")
551
- failed(error_code, message, value)
552
- end
553
-
554
- def operation_key
555
- self.class.name
556
- end
557
- end
102
+ OrderPipeline.call(order_id: 123, send_email: true)
558
103
  ```
559
104
 
560
- ### Using with Action Policy (`action_policy` gem)
105
+ ### Result Types
561
106
 
562
- Base you `ApplicationOperation` on the following:
107
+ TypedOperation includes built-in `Success`/`Failure` types for explicit error handling:
563
108
 
564
109
  ```ruby
565
- class ApplicationOperation < ::TypedOperation::Base
566
- # ...
567
- include ActionPolicy::Behaviour
568
-
569
- # ...
570
-
571
- param :initiator, ::User
572
- authorize :initiator
573
- # Or
574
- # param :initiator, optional(::User)
575
- # authorize :initiator, through: {optional: true}
576
-
577
- # ...
578
- end
579
- ```
580
- ### Using with `literal` monads
110
+ class ProcessPayment < TypedOperation::Base
111
+ include TypedOperation::Result::Mixin # Built-in, no dependencies
581
112
 
582
- You can use the `literal` gem to provide a `Result` type for your operations.
583
-
584
- ```ruby
585
- class MyOperation < ::TypedOperation::Base
586
- param :account_name, String
587
- param :owner, String
113
+ param :amount, Numeric
588
114
 
589
115
  def perform
590
- create_account.bind do |account|
591
- associate_owner(account).map { account }
592
- end
593
- end
594
-
595
- private
596
-
597
- def create_account
598
- # ...
599
- # Literal::Failure.new(:cant_create_account)
600
- Literal::Success.new(account_name)
601
- end
602
-
603
- def associate_owner(account)
604
- # ...
605
- Literal::Failure.new(:cant_associate_owner)
606
- # Literal::Success.new("ok")
116
+ result = PaymentGateway.charge(amount)
117
+ result.ok? ? Success(result.id) : Failure(:payment_failed)
607
118
  end
608
119
  end
609
120
 
610
- MyOperation.new(account_name: "foo", owner: "bar").call
611
- # => Literal::Failure(:cant_associate_owner)
612
- ```
613
-
614
- ### Using with `Dry::Monads`
615
-
616
- As per the example in [`Dry::Monads` documentation](https://dry-rb.org/gems/dry-monads/1.0/do-notation/)
617
-
618
- ```ruby
619
- class MyOperation < ::TypedOperation::Base
620
- include Dry::Monads[:result]
621
- include Dry::Monads::Do.for(:call)
622
-
623
- param :account_name, String
624
- param :owner, ::Owner
625
-
626
- def perform
627
- account = yield create_account(account_name)
628
- yield associate_owner(account, owner)
629
-
630
- Success(account)
631
- end
632
-
633
- private
634
-
635
- def create_account(account_name)
636
- # returns Success(account) or Failure(:cant_create)
637
- end
638
- end
121
+ result = ProcessPayment.call(amount: 100)
122
+ result.success? # => true/false
123
+ result.value! # => the value (raises on failure)
639
124
  ```
640
125
 
641
- ## Installation
126
+ For advanced features like Do notation, use [Dry::Monads](https://dry-rb.org/gems/dry-monads/) instead. See [Getting Started: Error Handling](website/docs/getting-started.md#error-handling) for details.
642
127
 
643
- Add this line to your application's Gemfile:
644
-
645
- ```ruby
646
- gem "typed_operation"
647
- ```
648
-
649
- And then execute:
650
- ```bash
651
- $ bundle
652
- ```
653
-
654
- Or install it yourself as:
655
- ```bash
656
- $ gem install typed_operation
657
- ```
658
-
659
- ### Add an `ApplicationOperation` to your project
660
-
661
- ```ruby
662
- bin/rails g typed_operation:install
663
- ```
664
-
665
- Use the `--dry_monads` switch to `include Dry::Monads[:result]` into your `ApplicationOperation` (don't forget to also
666
- add `gem "dry-monads"` to your Gemfile)
667
-
668
- ```ruby
669
- bin/rails g typed_operation:install --dry_monads
670
- ```
671
-
672
- ## Generate a new Operation
673
-
674
- ```ruby
675
- bin/rails g typed_operation TestOperation
676
- ```
677
-
678
- You can optionally specify the directory to generate the operation in:
679
-
680
- ```ruby
681
- bin/rails g typed_operation TestOperation --path=app/operations
682
- ```
128
+ ## Documentation
683
129
 
684
- The default path is `app/operations`.
130
+ For complete documentation, visit the [TypedOperation documentation site](https://stevegeek.github.io/typed_operation/) or see the `website/docs/` directory:
685
131
 
686
- The generator will also create a test file.
132
+ - [Getting Started](website/docs/getting-started.md) - Installation and basic usage
133
+ - [API Reference](website/docs/api.md) - Complete API documentation
134
+ - [Pipelines](website/docs/pipelines.md) - Pipeline DSL and operation composition
135
+ - [Integrations](website/docs/integrations.md) - Rails, Dry::Monads, and Action Policy
136
+ - [Instrumentation](website/docs/instrumentation.md) - Debugging and tracing operations
137
+ - [Best Practices](website/docs/best-practices.md) - Patterns and recommendations
138
+ - [Examples](website/docs/examples.md) - Comprehensive code examples
687
139
 
688
140
  ## Contributing
689
141
 
690
- Bug reports and pull requests are welcome on GitHub at https://github.com/stevegeek/typed_operation. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/stevegeek/typed_operation/blob/master/CODE_OF_CONDUCT.md).
142
+ Bug reports and pull requests are welcome on GitHub at https://github.com/stevegeek/typed_operation.
691
143
 
692
144
  ## License
693
145