typed_operation 1.0.0.beta1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 464b0e645a604caaee0ab2b83f21b56cb0ca7a6b6b69e3475dcce17366ed2c80
4
- data.tar.gz: c59c8522a9c8c04be5a589649596b85bd42cb8f4faeae4073230fd42470556ce
3
+ metadata.gz: ff925e2cc5ce03a696a209e0ca61130bff94ddb6ce28f2670ebaaae18f1968ba
4
+ data.tar.gz: b14a0952694653f918135a57180c8c7746aeffcb14385d17aeef7a1283726e99
5
5
  SHA512:
6
- metadata.gz: 7c42242a9df90fd0751b3e5fcd77a6b71ded4e9bc5cfc7725082e2d1ff200643dce87c10e7f80d2ae2eececb2d00231083396ed1d2aaf10a4fb88e1eaa287aa3
7
- data.tar.gz: bfad02c10284d3895956b3390ac6529f1b815286f19e2cb5644ed5b687d01e2248f664cc166925d9db4a521ebb20956143fb9a7c98592c193915cbb812ffd90b
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.beta1"
14
- ```
9
+ **Note the version described here (~ 1.0.0) is not yet released on Rubygems, it is waiting for a release of `literal`)**
15
10
 
16
11
  ## Features
17
12
 
@@ -54,28 +49,16 @@ class ShelveBookOperation < ::TypedOperation::Base
54
49
 
55
50
  named_param :category, String, default: "unknown".freeze
56
51
 
57
- # optional hook called when the operation is initialized, and after the parameters have been set
52
+ # to setup (optional)
58
53
  def prepare
59
54
  raise ArgumentError, "ISBN is invalid" unless valid_isbn?
60
55
  end
61
56
 
62
- # optionally hook in before execution ... and call super to allow subclasses to hook in too
63
- def before_execute_operation
64
- # ...
65
- super
66
- end
67
-
68
- # The 'work' of the operation, this is the main body of the operation and must be implemented
69
- def perform
57
+ # The 'work' of the operation
58
+ def call
70
59
  "Put away '#{title}' by author ID #{author_id}#{shelf_code ? " on shelf #{shelf_code}" : "" }"
71
60
  end
72
61
 
73
- # optionally hook in after execution ... and call super to allow subclasses to hook in too
74
- def after_execute_operation(result)
75
- # ...
76
- super
77
- end
78
-
79
62
  private
80
63
 
81
64
  def valid_isbn?
@@ -109,15 +92,13 @@ shelve.call(author_id: "1", isbn: false)
109
92
 
110
93
  ### Partially applying parameters
111
94
 
112
- Operations can also be partially applied and curried:
113
-
114
95
  ```ruby
115
96
  class TestOperation < ::TypedOperation::Base
116
97
  param :foo, String, positional: true
117
98
  param :bar, String
118
99
  param :baz, String, &:to_s
119
100
 
120
- def perform = "It worked! (#{foo}, #{bar}, #{baz})"
101
+ def call = "It worked! (#{foo}, #{bar}, #{baz})"
121
102
  end
122
103
 
123
104
  # Invoking the operation directly
@@ -170,76 +151,21 @@ TestOperation.with("1").with(bar: "2").with(baz: 3).operation
170
151
 
171
152
  ## Documentation
172
153
 
173
- ### Create an operation (subclass `TypedOperation::Base` or `TypedOperation::ImmutableBase`)
154
+ ### Create an operation (subclass `TypedOperation::Base`)
174
155
 
175
- Create an operation by subclassing `TypedOperation::Base` or `TypedOperation::ImmutableBase` and specifying the parameters the operation requires.
156
+ Create an operation by subclassing `TypedOperation::Base` and specifying the parameters the operation requires.
176
157
 
177
- - `TypedOperation::Base` (uses `Literal::Struct`) is the parent class for an operation where the arguments are potentially mutable (ie not frozen).
178
- No attribute writer methods are defined, so the arguments can not be changed after initialization, but the values passed in are not guaranteed to be frozen.
179
- - `TypedOperation::ImmutableBase` (uses `Literal::Data`) is the parent class for an operation where the arguments are immutable (frozen on initialization),
180
- thus giving a somewhat stronger immutability guarantee (ie that the operation does not mutate its arguments).
181
-
182
- The subclass must implement the `#perform` method which is where the operations main work is done.
158
+ The subclass must implement the `#call` method which is where the operations main work is done.
183
159
 
