typed_operation 0.4.1 → 1.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +729 -30
- data/lib/generators/templates/operation.rb +12 -6
- data/lib/typed_operation/action_policy_auth.rb +141 -0
- data/lib/typed_operation/base.rb +14 -47
- data/lib/typed_operation/curried.rb +40 -0
- data/lib/typed_operation/immutable_base.rb +14 -0
- data/lib/typed_operation/operations/attribute_builder.rb +81 -0
- data/lib/typed_operation/operations/callable.rb +23 -0
- data/lib/typed_operation/operations/deconstruct.rb +16 -0
- data/lib/typed_operation/operations/executable.rb +29 -0
- data/lib/typed_operation/operations/introspection.rb +38 -0
- data/lib/typed_operation/operations/lifecycle.rb +12 -0
- data/lib/typed_operation/operations/parameters.rb +31 -0
- data/lib/typed_operation/operations/partial_application.rb +16 -0
- data/lib/typed_operation/partially_applied.rb +72 -16
- data/lib/typed_operation/prepared.rb +5 -1
- data/lib/typed_operation/version.rb +1 -1
- data/lib/typed_operation.rb +22 -2
- metadata +22 -60
- data/Rakefile +0 -3
- data/lib/tasks/typed_operation_tasks.rake +0 -4
@@ -4,14 +4,17 @@
|
|
4
4
|
module <%= namespace_name %>
|
5
5
|
class <%= name %> < ::ApplicationOperation
|
6
6
|
# Replace with implementation...
|
7
|
-
|
8
|
-
param :
|
7
|
+
positional_param :required_positional_param, String
|
8
|
+
param :required_named_param, String
|
9
|
+
param :an_optional_param, Integer, optional: true do |value|
|
10
|
+
value.to_i
|
11
|
+
end
|
9
12
|
|
10
13
|
def prepare
|
11
14
|
# Prepare...
|
12
15
|
end
|
13
16
|
|
14
|
-
def
|
17
|
+
def perform
|
15
18
|
# Perform...
|
16
19
|
"Hello World!"
|
17
20
|
end
|
@@ -20,14 +23,17 @@ end
|
|
20
23
|
<% else %>
|
21
24
|
class <%= name %> < ::ApplicationOperation
|
22
25
|
# Replace with implementation...
|
23
|
-
|
24
|
-
param :
|
26
|
+
positional_param :required_positional_param, String
|
27
|
+
param :required_named_param, String
|
28
|
+
param :an_optional_param, Integer, optional: true do |value|
|
29
|
+
value.to_i
|
30
|
+
end
|
25
31
|
|
26
32
|
def prepare
|
27
33
|
# Prepare...
|
28
34
|
end
|
29
35
|
|
30
|
-
def
|
36
|
+
def perform
|
31
37
|
# Perform...
|
32
38
|
"Hello World!"
|
33
39
|
end
|
@@ -0,0 +1,141 @@
|
|
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
|
data/lib/typed_operation/base.rb
CHANGED
@@ -1,58 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "vident/typed"
|
4
|
-
require "vident/typed/attributes"
|
5
|
-
|
6
3
|
module TypedOperation
|
7
|
-
class Base
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
def call(...)
|
12
|
-
new(...).call
|
13
|
-
end
|
14
|
-
|
15
|
-
def curry(**args)
|
16
|
-
PartiallyApplied.new(self, **args).curry
|
17
|
-
end
|
18
|
-
alias_method :[], :curry
|
19
|
-
alias_method :with, :curry
|
20
|
-
|
21
|
-
def to_proc
|
22
|
-
method(:call).to_proc
|
23
|
-
end
|
24
|
-
|
25
|
-
def operation_key
|
26
|
-
name.underscore.to_sym
|
27
|
-
end
|
4
|
+
class Base < Literal::Struct
|
5
|
+
extend Operations::Introspection
|
6
|
+
extend Operations::Parameters
|
7
|
+
extend Operations::PartialApplication
|
28
8
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
end
|
9
|
+
include Operations::Lifecycle
|
10
|
+
include Operations::Callable
|
11
|
+
include Operations::Deconstruct
|
12
|
+
include Operations::Executable
|
34
13
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
rescue ::Dry::Struct::Error => e
|
39
|
-
raise ParameterError, e.message
|
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:)
|
40
17
|
end
|
41
|
-
prepare if respond_to?(:prepare)
|
42
18
|
end
|
43
19
|
|
44
|
-
def
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
def to_proc
|
49
|
-
method(:call).to_proc
|
50
|
-
end
|
51
|
-
|
52
|
-
private
|
53
|
-
|
54
|
-
def operation_key
|
55
|
-
self.class.operation_key
|
20
|
+
def with(...)
|
21
|
+
# copy to new operation with new attrs
|
22
|
+
self.class.new(**attributes.merge(...))
|
56
23
|
end
|
57
24
|
end
|
58
25
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypedOperation
|
4
|
+
class Curried
|
5
|
+
def initialize(operation_class, partial_operation = nil)
|
6
|
+
@operation_class = operation_class
|
7
|
+
@partial_operation = partial_operation || operation_class.with
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(arg)
|
11
|
+
raise ArgumentError, "A prepared operation should not be curried" if @partial_operation.prepared?
|
12
|
+
|
13
|
+
next_partially_applied = if next_parameter_positional?
|
14
|
+
@partial_operation.with(arg)
|
15
|
+
else
|
16
|
+
@partial_operation.with(next_keyword_parameter => arg)
|
17
|
+
end
|
18
|
+
if next_partially_applied.prepared?
|
19
|
+
next_partially_applied.call
|
20
|
+
else
|
21
|
+
Curried.new(@operation_class, next_partially_applied)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_proc
|
26
|
+
method(:call).to_proc
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def next_keyword_parameter
|
32
|
+
remaining = @operation_class.required_keyword_parameters - @partial_operation.keyword_args.keys
|
33
|
+
remaining.first
|
34
|
+
end
|
35
|
+
|
36
|
+
def next_parameter_positional?
|
37
|
+
@partial_operation.positional_args.size < @operation_class.required_positional_parameters.size
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,14 @@
|
|
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
|
@@ -0,0 +1,81 @@
|
|
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
|
@@ -0,0 +1,23 @@
|
|
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
|
@@ -0,0 +1,16 @@
|
|
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
|
@@ -0,0 +1,29 @@
|
|
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
|
@@ -0,0 +1,38 @@
|
|
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
|
@@ -0,0 +1,12 @@
|
|
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
|
@@ -0,0 +1,31 @@
|
|
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
|
@@ -0,0 +1,16 @@
|
|
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
|
@@ -2,31 +2,87 @@
|
|
2
2
|
|
3
3
|
module TypedOperation
|
4
4
|
class PartiallyApplied
|
5
|
-
def initialize(
|
6
|
-
@
|
7
|
-
@
|
5
|
+
def initialize(operation_class, *positional_args, **keyword_args)
|
6
|
+
@operation_class = operation_class
|
7
|
+
@positional_args = positional_args
|
8
|
+
@keyword_args = keyword_args
|
8
9
|
end
|
9
10
|
|
10
|
-
def
|
11
|
-
|
12
|
-
|
13
|
-
required_keys = @operation.attribute_names.select { |name| @operation.attribute_metadata(name)[:required] != false }
|
14
|
-
missing_keys = required_keys - all_args.keys
|
11
|
+
def with(*positional, **keyword)
|
12
|
+
all_positional = positional_args + positional
|
13
|
+
all_kw_args = keyword_args.merge(keyword)
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
validate_positional_arg_count!(all_positional.size)
|
16
|
+
|
17
|
+
if partially_applied?(all_positional, all_kw_args)
|
18
|
+
PartiallyApplied.new(operation_class, *all_positional, **all_kw_args)
|
19
19
|
else
|
20
|
-
Prepared.new(
|
20
|
+
Prepared.new(operation_class, *all_positional, **all_kw_args)
|
21
21
|
end
|
22
22
|
end
|
23
|
-
alias_method :[], :
|
24
|
-
|
23
|
+
alias_method :[], :with
|
24
|
+
|
25
|
+
def curry
|
26
|
+
Curried.new(operation_class, self)
|
27
|
+
end
|
25
28
|
|
26
29
|
def call(...)
|
27
|
-
prepared =
|
30
|
+
prepared = with(...)
|
28
31
|
return prepared.operation.call if prepared.is_a?(Prepared)
|
29
|
-
raise "Cannot call PartiallyApplied operation #{
|
32
|
+
raise MissingParameterError, "Cannot call PartiallyApplied operation #{operation_class.name} (key: #{operation_class.name}), are you expecting it to be Prepared?"
|
33
|
+
end
|
34
|
+
|
35
|
+
def operation
|
36
|
+
raise MissingParameterError, "Cannot instantiate Operation #{operation_class.name} (key: #{operation_class.name}), as it is only partially applied."
|
37
|
+
end
|
38
|
+
|
39
|
+
def prepared?
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_proc
|
44
|
+
method(:call).to_proc
|
45
|
+
end
|
46
|
+
|
47
|
+
def deconstruct
|
48
|
+
positional_args + keyword_args.values
|
49
|
+
end
|
50
|
+
|
51
|
+
def deconstruct_keys(keys)
|
52
|
+
h = keyword_args.dup
|
53
|
+
positional_args.each_with_index { |v, i| h[positional_parameters[i]] = v }
|
54
|
+
keys ? h.slice(*keys) : h
|
55
|
+
end
|
56
|
+
|
57
|
+
attr_reader :positional_args, :keyword_args
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
attr_reader :operation_class
|
62
|
+
|
63
|
+
def required_positional_parameters
|
64
|
+
@required_positional_parameters ||= operation_class.required_positional_parameters
|
65
|
+
end
|
66
|
+
|
67
|
+
def required_keyword_parameters
|
68
|
+
@required_keyword_parameters ||= operation_class.required_keyword_parameters
|
69
|
+
end
|
70
|
+
|
71
|
+
def positional_parameters
|
72
|
+
@positional_parameters ||= operation_class.positional_parameters
|
73
|
+
end
|
74
|
+
|
75
|
+
def validate_positional_arg_count!(count)
|
76
|
+
if count > positional_parameters.size
|
77
|
+
raise ArgumentError, "Too many positional arguments provided for #{operation_class.name} (key: #{operation_class.name})"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def partially_applied?(all_positional, all_kw_args)
|
82
|
+
missing_positional = required_positional_parameters.size - all_positional.size
|
83
|
+
missing_keys = required_keyword_parameters - all_kw_args.keys
|
84
|
+
|
85
|
+
missing_positional > 0 || missing_keys.size > 0
|
30
86
|
end
|
31
87
|
end
|
32
88
|
end
|