teckel 0.7.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df9c73577c9c93fbaaf2e7661a07bdc3b1b0397832a62a8edbe4eea846c4c029
4
- data.tar.gz: 037041bf0eb6ae8bc41e354772d3ce1d04002a35ecccc0b1ecefa33a85845c3b
3
+ metadata.gz: 213ed6f9b3d25db8b7fc284ced55aa1a3a71b4ec54f10fd3ed3bf4e0a315dbd3
4
+ data.tar.gz: d013f110763117b066987bfb6c085e8682db372856ea7ab27f4803858be96e3c
5
5
  SHA512:
6
- metadata.gz: 2f6f601e816c972aed8e511b05fdbc418a48aab10c2ca246d108661cc25a8d514a41944b2e01b9294757a2a717696491f20f601f5f60e53faa45651aec48bc7f
7
- data.tar.gz: 68bc46e5ed21cf5e639ee1fc00265b278f9062f42de9d179a07559862990a078df9dcb2ef26956eb629cf08e71409b9b05bd363a11930cb64ecdee533d63d2ea
6
+ metadata.gz: 82db9106f0da688411be738866692455653eb59298afa01ba24500975b0498bf9a164f9f59e2741a4ed9c0c252174bd86fa71dd4204338fe04424d961c21621b
7
+ data.tar.gz: 6c693373c212e3b127dde042fdaba1ab7865d813cba17d0624a73a62e62589fc68bfe86ed93b9e7c32295deb6053411e4ea0cf57f0897b6f8e7e339ce60dd7be
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changes
2
2
 
3
+ ## 0.8.0
4
+
5
+ - Add mutation testing (currently about 80% covered)
6
+ - Breaking: `Teckel::Operation::Result` no longer converts the `successful` value to a boolean.
7
+ When using the default result implementation, nothing changes for you.
8
+ When you manually pass anything else into it, `succesful?` will return this value. The `failure` and `success` methods
9
+ work on the "Truthy" value of `successful`
10
+ ```ruby
11
+ result = Result.new(some_value, 42)
12
+ result.successful?
13
+ # => 42
14
+ ```
15
+ - Change: `freeze`ing an `Operation` or `Chain` will also freeze their internal config (input, output, steps, etc.)
16
+
17
+ Internal:
18
+
19
+ - Some refactoring to cover mutations
20
+ - Extracted creating the runable `Operation` instance into a new, public `runable(settings = UNDEFINED)` method.
21
+ Mainly to reduce code duplication but this also makes testing, stubbing and mocking easier.
22
+
3
23
  ## 0.7.0
4
24
 
5
25
  - Breaking: `Teckel::Chain` will not be required by default. require manually if needed `require "teckel/chain"` [GH-24]
