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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +76 -724
  3. data/lib/generators/typed_operation/install/install_generator.rb +3 -0
  4. data/lib/generators/typed_operation_generator.rb +8 -4
  5. data/lib/typed_operation/action_policy_auth.rb +44 -24
  6. data/lib/typed_operation/base.rb +4 -1
  7. data/lib/typed_operation/callable_resolver.rb +30 -0
  8. data/lib/typed_operation/chains/chained_operation.rb +27 -0
  9. data/lib/typed_operation/chains/fallback_chain.rb +32 -0
  10. data/lib/typed_operation/chains/map_chain.rb +37 -0
  11. data/lib/typed_operation/chains/sequence_chain.rb +54 -0
  12. data/lib/typed_operation/chains/smart_chain.rb +161 -0
  13. data/lib/typed_operation/chains/splat_chain.rb +53 -0
  14. data/lib/typed_operation/configuration.rb +52 -0
  15. data/lib/typed_operation/context.rb +193 -0
  16. data/lib/typed_operation/curried.rb +14 -1
  17. data/lib/typed_operation/explainable.rb +14 -0
  18. data/lib/typed_operation/immutable_base.rb +4 -1
  19. data/lib/typed_operation/instrumentation/trace.rb +71 -0
  20. data/lib/typed_operation/instrumentation/tree_formatter.rb +141 -0
  21. data/lib/typed_operation/instrumentation.rb +214 -0
  22. data/lib/typed_operation/operations/composition.rb +41 -0
  23. data/lib/typed_operation/operations/executable.rb +27 -1
  24. data/lib/typed_operation/operations/introspection.rb +9 -1
  25. data/lib/typed_operation/operations/lifecycle.rb +4 -1
  26. data/lib/typed_operation/operations/parameters.rb +11 -5
  27. data/lib/typed_operation/operations/partial_application.rb +4 -0
  28. data/lib/typed_operation/operations/property_builder.rb +46 -22
  29. data/lib/typed_operation/partially_applied.rb +33 -10
  30. data/lib/typed_operation/pipeline/builder.rb +88 -0
  31. data/lib/typed_operation/pipeline/chainable_wrapper.rb +23 -0
  32. data/lib/typed_operation/pipeline/empty_pipeline_chain.rb +25 -0
  33. data/lib/typed_operation/pipeline/step_wrapper.rb +94 -0
  34. data/lib/typed_operation/pipeline.rb +176 -0
  35. data/lib/typed_operation/prepared.rb +13 -0
  36. data/lib/typed_operation/railtie.rb +4 -0
  37. data/lib/typed_operation/result/adapters/built_in.rb +28 -0
  38. data/lib/typed_operation/result/adapters/dry_monads.rb +36 -0
  39. data/lib/typed_operation/result/failure.rb +78 -0
  40. data/lib/typed_operation/result/mixin.rb +24 -0
  41. data/lib/typed_operation/result/success.rb +75 -0
  42. data/lib/typed_operation/result.rb +39 -0
  43. data/lib/typed_operation/version.rb +5 -1
  44. data/lib/typed_operation.rb +18 -1
  45. metadata +27 -3
  46. 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] # Wraps signature in NilableType
11
- @positional = options[:positional] # Changes kind to positional
27
+ @optional = options[:optional]
28
+ @positional = options[:positional]
12
29
  @reader = options[:reader] || :public
13
- @default_key = options.key?(:default)
30
+ @has_default = options.key?(:default)
14
31
  @default = options[:default]
15
32
 
16
- prepare_type_signature_for_literal
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
- ->(v) { (v == Literal::Null || v.nil?) ? v : converter.call(v) }
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: default_value_for_literal,
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
- def prepare_type_signature_for_literal
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
- validate_positional_order_params! if @positional
60
+ validate_positional_order! if @positional
42
61
  end
43
62
 
44
- # If already wrapped in a Nilable then don't wrap again
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 = Literal::Types::UnionType.new(@signature, NilClass) if has_default_value_nil?
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
- 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
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
- @default_key
87
+ @has_default
88
+ end
89
+
90
+ #: () -> bool
91
+ def has_nil_default?
92
+ default_provided? && @default.nil?
70
93
  end
71
94
 
72
- def default_value_for_literal
73
- if has_default_value_nil? || type_nilable?
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} (key: #{operation_class.name}), are you expecting it to be Prepared?"
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} (key: #{operation_class.name}), as it is only partially applied."
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
- h = keyword_args.dup
53
- positional_args.each_with_index { |v, i| h[positional_parameters[i]] = v }
54
- keys ? h.slice(*keys) : h
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
- attr_reader :operation_class
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} (key: #{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