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,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
@@ -0,0 +1,39 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "result/success"
5
+ require_relative "result/failure"
6
+ require_relative "result/adapters/built_in"
7
+ require_relative "result/adapters/dry_monads"
8
+ require_relative "result/mixin"
9
+
10
+ module TypedOperation
11
+ # Result types for representing success and failure outcomes.
12
+ #
13
+ # TypedOperation provides built-in Success and Failure types that work
14
+ # out of the box. You can also configure it to use Dry::Monads or a
15
+ # custom adapter.
16
+ #
17
+ # @example Default usage (built-in types)
18
+ # result = MyPipeline.call(input)
19
+ # result.success? # => true or false
20
+ # result.value! # => the value (raises on failure)
21
+ #
22
+ # @example Configure to use Dry::Monads
23
+ # TypedOperation.configure do |config|
24
+ # config.result_adapter = :dry_monads
25
+ # end
26
+ #
27
+ # @example Pattern matching
28
+ # case result
29
+ # in Success[value]
30
+ # handle_success(value)
31
+ # in Failure[error]
32
+ # handle_failure(error)
33
+ # end
34
+ #
35
+ module Result
36
+ # Alias for easy access to UnwrapError from Result module
37
+ UnwrapError = TypedOperation::UnwrapError
38
+ end
39
+ end
@@ -1,3 +1,7 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
1
4
  module TypedOperation
2
- VERSION = "1.0.0.beta3"
5
+ #: String
6
+ VERSION = "1.0.0.beta4"
3
7
  end
@@ -1,20 +1,37 @@
1
+ # rbs_inline: enabled
2
+
1
3
  require "literal"
2
4
 
3
5
  require "typed_operation/version"
4
6
  require "typed_operation/railtie" if defined?(Rails::Railtie)
7
+ require "typed_operation/result"
8
+ require "typed_operation/configuration"
5
9
  require "typed_operation/operations/introspection"
6
10
  require "typed_operation/operations/parameters"
7
11
  require "typed_operation/operations/partial_application"
8
- require "typed_operation/operations/callable"
9
12
  require "typed_operation/operations/lifecycle"
10
13
  require "typed_operation/operations/property_builder"
11
14
  require "typed_operation/operations/executable"
15
+ require "typed_operation/operations/composition"
12
16
  require "typed_operation/curried"
17
+ require "typed_operation/instrumentation"
18
+ require "typed_operation/explainable"
13
19
  require "typed_operation/immutable_base"
14
20
  require "typed_operation/base"
15
21
  require "typed_operation/partially_applied"
16
22
  require "typed_operation/prepared"
23
+ require "typed_operation/callable_resolver"
24
+ require "typed_operation/chains/chained_operation"
25
+ require "typed_operation/chains/sequence_chain"
26
+ require "typed_operation/chains/splat_chain"
27
+ require "typed_operation/chains/smart_chain"
28
+ require "typed_operation/chains/map_chain"
29
+ require "typed_operation/chains/fallback_chain"
30
+ require "typed_operation/context"
31
+ require "typed_operation/pipeline"
17
32
 
33
+ # TypedOperation provides a framework for defining type-safe, composable operations
34
+ # with support for partial application, currying, and parameter validation.
18
35
  module TypedOperation
19
36
  class InvalidOperationError < StandardError; end
20
37
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typed_operation
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.beta3
4
+ version: 1.0.0.beta4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Ierodiaconou
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-07 00:00:00.000000000 Z
10
+ date: 2025-12-10 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: literal
@@ -49,9 +49,22 @@ files:
49
49
  - lib/typed_operation.rb
50
50
  - lib/typed_operation/action_policy_auth.rb
51
51
  - lib/typed_operation/base.rb
52
+ - lib/typed_operation/callable_resolver.rb
53
+ - lib/typed_operation/chains/chained_operation.rb
54
+ - lib/typed_operation/chains/fallback_chain.rb
55
+ - lib/typed_operation/chains/map_chain.rb
56
+ - lib/typed_operation/chains/sequence_chain.rb
57
+ - lib/typed_operation/chains/smart_chain.rb
58
+ - lib/typed_operation/chains/splat_chain.rb
59
+ - lib/typed_operation/configuration.rb
60
+ - lib/typed_operation/context.rb
52
61
  - lib/typed_operation/curried.rb
62
+ - lib/typed_operation/explainable.rb
53
63
  - lib/typed_operation/immutable_base.rb
54
- - lib/typed_operation/operations/callable.rb
64
+ - lib/typed_operation/instrumentation.rb
65
+ - lib/typed_operation/instrumentation/trace.rb
66
+ - lib/typed_operation/instrumentation/tree_formatter.rb
67
+ - lib/typed_operation/operations/composition.rb
55
68
  - lib/typed_operation/operations/executable.rb
56
69
  - lib/typed_operation/operations/introspection.rb
57
70
  - lib/typed_operation/operations/lifecycle.rb
@@ -59,8 +72,19 @@ files:
59
72
  - lib/typed_operation/operations/partial_application.rb
60
73
  - lib/typed_operation/operations/property_builder.rb
61
74
  - lib/typed_operation/partially_applied.rb
75
+ - lib/typed_operation/pipeline.rb
76
+ - lib/typed_operation/pipeline/builder.rb
77
+ - lib/typed_operation/pipeline/chainable_wrapper.rb
78
+ - lib/typed_operation/pipeline/empty_pipeline_chain.rb
79
+ - lib/typed_operation/pipeline/step_wrapper.rb
62
80
  - lib/typed_operation/prepared.rb
63
81
  - lib/typed_operation/railtie.rb
82
+ - lib/typed_operation/result.rb
83
+ - lib/typed_operation/result/adapters/built_in.rb
84
+ - lib/typed_operation/result/adapters/dry_monads.rb
85
+ - lib/typed_operation/result/failure.rb
86
+ - lib/typed_operation/result/mixin.rb
87
+ - lib/typed_operation/result/success.rb
64
88
  - lib/typed_operation/version.rb
65
89
  homepage: https://github.com/stevegeek/typed_operation
66
90
  licenses:
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TypedOperation
4
- module Operations
5
- module Callable
6
- def self.included(base)
7
- base.extend(CallableMethods)
8
- end
9
-
10
- module CallableMethods
11
- def call(...)
12
- new(...).call
13
- end
14
-
15
- def to_proc
16
- method(:call).to_proc
17
- end
18
- end
19
-
20
- include CallableMethods
21
- end
22
- end
23
- end