teckel 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -0
  3. data/LICENSE_LOGO +4 -0
  4. data/README.md +4 -4
  5. data/lib/teckel.rb +9 -3
  6. data/lib/teckel/chain.rb +99 -271
  7. data/lib/teckel/chain/result.rb +38 -0
  8. data/lib/teckel/chain/runner.rb +51 -0
  9. data/lib/teckel/chain/step.rb +18 -0
  10. data/lib/teckel/config.rb +1 -23
  11. data/lib/teckel/contracts.rb +19 -0
  12. data/lib/teckel/operation.rb +309 -215
  13. data/lib/teckel/operation/result.rb +92 -0
  14. data/lib/teckel/operation/runner.rb +70 -0
  15. data/lib/teckel/result.rb +52 -53
  16. data/lib/teckel/version.rb +1 -1
  17. data/spec/chain/inheritance_spec.rb +116 -0
  18. data/spec/chain/results_spec.rb +53 -0
  19. data/spec/chain_around_hook_spec.rb +100 -0
  20. data/spec/chain_spec.rb +180 -0
  21. data/spec/config_spec.rb +26 -0
  22. data/spec/doctest_helper.rb +7 -0
  23. data/spec/operation/inheritance_spec.rb +94 -0
  24. data/spec/operation/result_spec.rb +34 -0
  25. data/spec/operation/results_spec.rb +117 -0
  26. data/spec/operation_spec.rb +485 -0
  27. data/spec/rb27/pattern_matching_spec.rb +193 -0
  28. data/spec/result_spec.rb +20 -0
  29. data/spec/spec_helper.rb +25 -0
  30. data/spec/support/dry_base.rb +8 -0
  31. data/spec/support/fake_db.rb +12 -0
  32. data/spec/support/fake_models.rb +20 -0
  33. data/spec/teckel_spec.rb +7 -0
  34. metadata +52 -25
  35. data/.codeclimate.yml +0 -3
  36. data/.github/workflows/ci.yml +0 -92
  37. data/.github/workflows/pages.yml +0 -50
  38. data/.gitignore +0 -15
  39. data/.rspec +0 -3
  40. data/.rubocop.yml +0 -12
  41. data/.ruby-version +0 -1
  42. data/DEVELOPMENT.md +0 -32
  43. data/Gemfile +0 -16
  44. data/Rakefile +0 -35
  45. data/bin/console +0 -15
  46. data/bin/rake +0 -29
  47. data/bin/rspec +0 -29
  48. data/bin/rubocop +0 -18
  49. data/bin/setup +0 -8
  50. data/lib/teckel/none.rb +0 -18
  51. data/lib/teckel/operation/results.rb +0 -72
  52. data/teckel.gemspec +0 -32
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module Teckel
6
+ module Chain
7
+ class Result < Teckel::Operation::Result
8
+ extend Forwardable
9
+
10
+ # @param value [Object] The result value
11
+ # @param success [Boolean] whether this is a successful result
12
+ # @param step [Teckel::Chain::Step]
13
+ def initialize(value, success, step)
14
+ super(value, success)
15
+ @step = step
16
+ end
17
+
18
+ class << self
19
+ alias :[] :new
20
+ end
21
+
22
+ # @!method step
23
+ # Delegates to +step.name+
24
+ # @return [String,Symbol] The step name of the failed operation.
25
+ def_delegator :@step, :name, :step
26
+
27
+ def deconstruct
28
+ [successful?, @step.name, value]
29
+ end
30
+
31
+ def deconstruct_keys(keys)
32
+ super.tap { |e|
33
+ e[:step] = @step.name if keys.include?(:step)
34
+ }
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teckel
4
+ module Chain
5
+ # The default implementation for executing a {Chain}
6
+ #
7
+ # @!visibility protected
8
+ class Runner
9
+ # @!visibility private
10
+ UNDEFINED = Object.new
11
+
12
+ def initialize(chain, settings = UNDEFINED)
13
+ @chain, @settings = chain, settings
14
+ end
15
+ attr_reader :chain, :settings
16
+
17
+ # Run steps
18
+ #
19
+ # @param input Any form of input the first steps +input+ class can handle
20
+ #
21
+ # @return [Teckel::Chain::Result] The result object wrapping
22
+ # either the success or failure value.
23
+ def call(input)
24
+ last_result = nil
25
+ last_step = nil
26
+ steps.each do |step|
27
+ last_step = step
28
+ value = last_result ? last_result.value : input
29
+
30
+ last_result = step.operation.call(value)
31
+
32
+ break if last_result.failure?
33
+ end
34
+
35
+ chain.result_constructor.call(last_result.value, last_result.successful?, last_step)
36
+ end
37
+
38
+ def steps
39
+ if settings == UNDEFINED
40
+ chain.steps
41
+ else
42
+ Enumerator.new do |yielder|
43
+ chain.steps.each do |step|
44
+ yielder << (settings.key?(step.name) ? step.with(settings[step.name]) : step)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teckel
4
+ module Chain
5
+ # Internal wrapper of a step definition
6
+ Step = Struct.new(:name, :operation) do
7
+ def finalize!
8
+ name.freeze
9
+ operation.finalize!
10
+ freeze
11
+ end
12
+
13
+ def with(settings)
14
+ self.class.new(name, operation.with(settings))
15
+ end
16
+ end
17
+ end
18
+ end
@@ -2,29 +2,6 @@
2
2
 
