typed_operation 1.0.0.pre1 → 1.0.0.pre3
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 +108 -19
- data/Rakefile +15 -1
- data/lib/generators/templates/operation.rb +2 -2
- data/lib/typed_operation/base.rb +14 -95
- data/lib/typed_operation/immutable_base.rb +14 -0
- data/lib/typed_operation/operations/attribute_builder.rb +81 -0
- data/lib/typed_operation/operations/callable.rb +23 -0
- data/lib/typed_operation/operations/deconstruct.rb +16 -0
- data/lib/typed_operation/operations/executable.rb +29 -0
- data/lib/typed_operation/operations/introspection.rb +38 -0
- data/lib/typed_operation/operations/lifecycle.rb +12 -0
- data/lib/typed_operation/operations/parameters.rb +31 -0
- data/lib/typed_operation/operations/partial_application.rb +16 -0
- data/lib/typed_operation/version.rb +1 -1
- data/lib/typed_operation.rb +11 -2
- metadata +11 -4
- data/lib/typed_operation/attribute_builder.rb +0 -73
- data/lib/typed_operation/nilable_type.rb +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c16e335815316d0cc264b143755c63556fa1a016403198e83aeeeeb067307d4a
|
4
|
+
data.tar.gz: ddd2475fd3bb2898245b9430136dd881b3ef52fe92abbd7ffe4230e243398aad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1134e7f162febf03703dc2b36a60f799779a9170f6bf75888df7ffe58245b73b3f66b3b434509d0444537ed818c7eb5d3946df61cbe0cf485e4cd02b4bdbc2de
|
7
|
+
data.tar.gz: 1ceee6f8f37b538cf25b9011e188c79d49be4618f23b12b314fbb4295affffbf25173d61dcbfebf20589c2aad2a85a8531636adf3e4870286ab8b790351322d7
|
data/README.md
CHANGED
@@ -49,16 +49,28 @@ class ShelveBookOperation < ::TypedOperation::Base
|
|
49
49
|
|
50
50
|
named_param :category, String, default: "unknown".freeze
|
51
51
|
|
52
|
-
#
|
52
|
+
# optional hook called when the operation is initialized, and after the parameters have been set
|
53
53
|
def prepare
|
54
54
|
raise ArgumentError, "ISBN is invalid" unless valid_isbn?
|
55
55
|
end
|
56
56
|
|
57
|
-
#
|
58
|
-
def
|
57
|
+
# optionally hook in before execution ... and call super to allow subclasses to hook in too
|
58
|
+
def before_execute_operation
|
59
|
+
# ...
|
60
|
+
super
|
61
|
+
end
|
62
|
+
|
63
|
+
# The 'work' of the operation, this is the main body of the operation and must be implemented
|
64
|
+
def perform
|
59
65
|
"Put away '#{title}' by author ID #{author_id}#{shelf_code ? " on shelf #{shelf_code}" : "" }"
|
60
66
|
end
|
61
67
|
|
68
|
+
# optionally hook in after execution ... and call super to allow subclasses to hook in too
|
69
|
+
def after_execute_operation(result)
|
70
|
+
# ...
|
71
|
+
super
|
72
|
+
end
|
73
|
+
|
62
74
|
private
|
63
75
|
|
64
76
|
def valid_isbn?
|
@@ -92,13 +104,15 @@ shelve.call(author_id: "1", isbn: false)
|
|
92
104
|
|
93
105
|
### Partially applying parameters
|
94
106
|
|
107
|
+
Operations can also be partially applied and curried:
|
108
|
+
|
95
109
|
```ruby
|
96
110
|
class TestOperation < ::TypedOperation::Base
|
97
111
|
param :foo, String, positional: true
|
98
112
|
param :bar, String
|
99
113
|
param :baz, String, &:to_s
|
100
114
|
|
101
|
-
def
|
115
|
+
def perform = "It worked! (#{foo}, #{bar}, #{baz})"
|
102
116
|
end
|
103
117
|
|
104
118
|
# Invoking the operation directly
|
@@ -151,21 +165,76 @@ TestOperation.with("1").with(bar: "2").with(baz: 3).operation
|
|
151
165
|
|
152
166
|
## Documentation
|
153
167
|
|
154
|
-
### Create an operation (subclass `TypedOperation::Base`)
|
168
|
+
### Create an operation (subclass `TypedOperation::Base` or `TypedOperation::ImmutableBase`)
|
169
|
+
|
170
|
+
Create an operation by subclassing `TypedOperation::Base` or `TypedOperation::ImmutableBase` and specifying the parameters the operation requires.
|
155
171
|
|
156
|
-
|
172
|
+
- `TypedOperation::Base` (uses `Literal::Struct`) is the parent class for an operation where the arguments are potentially mutable (ie not frozen).
|
173
|
+
No attribute writer methods are defined, so the arguments can not be changed after initialization, but the values passed in are not guaranteed to be frozen.
|
174
|
+
- `TypedOperation::ImmutableBase` (uses `Literal::Data`) is the parent class for an operation where the arguments are immutable (frozen on initialization),
|
175
|
+
thus giving a somewhat stronger immutability guarantee (ie that the operation does not mutate its arguments).
|
157
176
|
|
158
|
-
The subclass must implement the `#
|
177
|
+
The subclass must implement the `#perform` method which is where the operations main work is done.
|
159
178
|
|
160
179
|
The operation can also implement:
|
161
180
|
|
162
181
|
- `#prepare` - called when the operation is initialized, and after the parameters have been set
|
182
|
+
- `#before_execute_operation` - optionally hook in before execution ... and call super to allow subclasses to hook in too
|
183
|
+
- `#after_execute_operation` - optionally hook in after execution ... and call super to allow subclasses to hook in too
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
# optionally hook in before execution...
|
187
|
+
def before_execute_operation
|
188
|
+
# Remember to call super
|
189
|
+
super
|
190
|
+
end
|
191
|
+
|
192
|
+
def perform
|
193
|
+
# ... implement me!
|
194
|
+
end
|
195
|
+
|
196
|
+
# optionally hook in after execution...
|
197
|
+
def after_execute_operation(result)
|
198
|
+
# Remember to call super, note the result is passed in and the return value of this method is the result of the operation
|
199
|
+
# thus allowing you to modify the result if you wish
|
200
|
+
super
|
201
|
+
end
|
202
|
+
```
|
163
203
|
|
164
204
|
### Specifying parameters (using `.param`)
|
165
205
|
|
166
206
|
Parameters are specified using the provided class methods (`.positional_param` and `.named_param`),
|
167
207
|
or using the underlying `.param` method.
|
168
208
|
|
209
|
+
Types are specified using the `literal` gem. In many cases this simply means providing the class of the
|
210
|
+
expected type, but there are also some other useful types provided by `literal` (eg `Union`).
|
211
|
+
|
212
|
+
These can be either accessed via the `Literal` module, eg `Literal::Types::BooleanType`:
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
class MyOperation < ::TypedOperation::Base
|
216
|
+
param :name, String
|
217
|
+
param :age, Integer, optional: true
|
218
|
+
param :choices, Literal::Types::ArrayType.new(String)
|
219
|
+
param :chose, Literal::Types::BooleanType
|
220
|
+
end
|
221
|
+
|
222
|
+
MyOperation.new(name: "bob", choices: ["st"], chose: true)
|
223
|
+
```
|
224
|
+
|
225
|
+
or by including the `Literal::Types` module into your operation class, and using the aliases provided:
|
226
|
+
|
227
|
+
```ruby
|
228
|
+
class MyOperation < ::TypedOperation::Base
|
229
|
+
include Literal::Types
|
230
|
+
|
231
|
+
param :name, String
|
232
|
+
param :age, _Nilable(Integer) # optional can also be specifed using `.optional`
|
233
|
+
param :choices, _Array(String)
|
234
|
+
param :chose, _Boolean
|
235
|
+
end
|
236
|
+
```
|
237
|
+
|
169
238
|
Type constraints can be modified to make the parameter optional using `.optional`.
|
170
239
|
|
171
240
|
#### Your own aliases
|
@@ -223,7 +292,7 @@ class MyOperation < ::TypedOperation::Base
|
|
223
292
|
# Or alternatively => `param :name, String, positional: true`
|
224
293
|
positional_param :age, Integer, default: -> { 0 }
|
225
294
|
|
226
|
-
def
|
295
|
+
def perform
|
227
296
|
puts "Hello #{name} (#{age})"
|
228
297
|
end
|
229
298
|
end
|
@@ -254,7 +323,7 @@ class MyOperation < ::TypedOperation::Base
|
|
254
323
|
# Or alternatively => `param :name, String`
|
255
324
|
named_param :age, Integer, default: -> { 0 }
|
256
325
|
|
257
|
-
def
|
326
|
+
def perform
|
258
327
|
puts "Hello #{name} (#{age})"
|
259
328
|
end
|
260
329
|
end
|
@@ -275,7 +344,7 @@ class MyOperation < ::TypedOperation::Base
|
|
275
344
|
positional_param :name, String
|
276
345
|
named_param :age, Integer, default: -> { 0 }
|
277
346
|
|
278
|
-
def
|
347
|
+
def perform
|
279
348
|
puts "Hello #{name} (#{age})"
|
280
349
|
end
|
281
350
|
end
|
@@ -319,7 +388,7 @@ You can specify a block after a parameter definition to coerce the argument valu
|
|
319
388
|
|
320
389
|
```ruby
|
321
390
|
param :name, String, &:to_s
|
322
|
-
param :choice,
|
391
|
+
param :choice, Literal::Types::BooleanType do |v|
|
323
392
|
v == "y"
|
324
393
|
end
|
325
394
|
```
|
@@ -366,9 +435,10 @@ An operation can be invoked by:
|
|
366
435
|
|
367
436
|
- instantiating it with at least required params and then calling the `#call` method on the instance
|
368
437
|
- 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.
|
438
|
+
method on `TypedOperation::Prepared` can be used to instantiate and call the operation.
|
370
439
|
- once an operation is curried, the `#call` method on last TypedOperation::Curried in the chain will invoke the operation
|
371
440
|
- calling `#call` on a partially applied operation and passing in any remaining required parameters
|
441
|
+
- calling `#execute_operation` on an operation instance (this is the method that is called by `#call`)
|
372
442
|
|
373
443
|
See the many examples in this document.
|
374
444
|
|
@@ -487,6 +557,26 @@ class ApplicationOperation < ::TypedOperation::Base
|
|
487
557
|
end
|
488
558
|
```
|
489
559
|
|
560
|
+
### Using with Action Policy (`action_policy` gem)
|
561
|
+
|
562
|
+
Base you `ApplicationOperation` on the following:
|
563
|
+
|
564
|
+
```ruby
|
565
|
+
class ApplicationOperation < ::TypedOperation::Base
|
566
|
+
# ...
|
567
|
+
include ActionPolicy::Behaviour
|
568
|
+
|
569
|
+
# ...
|
570
|
+
|
571
|
+
param :initiator, ::User
|
572
|
+
authorize :initiator
|
573
|
+
# Or
|
574
|
+
# param :initiator, optional(::User)
|
575
|
+
# authorize :initiator, through: {optional: true}
|
576
|
+
|
577
|
+
# ...
|
578
|
+
end
|
579
|
+
```
|
490
580
|
### Using with `literal` monads
|
491
581
|
|
492
582
|
You can use the `literal` gem to provide a `Result` type for your operations.
|
@@ -496,30 +586,29 @@ class MyOperation < ::TypedOperation::Base
|
|
496
586
|
param :account_name, String
|
497
587
|
param :owner, String
|
498
588
|
|
499
|
-
def
|
589
|
+
def perform
|
500
590
|
create_account.bind do |account|
|
501
|
-
associate_owner(account).
|
502
|
-
account
|
503
|
-
end
|
591
|
+
associate_owner(account).map { account }
|
504
592
|
end
|
505
593
|
end
|
506
594
|
|
507
595
|
private
|
508
596
|
|
509
597
|
def create_account
|
510
|
-
#
|
598
|
+
# ...
|
599
|
+
# Literal::Failure.new(:cant_create_account)
|
511
600
|
Literal::Success.new(account_name)
|
512
601
|
end
|
513
602
|
|
514
603
|
def associate_owner(account)
|
515
604
|
# ...
|
516
605
|
Literal::Failure.new(:cant_associate_owner)
|
606
|
+
# Literal::Success.new("ok")
|
517
607
|
end
|
518
608
|
end
|
519
609
|
|
520
610
|
MyOperation.new(account_name: "foo", owner: "bar").call
|
521
611
|
# => Literal::Failure(:cant_associate_owner)
|
522
|
-
|
523
612
|
```
|
524
613
|
|
525
614
|
### Using with `Dry::Monads`
|
@@ -534,7 +623,7 @@ class MyOperation < ::TypedOperation::Base
|
|
534
623
|
param :account_name, String
|
535
624
|
param :owner, ::Owner
|
536
625
|
|
537
|
-
def
|
626
|
+
def perform
|
538
627
|
account = yield create_account(account_name)
|
539
628
|
yield associate_owner(account, owner)
|
540
629
|
|
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]
|
@@ -14,7 +14,7 @@ module <%= namespace_name %>
|
|
14
14
|
# Prepare...
|
15
15
|
end
|
16
16
|
|
17
|
-
def
|
17
|
+
def perform
|
18
18
|
# Perform...
|
19
19
|
"Hello World!"
|
20
20
|
end
|
@@ -33,7 +33,7 @@ class <%= name %> < ::ApplicationOperation
|
|
33
33
|
# Prepare...
|
34
34
|
end
|
35
35
|
|
36
|
-
def
|
36
|
+
def perform
|
37
37
|
# Perform...
|
38
38
|
"Hello World!"
|
39
39
|
end
|
data/lib/typed_operation/base.rb
CHANGED
@@ -1,106 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "literal"
|
4
|
-
|
5
3
|
module TypedOperation
|
6
|
-
class Base < Literal::
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
end
|
4
|
+
class Base < Literal::Struct
|
5
|
+
extend Operations::Introspection
|
6
|
+
extend Operations::Parameters
|
7
|
+
extend Operations::PartialApplication
|
11
8
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
def curry
|
18
|
-
Curried.new(self)
|
19
|
-
end
|
20
|
-
|
21
|
-
def to_proc
|
22
|
-
method(:call).to_proc
|
23
|
-
end
|
24
|
-
|
25
|
-
# Method to define parameters for your operation.
|
26
|
-
|
27
|
-
# Parameter for keyword argument, or a positional argument if you use positional: true
|
28
|
-
# Required, but you can set a default or use optional: true if you want optional
|
29
|
-
def param(name, signature = :any, **options, &converter)
|
30
|
-
AttributeBuilder.new(self, name, signature, options).define(&converter)
|
31
|
-
end
|
32
|
-
|
33
|
-
# Alternative DSL
|
34
|
-
|
35
|
-
# Parameter for positional argument
|
36
|
-
def positional_param(name, signature = :any, **options, &converter)
|
37
|
-
param(name, signature, **options.merge(positional: true), &converter)
|
38
|
-
end
|
39
|
-
|
40
|
-
# Parameter for a keyword or named argument
|
41
|
-
def named_param(name, signature = :any, **options, &converter)
|
42
|
-
param(name, signature, **options.merge(positional: false), &converter)
|
43
|
-
end
|
44
|
-
|
45
|
-
# Wrap a type signature in a NilableType meaning it is optional to TypedOperation
|
46
|
-
def optional(type_signature)
|
47
|
-
NilableType.new(type_signature)
|
48
|
-
end
|
49
|
-
|
50
|
-
# Introspection methods
|
51
|
-
|
52
|
-
def positional_parameters
|
53
|
-
literal_attributes.filter_map { |name, attribute| name if attribute.positional? }
|
54
|
-
end
|
9
|
+
include Operations::Lifecycle
|
10
|
+
include Operations::Callable
|
11
|
+
include Operations::Deconstruct
|
12
|
+
include Operations::Executable
|
55
13
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
def required_positional_parameters
|
61
|
-
required_parameters.filter_map { |name, attribute| name if attribute.positional? }
|
62
|
-
end
|
63
|
-
|
64
|
-
def required_keyword_parameters
|
65
|
-
required_parameters.filter_map { |name, attribute| name unless attribute.positional? }
|
66
|
-
end
|
67
|
-
|
68
|
-
def optional_positional_parameters
|
69
|
-
positional_parameters - required_positional_parameters
|
70
|
-
end
|
71
|
-
|
72
|
-
def optional_keyword_parameters
|
73
|
-
keyword_parameters - required_keyword_parameters
|
74
|
-
end
|
75
|
-
|
76
|
-
private
|
77
|
-
|
78
|
-
def required_parameters
|
79
|
-
literal_attributes.filter do |name, attribute|
|
80
|
-
attribute.default.nil? # Any optional parameters will have a default value/proc in their Literal::Attribute
|
81
|
-
end
|
14
|
+
class << self
|
15
|
+
def attribute(name, type, special = nil, reader: :public, writer: :public, positional: false, default: nil)
|
16
|
+
super(name, type, special, reader:, writer: false, positional:, default:)
|
82
17
|
end
|
83
18
|
end
|
84
19
|
|
85
|
-
def
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
def call
|
90
|
-
raise InvalidOperationError, "You must implement #call"
|
91
|
-
end
|
92
|
-
|
93
|
-
def to_proc
|
94
|
-
method(:call).to_proc
|
95
|
-
end
|
96
|
-
|
97
|
-
def deconstruct
|
98
|
-
attributes.values
|
99
|
-
end
|
100
|
-
|
101
|
-
def deconstruct_keys(keys)
|
102
|
-
h = attributes.to_h
|
103
|
-
keys ? h.slice(*keys) : h
|
20
|
+
def with(...)
|
21
|
+
# copy to new operation with new attrs
|
22
|
+
self.class.new(**attributes.merge(...))
|
104
23
|
end
|
105
24
|
end
|
106
25
|
end
|
@@ -0,0 +1,14 @@
|
|
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::Lifecycle
|
10
|
+
include Operations::Callable
|
11
|
+
include Operations::Deconstruct
|
12
|
+
include Operations::Executable
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,81 @@
|
|
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
|
+
# If nilable, then converter should not attempt to call the type converter block if the value is nil
|
21
|
+
coerce_by = if type_nilable? && converter
|
22
|
+
->(v) { (v == Literal::Null || v.nil?) ? v : converter.call(v) }
|
23
|
+
else
|
24
|
+
converter
|
25
|
+
end
|
26
|
+
@typed_operation.attribute(
|
27
|
+
@name,
|
28
|
+
@signature,
|
29
|
+
default: default_value_for_literal,
|
30
|
+
positional: @positional,
|
31
|
+
reader: @reader,
|
32
|
+
&coerce_by
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def prepare_type_signature_for_literal
|
39
|
+
@signature = Literal::Types::NilableType.new(@signature) if needs_to_be_nilable?
|
40
|
+
union_with_nil_to_support_nil_default
|
41
|
+
validate_positional_order_params! if @positional
|
42
|
+
end
|
43
|
+
|
44
|
+
# If already wrapped in a Nilable then don't wrap again
|
45
|
+
def needs_to_be_nilable?
|
46
|
+
@optional && !type_nilable?
|
47
|
+
end
|
48
|
+
|
49
|
+
def type_nilable?
|
50
|
+
@signature.is_a?(Literal::Types::NilableType)
|
51
|
+
end
|
52
|
+
|
53
|
+
def union_with_nil_to_support_nil_default
|
54
|
+
@signature = Literal::Union.new(@signature, NilClass) if has_default_value_nil?
|
55
|
+
end
|
56
|
+
|
57
|
+
def has_default_value_nil?
|
58
|
+
default_provided? && @default.nil?
|
59
|
+
end
|
60
|
+
|
61
|
+
def validate_positional_order_params!
|
62
|
+
# Optional ones can always be added after required ones, or before any others, but required ones must be first
|
63
|
+
unless type_nilable? || @typed_operation.optional_positional_parameters.empty?
|
64
|
+
raise ParameterError, "Cannot define required positional parameter '#{@name}' after optional positional parameters"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def default_provided?
|
69
|
+
@default_key
|
70
|
+
end
|
71
|
+
|
72
|
+
def default_value_for_literal
|
73
|
+
if has_default_value_nil? || type_nilable?
|
74
|
+
-> {}
|
75
|
+
else
|
76
|
+
@default
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,23 @@
|
|
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
|
+
end
|
22
|
+
end
|
23
|
+
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,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedOperation
|
4
|
+
module Operations
|
5
|
+
module Executable
|
6
|
+
def call
|
7
|
+
execute_operation
|
8
|
+
end
|
9
|
+
|
10
|
+
def execute_operation
|
11
|
+
before_execute_operation
|
12
|
+
retval = perform
|
13
|
+
after_execute_operation(retval)
|
14
|
+
end
|
15
|
+
|
16
|
+
def before_execute_operation
|
17
|
+
# noop
|
18
|
+
end
|
19
|
+
|
20
|
+
def after_execute_operation(retval)
|
21
|
+
retval
|
22
|
+
end
|
23
|
+
|
24
|
+
def perform
|
25
|
+
raise InvalidOperationError, "Operation #{self.class} does not implement #perform"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
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,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedOperation
|
4
|
+
module Operations
|
5
|
+
module Lifecycle
|
6
|
+
# This is called by Literal on initialization of underlying Struct/Data
|
7
|
+
def after_initialization
|
8
|
+
prepare if respond_to?(:prepare)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
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
|
data/lib/typed_operation.rb
CHANGED
@@ -2,11 +2,20 @@ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.2.0")
|
|
2
2
|
require "polyfill-data"
|
3
3
|
end
|
4
4
|
|
5
|
+
require "literal"
|
6
|
+
|
5
7
|
require "typed_operation/version"
|
6
8
|
require "typed_operation/railtie" if defined?(Rails::Railtie)
|
7
|
-
require "typed_operation/
|
8
|
-
require "typed_operation/
|
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/operations/executable"
|
9
17
|
require "typed_operation/curried"
|
18
|
+
require "typed_operation/immutable_base"
|
10
19
|
require "typed_operation/base"
|
11
20
|
require "typed_operation/partially_applied"
|
12
21
|
require "typed_operation/prepared"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: typed_operation
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.
|
4
|
+
version: 1.0.0.pre3
|
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-08-
|
11
|
+
date: 2023-08-25 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: TypedOperation is a command pattern implementation where inputs can be
|
14
14
|
defined with runtime type checks. Operations can be partially applied.
|
@@ -30,10 +30,17 @@ files:
|
|
30
30
|
- lib/generators/typed_operation_generator.rb
|
31
31
|
- lib/tasks/typed_operation_tasks.rake
|
32
32
|
- lib/typed_operation.rb
|
33
|
-
- lib/typed_operation/attribute_builder.rb
|
34
33
|
- lib/typed_operation/base.rb
|
35
34
|
- lib/typed_operation/curried.rb
|
36
|
-
- lib/typed_operation/
|
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/executable.rb
|
40
|
+
- lib/typed_operation/operations/introspection.rb
|
41
|
+
- lib/typed_operation/operations/lifecycle.rb
|
42
|
+
- lib/typed_operation/operations/parameters.rb
|
43
|
+
- lib/typed_operation/operations/partial_application.rb
|
37
44
|
- lib/typed_operation/partially_applied.rb
|
38
45
|
- lib/typed_operation/prepared.rb
|
39
46
|
- lib/typed_operation/railtie.rb
|
@@ -1,73 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module TypedOperation
|
4
|
-
class AttributeBuilder
|
5
|
-
def initialize(typed_operation, parameter_name, type_signature, options)
|
6
|
-
@typed_operation = typed_operation
|
7
|
-
@name = parameter_name
|
8
|
-
@signature = type_signature
|
9
|
-
@optional = options[:optional]
|
10
|
-
@positional = options[:positional]
|
11
|
-
@reader = options[:reader] || :public
|
12
|
-
@default_key = options.key?(:default)
|
13
|
-
@default = options[:default]
|
14
|
-
|
15
|
-
prepare_type_signature_for_literal
|
16
|
-
end
|
17
|
-
|
18
|
-
def define(&converter)
|
19
|
-
@typed_operation.attribute(
|
20
|
-
@name,
|
21
|
-
@signature,
|
22
|
-
default: default_value_for_literal,
|
23
|
-
positional: @positional,
|
24
|
-
reader: @reader,
|
25
|
-
&converter
|
26
|
-
)
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
def prepare_type_signature_for_literal
|
32
|
-
@signature = NilableType.new(@signature) if needs_to_be_nilable?
|
33
|
-
union_with_nil_to_support_nil_default
|
34
|
-
validate_positional_order_params! if @positional
|
35
|
-
end
|
36
|
-
|
37
|
-
# If already wrapped in a Nilable then don't wrap again
|
38
|
-
def needs_to_be_nilable?
|
39
|
-
@optional && !type_nilable?
|
40
|
-
end
|
41
|
-
|
42
|
-
def type_nilable?
|
43
|
-
@signature.is_a?(NilableType)
|
44
|
-
end
|
45
|
-
|
46
|
-
def union_with_nil_to_support_nil_default
|
47
|
-
@signature = Literal::Union.new(@signature, NilClass) if has_default_value_nil?
|
48
|
-
end
|
49
|
-
|
50
|
-
def has_default_value_nil?
|
51
|
-
default_provided? && @default.nil?
|
52
|
-
end
|
53
|
-
|
54
|
-
def validate_positional_order_params!
|
55
|
-
# Optional ones can always be added after required ones, or before any others, but required ones must be first
|
56
|
-
unless type_nilable? || @typed_operation.optional_positional_parameters.empty?
|
57
|
-
raise ParameterError, "Cannot define required positional parameter '#{@name}' after optional positional parameters"
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
def default_provided?
|
62
|
-
@default_key
|
63
|
-
end
|
64
|
-
|
65
|
-
def default_value_for_literal
|
66
|
-
if has_default_value_nil? || type_nilable?
|
67
|
-
-> {}
|
68
|
-
else
|
69
|
-
@default
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|