typed_operation 1.0.0.pre3 → 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 +76 -624
- 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 +4 -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 +4 -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 +27 -1
- data/lib/typed_operation/operations/introspection.rb +14 -8
- data/lib/typed_operation/operations/lifecycle.rb +5 -2
- 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 +18 -6
- metadata +58 -18
- data/Rakefile +0 -17
- data/lib/tasks/typed_operation_tasks.rake +0 -4
- data/lib/typed_operation/operations/attribute_builder.rb +0 -81
- data/lib/typed_operation/operations/callable.rb +0 -23
- 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
|
|
@@ -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,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
|
-
|
|
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
|
-
|
|
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 { |
|
|
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 { |
|
|
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,10 +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
|
-
# This is called by Literal on initialization of underlying Struct/Data
|
|
7
|
-
|
|
8
|
+
# This is called by Literal on initialization of underlying Struct/Data.
|
|
9
|
+
#: () -> void
|
|
10
|
+
def after_initialize
|
|
8
11
|
prepare if respond_to?(:prepare)
|
|
9
12
|
end
|
|
10
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
|
-
#
|
|
8
|
-
|
|
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
|
-
|
|
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}
|
|
49
|
+
raise MissingParameterError, "Cannot call PartiallyApplied operation #{operation_class.name}, are you expecting it to be Prepared?"
|
|
33
50
|
end
|
|
34
51
|
|
|
52
|
+
#: () -> untyped
|
|
35
53
|
def operation
|
|
36
|
-
raise MissingParameterError, "Cannot instantiate Operation #{operation_class.name}
|
|
54
|
+
raise MissingParameterError, "Cannot instantiate Operation #{operation_class.name}, as it is only partially applied."
|
|
37
55
|
end
|
|
38
56
|
|
|
57
|
+
#: () -> false
|
|
39
58
|
def prepared?
|
|
40
59
|
false
|
|
41
60
|
end
|
|
42
61
|
|
|
62
|
+
#: () -> Proc
|
|
43
63
|
def to_proc
|
|
44
64
|
method(:call).to_proc
|
|
45
65
|
end
|
|
46
66
|
|
|
67
|
+
#: () -> Array[untyped]
|
|
47
68
|
def deconstruct
|
|
48
69
|
positional_args + keyword_args.values
|
|
49
70
|
end
|
|
50
71
|
|
|
72
|
+
#: (Array[Symbol]?) -> Hash[Symbol, untyped]
|
|
51
73
|
def deconstruct_keys(keys)
|
|
52
|
-
|
|
53
|
-
positional_args.each_with_index { |
|
|
54
|
-
keys ?
|
|
74
|
+
hash = keyword_args.dup
|
|
75
|
+
positional_args.each_with_index { |value, index| hash[positional_parameters[index]] = value }
|
|
76
|
+
keys ? hash.slice(*keys) : hash
|
|
55
77
|
end
|
|
56
78
|
|
|
57
|
-
attr_reader :positional_args, :keyword_args
|
|
58
|
-
|
|
59
79
|
private
|
|
60
80
|
|
|
61
|
-
|
|
62
|
-
|
|
81
|
+
#: () -> Array[Symbol]
|
|
63
82
|
def required_positional_parameters
|
|
64
83
|
@required_positional_parameters ||= operation_class.required_positional_parameters
|
|
65
84
|
end
|
|
66
85
|
|
|
86
|
+
#: () -> Array[Symbol]
|
|
67
87
|
def required_keyword_parameters
|
|
68
88
|
@required_keyword_parameters ||= operation_class.required_keyword_parameters
|
|
69
89
|
end
|
|
70
90
|
|
|
91
|
+
#: () -> Array[Symbol]
|
|
71
92
|
def positional_parameters
|
|
72
93
|
@positional_parameters ||= operation_class.positional_parameters
|
|
73
94
|
end
|
|
74
95
|
|
|
96
|
+
#: (Integer) -> void
|
|
75
97
|
def validate_positional_arg_count!(count)
|
|
76
98
|
if count > positional_parameters.size
|
|
77
|
-
raise ArgumentError, "Too many positional arguments provided for #{operation_class.name}
|
|
99
|
+
raise ArgumentError, "Too many positional arguments provided for #{operation_class.name}"
|
|
78
100
|
end
|
|
79
101
|
end
|
|
80
102
|
|
|
103
|
+
#: (Array[untyped], Hash[Symbol, untyped]) -> bool
|
|
81
104
|
def partially_applied?(all_positional, all_kw_args)
|
|
82
105
|
missing_positional = required_positional_parameters.size - all_positional.size
|
|
83
106
|
missing_keys = required_keyword_parameters - all_kw_args.keys
|