teckel 0.7.0 → 0.8.0

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