3
3
  module Teckel
4
4
  class Config
5
- @default_constructor = :[]
6
- class << self
7
- # @!attribute [r] default_constructor()
8
- # The default constructor method for +input+, +output+ and +error+ class (default: +:[]+)
9
- # @return [Class] The Output class
10
-
11
- # @!method default_constructor(sym_or_proc)
12
- # Set the default constructor method for +input+, +output+ and +error+ class
13
- #
14
- # defaults to +:[]+
15
- #
16
- # @param sym_or_proc [Symbol,#call] The method name on the +input+,
17
- # +output+ and +error+ class or a callable which accepts the
18
- # +input+, +output+ or +error+
19
- #
20
- # @return [Symbol,#call]
21
- def default_constructor(sym_or_proc = nil)
22
- return @default_constructor if sym_or_proc.nil?
23
-
24
- @default_constructor = sym_or_proc
25
- end
26
- end
27
-
28
5
  # @!visibility private
29
6
  def initialize
30
7
  @config = {}
@@ -38,6 +15,7 @@ module Teckel
38
15
  # - sets (and returns) the blocks return value otherwise
39
16
  # - calling without +value+ and +block+ works like {Hash#[]}
40
17
  #
18
+ # @raise [FrozenConfigError] When overwriting a key
41
19
  # @!visibility private
42
20
  def for(key, value = nil, &block)
43
21
  if value.nil?
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teckel
4
+ module Contracts
5
+ # Simple contract for enforcing data to be not set or +nil+
6
+ module None
7
+ class << self
8
+ # Always return nil
9
+ # @return nil
10
+ # @raise [ArgumentError] when called with any non-nil arguments
11
+ def [](*args)
12
+ raise ArgumentError, "None called with arguments" if args.any?(&:itself)
13
+ end
14
+
15
+ alias :new :[]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "operation/result"
4
+ require_relative "operation/runner"
5
+
3
6
  module Teckel
4
7
  # The main operation Mixin
5
8
  #
@@ -10,7 +13,7 @@ module Teckel
10
13
  # +input+. +output+ and +error+ methods to point them to anonymous classes.
11
14
  #
12
15
  # If you like "traditional" result objects to ask +successful?+ or +failure?+ on,
13
- # see {Teckel::Operation::Results Teckel::Operation::Results}
16
+ # use {.result!} and get {Teckel::Operation::Result}
14
17
  #
15
18
  # By default, +input+. +output+ and +error+ classes are build using +:[]+
16
19
  # (eg: +Input[some: :param]+).
@@ -18,43 +21,6 @@ module Teckel
18
21
  # {ClassMethods#output_constructor output_constructor} and
19
22
  # {ClassMethods#error_constructor error_constructor} to change them.
20
23
  #
