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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 501477c614c107723d1c915099dd06209336708ba5e9672c26622d02a9cd4782
4
- data.tar.gz: b5ab846baf478e3cce3fd8ea1752c04ee708c9a7f84d2be6d47d4c9a94505ac5
3
+ metadata.gz: ff925e2cc5ce03a696a209e0ca61130bff94ddb6ce28f2670ebaaae18f1968ba
4
+ data.tar.gz: b14a0952694653f918135a57180c8c7746aeffcb14385d17aeef7a1283726e99
5
5
  SHA512:
6
- metadata.gz: c3c399b74903b5b9e40ab7838c7d20bafa68969d1200bf2afb11457e41437ae8fa823e7afc14fabe71cd290cd798823f97a0918c4a12d50f6a8768ac1a77ea8e
7
- data.tar.gz: b1ce5f67d1b87ef907f00904288b25edd678a2afab0f68a5f2c5804831f5d05df0fad134a4c28a4893d3867f61c671d548bf687f37b822ff32754355845d9abd
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 pre-release on Rubygems (v1.0.0 is waiting for a release of `literal`). To use it now you can simply require `literal` from github in your Gemfile:**
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
- # `param` creates named parameters by default
48
- param :author_id, Integer, &:to_i
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
- param :shelf_code, optional(Integer)
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
- param :category, String, default: "unknown".freeze
50
+ named_param :category, String, default: "unknown".freeze
57
51
 
58
- # optional hook called when the operation is initialized, and after the parameters have been set
52
+ # to setup (optional)
59
53
  def prepare
60
54
  raise ArgumentError, "ISBN is invalid" unless valid_isbn?
61
55
  end
62
56
 
63
- # optionally hook in before execution ... and call super to allow subclasses to hook in too
64
- def before_execute_operation
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 perform = "It worked! (#{foo}, #{bar}, #{baz})"
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` or `TypedOperation::ImmutableBase`)
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
- - `TypedOperation::Base` (uses `Literal::Struct`) is the parent class for an operation where the arguments are potentially mutable (ie not frozen).
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 `#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.
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 perform
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 perform
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 perform
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, Literal::Types::BooleanType do |v|
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 `TypedOperation::Prepared` can be used to instantiate and call the operation.
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 Action Policy (`action_policy` gem)
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 then either provide a block with the logic to perform the authorization check, or provide a policy class.
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 MyUpdateOperation < ApplicationOperation
611
- param :initiator, ::AdminUser
612
- param :order, ::Order
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
- ```ruby
630
- class MyUpdateOperation < ApplicationOperation
631
- action_type :update
632
-
633
- class MyPolicyClass < OperationPolicy
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
- with multiple parameters:
507
+ private
653
508
 
654
- ```ruby
655
- class MyUpdateOperation < ApplicationOperation
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
- MyUpdateOperation.call # => Raises an error that MyUpdateOperation does not perform any authorization
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(:perform, :create_account)
532
+ include Dry::Monads::Do.for(:call)
724
533
 
725
534
  param :account_name, String
726
535
  param :owner, ::Owner
727
536
 
728
- def perform
537
+ def call
729
538
  account = yield create_account(account_name)
730
- yield AnotherOperation.call(account, owner)
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 --action_policy
580
+ bin/rails g typed_operation:install --dry_monads
775
581
  ```
776
582
 
777
583
  ## Generate a new Operation
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -14,7 +14,7 @@ module <%= namespace_name %>
14
14
  # Prepare...
15
15
  end
16
16
 
17
- def perform
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 perform
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
- # Some examples:
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,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :typed_operation do
3
+ # # Task goes here
4
+ # 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
@@ -1,13 +1,106 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "literal"
4
+
3
5
  module TypedOperation
4
- class Base < Literal::Struct
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
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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "literal"
4
+
5
+ module TypedOperation
6
+ class NilableType < Literal::Union
7
+ def initialize(*types)
8
+ @types = types + [NilClass]
9
+ end
10
+ end
11
+ end
@@ -1,3 +1,3 @@
1
1
  module TypedOperation
2
- VERSION = "1.0.0.beta2"
2
+ VERSION = "1.0.0.pre1"
3
3
  end
@@ -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/operations/introspection"
10
- require "typed_operation/operations/parameters"
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.beta2
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: 2024-06-24 00:00:00.000000000 Z
11
+ date: 2023-08-20 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: Command pattern, which is callable, and can be partially applied, curried
14
- and has typed parameters. Authorization to execute via action_policy if desired.
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/action_policy_auth.rb
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/immutable_base.rb
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.2'
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: '0'
60
+ version: 1.3.1
66
61
  requirements: []
67
- rubygems_version: 3.5.3
62
+ rubygems_version: 3.4.18
68
63
  signing_key:
69
64
  specification_version: 4
70
- summary: TypedOperation is a command pattern with typed parameters, which is callable,
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,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TypedOperation
4
- module Operations
5
- module Lifecycle
6
- # This is called by Literal on initialization of underlying Struct/Data
7
- def after_initialize
8
- prepare if respond_to?(:prepare)
9
- end
10
- end
11
- end
12
- 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