teckel 0.0.1 → 0.1.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/.github/workflows/ci.yml +67 -0
- data/.github/workflows/pages.yml +50 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +12 -0
- data/.ruby-version +1 -0
- data/.yardopts +5 -0
- data/CHANGELOG.md +0 -0
- data/DEVELOPMENT.md +28 -0
- data/Gemfile +3 -1
- data/Gemfile.lock +71 -0
- data/LICENSE +201 -0
- data/README.md +62 -6
- data/Rakefile +25 -1
- data/bin/console +1 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +18 -0
- data/lib/teckel/chain.rb +186 -0
- data/lib/teckel/config.rb +69 -0
- data/lib/teckel/operation/results.rb +71 -0
- data/lib/teckel/operation.rb +340 -0
- data/lib/teckel/result.rb +63 -0
- data/lib/teckel/version.rb +3 -1
- data/lib/teckel.rb +8 -1
- data/teckel.gemspec +9 -4
- metadata +72 -7
- data/.travis.yml +0 -7
data/bin/rubocop
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/inline'
|
5
|
+
require 'bundler'
|
6
|
+
|
7
|
+
# We need the `Bundler.settings.temporary` for a bundler bug:
|
8
|
+
# https://github.com/bundler/bundler/issues/7114
|
9
|
+
# Will get fixed in bundler version 2.1.0
|
10
|
+
Bundler.settings.temporary(frozen: false) do
|
11
|
+
gemfile do
|
12
|
+
source 'https://rubygems.org'
|
13
|
+
gem 'rubocop', '~> 0.78.0'
|
14
|
+
gem 'relaxed-rubocop', '2.4'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
load Gem.bin_path("rubocop", "rubocop")
|
data/lib/teckel/chain.rb
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Teckel
|
6
|
+
# Railway style execution of multiple Operations.
|
7
|
+
#
|
8
|
+
# - Runs multiple Operations (steps) in order.
|
9
|
+
# - The output of an earlier step is passed as input to the next step.
|
10
|
+
# - Any failure will stop the execution chain (none of the later steps is called).
|
11
|
+
# - All Operations (steps) must behave like +Teckel::Operation::Results+ and
|
12
|
+
# return a result object like +Teckel::Result+
|
13
|
+
# - A failure response is wrapped into a +Teckel::Chain::StepFailure+ giving
|
14
|
+
# additional information about which step failed
|
15
|
+
#
|
16
|
+
# @see Teckel::Operation::Results
|
17
|
+
# @see Teckel::Result
|
18
|
+
# @see Teckel::Chain::StepFailure
|
19
|
+
#
|
20
|
+
# @example Defining a simple Chain with three steps
|
21
|
+
# class CreateUser
|
22
|
+
# include ::Teckel::Operation::Results
|
23
|
+
#
|
24
|
+
# input Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer.optional)
|
25
|
+
# output Types.Instance(User)
|
26
|
+
# error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
|
27
|
+
#
|
28
|
+
# def call(input)
|
29
|
+
# user = User.new(name: input[:name], age: input[:age])
|
30
|
+
# if user.safe
|
31
|
+
# success!(user)
|
32
|
+
# else
|
33
|
+
# fail!(message: "Could not safe User", errors: user.errors)
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# class LogUser
|
39
|
+
# include ::Teckel::Operation::Results
|
40
|
+
#
|
41
|
+
# input Types.Instance(User)
|
42
|
+
# output input
|
43
|
+
#
|
44
|
+
# def call(usr)
|
45
|
+
# Logger.new(File::NULL).info("User #{usr.name} created")
|
46
|
+
# usr # we need to return the correct output type
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# class AddFriend
|
51
|
+
# class << self
|
52
|
+
# # Don't actually do this! It's not safe and for generating the failure sample only.
|
53
|
+
# attr_accessor :fail_befriend
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# include ::Teckel::Operation::Results
|
57
|
+
#
|
58
|
+
# input Types.Instance(User)
|
59
|
+
# output Types::Hash.schema(user: Types.Instance(User), friend: Types.Instance(User))
|
60
|
+
# error Types::Hash.schema(message: Types::String)
|
61
|
+
#
|
62
|
+
# def call(user)
|
63
|
+
# if self.class.fail_befriend
|
64
|
+
# fail!(message: "Did not find a friend.")
|
65
|
+
# else
|
66
|
+
# { user: user, friend: User.new(name: "A friend", age: 42) }
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# class MyChain
|
72
|
+
# include Teckel::Chain
|
73
|
+
#
|
74
|
+
# step :create, CreateUser
|
75
|
+
# step :log, LogUser
|
76
|
+
# step :befriend, AddFriend
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# result = MyChain.call(name: "Bob", age: 23)
|
80
|
+
# result.is_a?(Teckel::Result) #=> true
|
81
|
+
# result.success[:user].is_a?(User) #=> true
|
82
|
+
# result.success[:friend].is_a?(User) #=> true
|
83
|
+
#
|
84
|
+
# AddFriend.fail_befriend = true
|
85
|
+
# failure_result = MyChain.call(name: "Bob", age: 23)
|
86
|
+
# failure_result.is_a?(Teckel::Chain::StepFailure) #=> true
|
87
|
+
#
|
88
|
+
# # additional step information
|
89
|
+
# failure_result.step_name #=> :befriend
|
90
|
+
# failure_result.step #=> AddFriend
|
91
|
+
#
|
92
|
+
# # otherwise behaves just like a normal +Result+
|
93
|
+
# failure_result.failure? #=> true
|
94
|
+
# failure_result.failure #=> {message: "Did not find a friend."}
|
95
|
+
module Chain
|
96
|
+
# Like +Teckel::Result+ but for failing Chains
|
97
|
+
#
|
98
|
+
# When a Chain fails, it stores the failed +Operation+ and it's name.
|
99
|
+
class StepFailure
|
100
|
+
extend Forwardable
|
101
|
+
|
102
|
+
def initialize(step, step_name, result)
|
103
|
+
@step, @step_name, @result = step, step_name, result
|
104
|
+
end
|
105
|
+
|
106
|
+
# @!attribute step [R]
|
107
|
+
# @return [Teckel::Operation] the failed Operation
|
108
|
+
attr_reader :step
|
109
|
+
|
110
|
+
# @!attribute step_name [R]
|
111
|
+
# @return [String] the step name of the failed Operation
|
112
|
+
attr_reader :step_name
|
113
|
+
|
114
|
+
# @!attribute result [R]
|
115
|
+
# @return [Teckel::Result] the failure Result
|
116
|
+
attr_reader :result
|
117
|
+
|
118
|
+
# @!method value
|
119
|
+
# Delegates to +result.value+
|
120
|
+
# @see Teckel::Result#value
|
121
|
+
# @!method successful?
|
122
|
+
# Delegates to +result.successful?+
|
123
|
+
# @see Teckel::Result#successful?
|
124
|
+
# @!method success
|
125
|
+
# Delegates to +result.success+
|
126
|
+
# @see Teckel::Result#success
|
127
|
+
# @!method failure?
|
128
|
+
# Delegates to +result.failure?+
|
129
|
+
# @see Teckel::Result#failure?
|
130
|
+
# @!method failure
|
131
|
+
# Delegates to +result.failure+
|
132
|
+
# @see Teckel::Result#failure
|
133
|
+
def_delegators :@result, :value, :successful?, :success, :failure?, :failure
|
134
|
+
end
|
135
|
+
|
136
|
+
module ClassMethods
|
137
|
+
def input
|
138
|
+
@steps.first&.last&.input
|
139
|
+
end
|
140
|
+
|
141
|
+
def output
|
142
|
+
@steps.last&.last&.output
|
143
|
+
end
|
144
|
+
|
145
|
+
def errors
|
146
|
+
@steps.each_with_object([]) do |e, m|
|
147
|
+
err = e.last&.error
|
148
|
+
m << err if err
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def call(input)
|
153
|
+
new.call!(@steps, input)
|
154
|
+
end
|
155
|
+
|
156
|
+
def step(name, operation)
|
157
|
+
@steps << [name, operation]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
module InstanceMethods
|
162
|
+
def call!(steps, input)
|
163
|
+
result = input
|
164
|
+
failed = nil
|
165
|
+
steps.each do |(name, step)|
|
166
|
+
result = step.call(result)
|
167
|
+
if result.failure?
|
168
|
+
failed = StepFailure.new(step, name, result)
|
169
|
+
break
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
failed || result
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def self.included(receiver)
|
178
|
+
receiver.extend ClassMethods
|
179
|
+
receiver.send :include, InstanceMethods
|
180
|
+
|
181
|
+
receiver.class_eval do
|
182
|
+
@steps = []
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Teckel
|
4
|
+
class Config
|
5
|
+
class FrozenConfigError < Teckel::Error; end
|
6
|
+
|
7
|
+
@default_constructor = :[]
|
8
|
+
class << self
|
9
|
+
def default_constructor(sym_or_proc = nil)
|
10
|
+
return @default_constructor if sym_or_proc.nil?
|
11
|
+
|
12
|
+
@default_constructor = sym_or_proc
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@input_class = nil
|
18
|
+
@input_constructor = nil
|
19
|
+
|
20
|
+
@output_class = nil
|
21
|
+
@output_constructor = nil
|
22
|
+
|
23
|
+
@error_class = nil
|
24
|
+
@error_constructor = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def input(klass = nil)
|
28
|
+
return @input_class if klass.nil?
|
29
|
+
raise FrozenConfigError unless @input_class.nil?
|
30
|
+
|
31
|
+
@input_class = klass
|
32
|
+
end
|
33
|
+
|
34
|
+
def input_constructor(sym_or_proc = nil)
|
35
|
+
return (@input_constructor || self.class.default_constructor) if sym_or_proc.nil?
|
36
|
+
raise FrozenConfigError unless @input_constructor.nil?
|
37
|
+
|
38
|
+
@input_constructor = sym_or_proc
|
39
|
+
end
|
40
|
+
|
41
|
+
def output(klass = nil)
|
42
|
+
return @output_class if klass.nil?
|
43
|
+
raise FrozenConfigError unless @output_class.nil?
|
44
|
+
|
45
|
+
@output_class = klass
|
46
|
+
end
|
47
|
+
|
48
|
+
def output_constructor(sym_or_proc = nil)
|
49
|
+
return (@output_constructor || self.class.default_constructor) if sym_or_proc.nil?
|
50
|
+
raise FrozenConfigError unless @output_constructor.nil?
|
51
|
+
|
52
|
+
@output_constructor = sym_or_proc
|
53
|
+
end
|
54
|
+
|
55
|
+
def error(klass = nil)
|
56
|
+
return @error_class if klass.nil?
|
57
|
+
raise FrozenConfigError unless @error_class.nil?
|
58
|
+
|
59
|
+
@error_class = klass
|
60
|
+
end
|
61
|
+
|
62
|
+
def error_constructor(sym_or_proc = nil)
|
63
|
+
return (@error_constructor || self.class.default_constructor) if sym_or_proc.nil?
|
64
|
+
raise FrozenConfigError unless @error_constructor.nil?
|
65
|
+
|
66
|
+
@error_constructor = sym_or_proc
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Teckel
|
4
|
+
module Operation
|
5
|
+
# Works just like +Teckel::Operation+, but wraps +output+ and +error+ into a +Teckel::Result+.
|
6
|
+
#
|
7
|
+
# A +Teckel::Result+ given as +input+ will get unwrapped, so that the original +value+
|
8
|
+
# gets passed to your Operation code.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
#
|
12
|
+
# class CreateUser
|
13
|
+
# include Teckel::Operation::Results
|
14
|
+
#
|
15
|
+
# input Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer)
|
16
|
+
# output Types.Instance(User)
|
17
|
+
# error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
|
18
|
+
#
|
19
|
+
# # @param [Hash<name: String, age: Integer>]
|
20
|
+
# # @return [User | Hash<message: String, errors: [Hash]>]
|
21
|
+
# def call(input)
|
22
|
+
# user = User.new(name: input[:name], age: input[:age])
|
23
|
+
# if user.safe
|
24
|
+
# # exits early with success, prevents any further execution
|
25
|
+
# success!(user)
|
26
|
+
# else
|
27
|
+
# fail!(message: "Could not safe User", errors: user.errors)
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# # A success call:
|
33
|
+
# CreateUser.call(name: "Bob", age: 23).is_a?(Teckel::Result) #=> true
|
34
|
+
# CreateUser.call(name: "Bob", age: 23).success.is_a?(User) #=> true
|
35
|
+
#
|
36
|
+
# # A failure call:
|
37
|
+
# CreateUser.call(name: "Bob", age: 10).is_a?(Teckel::Result) #=> true
|
38
|
+
# CreateUser.call(name: "Bob", age: 10).failure.is_a?(Hash) #=> true
|
39
|
+
#
|
40
|
+
# # Unwrapping success input:
|
41
|
+
# CreateUser.call(Teckel::Result.new({name: "Bob", age: 23}, true)).success.is_a?(User) #=> true
|
42
|
+
#
|
43
|
+
# # Unwrapping failure input:
|
44
|
+
# CreateUser.call(Teckel::Result.new({name: "Bob", age: 23}, false)).success.is_a?(User) #=> true
|
45
|
+
#
|
46
|
+
# @api public
|
47
|
+
module Results
|
48
|
+
module InstanceMethods
|
49
|
+
private
|
50
|
+
|
51
|
+
def build_input(input)
|
52
|
+
input = input.value if input.is_a?(Teckel::Result)
|
53
|
+
super(input)
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_output(*args)
|
57
|
+
Teckel::Result.new(super, true)
|
58
|
+
end
|
59
|
+
|
60
|
+
def build_error(*args)
|
61
|
+
Teckel::Result.new(super, false)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.included(receiver)
|
66
|
+
receiver.send :include, Teckel::Operation unless Teckel::Operation >= receiver
|
67
|
+
receiver.send :include, InstanceMethods
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,340 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Teckel
|
4
|
+
# The main operation Mixin
|
5
|
+
#
|
6
|
+
# Each operation is expected to declare +input+. +output+ and +error+ classes.
|
7
|
+
#
|
8
|
+
# There are two ways of declaring those classes. The first way is to define
|
9
|
+
# the constants +Input+, +Output+ and +Error+, the second way is to use the
|
10
|
+
# +input+. +output+ and +error+ methods to point them to anonymous classes.
|
11
|
+
#
|
12
|
+
# If you like "traditional" result objects (to ask +successful?+ or +failure?+ on)
|
13
|
+
# see +Teckel::Operation::Results+
|
14
|
+
#
|
15
|
+
# @see Teckel::Operation::Results
|
16
|
+
#
|
17
|
+
# @example class definitions via constants
|
18
|
+
# class CreateUserViaConstants
|
19
|
+
# include Teckel::Operation
|
20
|
+
#
|
21
|
+
# class Input
|
22
|
+
# def initialize(name:, age:)
|
23
|
+
# @name, @age = name, age
|
24
|
+
# end
|
25
|
+
# attr_reader :name, :age
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# Output = ::User
|
29
|
+
#
|
30
|
+
# class Error
|
31
|
+
# def initialize(message, errors)
|
32
|
+
# @message, @errors = message, errors
|
33
|
+
# end
|
34
|
+
# attr_reader :message, :errors
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# input_constructor :new
|
38
|
+
# error_constructor :new
|
39
|
+
#
|
40
|
+
# # @param [CreateUser::Input]
|
41
|
+
# # @return [User | CreateUser::Error]
|
42
|
+
# def call(input)
|
43
|
+
# user = ::User.new(name: input.name, age: input.age)
|
44
|
+
# if user.safe
|
45
|
+
# user
|
46
|
+
# else
|
47
|
+
# fail!(message: "Could not safe User", errors: user.errors)
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# CreateUserViaConstants.call(name: "Bob", age: 23).is_a?(User) #=> true
|
53
|
+
#
|
54
|
+
# @example class definitions via methods
|
55
|
+
# class CreateUserViaMethods
|
56
|
+
# include Teckel::Operation
|
57
|
+
#
|
58
|
+
# input Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer)
|
59
|
+
# output Types.Instance(User)
|
60
|
+
# error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
|
61
|
+
#
|
62
|
+
# # @param [Hash<name: String, age: Integer>]
|
63
|
+
# # @return [User | Hash<message: String, errors: [Hash]>]
|
64
|
+
# def call(input)
|
65
|
+
# user = User.new(name: input[:name], age: input[:age])
|
66
|
+
# if user.safe
|
67
|
+
# # exits early with success, prevents any further execution
|
68
|
+
# success!(user)
|
69
|
+
# else
|
70
|
+
# fail!(message: "Could not safe User", errors: user.errors)
|
71
|
+
# end
|
72
|
+
# end
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# # A success call:
|
76
|
+
# CreateUserViaMethods.call(name: "Bob", age: 23).is_a?(User) #=> true
|
77
|
+
#
|
78
|
+
# # A failure call:
|
79
|
+
# CreateUserViaMethods.call(name: "Bob", age: 10).eql?(message: "Could not safe User", errors: [{age: "underage"}]) #=> true
|
80
|
+
#
|
81
|
+
# # Build your Input, Output and Error classes in a way that let you know:
|
82
|
+
# begin; CreateUserViaMethods.call(unwanted: "input"); rescue => e; e end.is_a?(::Dry::Types::MissingKeyError) #=> true
|
83
|
+
#
|
84
|
+
# # Feed an instance of the input class directly to call:
|
85
|
+
# CreateUserViaMethods.call(CreateUserViaMethods.input[name: "Bob", age: 23]).is_a?(User) #=> true
|
86
|
+
#
|
87
|
+
# @api public
|
88
|
+
module Operation
|
89
|
+
module ClassMethods
|
90
|
+
# @!attribute [r] input()
|
91
|
+
# Get the configured class wrapping the input data structure.
|
92
|
+
# @return [Class] The +input+ class
|
93
|
+
|
94
|
+
# @!method input(klass)
|
95
|
+
# Set the class wrapping the input data structure.
|
96
|
+
# @param klass [Class] The +input+ class
|
97
|
+
# @return [Class] The +input+ class
|
98
|
+
def input(klass = nil)
|
99
|
+
return @input_class if @input_class
|
100
|
+
|
101
|
+
@input_class = @config.input(klass)
|
102
|
+
@input_class ||= self::Input if const_defined?(:Input)
|
103
|
+
@input_class
|
104
|
+
end
|
105
|
+
|
106
|
+
# @!attribute [r] input_constructor()
|
107
|
+
# The callable constructor to build an instance of the +input+ class.
|
108
|
+
# @return [Class] The Input class
|
109
|
+
|
110
|
+
# @!method input_constructor(sym_or_proc)
|
111
|
+
# Define how to build the +input+.
|
112
|
+
# @param sym_or_proc [Symbol|#call]
|
113
|
+
# - Either a +Symbol+ representing the _public_ method to call on the +input+ class.
|
114
|
+
# - Or a callable (like a +Proc+).
|
115
|
+
# @return [#call] The callable constructor
|
116
|
+
#
|
117
|
+
# @example simple symbol to method constructor
|
118
|
+
# class MyOperation
|
119
|
+
# include Teckel::Operation
|
120
|
+
#
|
121
|
+
# class Input
|
122
|
+
# # ...
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# # If you need more control over how to build a new +Input+ instance
|
126
|
+
# # MyOperation.call(name: "Bob", age: 23) # -> Input.new(name: "Bob", age: 23)
|
127
|
+
# input_constructor :new
|
128
|
+
# end
|
129
|
+
#
|
130
|
+
# MyOperation.input_constructor.is_a?(Method) #=> true
|
131
|
+
#
|
132
|
+
# @example Custom Proc constructor
|
133
|
+
# class MyOperation
|
134
|
+
# include Teckel::Operation
|
135
|
+
#
|
136
|
+
# class Input
|
137
|
+
# # ...
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# # If you need more control over how to build a new +Input+ instance
|
141
|
+
# # MyOperation.call("foo", opt: "bar") # -> Input.new(name: "foo", opt: "bar")
|
142
|
+
# input_constructor ->(name, options) { Input.new(name: name, **options) }
|
143
|
+
# end
|
144
|
+
#
|
145
|
+
# MyOperation.input_constructor.is_a?(Proc) #=> true
|
146
|
+
def input_constructor(sym_or_proc = nil)
|
147
|
+
return @input_constructor if @input_constructor
|
148
|
+
|
149
|
+
constructor = @config.input_constructor(sym_or_proc)
|
150
|
+
@input_constructor =
|
151
|
+
if constructor.is_a?(Symbol) && input.respond_to?(constructor)
|
152
|
+
input.public_method(constructor)
|
153
|
+
elsif sym_or_proc.respond_to?(:call)
|
154
|
+
sym_or_proc
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# @!attribute [r] output()
|
159
|
+
# Get the configured class wrapping the output data structure.
|
160
|
+
# @return [Class] The +output+ class
|
161
|
+
|
162
|
+
# @!method output(klass)
|
163
|
+
# Set the class wrapping the output data structure.
|
164
|
+
# @param klass [Class] The +output+ class
|
165
|
+
# @return [Class] The +output+ class
|
166
|
+
def output(klass = nil)
|
167
|
+
return @output_class if @output_class
|
168
|
+
|
169
|
+
@output_class = @config.output(klass)
|
170
|
+
@output_class ||= self::Output if const_defined?(:Output)
|
171
|
+
@output_class
|
172
|
+
end
|
173
|
+
|
174
|
+
# @!attribute [r] output_constructor()
|
175
|
+
# The callable constructor to build an instance of the +output+ class.
|
176
|
+
# @return [Class] The Output class
|
177
|
+
|
178
|
+
# @!method output_constructor(sym_or_proc)
|
179
|
+
# Define how to build the +output+.
|
180
|
+
# @param sym_or_proc [Symbol|#call]
|
181
|
+
# - Either a +Symbol+ representing the _public_ method to call on the +output+ class.
|
182
|
+
# - Or a callable (like a +Proc+).
|
183
|
+
# @return [#call] The callable constructor
|
184
|
+
#
|
185
|
+
# @example
|
186
|
+
# class MyOperation
|
187
|
+
# include Teckel::Operation
|
188
|
+
#
|
189
|
+
# class Output
|
190
|
+
# # ....
|
191
|
+
# end
|
192
|
+
#
|
193
|
+
# # MyOperation.call("foo", "bar") # -> Output.new("foo", "bar")
|
194
|
+
# output_constructor :new
|
195
|
+
#
|
196
|
+
# # If you need more control over how to build a new +Output+ instance
|
197
|
+
# # MyOperation.call("foo", opt: "bar") # -> Output.new(name: "foo", opt: "bar")
|
198
|
+
# output_constructor ->(name, options) { Output.new(name: name, **options) }
|
199
|
+
# end
|
200
|
+
def output_constructor(sym_or_proc = nil)
|
201
|
+
return @output_constructor if @output_constructor
|
202
|
+
|
203
|
+
constructor = @config.output_constructor(sym_or_proc)
|
204
|
+
@output_constructor =
|
205
|
+
if constructor.is_a?(Symbol) && output.respond_to?(constructor)
|
206
|
+
output.public_method(constructor)
|
207
|
+
elsif sym_or_proc.respond_to?(:call)
|
208
|
+
sym_or_proc
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# @!attribute [r] error()
|
213
|
+
# Get the configured class wrapping the error data structure.
|
214
|
+
# @return [Class] The +error+ class
|
215
|
+
|
216
|
+
# @!method error(klass)
|
217
|
+
# Set the class wrapping the error data structure.
|
218
|
+
# @param klass [Class] The +error+ class
|
219
|
+
# @return [Class] The +error+ class
|
220
|
+
def error(klass = nil)
|
221
|
+
return @error_class if @error_class
|
222
|
+
|
223
|
+
@error_class = @config.error(klass)
|
224
|
+
@error_class ||= self::Error if const_defined?(:Error)
|
225
|
+
@error_class
|
226
|
+
end
|
227
|
+
|
228
|
+
# @!attribute [r] error_constructor()
|
229
|
+
# The callable constructor to build an instance of the +error+ class.
|
230
|
+
# @return [Class] The Error class
|
231
|
+
|
232
|
+
# @!method error_constructor(sym_or_proc)
|
233
|
+
# Define how to build the +error+.
|
234
|
+
# @param sym_or_proc [Symbol|#call]
|
235
|
+
# - Either a +Symbol+ representing the _public_ method to call on the +error+ class.
|
236
|
+
# - Or a callable (like a +Proc+).
|
237
|
+
# @return [#call] The callable constructor
|
238
|
+
#
|
239
|
+
# @example
|
240
|
+
# class MyOperation
|
241
|
+
# include Teckel::Operation
|
242
|
+
#
|
243
|
+
# class Error
|
244
|
+
# # ....
|
245
|
+
# end
|
246
|
+
#
|
247
|
+
# # MyOperation.call("foo", "bar") # -> Error.new("foo", "bar")
|
248
|
+
# error_constructor :new
|
249
|
+
#
|
250
|
+
# # If you need more control over how to build a new +Error+ instance
|
251
|
+
# # MyOperation.call("foo", opt: "bar") # -> Error.new(name: "foo", opt: "bar")
|
252
|
+
# error_constructor ->(name, options) { Error.new(name: name, **options) }
|
253
|
+
# end
|
254
|
+
def error_constructor(sym_or_proc = nil)
|
255
|
+
return @error_constructor if @error_constructor
|
256
|
+
|
257
|
+
constructor = @config.error_constructor(sym_or_proc)
|
258
|
+
@error_constructor =
|
259
|
+
if constructor.is_a?(Symbol) && error.respond_to?(constructor)
|
260
|
+
error.public_method(constructor)
|
261
|
+
elsif sym_or_proc.respond_to?(:call)
|
262
|
+
sym_or_proc
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Invoke the Operation
|
267
|
+
#
|
268
|
+
# @param input Any form of input your +input+ class can handle via the given +input_constructor+
|
269
|
+
# @return Either An instance of your defined +error+ class or +output+ class
|
270
|
+
def call(input)
|
271
|
+
new.call!(input)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
module InstanceMethods
|
276
|
+
# @!visibility protected
|
277
|
+
def call!(input)
|
278
|
+
catch(:failure) do
|
279
|
+
out = catch(:success) do
|
280
|
+
simple_ret = call(build_input(input))
|
281
|
+
build_output(simple_ret)
|
282
|
+
end
|
283
|
+
return out
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# @!visibility protected
|
288
|
+
def success!(*args)
|
289
|
+
throw :success, build_output(*args)
|
290
|
+
end
|
291
|
+
|
292
|
+
# @!visibility protected
|
293
|
+
def fail!(*args)
|
294
|
+
throw :failure, build_error(*args)
|
295
|
+
end
|
296
|
+
|
297
|
+
private
|
298
|
+
|
299
|
+
def build_input(input)
|
300
|
+
self.class.input_constructor.call(input)
|
301
|
+
end
|
302
|
+
|
303
|
+
def build_output(*args)
|
304
|
+
if args.size == 1 && self.class.output === args.first # rubocop:disable Style/CaseEquality
|
305
|
+
args.first
|
306
|
+
else
|
307
|
+
self.class.output_constructor.call(*args)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def build_error(*args)
|
312
|
+
if args.size == 1 && self.class.error === args.first # rubocop:disable Style/CaseEquality
|
313
|
+
args.first
|
314
|
+
else
|
315
|
+
self.class.error_constructor.call(*args)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def self.included(receiver)
|
321
|
+
receiver.extend ClassMethods
|
322
|
+
receiver.send :include, InstanceMethods
|
323
|
+
|
324
|
+
receiver.class_eval do
|
325
|
+
@config = Config.new
|
326
|
+
|
327
|
+
@input_class = nil
|
328
|
+
@input_constructor = nil
|
329
|
+
|
330
|
+
@output_class = nil
|
331
|
+
@output_constructor = nil
|
332
|
+
|
333
|
+
@error_class = nil
|
334
|
+
@error_constructor = nil
|
335
|
+
|
336
|
+
protected :success!, :fail!
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|