21
- # @example class definitions via constants
22
- # class CreateUserViaConstants
23
- # include Teckel::Operation
24
- #
25
- # class Input
26
- # def initialize(name:, age:)
27
- # @name, @age = name, age
28
- # end
29
- # attr_reader :name, :age
30
- # end
31
- #
32
- # Output = ::User
33
- #
34
- # class Error
35
- # def initialize(message, errors)
36
- # @message, @errors = message, errors
37
- # end
38
- # attr_reader :message, :errors
39
- # end
40
- #
41
- # input_constructor :new
42
- # error_constructor :new
43
- #
44
- # # @param [CreateUser::Input]
45
- # # @return [User,CreateUser::Error]
46
- # def call(input)
47
- # user = ::User.new(name: input.name, age: input.age)
48
- # if user.save
49
- # user
50
- # else
51
- # fail!(message: "Could not save User", errors: user.errors)
52
- # end
53
- # end
54
- # end
55
- #
56
- # CreateUserViaConstants.call(name: "Bob", age: 23).is_a?(User) #=> true
57
- #
58
24
  # @example class definitions via methods
59
25
  # class CreateUserViaMethods
60
26
  # include Teckel::Operation
@@ -89,201 +55,272 @@ module Teckel
89
55
  #
90
56
  # @!visibility public
91
57
  module Operation
92
- # The default implementation for executing a single {Operation}
93
- #
94
- # @!visibility protected
95
- class Runner
96
- # @!visibility private
97
- UNDEFINED = Object.new.freeze
98
-
99
- def initialize(operation)
100
- @operation = operation
101
- end
102
- attr_reader :operation
103
-
104
- def call(input)
105
- err = catch(:failure) do
106
- simple_return = UNDEFINED
107
- out = catch(:success) do
108
- simple_return = @operation.new.call(build_input(input))
109
- end
110
- return simple_return == UNDEFINED ? build_output(*out) : build_output(simple_return)
111
- end
112
- build_error(*err)
113
- end
114
-
115
- private
116
-
117
- def build_input(input)
118
- operation.input_constructor.call(input)
119
- end
120
-
121
- def build_output(*args)
122
- if args.size == 1 && operation.output === args.first # rubocop:disable Style/CaseEquality
123
- args.first
124
- else
125
- operation.output_constructor.call(*args)
126
- end
127
- end
128
-
129
- def build_error(*args)
130
- if args.size == 1 && operation.error === args.first # rubocop:disable Style/CaseEquality
131
- args.first
132
- else
133
- operation.error_constructor.call(*args)
134
- end
135
- end
136
- end
137
-
138
58
  module ClassMethods
139
- # @!attribute [r] input()
140
- # Get the configured class wrapping the input data structure.
141
- # @return [Class] The +input+ class
142
-
143
- # @!method input(klass)
144
- # Set the class wrapping the input data structure.
145
- # @param klass [Class] The +input+ class
146
- # @return [Class] The +input+ class
59
+ # @!group Contacts definition
60
+
61
+ # @overload input()
62
+ # Get the configured class wrapping the input data structure.
63
+ # @return [Class] The +input+ class
64
+ # @overload input(klass)
65
+ # Set the class wrapping the input data structure.
66
+ # @param klass [Class] The +input+ class
67
+ # @return [Class] The +input+ class
147
68
  def input(klass = nil)
148
69
  @config.for(:input, klass) { self::Input if const_defined?(:Input) } ||
149
70
  raise(Teckel::MissingConfigError, "Missing input config for #{self}")
150
71
  end
151
72
 
152
- # @!attribute [r] input_constructor()
153
- # The callable constructor to build an instance of the +input+ class.
154
- # @return [Class] The Input class
155
-
156
- # @!method input_constructor(sym_or_proc)
157
- # Define how to build the +input+.
158
- # @param sym_or_proc [Symbol, #call]
159
- # - Either a +Symbol+ representing the _public_ method to call on the +input+ class.
160
- # - Or anything that response to +#call+ (like a +Proc+).
161
- # @return [#call] The callable constructor
73
+ # @overload input_constructor()
74
+ # The callable constructor to build an instance of the +input+ class.
75
+ # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
76
+ # @return [Proc] A callable that will return an instance of the +input+ class.
162
77
  #
