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,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teckel
4
+ module Operation
5
+ # The optional, default result object for {Teckel::Operation}s.
6
+ # Wraps +output+ and +error+ into a {Teckel::Operation::Result}.
7
+ #
8
+ # @example
9
+ # class CreateUser
10
+ # include Teckel::Operation
11
+ #
12
+ # result! # Shortcut to use this Result object
13
+ #
14
+ # input Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer)
15
+ # output Types.Instance(User)
16
+ # error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
17
+ #
18
+ # def call(input)
19
+ # user = User.new(name: input[:name], age: input[:age])
20
+ # if user.save
21
+ # success!(user) # exits early with success, prevents any further execution
22
+ # else
23
+ # fail!(message: "Could not save User", errors: user.errors)
24
+ # end
25
+ # end
26
+ # end
27
+ #
28
+ # # A success call:
29
+ # CreateUser.call(name: "Bob", age: 23).is_a?(Teckel::Operation::Result) #=> true
30
+ # CreateUser.call(name: "Bob", age: 23).success.is_a?(User) #=> true
31
+ #
32
+ # # A failure call:
33
+ # CreateUser.call(name: "Bob", age: 10).is_a?(Teckel::Operation::Result) #=> true
34
+ # CreateUser.call(name: "Bob", age: 10).failure.is_a?(Hash) #=> true
35
+ #
36
+ # @!visibility public
37
+ class Result
38
+ include Teckel::Result
39
+
40
+ # @param value [Object] The result value
41
+ # @param success [Boolean] whether this is a successful result
42
+ def initialize(value, success)
43
+ @value = value
44
+ @success = (!!success).freeze
45
+ end
46
+
47
+ # Whether this is a success result
48
+ # @return [Boolean]
49
+ def successful?
50
+ @success
51
+ end
52
+
53
+ # @!attribute [r] value
54
+ # @return [Mixed] the value/payload
55
+ attr_reader :value
56
+
57
+ # Get the error/failure value
58
+ # @yield [Mixed] If a block is given and this is not a failure result, the value is yielded to the block
59
+ # @param default [Mixed] return this default value if it's not a failure result
60
+ # @return [Mixed] the value/payload
61
+ def failure(default = nil, &block)
62
+ return @value unless @success
63
+ return yield(@value) if block
64
+
65
+ default
66
+ end
67
+
68
+ # Get the success value
69
+ # @yield [Mixed] If a block is given and this is not a success result, the value is yielded to the block
70
+ # @param default [Mixed] return this default value if it's not a success result
71
+ # @return [Mixed] the value/payload
72
+ def success(default = nil, &block)
73
+ return @value if @success
74
+ return yield(@value) if block
75
+
76
+ default
77
+ end
78
+ end
79
+
80
+ # The default "no-op" Result handler. Just returns the value, ignoring the
81
+ # success state.
82
+ module ValueResult
83
+ class << self
84
+ def [](value, *_)
85
+ value
86
+ end
87
+
88
+ alias :new :[]
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teckel
4
+ module Operation
5
+ # The default implementation for executing a single {Operation}
6
+ # @note You shouldn't need to call this explicitly.
7
+ # Use {ClassMethods#with MyOperation.with()} or {ClassMethods#with MyOperation.call()} instead.
8
+ # @!visibility protected
9
+ class Runner
10
+ # @!visibility private
11
+ UNDEFINED = Object.new.freeze
12
+
13
+ def initialize(operation, settings = UNDEFINED)
14
+ @operation, @settings = operation, settings
15
+ end
16
+ attr_reader :operation, :settings
17
+
18
+ def call(input = nil)
19
+ err = catch(:failure) do
20
+ simple_return = UNDEFINED
21
+ out = catch(:success) do
22
+ simple_return = call!(build_input(input))
23
+ end
24
+ return simple_return == UNDEFINED ? build_output(*out) : build_output(simple_return)
25
+ end
26
+ build_error(*err)
27
+ end
28
+
29
+ # This is just here to raise a meaningful error.
30
+ # @!visibility private
31
+ def with(*)
32
+ raise Teckel::Error, "Operation already has settings assigned."
33
+ end
34
+
35
+ private
36
+
37
+ def call!(input)
38
+ op = @operation.new
39
+ op.settings = settings if settings != UNDEFINED
40
+ op.call(input)
41
+ end
42
+
43
+ def build_input(input)
44
+ operation.input_constructor.call(input)
45
+ end
46
+
47
+ def build_output(*args)
48
+ value =
49
+ if args.size == 1 && operation.output === args.first # rubocop:disable Style/CaseEquality
50
+ args.first
51
+ else
52
+ operation.output_constructor.call(*args)
53
+ end
54
+
55
+ operation.result_constructor.call(value, true)
56
+ end
57
+
58
+ def build_error(*args)
59
+ value =
60
+ if args.size == 1 && operation.error === args.first # rubocop:disable Style/CaseEquality
61
+ args.first
62
+ else
63
+ operation.error_constructor.call(*args)
64
+ end
65
+
66
+ operation.result_constructor.call(value, false)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -1,63 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Teckel
4
- # Wrapper for +output+ and +error+ return values of Operations
5
- #
6
- # @example asking for status
7
- #
8
- # Teckel::Result.new("some output", true).successful? #=> true
9
- # Teckel::Result.new("some output", true).failure? #=> false
10
- #
11
- # Teckel::Result.new("some error", false).successful? #=> false
12
- # Teckel::Result.new("some error", false).failure? #=> true
13
- #
14
- # @example Use +.value+ to get the wrapped value regardless of success state
15
- #
16
- # Teckel::Result.new("some output", true).value #=> "some output"
17
- # Teckel::Result.new("some error", false).value #=> "some error"
18
- #
19
- # @example Use +.success+ to get the wrapped value of a successful result
20
- #
21
- # # Note: The +.failure+ method works just the same for successful results
22
- # Teckel::Result.new("some output", true).success #=> "some output"
23
- # Teckel::Result.new("some error", false).success #=> nil
24
- # Teckel::Result.new("some error", false).success("other default") #=> "other default"
25
- # Teckel::Result.new("some error", false).success { |value| "Failed: #{value}" } #=> "Failed: some error"
26
- #
27
- # @!visibility public
28
- class Result
29
- # @param value [Mixed] the value/payload of the result.
30
- # @param success [Bool] whether this is a successful result
31
- def initialize(value, success)
32
- @value = value
33
- @success = (!!success).freeze
4
+ # @abstract The interface an {Operation}s result object needs to adopt.
5
+ #
6
+ # @example
7
+ # class MyResult
8
+ # include Teckel::Result
9
+ #
10
+ # def initialize(value, success)
11
+ # @value = value
12
+ # @success = (!!success).freeze
13
+ # end
14
+ #
15
+ # def successful?; @success end
16
+ #
17
+ # def value; @value end
18
+ # end
19
+ module Result
20
+ module ClassMethods
21
+ def [](value, success)
22
+ new(value, success)
23
+ end
34
24
  end
