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,214 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "instrumentation/trace"
5
+ require_relative "instrumentation/tree_formatter"
6
+
7
+ module TypedOperation
8
+ # Provides runtime tracing and debugging capabilities for operations.
9
+ #
10
+ # Usage:
11
+ # # Trace a single operation call
12
+ # result = MyOperation.explain(param: value)
13
+ # # Prints execution tree to stdout, returns the operation result
14
+ #
15
+ # # Trace with partial application
16
+ # result = MyOperation.with(partial: value).explain(rest: value)
17
+ #
18
+ # # Trace a block of code (captures all instrumented operations)
19
+ # TypedOperation::Instrumentation.explaining do
20
+ # MyOperation.call(params)
21
+ # AnotherOperation.call(other_params)
22
+ # end
23
+ #
24
+ # # Configure output and color
25
+ # TypedOperation::Instrumentation.with_output(my_io, color: false) do
26
+ # MyOperation.explain(param: value)
27
+ # end
28
+ #
29
+ module Instrumentation
30
+ THREAD_KEY = :typed_operation_trace_stack #: Symbol
31
+ CHAIN_CONTEXT_KEY = :typed_operation_chain_context #: Symbol
32
+ OUTPUT_KEY = :typed_operation_output #: Symbol
33
+ COLOR_KEY = :typed_operation_color #: Symbol
34
+
35
+ class << self
36
+ # Execute a block with custom output and color settings.
37
+ #: [T] (IO, ?color: bool?) { () -> T } -> T
38
+ def with_output(io, color: nil, &block)
39
+ old_output = Thread.current[OUTPUT_KEY]
40
+ old_color = Thread.current[COLOR_KEY]
41
+ Thread.current[OUTPUT_KEY] = io
42
+ Thread.current[COLOR_KEY] = color
43
+ yield
44
+ ensure
45
+ Thread.current[OUTPUT_KEY] = old_output
46
+ Thread.current[COLOR_KEY] = old_color
47
+ end
48
+
49
+ # Execute a block with tracing enabled, printing the trace tree afterward.
50
+ #: [T] () { () -> T } -> T
51
+ def explaining(&block)
52
+ trace_root = Trace.new(operation_class: BlockExecution, params: {})
53
+ push_trace(trace_root)
54
+
55
+ result = nil
56
+ exception = nil
57
+ begin
58
+ result = block.call
59
+ rescue => e
60
+ exception = e
61
+ end
62
+
63
+ trace_root.finish!(result: result, exception: exception)
64
+ pop_trace
65
+
66
+ formatter = TreeFormatter.new(color: color)
67
+ trace_root.children.each do |child_trace|
68
+ output.puts formatter.format(child_trace)
69
+ end
70
+
71
+ raise exception if exception
72
+ result
73
+ end
74
+
75
+ #: () -> IO
76
+ def output
77
+ Thread.current[OUTPUT_KEY] || $stdout
78
+ end
79
+
80
+ #: () -> bool
81
+ def color
82
+ val = Thread.current[COLOR_KEY]
83
+ val.nil? || val
84
+ end
85
+
86
+ #: () -> Trace?
87
+ def current_trace
88
+ stack = Thread.current[THREAD_KEY]
89
+ stack&.last
90
+ end
91
+
92
+ #: () -> bool
93
+ def tracing?
94
+ !current_trace.nil?
95
+ end
96
+
97
+ #: (Trace) -> void
98
+ def push_trace(trace)
99
+ Thread.current[THREAD_KEY] ||= []
100
+ parent = current_trace
101
+ parent&.add_child(trace)
102
+ Thread.current[THREAD_KEY].push(trace)
103
+ end
104
+
105
+ #: () -> Trace?
106
+ def pop_trace
107
+ stack = Thread.current[THREAD_KEY]
108
+ stack&.pop
109
+ Thread.current[THREAD_KEY] = nil if stack&.empty?
110
+ end
111
+
112
+ #: () -> void
113
+ def clear_trace!
114
+ Thread.current[THREAD_KEY] = nil
115
+ Thread.current[CHAIN_CONTEXT_KEY] = nil
116
+ end
117
+
118
+ #: (pass_mode: Symbol, ?extracted_params: Array[Symbol]?, ?fallback_used: bool) -> void
119
+ def set_chain_context(pass_mode:, extracted_params: nil, fallback_used: false)
120
+ Thread.current[CHAIN_CONTEXT_KEY] = {
121
+ pass_mode: pass_mode,
122
+ extracted_params: extracted_params,
123
+ fallback_used: fallback_used
124
+ }
125
+ end
126
+
127
+ #: () -> Hash[Symbol, untyped]?
128
+ def take_chain_context
129
+ ctx = Thread.current[CHAIN_CONTEXT_KEY]
130
+ Thread.current[CHAIN_CONTEXT_KEY] = nil
131
+ ctx
132
+ end
133
+ end
134
+
135
+ # Placeholder class for block-based tracing
136
+ class BlockExecution; end
137
+
138
+ # Module to be prepended to operation classes to enable tracing.
139
+ # This is automatically included when using `explain`.
140
+ module Traceable
141
+ #: () -> untyped
142
+ def execute_operation
143
+ return super unless Instrumentation.tracing?
144
+
145
+ trace = Trace.new(
146
+ operation_class: self.class,
147
+ params: extract_params_for_trace
148
+ )
149
+
150
+ # Apply chain context if available
151
+ if (chain_ctx = Instrumentation.take_chain_context)
152
+ trace.pass_mode = chain_ctx[:pass_mode]
153
+ trace.extracted_params = chain_ctx[:extracted_params]
154
+ trace.fallback_used = chain_ctx[:fallback_used]
155
+ end
156
+
157
+ Instrumentation.push_trace(trace)
158
+
159
+ result = nil
160
+ exception = nil
161
+ begin
162
+ result = super
163
+ rescue => e
164
+ exception = e
165
+ end
166
+
167
+ trace.finish!(result: result, exception: exception)
168
+ Instrumentation.pop_trace
169
+
170
+ raise exception if exception
171
+ result
172
+ end
173
+
174
+ private
175
+
176
+ #: () -> Hash[Symbol, untyped]
177
+ def extract_params_for_trace
178
+ self.class.literal_properties.keys.each_with_object({}) do |key, hash|
179
+ hash[key] = send(key)
180
+ end
181
+ rescue
182
+ {}
183
+ end
184
+ end
185
+
186
+ # Class methods added to operations for the `explain` entry point.
187
+ module Explainable
188
+ #: (*untyped, **untyped) ?{ () -> untyped } -> untyped
189
+ def explain(*args, **kwargs, &block)
190
+ trace = Trace.new(operation_class: self, params: kwargs.merge(positional: args))
191
+ Instrumentation.push_trace(trace)
192
+
193
+ result = nil
194
+ exception = nil
195
+ begin
196
+ result = call(*args, **kwargs, &block)
197
+ rescue => e
198
+ exception = e
199
+ end
200
+
201
+ trace.finish!(result: result, exception: exception)
202
+ Instrumentation.pop_trace
203
+
204
+ formatter = TreeFormatter.new(color: Instrumentation.color)
205
+ Instrumentation.output.puts formatter.format(trace)
206
+
207
+ Instrumentation.clear_trace!
208
+
209
+ raise exception if exception
210
+ result
211
+ end
212
+ end
213
+ end
214
+ end
@@ -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
@@ -0,0 +1,55 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module TypedOperation
5
+ module Operations
6
+ # Defines the execution lifecycle for operations with before/after hooks.
7
+ # Operations must implement #perform to define their core logic.
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
26
+ def call
27
+ execute_operation
28
+ end
29
+
30
+ #: () -> Proc
31
+ def to_proc = method(:call).to_proc
32
+
33
+ #: () -> untyped
34
+ def execute_operation
35
+ before_execute_operation
36
+ retval = perform
37
+ after_execute_operation(retval)
38
+ end
39
+
40
+ #: () -> void
41
+ def before_execute_operation
42
+ end
43
+
44
+ #: (untyped) -> untyped
45
+ def after_execute_operation(retval)
46
+ retval
47
+ end
48
+
49
+ #: () -> untyped
50
+ def perform
51
+ raise InvalidOperationError, "Operation #{self.class} does not implement #perform"
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,35 +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
- literal_attributes.filter_map { |name, attribute| name if attribute.positional? }
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
- literal_attributes.filter_map { |name, attribute| name unless attribute.positional? }
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
- literal_attributes.filter do |name, attribute|
17
- attribute.default.nil? # Any optional parameters will have a default value/proc in their Literal::Attribute
18
- end
20
+ literal_properties.filter { |property| property.required? }
19
21
  end
