typed_operation 0.4.2 → 1.0.0.pre2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +546 -32
- data/Rakefile +15 -1
- data/lib/generators/templates/operation.rb +10 -4
- data/lib/typed_operation/base.rb +13 -47
- data/lib/typed_operation/curried.rb +40 -0
- data/lib/typed_operation/immutable_base.rb +13 -0
- data/lib/typed_operation/operations/attribute_builder.rb +75 -0
- data/lib/typed_operation/operations/callable.rb +27 -0
- data/lib/typed_operation/operations/deconstruct.rb +16 -0
- data/lib/typed_operation/operations/introspection.rb +38 -0
- data/lib/typed_operation/operations/lifecycle.rb +11 -0
- data/lib/typed_operation/operations/parameters.rb +31 -0
- data/lib/typed_operation/operations/partial_application.rb +16 -0
- data/lib/typed_operation/partially_applied.rb +72 -19
- data/lib/typed_operation/prepared.rb +5 -1
- data/lib/typed_operation/version.rb +1 -1
- data/lib/typed_operation.rb +18 -1
- metadata +16 -55
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1337560f2bffbfd45574c1df5f007c951dba43293324c309c3409aab18e57f0c
|
4
|
+
data.tar.gz: 63deedbca3cde1fadb88668af46ec21652e17f9600d23c0e13c8b22429c05e0c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0a8fdc89c772282de6c654365e18d7397532b84aebd738285499998c04c32906dc1e63961f5b47d139dfff532a458461dbe07a29bdfcda9f29d8132d645599df
|
7
|
+
data.tar.gz: cee03eb2c1e2d9100e657d98a1b70f6889b0e276251dd839f343586ff89f17287853224a487357343fc421ff6723686a2bf672c7ad53a058246668bb1d80a926
|
data/README.md
CHANGED
@@ -1,33 +1,513 @@
|
|
1
1
|
# TypedOperation
|
2
2
|
|
3
|
-
An implementation of a Command pattern, which is callable, and can be partially applied
|
3
|
+
An implementation of a Command pattern, which is callable, and can be partially applied.
|
4
4
|
|
5
|
-
Inputs to the operation are specified as typed attributes
|
5
|
+
Inputs to the operation are specified as typed attributes (uses [`literal`](https://github.com/joeldrapper/literal)).
|
6
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
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
10
|
|
11
|
-
|
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
|
94
|
+
|
95
|
+
Operations can also be partially applied and curried:
|
96
|
+
|
97
|
+
```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)"
|
152
|
+
```
|
153
|
+
|
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.
|
159
|
+
|
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
|
189
|
+
|
190
|
+
MyOperation.new(name: "bob", choices: ["st"], chose: true)
|
191
|
+
```
|
192
|
+
|
193
|
+
or by including the `Literal::Types` module into your operation class, and using the aliases provided:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
class MyOperation < ::TypedOperation::Base
|
197
|
+
include Literal::Types
|
198
|
+
|
199
|
+
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:
|
12
217
|
|
13
218
|
```ruby
|
14
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
|
230
|
+
|
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})"
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
MyOperation.new("Steve").call
|
269
|
+
# => "Hello Steve (0)"
|
270
|
+
|
271
|
+
MyOperation.with("Steve").call(20)
|
272
|
+
# => "Hello Steve (20)"
|
273
|
+
```
|
274
|
+
|
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.
|
287
|
+
|
288
|
+
```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
|
298
|
+
|
299
|
+
MyOperation.new(name: "Steve").call
|
300
|
+
# => "Hello Steve (0)"
|
301
|
+
|
302
|
+
MyOperation.with(name: "Steve").call(age: 20)
|
303
|
+
# => "Hello Steve (20)"
|
304
|
+
```
|
305
|
+
|
306
|
+
#### Using both positional and named parameters
|
307
|
+
|
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})"
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
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) }
|
373
|
+
```
|
374
|
+
|
375
|
+
If using the directive `# frozen_string_literal: true` then you string values are frozen by default.
|
376
|
+
|
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**
|
386
|
+
|
387
|
+
```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
|
398
|
+
```
|
399
|
+
|
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.
|
416
|
+
|
417
|
+
```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})"
|
426
|
+
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
|
15
486
|
include Dry::Monads[:result, :do]
|
16
487
|
|
17
|
-
|
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)
|
18
496
|
|
19
497
|
private
|
20
498
|
|
499
|
+
# We setup some helper methods for our operations to use
|
500
|
+
|
21
501
|
def succeeded(value)
|
22
502
|
Success(value)
|
23
503
|
end
|
24
504
|
|
25
505
|
def failed_with_value(value, message: "Operation failed", error_code: nil)
|
26
|
-
failed(error_code ||
|
506
|
+
failed(error_code || operation_key, message, value)
|
27
507
|
end
|
28
508
|
|
29
509
|
def failed_with_message(message, error_code: nil)
|
30
|
-
failed(error_code ||
|
510
|
+
failed(error_code || operation_key, message)
|
31
511
|
end
|
32
512
|
|
33
513
|
def failed(error_code, message = "Operation failed", value = nil)
|
@@ -37,48 +517,76 @@ class ApplicationOperation < ::TypedOperation::Base
|
|
37
517
|
def failed_with_code_and_value(error_code, value, message: "Operation failed")
|
38
518
|
failed(error_code, message, value)
|
39
519
|
end
|
40
|
-
end
|
41
520
|
|
521
|
+
def operation_key
|
522
|
+
self.class.name
|
523
|
+
end
|
524
|
+
end
|
42
525
|
```
|
43
526
|
|
44
|
-
|
527
|
+
### Using with `literal` monads
|
528
|
+
|
529
|
+
You can use the `literal` gem to provide a `Result` type for your operations.
|
45
530
|
|
46
531
|
```ruby
|
47
|
-
class
|
48
|
-
param :
|
49
|
-
param :
|
50
|
-
param :baz, String, convert: true
|
532
|
+
class MyOperation < ::TypedOperation::Base
|
533
|
+
param :account_name, String
|
534
|
+
param :owner, String
|
51
535
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
536
|
+
def call
|
537
|
+
create_account.bind do |account|
|
538
|
+
associate_owner(account).map { account }
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
private
|
543
|
+
|
544
|
+
def create_account
|
545
|
+
# ...
|
546
|
+
# Literal::Failure.new(:cant_create_account)
|
547
|
+
Literal::Success.new(account_name)
|
55
548
|
end
|
56
549
|
|
57
|
-
def
|
58
|
-
|
59
|
-
|
550
|
+
def associate_owner(account)
|
551
|
+
# ...
|
552
|
+
Literal::Failure.new(:cant_associate_owner)
|
553
|
+
# Literal::Success.new("ok")
|
60
554
|
end
|
61
555
|
end
|
556
|
+
|
557
|
+
MyOperation.new(account_name: "foo", owner: "bar").call
|
558
|
+
# => Literal::Failure(:cant_associate_owner)
|
62
559
|
```
|
63
560
|
|
64
|
-
|
65
|
-
TestOperation.(foo: "1", bar: "2", baz: 3)
|
66
|
-
# => Success("It worked!")
|
561
|
+
### Using with `Dry::Monads`
|
67
562
|
|
68
|
-
|
69
|
-
# => #<TypedOperation::PartiallyApplied:0x000000014a655310 @applied_args={:foo=>"1", :bar=>"2"}, @operation=TestOperation>
|
563
|
+
As per the example in [`Dry::Monads` documentation](https://dry-rb.org/gems/dry-monads/1.0/do-notation/)
|
70
564
|
|
71
|
-
|
72
|
-
|
565
|
+
```ruby
|
566
|
+
class MyOperation < ::TypedOperation::Base
|
567
|
+
include Dry::Monads[:result]
|
568
|
+
include Dry::Monads::Do.for(:call)
|
73
569
|
|
74
|
-
|
75
|
-
|
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)
|
76
576
|
|
77
|
-
|
78
|
-
|
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
|
79
586
|
```
|
80
587
|
|
81
588
|
## Installation
|
589
|
+
|
82
590
|
Add this line to your application's Gemfile:
|
83
591
|
|
84
592
|
```ruby
|
@@ -125,7 +633,13 @@ The default path is `app/operations`.
|
|
125
633
|
The generator will also create a test file.
|
126
634
|
|
127
635
|
## Contributing
|
128
|
-
|
636
|
+
|
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).
|
129
638
|
|
130
639
|
## License
|
640
|
+
|
131
641
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
642
|
+
|
643
|
+
## Code of Conduct
|
644
|
+
|
645
|
+
Everyone interacting in the TypedOperation project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/stevegeek/typed_operation/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "bundler/gem_tasks"
|
4
|
+
require "bundler/setup"
|
5
|
+
require "rake/testtask"
|
6
|
+
|
7
|
+
ENV["NO_RAILS"] = "true"
|
8
|
+
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
10
|
+
t.libs << "test"
|
11
|
+
t.libs << "lib"
|
12
|
+
t.test_files = FileList["test/typed_operation/**/*_test.rb"]
|
13
|
+
end
|
14
|
+
|
15
|
+
require "standard/rake"
|
16
|
+
|
17
|
+
task default: %i[test standard]
|
@@ -4,8 +4,11 @@
|
|
4
4
|
module <%= namespace_name %>
|
5
5
|
class <%= name %> < ::ApplicationOperation
|
6
6
|
# Replace with implementation...
|
7
|
-
|
8
|
-
param :
|
7
|
+
positional_param :required_positional_param, String
|
8
|
+
param :required_named_param, String
|
9
|
+
param :an_optional_param, Integer, optional: true do |value|
|
10
|
+
value.to_i
|
11
|
+
end
|
9
12
|
|
10
13
|
def prepare
|
11
14
|
# Prepare...
|
@@ -20,8 +23,11 @@ end
|
|
20
23
|
<% else %>
|
21
24
|
class <%= name %> < ::ApplicationOperation
|
22
25
|
# Replace with implementation...
|
23
|
-
|
24
|
-
param :
|
26
|
+
positional_param :required_positional_param, String
|
27
|
+
param :required_named_param, String
|
28
|
+
param :an_optional_param, Integer, optional: true do |value|
|
29
|
+
value.to_i
|
30
|
+
end
|
25
31
|
|
26
32
|
def prepare
|
27
33
|
# Prepare...
|
data/lib/typed_operation/base.rb
CHANGED
@@ -1,58 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "vident/typed"
|
4
|
-
require "vident/typed/attributes"
|
5
|
-
|
6
3
|
module TypedOperation
|
7
|
-
class Base
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
def call(...)
|
12
|
-
new(...).call
|
13
|
-
end
|
14
|
-
|
15
|
-
def curry(**args)
|
16
|
-
PartiallyApplied.new(self, **args).curry
|
17
|
-
end
|
18
|
-
alias_method :[], :curry
|
19
|
-
alias_method :with, :curry
|
20
|
-
|
21
|
-
def to_proc
|
22
|
-
method(:call).to_proc
|
23
|
-
end
|
24
|
-
|
25
|
-
def operation_key
|
26
|
-
name.underscore.to_sym
|
27
|
-
end
|
4
|
+
class Base < Literal::Struct
|
5
|
+
extend Operations::Introspection
|
6
|
+
extend Operations::Parameters
|
7
|
+
extend Operations::PartialApplication
|
28
8
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
end
|
33
|
-
end
|
9
|
+
include Operations::Callable
|
10
|
+
include Operations::Lifecycle
|
11
|
+
include Operations::Deconstruct
|
34
12
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
rescue ::Dry::Struct::Error => e
|
39
|
-
raise ParameterError, e.message
|
13
|
+
class << self
|
14
|
+
def attribute(name, type, special = nil, reader: :public, writer: :public, positional: false, default: nil)
|
15
|
+
super(name, type, special, reader:, writer: false, positional:, default:)
|
40
16
|
end
|
41
|
-
prepare if respond_to?(:prepare)
|
42
17
|
end
|
43
18
|
|
44
|
-
def
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
def to_proc
|
49
|
-
method(:call).to_proc
|
50
|
-
end
|
51
|
-
|
52
|
-
private
|
53
|
-
|
54
|
-
def operation_key
|
55
|
-
self.class.operation_key
|
19
|
+
def with(...)
|
20
|
+
# copy to new operation with new attrs
|
21
|
+
self.class.new(**attributes.merge(...))
|
56
22
|
end
|
57
23
|
end
|
58
24
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedOperation
|
4
|
+
class Curried
|
5
|
+
def initialize(operation_class, partial_operation = nil)
|
6
|
+
@operation_class = operation_class
|
7
|
+
@partial_operation = partial_operation || operation_class.with
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(arg)
|
11
|
+
raise ArgumentError, "A prepared operation should not be curried" if @partial_operation.prepared?
|
12
|
+
|
13
|
+
next_partially_applied = if next_parameter_positional?
|
14
|
+
@partial_operation.with(arg)
|
15
|
+
else
|
16
|
+
@partial_operation.with(next_keyword_parameter => arg)
|
17
|
+
end
|
18
|
+
if next_partially_applied.prepared?
|
19
|
+
next_partially_applied.call
|
20
|
+
else
|
21
|
+
Curried.new(@operation_class, next_partially_applied)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_proc
|
26
|
+
method(:call).to_proc
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def next_keyword_parameter
|
32
|
+
remaining = @operation_class.required_keyword_parameters - @partial_operation.keyword_args.keys
|
33
|
+
remaining.first
|
34
|
+
end
|
35
|
+
|
36
|
+
def next_parameter_positional?
|
37
|
+
@partial_operation.positional_args.size < @operation_class.required_positional_parameters.size
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedOperation
|
4
|
+
class ImmutableBase < Literal::Data
|
5
|
+
extend Operations::Introspection
|
6
|
+
extend Operations::Parameters
|
7
|
+
extend Operations::PartialApplication
|
8
|
+
|
9
|
+
include Operations::Callable
|
10
|
+
include Operations::Lifecycle
|
11
|
+
include Operations::Deconstruct
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedOperation
|
4
|
+
module Operations
|
5
|
+
class AttributeBuilder
|
6
|
+
def initialize(typed_operation, parameter_name, type_signature, options)
|
7
|
+
@typed_operation = typed_operation
|
8
|
+
@name = parameter_name
|
9
|
+
@signature = type_signature
|
10
|
+
@optional = options[:optional]
|
11
|
+
@positional = options[:positional]
|
12
|
+
@reader = options[:reader] || :public
|
13
|
+
@default_key = options.key?(:default)
|
14
|
+
@default = options[:default]
|
15
|
+
|
16
|
+
prepare_type_signature_for_literal
|
17
|
+
end
|
18
|
+
|
19
|
+
def define(&converter)
|
20
|
+
@typed_operation.attribute(
|
21
|
+
@name,
|
22
|
+
@signature,
|
23
|
+
default: default_value_for_literal,
|
24
|
+
positional: @positional,
|
25
|
+
reader: @reader,
|
26
|
+
&converter
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def prepare_type_signature_for_literal
|
33
|
+
@signature = Literal::Types::NilableType.new(@signature) if needs_to_be_nilable?
|
34
|
+
union_with_nil_to_support_nil_default
|
35
|
+
validate_positional_order_params! if @positional
|
36
|
+
end
|
37
|
+
|
38
|
+
# If already wrapped in a Nilable then don't wrap again
|
39
|
+
def needs_to_be_nilable?
|
40
|
+
@optional && !type_nilable?
|
41
|
+
end
|
42
|
+
|
43
|
+
def type_nilable?
|
44
|
+
@signature.is_a?(Literal::Types::NilableType)
|
45
|
+
end
|
46
|
+
|
47
|
+
def union_with_nil_to_support_nil_default
|
48
|
+
@signature = Literal::Union.new(@signature, NilClass) if has_default_value_nil?
|
49
|
+
end
|
50
|
+
|
51
|
+
def has_default_value_nil?
|
52
|
+
default_provided? && @default.nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
def validate_positional_order_params!
|
56
|
+
# Optional ones can always be added after required ones, or before any others, but required ones must be first
|
57
|
+
unless type_nilable? || @typed_operation.optional_positional_parameters.empty?
|
58
|
+
raise ParameterError, "Cannot define required positional parameter '#{@name}' after optional positional parameters"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def default_provided?
|
63
|
+
@default_key
|
64
|
+
end
|
65
|
+
|
66
|
+
def default_value_for_literal
|
67
|
+
if has_default_value_nil? || type_nilable?
|
68
|
+
-> {}
|
69
|
+
else
|
70
|
+
@default
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedOperation
|
4
|
+
module Operations
|
5
|
+
module Callable
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(CallableMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module CallableMethods
|
11
|
+
def call(...)
|
12
|
+
new(...).call
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_proc
|
16
|
+
method(:call).to_proc
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
include CallableMethods
|
21
|
+
|
22
|
+
def call
|
23
|
+
raise InvalidOperationError, "You must implement #call"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedOperation
|
4
|
+
module Operations
|
5
|
+
module Deconstruct
|
6
|
+
def deconstruct
|
7
|
+
attributes.values
|
8
|
+
end
|
9
|
+
|
10
|
+
def deconstruct_keys(keys)
|
11
|
+
h = attributes.to_h
|
12
|
+
keys ? h.slice(*keys) : h
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedOperation
|
4
|
+
module Operations
|
5
|
+
# Introspection methods
|
6
|
+
module Introspection
|
7
|
+
def positional_parameters
|
8
|
+
literal_attributes.filter_map { |name, attribute| name if attribute.positional? }
|
9
|
+
end
|
10
|
+
|
11
|
+
def keyword_parameters
|
12
|
+
literal_attributes.filter_map { |name, attribute| name unless attribute.positional? }
|
13
|
+
end
|
14
|
+
|
15
|
+
def required_parameters
|
16
|
+
literal_attributes.filter do |name, attribute|
|
17
|
+
attribute.default.nil? # Any optional parameters will have a default value/proc in their Literal::Attribute
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def required_positional_parameters
|
22
|
+
required_parameters.filter_map { |name, attribute| name if attribute.positional? }
|
23
|
+
end
|
24
|
+
|
25
|
+
def required_keyword_parameters
|
26
|
+
required_parameters.filter_map { |name, attribute| name unless attribute.positional? }
|
27
|
+
end
|
28
|
+
|
29
|
+
def optional_positional_parameters
|
30
|
+
positional_parameters - required_positional_parameters
|
31
|
+
end
|
32
|
+
|
33
|
+
def optional_keyword_parameters
|
34
|
+
keyword_parameters - required_keyword_parameters
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedOperation
|
4
|
+
module Operations
|
5
|
+
# Method to define parameters for your operation.
|
6
|
+
module Parameters
|
7
|
+
# Parameter for keyword argument, or a positional argument if you use positional: true
|
8
|
+
# Required, but you can set a default or use optional: true if you want optional
|
9
|
+
def param(name, signature = :any, **options, &converter)
|
10
|
+
AttributeBuilder.new(self, name, signature, options).define(&converter)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Alternative DSL
|
14
|
+
|
15
|
+
# Parameter for positional argument
|
16
|
+
def positional_param(name, signature = :any, **options, &converter)
|
17
|
+
param(name, signature, **options.merge(positional: true), &converter)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Parameter for a keyword or named argument
|
21
|
+
def named_param(name, signature = :any, **options, &converter)
|
22
|
+
param(name, signature, **options.merge(positional: false), &converter)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Wrap a type signature in a NilableType meaning it is optional to TypedOperation
|
26
|
+
def optional(type_signature)
|
27
|
+
Literal::Types::NilableType.new(type_signature)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedOperation
|
4
|
+
module Operations
|
5
|
+
module PartialApplication
|
6
|
+
def with(...)
|
7
|
+
PartiallyApplied.new(self, ...).with
|
8
|
+
end
|
9
|
+
alias_method :[], :with
|
10
|
+
|
11
|
+
def curry
|
12
|
+
Curried.new(self)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -2,34 +2,87 @@
|
|
2
2
|
|
3
3
|
module TypedOperation
|
4
4
|
class PartiallyApplied
|
5
|
-
def initialize(
|
6
|
-
@
|
7
|
-
@
|
5
|
+
def initialize(operation_class, *positional_args, **keyword_args)
|
6
|
+
@operation_class = operation_class
|
7
|
+
@positional_args = positional_args
|
8
|
+
@keyword_args = keyword_args
|
8
9
|
end
|
9
10
|
|
10
|
-
def
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
meta[:required] != false && !meta[:typed_attribute_options].key?(:default)
|
16
|
-
end
|
17
|
-
missing_keys = required_keys - all_args.keys
|
11
|
+
def with(*positional, **keyword)
|
12
|
+
all_positional = positional_args + positional
|
13
|
+
all_kw_args = keyword_args.merge(keyword)
|
14
|
+
|
15
|
+
validate_positional_arg_count!(all_positional.size)
|
18
16
|
|
19
|
-
if
|
20
|
-
|
21
|
-
PartiallyApplied.new(@operation, **all_args)
|
17
|
+
if partially_applied?(all_positional, all_kw_args)
|
18
|
+
PartiallyApplied.new(operation_class, *all_positional, **all_kw_args)
|
22
19
|
else
|
23
|
-
Prepared.new(
|
20
|
+
Prepared.new(operation_class, *all_positional, **all_kw_args)
|
24
21
|
end
|
25
22
|
end
|
26
|
-
alias_method :[], :
|
27
|
-
|
23
|
+
alias_method :[], :with
|
24
|
+
|
25
|
+
def curry
|
26
|
+
Curried.new(operation_class, self)
|
27
|
+
end
|
28
28
|
|
29
29
|
def call(...)
|
30
|
-
prepared =
|
30
|
+
prepared = with(...)
|
31
31
|
return prepared.operation.call if prepared.is_a?(Prepared)
|
32
|
-
raise
|
32
|
+
raise MissingParameterError, "Cannot call PartiallyApplied operation #{operation_class.name} (key: #{operation_class.name}), are you expecting it to be Prepared?"
|
33
|
+
end
|
34
|
+
|
35
|
+
def operation
|
36
|
+
raise MissingParameterError, "Cannot instantiate Operation #{operation_class.name} (key: #{operation_class.name}), as it is only partially applied."
|
37
|
+
end
|
38
|
+
|
39
|
+
def prepared?
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_proc
|
44
|
+
method(:call).to_proc
|
45
|
+
end
|
46
|
+
|
47
|
+
def deconstruct
|
48
|
+
positional_args + keyword_args.values
|
49
|
+
end
|
50
|
+
|
51
|
+
def deconstruct_keys(keys)
|
52
|
+
h = keyword_args.dup
|
53
|
+
positional_args.each_with_index { |v, i| h[positional_parameters[i]] = v }
|
54
|
+
keys ? h.slice(*keys) : h
|
55
|
+
end
|
56
|
+
|
57
|
+
attr_reader :positional_args, :keyword_args
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
attr_reader :operation_class
|
62
|
+
|
63
|
+
def required_positional_parameters
|
64
|
+
@required_positional_parameters ||= operation_class.required_positional_parameters
|
65
|
+
end
|
66
|
+
|
67
|
+
def required_keyword_parameters
|
68
|
+
@required_keyword_parameters ||= operation_class.required_keyword_parameters
|
69
|
+
end
|
70
|
+
|
71
|
+
def positional_parameters
|
72
|
+
@positional_parameters ||= operation_class.positional_parameters
|
73
|
+
end
|
74
|
+
|
75
|
+
def validate_positional_arg_count!(count)
|
76
|
+
if count > positional_parameters.size
|
77
|
+
raise ArgumentError, "Too many positional arguments provided for #{operation_class.name} (key: #{operation_class.name})"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def partially_applied?(all_positional, all_kw_args)
|
82
|
+
missing_positional = required_positional_parameters.size - all_positional.size
|
83
|
+
missing_keys = required_keyword_parameters - all_kw_args.keys
|
84
|
+
|
85
|
+
missing_positional > 0 || missing_keys.size > 0
|
33
86
|
end
|
34
87
|
end
|
35
88
|
end
|
data/lib/typed_operation.rb
CHANGED
@@ -1,11 +1,28 @@
|
|
1
|
+
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.2.0")
|
2
|
+
require "polyfill-data"
|
3
|
+
end
|
4
|
+
|
5
|
+
require "literal"
|
6
|
+
|
1
7
|
require "typed_operation/version"
|
2
|
-
require "typed_operation/railtie"
|
8
|
+
require "typed_operation/railtie" if defined?(Rails::Railtie)
|
9
|
+
require "typed_operation/operations/introspection"
|
10
|
+
require "typed_operation/operations/parameters"
|
11
|
+
require "typed_operation/operations/partial_application"
|
12
|
+
require "typed_operation/operations/callable"
|
13
|
+
require "typed_operation/operations/lifecycle"
|
14
|
+
require "typed_operation/operations/deconstruct"
|
15
|
+
require "typed_operation/operations/attribute_builder"
|
16
|
+
require "typed_operation/curried"
|
17
|
+
require "typed_operation/immutable_base"
|
3
18
|
require "typed_operation/base"
|
4
19
|
require "typed_operation/partially_applied"
|
5
20
|
require "typed_operation/prepared"
|
6
21
|
|
7
22
|
module TypedOperation
|
8
23
|
class InvalidOperationError < StandardError; end
|
24
|
+
|
9
25
|
class MissingParameterError < ArgumentError; end
|
26
|
+
|
10
27
|
class ParameterError < TypeError; end
|
11
28
|
end
|
metadata
CHANGED
@@ -1,63 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: typed_operation
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0.pre2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen Ierodiaconou
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
12
|
-
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: rails
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '6.0'
|
20
|
-
- - "<"
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: '8.0'
|
23
|
-
type: :runtime
|
24
|
-
prerelease: false
|
25
|
-
version_requirements: !ruby/object:Gem::Requirement
|
26
|
-
requirements:
|
27
|
-
- - ">="
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
version: '6.0'
|
30
|
-
- - "<"
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: '8.0'
|
33
|
-
- !ruby/object:Gem::Dependency
|
34
|
-
name: vident-typed
|
35
|
-
requirement: !ruby/object:Gem::Requirement
|
36
|
-
requirements:
|
37
|
-
- - "~>"
|
38
|
-
- !ruby/object:Gem::Version
|
39
|
-
version: 0.1.0
|
40
|
-
type: :runtime
|
41
|
-
prerelease: false
|
42
|
-
version_requirements: !ruby/object:Gem::Requirement
|
43
|
-
requirements:
|
44
|
-
- - "~>"
|
45
|
-
- !ruby/object:Gem::Version
|
46
|
-
version: 0.1.0
|
47
|
-
- !ruby/object:Gem::Dependency
|
48
|
-
name: dry-initializer
|
49
|
-
requirement: !ruby/object:Gem::Requirement
|
50
|
-
requirements:
|
51
|
-
- - "~>"
|
52
|
-
- !ruby/object:Gem::Version
|
53
|
-
version: '3.0'
|
54
|
-
type: :runtime
|
55
|
-
prerelease: false
|
56
|
-
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
requirements:
|
58
|
-
- - "~>"
|
59
|
-
- !ruby/object:Gem::Version
|
60
|
-
version: '3.0'
|
11
|
+
date: 2023-08-22 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
61
13
|
description: TypedOperation is a command pattern implementation where inputs can be
|
62
14
|
defined with runtime type checks. Operations can be partially applied.
|
63
15
|
email:
|
@@ -79,6 +31,15 @@ files:
|
|
79
31
|
- lib/tasks/typed_operation_tasks.rake
|
80
32
|
- lib/typed_operation.rb
|
81
33
|
- lib/typed_operation/base.rb
|
34
|
+
- lib/typed_operation/curried.rb
|
35
|
+
- lib/typed_operation/immutable_base.rb
|
36
|
+
- lib/typed_operation/operations/attribute_builder.rb
|
37
|
+
- lib/typed_operation/operations/callable.rb
|
38
|
+
- lib/typed_operation/operations/deconstruct.rb
|
39
|
+
- lib/typed_operation/operations/introspection.rb
|
40
|
+
- lib/typed_operation/operations/lifecycle.rb
|
41
|
+
- lib/typed_operation/operations/parameters.rb
|
42
|
+
- lib/typed_operation/operations/partial_application.rb
|
82
43
|
- lib/typed_operation/partially_applied.rb
|
83
44
|
- lib/typed_operation/prepared.rb
|
84
45
|
- lib/typed_operation/railtie.rb
|
@@ -97,14 +58,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
97
58
|
requirements:
|
98
59
|
- - ">="
|
99
60
|
- !ruby/object:Gem::Version
|
100
|
-
version: '
|
61
|
+
version: '3.1'
|
101
62
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
63
|
requirements:
|
103
|
-
- - "
|
64
|
+
- - ">"
|
104
65
|
- !ruby/object:Gem::Version
|
105
|
-
version:
|
66
|
+
version: 1.3.1
|
106
67
|
requirements: []
|
107
|
-
rubygems_version: 3.4.
|
68
|
+
rubygems_version: 3.4.18
|
108
69
|
signing_key:
|
109
70
|
specification_version: 4
|
110
71
|
summary: TypedOperation is a command pattern implementation
|