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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +79 -574
  3. data/lib/generators/templates/operation.rb +2 -2
  4. data/lib/generators/typed_operation/install/USAGE +1 -0
  5. data/lib/generators/typed_operation/install/install_generator.rb +8 -0
  6. data/lib/generators/typed_operation/install/templates/application_operation.rb +24 -2
  7. data/lib/generators/typed_operation_generator.rb +8 -4
  8. data/lib/typed_operation/action_policy_auth.rb +161 -0
  9. data/lib/typed_operation/base.rb +5 -13
  10. data/lib/typed_operation/callable_resolver.rb +30 -0
  11. data/lib/typed_operation/chains/chained_operation.rb +27 -0
  12. data/lib/typed_operation/chains/fallback_chain.rb +32 -0
  13. data/lib/typed_operation/chains/map_chain.rb +37 -0
  14. data/lib/typed_operation/chains/sequence_chain.rb +54 -0
  15. data/lib/typed_operation/chains/smart_chain.rb +161 -0
  16. data/lib/typed_operation/chains/splat_chain.rb +53 -0
  17. data/lib/typed_operation/configuration.rb +52 -0
  18. data/lib/typed_operation/context.rb +193 -0
  19. data/lib/typed_operation/curried.rb +14 -1
  20. data/lib/typed_operation/explainable.rb +14 -0
  21. data/lib/typed_operation/immutable_base.rb +5 -2
  22. data/lib/typed_operation/instrumentation/trace.rb +71 -0
  23. data/lib/typed_operation/instrumentation/tree_formatter.rb +141 -0
  24. data/lib/typed_operation/instrumentation.rb +214 -0
  25. data/lib/typed_operation/operations/composition.rb +41 -0
  26. data/lib/typed_operation/operations/executable.rb +55 -0
  27. data/lib/typed_operation/operations/introspection.rb +14 -8
  28. data/lib/typed_operation/operations/lifecycle.rb +5 -1
  29. data/lib/typed_operation/operations/parameters.rb +21 -6
  30. data/lib/typed_operation/operations/partial_application.rb +4 -0
  31. data/lib/typed_operation/operations/property_builder.rb +105 -0
  32. data/lib/typed_operation/partially_applied.rb +33 -10
  33. data/lib/typed_operation/pipeline/builder.rb +88 -0
  34. data/lib/typed_operation/pipeline/chainable_wrapper.rb +23 -0
  35. data/lib/typed_operation/pipeline/empty_pipeline_chain.rb +25 -0
  36. data/lib/typed_operation/pipeline/step_wrapper.rb +94 -0
  37. data/lib/typed_operation/pipeline.rb +176 -0
  38. data/lib/typed_operation/prepared.rb +13 -0
  39. data/lib/typed_operation/railtie.rb +4 -0
  40. data/lib/typed_operation/result/adapters/built_in.rb +28 -0
  41. data/lib/typed_operation/result/adapters/dry_monads.rb +36 -0
  42. data/lib/typed_operation/result/failure.rb +78 -0
  43. data/lib/typed_operation/result/mixin.rb +24 -0
  44. data/lib/typed_operation/result/success.rb +75 -0
  45. data/lib/typed_operation/result.rb +39 -0
  46. data/lib/typed_operation/version.rb +5 -1
  47. data/lib/typed_operation.rb +19 -6
  48. metadata +59 -18
  49. data/Rakefile +0 -17
  50. data/lib/tasks/typed_operation_tasks.rake +0 -4
  51. data/lib/typed_operation/operations/attribute_builder.rb +0 -75
  52. data/lib/typed_operation/operations/callable.rb +0 -27
  53. 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
@@ -1,4 +1,8 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
1
4
  module TypedOperation
5
+ # Rails integration for TypedOperation, registers generators.
2
6
  class Railtie < ::Rails::Railtie
3
7
  generators do
4
8
  require "generators/typed_operation/install/install_generator"
@@ -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