35
25
 
36
- # @!attribute [r] value
37
- # @return [Mixed] the value/payload
38
- attr_reader :value
39
-
40
- def successful?
41
- @success
42
- end
43
-
44
- def failure?
45
- !@success
46
- end
47
-
48
- def failure(default = nil, &block)
49
- return @value if !@success
50
- return yield(@value) if block
51
-
52
- default
26
+ module InstanceMethods
27
+ # Whether this is a success result
28
+ # @return [Boolean]
29
+ def successful?
30
+ raise NotImplementedError, "Result object does not implement `successful?`"
31
+ end
32
+
33
+ # Whether this is a error/failure result
34
+ # @return [Boolean]
35
+ def failure?
36
+ !successful?
37
+ end
38
+
39
+ # @!attribute [r] value
40
+ # @return [Mixed] the value/payload
41
+ def value
42
+ raise NotImplementedError, "Result object does not implement `value`"
43
+ end
44
+
45
+ def deconstruct
46
+ [successful?, value]
47
+ end
48
+
49
+ def deconstruct_keys(keys)
50
+ {}.tap do |e|
51
+ e[:success] = successful? if keys.include?(:success)
52
+ e[:value] = value if keys.include?(:value)
53
+ end
54
+ end
53
55
  end
54
56
 
55
- def success(default = nil, &block)
56
- return @value if @success
57
-
58
- return yield(@value) if block
59
-
60
- default
57
+ def self.included(receiver)
58
+ receiver.extend ClassMethods
59
+ receiver.send :include, InstanceMethods
61
60
  end
62
61
  end
