typed_operation 1.0.0.beta1 → 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: 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