163
- # @example simple symbol to method constructor
164
- # class MyOperation
165
- # include Teckel::Operation
78
+ # @overload input_constructor(sym_or_proc)
79
+ # Define how to build the +input+.
80
+ # @param sym_or_proc [Symbol, #call]
81
+ # - Either a +Symbol+ representing the _public_ method to call on the +input+ class.
82
+ # - Or anything that response to +#call+ (like a +Proc+).
83
+ # @return [#call] The callable constructor
166
84
  #
167
- # class Input
168
- # def initialize(name:, age:); end
85
+ # @example simple symbol to method constructor
86
+ # class MyOperation
87
+ # include Teckel::Operation
88
+ #
89
+ # class Input
90
+ # def initialize(name:, age:); end
91
+ # end
92
+ #
93
+ # # If you need more control over how to build a new +Input+ instance
94
+ # # MyOperation.call(name: "Bob", age: 23) # -> Input.new(name: "Bob", age: 23)
95
+ # input_constructor :new
169
96
  # end
170
97
  #
171
- # # If you need more control over how to build a new +Input+ instance
172
- # # MyOperation.call(name: "Bob", age: 23) # -> Input.new(name: "Bob", age: 23)
173
- # input_constructor :new
174
- # end
98
+ # MyOperation.input_constructor.is_a?(Method) #=> true
175
99
  #
176
- # MyOperation.input_constructor.is_a?(Method) #=> true
100
+ # @example Custom Proc constructor
101
+ # class MyOperation
102
+ # include Teckel::Operation
177
103
  #
178
- # @example Custom Proc constructor
179
- # class MyOperation
180
- # include Teckel::Operation
104
+ # class Input
105
+ # def initialize(*args, **opts); end
106
+ # end
181
107
  #
182
- # class Input
183
- # def initialize(*args, **opts); end
108
+ # # If you need more control over how to build a new +Input+ instance
109
+ # # MyOperation.call("foo", opt: "bar") # -> Input.new(name: "foo", opt: "bar")
110
+ # input_constructor ->(name, options) { Input.new(name: name, **options) }
184
111
  # end
185
112
  #
186
- # # If you need more control over how to build a new +Input+ instance
187
- # # MyOperation.call("foo", opt: "bar") # -> Input.new(name: "foo", opt: "bar")
188
- # input_constructor ->(name, options) { Input.new(name: name, **options) }
189
- # end
190
- #
191
- # MyOperation.input_constructor.is_a?(Proc) #=> true
192
- def input_constructor(sym_or_proc = Config.default_constructor)
193
- @config.for(:input_constructor) { build_counstructor(input, sym_or_proc) } ||
113
+ # MyOperation.input_constructor.is_a?(Proc) #=> true
114
+ def input_constructor(sym_or_proc = nil)
115
+ get_set_counstructor(:input_constructor, input, sym_or_proc) ||
194
116
  raise(MissingConfigError, "Missing input_constructor config for #{self}")
195
117
  end
196
118
 
197
- # @!attribute [r] output()
198
- # Get the configured class wrapping the output data structure.
199
- # @return [Class] The +output+ class
200
-
201
- # @!method output(klass)
202
- # Set the class wrapping the output data structure.
203
- # @param klass [Class] The +output+ class
204
- # @return [Class] The +output+ class
119
+ # @overload output()
120
+ # Get the configured class wrapping the output data structure.
121
+ # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
122
+ # @return [Class] The +output+ class
123
+ #
124
+ # @overload output(klass)
125
+ # Set the class wrapping the output data structure.
126
+ # @param klass [Class] The +output+ class
127
+ # @return [Class] The +output+ class
205
128
  def output(klass = nil)
206
129
  @config.for(:output, klass) { self::Output if const_defined?(:Output) } ||
207
130
  raise(Teckel::MissingConfigError, "Missing output config for #{self}")
208
131
  end
209
132
 
