teckel 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|