typed_operation 1.0.0.beta2 → 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 +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
|