teckel 0.3.0 → 0.4.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.
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