typed_operation 1.0.0.beta4 → 1.0.0.pre1

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