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