typed_operation 1.0.0.beta3 → 1.0.0.beta4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +76 -724
- data/lib/generators/typed_operation/install/install_generator.rb +3 -0
- data/lib/generators/typed_operation_generator.rb +8 -4
- data/lib/typed_operation/action_policy_auth.rb +44 -24
- data/lib/typed_operation/base.rb +4 -1
- data/lib/typed_operation/callable_resolver.rb +30 -0
- data/lib/typed_operation/chains/chained_operation.rb +27 -0
- data/lib/typed_operation/chains/fallback_chain.rb +32 -0
- data/lib/typed_operation/chains/map_chain.rb +37 -0
- data/lib/typed_operation/chains/sequence_chain.rb +54 -0
- data/lib/typed_operation/chains/smart_chain.rb +161 -0
- data/lib/typed_operation/chains/splat_chain.rb +53 -0
- data/lib/typed_operation/configuration.rb +52 -0
- data/lib/typed_operation/context.rb +193 -0
- data/lib/typed_operation/curried.rb +14 -1
- data/lib/typed_operation/explainable.rb +14 -0
- data/lib/typed_operation/immutable_base.rb +4 -1
- data/lib/typed_operation/instrumentation/trace.rb +71 -0
- data/lib/typed_operation/instrumentation/tree_formatter.rb +141 -0
- data/lib/typed_operation/instrumentation.rb +214 -0
- data/lib/typed_operation/operations/composition.rb +41 -0
- data/lib/typed_operation/operations/executable.rb +27 -1
- data/lib/typed_operation/operations/introspection.rb +9 -1
- data/lib/typed_operation/operations/lifecycle.rb +4 -1
- data/lib/typed_operation/operations/parameters.rb +11 -5
- data/lib/typed_operation/operations/partial_application.rb +4 -0
- data/lib/typed_operation/operations/property_builder.rb +46 -22
- data/lib/typed_operation/partially_applied.rb +33 -10
- data/lib/typed_operation/pipeline/builder.rb +88 -0
- data/lib/typed_operation/pipeline/chainable_wrapper.rb +23 -0
- data/lib/typed_operation/pipeline/empty_pipeline_chain.rb +25 -0
- data/lib/typed_operation/pipeline/step_wrapper.rb +94 -0
- data/lib/typed_operation/pipeline.rb +176 -0
- data/lib/typed_operation/prepared.rb +13 -0
- data/lib/typed_operation/railtie.rb +4 -0
- data/lib/typed_operation/result/adapters/built_in.rb +28 -0
- data/lib/typed_operation/result/adapters/dry_monads.rb +36 -0
- data/lib/typed_operation/result/failure.rb +78 -0
- data/lib/typed_operation/result/mixin.rb +24 -0
- data/lib/typed_operation/result/success.rb +75 -0
- data/lib/typed_operation/result.rb +39 -0
- data/lib/typed_operation/version.rb +5 -1
- data/lib/typed_operation.rb +18 -1
- metadata +27 -3
- data/lib/typed_operation/operations/callable.rb +0 -23
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module TypedOperation
|
|
5
|
+
module Operations
|
|
6
|
+
# Provides composition methods for chaining operations together.
|
|
7
|
+
# Included in operation classes and composed operations (ChainedOperation, Pipeline).
|
|
8
|
+
module Composition
|
|
9
|
+
# Smart sequential composition with context accumulation.
|
|
10
|
+
# Auto-detects whether to spread kwargs or pass as single value.
|
|
11
|
+
#: (?(singleton(Base) | Proc), **untyped) ?{ (untyped) -> untyped } -> SmartChain
|
|
12
|
+
def then(operation = nil, **extra_kwargs, &block)
|
|
13
|
+
SmartChain.new(self, operation || block, extra_kwargs)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Spreads the context as **kwargs to the next operation.
|
|
17
|
+
#: (?(singleton(Base) | Proc)) ?{ (untyped) -> untyped } -> SplatChain
|
|
18
|
+
def then_spreads(operation = nil, &block)
|
|
19
|
+
SplatChain.new(self, operation || block)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Passes the context as a single positional argument to the next operation.
|
|
23
|
+
#: (?(singleton(Base) | Proc)) ?{ (untyped) -> untyped } -> SequenceChain
|
|
24
|
+
def then_passes(operation = nil, &block)
|
|
25
|
+
SequenceChain.new(self, operation || block)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Transforms the success value, replacing the context entirely.
|
|
29
|
+
#: () { (untyped) -> untyped } -> MapChain
|
|
30
|
+
def transform(&block)
|
|
31
|
+
MapChain.new(self, block)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Provides a fallback when the operation fails.
|
|
35
|
+
#: (?(singleton(Base) | Proc)) ?{ (untyped) -> untyped } -> FallbackChain
|
|
36
|
+
def or_else(fallback = nil, &block)
|
|
37
|
+
FallbackChain.new(self, fallback || block)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -1,26 +1,52 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
module TypedOperation
|
|
4
5
|
module Operations
|
|
6
|
+
# Defines the execution lifecycle for operations with before/after hooks.
|
|
7
|
+
# Operations must implement #perform to define their core logic.
|
|
5
8
|
module Executable
|
|
9
|
+
#: (Module) -> void
|
|
10
|
+
def self.included(base)
|
|
11
|
+
base.extend(ClassMethods)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module ClassMethods
|
|
15
|
+
# @rbs!
|
|
16
|
+
# def new: (*untyped, **untyped) -> untyped
|
|
17
|
+
|
|
18
|
+
#: (*untyped, **untyped) -> untyped
|
|
19
|
+
def call(...) = new(...).call
|
|
20
|
+
|
|
21
|
+
#: () -> Proc
|
|
22
|
+
def to_proc = method(:call).to_proc
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
#: () -> untyped
|
|
6
26
|
def call
|
|
7
27
|
execute_operation
|
|
8
28
|
end
|
|
9
29
|
|
|
30
|
+
#: () -> Proc
|
|
31
|
+
def to_proc = method(:call).to_proc
|
|
32
|
+
|
|
33
|
+
#: () -> untyped
|
|
10
34
|
def execute_operation
|
|
11
35
|
before_execute_operation
|
|
12
36
|
retval = perform
|
|
13
37
|
after_execute_operation(retval)
|
|
14
38
|
end
|
|
15
39
|
|
|
40
|
+
#: () -> void
|
|
16
41
|
def before_execute_operation
|
|
17
|
-
# noop
|
|
18
42
|
end
|
|
19
43
|
|
|
44
|
+
#: (untyped) -> untyped
|
|
20
45
|
def after_execute_operation(retval)
|
|
21
46
|
retval
|
|
22
47
|
end
|
|
23
48
|
|
|
49
|
+
#: () -> untyped
|
|
24
50
|
def perform
|
|
25
51
|
raise InvalidOperationError, "Operation #{self.class} does not implement #perform"
|
|
26
52
|
end
|
|
@@ -1,33 +1,41 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
module TypedOperation
|
|
4
5
|
module Operations
|
|
5
|
-
# Introspection methods
|
|
6
|
+
# Introspection methods for querying operation parameters.
|
|
6
7
|
module Introspection
|
|
8
|
+
#: () -> Array[Symbol]
|
|
7
9
|
def positional_parameters
|
|
8
10
|
literal_properties.filter_map { |property| property.name if property.positional? }
|
|
9
11
|
end
|
|
10
12
|
|
|
13
|
+
#: () -> Array[Symbol]
|
|
11
14
|
def keyword_parameters
|
|
12
15
|
literal_properties.filter_map { |property| property.name if property.keyword? }
|
|
13
16
|
end
|
|
14
17
|
|
|
18
|
+
#: () -> Array[Literal::_Property]
|
|
15
19
|
def required_parameters
|
|
16
20
|
literal_properties.filter { |property| property.required? }
|
|
17
21
|
end
|
|
18
22
|
|
|
23
|
+
#: () -> Array[Symbol]
|
|
19
24
|
def required_positional_parameters
|
|
20
25
|
required_parameters.filter_map { |property| property.name if property.positional? }
|
|
21
26
|
end
|
|
22
27
|
|
|
28
|
+
#: () -> Array[Symbol]
|
|
23
29
|
def required_keyword_parameters
|
|
24
30
|
required_parameters.filter_map { |property| property.name if property.keyword? }
|
|
25
31
|
end
|
|
26
32
|
|
|
33
|
+
#: () -> Array[Symbol]
|
|
27
34
|
def optional_positional_parameters
|
|
28
35
|
positional_parameters - required_positional_parameters
|
|
29
36
|
end
|
|
30
37
|
|
|
38
|
+
#: () -> Array[Symbol]
|
|
31
39
|
def optional_keyword_parameters
|
|
32
40
|
keyword_parameters - required_keyword_parameters
|
|
33
41
|
end
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
module TypedOperation
|
|
4
5
|
module Operations
|
|
6
|
+
# Hooks into Literal's initialization lifecycle to call #prepare if defined.
|
|
5
7
|
module Lifecycle
|
|
6
|
-
# This is called by Literal on initialization of underlying Struct/Data
|
|
8
|
+
# This is called by Literal on initialization of underlying Struct/Data.
|
|
9
|
+
#: () -> void
|
|
7
10
|
def after_initialize
|
|
8
11
|
prepare if respond_to?(:prepare)
|
|
9
12
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
module TypedOperation
|
|
@@ -5,6 +6,7 @@ module TypedOperation
|
|
|
5
6
|
# Method to define parameters for your operation.
|
|
6
7
|
module Parameters
|
|
7
8
|
# Override literal `prop` to prevent creating writers (Literal::Data does this by default)
|
|
9
|
+
#: (Symbol, Literal::Types::_Matchable, ?Symbol, ?reader: Symbol, ?writer: Symbol | bool, ?default: untyped) -> void
|
|
8
10
|
def prop(name, type, kind = :keyword, reader: :public, writer: :public, default: nil)
|
|
9
11
|
if self < ImmutableBase
|
|
10
12
|
super(name, type, kind, reader:, default:)
|
|
@@ -13,25 +15,29 @@ module TypedOperation
|
|
|
13
15
|
end
|
|
14
16
|
end
|
|
15
17
|
|
|
16
|
-
# Parameter for keyword argument, or a positional argument if you use positional: true
|
|
17
|
-
# Required, but you can set a default or use optional: true if you want optional
|
|
18
|
+
# Parameter for keyword argument, or a positional argument if you use positional: true.
|
|
19
|
+
# Required, but you can set a default or use optional: true if you want optional.
|
|
20
|
+
#: (Symbol, ?Literal::Types::_Matchable, **untyped) ?{ (untyped) -> untyped } -> void
|
|
18
21
|
def param(name, signature = :any, **options, &converter)
|
|
19
22
|
PropertyBuilder.new(self, name, signature, options).define(&converter)
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
# Alternative DSL
|
|
23
26
|
|
|
24
|
-
# Parameter for positional argument
|
|
27
|
+
# Parameter for positional argument.
|
|
28
|
+
#: (Symbol, ?Literal::Types::_Matchable, **untyped) ?{ (untyped) -> untyped } -> void
|
|
25
29
|
def positional_param(name, signature = :any, **options, &converter)
|
|
26
30
|
param(name, signature, **options.merge(positional: true), &converter)
|
|
27
31
|
end
|
|
28
32
|
|
|
29
|
-
# Parameter for a keyword or named argument
|
|
33
|
+
# Parameter for a keyword or named argument.
|
|
34
|
+
#: (Symbol, ?Literal::Types::_Matchable, **untyped) ?{ (untyped) -> untyped } -> void
|
|
30
35
|
def named_param(name, signature = :any, **options, &converter)
|
|
31
36
|
param(name, signature, **options.merge(positional: false), &converter)
|
|
32
37
|
end
|
|
33
38
|
|
|
34
|
-
# Wrap a type signature in a NilableType meaning it is optional to TypedOperation
|
|
39
|
+
# Wrap a type signature in a NilableType meaning it is optional to TypedOperation.
|
|
40
|
+
#: (Literal::Types::_Matchable) -> Literal::Types::NilableType
|
|
35
41
|
def optional(type_signature)
|
|
36
42
|
Literal::Types::NilableType.new(type_signature)
|
|
37
43
|
end
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
module TypedOperation
|
|
4
5
|
module Operations
|
|
6
|
+
# Enables partial application of operation parameters via #with and currying via #curry.
|
|
5
7
|
module PartialApplication
|
|
8
|
+
#: (*untyped, **untyped) -> PartiallyApplied
|
|
6
9
|
def with(...)
|
|
7
10
|
PartiallyApplied.new(self, ...).with
|
|
8
11
|
end
|
|
9
12
|
alias_method :[], :with
|
|
10
13
|
|
|
14
|
+
#: () -> Curried
|
|
11
15
|
def curry
|
|
12
16
|
Curried.new(self)
|
|
13
17
|
end
|
|
@@ -1,25 +1,43 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
module TypedOperation
|
|
4
5
|
module Operations
|
|
6
|
+
# Builds typed properties for operations, handling optional parameters,
|
|
7
|
+
# defaults, and type conversions with Literal types.
|
|
8
|
+
#
|
|
9
|
+
# @rbs generic D -- type of default value
|
|
5
10
|
class PropertyBuilder
|
|
11
|
+
include Literal::Types
|
|
12
|
+
|
|
13
|
+
# @rbs @typed_operation: untyped
|
|
14
|
+
# @rbs @name: Symbol
|
|
15
|
+
# @rbs @signature: untyped
|
|
16
|
+
# @rbs @optional: bool?
|
|
17
|
+
# @rbs @positional: bool?
|
|
18
|
+
# @rbs @reader: Symbol
|
|
19
|
+
# @rbs @has_default: bool
|
|
20
|
+
# @rbs @default: untyped
|
|
21
|
+
|
|
22
|
+
#: (singleton(Base) | singleton(ImmutableBase), Symbol, Literal::Types::_Matchable, Hash[Symbol, untyped]) -> void
|
|
6
23
|
def initialize(typed_operation, parameter_name, type_signature, options)
|
|
7
24
|
@typed_operation = typed_operation
|
|
8
25
|
@name = parameter_name
|
|
9
26
|
@signature = type_signature
|
|
10
|
-
@optional = options[:optional]
|
|
11
|
-
@positional = options[:positional]
|
|
27
|
+
@optional = options[:optional]
|
|
28
|
+
@positional = options[:positional]
|
|
12
29
|
@reader = options[:reader] || :public
|
|
13
|
-
@
|
|
30
|
+
@has_default = options.key?(:default)
|
|
14
31
|
@default = options[:default]
|
|
15
32
|
|
|
16
|
-
|
|
33
|
+
prepare_type_signature
|
|
17
34
|
end
|
|
18
35
|
|
|
36
|
+
#: () { (untyped) -> untyped } -> void
|
|
37
|
+
#: () -> void
|
|
19
38
|
def define(&converter)
|
|
20
|
-
# If nilable, then converter should not attempt to call the type converter block if the value is nil
|
|
21
39
|
coerce_by = if type_nilable? && converter
|
|
22
|
-
->(
|
|
40
|
+
->(value) { (value == Literal::Null || value.nil?) ? value : converter.call(value) }
|
|
23
41
|
else
|
|
24
42
|
converter
|
|
25
43
|
end
|
|
@@ -27,7 +45,7 @@ module TypedOperation
|
|
|
27
45
|
@name,
|
|
28
46
|
@signature,
|
|
29
47
|
@positional ? :positional : :keyword,
|
|
30
|
-
default:
|
|
48
|
+
default: resolved_default,
|
|
31
49
|
reader: @reader,
|
|
32
50
|
&coerce_by
|
|
33
51
|
)
|
|
@@ -35,43 +53,49 @@ module TypedOperation
|
|
|
35
53
|
|
|
36
54
|
private
|
|
37
55
|
|
|
38
|
-
|
|
56
|
+
#: () -> void
|
|
57
|
+
def prepare_type_signature
|
|
39
58
|
@signature = Literal::Types::NilableType.new(@signature) if needs_to_be_nilable?
|
|
40
59
|
union_with_nil_to_support_nil_default
|
|
41
|
-
|
|
60
|
+
validate_positional_order! if @positional
|
|
42
61
|
end
|
|
43
62
|
|
|
44
|
-
|
|
63
|
+
#: () -> bool
|
|
45
64
|
def needs_to_be_nilable?
|
|
46
|
-
@optional && !type_nilable?
|
|
65
|
+
!!(@optional && !type_nilable?)
|
|
47
66
|
end
|
|
48
67
|
|
|
68
|
+
#: () -> bool
|
|
49
69
|
def type_nilable?
|
|
50
70
|
@signature.is_a?(Literal::Types::NilableType)
|
|
51
71
|
end
|
|
52
72
|
|
|
73
|
+
#: () -> void
|
|
53
74
|
def union_with_nil_to_support_nil_default
|
|
54
|
-
@signature =
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def has_default_value_nil?
|
|
58
|
-
default_provided? && @default.nil?
|
|
75
|
+
@signature = _Union(@signature, NilClass) if has_nil_default?
|
|
59
76
|
end
|
|
60
77
|
|
|
61
|
-
|
|
62
|
-
|
|
78
|
+
#: () -> void
|
|
79
|
+
def validate_positional_order!
|
|
63
80
|
unless type_nilable? || @typed_operation.optional_positional_parameters.empty?
|
|
64
81
|
raise ParameterError, "Cannot define required positional parameter '#{@name}' after optional positional parameters"
|
|
65
82
|
end
|
|
66
83
|
end
|
|
67
84
|
|
|
85
|
+
#: () -> bool
|
|
68
86
|
def default_provided?
|
|
69
|
-
@
|
|
87
|
+
@has_default
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
#: () -> bool
|
|
91
|
+
def has_nil_default?
|
|
92
|
+
default_provided? && @default.nil?
|
|
70
93
|
end
|
|
71
94
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
95
|
+
#: () -> (D | Proc | nil)
|
|
96
|
+
def resolved_default
|
|
97
|
+
if has_nil_default? || (type_nilable? && !default_provided?)
|
|
98
|
+
-> {} # 'nil' as in a proc that returns nil
|
|
75
99
|
else
|
|
76
100
|
@default
|
|
77
101
|
end
|
|
@@ -1,13 +1,28 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
module TypedOperation
|
|
5
|
+
# Represents an operation with some, but not all, required parameters provided.
|
|
6
|
+
# Allows incrementally building operation arguments before execution.
|
|
4
7
|
class PartiallyApplied
|
|
8
|
+
include Operations::Composition
|
|
9
|
+
|
|
10
|
+
# @rbs @operation_class: untyped
|
|
11
|
+
# @rbs @positional_args: Array[untyped]
|
|
12
|
+
# @rbs @keyword_args: Hash[Symbol, untyped]
|
|
13
|
+
|
|
14
|
+
attr_reader :positional_args #: Array[untyped]
|
|
15
|
+
attr_reader :keyword_args #: Hash[Symbol, untyped]
|
|
16
|
+
attr_reader :operation_class #: untyped
|
|
17
|
+
|
|
18
|
+
#: (untyped, *untyped, **untyped) -> void
|
|
5
19
|
def initialize(operation_class, *positional_args, **keyword_args)
|
|
6
20
|
@operation_class = operation_class
|
|
7
21
|
@positional_args = positional_args
|
|
8
22
|
@keyword_args = keyword_args
|
|
9
23
|
end
|
|
10
24
|
|
|
25
|
+
#: (*untyped, **untyped) -> (PartiallyApplied | Prepared)
|
|
11
26
|
def with(*positional, **keyword)
|
|
12
27
|
all_positional = positional_args + positional
|
|
13
28
|
all_kw_args = keyword_args.merge(keyword)
|
|
@@ -22,62 +37,70 @@ module TypedOperation
|
|
|
22
37
|
end
|
|
23
38
|
alias_method :[], :with
|
|
24
39
|
|
|
40
|
+
#: () -> Curried
|
|
25
41
|
def curry
|
|
26
42
|
Curried.new(operation_class, self)
|
|
27
43
|
end
|
|
28
44
|
|
|
45
|
+
#: (*untyped, **untyped) -> untyped
|
|
29
46
|
def call(...)
|
|
30
47
|
prepared = with(...)
|
|
31
48
|
return prepared.operation.call if prepared.is_a?(Prepared)
|
|
32
|
-
raise MissingParameterError, "Cannot call PartiallyApplied operation #{operation_class.name}
|
|
49
|
+
raise MissingParameterError, "Cannot call PartiallyApplied operation #{operation_class.name}, are you expecting it to be Prepared?"
|
|
33
50
|
end
|
|
34
51
|
|
|
52
|
+
#: () -> untyped
|
|
35
53
|
def operation
|
|
36
|
-
raise MissingParameterError, "Cannot instantiate Operation #{operation_class.name}
|
|
54
|
+
raise MissingParameterError, "Cannot instantiate Operation #{operation_class.name}, as it is only partially applied."
|
|
37
55
|
end
|
|
38
56
|
|
|
57
|
+
#: () -> false
|
|
39
58
|
def prepared?
|
|
40
59
|
false
|
|
41
60
|
end
|
|
42
61
|
|
|
62
|
+
#: () -> Proc
|
|
43
63
|
def to_proc
|
|
44
64
|
method(:call).to_proc
|
|
45
65
|
end
|
|
46
66
|
|
|
67
|
+
#: () -> Array[untyped]
|
|
47
68
|
def deconstruct
|
|
48
69
|
positional_args + keyword_args.values
|
|
49
70
|
end
|
|
50
71
|
|
|
72
|
+
#: (Array[Symbol]?) -> Hash[Symbol, untyped]
|
|
51
73
|
def deconstruct_keys(keys)
|
|
52
|
-
|
|
53
|
-
positional_args.each_with_index { |
|
|
54
|
-
keys ?
|
|
74
|
+
hash = keyword_args.dup
|
|
75
|
+
positional_args.each_with_index { |value, index| hash[positional_parameters[index]] = value }
|
|
76
|
+
keys ? hash.slice(*keys) : hash
|
|
55
77
|
end
|
|
56
78
|
|
|
57
|
-
attr_reader :positional_args, :keyword_args
|
|
58
|
-
|
|
59
79
|
private
|
|
60
80
|
|
|
61
|
-
|
|
62
|
-
|
|
81
|
+
#: () -> Array[Symbol]
|
|
63
82
|
def required_positional_parameters
|
|
64
83
|
@required_positional_parameters ||= operation_class.required_positional_parameters
|
|
65
84
|
end
|
|
66
85
|
|
|
86
|
+
#: () -> Array[Symbol]
|
|
67
87
|
def required_keyword_parameters
|
|
68
88
|
@required_keyword_parameters ||= operation_class.required_keyword_parameters
|
|
69
89
|
end
|
|
70
90
|
|
|
91
|
+
#: () -> Array[Symbol]
|
|
71
92
|
def positional_parameters
|
|
72
93
|
@positional_parameters ||= operation_class.positional_parameters
|
|
73
94
|
end
|
|
74
95
|
|
|
96
|
+
#: (Integer) -> void
|
|
75
97
|
def validate_positional_arg_count!(count)
|
|
76
98
|
if count > positional_parameters.size
|
|
77
|
-
raise ArgumentError, "Too many positional arguments provided for #{operation_class.name}
|
|
99
|
+
raise ArgumentError, "Too many positional arguments provided for #{operation_class.name}"
|
|
78
100
|
end
|
|
79
101
|
end
|
|
80
102
|
|
|
103
|
+
#: (Array[untyped], Hash[Symbol, untyped]) -> bool
|
|
81
104
|
def partially_applied?(all_positional, all_kw_args)
|
|
82
105
|
missing_positional = required_positional_parameters.size - all_positional.size
|
|
83
106
|
missing_keys = required_keyword_parameters - all_kw_args.keys
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module TypedOperation
|
|
5
|
+
class Pipeline
|
|
6
|
+
# Builder class for the Pipeline DSL.
|
|
7
|
+
class Builder
|
|
8
|
+
# @rbs @steps: Array[Hash[Symbol, untyped]]
|
|
9
|
+
# @rbs @failure_handler: (^(untyped, Symbol) -> untyped)?
|
|
10
|
+
|
|
11
|
+
attr_reader :steps #: Array[Hash[Symbol, untyped]]
|
|
12
|
+
attr_reader :failure_handler #: (^(untyped, Symbol) -> untyped)?
|
|
13
|
+
|
|
14
|
+
#: () -> void
|
|
15
|
+
def initialize
|
|
16
|
+
@steps = []
|
|
17
|
+
@failure_handler = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Define a step in the pipeline.
|
|
21
|
+
#: (Symbol | untyped, ?untyped, ?if: (^(untyped) -> boolish)?) -> void
|
|
22
|
+
def step(name_or_operation, operation = nil, if: nil)
|
|
23
|
+
condition = binding.local_variable_get(:if)
|
|
24
|
+
|
|
25
|
+
@steps << if operation
|
|
26
|
+
{
|
|
27
|
+
type: :step,
|
|
28
|
+
name: name_or_operation,
|
|
29
|
+
operation: operation,
|
|
30
|
+
condition: condition
|
|
31
|
+
}
|
|
32
|
+
else
|
|
33
|
+
{
|
|
34
|
+
type: :step,
|
|
35
|
+
name: derive_name(name_or_operation),
|
|
36
|
+
operation: name_or_operation,
|
|
37
|
+
condition: condition
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Define a transform step that maps the success value.
|
|
43
|
+
#: () { (untyped) -> untyped } -> void
|
|
44
|
+
def transform(&block)
|
|
45
|
+
@steps << {
|
|
46
|
+
type: :transform,
|
|
47
|
+
name: :transform,
|
|
48
|
+
operation: block,
|
|
49
|
+
condition: nil
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Define a fallback for error recovery.
|
|
54
|
+
#: (?untyped) ?{ (untyped) -> untyped } -> void
|
|
55
|
+
def fallback(operation = nil, &block)
|
|
56
|
+
@steps << {
|
|
57
|
+
type: :fallback,
|
|
58
|
+
name: operation ? derive_name(operation) : :fallback,
|
|
59
|
+
operation: operation || block,
|
|
60
|
+
condition: nil
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
alias_method :or_else, :fallback
|
|
64
|
+
|
|
65
|
+
# Define a failure handler for the pipeline.
|
|
66
|
+
#: () { (untyped, Symbol) -> untyped } -> void
|
|
67
|
+
def on_failure(&block)
|
|
68
|
+
@failure_handler = block
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
#: (untyped) -> Symbol
|
|
74
|
+
def derive_name(operation)
|
|
75
|
+
return :anonymous unless operation.respond_to?(:name) && operation.name
|
|
76
|
+
|
|
77
|
+
operation.name
|
|
78
|
+
.split("::")
|
|
79
|
+
.last
|
|
80
|
+
.gsub(/Operation$/, "")
|
|
81
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
82
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
83
|
+
.downcase
|
|
84
|
+
.to_sym
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module TypedOperation
|
|
5
|
+
class Pipeline
|
|
6
|
+
# Wrapper that exposes full Composition methods while delegating to Pipeline.
|
|
7
|
+
class ChainableWrapper
|
|
8
|
+
include Operations::Composition
|
|
9
|
+
|
|
10
|
+
# @rbs @pipeline: Pipeline
|
|
11
|
+
|
|
12
|
+
#: (Pipeline) -> void
|
|
13
|
+
def initialize(pipeline)
|
|
14
|
+
@pipeline = pipeline
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
#: (*untyped, **untyped) -> untyped
|
|
18
|
+
def call(...)
|
|
19
|
+
@pipeline.call(...)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module TypedOperation
|
|
5
|
+
class Pipeline
|
|
6
|
+
# Handles empty pipelines - just passes through input as Success.
|
|
7
|
+
class EmptyPipelineChain
|
|
8
|
+
include Result::Mixin
|
|
9
|
+
|
|
10
|
+
#: (*untyped, **untyped) -> untyped
|
|
11
|
+
def call(*args, **kwargs)
|
|
12
|
+
value = if kwargs.any?
|
|
13
|
+
kwargs
|
|
14
|
+
elsif args.size == 1
|
|
15
|
+
args.first
|
|
16
|
+
elsif args.any?
|
|
17
|
+
args
|
|
18
|
+
else
|
|
19
|
+
{}
|
|
20
|
+
end
|
|
21
|
+
Success(value)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module TypedOperation
|
|
5
|
+
class Pipeline
|
|
6
|
+
# Wraps a step operation to handle conditions and error attribution.
|
|
7
|
+
class StepWrapper
|
|
8
|
+
include Result::Mixin
|
|
9
|
+
include CallableResolver
|
|
10
|
+
|
|
11
|
+
# @rbs @operation: untyped
|
|
12
|
+
# @rbs @name: Symbol
|
|
13
|
+
# @rbs @condition: (^(untyped) -> bool)?
|
|
14
|
+
# @rbs @failure_handler: (^(untyped, Symbol) -> untyped)?
|
|
15
|
+
# @rbs @first_step: bool
|
|
16
|
+
# @rbs @uses_kwargs: bool
|
|
17
|
+
|
|
18
|
+
#: (untyped, name: Symbol, condition: (^(untyped) -> bool)?, failure_handler: (^(untyped, Symbol) -> untyped)?, ?first_step: bool) -> void
|
|
19
|
+
def initialize(operation, name:, condition:, failure_handler:, first_step: false)
|
|
20
|
+
@operation = operation
|
|
21
|
+
@name = name
|
|
22
|
+
@condition = condition
|
|
23
|
+
@failure_handler = failure_handler
|
|
24
|
+
@first_step = first_step
|
|
25
|
+
@uses_kwargs = uses_kwargs?(operation)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
#: (*untyped, **untyped) -> untyped
|
|
29
|
+
def call(*args, **kwargs)
|
|
30
|
+
if @first_step
|
|
31
|
+
call_first_step(*args, **kwargs)
|
|
32
|
+
else
|
|
33
|
+
call_subsequent_step(args.first)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
#: (*untyped, **untyped) -> untyped
|
|
40
|
+
def call_first_step(*args, **kwargs)
|
|
41
|
+
context = kwargs.any? ? kwargs : (args.first || {})
|
|
42
|
+
|
|
43
|
+
if (condition = @condition) && !condition.call(context)
|
|
44
|
+
return Success(context)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
result = if args.empty? && kwargs.any?
|
|
48
|
+
@operation.call(**kwargs)
|
|
49
|
+
elsif args.any? && kwargs.empty?
|
|
50
|
+
@operation.call(*args)
|
|
51
|
+
elsif args.any? && kwargs.any?
|
|
52
|
+
@operation.call(*args, **kwargs)
|
|
53
|
+
else
|
|
54
|
+
@operation.call
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
handle_failure(result)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
#: (untyped) -> untyped
|
|
61
|
+
def call_subsequent_step(input)
|
|
62
|
+
context = normalize_input(input)
|
|
63
|
+
|
|
64
|
+
if (condition = @condition) && !condition.call(context)
|
|
65
|
+
return Success(context)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
result = if @uses_kwargs && context.is_a?(Hash)
|
|
69
|
+
@operation.call(**context)
|
|
70
|
+
else
|
|
71
|
+
@operation.call(context)
|
|
72
|
+
end
|
|
73
|
+
handle_failure(result)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
#: (untyped) -> untyped
|
|
77
|
+
def handle_failure(result)
|
|
78
|
+
if result.failure? && @failure_handler
|
|
79
|
+
return @failure_handler.call(result.failure, @name)
|
|
80
|
+
end
|
|
81
|
+
result
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
#: (untyped) -> untyped
|
|
85
|
+
def normalize_input(input)
|
|
86
|
+
case input
|
|
87
|
+
when Hash then input
|
|
88
|
+
when nil then {}
|
|
89
|
+
else input
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|