typed_operation 1.0.0.pre1 → 1.0.0.pre3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|