teckel 0.1.0 → 0.6.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +114 -0
  3. data/LICENSE_LOGO +4 -0
  4. data/README.md +22 -14
  5. data/lib/teckel.rb +11 -2
  6. data/lib/teckel/chain.rb +47 -152
  7. data/lib/teckel/chain/config.rb +246 -0
  8. data/lib/teckel/chain/result.rb +38 -0
  9. data/lib/teckel/chain/runner.rb +62 -0
  10. data/lib/teckel/chain/step.rb +18 -0
  11. data/lib/teckel/config.rb +41 -52
  12. data/lib/teckel/contracts.rb +19 -0
  13. data/lib/teckel/operation.rb +108 -253
  14. data/lib/teckel/operation/config.rb +396 -0
  15. data/lib/teckel/operation/result.rb +92 -0
  16. data/lib/teckel/operation/runner.rb +75 -0
  17. data/lib/teckel/result.rb +52 -53
  18. data/lib/teckel/version.rb +1 -1
  19. data/spec/chain/default_settings_spec.rb +39 -0
  20. data/spec/chain/inheritance_spec.rb +116 -0
  21. data/spec/chain/none_input_spec.rb +36 -0
  22. data/spec/chain/results_spec.rb +53 -0
  23. data/spec/chain_around_hook_spec.rb +100 -0
  24. data/spec/chain_spec.rb +180 -0
  25. data/spec/config_spec.rb +26 -0
  26. data/spec/doctest_helper.rb +7 -0
  27. data/spec/operation/contract_trace_spec.rb +116 -0
  28. data/spec/operation/default_settings_spec.rb +94 -0
  29. data/spec/operation/inheritance_spec.rb +94 -0
  30. data/spec/operation/result_spec.rb +34 -0
  31. data/spec/operation/results_spec.rb +117 -0
  32. data/spec/operation_spec.rb +483 -0
  33. data/spec/rb27/pattern_matching_spec.rb +193 -0
  34. data/spec/result_spec.rb +22 -0
  35. data/spec/spec_helper.rb +25 -0
  36. data/spec/support/dry_base.rb +8 -0
  37. data/spec/support/fake_db.rb +12 -0
  38. data/spec/support/fake_models.rb +20 -0
  39. data/spec/teckel_spec.rb +7 -0
  40. metadata +64 -46
  41. data/.github/workflows/ci.yml +0 -67
  42. data/.github/workflows/pages.yml +0 -50
  43. data/.gitignore +0 -13
  44. data/.rspec +0 -3
  45. data/.rubocop.yml +0 -12
  46. data/.ruby-version +0 -1
  47. data/DEVELOPMENT.md +0 -28
  48. data/Gemfile +0 -8
  49. data/Gemfile.lock +0 -71
  50. data/Rakefile +0 -30
  51. data/bin/console +0 -15
  52. data/bin/rake +0 -29
  53. data/bin/rspec +0 -29
  54. data/bin/rubocop +0 -18
  55. data/bin/setup +0 -8
  56. data/lib/teckel/operation/results.rb +0 -71
  57. data/teckel.gemspec +0 -33
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f8117e6d304e571ccfd26db58af5122330494e3df8b2597cfa5f8e11ad19b12
4
- data.tar.gz: 9aeecbf92e87d3fa0610defed63f0e3f1048925348c43af00d83aab68ffb1265
3
+ metadata.gz: d55dd7fb782cfdcb3cc960423e46ab2ce8b49c9396273f10749105883d55ed6f
4
+ data.tar.gz: e3b7428d98daeb42cc086b4a5cd5081b5c3b4ca90c14b0cf3f4cd9576243d0e3
5
5
  SHA512:
