typed_operation 1.0.0.pre2 → 1.0.0
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 +79 -574
- data/lib/generators/templates/operation.rb +2 -2
- data/lib/generators/typed_operation/install/USAGE +1 -0
- data/lib/generators/typed_operation/install/install_generator.rb +8 -0
- data/lib/generators/typed_operation/install/templates/application_operation.rb +24 -2
- data/lib/generators/typed_operation_generator.rb +8 -4
- data/lib/typed_operation/action_policy_auth.rb +161 -0
- data/lib/typed_operation/base.rb +5 -13
- 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 +5 -2
- 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 +55 -0
- data/lib/typed_operation/operations/introspection.rb +14 -8
- data/lib/typed_operation/operations/lifecycle.rb +5 -1
- data/lib/typed_operation/operations/parameters.rb +21 -6
- data/lib/typed_operation/operations/partial_application.rb +4 -0
- data/lib/typed_operation/operations/property_builder.rb +105 -0
- 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 +19 -6
- metadata +59 -18
- data/Rakefile +0 -17
- data/lib/tasks/typed_operation_tasks.rake +0 -4
- data/lib/typed_operation/operations/attribute_builder.rb +0 -75
- data/lib/typed_operation/operations/callable.rb +0 -27
- data/lib/typed_operation/operations/deconstruct.rb +0 -16
|
@@ -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
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "pipeline/step_wrapper"
|
|
5
|
+
require_relative "pipeline/empty_pipeline_chain"
|
|
6
|
+
require_relative "pipeline/chainable_wrapper"
|
|
7
|
+
require_relative "pipeline/builder"
|
|
8
|
+
|
|
9
|
+
module TypedOperation
|
|
10
|
+
# DSL for building declarative pipelines of operations.
|
|
11
|
+
# Pipeline is syntactic sugar over the composition/chaining system.
|
|
12
|
+
# Internally builds chains, adding named steps, conditions, and error attribution.
|
|
13
|
+
class Pipeline
|
|
14
|
+
# @rbs @chain: untyped
|
|
15
|
+
|
|
16
|
+
attr_reader :steps #: Array[Hash[Symbol, untyped]]
|
|
17
|
+
attr_reader :failure_handler #: (^(untyped, Symbol) -> untyped)?
|
|
18
|
+
|
|
19
|
+
# Build a new pipeline using the DSL.
|
|
20
|
+
#: () { () -> void } -> Pipeline
|
|
21
|
+
def self.build(&block)
|
|
22
|
+
builder = Builder.new
|
|
23
|
+
builder.instance_eval(&block)
|
|
24
|
+
new(builder.steps, builder.failure_handler)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
#: (?Array[Hash[Symbol, untyped]], ?(^(untyped, Symbol) -> untyped)?) -> void
|
|
28
|
+
def initialize(steps = [], failure_handler = nil)
|
|
29
|
+
@steps = steps.freeze
|
|
30
|
+
@failure_handler = failure_handler
|
|
31
|
+
@chain = nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Execute the pipeline by delegating to the internal chain.
|
|
35
|
+
#: (*untyped, **untyped) -> untyped
|
|
36
|
+
def call(*args, **kwargs)
|
|
37
|
+
chain.call(*args, **kwargs)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Append an operation as a new step, returning a new Pipeline.
|
|
41
|
+
#: (untyped, ?name: Symbol?, ?if: (^(untyped) -> boolish)?) -> Pipeline
|
|
42
|
+
def append(operation, name: nil, if: nil)
|
|
43
|
+
condition = binding.local_variable_get(:if)
|
|
44
|
+
new_step = {
|
|
45
|
+
type: :step,
|
|
46
|
+
name: name || derive_name(operation),
|
|
47
|
+
operation: operation,
|
|
48
|
+
condition: condition
|
|
49
|
+
}
|
|
50
|
+
self.class.new(@steps + [new_step], @failure_handler)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Compose with another pipeline, merging steps.
|
|
54
|
+
#: (Pipeline, ?on_failure: (:left | :right | ^(untyped, Symbol) -> untyped)?) -> Pipeline
|
|
55
|
+
def compose(other, on_failure: nil)
|
|
56
|
+
handler = resolve_failure_handlers(other, on_failure)
|
|
57
|
+
self.class.new(@steps + other.steps, handler)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Smart composition operator.
|
|
61
|
+
#: (Pipeline | untyped) -> Pipeline
|
|
62
|
+
def +(other)
|
|
63
|
+
case other
|
|
64
|
+
when Pipeline
|
|
65
|
+
compose(other)
|
|
66
|
+
else
|
|
67
|
+
append(other)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Convert to a bare chain for full Composition flexibility.
|
|
72
|
+
#: () -> ChainableWrapper
|
|
73
|
+
def to_chain
|
|
74
|
+
ChainableWrapper.new(self)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
#: () -> untyped
|
|
80
|
+
def chain
|
|
81
|
+
@chain ||= build_chain
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
#: () -> untyped
|
|
85
|
+
def build_chain
|
|
86
|
+
return EmptyPipelineChain.new if @steps.empty?
|
|
87
|
+
|
|
88
|
+
@steps.each_with_index.reduce(nil) do |current_chain, (step, index)|
|
|
89
|
+
is_first = index == 0
|
|
90
|
+
wrapped = wrap_step(step, first_step: is_first)
|
|
91
|
+
|
|
92
|
+
if current_chain.nil?
|
|
93
|
+
wrapped
|
|
94
|
+
else
|
|
95
|
+
combine_into_chain(current_chain, step, wrapped)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
#: (untyped, Hash[Symbol, untyped], untyped) -> untyped
|
|
101
|
+
def combine_into_chain(current_chain, step, wrapped)
|
|
102
|
+
case step[:type]
|
|
103
|
+
when :fallback
|
|
104
|
+
FallbackChain.new(current_chain, wrapped)
|
|
105
|
+
when :transform
|
|
106
|
+
MapChain.new(current_chain, wrapped)
|
|
107
|
+
else
|
|
108
|
+
SequenceChain.new(current_chain, wrapped)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
#: (Hash[Symbol, untyped], ?first_step: bool) -> untyped
|
|
113
|
+
def wrap_step(step, first_step: false)
|
|
114
|
+
case step[:type]
|
|
115
|
+
when :transform
|
|
116
|
+
step[:operation]
|
|
117
|
+
when :fallback
|
|
118
|
+
wrap_fallback(step)
|
|
119
|
+
else
|
|
120
|
+
StepWrapper.new(
|
|
121
|
+
step[:operation],
|
|
122
|
+
name: step[:name],
|
|
123
|
+
condition: step[:condition],
|
|
124
|
+
failure_handler: @failure_handler,
|
|
125
|
+
first_step: first_step
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
#: (Hash[Symbol, untyped]) -> untyped
|
|
131
|
+
def wrap_fallback(step)
|
|
132
|
+
if step[:operation].is_a?(Proc)
|
|
133
|
+
step[:operation]
|
|
134
|
+
else
|
|
135
|
+
StepWrapper.new(
|
|
136
|
+
step[:operation],
|
|
137
|
+
name: step[:name] || :fallback,
|
|
138
|
+
condition: nil,
|
|
139
|
+
failure_handler: @failure_handler
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
#: (Pipeline, (:left | :right | ^(untyped, Symbol) -> untyped)?) -> (^(untyped, Symbol) -> untyped)?
|
|
145
|
+
def resolve_failure_handlers(other, on_failure)
|
|
146
|
+
return @failure_handler if other.failure_handler.nil?
|
|
147
|
+
return other.failure_handler if @failure_handler.nil?
|
|
148
|
+
|
|
149
|
+
case on_failure
|
|
150
|
+
when :left then @failure_handler
|
|
151
|
+
when :right then other.failure_handler
|
|
152
|
+
when Proc then on_failure
|
|
153
|
+
when nil
|
|
154
|
+
raise ArgumentError,
|
|
155
|
+
"Both pipelines have failure handlers. Specify on_failure: :left, :right, or a Proc"
|
|
156
|
+
else
|
|
157
|
+
raise ArgumentError, "Invalid on_failure option: #{on_failure.inspect}"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
#: (untyped) -> Symbol
|
|
162
|
+
def derive_name(operation)
|
|
163
|
+
return :anonymous if operation.nil?
|
|
164
|
+
return :anonymous unless operation.respond_to?(:name) && operation.name
|
|
165
|
+
|
|
166
|
+
operation.name
|
|
167
|
+
.split("::")
|
|
168
|
+
.last
|
|
169
|
+
.gsub(/Operation$/, "")
|
|
170
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
171
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
172
|
+
.downcase
|
|
173
|
+
.to_sym
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -1,13 +1,26 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
module TypedOperation
|
|
5
|
+
# Represents an operation with all required parameters provided and ready for execution.
|
|
6
|
+
# Created when a PartiallyApplied operation receives its final required parameters.
|
|
4
7
|
class Prepared < PartiallyApplied
|
|
8
|
+
#: () -> TypedOperation::Base
|
|
5
9
|
def operation
|
|
6
10
|
operation_class.new(*@positional_args, **@keyword_args)
|
|
7
11
|
end
|
|
8
12
|
|
|
13
|
+
#: () -> true
|
|
9
14
|
def prepared?
|
|
10
15
|
true
|
|
11
16
|
end
|
|
17
|
+
|
|
18
|
+
#: () -> untyped
|
|
19
|
+
def explain
|
|
20
|
+
unless operation_class.respond_to?(:explain)
|
|
21
|
+
raise InvalidOperationError, "#{operation_class.name} does not support .explain. Include TypedOperation::Explainable in your operation class."
|
|
22
|
+
end
|
|
23
|
+
operation_class.explain(*@positional_args, **@keyword_args)
|
|
24
|
+
end
|
|
12
25
|
end
|
|
13
26
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module TypedOperation
|
|
5
|
+
module Result
|
|
6
|
+
module Adapters
|
|
7
|
+
# Default adapter using built-in Success/Failure classes.
|
|
8
|
+
class BuiltIn
|
|
9
|
+
# Create a Success result.
|
|
10
|
+
#: (untyped) -> Result::Success
|
|
11
|
+
def success(value)
|
|
12
|
+
Result::Success.new(value)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Create a Failure result.
|
|
16
|
+
#: (untyped) -> Result::Failure
|
|
17
|
+
def failure(error)
|
|
18
|
+
Result::Failure.new(error)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
#: () -> String
|
|
22
|
+
def name
|
|
23
|
+
"TypedOperation::Result (built-in)"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module TypedOperation
|
|
5
|
+
module Result
|
|
6
|
+
module Adapters
|
|
7
|
+
# Adapter for Dry::Monads Result type.
|
|
8
|
+
# Requires the dry-monads gem to be available.
|
|
9
|
+
# This is a lazy-loading adapter that only loads dry-monads when instantiated.
|
|
10
|
+
class DryMonads
|
|
11
|
+
#: () -> void
|
|
12
|
+
def initialize
|
|
13
|
+
require "dry-monads"
|
|
14
|
+
extend Dry::Monads[:result]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Create a Success result using Dry::Monads.
|
|
18
|
+
#: (untyped) -> untyped
|
|
19
|
+
def success(value)
|
|
20
|
+
Success(value)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Create a Failure result using Dry::Monads.
|
|
24
|
+
#: (untyped) -> untyped
|
|
25
|
+
def failure(error)
|
|
26
|
+
Failure(error)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#: () -> String
|
|
30
|
+
def name
|
|
31
|
+
"Dry::Monads::Result"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module TypedOperation
|
|
5
|
+
# Error raised when trying to unwrap a Failure result.
|
|
6
|
+
class UnwrapError < StandardError; end
|
|
7
|
+
|
|
8
|
+
module Result
|
|
9
|
+
# Represents a failed result. Immutable value object.
|
|
10
|
+
class Failure
|
|
11
|
+
# @rbs @error: untyped
|
|
12
|
+
|
|
13
|
+
attr_reader :error #: untyped
|
|
14
|
+
|
|
15
|
+
#: (untyped) -> void
|
|
16
|
+
def initialize(error)
|
|
17
|
+
@error = error
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
#: () -> false
|
|
22
|
+
def success?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#: () -> true
|
|
27
|
+
def failure?
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Raises UnwrapError since this is a Failure.
|
|
32
|
+
#: () -> bot
|
|
33
|
+
def value!
|
|
34
|
+
raise UnwrapError, "Cannot unwrap Failure: #{@error.inspect}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns the wrapped error.
|
|
38
|
+
#: () -> untyped
|
|
39
|
+
def failure
|
|
40
|
+
@error
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Pattern matching support - array destructuring.
|
|
44
|
+
#: () -> Array[untyped]
|
|
45
|
+
def deconstruct
|
|
46
|
+
@error.is_a?(::Array) ? @error : [@error]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Pattern matching support - hash destructuring.
|
|
50
|
+
# Delegates to the inner error if it responds to deconstruct_keys.
|
|
51
|
+
#: (Array[Symbol]?) -> Hash[Symbol, untyped]
|
|
52
|
+
def deconstruct_keys(keys)
|
|
53
|
+
if @error.respond_to?(:deconstruct_keys)
|
|
54
|
+
@error.deconstruct_keys(keys)
|
|
55
|
+
else
|
|
56
|
+
{error: @error, failure: @error}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
#: (untyped) -> bool
|
|
61
|
+
def ==(other)
|
|
62
|
+
other.is_a?(Failure) && other.error == @error
|
|
63
|
+
end
|
|
64
|
+
alias_method :eql?, :==
|
|
65
|
+
|
|
66
|
+
#: () -> Integer
|
|
67
|
+
def hash
|
|
68
|
+
[self.class, @error].hash
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
#: () -> String
|
|
72
|
+
def inspect
|
|
73
|
+
"Failure(#{@error.inspect})"
|
|
74
|
+
end
|
|
75
|
+
alias_method :to_s, :inspect
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module TypedOperation
|
|
5
|
+
module Result
|
|
6
|
+
# Mixin providing Success/Failure factory methods to chain classes.
|
|
7
|
+
# Include this module to call Success(value) and Failure(error) directly.
|
|
8
|
+
module Mixin
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# Create a Success result using the configured adapter.
|
|
12
|
+
#: (untyped) -> (Result::Success | untyped)
|
|
13
|
+
def Success(value)
|
|
14
|
+
TypedOperation.configuration.result_adapter.success(value)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Create a Failure result using the configured adapter.
|
|
18
|
+
#: (untyped) -> (Result::Failure | untyped)
|
|
19
|
+
def Failure(error)
|
|
20
|
+
TypedOperation.configuration.result_adapter.failure(error)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module TypedOperation
|
|
5
|
+
module Result
|
|
6
|
+
# Represents a successful result. Immutable value object.
|
|
7
|
+
class Success
|
|
8
|
+
# @rbs @value: untyped
|
|
9
|
+
|
|
10
|
+
attr_reader :value #: untyped
|
|
11
|
+
|
|
12
|
+
#: (untyped) -> void
|
|
13
|
+
def initialize(value)
|
|
14
|
+
@value = value
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
#: () -> true
|
|
19
|
+
def success?
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
#: () -> false
|
|
24
|
+
def failure?
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns the wrapped value.
|
|
29
|
+
#: () -> untyped
|
|
30
|
+
def value!
|
|
31
|
+
@value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns nil for Success.
|
|
35
|
+
#: () -> nil
|
|
36
|
+
def failure
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Pattern matching support - array destructuring.
|
|
41
|
+
#: () -> Array[untyped]
|
|
42
|
+
def deconstruct
|
|
43
|
+
@value.is_a?(::Array) ? @value : [@value]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Pattern matching support - hash destructuring.
|
|
47
|
+
# Delegates to the inner value if it responds to deconstruct_keys.
|
|
48
|
+
#: (Array[Symbol]?) -> Hash[Symbol, untyped]
|
|
49
|
+
def deconstruct_keys(keys)
|
|
50
|
+
if @value.respond_to?(:deconstruct_keys)
|
|
51
|
+
@value.deconstruct_keys(keys)
|
|
52
|
+
else
|
|
53
|
+
{value: @value}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
#: (untyped) -> bool
|
|
58
|
+
def ==(other)
|
|
59
|
+
other.is_a?(Success) && other.value == @value
|
|
60
|
+
end
|
|
61
|
+
alias_method :eql?, :==
|
|
62
|
+
|
|
63
|
+
#: () -> Integer
|
|
64
|
+
def hash
|
|
65
|
+
[self.class, @value].hash
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
#: () -> String
|
|
69
|
+
def inspect
|
|
70
|
+
"Success(#{@value.inspect})"
|
|
71
|
+
end
|
|
72
|
+
alias_method :to_s, :inspect
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|