20
22
 
23
+ #: () -> Array[Symbol]
21
24
  def required_positional_parameters
22
- required_parameters.filter_map { |name, attribute| name if attribute.positional? }
25
+ required_parameters.filter_map { |property| property.name if property.positional? }
23
26
  end
24
27
 
28
+ #: () -> Array[Symbol]
25
29
  def required_keyword_parameters
26
- required_parameters.filter_map { |name, attribute| name unless attribute.positional? }
30
+ required_parameters.filter_map { |property| property.name if property.keyword? }
27
31
  end
28
32
 
33
+ #: () -> Array[Symbol]
29
34
  def optional_positional_parameters
30
35
  positional_parameters - required_positional_parameters
31
36
  end
32
37
 
38
+ #: () -> Array[Symbol]
33
39
  def optional_keyword_parameters
34
40
  keyword_parameters - required_keyword_parameters
35
41
  end
@@ -1,9 +1,13 @@
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
- def after_initialization
8
+ # This is called by Literal on initialization of underlying Struct/Data.
9
+ #: () -> void
10
+ def after_initialize
7
11
  prepare if respond_to?(:prepare)
8
12
  end
9
13
  end
@@ -1,28 +1,43 @@
1
+ # rbs_inline: enabled
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module TypedOperation
4
5
  module Operations
