teckel 0.1.0 → 0.6.0

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