210
- # @!attribute [r] output_constructor()
211
- # The callable constructor to build an instance of the +output+ class.
212
- # @return [Class] The Output class
213
-
214
- # @!method output_constructor(sym_or_proc)
215
- # Define how to build the +output+.
216
- # @param sym_or_proc [Symbol, #call]
217
- # - Either a +Symbol+ representing the _public_ method to call on the +output+ class.
218
- # - Or anything that response to +#call+ (like a +Proc+).
219
- # @return [#call] The callable constructor
133
+ # @overload output_constructor()
134
+ # The callable constructor to build an instance of the +output+ class.
135
+ # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
136
+ # @return [Proc] A callable that will return an instance of +output+ class.
220
137
  #
221
- # @example
222
- # class MyOperation
223
- # include Teckel::Operation
138
+ # @overload output_constructor(sym_or_proc)
139
+ # Define how to build the +output+.
140
+ # @param sym_or_proc [Symbol, #call]
141
+ # - Either a +Symbol+ representing the _public_ method to call on the +output+ class.
142
+ # - Or anything that response to +#call+ (like a +Proc+).
143
+ # @return [#call] The callable constructor
224
144
  #
225
- # class Output
226
- # def initialize(*args, **opts); end
227
- # end
145
+ # @example
146
+ # class MyOperation
147
+ # include Teckel::Operation
228
148
  #
229
- # # MyOperation.call("foo", "bar") # -> Output.new("foo", "bar")
230
- # output_constructor :new
149
+ # class Output
150
+ # def initialize(*args, **opts); end
151
+ # end
231
152
  #
232
- # # If you need more control over how to build a new +Output+ instance
233
- # # MyOperation.call("foo", opt: "bar") # -> Output.new(name: "foo", opt: "bar")
234
- # output_constructor ->(name, options) { Output.new(name: name, **options) }
235
- # end
236
- def output_constructor(sym_or_proc = Config.default_constructor)
237
- @config.for(:output_constructor) { build_counstructor(output, sym_or_proc) } ||
153
+ # # MyOperation.call("foo", "bar") # -> Output.new("foo", "bar")
154
+ # output_constructor :new
155
+ #
156
+ # # If you need more control over how to build a new +Output+ instance
157
+ # # MyOperation.call("foo", opt: "bar") # -> Output.new(name: "foo", opt: "bar")
158
+ # output_constructor ->(name, options) { Output.new(name: name, **options) }
159
+ # end
160
+ def output_constructor(sym_or_proc = nil)
161
+ get_set_counstructor(:output_constructor, output, sym_or_proc) ||
238
162
  raise(MissingConfigError, "Missing output_constructor config for #{self}")
239
163
  end
240
164
 
241
- # @!attribute [r] error()
242
- # Get the configured class wrapping the error data structure.
243
- # @return [Class] The +error+ class
244
-
245
- # @!method error(klass)
246
- # Set the class wrapping the error data structure.
247
- # @param klass [Class] The +error+ class
248
- # @return [Class,nil] The +error+ class or +nil+ if it does not error
165
+ # @overload error()
166
+ # Get the configured class wrapping the error data structure.
167
+ # @return [Class] The +error+ class
168
+ #
169
+ # @overload error(klass)
170
+ # Set the class wrapping the error data structure.
171
+ # @param klass [Class] The +error+ class
172
+ # @return [Class,nil] The +error+ class or +nil+ if it does not error
249
173
  def error(klass = nil)
250
174
  @config.for(:error, klass) { self::Error if const_defined?(:Error) } ||
251
175
  raise(Teckel::MissingConfigError, "Missing error config for #{self}")
252
176
  end
253
177
 
254
- # @!attribute [r] error_constructor()
255
- # The callable constructor to build an instance of the +error+ class.
256
- # @return [Class] The Error class
257
-
258
- # @!method error_constructor(sym_or_proc)
259
- # Define how to build the +error+.
260
- # @param sym_or_proc [Symbol, #call]
261
- # - Either a +Symbol+ representing the _public_ method to call on the +error+ class.
262
- # - Or anything that response to +#call+ (like a +Proc+).
263
- # @return [#call] The callable constructor
178
+ # @overload error_constructor()
179
+ # The callable constructor to build an instance of the +error+ class.
180
+ # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
181
+ # @return [Proc] A callable that will return an instance of +error+ class.
264
182
  #
