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