typed_operation 1.0.0.beta2 → 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 +40 -234
- data/Rakefile +3 -0
- data/lib/generators/templates/operation.rb +2 -2
- data/lib/generators/typed_operation/install/USAGE +0 -1
- data/lib/generators/typed_operation/install/install_generator.rb +0 -5
- data/lib/generators/typed_operation/install/templates/application_operation.rb +2 -24
- data/lib/tasks/typed_operation_tasks.rake +4 -0
- data/lib/typed_operation/attribute_builder.rb +73 -0
- data/lib/typed_operation/base.rb +101 -8
- data/lib/typed_operation/nilable_type.rb +11 -0
- data/lib/typed_operation/version.rb +1 -1
- data/lib/typed_operation.rb +2 -10
- metadata +13 -19
- data/lib/typed_operation/action_policy_auth.rb +0 -141
- data/lib/typed_operation/immutable_base.rb +0 -13
- data/lib/typed_operation/operations/callable.rb +0 -23
- data/lib/typed_operation/operations/executable.rb +0 -29
- data/lib/typed_operation/operations/introspection.rb +0 -36
- data/lib/typed_operation/operations/lifecycle.rb +0 -12
- data/lib/typed_operation/operations/parameters.rb +0 -36
- data/lib/typed_operation/operations/partial_application.rb +0 -16
- data/lib/typed_operation/operations/property_builder.rb +0 -81
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.beta2"
|
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
|
|
@@ -44,39 +39,26 @@ class ShelveBookOperation < ::TypedOperation::Base
|
|
44
39
|
# Or if you prefer:
|
45
40
|
# `param :description, String`
|
46
41
|
|
47
|
-
|
48
|
-
|
49
|
-
param :isbn, String
|
42
|
+
named_param :author_id, Integer, &:to_i
|
43
|
+
named_param :isbn, String
|
50
44
|
|
51
45
|
# Optional parameters are specified by wrapping the type constraint in the `optional` method, or using the `optional:` option
|
52
|
-
|
46
|
+
named_param :shelf_code, optional(Integer)
|
53
47
|
# Or if you prefer:
|
54
48
|
# `named_param :shelf_code, Integer, optional: true`
|
55
49
|
|
56
|
-
|
50
|
+
named_param :category, String, default: "unknown".freeze
|
57
51
|
|
58
|
-
#
|
52
|
+
# to setup (optional)
|
59
53
|
def prepare
|
60
54
|
raise ArgumentError, "ISBN is invalid" unless valid_isbn?
|
61
55
|
end
|
62
56
|
|
63
|
-
#
|
64
|
-
def
|
65
|
-
# ...
|
66
|
-
super
|
67
|
-
end
|
68
|
-
|
69
|
-
# The 'work' of the operation, this is the main body of the operation and must be implemented
|
70
|
-
def perform
|
57
|
+
# The 'work' of the operation
|
58
|
+
def call
|
71
59
|
"Put away '#{title}' by author ID #{author_id}#{shelf_code ? " on shelf #{shelf_code}" : "" }"
|
72
60
|
end
|
73
61
|
|
74
|
-
# optionally hook in after execution ... and call super to allow subclasses to hook in too
|
75
|
-
def after_execute_operation(result)
|
76
|
-
# ...
|
77
|
-
super
|
78
|
-
end
|
79
|
-
|
80
62
|
private
|
81
63
|
|
82
64
|
def valid_isbn?
|
@@ -110,15 +92,13 @@ shelve.call(author_id: "1", isbn: false)
|
|
110
92
|
|
111
93
|
### Partially applying parameters
|
112
94
|
|
113
|
-
Operations can also be partially applied and curried:
|
114
|
-
|
115
95
|
```ruby
|
116
96
|
class TestOperation < ::TypedOperation::Base
|
117
97
|
param :foo, String, positional: true
|
118
98
|
param :bar, String
|
119
99
|
param :baz, String, &:to_s
|
120
100
|
|
121
|
-
def
|
101
|
+
def call = "It worked! (#{foo}, #{bar}, #{baz})"
|
122
102
|
end
|
123
103
|
|
124
104
|
# Invoking the operation directly
|
@@ -171,76 +151,21 @@ TestOperation.with("1").with(bar: "2").with(baz: 3).operation
|
|
171
151
|
|
172
152
|
## Documentation
|
173
153
|
|
174
|
-
### Create an operation (subclass `TypedOperation::Base`
|
175
|
-
|
176
|
-
Create an operation by subclassing `TypedOperation::Base` or `TypedOperation::ImmutableBase` and specifying the parameters the operation requires.
|
154
|
+
### Create an operation (subclass `TypedOperation::Base`)
|
177
155
|
|
178
|
-
|
179
|
-
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.
|
180
|
-
- `TypedOperation::ImmutableBase` (uses `Literal::Data`) is the parent class for an operation where the arguments are immutable (frozen on initialization),
|
181
|
-
thus giving a somewhat stronger immutability guarantee (ie that the operation does not mutate its arguments).
|
156
|
+
Create an operation by subclassing `TypedOperation::Base` and specifying the parameters the operation requires.
|
182
157
|
|
183
|
-
The subclass must implement the `#
|
158
|
+
The subclass must implement the `#call` method which is where the operations main work is done.
|
184
159
|
|
185
160
|
The operation can also implement:
|
186
161
|
|
187
162
|
- `#prepare` - called when the operation is initialized, and after the parameters have been set
|
188
|
-
- `#before_execute_operation` - optionally hook in before execution ... and call super to allow subclasses to hook in too
|
189
|
-
- `#after_execute_operation` - optionally hook in after execution ... and call super to allow subclasses to hook in too
|
190
|
-
|
191
|
-
```ruby
|
192
|
-
# optionally hook in before execution...
|
193
|
-
def before_execute_operation
|
194
|
-
# Remember to call super
|
195
|
-
super
|
196
|
-
end
|
197
|
-
|
198
|
-
def perform
|
199
|
-
# ... implement me!
|
200
|
-
end
|
201
|
-
|
202
|
-
# optionally hook in after execution...
|
203
|
-
def after_execute_operation(result)
|
204
|
-
# Remember to call super, note the result is passed in and the return value of this method is the result of the operation
|
205
|
-
# thus allowing you to modify the result if you wish
|
206
|
-
super
|
207
|
-
end
|
208
|
-
```
|
209
163
|
|
210
164
|
### Specifying parameters (using `.param`)
|
211
165
|
|
212
166
|
Parameters are specified using the provided class methods (`.positional_param` and `.named_param`),
|
213
167
|
or using the underlying `.param` method.
|
214
168
|
|
215
|
-
Types are specified using the `literal` gem. In many cases this simply means providing the class of the
|
216
|
-
expected type, but there are also some other useful types provided by `literal` (eg `Union`).
|
217
|
-
|
218
|
-
These can be either accessed via the `Literal` module, eg `Literal::Types::BooleanType`:
|
219
|
-
|
220
|
-
```ruby
|
221
|
-
class MyOperation < ::TypedOperation::Base
|
222
|
-
param :name, String
|
223
|
-
param :age, Integer, optional: true
|
224
|
-
param :choices, Literal::Types::ArrayType.new(String)
|
225
|
-
param :chose, Literal::Types::BooleanType
|
226
|
-
end
|
227
|
-
|
228
|
-
MyOperation.new(name: "bob", choices: ["st"], chose: true)
|
229
|
-
```
|
230
|
-
|
231
|
-
or by including the `Literal::Types` module into your operation class, and using the aliases provided:
|
232
|
-
|
233
|
-
```ruby
|
234
|
-
class MyOperation < ::TypedOperation::Base
|
235
|
-
include Literal::Types
|
236
|
-
|
237
|
-
param :name, String
|
238
|
-
param :age, _Nilable(Integer) # optional can also be specifed using `.optional`
|
239
|
-
param :choices, _Array(String)
|
240
|
-
param :chose, _Boolean
|
241
|
-
end
|
242
|
-
```
|
243
|
-
|
244
169
|
Type constraints can be modified to make the parameter optional using `.optional`.
|
245
170
|
|
246
171
|
#### Your own aliases
|
@@ -298,7 +223,7 @@ class MyOperation < ::TypedOperation::Base
|
|
298
223
|
# Or alternatively => `param :name, String, positional: true`
|
299
224
|
positional_param :age, Integer, default: -> { 0 }
|
300
225
|
|
301
|
-
def
|
226
|
+
def call
|
302
227
|
puts "Hello #{name} (#{age})"
|
303
228
|
end
|
304
229
|
end
|
@@ -329,7 +254,7 @@ class MyOperation < ::TypedOperation::Base
|
|
329
254
|
# Or alternatively => `param :name, String`
|
330
255
|
named_param :age, Integer, default: -> { 0 }
|
331
256
|
|
332
|
-
def
|
257
|
+
def call
|
333
258
|
puts "Hello #{name} (#{age})"
|
334
259
|
end
|
335
260
|
end
|
@@ -350,7 +275,7 @@ class MyOperation < ::TypedOperation::Base
|
|
350
275
|
positional_param :name, String
|
351
276
|
named_param :age, Integer, default: -> { 0 }
|
352
277
|
|
353
|
-
def
|
278
|
+
def call
|
354
279
|
puts "Hello #{name} (#{age})"
|
355
280
|
end
|
356
281
|
end
|
@@ -394,7 +319,7 @@ You can specify a block after a parameter definition to coerce the argument valu
|
|
394
319
|
|
395
320
|
```ruby
|
396
321
|
param :name, String, &:to_s
|
397
|
-
param :choice,
|
322
|
+
param :choice, Union(FalseClass, TrueClass) do |v|
|
398
323
|
v == "y"
|
399
324
|
end
|
400
325
|
```
|
@@ -441,10 +366,9 @@ An operation can be invoked by:
|
|
441
366
|
|
442
367
|
- instantiating it with at least required params and then calling the `#call` method on the instance
|
443
368
|
- once a partially applied operation has been prepared (all required parameters have been set), the call
|
444
|
-
method on
|
369
|
+
method on TypedOperation::Prepared can be used to instantiate and call the operation.
|
445
370
|
- once an operation is curried, the `#call` method on last TypedOperation::Curried in the chain will invoke the operation
|
446
371
|
- calling `#call` on a partially applied operation and passing in any remaining required parameters
|
447
|
-
- calling `#execute_operation` on an operation instance (this is the method that is called by `#call`)
|
448
372
|
|
449
373
|
See the many examples in this document.
|
450
374
|
|
@@ -522,8 +446,7 @@ This is an example of a `ApplicationOperation` in a Rails app that uses `Dry::Mo
|
|
522
446
|
|
523
447
|
class ApplicationOperation < ::TypedOperation::Base
|
524
448
|
# We choose to use dry-monads for our operations, so include the required modules
|
525
|
-
include Dry::Monads[:result]
|
526
|
-
include Dry::Monads::Do.for(:perform)
|
449
|
+
include Dry::Monads[:result, :do]
|
527
450
|
|
528
451
|
class << self
|
529
452
|
# Setup our own preferred names for the DSL methods
|
@@ -564,155 +487,41 @@ class ApplicationOperation < ::TypedOperation::Base
|
|
564
487
|
end
|
565
488
|
```
|
566
489
|
|
567
|
-
### Using with
|
568
|
-
|
569
|
-
> Note, this optional feature requires the `action_policy` gem to be installed and does not yet work with `ImmutableBase`.
|
570
|
-
|
571
|
-
Add `TypedOperation::ActionPolicyAuth` to your `ApplicationOperation` (first `require` the module):
|
572
|
-
|
573
|
-
```ruby
|
574
|
-
require "typed_operation/action_policy_auth"
|
575
|
-
|
576
|
-
class ApplicationOperation < ::TypedOperation::Base
|
577
|
-
# ...
|
578
|
-
include TypedOperation::ActionPolicyAuth
|
579
|
-
|
580
|
-
# You can specify a parameter to take the authorization context object, eg a user (can also be optional if some
|
581
|
-
# operations don't require authorization)
|
582
|
-
param :initiator, ::User # or optional(::User)
|
583
|
-
end
|
584
|
-
```
|
585
|
-
|
586
|
-
#### Specify the action with `.action_type`
|
587
|
-
|
588
|
-
Every operation must define what action_type it is, eg:
|
589
|
-
|
590
|
-
```ruby
|
591
|
-
class MyUpdateOperation < ApplicationOperation
|
592
|
-
action_type :update
|
593
|
-
end
|
594
|
-
```
|
595
|
-
|
596
|
-
Any symbol can be used as the `action_type` and this is by default used to determine which policy method to call.
|
597
|
-
|
598
|
-
#### Configuring auth with `.authorized_via`
|
599
|
-
|
600
|
-
`.authorized_via` is used to specify how to authorize the operation. You must specify the name of a parameter
|
601
|
-
for the policy authorization context. You can also specify multiple parameters if you wish.
|
490
|
+
### Using with `literal` monads
|
602
491
|
|
603
|
-
You can
|
604
|
-
|
605
|
-
The `record:` option lets you provide the name of the parameter which will be passed as the policy 'record'.
|
606
|
-
|
607
|
-
For example:
|
492
|
+
You can use the `literal` gem to provide a `Result` type for your operations.
|
608
493
|
|
609
494
|
```ruby
|
610
|
-
class
|
611
|
-
param :
|
612
|
-
param :
|
613
|
-
|
614
|
-
action_type :update
|
615
|
-
|
616
|
-
authorized_via :initiator, record: :order do
|
617
|
-
# ... the permissions check, admin users can edit orders that are not finalized
|
618
|
-
initiator.admin? && !record.finalized
|
619
|
-
end
|
620
|
-
|
621
|
-
def perform
|
622
|
-
# ...
|
623
|
-
end
|
624
|
-
end
|
625
|
-
```
|
626
|
-
|
627
|
-
You can instead provide a policy class implementation:
|
495
|
+
class MyOperation < ::TypedOperation::Base
|
496
|
+
param :account_name, String
|
497
|
+
param :owner, String
|
628
498
|
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
# The action_type defined on the operation determines which method is called on the policy class
|
635
|
-
def update?
|
636
|
-
# ... the permissions check
|
637
|
-
initiator.admin?
|
499
|
+
def call
|
500
|
+
create_account.bind do |account|
|
501
|
+
associate_owner(account).bind do
|
502
|
+
account
|
503
|
+
end
|
638
504
|
end
|
639
|
-
|
640
|
-
# def my_check?
|
641
|
-
# # ... the permissions check
|
642
|
-
# end
|
643
505
|
end
|
644
|
-
|
645
|
-
authorized_via :initiator, with: MyPolicyClass
|
646
|
-
|
647
|
-
# It is also possible to specify which policy method to call
|
648
|
-
# authorized_via :initiator, with: MyPolicyClass, to: :my_check?
|
649
|
-
end
|
650
|
-
```
|
651
506
|
|
652
|
-
|
507
|
+
private
|
653
508
|
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
param :initiator, ::AdminUser
|
658
|
-
param :user, ::User
|
659
|
-
|
660
|
-
authorized_via :initiator, :user do
|
661
|
-
initiator.active? && user.active?
|
509
|
+
def create_account
|
510
|
+
# returns Literal::Success(account) or Literal::Failure(:cant_create)
|
511
|
+
Literal::Success.new(account_name)
|
662
512
|
end
|
663
513
|
|
664
|
-
|
665
|
-
end
|
666
|
-
```
|
667
|
-
|
668
|
-
#### `.verify_authorized!`
|
669
|
-
|
670
|
-
To ensure that subclasses always implement authentication you can add a call to `.verify_authorized!` to your base
|
671
|
-
operation class.
|
672
|
-
|
673
|
-
This will cause the execution of any subclasses to fail if no authorization is performed.
|
674
|
-
|
675
|
-
```ruby
|
676
|
-
class MustAuthOperation < ApplicationOperation
|
677
|
-
verify_authorized!
|
678
|
-
end
|
679
|
-
|
680
|
-
class MyUpdateOperation < MustAuthOperation
|
681
|
-
def perform
|
514
|
+
def associate_owner(account)
|
682
515
|
# ...
|
516
|
+
Literal::Failure.new(:cant_associate_owner)
|
683
517
|
end
|
684
518
|
end
|
685
519
|
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
#### `#on_authorization_failure(err)`
|
520
|
+
MyOperation.new(account_name: "foo", owner: "bar").call
|
521
|
+
# => Literal::Failure(:cant_associate_owner)
|
690
522
|
|
691
|
-
A hook is provided to allow you to do some work on an authorization failure.
|
692
|
-
|
693
|
-
Simply override the `#on_authorization_failure(err)` method in your operation.
|
694
|
-
|
695
|
-
```ruby
|
696
|
-
class MyUpdateOperation < ApplicationOperation
|
697
|
-
action_type :update
|
698
|
-
|
699
|
-
authorized_via :initiator do
|
700
|
-
# ... the permissions check
|
701
|
-
initiator.admin?
|
702
|
-
end
|
703
|
-
|
704
|
-
def perform
|
705
|
-
# ...
|
706
|
-
end
|
707
|
-
|
708
|
-
def on_authorization_failure(err)
|
709
|
-
# ... do something with the error, eg logging
|
710
|
-
end
|
711
|
-
end
|
712
523
|
```
|
713
524
|
|
714
|
-
Note you are provided the ActionPolicy error object, but you cannot stop the error from being re-raised.
|
715
|
-
|
716
525
|
### Using with `Dry::Monads`
|
717
526
|
|
718
527
|
As per the example in [`Dry::Monads` documentation](https://dry-rb.org/gems/dry-monads/1.0/do-notation/)
|
@@ -720,14 +529,14 @@ As per the example in [`Dry::Monads` documentation](https://dry-rb.org/gems/dry-
|
|
720
529
|
```ruby
|
721
530
|
class MyOperation < ::TypedOperation::Base
|
722
531
|
include Dry::Monads[:result]
|
723
|
-
include Dry::Monads::Do.for(:
|
532
|
+
include Dry::Monads::Do.for(:call)
|
724
533
|
|
725
534
|
param :account_name, String
|
726
535
|
param :owner, ::Owner
|
727
536
|
|
728
|
-
def
|
537
|
+
def call
|
729
538
|
account = yield create_account(account_name)
|
730
|
-
yield
|
539
|
+
yield associate_owner(account, owner)
|
731
540
|
|
732
541
|
Success(account)
|
733
542
|
end
|
@@ -767,11 +576,8 @@ bin/rails g typed_operation:install
|
|
767
576
|
Use the `--dry_monads` switch to `include Dry::Monads[:result]` into your `ApplicationOperation` (don't forget to also
|
768
577
|
add `gem "dry-monads"` to your Gemfile)
|
769
578
|
|
770
|
-
Use the `--action_policy` switch to add the `TypedOperation::ActionPolicyAuth` module to your `ApplicationOperation`
|
771
|
-
(and you will also need to add `gem "action_policy"` to your Gemfile).
|
772
|
-
|
773
579
|
```ruby
|
774
|
-
bin/rails g typed_operation:install --dry_monads
|
580
|
+
bin/rails g typed_operation:install --dry_monads
|
775
581
|
```
|
776
582
|
|
777
583
|
## Generate a new Operation
|
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
|
@@ -12,7 +12,6 @@ rails generate typed_operation:install
|
|
12
12
|
Options:
|
13
13
|
--------
|
14
14
|
--dry_monads: if specified the ApplicationOperation will include dry-monads Result and Do notation.
|
15
|
-
--action_policy: if specified the ApplicationOperation will include action_policy authorization integration.
|
16
15
|
|
17
16
|
Example:
|
18
17
|
--------
|
@@ -6,7 +6,6 @@ module TypedOperation
|
|
6
6
|
module Install
|
7
7
|
class InstallGenerator < Rails::Generators::Base
|
8
8
|
class_option :dry_monads, type: :boolean, default: false
|
9
|
-
class_option :action_policy, type: :boolean, default: false
|
10
9
|
|
11
10
|
source_root File.expand_path("templates", __dir__)
|
12
11
|
|
@@ -19,10 +18,6 @@ module TypedOperation
|
|
19
18
|
def include_dry_monads?
|
20
19
|
options[:dry_monads]
|
21
20
|
end
|
22
|
-
|
23
|
-
def include_action_policy?
|
24
|
-
options[:action_policy]
|
25
|
-
end
|
26
21
|
end
|
27
22
|
end
|
28
23
|
end
|
@@ -1,36 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class ApplicationOperation < ::TypedOperation::Base
|
4
|
-
<% if include_action_policy? -%>
|
5
|
-
include TypedOperation::ActionPolicyAuth
|
6
|
-
|
7
|
-
<% end -%>
|
8
4
|
<% if include_dry_monads? -%>
|
9
|
-
include Dry::Monads[:result]
|
10
|
-
include Dry::Monads::Do.for(:perform)
|
5
|
+
include Dry::Monads[:result, :do]
|
11
6
|
|
12
|
-
# Helper to execute then unwrap a successful result or raise an exception
|
13
7
|
def call!
|
14
8
|
call.value!
|
15
9
|
end
|
16
10
|
|
17
11
|
<% end -%>
|
18
12
|
# Other common parameters & methods for Operations of this application...
|
19
|
-
#
|
20
|
-
#
|
21
|
-
# def self.operation_key
|
22
|
-
# name.underscore.to_sym
|
23
|
-
# end
|
24
|
-
#
|
25
|
-
# def operation_key
|
26
|
-
# self.class.operation_key
|
27
|
-
# end
|
28
|
-
#
|
29
|
-
# # Translation and localization
|
30
|
-
#
|
31
|
-
# def translate(key, **)
|
32
|
-
# key = "operations.#{operation_key}.#{key}" if key.start_with?(".")
|
33
|
-
# I18n.t(key, **)
|
34
|
-
# end
|
35
|
-
# alias_method :t, :translate
|
13
|
+
# ...
|
36
14
|
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,13 +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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
6
|
+
class Base < Literal::Data
|
7
|
+
class << self
|
8
|
+
def call(...)
|
9
|
+
new(...).call
|
10
|
+
end
|
11
|
+
|
12
|
+
def with(...)
|
13
|
+
PartiallyApplied.new(self, ...).with
|
14
|
+
end
|
15
|
+
alias_method :[], :with
|
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
|
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
|
99
|
+
end
|
100
|
+
|
101
|
+
def deconstruct_keys(keys)
|
102
|
+
h = attributes.to_h
|
103
|
+
keys ? h.slice(*keys) : h
|
104
|
+
end
|
12
105
|
end
|
13
106
|
end
|
data/lib/typed_operation.rb
CHANGED
@@ -2,19 +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/property_builder"
|
15
|
-
require "typed_operation/operations/executable"
|
7
|
+
require "typed_operation/nilable_type"
|
8
|
+
require "typed_operation/attribute_builder"
|
16
9
|
require "typed_operation/curried"
|
17
|
-
require "typed_operation/immutable_base"
|
18
10
|
require "typed_operation/base"
|
19
11
|
require "typed_operation/partially_applied"
|
20
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:
|
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,18 +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/callable.rb
|
36
|
-
- lib/typed_operation/operations/executable.rb
|
37
|
-
- lib/typed_operation/operations/introspection.rb
|
38
|
-
- lib/typed_operation/operations/lifecycle.rb
|
39
|
-
- lib/typed_operation/operations/parameters.rb
|
40
|
-
- lib/typed_operation/operations/partial_application.rb
|
41
|
-
- lib/typed_operation/operations/property_builder.rb
|
36
|
+
- lib/typed_operation/nilable_type.rb
|
42
37
|
- lib/typed_operation/partially_applied.rb
|
43
38
|
- lib/typed_operation/prepared.rb
|
44
39
|
- lib/typed_operation/railtie.rb
|
@@ -57,16 +52,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
57
52
|
requirements:
|
58
53
|
- - ">="
|
59
54
|
- !ruby/object:Gem::Version
|
60
|
-
version: '3.
|
55
|
+
version: '3.1'
|
61
56
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
57
|
requirements:
|
63
|
-
- - "
|
58
|
+
- - ">"
|
64
59
|
- !ruby/object:Gem::Version
|
65
|
-
version:
|
60
|
+
version: 1.3.1
|
66
61
|
requirements: []
|
67
|
-
rubygems_version: 3.
|
62
|
+
rubygems_version: 3.4.18
|
68
63
|
signing_key:
|
69
64
|
specification_version: 4
|
70
|
-
summary: TypedOperation is a command pattern
|
71
|
-
and can be partially applied.
|
65
|
+
summary: TypedOperation is a command pattern implementation
|
72
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,13 +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::Executable
|
12
|
-
end
|
13
|
-
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,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,36 +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_properties.filter_map { |property| property.name if property.positional? }
|
9
|
-
end
|
10
|
-
|
11
|
-
def keyword_parameters
|
12
|
-
literal_properties.filter_map { |property| property.name if property.keyword? }
|
13
|
-
end
|
14
|
-
|
15
|
-
def required_parameters
|
16
|
-
literal_properties.filter { |property| property.required? }
|
17
|
-
end
|
18
|
-
|
19
|
-
def required_positional_parameters
|
20
|
-
required_parameters.filter_map { |property| property.name if property.positional? }
|
21
|
-
end
|
22
|
-
|
23
|
-
def required_keyword_parameters
|
24
|
-
required_parameters.filter_map { |property| property.name if property.keyword? }
|
25
|
-
end
|
26
|
-
|
27
|
-
def optional_positional_parameters
|
28
|
-
positional_parameters - required_positional_parameters
|
29
|
-
end
|
30
|
-
|
31
|
-
def optional_keyword_parameters
|
32
|
-
keyword_parameters - required_keyword_parameters
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
@@ -1,36 +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
|
-
# Override literal `prop` to prevent creating writers (Literal::Data does this by default)
|
8
|
-
def self.prop(name, type, kind = :keyword, reader: :public, writer: :public, default: nil)
|
9
|
-
super(name, type, kind, reader:, writer: false, default:)
|
10
|
-
end
|
11
|
-
|
12
|
-
# Parameter for keyword argument, or a positional argument if you use positional: true
|
13
|
-
# Required, but you can set a default or use optional: true if you want optional
|
14
|
-
def param(name, signature = :any, **options, &converter)
|
15
|
-
PropertyBuilder.new(self, name, signature, options).define(&converter)
|
16
|
-
end
|
17
|
-
|
18
|
-
# Alternative DSL
|
19
|
-
|
20
|
-
# Parameter for positional argument
|
21
|
-
def positional_param(name, signature = :any, **options, &converter)
|
22
|
-
param(name, signature, **options.merge(positional: true), &converter)
|
23
|
-
end
|
24
|
-
|
25
|
-
# Parameter for a keyword or named argument
|
26
|
-
def named_param(name, signature = :any, **options, &converter)
|
27
|
-
param(name, signature, **options.merge(positional: false), &converter)
|
28
|
-
end
|
29
|
-
|
30
|
-
# Wrap a type signature in a NilableType meaning it is optional to TypedOperation
|
31
|
-
def optional(type_signature)
|
32
|
-
Literal::Types::NilableType.new(type_signature)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
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
|
@@ -1,81 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module TypedOperation
|
4
|
-
module Operations
|
5
|
-
class PropertyBuilder
|
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] # Wraps signature in NilableType
|
11
|
-
@positional = options[:positional] # Changes kind to 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.prop(
|
27
|
-
@name,
|
28
|
-
@signature,
|
29
|
-
@positional ? :positional : :keyword,
|
30
|
-
default: default_value_for_literal,
|
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::Types::UnionType.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
|