265
- # @example
266
- # class MyOperation
267
- # include Teckel::Operation
183
+ # @overload error_constructor(sym_or_proc)
184
+ # Define how to build the +error+.
185
+ # @param sym_or_proc [Symbol, #call]
186
+ # - Either a +Symbol+ representing the _public_ method to call on the +error+ class.
187
+ # - Or anything that response to +#call+ (like a +Proc+).
188
+ # @return [#call] The callable constructor
268
189
  #
269
- # class Error
270
- # def initialize(*args, **opts); end
271
- # end
190
+ # @example
191
+ # class MyOperation
192
+ # include Teckel::Operation
272
193
  #
273
- # # MyOperation.call("foo", "bar") # -> Error.new("foo", "bar")
274
- # error_constructor :new
194
+ # class Error
195
+ # def initialize(*args, **opts); end
196
+ # end
275
197
  #
276
- # # If you need more control over how to build a new +Error+ instance
277
- # # MyOperation.call("foo", opt: "bar") # -> Error.new(name: "foo", opt: "bar")
278
- # error_constructor ->(name, options) { Error.new(name: name, **options) }
279
- # end
280
- def error_constructor(sym_or_proc = Config.default_constructor)
281
- @config.for(:error_constructor) { build_counstructor(error, sym_or_proc) } ||
198
+ # # MyOperation.call("foo", "bar") # -> Error.new("foo", "bar")
199
+ # error_constructor :new
200
+ #
201
+ # # If you need more control over how to build a new +Error+ instance
202
+ # # MyOperation.call("foo", opt: "bar") # -> Error.new(name: "foo", opt: "bar")
203
+ # error_constructor ->(name, options) { Error.new(name: name, **options) }
204
+ # end
205
+ def error_constructor(sym_or_proc = nil)
206
+ get_set_counstructor(:error_constructor, error, sym_or_proc) ||
282
207
  raise(MissingConfigError, "Missing error_constructor config for #{self}")
283
208
  end
284
209
 