184
160
  The operation can also implement:
185
161
 
186
162
  - `#prepare` - called when the operation is initialized, and after the parameters have been set
187
- - `#before_execute_operation` - optionally hook in before execution ... and call super to allow subclasses to hook in too
188
- - `#after_execute_operation` - optionally hook in after execution ... and call super to allow subclasses to hook in too
189
-
190
- ```ruby
191
- # optionally hook in before execution...
192
- def before_execute_operation
193
- # Remember to call super
194
- super
195
- end
196
-
197
- def perform
198
- # ... implement me!
199
- end
200
-
201
- # optionally hook in after execution...
202
- def after_execute_operation(result)
203
- # Remember to call super, note the result is passed in and the return value of this method is the result of the operation
204
- # thus allowing you to modify the result if you wish
205
- super
206
- end
207
- ```
208
163
 
209
164
  ### Specifying parameters (using `.param`)
210
165
 
211
166
  Parameters are specified using the provided class methods (`.positional_param` and `.named_param`),
212
167
  or using the underlying `.param` method.
213
168
 
214
- Types are specified using the `literal` gem. In many cases this simply means providing the class of the
215
- expected type, but there are also some other useful types provided by `literal` (eg `Union`).
216
-
217
- These can be either accessed via the `Literal` module, eg `Literal::Types::BooleanType`:
218
-
219
- ```ruby
220
- class MyOperation < ::TypedOperation::Base
221
- param :name, String
222
- param :age, Integer, optional: true
223
- param :choices, Literal::Types::ArrayType.new(String)
224
- param :chose, Literal::Types::BooleanType
225
- end
226
-
227
- MyOperation.new(name: "bob", choices: ["st"], chose: true)
228
- ```
229
-
230
- or by including the `Literal::Types` module into your operation class, and using the aliases provided:
231
-
232
- ```ruby
233
- class MyOperation < ::TypedOperation::Base
234
- include Literal::Types
235
-
236
- param :name, String
237
- param :age, _Nilable(Integer) # optional can also be specifed using `.optional`
238
- param :choices, _Array(String)
239
- param :chose, _Boolean
240
- end
241
- ```
242
-
243
169
  Type constraints can be modified to make the parameter optional using `.optional`.
244
170
 
245
171
  #### Your own aliases
@@ -297,7 +223,7 @@ class MyOperation < ::TypedOperation::Base
297
223
  # Or alternatively => `param :name, String, positional: true`
298
224
  positional_param :age, Integer, default: -> { 0 }
299
225
 
300
- def perform
226
+ def call
301
227
  puts "Hello #{name} (#{age})"
302
228
  end
303
229
  end
@@ -328,7 +254,7 @@ class MyOperation < ::TypedOperation::Base
328
254
  # Or alternatively => `param :name, String`
329
255
  named_param :age, Integer, default: -> { 0 }
330
256
 
331
- def perform
257
+ def call
332
258
  puts "Hello #{name} (#{age})"
333
259
  end
334
260
  end
@@ -349,7 +275,7 @@ class MyOperation < ::TypedOperation::Base
349
275
  positional_param :name, String
350
276
  named_param :age, Integer, default: -> { 0 }
351
277
 
352
- def perform
278
+ def call
353
279
  puts "Hello #{name} (#{age})"
354
280
  end
355
281
  end
@@ -393,7 +319,7 @@ You can specify a block after a parameter definition to coerce the argument valu
393
319
 
394
320
  ```ruby
395
321
  param :name, String, &:to_s
396
- param :choice, Literal::Types::BooleanType do |v|
322
+ param :choice, Union(FalseClass, TrueClass) do |v|
397
323
  v == "y"
398
324
  end
399
325
  ```
@@ -440,10 +366,9 @@ An operation can be invoked by:
440
366
 
441
367
  - instantiating it with at least required params and then calling the `#call` method on the instance