5
6
  # Method to define parameters for your operation.
6
7
  module Parameters
7
- # Parameter for keyword argument, or a positional argument if you use positional: true
8
- # Required, but you can set a default or use optional: true if you want optional
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
10
+ def prop(name, type, kind = :keyword, reader: :public, writer: :public, default: nil)
11
+ if self < ImmutableBase
12
+ super(name, type, kind, reader:, default:)
13
+ else
14
+ super(name, type, kind, reader:, writer: false, default:)
15
+ end
16
+ end
17
+
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
9
21
  def param(name, signature = :any, **options, &converter)
10
- AttributeBuilder.new(self, name, signature, options).define(&converter)
22
+ PropertyBuilder.new(self, name, signature, options).define(&converter)
11
23
  end
12
24
 
13
25
  # Alternative DSL
14
26
 
15
- # Parameter for positional argument
27
+ # Parameter for positional argument.
28
+ #: (Symbol, ?Literal::Types::_Matchable, **untyped) ?{ (untyped) -> untyped } -> void
16
29
  def positional_param(name, signature = :any, **options, &converter)
17
30
  param(name, signature, **options.merge(positional: true), &converter)
18
31
  end
19
32
 
20
- # Parameter for a keyword or named argument
33
+ # Parameter for a keyword or named argument.
34
+ #: (Symbol, ?Literal::Types::_Matchable, **untyped) ?{ (untyped) -> untyped } -> void
21
35
  def named_param(name, signature = :any, **options, &converter)
22
36
  param(name, signature, **options.merge(positional: false), &converter)
23
37
  end
24
38
 
25
- # 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
26
41
  def optional(type_signature)
27
42
  Literal::Types::NilableType.new(type_signature)
28
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
@@ -0,0 +1,105 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module TypedOperation
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
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
23
+ def initialize(typed_operation, parameter_name, type_signature, options)
24
+ @typed_operation = typed_operation
25
+ @name = parameter_name
26
+ @signature = type_signature
27
+ @optional = options[:optional]
28
+ @positional = options[:positional]
29
+ @reader = options[:reader] || :public
30
+ @has_default = options.key?(:default)
31
+ @default = options[:default]
32
+
33
+ prepare_type_signature
34
+ end
35
+
36
+ #: () { (untyped) -> untyped } -> void
37
+ #: () -> void
38
+ def define(&converter)
39
+ coerce_by = if type_nilable? && converter
40
+ ->(value) { (value == Literal::Undefined || value.nil?) ? value : converter.call(value) }
41
+ else
42
+ converter
43
+ end
44
+ @typed_operation.prop(
45
+ @name,
46
+ @signature,
47
+ @positional ? :positional : :keyword,
48
+ default: resolved_default,
49
+ reader: @reader,
50
+ &coerce_by
51
+ )
52
+ end
53
+
54
+ private
55
+
56
+ #: () -> void
57
+ def prepare_type_signature
58
+ @signature = Literal::Types::NilableType.new(@signature) if needs_to_be_nilable?
59
+ union_with_nil_to_support_nil_default
60
+ validate_positional_order! if @positional
61
+ end
62
+
63
+ #: () -> bool
64
+ def needs_to_be_nilable?
65
+ !!(@optional && !type_nilable?)
66
+ end
67
+
68
+ #: () -> bool
69
+ def type_nilable?
70
+ @signature.is_a?(Literal::Types::NilableType)
71
+ end
72
+
73
+ #: () -> void
74
+ def union_with_nil_to_support_nil_default
75
+ @signature = _Union(@signature, NilClass) if has_nil_default?
76
+ end
77
+
78
+ #: () -> void
79
+ def validate_positional_order!
80
+ unless type_nilable? || @typed_operation.optional_positional_parameters.empty?
81
+ raise ParameterError, "Cannot define required positional parameter '#{@name}' after optional positional parameters"
82
+ end
83
+ end
84
+
85
+ #: () -> bool
86
+ def default_provided?
87
+ @has_default
88
+ end
89
+
90
+ #: () -> bool
91
+ def has_nil_default?
92
+ default_provided? && @default.nil?
93
+ end
94
+
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
99
+ else
100
+ @default
101
+ end
102
+ end
103
+ end
104
+ end
105
+ 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