285
- # Convenience method for setting {#input}, {#output} or {#error} to the {None} value.
286
- # @return [Object] The {Teckel::None} class.
210
+ # @!endgroup
211
+
212
+ # @overload settings()
213
+ # Get the configured class wrapping the settings data structure.
214
+ # @return [Class] The +settings+ class, or {Teckel::Contracts::None} as default
215
+ #
216
+ # @overload settings(klass)
217
+ # Set the class wrapping the settings data structure.
218
+ # @param klass [Class] The +settings+ class
219
+ # @return [Class] The +settings+ class configured
220
+ def settings(klass = nil)
221
+ @config.for(:settings, klass) { const_defined?(:Settings) ? self::Settings : none }
222
+ end
223
+
224
+ # @overload settings_constructor()
225
+ # The callable constructor to build an instance of the +settings+ class.
226
+ # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
227
+ # @return [Proc] A callable that will return an instance of +settings+ class.
228
+ #
229
+ # @overload settings_constructor(sym_or_proc)
230
+ # Define how to build the +settings+.
231
+ # @param sym_or_proc [Symbol, #call]
232
+ # - Either a +Symbol+ representing the _public_ method to call on the +settings+ class.
233
+ # - Or anything that response to +#call+ (like a +Proc+).
234
+ # @return [#call] The callable constructor
235
+ #
236
+ # @example
237
+ # class MyOperation
238
+ # include Teckel::Operation
239
+ #
240
+ # class Settings
241
+ # def initialize(*args); end
242
+ # end
243
+ #
244
+ # # MyOperation.with("foo", "bar") # -> Settings.new("foo", "bar")
245
+ # settings_constructor :new
246
+ # end
247
+ def settings_constructor(sym_or_proc = nil)
248
+ get_set_counstructor(:settings_constructor, settings, sym_or_proc) ||
249
+ raise(MissingConfigError, "Missing settings_constructor config for #{self}")
250
+ end
251
+
252
+ # @overload runner()
253
+ # @return [Class] The Runner class
254
+ # @!visibility protected
255
+ #
256
+ # @overload runner(klass)
257
+ # Overwrite the default runner
258
+ # @param klass [Class] A class like the {Runner}
259
+ # @!visibility protected
260
+ def runner(klass = nil)
261
+ @config.for(:runner, klass) { Teckel::Operation::Runner }
262
+ end
263
+
264
+ # @overload result()
265
+ # Get the configured result object class wrapping {error} or {output}.
266
+ # The {ValueResult} default will act as a pass-through and does. Any error
267
+ # or output will just returned as-is.
268
+ # @return [Class] The +result+ class, or {ValueResult} as default
269
+ #
270
+ # @overload result(klass)
271
+ # Set the result object class wrapping {error} or {output}.
272
+ # @param klass [Class] The +result+ class
273
+ # @return [Class] The +result+ class configured
274
+ def result(klass = nil)
275
+ @config.for(:result, klass) { const_defined?(:Result, false) ? self::Result : ValueResult }
276
+ end
277
+
278
+ # @overload result_constructor()
279
+ # The callable constructor to build an instance of the +result+ class.
280
+ # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
281
+ # @return [Proc] A callable that will return an instance of +result+ class.
282
+ #
283
+ # @overload result_constructor(sym_or_proc)
284
+ # Define how to build the +result+.
285
+ # @param sym_or_proc [Symbol, #call]
286
+ # - Either a +Symbol+ representing the _public_ method to call on the +result+ class.
287
+ # - Or anything that response to +#call+ (like a +Proc+).
288
+ # @return [#call] The callable constructor
289
+ #
290
+ # @example
291
+ # class MyOperation
292
+ # include Teckel::Operation
293
+ #
294
+ # class Result
295
+ # include Teckel::Result
296
+ # def initialize(value, success, opts = {}); end
297
+ # end
298
+ #
299
+ # # If you need more control over how to build a new +Settings+ instance
300
+ # result_constructor ->(value, success) { result.new(value, success, {foo: :bar}) }
301
+ # end
302
+ def result_constructor(sym_or_proc = nil)
303
+ get_set_counstructor(:result_constructor, result, sym_or_proc) ||
304
+ raise(MissingConfigError, "Missing result_constructor config for #{self}")
305
+ end
306
+
307
+ # @!group Shortcuts
308
+
309
+ # Shortcut to use {Teckel::Operation::Result} as a result object,
310
+ # wrapping any {error} or {output}.
311
+ #
312
+ # @!visibility protected
313
+ # @note Don't use in conjunction with {result} or {result_constructor}
314
+ # @return [nil]
315
+ def result!
316
+ @config.for(:result, Teckel::Operation::Result)
317
+ @config.for(:result_constructor, Teckel::Operation::Result.method(:new))
318
+ nil
319
+ end
320
+
321
+ # Convenience method for setting {#input}, {#output} or {#error} to the
322
+ # {Teckel::Contracts::None} value.
323
+ # @return [Object] The {Teckel::Contracts::None} class.
287
324
  #
288
325
  # @example Enforcing nil input, output or error
289
326
  # class MyOperation
@@ -292,7 +329,7 @@ module Teckel
292
329
  # input none
293
330
  #
294
331
  # # same as
295
- # output Teckel::None
332
+ # output Teckel::Contracts::None
296
333
  #
297
334
  # error none
298
335
  #
@@ -310,35 +347,71 @@ module Teckel
310
347
  #
311
348
  # MyOperation.call #=> nil
312
349
  def none
313
- None
350
+ Teckel::Contracts::None
314
351
  end
315
352
 
316
- # @!attribute [r] runner()
317
- # @return [Class] The Runner class
318
- # @!visibility protected
319
-
320
- # Overwrite the default runner
321
- # @param klass [Class] A class like the {Runner}
322
- # @!visibility protected
323
- def runner(klass = nil)
324
- @config.for(:runner, klass) { Runner }
325
- end
353
+ # @endgroup
326
354
 
327
355
  # Invoke the Operation
328
356
  #
329
- # @param input Any form of input your +input+ class can handle via the given +input_constructor+
330
- # @return Either An instance of your defined +error+ class or +output+ class
357
+ # @param input Any form of input your {#input} class can handle via the given {#input_constructor}
358
+ # @return Either An instance of your defined {#error} class or {#output} class
331
359
  # @!visibility public
