typed_operation 1.0.0.beta1 → 1.0.0.pre1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +20 -241
- data/Rakefile +3 -0
- data/lib/generators/templates/operation.rb +2 -2
- data/lib/tasks/typed_operation_tasks.rake +4 -0
- data/lib/typed_operation/attribute_builder.rb +73 -0
- data/lib/typed_operation/base.rb +95 -14
- data/lib/typed_operation/nilable_type.rb +11 -0
- data/lib/typed_operation/version.rb +1 -1
- data/lib/typed_operation.rb +2 -11
- metadata +10 -17
- data/lib/typed_operation/action_policy_auth.rb +0 -141
- data/lib/typed_operation/immutable_base.rb +0 -14
- data/lib/typed_operation/operations/attribute_builder.rb +0 -81
- data/lib/typed_operation/operations/callable.rb +0 -23
- data/lib/typed_operation/operations/deconstruct.rb +0 -16
- data/lib/typed_operation/operations/executable.rb +0 -29
- data/lib/typed_operation/operations/introspection.rb +0 -38
- data/lib/typed_operation/operations/lifecycle.rb +0 -12
- data/lib/typed_operation/operations/parameters.rb +0 -31
- data/lib/typed_operation/operations/partial_application.rb +0 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff925e2cc5ce03a696a209e0ca61130bff94ddb6ce28f2670ebaaae18f1968ba
|
4
|
+
data.tar.gz: b14a0952694653f918135a57180c8c7746aeffcb14385d17aeef7a1283726e99
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0cb23aed491058bdefba04d75661ec4cb5110a2491ba9d4b3afc929d41e1863a09b9ac09b41bdbd6306856af6d5552f2c86c3057b53df3076d0674a9c1feafda
|
7
|
+
data.tar.gz: 143f6ba5a00d5cabadec3f7f326dc7a724fdd2666ca0dffa326cacf9f9af2f4dce797f77a727468cd919eb42a4b6de530105d6866e49797997c10ff5a36db9fe
|
data/README.md
CHANGED
@@ -6,12 +6,7 @@ Inputs to the operation are specified as typed attributes (uses [`literal`](http
|
|
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
|
-
**Note the version described here (~ 1.0.0) is
|
10
|
-
|
11
|
-
```ruby
|
12
|
-
gem "literal", github: "joeldrapper/literal", branch: "main"
|
13
|
-
gem "typed_operation", "~> 1.0.0.beta1"
|
14
|
-
```
|
9
|
+
**Note the version described here (~ 1.0.0) is not yet released on Rubygems, it is waiting for a release of `literal`)**
|
15
10
|
|
16
11
|
## Features
|
17
12
|
|
@@ -54,28 +49,16 @@ class ShelveBookOperation < ::TypedOperation::Base
|
|
54
49
|
|
55
50
|
named_param :category, String, default: "unknown".freeze
|
56
51
|
|
57
|
-
#
|
52
|
+
# to setup (optional)
|
58
53
|
def prepare
|
59
54
|
raise ArgumentError, "ISBN is invalid" unless valid_isbn?
|
60
55
|
end
|
61
56
|
|
62
|
-
#
|
63
|
-
def
|
64
|
-
# ...
|
65
|
-
super
|
66
|
-
end
|
67
|
-
|
68
|
-
# The 'work' of the operation, this is the main body of the operation and must be implemented
|
69
|
-
def perform
|
57
|
+
# The 'work' of the operation
|
58
|
+
def call
|
70
59
|
"Put away '#{title}' by author ID #{author_id}#{shelf_code ? " on shelf #{shelf_code}" : "" }"
|
71
60
|
end
|
72
61
|
|
73
|
-
# optionally hook in after execution ... and call super to allow subclasses to hook in too
|
74
|
-
def after_execute_operation(result)
|
75
|
-
# ...
|
76
|
-
super
|
77
|
-
end
|
78
|
-
|
79
62
|
private
|
80
63
|
|
81
64
|
def valid_isbn?
|
@@ -109,15 +92,13 @@ shelve.call(author_id: "1", isbn: false)
|
|
109
92
|
|
110
93
|
### Partially applying parameters
|
111
94
|
|
112
|
-
Operations can also be partially applied and curried:
|
113
|
-
|
114
95
|
```ruby
|
115
96
|
class TestOperation < ::TypedOperation::Base
|
116
97
|
param :foo, String, positional: true
|
117
98
|
param :bar, String
|
118
99
|
param :baz, String, &:to_s
|
119
100
|
|
120
|
-
def
|
101
|
+
def call = "It worked! (#{foo}, #{bar}, #{baz})"
|
121
102
|
end
|
122
103
|
|
123
104
|
# Invoking the operation directly
|
@@ -170,76 +151,21 @@ TestOperation.with("1").with(bar: "2").with(baz: 3).operation
|
|
170
151
|
|
171
152
|
## Documentation
|
172
153
|
|
173
|
-
### Create an operation (subclass `TypedOperation::Base`
|
154
|
+
### Create an operation (subclass `TypedOperation::Base`)
|
174
155
|
|
175
|
-
Create an operation by subclassing `TypedOperation::Base`
|
156
|
+
Create an operation by subclassing `TypedOperation::Base` and specifying the parameters the operation requires.
|
176
157
|
|
177
|
-
|
178
|
-
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.
|
179
|
-
- `TypedOperation::ImmutableBase` (uses `Literal::Data`) is the parent class for an operation where the arguments are immutable (frozen on initialization),
|
180
|
-
thus giving a somewhat stronger immutability guarantee (ie that the operation does not mutate its arguments).
|
181
|
-
|
182
|
-
The subclass must implement the `#perform` method which is where the operations main work is done.
|
158
|
+
The subclass must implement the `#call` method which is where the operations main work is done.
|
183
159
|
|
184
160
|
The operation can also implement:
|
185
161
|
|
186
162
|
- `#prepare` - called when the operation is initialized, and after the parameters have been set
|
187
|
-
- `#before_execute_operation` - optionally hook in before execution ... and call super to allow subclasses to hook in too
|
188
|
-
- `#after_execute_operation` - optionally hook in after execution ... and call super to allow subclasses to hook in too
|
189
|
-
|
190
|
-
```ruby
|
191
|
-
# optionally hook in before execution...
|
192
|
-
def before_execute_operation
|
193
|
-
# Remember to call super
|
194
|
-
super
|
195
|
-
end
|
196
|
-
|
197
|
-
def perform
|
198
|
-
# ... implement me!
|
199
|
-
end
|
200
|
-
|
201
|
-
# optionally hook in after execution...
|
202
|
-
def after_execute_operation(result)
|
203
|
-
# Remember to call super, note the result is passed in and the return value of this method is the result of the operation
|
204
|
-
# thus allowing you to modify the result if you wish
|
205
|
-
super
|
206
|
-
end
|
207
|
-
```
|
208
163
|
|
209
164
|
### Specifying parameters (using `.param`)
|
210
165
|
|
211
166
|
Parameters are specified using the provided class methods (`.positional_param` and `.named_param`),
|
212
167
|
or using the underlying `.param` method.
|
213
168
|
|
214
|
-
Types are specified using the `literal` gem. In many cases this simply means providing the class of the
|
215
|
-
expected type, but there are also some other useful types provided by `literal` (eg `Union`).
|
216
|
-
|
217
|
-
These can be either accessed via the `Literal` module, eg `Literal::Types::BooleanType`:
|
218
|
-
|
219
|
-
```ruby
|
220
|
-
class MyOperation < ::TypedOperation::Base
|
221
|
-
param :name, String
|
222
|
-
param :age, Integer, optional: true
|
223
|
-
param :choices, Literal::Types::ArrayType.new(String)
|
224
|
-
param :chose, Literal::Types::BooleanType
|
225
|
-
end
|
226
|
-
|
227
|
-
MyOperation.new(name: "bob", choices: ["st"], chose: true)
|
228
|
-
```
|
229
|
-
|
230
|
-
or by including the `Literal::Types` module into your operation class, and using the aliases provided:
|
231
|
-
|
232
|
-
```ruby
|
233
|
-
class MyOperation < ::TypedOperation::Base
|
234
|
-
include Literal::Types
|
235
|
-
|
236
|
-
param :name, String
|
237
|
-
param :age, _Nilable(Integer) # optional can also be specifed using `.optional`
|
238
|
-
param :choices, _Array(String)
|
239
|
-
param :chose, _Boolean
|
240
|
-
end
|
241
|
-
```
|
242
|
-
|
243
169
|
Type constraints can be modified to make the parameter optional using `.optional`.
|
244
170
|
|
245
171
|
#### Your own aliases
|
@@ -297,7 +223,7 @@ class MyOperation < ::TypedOperation::Base
|
|
297
223
|
# Or alternatively => `param :name, String, positional: true`
|
298
224
|
positional_param :age, Integer, default: -> { 0 }
|
299
225
|
|
300
|
-
def
|
226
|
+
def call
|
301
227
|
puts "Hello #{name} (#{age})"
|
302
228
|
end
|
303
229
|
end
|
@@ -328,7 +254,7 @@ class MyOperation < ::TypedOperation::Base
|
|
328
254
|
# Or alternatively => `param :name, String`
|
329
255
|
named_param :age, Integer, default: -> { 0 }
|
330
256
|
|
331
|
-
def
|
257
|
+
def call
|
332
258
|
puts "Hello #{name} (#{age})"
|
333
259
|
end
|
334
260
|
end
|
@@ -349,7 +275,7 @@ class MyOperation < ::TypedOperation::Base
|
|
349
275
|
positional_param :name, String
|
350
276
|
named_param :age, Integer, default: -> { 0 }
|
351
277
|
|
352
|
-
def
|
278
|
+
def call
|
353
279
|
puts "Hello #{name} (#{age})"
|
354
280
|
end
|
355
281
|
end
|
@@ -393,7 +319,7 @@ You can specify a block after a parameter definition to coerce the argument valu
|
|
393
319
|
|
394
320
|
```ruby
|
395
321
|
param :name, String, &:to_s
|
396
|
-
param :choice,
|
322
|
+
param :choice, Union(FalseClass, TrueClass) do |v|
|
397
323
|
v == "y"
|
398
324
|
end
|
399
325
|
```
|
@@ -440,10 +366,9 @@ An operation can be invoked by:
|
|
440
366
|
|
441
367
|
- instantiating it with at least required params and then calling the `#call` method on the instance
|
442
368
|
- once a partially applied operation has been prepared (all required parameters have been set), the call
|
443
|
-
method on
|
369
|
+
method on TypedOperation::Prepared can be used to instantiate and call the operation.
|
444
370
|
- once an operation is curried, the `#call` method on last TypedOperation::Curried in the chain will invoke the operation
|
445
371
|
- calling `#call` on a partially applied operation and passing in any remaining required parameters
|
446
|
-
- calling `#execute_operation` on an operation instance (this is the method that is called by `#call`)
|
447
372
|
|
448
373
|
See the many examples in this document.
|
449
374
|
|
@@ -562,153 +487,6 @@ class ApplicationOperation < ::TypedOperation::Base
|
|
562
487
|
end
|
563
488
|
```
|
564
489
|
|
565
|
-
### Using with Action Policy (`action_policy` gem)
|
566
|
-
|
567
|
-
Add `TypedOperation::ActionPolicyAuth` to your `ApplicationOperation` (first `require` the module):
|
568
|
-
|
569
|
-
```ruby
|
570
|
-
require "typed_operation/action_policy_auth"
|
571
|
-
|
572
|
-
class ApplicationOperation < ::TypedOperation::Base
|
573
|
-
# ...
|
574
|
-
include TypedOperation::ActionPolicyAuth
|
575
|
-
|
576
|
-
# You can specify a parameter to take the authorization context object, eg a user (can also be optional if some
|
577
|
-
# operations don't require authorization)
|
578
|
-
param :initiator, ::User # or optional(::User)
|
579
|
-
end
|
580
|
-
```
|
581
|
-
|
582
|
-
#### Specify the action with `.action_type`
|
583
|
-
|
584
|
-
Every operation must define what action_type it is, eg:
|
585
|
-
|
586
|
-
```ruby
|
587
|
-
class MyUpdateOperation < ApplicationOperation
|
588
|
-
action_type :update
|
589
|
-
end
|
590
|
-
```
|
591
|
-
|
592
|
-
Any symbol can be used as the `action_type` and this is by default used to determine which policy method to call.
|
593
|
-
|
594
|
-
#### Configuring auth with `.authorized_via`
|
595
|
-
|
596
|
-
`.authorized_via` is used to specify how to authorize the operation. You must specify the name of a parameter
|
597
|
-
for the policy authorization context. You can also specify multiple parameters if you wish.
|
598
|
-
|
599
|
-
You can then either provide a block with the logic to perform the authorization check, or provide a policy class.
|
600
|
-
|
601
|
-
The `record:` option lets you provide the name of the parameter which will be passed as the policy 'record'.
|
602
|
-
|
603
|
-
For example:
|
604
|
-
|
605
|
-
```ruby
|
606
|
-
class MyUpdateOperation < ApplicationOperation
|
607
|
-
param :initiator, ::AdminUser
|
608
|
-
param :order, ::Order
|
609
|
-
|
610
|
-
action_type :update
|
611
|
-
|
612
|
-
authorized_via :initiator, record: :order do
|
613
|
-
# ... the permissions check, admin users can edit orders that are not finalized
|
614
|
-
initiator.admin? && !record.finalized
|
615
|
-
end
|
616
|
-
|
617
|
-
def perform
|
618
|
-
# ...
|
619
|
-
end
|
620
|
-
end
|
621
|
-
```
|
622
|
-
|
623
|
-
You can instead provide a policy class implementation:
|
624
|
-
|
625
|
-
```ruby
|
626
|
-
class MyUpdateOperation < ApplicationOperation
|
627
|
-
action_type :update
|
628
|
-
|
629
|
-
class MyPolicyClass < OperationPolicy
|
630
|
-
# The action_type defined on the operation determines which method is called on the policy class
|
631
|
-
def update?
|
632
|
-
# ... the permissions check
|
633
|
-
initiator.admin?
|
634
|
-
end
|
635
|
-
|
636
|
-
# def my_check?
|
637
|
-
# # ... the permissions check
|
638
|
-
# end
|
639
|
-
end
|
640
|
-
|
641
|
-
authorized_via :initiator, with: MyPolicyClass
|
642
|
-
|
643
|
-
# It is also possible to specify which policy method to call
|
644
|
-
# authorized_via :initiator, with: MyPolicyClass, to: :my_check?
|
645
|
-
end
|
646
|
-
```
|
647
|
-
|
648
|
-
with multiple parameters:
|
649
|
-
|
650
|
-
```ruby
|
651
|
-
class MyUpdateOperation < ApplicationOperation
|
652
|
-
# ...
|
653
|
-
param :initiator, ::AdminUser
|
654
|
-
param :user, ::User
|
655
|
-
|
656
|
-
authorized_via :initiator, :user do
|
657
|
-
initiator.active? && user.active?
|
658
|
-
end
|
659
|
-
|
660
|
-
# ...
|
661
|
-
end
|
662
|
-
```
|
663
|
-
|
664
|
-
#### `.verify_authorized!`
|
665
|
-
|
666
|
-
To ensure that subclasses always implement authentication you can add a call to `.verify_authorized!` to your base
|
667
|
-
operation class.
|
668
|
-
|
669
|
-
This will cause the execution of any subclasses to fail if no authorization is performed.
|
670
|
-
|
671
|
-
```ruby
|
672
|
-
class MustAuthOperation < ApplicationOperation
|
673
|
-
verify_authorized!
|
674
|
-
end
|
675
|
-
|
676
|
-
class MyUpdateOperation < MustAuthOperation
|
677
|
-
def perform
|
678
|
-
# ...
|
679
|
-
end
|
680
|
-
end
|
681
|
-
|
682
|
-
MyUpdateOperation.call # => Raises an error that MyUpdateOperation does not perform any authorization
|
683
|
-
```
|
684
|
-
|
685
|
-
#### `#on_authorization_failure(err)`
|
686
|
-
|
687
|
-
A hook is provided to allow you to do some work on an authorization failure.
|
688
|
-
|
689
|
-
Simply override the `#on_authorization_failure(err)` method in your operation.
|
690
|
-
|
691
|
-
```ruby
|
692
|
-
class MyUpdateOperation < ApplicationOperation
|
693
|
-
action_type :update
|
694
|
-
|
695
|
-
authorized_via :initiator do
|
696
|
-
# ... the permissions check
|
697
|
-
initiator.admin?
|
698
|
-
end
|
699
|
-
|
700
|
-
def perform
|
701
|
-
# ...
|
702
|
-
end
|
703
|
-
|
704
|
-
def on_authorization_failure(err)
|
705
|
-
# ... do something with the error, eg logging
|
706
|
-
end
|
707
|
-
end
|
708
|
-
```
|
709
|
-
|
710
|
-
Note you are provided the ActionPolicy error object, but you cannot stop the error from being re-raised.
|
711
|
-
|
712
490
|
### Using with `literal` monads
|
713
491
|
|
714
492
|
You can use the `literal` gem to provide a `Result` type for your operations.
|
@@ -718,29 +496,30 @@ class MyOperation < ::TypedOperation::Base
|
|
718
496
|
param :account_name, String
|
719
497
|
param :owner, String
|
720
498
|
|
721
|
-
def
|
499
|
+
def call
|
722
500
|
create_account.bind do |account|
|
723
|
-
associate_owner(account).
|
501
|
+
associate_owner(account).bind do
|
502
|
+
account
|
503
|
+
end
|
724
504
|
end
|
725
505
|
end
|
726
506
|
|
727
507
|
private
|
728
508
|
|
729
509
|
def create_account
|
730
|
-
#
|
731
|
-
# Literal::Failure.new(:cant_create_account)
|
510
|
+
# returns Literal::Success(account) or Literal::Failure(:cant_create)
|
732
511
|
Literal::Success.new(account_name)
|
733
512
|
end
|
734
513
|
|
735
514
|
def associate_owner(account)
|
736
515
|
# ...
|
737
516
|
Literal::Failure.new(:cant_associate_owner)
|
738
|
-
# Literal::Success.new("ok")
|
739
517
|
end
|
740
518
|
end
|
741
519
|
|
742
520
|
MyOperation.new(account_name: "foo", owner: "bar").call
|
743
521
|
# => Literal::Failure(:cant_associate_owner)
|
522
|
+
|
744
523
|
```
|
745
524
|
|
746
525
|
### Using with `Dry::Monads`
|
@@ -755,7 +534,7 @@ class MyOperation < ::TypedOperation::Base
|
|
755
534
|
param :account_name, String
|
756
535
|
param :owner, ::Owner
|
757
536
|
|
758
|
-
def
|
537
|
+
def call
|
759
538
|
account = yield create_account(account_name)
|
760
539
|
yield associate_owner(account, owner)
|
761
540
|
|
data/Rakefile
ADDED
@@ -14,7 +14,7 @@ module <%= namespace_name %>
|
|
14
14
|
# Prepare...
|
15
15
|
end
|
16
16
|
|
17
|
-
def
|
17
|
+
def call
|
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 call
|
37
37
|
# Perform...
|
38
38
|
"Hello World!"
|
39
39
|
end
|
@@ -0,0 +1,73 @@
|
|
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
|
data/lib/typed_operation/base.rb
CHANGED
@@ -1,25 +1,106 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "literal"
|
4
|
+
|
3
5
|
module TypedOperation
|
4
|
-
class Base < Literal::
|
5
|
-
|
6
|
-
|
7
|
-
|
6
|
+
class Base < Literal::Data
|
7
|
+
class << self
|
8
|
+
def call(...)
|
9
|
+
new(...).call
|
10
|
+
end
|
8
11
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
12
|
+
def with(...)
|
13
|
+
PartiallyApplied.new(self, ...).with
|
14
|
+
end
|
15
|
+
alias_method :[], :with
|
13
16
|
|
14
|
-
|
15
|
-
|
16
|
-
super(name, type, special, reader:, writer: false, positional:, default:)
|
17
|
+
def curry
|
18
|
+
Curried.new(self)
|
17
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
|
55
|
+
|
56
|
+
def keyword_parameters
|
57
|
+
literal_attributes.filter_map { |name, attribute| name unless attribute.positional? }
|
58
|
+
end
|
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
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def after_initialization
|
86
|
+
prepare if respond_to?(:prepare)
|
87
|
+
end
|
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
|
18
99
|
end
|
19
100
|
|
20
|
-
def
|
21
|
-
|
22
|
-
|
101
|
+
def deconstruct_keys(keys)
|
102
|
+
h = attributes.to_h
|
103
|
+
keys ? h.slice(*keys) : h
|
23
104
|
end
|
24
105
|
end
|
25
106
|
end
|
data/lib/typed_operation.rb
CHANGED
@@ -2,20 +2,11 @@ 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
|
-
|
7
5
|
require "typed_operation/version"
|
8
6
|
require "typed_operation/railtie" if defined?(Rails::Railtie)
|
9
|
-
require "typed_operation/
|
10
|
-
require "typed_operation/
|
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"
|
7
|
+
require "typed_operation/nilable_type"
|
8
|
+
require "typed_operation/attribute_builder"
|
17
9
|
require "typed_operation/curried"
|
18
|
-
require "typed_operation/immutable_base"
|
19
10
|
require "typed_operation/base"
|
20
11
|
require "typed_operation/partially_applied"
|
21
12
|
require "typed_operation/prepared"
|
metadata
CHANGED
@@ -1,17 +1,17 @@
|
|
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.pre1
|
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-20 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
|
-
description:
|
14
|
-
|
13
|
+
description: TypedOperation is a command pattern implementation where inputs can be
|
14
|
+
defined with runtime type checks. Operations can be partially applied.
|
15
15
|
email:
|
16
16
|
- stevegeek@gmail.com
|
17
17
|
executables: []
|
@@ -20,6 +20,7 @@ extra_rdoc_files: []
|
|
20
20
|
files:
|
21
21
|
- MIT-LICENSE
|
22
22
|
- README.md
|
23
|
+
- Rakefile
|
23
24
|
- lib/generators/USAGE
|
24
25
|
- lib/generators/templates/operation.rb
|
25
26
|
- lib/generators/templates/operation_test.rb
|
@@ -27,19 +28,12 @@ files:
|
|
27
28
|
- lib/generators/typed_operation/install/install_generator.rb
|
28
29
|
- lib/generators/typed_operation/install/templates/application_operation.rb
|
29
30
|
- lib/generators/typed_operation_generator.rb
|
31
|
+
- lib/tasks/typed_operation_tasks.rake
|
30
32
|
- lib/typed_operation.rb
|
31
|
-
- lib/typed_operation/
|
33
|
+
- lib/typed_operation/attribute_builder.rb
|
32
34
|
- lib/typed_operation/base.rb
|
33
35
|
- lib/typed_operation/curried.rb
|
34
|
-
- lib/typed_operation/
|
35
|
-
- lib/typed_operation/operations/attribute_builder.rb
|
36
|
-
- lib/typed_operation/operations/callable.rb
|
37
|
-
- lib/typed_operation/operations/deconstruct.rb
|
38
|
-
- lib/typed_operation/operations/executable.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
|
36
|
+
- lib/typed_operation/nilable_type.rb
|
43
37
|
- lib/typed_operation/partially_applied.rb
|
44
38
|
- lib/typed_operation/prepared.rb
|
45
39
|
- lib/typed_operation/railtie.rb
|
@@ -65,9 +59,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
65
59
|
- !ruby/object:Gem::Version
|
66
60
|
version: 1.3.1
|
67
61
|
requirements: []
|
68
|
-
rubygems_version: 3.4.
|
62
|
+
rubygems_version: 3.4.18
|
69
63
|
signing_key:
|
70
64
|
specification_version: 4
|
71
|
-
summary: TypedOperation is a command pattern
|
72
|
-
and can be partially applied.
|
65
|
+
summary: TypedOperation is a command pattern implementation
|
73
66
|
test_files: []
|
@@ -1,141 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "action_policy"
|
4
|
-
|
5
|
-
# An optional module to include in your operation to enable authorization via ActionPolicies
|
6
|
-
module TypedOperation
|
7
|
-
class MissingAuthentication < StandardError; end
|
8
|
-
|
9
|
-
module ActionPolicyAuth
|
10
|
-
# Base class for any action policy classes used by operations
|
11
|
-
class OperationPolicy
|
12
|
-
include ActionPolicy::Policy::Core
|
13
|
-
include ActionPolicy::Policy::Authorization
|
14
|
-
include ActionPolicy::Policy::PreCheck
|
15
|
-
include ActionPolicy::Policy::Reasons
|
16
|
-
include ActionPolicy::Policy::Aliases
|
17
|
-
include ActionPolicy::Policy::Scoping
|
18
|
-
include ActionPolicy::Policy::Cache
|
19
|
-
include ActionPolicy::Policy::CachedApply
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.included(base)
|
23
|
-
base.include ::ActionPolicy::Behaviour
|
24
|
-
base.extend ClassMethods
|
25
|
-
end
|
26
|
-
|
27
|
-
module ClassMethods
|
28
|
-
# Configure the operation to use ActionPolicy for authorization
|
29
|
-
def authorized_via(*via, with: nil, to: nil, record: nil, &auth_block)
|
30
|
-
# If a block is provided, you must not provide a policy class or method
|
31
|
-
raise ArgumentError, "You must not provide a policy class or method when using a block" if auth_block && (with || to)
|
32
|
-
|
33
|
-
parameters = positional_parameters + keyword_parameters
|
34
|
-
raise ArgumentError, "authorize_via must be called with a valid param name" unless via.all? { |param| parameters.include?(param) }
|
35
|
-
@_authorized_via_param = via
|
36
|
-
|
37
|
-
action_type_method = "#{action_type}?".to_sym if action_type
|
38
|
-
# If an method name is provided, use it
|
39
|
-
policy_method = to || action_type_method || raise(::TypedOperation::InvalidOperationError, "You must provide an action type or policy method name")
|
40
|
-
@_policy_method = policy_method
|
41
|
-
# If a policy class is provided, use it
|
42
|
-
@_policy_class = if with
|
43
|
-
with
|
44
|
-
elsif auth_block
|
45
|
-
policy_class = Class.new(OperationPolicy) do
|
46
|
-
authorize(*via)
|
47
|
-
|
48
|
-
define_method(policy_method, &auth_block)
|
49
|
-
end
|
50
|
-
const_set(:Policy, policy_class)
|
51
|
-
policy_class
|
52
|
-
else
|
53
|
-
raise ::TypedOperation::InvalidOperationError, "You must provide either a policy class or a block"
|
54
|
-
end
|
55
|
-
|
56
|
-
if record
|
57
|
-
unless parameters.include?(record) || method_defined?(record) || private_instance_methods.include?(record)
|
58
|
-
raise ArgumentError, "to_authorize must be called with a valid param or method name"
|
59
|
-
end
|
60
|
-
@_to_authorize_param = record
|
61
|
-
end
|
62
|
-
|
63
|
-
# Configure action policy to use the param named in via as the context when instantiating the policy
|
64
|
-
# ::ActionPolicy::Behaviour does not provide a authorize(*ids) method, so we have call once per param
|
65
|
-
via.each do |param|
|
66
|
-
authorize param
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def action_type(type = nil)
|
71
|
-
@_action_type = type.to_sym if type
|
72
|
-
@_action_type
|
73
|
-
end
|
74
|
-
|
75
|
-
def operation_policy_method
|
76
|
-
@_policy_method
|
77
|
-
end
|
78
|
-
|
79
|
-
def operation_policy_class
|
80
|
-
@_policy_class
|
81
|
-
end
|
82
|
-
|
83
|
-
def operation_record_to_authorize
|
84
|
-
@_to_authorize_param
|
85
|
-
end
|
86
|
-
|
87
|
-
def checks_authorization?
|
88
|
-
!(@_authorized_via_param.nil? || @_authorized_via_param.empty?)
|
89
|
-
end
|
90
|
-
|
91
|
-
# You can use this on an operation base class to ensure and subclasses always enable authorization
|
92
|
-
def verify_authorized!
|
93
|
-
return if verify_authorized?
|
94
|
-
@_verify_authorized = true
|
95
|
-
end
|
96
|
-
|
97
|
-
def verify_authorized?
|
98
|
-
@_verify_authorized
|
99
|
-
end
|
100
|
-
|
101
|
-
def inherited(subclass)
|
102
|
-
super
|
103
|
-
subclass.instance_variable_set(:@_authorized_via_param, @_authorized_via_param)
|
104
|
-
subclass.instance_variable_set(:@_verify_authorized, @_verify_authorized)
|
105
|
-
subclass.instance_variable_set(:@_policy_class, @_policy_class)
|
106
|
-
subclass.instance_variable_set(:@_policy_method, @_policy_method)
|
107
|
-
subclass.instance_variable_set(:@_action_type, @_action_type)
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
private
|
112
|
-
|
113
|
-
# Redefine it as private
|
114
|
-
def execute_operation
|
115
|
-
if self.class.verify_authorized? && !self.class.checks_authorization?
|
116
|
-
raise ::TypedOperation::MissingAuthentication, "Operation #{self.class.name} must authorize. Remember to use `.authorize_via`"
|
117
|
-
end
|
118
|
-
operation_check_authorized! if self.class.checks_authorization?
|
119
|
-
super
|
120
|
-
end
|
121
|
-
|
122
|
-
def operation_check_authorized!
|
123
|
-
policy = self.class.operation_policy_class
|
124
|
-
raise "No Action Policy policy class provided, or no #{self.class.name}::Policy found for this action" unless policy
|
125
|
-
policy_method = self.class.operation_policy_method
|
126
|
-
raise "No policy method provided or action_type not set for #{self.class.name}" unless policy_method
|
127
|
-
# Record to authorize, if nil then action policy tries to work it out implicitly
|
128
|
-
record_to_authorize = send(self.class.operation_record_to_authorize) if self.class.operation_record_to_authorize
|
129
|
-
|
130
|
-
authorize! record_to_authorize, to: policy_method, with: policy
|
131
|
-
rescue ::ActionPolicy::Unauthorized => e
|
132
|
-
on_authorization_failure(e)
|
133
|
-
raise e
|
134
|
-
end
|
135
|
-
|
136
|
-
# A hook for subclasses to override to do something on an authorization failure
|
137
|
-
def on_authorization_failure(authorization_error)
|
138
|
-
# noop
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
@@ -1,14 +0,0 @@
|
|
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
|
@@ -1,81 +0,0 @@
|
|
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
|
@@ -1,23 +0,0 @@
|
|
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
|
@@ -1,16 +0,0 @@
|
|
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
|
@@ -1,29 +0,0 @@
|
|
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
|
@@ -1,38 +0,0 @@
|
|
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
|
@@ -1,12 +0,0 @@
|
|
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
|
@@ -1,31 +0,0 @@
|
|
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
|
@@ -1,16 +0,0 @@
|
|
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
|