63
62
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Teckel
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'support/dry_base'
4
+ require 'support/fake_models'
5
+
6
+ RSpec.describe Teckel::Chain do
7
+ module TeckelChainDefaultsViaBaseClass
8
+ LOG = [] # rubocop:disable Style/MutableConstant
9
+
10
+ class LoggingChain
11
+ include Teckel::Chain
12
+
13
+ around do |chain, input|
14
+ require 'benchmark'
15
+ result = nil
16
+ LOG << Benchmark.measure { result = chain.call(input) }
17
+ result
18
+ end
19
+
20
+ freeze
21
+ end
22
+
23
+ class OperationA
24
+ include Teckel::Operation
25
+
26
+ result!
27
+
28
+ input none
29
+ output Types::Integer
30
+ error none
31
+
32
+ def call(_)
33
+ rand(1000)
34
+ end
35
+
36
+ finalize!
37
+ end
38
+
39
+ class OperationB
40
+ include Teckel::Operation
41
+
42
+ result!
43
+
44
+ input none
45
+ output Types::String
46
+ error none
47
+
48
+ def call(_)
49
+ ("a".."z").to_a.sample
50
+ end
51
+
52
+ finalize!
53
+ end
54
+
55
+ class ChainA < LoggingChain
56
+ step :roll, OperationA
57
+
58
+ finalize!
59
+ end
60
+
61
+ class ChainB < LoggingChain
62
+ step :say, OperationB
63
+
64
+ finalize!
65
+ end
66
+
67
+ class ChainC < ChainB
68
+ finalize!
69
+ end
70
+ end
71
+
72
+ before do
73
+ TeckelChainDefaultsViaBaseClass::LOG.clear
74
+ end
75
+
76
+ let(:base_chain) { TeckelChainDefaultsViaBaseClass::LoggingChain }
77
+ let(:chain_a) { TeckelChainDefaultsViaBaseClass::ChainA }
78
+ let(:chain_b) { TeckelChainDefaultsViaBaseClass::ChainB }
79
+ let(:chain_c) { TeckelChainDefaultsViaBaseClass::ChainC }
80
+
81
+ it "inherits config" do
82
+ expect(chain_a.around)
83
+ expect(chain_b.around)
84
+
85
+ expect(base_chain.steps).to eq([])
86
+ expect(chain_a.steps.size).to eq(1)
87
+ expect(chain_b.steps.size).to eq(1)
88
+ end
89
+
90
+ it "runs chain a" do
91
+ expect {
92
+ result = chain_a.call
93
+ expect(result.success).to be_a(Integer)
94
+ }.to change {
95
+ TeckelChainDefaultsViaBaseClass::LOG.size
96
+ }.from(0).to(1)
97
+ end
98
+
99
+ it "runs chain b" do
100
+ expect {
101
+ result = chain_b.call
102
+ expect(result.success).to be_a(String)
103
+ }.to change {
104
+ TeckelChainDefaultsViaBaseClass::LOG.size
105
+ }.from(0).to(1)
106
+ end
107
+
108
+ it "inherits steps" do
109
+ expect {
110
+ result = chain_c.call
111
+ expect(result.success).to be_a(String)
112
+ }.to change {
113
+ TeckelChainDefaultsViaBaseClass::LOG.size
114
+ }.from(0).to(1)
115
+ end
116
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'support/dry_base'
4
+ require 'support/fake_models'
5
+
6
+ RSpec.describe Teckel::Chain do
7
+ module TeckelChainResultTest
8
+ class Message
9
+ include ::Teckel::Operation
10
+
11
+ result!
12
+
13
+ input Types::Hash.schema(message: Types::String)
14
+ error none
15
+ output Types::String
16
+
17
+ def call(input)
18
+ input[:message].upcase
19
+ end
20
+ end
21
+
22
+ class Chain
23
+ include Teckel::Chain
24
+
25
+ step :message, Message
26
+
27
+ class Result < Teckel::Operation::Result
28
+ def initialize(value, success, step, opts = {})
29
+ super(value, success)
30
+ @step = step
31
+ @opts = opts
32
+ end
33
+
34
+ class << self
35
+ alias :[] :new # Alias the default constructor to :new
36
+ end
37
+
38
+ attr_reader :opts, :step
39
+ end
40
+
41
+ result_constructor ->(value, success, step) {
42
+ result.new(value, success, step, time: Time.now.to_i)
43
+ }
44
+ end
45
+ end
46
+
47
+ specify do
48
+ result = TeckelChainResultTest::Chain.call(message: "Hello World!")
49
+ expect(result).to be_successful
50
+ expect(result.success).to eq("HELLO WORLD!")
51
+ expect(result.opts).to include(time: kind_of(Integer))
52
+ end
53
+ end