data/README.md CHANGED
@@ -3,10 +3,10 @@
3
3
  Ruby service classes with enforced<sup name="footnote-1-source">[1](#footnote-1)</sup> input, output and error data structure definition.
4
4
 
5
5
  [![Gem Version](https://img.shields.io/gem/v/teckel.svg)][gem]
6
- [![Build Status](https://github.com/dry-rb/dry-configurable/workflows/ci/badge.svg)][ci]
6
+ [![Build Status](https://github.com/fnordfish/teckel/actions/workflows/specs.yml/badge.svg)][ci]
7
7
  [![Maintainability](https://api.codeclimate.com/v1/badges/b3939aaec6271a567a57/maintainability)](https://codeclimate.com/github/fnordfish/teckel/maintainability)
8
8
  [![Test Coverage](https://api.codeclimate.com/v1/badges/b3939aaec6271a567a57/test_coverage)](https://codeclimate.com/github/fnordfish/teckel/test_coverage)
9
- [![API Documentation Coverage](https://inch-ci.org/github/fnordfish/teckel.svg?branch=master)][inch]
9
+ [![API Documentation Coverage](https://inch-ci.org/github/fnordfish/teckel.svg?branch=main)][inch]
10
10
 
11
11
  ## Installation
12
12
 
@@ -95,5 +95,5 @@ Please also see [DEVELOPMENT.md](DEVELOPMENT.md) for planned features and genera
95
95
  - <a name="footnote-1">1</a>: Obviously, it's still Ruby and you can cheat. Don’t! [↩](#footnote-1-source)
96
96
 
97
97
  [gem]: https://rubygems.org/gems/teckel
98
- [ci]: https://github.com/fnordfish/teckel/actions?query=workflow%3ACI
98
+ [ci]: https://github.com/fnordfish/teckel/actions/workflows/specs.yml
99
99
  [inch]: http://inch-ci.org/github/fnordfish/teckel
@@ -148,7 +148,7 @@ module Teckel
148
148
  } || raise(MissingConfigError, "Missing result_constructor config for #{self}")
149
149
  end
150
150
 
151
- # Declare default settings operation iin this chain should use when called without
151
+ # Declare default settings operation in this chain should use when called without
152
152
  # {Teckel::Chain::ClassMethods#with #with}.
153
153
  #
154
154
  # Explicit call-time settings will *not* get merged with declared default setting.
@@ -227,7 +227,7 @@ module Teckel
227
227
  # @return [self]
228
228
  # @!visibility public
229
229
  def dup
230
- dup_config(super)
230
+ dup_config(super())
231
231
  end
232
232
 
233
233
  # Produces a clone of this chain.
@@ -237,15 +237,25 @@ module Teckel
237
237
  # @!visibility public
238
238
  def clone
239
239
  if frozen?
240
- super
240
+ super()
241
241
  else
242
- dup_config(super)
242
+ dup_config(super())
243
243
  end
244
244
  end
245
245
 
246
+ # Prevents further modifications to this chain and its config
247
+ #
248
+ # @return [self]
249
+ # @!visibility public
250
+ def freeze
251
+ steps.freeze
252
+ @config.freeze
253
+ super()
254
+ end
255
+
246
256
  # @!visibility private
247
257
  def inherited(subclass)
248
- super dup_config(subclass)
258
+ super(dup_config(subclass))
249
259
  end
250
260
 
251
261
  # @!visibility private
@@ -264,10 +274,11 @@ module Teckel
264
274
  end
265
275
 
266
276
  def build_constructor(on, sym_or_proc)
267
- if sym_or_proc.is_a?(Symbol) && on.respond_to?(sym_or_proc)
268
- on.public_method(sym_or_proc)
269
- elsif sym_or_proc.respond_to?(:call)
277
+ case sym_or_proc
278
+ when Proc
270
279
  sym_or_proc
280
+ when Symbol
281
+ on.public_method(sym_or_proc) if on.respond_to?(sym_or_proc)
271
282
  end
272
283
  end
273
284
  end
@@ -29,7 +29,7 @@ module Teckel
29
29
  end
30
30
 
31
31
  def steps
32
- settings == UNDEFINED ? chain.steps : steps_with_settings
32
+ settings.eql?(UNDEFINED) ? chain.steps : steps_with_settings
33
33
  end
34
34
 
35
35
  private
data/lib/teckel/chain.rb CHANGED
@@ -74,8 +74,10 @@ module Teckel
74
74
  end
75
75
 
76
76
  def self.included(receiver)
77
- receiver.extend Config
78
- receiver.extend ClassMethods
77
+ receiver.class_eval do
78
+ extend Config
79
+ extend ClassMethods
80
+ end
79
81
  end
80
82
  end
81
83
  end
data/lib/teckel/config.rb CHANGED
@@ -35,12 +35,12 @@ module Teckel
35
35
  # @!visibility private
36
36
  def freeze
37
37
  @config.freeze
38
- super
38
+ super()
39
39
  end
40
40
 
41
41
  # @!visibility private
42
42
  def dup
43
- super.tap do |copy|
43
+ super().tap do |copy|
44
44
  copy.instance_variable_set(:@config, @config.dup)
45
45
  end
46
46
  end
@@ -12,7 +12,7 @@ module Teckel
12
12
  # @return [Class] The +input+ class
13
13
  def input(klass = nil)
14
14
  @config.for(:input, klass) { self::Input if const_defined?(:Input) } ||
15
- raise(Teckel::MissingConfigError, "Missing input config for #{self}")
15
+ raise(MissingConfigError, "Missing input config for #{self}")
16
16
  end
17
17
 
18
18
  # @overload input_constructor()
@@ -57,8 +57,7 @@ module Teckel
57
57
  #
58
58
  # MyOperation.input_constructor.is_a?(Proc) #=> true
59
59
  def input_constructor(sym_or_proc = nil)
60
- get_set_constructor(:input_constructor, input, sym_or_proc) ||
61
- raise(MissingConfigError, "Missing input_constructor config for #{self}")
60
+ get_set_constructor(:input_constructor, input, sym_or_proc)
62
61
  end
63
62
 
64
63
  # @overload output()
@@ -72,7 +71,7 @@ module Teckel
72
71
  # @return [Class] The +output+ class
73
72
  def output(klass = nil)
74
73
  @config.for(:output, klass) { self::Output if const_defined?(:Output) } ||
75
- raise(Teckel::MissingConfigError, "Missing output config for #{self}")
74
+ raise(MissingConfigError, "Missing output config for #{self}")
76
75
  end
77
76
 
78
77
  # @overload output_constructor()
@@ -103,8 +102,7 @@ module Teckel
103
102
  # output_constructor ->(name, options) { Output.new(name: name, **options) }
104
103
  # end
105
104
  def output_constructor(sym_or_proc = nil)
106
- get_set_constructor(:output_constructor, output, sym_or_proc) ||
107
- raise(MissingConfigError, "Missing output_constructor config for #{self}")
105
+ get_set_constructor(:output_constructor, output, sym_or_proc)
108
106
  end
109
107
 
110
108
  # @overload error()
@@ -117,7 +115,7 @@ module Teckel
117
115
  # @return [Class,nil] The +error+ class or +nil+ if it does not error
118
116
  def error(klass = nil)
119
117
  @config.for(:error, klass) { self::Error if const_defined?(:Error) } ||
120
- raise(Teckel::MissingConfigError, "Missing error config for #{self}")
118
+ raise(MissingConfigError, "Missing error config for #{self}")
121
119
  end
122
120
 
123
121
  # @overload error_constructor()
@@ -148,8 +146,7 @@ module Teckel
148
146
  # error_constructor ->(name, options) { Error.new(name: name, **options) }
149
147
  # end
150
148
  def error_constructor(sym_or_proc = nil)
151
- get_set_constructor(:error_constructor, error, sym_or_proc) ||
152
- raise(MissingConfigError, "Missing error_constructor config for #{self}")
149
+ get_set_constructor(:error_constructor, error, sym_or_proc)
153
150
  end
154
151
 
155
152
  # @!endgroup
@@ -219,12 +216,9 @@ module Teckel
219
216
  #
220
217
  # (Like calling +MyOperation.with(arg1, arg2, ...)+)
221
218
  def default_settings!(*args)
222
- callable =
223
- if args.empty?
224
- -> { settings_constructor.call }
225
- elsif args.length == 1
226
- build_constructor(settings, args.first)
227
- end
219
+ callable = if args.size.equal?(1)
220
+ build_constructor(settings, args.first)
221
+ end
228
222
 
229
223
  callable ||= -> { settings_constructor.call(*args) }
230
224
 
@@ -246,7 +240,7 @@ module Teckel
246
240
  # @param klass [Class] A class like the {Runner}
247
241
  # @!visibility protected
248
242
  def runner(klass = nil)
249
- @config.for(:runner, klass) { Teckel::Operation::Runner }
243
+ @config.for(:runner, klass) { Runner }
250
244
  end
251
245
 
252
246
  # @overload result()
@@ -301,8 +295,8 @@ module Teckel
301
295
  # @note Don't use in conjunction with {result} or {result_constructor}
302
296
  # @return [nil]
303
297
  def result!
304
- @config.for(:result, Teckel::Operation::Result)
305
- @config.for(:result_constructor, Teckel::Operation::Result.method(:new))
298
+ @config.for(:result, Result)
299
+ @config.for(:result_constructor, Result.method(:new))
306
300
  nil
307
301
  end
308
302
 
@@ -340,9 +334,7 @@ module Teckel
340
334
  # @return [self]
341
335
  # @!visibility public
342
336
  def dup
343
- super.tap do |copy|
344
- copy.instance_variable_set(:@config, @config.dup)
345
- end
337
+ dup_config(super())
346
338
  end
347
339
 
348
340
  # Produces a clone of this operation and all it's configuration
@@ -351,18 +343,24 @@ module Teckel
351
343
  # @!visibility public
352
344
  def clone
353
345
  if frozen?
354
- super
346
+ super()
355
347
  else
356
- super.tap do |copy|
357
- copy.instance_variable_set(:@config, @config.dup)
358
- end
348
+ dup_config(super())
359
349
  end
360
350
  end
361
351
 
352
+ # Prevents further modifications to this operation and its config
353
+ #
354
+ # @return [self]
355
+ # @!visibility public
356
+ def freeze
357
+ @config.freeze
358
+ super()
359
+ end
360
+
362
361
  # @!visibility private
363
362
  def inherited(subclass)
364
- subclass.instance_variable_set(:@config, @config.dup)
365
- super subclass
363
+ super(dup_config(subclass))
366
364
  end
367
365
 
368
366
  # @!visibility private
@@ -376,19 +374,25 @@ module Teckel
376
374
 
377
375
  private
378
376
 
377
+ def dup_config(other_class)
378
+ other_class.instance_variable_set(:@config, @config.dup)
379
+ other_class
380
+ end
381
+
379
382
  def get_set_constructor(name, on, sym_or_proc)
380
- constructor = build_constructor(on, sym_or_proc) unless sym_or_proc.nil?
383
+ constructor = build_constructor(on, sym_or_proc)
381
384
 
382
385
  @config.for(name, constructor) {
383
- build_constructor(on, Teckel::DEFAULT_CONSTRUCTOR)
386
+ build_constructor(on, DEFAULT_CONSTRUCTOR)
384
387
  }
385
388
  end
386
389
 
387
390
  def build_constructor(on, sym_or_proc)
388
- if sym_or_proc.is_a?(Symbol) && on.respond_to?(sym_or_proc)
389
- on.public_method(sym_or_proc)
390
- elsif sym_or_proc.respond_to?(:call)
391
+ case sym_or_proc
392
+ when Proc
391
393
  sym_or_proc
394
+ when Symbol
395
+ on.public_method(sym_or_proc) if on.respond_to?(sym_or_proc)
392
396
  end
393
397
  end
394
398
  end
@@ -38,16 +38,16 @@ module Teckel
38
38
  include Teckel::Result
39
39
 
40
40
  # @param value [Object] The result value
41
- # @param success [Boolean] whether this is a successful result
42
- def initialize(value, success)
41
+ # @param successful [Boolean] whether this is a successful result
42
+ def initialize(value, successful)
43
43
  @value = value
44
- @success = (!!success).freeze
44
+ @successful = successful
45
45
  end
46
46
 
47
47
  # Whether this is a success result
48
48
  # @return [Boolean]
49
49
  def successful?
50
- @success
50
+ @successful
51
51
  end
52
52
 
53
53
  # @!attribute [r] value
@@ -59,8 +59,8 @@ module Teckel
59
59
  # @param default [Mixed] return this default value if it's not a failure result
60
60
  # @return [Mixed] the value/payload
61
61
  def failure(default = nil, &block)
62
- return @value unless @success
63
- return yield(@value) if block
62
+ return value unless @successful
63
+ return yield(value) if block
64
64
 
65
65
  default
66
66
  end
@@ -70,8 +70,8 @@ module Teckel
70
70
  # @param default [Mixed] return this default value if it's not a success result
71
71
  # @return [Mixed] the value/payload
72
72
  def success(default = nil, &block)
73
- return @value if @success
74
- return yield(@value) if block
73
+ return value if @successful
74
+ return yield(value) if block
75
75
 
76
76
  default
77
77
  end
@@ -29,7 +29,7 @@ module Teckel
29
29
 
30
30
  op = operation.new
31
31
  op.runner = self
32
- op.settings = settings if settings != UNDEFINED
32
+ op.settings = settings unless settings.eql?(UNDEFINED)
33
33
 
34
34
  @instance = op
35
35
  end
@@ -37,7 +37,7 @@ module Teckel
37
37
  # This is just here to raise a meaningful error.
38
38
  # @!visibility private
39
39
  def with(*)
40
- raise Teckel::Error, "Operation already has settings assigned."
40
+ raise Error, "Operation already has settings assigned."
41
41
  end
42
42
 
43
43
  # Halt any further execution with a output value
@@ -46,7 +46,7 @@ module Teckel
46
46
  # @!visibility protected
47
47
  def success!(*args)
48
48
  value =
49
- if args.size == 1 && operation.output === args.first # rubocop:disable Style/CaseEquality
49
+ if args.size.equal?(1) && operation.output === args.first # rubocop:disable Style/CaseEquality
50
50
  args.first
51
51
  else
52
52
  operation.output_constructor.call(*args)
@@ -61,7 +61,7 @@ module Teckel
61
61
  # @!visibility protected
62
62
  def fail!(*args)
63
63
  value =
64
- if args.size == 1 && operation.error === args.first # rubocop:disable Style/CaseEquality
64
+ if args.size.equal?(1) && operation.error === args.first # rubocop:disable Style/CaseEquality
65
65
  args.first
66
66
  else
67
67
  operation.error_constructor.call(*args)
@@ -58,6 +58,9 @@ module Teckel
58
58
  # @!visibility public
59
59
  module Operation
60
60
  module ClassMethods
61
+ # @!visibility private
62
+ UNDEFINED = Object.new
63
+
61
64
  # Invoke the Operation
62
65
  #
63
66
  # @param input Any form of input your {Teckel::Operation::Config#input input} class can handle via the given
@@ -66,21 +69,15 @@ module Teckel
66
69
  # {Teckel::Operation::Config#output output} class
67
70
  # @!visibility public
68
71
  def call(input = nil)
69
- default_settings = self.default_settings
70
-
71
- if default_settings
72
- runner.new(self, default_settings.call)
73
- else
74
- runner.new(self)
75
- end.call(input)
72
+ runable.call(input)
76
73
  end
77
74
 
78
- # Provide {InstanceMethods#settings() settings} to the running operation.
75
+ # Provide {InstanceMethods#settings() settings} to the operation.
79
76
  #
80
77
  # This method is intended to be called on the operation class outside of
81
- # it's definition, prior to running {#call}.
78
+ # it's definition, prior to invoking {#call}.
82
79
  #
83
- # @param input Any form of input your {Teckel::Operation::Config#settings settings} class can handle via the given
80
+ # @param settings Any form of settings your {Teckel::Operation::Config#settings settings} class can handle via the given
84
81
  # {Teckel::Operation::Config#settings_constructor settings_constructor}
85
82
  # @return [Class] The configured {Teckel::Operation::Config#runner runner}
86
83
  # @!visibility public
@@ -111,11 +108,31 @@ module Teckel
111
108
  # MyOperation.with(false).call
112
109
  # MyOperation.call
113
110
  # LOG #=> []
114
- def with(input)
115
- runner.new(self, settings_constructor.call(input))
111
+ def with(settings)
112
+ runable(settings_constructor.call(settings))
116
113
  end
117
114
  alias :set :with
118
115
 
116
+ # Constructs a Runner instance for {call} and {with}.
117
+ #
118
+ # @note This method is public to make testing, stubbing and mocking easier.
119
+ # Your normal application code should use {with} and/or {call}
120
+ #
121
+ # @param settings Optional. Any form of settings your
122
+ # {Teckel::Operation::Config#settings settings} class can handle via the
123
+ # given {Teckel::Operation::Config#settings_constructor settings_constructor}
124
+ # @return [Class] The configured {Teckel::Operation::Config#runner runner}
125
+ # @!visibility public
126
+ def runable(settings = UNDEFINED)
127
+ if settings != UNDEFINED
128
+ runner.new(self, settings)
129
+ elsif default_settings
130
+ runner.new(self, default_settings.call)
131
+ else
132
+ runner.new(self)
133
+ end
134
+ end
135
+
119
136
  # Convenience method for setting {Teckel::Operation::Config#input input},
120
137
  # {Teckel::Operation::Config#output output} or
121
138
  # {Teckel::Operation::Config#error error} to the
@@ -145,7 +162,7 @@ module Teckel
145
162
  #
146
163
  # MyOperation.call #=> nil
147
164
  def none
148
- Teckel::Contracts::None
165
+ Contracts::None
149
166
  end
150
167
  end
151
168
 
@@ -187,9 +204,11 @@ module Teckel
187
204
  end
188
205
 
189
206
  def self.included(receiver)
190
- receiver.extend Config
191
- receiver.extend ClassMethods
192
- receiver.send :include, InstanceMethods
207
+ receiver.class_eval do
208
+ extend Config
209
+ extend ClassMethods
210
+ include InstanceMethods
211
+ end
193
212
  end
194
213
  end
195
214
  end
data/lib/teckel/result.rb CHANGED
@@ -55,8 +55,10 @@ module Teckel
55
55
  end
56
56
 
57
57
  def self.included(receiver)
58
- receiver.extend ClassMethods
59
- receiver.send :include, InstanceMethods
58
+ receiver.class_eval do
59
+ extend ClassMethods
60
+ include InstanceMethods
61
+ end
60
62
  end
61
63
  end
62
64
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Teckel
4
- VERSION = "0.7.0"
4
+ VERSION = "0.8.0"
5
5
  end
data/spec/chain_spec.rb CHANGED
@@ -67,6 +67,11 @@ module TeckelChainTest
67
67
  end
68
68
 
69
69
  RSpec.describe Teckel::Chain do
70
+ let(:frozen_error) do
71
+ # different ruby versions raise different errors
72
+ defined?(FrozenError) ? FrozenError : RuntimeError
73
+ end
74
+
70
75
  it 'Chain input points to first step input' do
71
76
  expect(TeckelChainTest::Chain.input).to eq(TeckelChainTest::CreateUser.input)
72
77
  end
@@ -85,8 +90,7 @@ RSpec.describe Teckel::Chain do
85
90
 
86
91
  context "success" do
87
92
  it "result matches" do
88
- result =
89
- TeckelChainTest::Chain.
93
+ result = TeckelChainTest::Chain.
90
94
  with(befriend: nil).
91
95
  call(name: "Bob", age: 23)
92
96
 
@@ -96,8 +100,7 @@ RSpec.describe Teckel::Chain do
96
100
 
97
101
  context "failure" do
98
102
  it "returns a Result for invalid input" do
99
- result =
100
- TeckelChainTest::Chain.
103
+ result = TeckelChainTest::Chain.
101
104
  with(befriend: :fail).
102
105
  call(name: "Bob", age: 0)
103
106
 
@@ -108,8 +111,7 @@ RSpec.describe Teckel::Chain do
108
111
  end
109
112
 
110
113
  it "returns a Result for failed step" do
111
- result =
112
- TeckelChainTest::Chain.
114
+ result = TeckelChainTest::Chain.
113
115
  with(befriend: :fail).
114
116
  call(name: "Bob", age: 23)
115
117
 
@@ -121,11 +123,6 @@ RSpec.describe Teckel::Chain do
121
123
  end
122
124
 
123
125
  describe "#finalize!" do
124
- let(:frozen_error) do
125
- # different ruby versions raise different errors
126
- defined?(FrozenError) ? FrozenError : RuntimeError
127
- end
128
-
129
126
  subject { TeckelChainTest::Chain.dup }
130
127
 
131
128
  it "freezes the Chain class and operation classes" do
@@ -177,4 +174,82 @@ RSpec.describe Teckel::Chain do
177
174
  expect(subject.call).to eq(:mocked)
178
175
  end
179
176
  end
177
+
178
+ describe "#clone" do
179
+ subject { TeckelChainTest::Chain.dup }
180
+ let(:klone) { subject.clone }
181
+
182
+ it 'clones' do
183
+ expect(klone.object_id).not_to be_eql(subject.object_id)
184
+ end
185
+
186
+ it 'clones config' do
187
+ orig_config = subject.instance_variable_get(:@config)
188
+ klone_config = klone.instance_variable_get(:@config)
189
+ expect(klone_config.object_id).not_to be_eql(orig_config.object_id)
190
+ end
191
+
192
+ it 'clones steps' do
193
+ orig_settings = subject.instance_variable_get(:@config).instance_variable_get(:@config)[:steps]
194
+ klone_settings = klone.instance_variable_get(:@config).instance_variable_get(:@config)[:steps]
195
+
196
+ expect(orig_settings).to be_a(Array)
197
+ expect(klone_settings).to be_a(Array)
198
+ expect(klone_settings.object_id).not_to be_eql(orig_settings.object_id)
199
+ end
200
+ end
201
+
202
+ describe "frozen" do
203
+ subject { TeckelChainTest::Chain.dup }
204
+
205
+ it "also freezes the config" do
206
+ expect { subject.freeze }.to change {
207
+ [
208
+ subject.frozen?,
209
+ subject.instance_variable_get(:@config).frozen?
210
+ ]
211
+ }.from([false, false]).to([true, true])
212
+ end
213
+
214
+ it "prevents changes to steps" do
215
+ subject.freeze
216
+ expect {
217
+ subject.class_eval do
218
+ step :yet_other, TeckelChainTest::AddFriend
219
+ end
220
+ }.to raise_error(frozen_error)
221
+ end
222
+
223
+ it "prevents changes to config" do
224
+ subject.freeze
225
+ expect {
226
+ subject.class_eval do
227
+ default_settings!(a: { say: "Chain Default" })
228
+ end
229
+ }.to raise_error(frozen_error)
230
+ end
231
+
232
+ describe '#clone' do
233
+ subject { TeckelChainTest::Chain.dup }
234
+
235
+ it 'clones the class' do
236
+ subject.freeze
237
+ klone = subject.clone
238
+
239
+ expect(klone).to be_frozen
240
+ expect(klone.object_id).not_to be_eql(subject.object_id)
241
+ end
242
+
243
+ it 'cloned class uses the same, frozen config' do
244
+ subject.freeze
245
+ klone = subject.clone
246
+
247
+ orig_config = subject.instance_variable_get(:@config)
248
+ klone_config = klone.instance_variable_get(:@config)
249
+
250
+ expect(klone_config).to be_frozen
251
+ expect(klone_config.object_id).to be_eql(orig_config.object_id)
252
+ end
253
+ end
254
+ end
180
255
  end
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+
5
+ RSpec.describe Teckel::Operation do
6
+ let(:operation) do
7
+ Class.new do
8
+ include Teckel::Operation
9
+ input none
10
+ output ->(o) { o }
11
+ error none
12
+
13
+ def call(_)
14
+ success! settings
15
+ end
16
+ end
17
+ end
18
+
19
+ let(:blank_operation) do
20
+ Class.new do
21
+ include Teckel::Operation
22
+ end
23
+ end
24
+
25
+ describe ".settings" do
26
+ specify "no settings" do
27
+ expect(operation.settings).to eq(Teckel::Contracts::None)
28
+ expect(operation.settings_constructor).to eq(Teckel::Contracts::None.method(:new))
29
+ end
30
+
31
+ specify "with settings klass" do
32
+ settings_klass = Struct.new(:name)
33
+ operation.settings(settings_klass)
34
+ expect(operation.settings).to eq(settings_klass)
35
+ end
36
+
37
+ specify "without settings class, with settings constructor as proc" do
38
+ settings_const = if RUBY_VERSION < '2.6.0'
39
+ ->(sets) { sets.map { |k, v| [k.to_s, v.to_i] }.to_h }
40
+ else
41
+ ->(sets) { sets.to_h { |k, v| [k.to_s, v.to_i] } }
42
+ end
43
+
44
+ operation.settings_constructor(settings_const)
45
+
46
+ expect(operation.settings).to eq(Teckel::Contracts::None)
47
+ expect(operation.settings_constructor).to eq(settings_const)
48
+
49
+ runner = operation.with(key: "1")
50
+ expect(runner).to be_a(Teckel::Operation::Runner)
51
+ expect(runner.settings).to eq({ "key" => 1 })
52
+ end
53
+
54
+ specify "with settings class, with settings constructor as symbol" do
55
+ settings_klass = Struct.new(:name) do
56
+ def self.make_one(opts)
57
+ new(opts[:name])
58
+ end
59
+ end
60
+
61
+ operation.settings(settings_klass)
62
+ operation.settings_constructor(:make_one)
63
+
64
+ expect(operation.settings).to eq(settings_klass)
65
+ expect(operation.settings_constructor).to eq(settings_klass.method(:make_one))
66
+
67
+ runner = operation.with(name: "value")
68
+ expect(runner).to be_a(Teckel::Operation::Runner)
69
+ expect(runner.settings).to be_a(settings_klass)
70
+ expect(runner.settings.name).to eq("value")
71
+ end
72
+
73
+ specify "with settings class as constant" do
74
+ settings_klass = Struct.new(:name)
75
+ operation.const_set(:Settings, settings_klass)
76
+
77
+ expect(operation.settings).to eq(settings_klass)
78
+ expect(operation.settings_constructor).to eq(settings_klass.method(:[]))
79
+ end
80
+ end
81
+
82
+ describe ".default_settings" do
83
+ specify "no default_settings" do
84
+ expect(operation.default_settings).to be_nil
85
+ expect(operation.runner).to receive(:new).with(operation).and_call_original
86
+
87
+ operation.call
88
+ end
89
+
90
+ specify "default_settings!() with no default_settings" do
91
+ operation.default_settings!
92
+ expect(operation.default_settings).to be_a(Proc)
93
+
94
+ expect(operation.default_settings).to receive(:call).with(no_args).and_wrap_original do |original_method, *args, &block|
95
+ settings = original_method.call(*args, &block)
96
+ expect(settings).to be_nil
97
+ expect(operation.runner).to receive(:new).with(operation, settings).and_call_original
98
+ settings
99
+ end
100
+
101
+ operation.call
102
+ end
103
+
104
+ specify "default_settings!() with default_settings" do
105
+ settings_klass = Struct.new(:name)
106
+
107
+ operation.settings(settings_klass)
108
+ operation.default_settings!
109
+
110
+ expect(operation.default_settings).to be_a(Proc)
111
+
112
+ expect(operation.default_settings).to receive(:call).with(no_args).and_wrap_original do |original_method, *args, &block|
113
+ settings = original_method.call(*args, &block)
114
+ expect(settings).to be_a(settings_klass).and have_attributes(name: nil)
115
+ expect(operation.runner).to receive(:new).with(operation, settings).and_call_original
116
+ settings
117
+ end
118
+
119
+ operation.call
120
+ end
121
+
122
+ specify "default_settings!(arg) with default_settings" do
123
+ settings_klass = Struct.new(:name)
124
+
125
+ operation.settings(settings_klass)
126
+ operation.default_settings!("Bob")
127
+
128
+ expect(operation.default_settings).to be_a(Proc)
129
+
130
+ expect(operation.default_settings).to receive(:call).with(no_args).and_wrap_original do |original_method, *args, &block|
131
+ settings = original_method.call(*args, &block)
132
+ expect(settings).to be_a(settings_klass).and have_attributes(name: "Bob")
133
+ expect(operation.runner).to receive(:new).with(operation, settings).and_call_original
134
+ settings
135
+ end
136
+
137
+ expect(operation.call).to be_a(Struct).and have_attributes(name: "Bob")
138
+ end
139
+ end
140
+
141
+ %i[input output error].each do |meth|
142
+ describe ".#{meth}" do
143
+ specify "missing .#{meth} config raises MissingConfigError" do
144
+ expect {
145
+ blank_operation.public_send(meth)
146
+ }.to raise_error(Teckel::MissingConfigError, "Missing #{meth} config for #{blank_operation}")
147
+ end
148
+ end
149
+
150
+ describe ".#{meth}_constructor" do
151
+ specify "missing .#{meth}_constructor config raises MissingConfigError for missing #{meth}" do
152
+ expect {
153
+ blank_operation.public_send(:"#{meth}_constructor")
154
+ }.to raise_error(Teckel::MissingConfigError, "Missing #{meth} config for #{blank_operation}")
155
+ end
156
+ end
157
+ end
158
+
159
+ specify "default settings config" do
160
+ expect(blank_operation.settings).to eq(Teckel::Contracts::None)
161
+ end
162
+
163
+ specify "default settings_constructor" do
164
+ expect(blank_operation.settings_constructor).to eq(Teckel::Contracts::None.method(:[]))
165
+ end
166
+
167
+ specify "default settings_constructor with settings config set" do
168
+ settings_klass = Struct.new(:name)
169
+ blank_operation.settings(settings_klass)
170
+
171
+ expect(blank_operation.settings_constructor).to eq(settings_klass.method(:[]))
172
+ end
173
+
174
+ specify "unsupported constructor method" do
175
+ blank_operation.settings(Class.new)
176
+ expect {
177
+ blank_operation.settings_constructor(:nope)
178
+ }.to raise_error(Teckel::MissingConfigError, "Missing settings_constructor config for #{blank_operation}")
179
+
180
+ expect {
181
+ blank_operation.settings_constructor
182
+ }.to raise_error(Teckel::MissingConfigError, "Missing settings_constructor config for #{blank_operation}")
183
+ end
184
+
185
+ describe "result" do
186
+ specify "default result config" do
187
+ expect(blank_operation.result).to eq(Teckel::Operation::ValueResult)
188
+ end
189
+
190
+ specify "default result_constructor" do
191
+ expect(blank_operation.result_constructor).to eq(Teckel::Operation::ValueResult.method(:[]))
192
+ end
193
+
194
+ specify "default result_constructor with settings config set" do
195
+ result_klass = OpenStruct.new
196
+ blank_operation.result(result_klass)
197
+
198
+ expect(blank_operation.result_constructor).to eq(result_klass.method(:[]))
199
+ end
200
+
201
+ specify "unsupported constructor method" do
202
+ blank_operation.result(Class.new)
203
+ expect {
204
+ blank_operation.result_constructor(:nope)
205
+ }.to raise_error(Teckel::MissingConfigError, "Missing result_constructor config for #{blank_operation}")
206
+
207
+ expect {
208
+ blank_operation.result_constructor
209
+ }.to raise_error(Teckel::MissingConfigError, "Missing result_constructor config for #{blank_operation}")
210
+ end
211
+
212
+ specify "with result class as constant" do
213
+ result_klass = OpenStruct.new
214
+ blank_operation.const_set(:Result, result_klass)
215
+
216
+ expect(blank_operation.result).to eq(result_klass)
217
+ expect(blank_operation.result_constructor).to eq(result_klass.method(:[]))
218
+ end
219
+ end
220
+
221
+ describe "result!" do
222
+ specify "default result config" do
223
+ blank_operation.result!
224
+ expect(blank_operation.result).to eq(Teckel::Operation::Result)
225
+ end
226
+ end
227
+ end
@@ -26,6 +26,7 @@ module TeckelOperationContractTrace
26
26
  end
27
27
  end
28
28
 
29
+ # rubocop:disable Style/EvalWithLocation
29
30
  # Hack to get reliable stack traces
30
31
  eval <<~RUBY, binding, "operation_success_error.rb"
31
32
  module TeckelOperationContractTrace
@@ -77,6 +78,7 @@ eval <<~RUBY, binding, "operation_input_error.rb"
77
78
  end
78
79
  end
79
80
  RUBY
81
+ # rubocop:enable Style/EvalWithLocation
80
82
 
81
83
  RSpec.describe Teckel::Operation do
82
84
  context "contract errors include meaningful trace" do
@@ -89,6 +89,32 @@ RSpec.describe Teckel::Operation do
89
89
  default_settings!(:default_value)
90
90
  end
91
91
  )
92
+
93
+ it_behaves_like(
94
+ "operation with default settings",
95
+ Class.new(TeckelOperationDefaultSettings::BaseOperation) do
96
+ settings Struct.new(:injected)
97
+
98
+ default_settings!("default_value")
99
+
100
+ output_constructor ->(out) { out&.to_sym }
101
+ end
102
+ )
103
+ end
104
+
105
+ describe "with default constructor and simple Settings class responding to passed default setting" do
106
+ it_behaves_like(
107
+ "operation with default settings",
108
+ Class.new(TeckelOperationDefaultSettings::BaseOperation) do
109
+ settings(Struct.new(:injected) do
110
+ def self.default
111
+ new(:default_value)
112
+ end
113
+ end)
114
+
115
+ default_settings!(:default)
116
+ end
117
+ )
92
118
  end
93
119
  end
94
120
  end
@@ -3,21 +3,15 @@
3
3
  RSpec.describe Teckel::Operation::Result do
4
4
  let(:failure_value) { "some error" }
5
5
  let(:failed_result) { Teckel::Operation::Result.new(failure_value, false) }
6
- let(:failed_nil_result) { Teckel::Operation::Result.new(failure_value, nil) }
7
6
 
8
7
  let(:success_value) { "some success" }
9
8
  let(:successful_result) { Teckel::Operation::Result.new(success_value, true) }
10
- let(:successful_1_result) { Teckel::Operation::Result.new(success_value, 1) }
11
9
 
12
10
  it { expect(successful_result.successful?).to eq(true) }
13
- it { expect(successful_1_result.successful?).to eq(true) }
14
11
  it { expect(failed_result.successful?).to eq(false) }
15
- it { expect(failed_nil_result.successful?).to eq(false) }
16
12
 
17
13
  it { expect(successful_result.failure?).to eq(false) }
18
- it { expect(successful_1_result.failure?).to eq(false) }
19
14
  it { expect(failed_result.failure?).to eq(true) }
20
- it { expect(failed_nil_result.failure?).to eq(true) }
21
15
 
22
16
  it { expect(successful_result.value).to eq(success_value) }
23
17
  it { expect(failed_result.value).to eq(failure_value) }
@@ -229,6 +229,11 @@ module TeckelOperationInjectSettingsTest
229
229
  end
230
230
 
231
231
  RSpec.describe Teckel::Operation do
232
+ let(:frozen_error) do
233
+ # different ruby versions raise different errors
234
+ defined?(FrozenError) ? FrozenError : RuntimeError
235
+ end
236
+
232
237
  context "predefined classes" do
233
238
  specify "Input" do
234
239
  expect(TeckelOperationPredefinedClassesTest::CreateUser.input).to eq(TeckelOperationPredefinedClassesTest::CreateUserInput)
@@ -331,8 +336,7 @@ RSpec.describe Teckel::Operation do
331
336
  end
332
337
 
333
338
  it "uses injected data" do
334
- result =
335
- TeckelOperationInjectSettingsTest::MyOperation.
339
+ result = TeckelOperationInjectSettingsTest::MyOperation.
336
340
  with(injected: [:stuff]).
337
341
  call
338
342
 
@@ -342,10 +346,10 @@ RSpec.describe Teckel::Operation do
342
346
  end
343
347
 
344
348
  specify "calling `with` multiple times raises an error" do
345
- op = TeckelOperationInjectSettingsTest::MyOperation.with(injected: :stuff_1)
349
+ op = TeckelOperationInjectSettingsTest::MyOperation.with(injected: :stuff1)
346
350
 
347
351
  expect {
348
- op.with(more: :stuff_2)
352
+ op.with(more: :stuff2)
349
353
  }.to raise_error(Teckel::Error, "Operation already has settings assigned.")
350
354
  end
351
355
  end
@@ -396,11 +400,6 @@ RSpec.describe Teckel::Operation do
396
400
  end
397
401
 
398
402
  describe "#finalize!" do
399
- let(:frozen_error) do
400
- # different ruby versions raise different errors
401
- defined?(FrozenError) ? FrozenError : RuntimeError
402
- end
403
-
404
403
  subject do
405
404
  Class.new do
406
405
  include ::Teckel::Operation
@@ -480,4 +479,53 @@ RSpec.describe Teckel::Operation do
480
479
  }.to raise_error Teckel::FrozenConfigError, "Configuration input is already set"
481
480
  end
482
481
  end
482
+
483
+ describe "frozen" do
484
+ subject do
485
+ Class.new do
486
+ include ::Teckel::Operation
487
+
488
+ input none
489
+ output none
490
+ error none
491
+
492
+ def call(_input); end
493
+ end
494
+ end
495
+
496
+ it "also freezes the config" do
497
+ expect { subject.freeze }.to change {
498
+ [
499
+ subject.frozen?,
500
+ subject.instance_variable_get(:@config).frozen?
501
+ ]
502
+ }.from([false, false]).to([true, true])
503
+ end
504
+
505
+ it "prevents changes to config" do
506
+ subject.freeze
507
+ expect { subject.settings Struct.new(:test) }.to raise_error(frozen_error)
508
+ end
509
+
510
+ describe '#clone' do
511
+ it 'clones the class' do
512
+ subject.freeze
513
+ klone = subject.clone
514
+
515
+ expect(klone).to be_frozen
516
+ expect(klone.object_id).not_to be_eql(subject.object_id)
517
+ end
518
+
519
+ it 'cloned class uses the same, frozen config' do
520
+ subject.freeze
521
+ klone = subject.clone
522
+
523
+ orig_config = subject.instance_variable_get(:@config)
524
+ klone_config = klone.instance_variable_get(:@config)
525
+
526
+ expect(klone_config).to be_frozen
527
+ expect(klone_config.object_id).to be_eql(orig_config.object_id)
528
+ end
529
+ end
530
+ end
483
531
  end
data/spec/spec_helper.rb CHANGED
@@ -3,8 +3,15 @@
3
3
  require "bundler/setup"
4
4
  if ENV['COVERAGE'] == 'true'
5
5
  require 'simplecov'
6
- require 'simplecov_json_formatter'
7
- SimpleCov.formatter = SimpleCov::Formatter::JSONFormatter
6
+
7
+ SimpleCov.formatter = case ENV['SIMPLECOV']&.downcase
8
+ when 'html'
9
+ SimpleCov::Formatter::HTMLFormatter
10
+ else
11
+ require 'simplecov_json_formatter'
12
+ SimpleCov::Formatter::JSONFormatter
13
+ end
14
+
8
15
  SimpleCov.start do
9
16
  add_filter %r{^/spec/}
10
17
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: teckel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Schulze
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-01-03 00:00:00.000000000 Z
11
+ date: 2021-09-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: mutant-rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: yard
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -115,6 +129,7 @@ files:
115
129
  - spec/chain_spec.rb
116
130
  - spec/config_spec.rb
117
131
  - spec/doctest_helper.rb
132
+ - spec/operation/config_spec.rb
118
133
  - spec/operation/contract_trace_spec.rb
119
134
  - spec/operation/default_settings_spec.rb
120
135
  - spec/operation/fail_on_input_spec.rb
@@ -133,10 +148,10 @@ homepage: https://github.com/fnordfish/teckel
133
148
  licenses:
134
149
  - Apache-2.0
135
150
  metadata:
136
- changelog_uri: https://github.com/fnordfish/teckel/blob/master/CHANGELOG.md
151
+ changelog_uri: https://github.com/fnordfish/teckel/blob/main/CHANGELOG.md
137
152
  source_code_uri: https://github.com/fnordfish/teckel
138
153
  bug_tracker_uri: https://github.com/fnordfish/teckel/issues
139
- documentation_uri: https://www.rubydoc.info/gems/teckel/0.7.0
154
+ documentation_uri: https://www.rubydoc.info/gems/teckel/0.8.0
140
155
  user_docs_uri: https://fnordfish.github.io/teckel/
141
156
  post_install_message:
142
157
  rdoc_options: []
@@ -153,7 +168,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
168
  - !ruby/object:Gem::Version
154
169
  version: '0'
155
170
  requirements: []
156
- rubygems_version: 3.2.3
171
+ rubygems_version: 3.2.15
157
172
  signing_key:
158
173
  specification_version: 4
159
174
  summary: Operations with enforced in/out/err data structures
@@ -166,6 +181,7 @@ test_files:
166
181
  - spec/chain_spec.rb
167
182
  - spec/config_spec.rb
168
183
  - spec/doctest_helper.rb
184
+ - spec/operation/config_spec.rb
169
185
  - spec/operation/contract_trace_spec.rb
170
186
  - spec/operation/default_settings_spec.rb
171
187
  - spec/operation/fail_on_input_spec.rb