442
368
  - once a partially applied operation has been prepared (all required parameters have been set), the call
443
- method on `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.
444
370
  - once an operation is curried, the `#call` method on last TypedOperation::Curried in the chain will invoke the operation
445
371
  - calling `#call` on a partially applied operation and passing in any remaining required parameters
446
- - calling `#execute_operation` on an operation instance (this is the method that is called by `#call`)
447
372
 
448
373
  See the many examples in this document.
449
374
 
@@ -562,153 +487,6 @@ class ApplicationOperation < ::TypedOperation::Base
562
487
  end
563
488
  ```
564
489
 
565
- ### Using with Action Policy (`action_policy` gem)
566
-
567
- Add `TypedOperation::ActionPolicyAuth` to your `ApplicationOperation` (first `require` the module):
568
-
569
- ```ruby
570
- require "typed_operation/action_policy_auth"
571
-
572
- class ApplicationOperation < ::TypedOperation::Base
573
- # ...
574
- include TypedOperation::ActionPolicyAuth
575
-
576
- # You can specify a parameter to take the authorization context object, eg a user (can also be optional if some
577
- # operations don't require authorization)
578
- param :initiator, ::User # or optional(::User)
579
- end
580
- ```
581
-
582
- #### Specify the action with `.action_type`
583
-
584
- Every operation must define what action_type it is, eg:
585
-
586
- ```ruby
587
- class MyUpdateOperation < ApplicationOperation
588
- action_type :update
589
- end
590
- ```
591
-
592
- Any symbol can be used as the `action_type` and this is by default used to determine which policy method to call.
593
-
594
- #### Configuring auth with `.authorized_via`
595
-
596
- `.authorized_via` is used to specify how to authorize the operation. You must specify the name of a parameter
597
- for the policy authorization context. You can also specify multiple parameters if you wish.
598
-
599
- You can then either provide a block with the logic to perform the authorization check, or provide a policy class.
600
-
601
- The `record:` option lets you provide the name of the parameter which will be passed as the policy 'record'.
602
-
603
- For example:
604
-
605
- ```ruby
606
- class MyUpdateOperation < ApplicationOperation
607
- param :initiator, ::AdminUser
608
- param :order, ::Order
609
-
610
- action_type :update
611
-
612
- authorized_via :initiator, record: :order do
613
- # ... the permissions check, admin users can edit orders that are not finalized
614
- initiator.admin? && !record.finalized
615
- end
616
-
617
- def perform
618
- # ...
619
- end
620
- end
621
- ```
622
-
623
- You can instead provide a policy class implementation:
624
-
625
- ```ruby
626
- class MyUpdateOperation < ApplicationOperation
627
- action_type :update
628
-
629
- class MyPolicyClass < OperationPolicy
630
- # The action_type defined on the operation determines which method is called on the policy class
631
- def update?
632
- # ... the permissions check
633
- initiator.admin?
634
- end
635
-
636
- # def my_check?
637
- # # ... the permissions check
638
- # end
639
- end
640
-
641
- authorized_via :initiator, with: MyPolicyClass
642
-
643
- # It is also possible to specify which policy method to call
644
- # authorized_via :initiator, with: MyPolicyClass, to: :my_check?
645
- end
646
- ```
647
-
648
- with multiple parameters:
649
-
650
- ```ruby
651
- class MyUpdateOperation < ApplicationOperation
652
- # ...
653
- param :initiator, ::AdminUser
654
- param :user, ::User
655
-
656
- authorized_via :initiator, :user do
657
- initiator.active? && user.active?
658
- end
659
-
660
- # ...
661
- end
662
- ```
663
-
664
- #### `.verify_authorized!`
665
-
666
- To ensure that subclasses always implement authentication you can add a call to `.verify_authorized!` to your base
667
- operation class.
668
-
669
- This will cause the execution of any subclasses to fail if no authorization is performed.
670
-
671
- ```ruby
672
- class MustAuthOperation < ApplicationOperation
673
- verify_authorized!
674
- end
675
-
676
- class MyUpdateOperation < MustAuthOperation
677
- def perform
678
- # ...
679
- end
680
- end
681
-
682
- MyUpdateOperation.call # => Raises an error that MyUpdateOperation does not perform any authorization
683
- ```
684
-
685
- #### `#on_authorization_failure(err)`
686
-
687
- A hook is provided to allow you to do some work on an authorization failure.
688
-
689
- Simply override the `#on_authorization_failure(err)` method in your operation.
690
-
691
- ```ruby
692
- class MyUpdateOperation < ApplicationOperation
693
- action_type :update
694
-
695
- authorized_via :initiator do
696
- # ... the permissions check
697
- initiator.admin?
698
- end
699
-
700
- def perform
701
- # ...
702
- end
703
-
704
- def on_authorization_failure(err)
705
- # ... do something with the error, eg logging
706
- end
707
- end
708
- ```
709
-
710
- Note you are provided the ActionPolicy error object, but you cannot stop the error from being re-raised.
711
-
712
490
  ### Using with `literal` monads