6
- metadata.gz: 7b6eef841002932cad55e8bd4a7121d173624633962de203e0eda72602bbf36afa947126100bd7795ab078540b4b7210a32fc0d51eea117c558943d418026526
7
- data.tar.gz: 60c090f9fd1ec11deb535dcd0e9838146401f361754f15469a80104b69ec58d68a64c5d32399b29d1fb0264538e418e3cf1c3188df6f3fac1af6c87f2fca631b
6
+ metadata.gz: '0148d426e8e7124a08a3a7dd3c42487cac5944d17ba9cabb98a8c378a86a5d9813b7b62c483d2203809644528d2e6b6cc4de51476aa67261b1405f3f41576613'
7
+ data.tar.gz: 6792ad4806b977d21d0ea621e311306cb13c239d8604e35e61937e79ce4f5488cbb410aa6ae22e9e12e48b61529f97e924aab22e100717808885c0b0c76980fd
@@ -0,0 +1,114 @@
1
+ # Changes
2
+
3
+ ## 0.6.0
4
+
5
+ - Breaking: Operations return values will be ignored. [GH-21]
6
+ * You'll need to use `success!` or `failure!`
7
+ * `success!` and `failure!` are now implemented on the `Runner`, which makes it easier to change their behavior (including the one above).
8
+
9
+ ## 0.5.0
10
+
11
+ - Fix: calling chain with settings and no input [GH-14]
12
+ - Add: Default settings for Operation and Chains [GH-17], [GH-18]
13
+ ```ruby
14
+ class MyOperation
15
+ include Teckel::Operation
16
+
17
+ settings Struct.new(:logger)
18
+
19
+ # If your settings class can cope with no input and you want to make sure
20
+ # `settings` gets initialized and set.
21
+ # settings will be #<struct logger=nil>
22
+ default_settings!
23
+
24
+ # settings will be #<struct logger=MyGlobalLogger>
25
+ default_settings!(MyGlobalLogger)
26
+
27
+ # settings will be #<struct logger=#<Logger:<...>>
28
+ default_settings! -> { settings.new(Logger.new("/tmp/my.log")) }
29
+ end
30
+
31
+ class Chain
32
+ include Teckel::Chain
33
+
34
+ # set or overwrite operation settings
35
+ default_settings!(a: MyOtherLogger)
36
+
37
+ step :a, MyOperation
38
+ end
39
+ ```
40
+
41
+ Internal:
42
+ - Move operation and chain config dsl methods into own module [GH-15]
43
+ - Code simplifications [GH-16]
44
+
45
+ ## 0.4.0
46
+
47
+ - Moving verbose examples from API docs into github pages
48
+ - `#finalize!` no longer freezes the entire Operation or Chain class, only it's settings. [GH-13]
49
+ - Add simple support for using Base classes. [GH-10]
50
+ Removes global configuration `Teckel::Config.default_constructor`
51
+ ```ruby
52
+ class ApplicationOperation
53
+ include Teckel::Operation
54
+ # you won't be able to overwrite any configuration in child classes,
55
+ # so take care which you want to declare
56
+ result!
57
+ settings Struct.new(:logger)
58
+ input_constructor :new
59
+ error Struct.new(:status, :messages)
60
+
61
+ def log(message)
62
+ return unless settings&.logger
63
+ logger << message
64
+ end
65
+ # you cannot call `finalize!` on partially declared Operations
66
+ end
67
+ ```
68
+ - Add support for setting your own Result objects. [GH-9]
69
+ - They should include and implement `Teckel::Result` which is needed by `Chain`.
70
+ - `Chain::StepFailure` got replaced with `Chain::Result`.
71
+ - the `Teckel::Operation::Results` module was removed. To let Operation use the default Result object, use the new helper `result!` instead.
72
+ - Add "settings"/dependency injection to Operation and Chains. [GH-7]
73
+ ```ruby
74
+ MyOperation.with(logger: STDOUT).call(params)
75
+
76
+ MyChain.with(some_step: { logger: STDOUT }).call(params)
77
+ ```
78
+ - [GH-5] Add support for ruby 2.7 pattern matching on Operation and Chain results. Both, array and hash notations are supported:
79
+ ```ruby
80
+ case MyOperation.call(params)
81
+ in [false, value]
82
+ # handle failure
83
+ in [true, value]
84
+ # handle success
85
+ end
86
+
87
+ case MyChain.call(params)
88
+ in { success: false, step: :foo, value: value }
89
+ # handle foo failure
90
+ in [success: false, step: :bar, value: value }
91
+ # handle bar failure
92
+ in { success: true, value: value }
93
+ # handle success
94
+ end
95
+ ```
96
+ - Fix setting a config twice to raise an error
97
+
98
+ ## 0.3.0
99
+
100
+ - `finalize!`'ing a Chain will also finalize all it's Operations
101
+ - Changed attribute naming of `StepFailure`:
102
+ + `.operation` will now give the operation class of the step - was `.step` before
103
+ + `.step` will now give the name of the step (which Operation failed) - was `.step_name` before
104
+
105
+ ## 0.2.0
106
+
107
+ - Around Hooks for Chains
108
+ - `finalize!`
109
+ - freezing Chains and Operations, to prevent further changes
110
+ - Operations check their config and raise if any is missing
111
+
112
+ ## 0.1.0
113
+
114
+ - Initial release
@@ -0,0 +1,4 @@
1
+ The Teckel Logo artwork is NOT part of the Work as described in the LICENSE.
2
+ Any Derivative Works shall not use the Teckel Logo.
3
+
4
+ Copyright 2020 Jana Vogel <jana@dotless.de>
data/README.md CHANGED
@@ -5,7 +5,8 @@ Ruby service classes with enforced<sup name="footnote-1-source">[1](#footnote-1)
5
5
  [![Gem Version](https://img.shields.io/gem/v/teckel.svg)][gem]
6
6
  [![Build Status](https://github.com/dry-rb/dry-configurable/workflows/ci/badge.svg)][ci]
7
7
  [![Maintainability](https://api.codeclimate.com/v1/badges/b3939aaec6271a567a57/maintainability)](https://codeclimate.com/github/fnordfish/teckel/maintainability)
8
- [![API Documentation Coverage](http://inch-ci.org/github/dry-rb/dry-configurable.svg)][inch]
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
10
 
10
11
  ## Installation
11
12
 
@@ -33,30 +34,35 @@ Working with [Interactor](https://github.com/collectiveidea/interactor), [Trailb
33
34
 
34
35
  ## Usage
35
36
 
36
- For a full overview please see the [Api Docs](https://fnordfish.github.io/teckel/doc/)
37
+ For a full overview please see the Docs:
38
+
39
+ * [Operations](https://fnordfish.github.io/teckel/operations/basics/)
40
+ * [Result Objects](https://fnordfish.github.io/teckel/operations/result_objects/)
41
+ * [Chains](https://fnordfish.github.io/teckel/chains/basics/)
37
42
 
38
- This example uses [Dry::Types](https://dry-rb.org/gems/dry-types/) to illustrate the flexibility. There's no dependency on dry-rb, choose what you like.
39
43
 
40
44
  ```ruby
41
45
  class CreateUser
42
46
  include Teckel::Operation
43
-
47
+
44
48
  # DSL style declaration
45
- input Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer)
46
-
49
+ input Struct.new(:name, :age, keyword_init: true)
50
+
47
51
  # Constant style declaration
48
- Output = Types.Instance(User)
52
+ Output = ::User
49
53
 
50
54
  # Well, also Constant style, but using classic `class` notation
51
- class Error < Dry::Struct
52
- attribute :message, Types::String
53
- attribute :status_code, Types::Integer
54
- attribute :meta, Types::Hash.optional
55
+ class Error
56
+ def initialize(message:, status_code:, meta:)
57
+ @message, @status_code, @meta = message, status_code, meta
58
+ end
59
+ attr_reader :message, :status_code, :meta
55
60
  end
61
+ error_constructor :new
56
62
 
57
63
  def call(input)
58
- user = User.create(input)
59
- if user.safe
64
+ user = User.new(name: input.name, age: input.age)
65
+ if user.save
60
66
  success!(user)
61
67
  else
62
68
  fail!(
@@ -68,7 +74,9 @@ class CreateUser
68
74
  end
69
75
  end
70
76
 
71
- result = CreateUser.call(name: "Bob", age: 23)
77
+ CreateUser.call(name: "Bob", age: 23) #=> #<User @age=23, @name="Bob">
78
+
79
+ CreateUser.call(name: "Bob", age: 5) #=> #<CreateUser::Error @message="Could not create User", @meta={:validation=>[{:age=>"underage"}]}, @status_code=400>
72
80
  ```
73
81
 
74
82
  ## Development
@@ -3,11 +3,20 @@
3
3
  require "teckel/version"
4
4
 
5
5
  module Teckel
6
+ # Base error class for this lib
6
7
  class Error < StandardError; end
8
+
9
+ # configuring the same value twice will raise this
10
+ class FrozenConfigError < Teckel::Error; end
11
+
12
+ # missing important configurations (like contracts) will raise this
13
+ class MissingConfigError < Teckel::Error; end
14
+
15
+ DEFAULT_CONSTRUCTOR = :[]
7
16
  end
8
17
 
9
18
  require_relative "teckel/config"
10
- require_relative "teckel/operation"
19
+ require_relative "teckel/contracts"
11
20
  require_relative "teckel/result"
12
- require_relative "teckel/operation/results"
21
+ require_relative "teckel/operation"
13
22
  require_relative "teckel/chain"
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'forwardable'
3
+ require_relative 'chain/config'
4
+ require_relative 'chain/step'
5
+ require_relative 'chain/result'
6
+ require_relative 'chain/runner'
4
7
 
5
8
  module Teckel
6
9
  # Railway style execution of multiple Operations.
@@ -8,179 +11,71 @@ module Teckel
8
11
  # - Runs multiple Operations (steps) in order.
9
12
  # - The output of an earlier step is passed as input to the next step.
10
13
  # - Any failure will stop the execution chain (none of the later steps is called).
11
- # - All Operations (steps) must behave like +Teckel::Operation::Results+ and
12
- # return a result object like +Teckel::Result+
13
- # - A failure response is wrapped into a +Teckel::Chain::StepFailure+ giving
14
- # additional information about which step failed
14
+ # - All Operations (steps) must return a {Teckel::Result}
15
+ # - The result is wrapped into a {Teckel::Chain::Result}
15
16
  #
16
- # @see Teckel::Operation::Results
17
- # @see Teckel::Result
18
- # @see Teckel::Chain::StepFailure
19
- #
20
- # @example Defining a simple Chain with three steps
21
- # class CreateUser
22
- # include ::Teckel::Operation::Results
23
- #
24
- # input Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer.optional)
25
- # output Types.Instance(User)
26
- # error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
27
- #
28
- # def call(input)
29
- # user = User.new(name: input[:name], age: input[:age])
30
- # if user.safe
31
- # success!(user)
32
- # else
33
- # fail!(message: "Could not safe User", errors: user.errors)
34
- # end
35
- # end
36
- # end
37
- #
38
- # class LogUser
39
- # include ::Teckel::Operation::Results
40
- #
41
- # input Types.Instance(User)
42
- # output input
43
- #
44
- # def call(usr)
45
- # Logger.new(File::NULL).info("User #{usr.name} created")
46
- # usr # we need to return the correct output type
47
- # end
48
- # end
49
- #
50
- # class AddFriend
51
- # class << self
52
- # # Don't actually do this! It's not safe and for generating the failure sample only.
53
- # attr_accessor :fail_befriend
54
- # end
55
- #
56
- # include ::Teckel::Operation::Results
57
- #
58
- # input Types.Instance(User)
59
- # output Types::Hash.schema(user: Types.Instance(User), friend: Types.Instance(User))
60
- # error Types::Hash.schema(message: Types::String)
61
- #
62
- # def call(user)
63
- # if self.class.fail_befriend
64
- # fail!(message: "Did not find a friend.")
65
- # else
66
- # { user: user, friend: User.new(name: "A friend", age: 42) }
67
- # end
68
- # end
69
- # end
70
- #
71
- # class MyChain
72
- # include Teckel::Chain
73
- #
74
- # step :create, CreateUser
75
- # step :log, LogUser
76
- # step :befriend, AddFriend
77
- # end
78
- #
79
- # result = MyChain.call(name: "Bob", age: 23)
80
- # result.is_a?(Teckel::Result) #=> true
81
- # result.success[:user].is_a?(User) #=> true
82
- # result.success[:friend].is_a?(User) #=> true
83
- #
84
- # AddFriend.fail_befriend = true
85
- # failure_result = MyChain.call(name: "Bob", age: 23)
86
- # failure_result.is_a?(Teckel::Chain::StepFailure) #=> true
87
- #
88
- # # additional step information
89
- # failure_result.step_name #=> :befriend
90
- # failure_result.step #=> AddFriend
91
- #
92
- # # otherwise behaves just like a normal +Result+
93
- # failure_result.failure? #=> true
94
- # failure_result.failure #=> {message: "Did not find a friend."}
17
+ # @see Teckel::Operation#result!
95
18
  module Chain
96
- # Like +Teckel::Result+ but for failing Chains
97
- #
98
- # When a Chain fails, it stores the failed +Operation+ and it's name.
99
- class StepFailure
100
- extend Forwardable
101
-
102
- def initialize(step, step_name, result)
103
- @step, @step_name, @result = step, step_name, result
104
- end
105
-
106
- # @!attribute step [R]
107
- # @return [Teckel::Operation] the failed Operation
108
- attr_reader :step
109
-
110
- # @!attribute step_name [R]
111
- # @return [String] the step name of the failed Operation
112
- attr_reader :step_name
113
-
114
- # @!attribute result [R]
115
- # @return [Teckel::Result] the failure Result
116
- attr_reader :result
117
-
118
- # @!method value
119
- # Delegates to +result.value+
120
- # @see Teckel::Result#value
121
- # @!method successful?
122
- # Delegates to +result.successful?+
123
- # @see Teckel::Result#successful?
124
- # @!method success
125
- # Delegates to +result.success+
126
- # @see Teckel::Result#success
127
- # @!method failure?
128
- # Delegates to +result.failure?+
129
- # @see Teckel::Result#failure?
130
- # @!method failure
131
- # Delegates to +result.failure+
132
- # @see Teckel::Result#failure
133
- def_delegators :@result, :value, :successful?, :success, :failure?, :failure
134
- end
135
-
136
19
  module ClassMethods
20
+ # The expected input for this chain
21
+ # @return [Class] The {Teckel::Operation.input} of the first step
137
22
  def input
138
- @steps.first&.last&.input
23
+ steps.first&.operation&.input
139
24
  end
140
25
 
26
+ # The expected output for this chain
27
+ # @return [Class] The {Teckel::Operation.output} of the last step
141
28
  def output
142
- @steps.last&.last&.output
29
+ steps.last&.operation&.output
143
30
  end
144
31
 
32
+ # List of all possible errors
33
+ # @return [<Class>] List of all steps {Teckel::Operation.error}s
145
34
  def errors
146
- @steps.each_with_object([]) do |e, m|
147
- err = e.last&.error
35
+ steps.each_with_object([]) do |step, m|
36
+ err = step.operation.error
148
37
  m << err if err
149
38
  end
150
39
  end
151
40
 
152
- def call(input)
153
- new.call!(@steps, input)
154
- end
41
+ # The primary interface to call the chain with the given input.
42
+ #
43
+ # @param input Any form of input the first steps +input+ class can handle
44
+ #
45
+ # @return [Teckel::Chain::Result] The result object wrapping
46
+ # the result value, the success state and last executed step.
47
+ def call(input = nil)
48
+ default_settings = self.default_settings
49
+
50
+ runner =
51
+ if default_settings
52
+ self.runner.new(self, default_settings)
53
+ else
54
+ self.runner.new(self)
55
+ end
155
56
 
156
- def step(name, operation)
157
- @steps << [name, operation]
57
+ if around
58
+ around.call(runner, input)
59
+ else
60
+ runner.call(input)
61
+ end
158
62
  end
159
- end
160
63
 
161
- module InstanceMethods
162
- def call!(steps, input)
163
- result = input
164
- failed = nil
165
- steps.each do |(name, step)|
166
- result = step.call(result)
167
- if result.failure?
168
- failed = StepFailure.new(step, name, result)
169
- break
170
- end
64
+ # @param settings [Hash{String,Symbol => Object}] Set settings for a step by it's name
65
+ def with(settings)
66
+ runner = self.runner.new(self, settings)
67
+ if around
68
+ ->(input) { around.call(runner, input) }
69
+ else
70
+ runner
171
71
  end
172
-
173
- failed || result
174
72
  end
73
+ alias :set :with
175
74
  end
176
75
 
177
76
  def self.included(receiver)
178
- receiver.extend ClassMethods
179
- receiver.send :include, InstanceMethods
180
-
181
- receiver.class_eval do
182
- @steps = []
183
- end
77
+ receiver.extend Config
78
+ receiver.extend ClassMethods
184
79
  end
185
80
  end
186
81
  end