332
360
  def call(input = nil)
333
361
  runner.new(self).call(input)
334
362
  end
335
363
 
364
+ # Provide {InstanceMethods#settings() settings} to the running operation.
365
+ #
366
+ # This method is intended to be called on the operation class outside of
367
+ # it's definition, prior to running {#call}.
368
+ #
369
+ # @param input Any form of input your {#settings} class can handle via the given {#settings_constructor}
370
+ # @return [Class] The configured {runner}
371
+ # @!visibility public
372
+ #
373
+ # @example Inject settings for an operation call
374
+ # LOG = []
375
+ #
376
+ # class MyOperation
377
+ # include ::Teckel::Operation
378
+ #
379
+ # settings Struct.new(:log)
380
+ #
381
+ # input none
382
+ # output none
383
+ # error none
384
+ #
385
+ # def call(_input)
386
+ # settings.log << "called" if settings&.log
387
+ # nil
388
+ # end
389
+ # end
390
+ #
391
+ # MyOperation.with(LOG).call
392
+ # LOG #=> ["called"]
393
+ #
394
+ # LOG.clear
395
+ #
396
+ # MyOperation.with(false).call
397
+ # MyOperation.call
398
+ # LOG #=> []
399
+ def with(input)
400
+ runner.new(self, settings_constructor.call(input))
401
+ end
402
+ alias :set :with
403
+
336
404
  # @!visibility private
337
405
  # @return [void]
338
406
  def define!
339
- %i[input input_constructor output output_constructor error error_constructor runner].each { |e|
340
- public_send(e)
341
- }
407
+ %i[
408
+ input input_constructor
409
+ output output_constructor
410
+ error error_constructor
411
+ settings settings_constructor
412
+ result result_constructor
413
+ runner
414
+ ].each { |e| public_send(e) }
342
415
  nil
343
416
  end
344
417
 
@@ -351,7 +424,7 @@ module Teckel
351
424
  def finalize!
352
425
  define!
353
426
  @config.freeze
354
- freeze
427
+ self
355
428
  end
356
429
 
357
430
  # Produces a shallow copy of this operation and all it's configuration.
@@ -378,8 +451,29 @@ module Teckel
378
451
  end
379
452
  end
380
453
 
454
+ # @!visibility private
455
+ def inherited(subclass)
456
+ subclass.instance_variable_set(:@config, @config.dup)
457
+ end
458
+
459
+ # @!visibility private
460
+ def self.extended(base)
461
+ base.instance_exec do
462
+ @config = Config.new
463
+ attr_accessor :settings
464
+ end
465
+ end
466
+
381
467
  private
382
468
 
469
+ def get_set_counstructor(name, on, sym_or_proc)
470
+ constructor = build_counstructor(on, sym_or_proc) unless sym_or_proc.nil?
471
+
472
+ @config.for(name, constructor) {
473
+ build_counstructor(on, Teckel::DEFAULT_CONSTRUCTOR)
474
+ }
475
+ end
476
+
383
477
  def build_counstructor(on, sym_or_proc)
384
478
  if sym_or_proc.is_a?(Symbol) && on.respond_to?(sym_or_proc)
385
479
  on.public_method(sym_or_proc)
@@ -390,6 +484,12 @@ module Teckel
390
484
  end
391
485
 
392
486
  module InstanceMethods
487
+ # @!attribute [r] settings()
488
+ # @return [Class,nil] When executed with settings, an instance of the
489
+ # configured {.settings} class. Otherwise +nil+
490
+ # @see ClassMethods#settings
491
+ # @!visibility public
492
+
393
493
  # Halt any further execution with a output value
394
494
  #
395
495
  # @return a thing matching your {Operation::ClassMethods#output Operation#output} definition
@@ -410,12 +510,6 @@ module Teckel
410
510
  def self.included(receiver)
411
511
  receiver.extend ClassMethods
412
512
  receiver.send :include, InstanceMethods
413
-
414
- receiver.class_eval do
415
- @config = Config.new
416
-
417
- protected :success!, :fail!
418
- end
419
513
  end
420
514
  end
421
515
  end