713
491
 
714
492
  You can use the `literal` gem to provide a `Result` type for your operations.
@@ -718,29 +496,30 @@ class MyOperation < ::TypedOperation::Base
718
496
  param :account_name, String
719
497
  param :owner, String
720
498
 
721
- def perform
499
+ def call
722
500
  create_account.bind do |account|
723
- associate_owner(account).map { account }
501
+ associate_owner(account).bind do
502
+ account
503
+ end
724
504
  end
725
505
  end
726
506
 
727
507
  private
728
508
 
729
509
  def create_account
730
- # ...
731
- # Literal::Failure.new(:cant_create_account)
510
+ # returns Literal::Success(account) or Literal::Failure(:cant_create)
732
511
  Literal::Success.new(account_name)
733
512
  end
734
513
 
735
514
  def associate_owner(account)
736
515
  # ...
737
516
  Literal::Failure.new(:cant_associate_owner)
738
- # Literal::Success.new("ok")
739
517
  end
740
518
  end
741
519
 
742
520
  MyOperation.new(account_name: "foo", owner: "bar").call
743
521
  # => Literal::Failure(:cant_associate_owner)
522
+
744
523
  ```
745
524
 
746
525
  ### Using with `Dry::Monads`
@@ -755,7 +534,7 @@ class MyOperation < ::TypedOperation::Base
755
534
  param :account_name, String
756
535
  param :owner, ::Owner
757
536
 
758
- def perform
537
+ def call
759
538
  account = yield create_account(account_name)
760
539
  yield associate_owner(account, owner)
761
540
 
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
@@ -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,25 +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
6
+ class Base < Literal::Data
7
+ class << self
8
+ def call(...)
9
+ new(...).call
10
+ end
8
11
 
9
- include Operations::Lifecycle
10
- include Operations::Callable
11
- include Operations::Deconstruct
12
- include Operations::Executable
12
+ def with(...)
13
+ PartiallyApplied.new(self, ...).with
14
+ end
15
+ alias_method :[], :with
13
16
 
14
- class << self
15
- def attribute(name, type, special = nil, reader: :public, writer: :public, positional: false, default: nil)
16
- super(name, type, special, reader:, writer: false, positional:, default:)
17
+ def curry
18
+ Curried.new(self)
17
19
  end
20
+
21
+ def to_proc
22
+ method(:call).to_proc
23
+ end
24
+
25
+ # Method to define parameters for your operation.
26
+
27
+ # Parameter for keyword argument, or a positional argument if you use positional: true
28
+ # Required, but you can set a default or use optional: true if you want optional
29
+ def param(name, signature = :any, **options, &converter)
30
+ AttributeBuilder.new(self, name, signature, options).define(&converter)
31
+ end
32
+
33
+ # Alternative DSL
34
+
35
+ # Parameter for positional argument
36
+ def positional_param(name, signature = :any, **options, &converter)
37
+ param(name, signature, **options.merge(positional: true), &converter)
38
+ end
39
+
40
+ # Parameter for a keyword or named argument
41
+ def named_param(name, signature = :any, **options, &converter)
42
+ param(name, signature, **options.merge(positional: false), &converter)
43
+ end
44
+
45
+ # Wrap a type signature in a NilableType meaning it is optional to TypedOperation
46
+ def optional(type_signature)
47
+ NilableType.new(type_signature)
48
+ end
49
+
50
+ # Introspection methods
51
+
52
+ def positional_parameters
53
+ literal_attributes.filter_map { |name, attribute| name if attribute.positional? }
54
+ end
55
+
56
+ def keyword_parameters
57
+ literal_attributes.filter_map { |name, attribute| name unless attribute.positional? }
58
+ end
59
+
60
+ def required_positional_parameters
61
+ required_parameters.filter_map { |name, attribute| name if attribute.positional? }
62
+ end
63
+
64
+ def required_keyword_parameters
65
+ required_parameters.filter_map { |name, attribute| name unless attribute.positional? }
66
+ end
67
+
68
+ def optional_positional_parameters
69
+ positional_parameters - required_positional_parameters
70
+ end
71
+
72
+ def optional_keyword_parameters
73
+ keyword_parameters - required_keyword_parameters
74
+ end
75
+
76
+ private
77
+
78
+ def required_parameters
79
+ literal_attributes.filter do |name, attribute|
80
+ attribute.default.nil? # Any optional parameters will have a default value/proc in their Literal::Attribute
81
+ end
82
+ end
83
+ end
84
+
85
+ def after_initialization
86
+ prepare if respond_to?(:prepare)
87
+ end
88
+
89
+ def call
90
+ raise InvalidOperationError, "You must implement #call"
91
+ end
92
+
93
+ def to_proc
94
+ method(:call).to_proc
95
+ end
96
+
97
+ def deconstruct
98
+ attributes.values
18
99
  end
19
100
 
20
- def with(...)
21
- # copy to new operation with new attrs
22
- self.class.new(**attributes.merge(...))
101
+ def deconstruct_keys(keys)
102
+ h = attributes.to_h
103
+ keys ? h.slice(*keys) : h
23
104
  end
24
105
  end
25
106
  end
@@ -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.beta1"
2
+ VERSION = "1.0.0.pre1"
3
3
  end
@@ -2,20 +2,11 @@ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.2.0")
2
2
  require "polyfill-data"
3
3
  end
4
4
 
5
- require "literal"
6
-
7
5
  require "typed_operation/version"
8
6
  require "typed_operation/railtie" if defined?(Rails::Railtie)
9
- require "typed_operation/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/deconstruct"
15
- require "typed_operation/operations/attribute_builder"
16
- require "typed_operation/operations/executable"
7
+ require "typed_operation/nilable_type"
8
+ require "typed_operation/attribute_builder"
17
9
  require "typed_operation/curried"
18
- require "typed_operation/immutable_base"
19
10
  require "typed_operation/base"
20
11
  require "typed_operation/partially_applied"
21
12
  require "typed_operation/prepared"
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typed_operation
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.beta1
4
+ version: 1.0.0.pre1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Ierodiaconou
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-26 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,19 +28,12 @@ files:
27
28
  - lib/generators/typed_operation/install/install_generator.rb
28
29
  - lib/generators/typed_operation/install/templates/application_operation.rb
29
30
  - lib/generators/typed_operation_generator.rb
31
+ - lib/tasks/typed_operation_tasks.rake
30
32
  - lib/typed_operation.rb
31
- - lib/typed_operation/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/attribute_builder.rb
36
- - lib/typed_operation/operations/callable.rb
37
- - lib/typed_operation/operations/deconstruct.rb
38
- - lib/typed_operation/operations/executable.rb
39
- - lib/typed_operation/operations/introspection.rb
40
- - lib/typed_operation/operations/lifecycle.rb
41
- - lib/typed_operation/operations/parameters.rb
42
- - lib/typed_operation/operations/partial_application.rb
36
+ - lib/typed_operation/nilable_type.rb
43
37
  - lib/typed_operation/partially_applied.rb
44
38
  - lib/typed_operation/prepared.rb
45
39
  - lib/typed_operation/railtie.rb
@@ -65,9 +59,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
65
59
  - !ruby/object:Gem::Version
66
60
  version: 1.3.1
67
61
  requirements: []
68
- rubygems_version: 3.4.19
62
+ rubygems_version: 3.4.18
69
63
  signing_key:
70
64
  specification_version: 4
71
- summary: TypedOperation is a command pattern with typed parameters, which is callable,
72
- and can be partially applied.
65
+ summary: TypedOperation is a command pattern implementation
73
66
  test_files: []
@@ -1,141 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "action_policy"
4
-
5
- # An optional module to include in your operation to enable authorization via ActionPolicies
6
- module TypedOperation
7
- class MissingAuthentication < StandardError; end
8
-
9
- module ActionPolicyAuth
10
- # Base class for any action policy classes used by operations
11
- class OperationPolicy
12
- include ActionPolicy::Policy::Core
13
- include ActionPolicy::Policy::Authorization
14
- include ActionPolicy::Policy::PreCheck
15
- include ActionPolicy::Policy::Reasons
16
- include ActionPolicy::Policy::Aliases
17
- include ActionPolicy::Policy::Scoping
18
- include ActionPolicy::Policy::Cache
19
- include ActionPolicy::Policy::CachedApply
20
- end
21
-
22
- def self.included(base)
23
- base.include ::ActionPolicy::Behaviour
24
- base.extend ClassMethods
25
- end
26
-
27
- module ClassMethods
28
- # Configure the operation to use ActionPolicy for authorization
29
- def authorized_via(*via, with: nil, to: nil, record: nil, &auth_block)
30
- # If a block is provided, you must not provide a policy class or method
31
- raise ArgumentError, "You must not provide a policy class or method when using a block" if auth_block && (with || to)
32
-
33
- parameters = positional_parameters + keyword_parameters
34
- raise ArgumentError, "authorize_via must be called with a valid param name" unless via.all? { |param| parameters.include?(param) }
35
- @_authorized_via_param = via
36
-
37
- action_type_method = "#{action_type}?".to_sym if action_type
38
- # If an method name is provided, use it
39
- policy_method = to || action_type_method || raise(::TypedOperation::InvalidOperationError, "You must provide an action type or policy method name")
40
- @_policy_method = policy_method
41
- # If a policy class is provided, use it
42
- @_policy_class = if with
43
- with
44
- elsif auth_block
45
- policy_class = Class.new(OperationPolicy) do
46
- authorize(*via)
47
-
48
- define_method(policy_method, &auth_block)
49
- end
50
- const_set(:Policy, policy_class)
51
- policy_class
52
- else
53
- raise ::TypedOperation::InvalidOperationError, "You must provide either a policy class or a block"
54
- end
55
-
56
- if record
57
- unless parameters.include?(record) || method_defined?(record) || private_instance_methods.include?(record)
58
- raise ArgumentError, "to_authorize must be called with a valid param or method name"
59
- end
60
- @_to_authorize_param = record
61
- end
62
-
63
- # Configure action policy to use the param named in via as the context when instantiating the policy
64
- # ::ActionPolicy::Behaviour does not provide a authorize(*ids) method, so we have call once per param
65
- via.each do |param|
66
- authorize param
67
- end
68
- end
69
-
70
- def action_type(type = nil)
71
- @_action_type = type.to_sym if type
72
- @_action_type
73
- end
74
-
75
- def operation_policy_method
76
- @_policy_method
77
- end
78
-
79
- def operation_policy_class
80
- @_policy_class
81
- end
82
-
83
- def operation_record_to_authorize
84
- @_to_authorize_param
85
- end
86
-
87
- def checks_authorization?
88
- !(@_authorized_via_param.nil? || @_authorized_via_param.empty?)
89
- end
90
-
91
- # You can use this on an operation base class to ensure and subclasses always enable authorization
92
- def verify_authorized!
93
- return if verify_authorized?
94
- @_verify_authorized = true
95
- end
96
-
97
- def verify_authorized?
98
- @_verify_authorized
99
- end
100
-
101
- def inherited(subclass)
102
- super
103
- subclass.instance_variable_set(:@_authorized_via_param, @_authorized_via_param)
104
- subclass.instance_variable_set(:@_verify_authorized, @_verify_authorized)
105
- subclass.instance_variable_set(:@_policy_class, @_policy_class)
106
- subclass.instance_variable_set(:@_policy_method, @_policy_method)
107
- subclass.instance_variable_set(:@_action_type, @_action_type)
108
- end
109
- end
110
-
111
- private
112
-
113
- # Redefine it as private
114
- def execute_operation
115
- if self.class.verify_authorized? && !self.class.checks_authorization?
116
- raise ::TypedOperation::MissingAuthentication, "Operation #{self.class.name} must authorize. Remember to use `.authorize_via`"
117
- end
118
- operation_check_authorized! if self.class.checks_authorization?
119
- super
120
- end
121
-
122
- def operation_check_authorized!
123
- policy = self.class.operation_policy_class
124
- raise "No Action Policy policy class provided, or no #{self.class.name}::Policy found for this action" unless policy
125
- policy_method = self.class.operation_policy_method
126
- raise "No policy method provided or action_type not set for #{self.class.name}" unless policy_method
127
- # Record to authorize, if nil then action policy tries to work it out implicitly
128
- record_to_authorize = send(self.class.operation_record_to_authorize) if self.class.operation_record_to_authorize
129
-
130
- authorize! record_to_authorize, to: policy_method, with: policy
131
- rescue ::ActionPolicy::Unauthorized => e
132
- on_authorization_failure(e)
133
- raise e
134
- end
135
-
136
- # A hook for subclasses to override to do something on an authorization failure
137
- def on_authorization_failure(authorization_error)
138
- # noop
139
- end
140
- end
141
- end
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TypedOperation
4
- class ImmutableBase < Literal::Data
5
- extend Operations::Introspection
6
- extend Operations::Parameters
7
- extend Operations::PartialApplication
8
-
9
- include Operations::Lifecycle
10
- include Operations::Callable
11
- include Operations::Deconstruct
12
- include Operations::Executable
13
- end
14
- end
@@ -1,81 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TypedOperation
4
- module Operations
5
- class AttributeBuilder
6
- def initialize(typed_operation, parameter_name, type_signature, options)
7
- @typed_operation = typed_operation
8
- @name = parameter_name
9
- @signature = type_signature
10
- @optional = options[:optional]
11
- @positional = options[:positional]
12
- @reader = options[:reader] || :public
13
- @default_key = options.key?(:default)
14
- @default = options[:default]
15
-
16
- prepare_type_signature_for_literal
17
- end
18
-
19
- def define(&converter)
20
- # If nilable, then converter should not attempt to call the type converter block if the value is nil
21
- coerce_by = if type_nilable? && converter
22
- ->(v) { (v == Literal::Null || v.nil?) ? v : converter.call(v) }
23
- else
24
- converter
25
- end
26
- @typed_operation.attribute(
27
- @name,
28
- @signature,
29
- default: default_value_for_literal,
30
- positional: @positional,
31
- reader: @reader,
32
- &coerce_by
33
- )
34
- end
35
-
36
- private
37
-
38
- def prepare_type_signature_for_literal
39
- @signature = Literal::Types::NilableType.new(@signature) if needs_to_be_nilable?
40
- union_with_nil_to_support_nil_default
41
- validate_positional_order_params! if @positional
42
- end
43
-
44
- # If already wrapped in a Nilable then don't wrap again
45
- def needs_to_be_nilable?
46
- @optional && !type_nilable?
47
- end
48
-
49
- def type_nilable?
50
- @signature.is_a?(Literal::Types::NilableType)
51
- end
52
-
53
- def union_with_nil_to_support_nil_default
54
- @signature = Literal::Union.new(@signature, NilClass) if has_default_value_nil?
55
- end
56
-
57
- def has_default_value_nil?
58
- default_provided? && @default.nil?
59
- end
60
-
61
- def validate_positional_order_params!
62
- # Optional ones can always be added after required ones, or before any others, but required ones must be first
63
- unless type_nilable? || @typed_operation.optional_positional_parameters.empty?
64
- raise ParameterError, "Cannot define required positional parameter '#{@name}' after optional positional parameters"
65
- end
66
- end
67
-
68
- def default_provided?
69
- @default_key
70
- end
71
-
72
- def default_value_for_literal
73
- if has_default_value_nil? || type_nilable?
74
- -> {}
75
- else
76
- @default
77
- end
78
- end
79
- end
80
- end
81
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TypedOperation
4
- module Operations
5
- module Callable
6
- def self.included(base)
7
- base.extend(CallableMethods)
8
- end
9
-
10
- module CallableMethods
11
- def call(...)
12
- new(...).call
13
- end
14
-
15
- def to_proc
16
- method(:call).to_proc
17
- end
18
- end
19
-
20
- include CallableMethods
21
- end
22
- end
23
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TypedOperation
4
- module Operations
5
- module Deconstruct
6
- def deconstruct
7
- attributes.values
8
- end
9
-
10
- def deconstruct_keys(keys)
11
- h = attributes.to_h
12
- keys ? h.slice(*keys) : h
13
- end
14
- end
15
- end
16
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TypedOperation
4
- module Operations
5
- module Executable
6
- def call
7
- execute_operation
8
- end
9
-
10
- def execute_operation
11
- before_execute_operation
12
- retval = perform
13
- after_execute_operation(retval)
14
- end
15
-
16
- def before_execute_operation
17
- # noop
18
- end
19
-
20
- def after_execute_operation(retval)
21
- retval
22
- end
23
-
24
- def perform
25
- raise InvalidOperationError, "Operation #{self.class} does not implement #perform"
26
- end
27
- end
28
- end
29
- end
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TypedOperation
4
- module Operations
5
- # Introspection methods
6
- module Introspection
7
- def positional_parameters
8
- literal_attributes.filter_map { |name, attribute| name if attribute.positional? }
9
- end
10
-
11
- def keyword_parameters
12
- literal_attributes.filter_map { |name, attribute| name unless attribute.positional? }
13
- end
14
-
15
- def required_parameters
16
- literal_attributes.filter do |name, attribute|
17
- attribute.default.nil? # Any optional parameters will have a default value/proc in their Literal::Attribute
18
- end
19
- end
20
-
21
- def required_positional_parameters
22
- required_parameters.filter_map { |name, attribute| name if attribute.positional? }
23
- end
24
-
25
- def required_keyword_parameters
26
- required_parameters.filter_map { |name, attribute| name unless attribute.positional? }
27
- end
28
-
29
- def optional_positional_parameters
30
- positional_parameters - required_positional_parameters
31
- end
32
-
33
- def optional_keyword_parameters
34
- keyword_parameters - required_keyword_parameters
35
- end
36
- end
37
- end
38
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TypedOperation
4
- module Operations
5
- module Lifecycle
6
- # This is called by Literal on initialization of underlying Struct/Data
7
- def after_initialization
8
- prepare if respond_to?(:prepare)
9
- end
10
- end
11
- end
12
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TypedOperation
4
- module Operations
5
- # Method to define parameters for your operation.
6
- module Parameters
7
- # Parameter for keyword argument, or a positional argument if you use positional: true
8
- # Required, but you can set a default or use optional: true if you want optional
9
- def param(name, signature = :any, **options, &converter)
10
- AttributeBuilder.new(self, name, signature, options).define(&converter)
11
- end
12
-
13
- # Alternative DSL
14
-
15
- # Parameter for positional argument
16
- def positional_param(name, signature = :any, **options, &converter)
17
- param(name, signature, **options.merge(positional: true), &converter)
18
- end
19
-
20
- # Parameter for a keyword or named argument
21
- def named_param(name, signature = :any, **options, &converter)
22
- param(name, signature, **options.merge(positional: false), &converter)
23
- end
24
-
25
- # Wrap a type signature in a NilableType meaning it is optional to TypedOperation
26
- def optional(type_signature)
27
- Literal::Types::NilableType.new(type_signature)
28
- end
29
- end
30
- end
31
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TypedOperation
4
- module Operations
5
- module PartialApplication
6
- def with(...)
7
- PartiallyApplied.new(self, ...).with
8
- end
9
- alias_method :[], :with
10
-
11
- def curry
12
- Curried.new(self)
13
- end
14
- end
15
- end
16
- end