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