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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +114 -0
- data/LICENSE_LOGO +4 -0
- data/README.md +22 -14
- data/lib/teckel.rb +11 -2
- data/lib/teckel/chain.rb +47 -152
- data/lib/teckel/chain/config.rb +246 -0
- data/lib/teckel/chain/result.rb +38 -0
- data/lib/teckel/chain/runner.rb +62 -0
- data/lib/teckel/chain/step.rb +18 -0
- data/lib/teckel/config.rb +41 -52
- data/lib/teckel/contracts.rb +19 -0
- data/lib/teckel/operation.rb +108 -253
- data/lib/teckel/operation/config.rb +396 -0
- data/lib/teckel/operation/result.rb +92 -0
- data/lib/teckel/operation/runner.rb +75 -0
- data/lib/teckel/result.rb +52 -53
- data/lib/teckel/version.rb +1 -1
- data/spec/chain/default_settings_spec.rb +39 -0
- data/spec/chain/inheritance_spec.rb +116 -0
- data/spec/chain/none_input_spec.rb +36 -0
- data/spec/chain/results_spec.rb +53 -0
- data/spec/chain_around_hook_spec.rb +100 -0
- data/spec/chain_spec.rb +180 -0
- data/spec/config_spec.rb +26 -0
- data/spec/doctest_helper.rb +7 -0
- data/spec/operation/contract_trace_spec.rb +116 -0
- data/spec/operation/default_settings_spec.rb +94 -0
- data/spec/operation/inheritance_spec.rb +94 -0
- data/spec/operation/result_spec.rb +34 -0
- data/spec/operation/results_spec.rb +117 -0
- data/spec/operation_spec.rb +483 -0
- data/spec/rb27/pattern_matching_spec.rb +193 -0
- data/spec/result_spec.rb +22 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/support/dry_base.rb +8 -0
- data/spec/support/fake_db.rb +12 -0
- data/spec/support/fake_models.rb +20 -0
- data/spec/teckel_spec.rb +7 -0
- metadata +64 -46
- data/.github/workflows/ci.yml +0 -67
- data/.github/workflows/pages.yml +0 -50
- data/.gitignore +0 -13
- data/.rspec +0 -3
- data/.rubocop.yml +0 -12
- data/.ruby-version +0 -1
- data/DEVELOPMENT.md +0 -28
- data/Gemfile +0 -8
- data/Gemfile.lock +0 -71
- data/Rakefile +0 -30
- data/bin/console +0 -15
- data/bin/rake +0 -29
- data/bin/rspec +0 -29
- data/bin/rubocop +0 -18
- data/bin/setup +0 -8
- data/lib/teckel/operation/results.rb +0 -71
- data/teckel.gemspec +0 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d55dd7fb782cfdcb3cc960423e46ab2ce8b49c9396273f10749105883d55ed6f
|
4
|
+
data.tar.gz: e3b7428d98daeb42cc086b4a5cd5081b5c3b4ca90c14b0cf3f4cd9576243d0e3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '0148d426e8e7124a08a3a7dd3c42487cac5944d17ba9cabb98a8c378a86a5d9813b7b62c483d2203809644528d2e6b6cc4de51476aa67261b1405f3f41576613'
|
7
|
+
data.tar.gz: 6792ad4806b977d21d0ea621e311306cb13c239d8604e35e61937e79ce4f5488cbb410aa6ae22e9e12e48b61529f97e924aab22e100717808885c0b0c76980fd
|
data/CHANGELOG.md
CHANGED
@@ -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
|
data/LICENSE_LOGO
ADDED
data/README.md
CHANGED
@@ -5,7 +5,8 @@ Ruby service classes with enforced<sup name="footnote-1-source">[1](#footnote-1)
|
|
5
5
|
[][gem]
|
6
6
|
[][ci]
|
7
7
|
[](https://codeclimate.com/github/fnordfish/teckel/maintainability)
|
8
|
-
[](https://codeclimate.com/github/fnordfish/teckel/test_coverage)
|
9
|
+
[][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
|
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
|
46
|
-
|
49
|
+
input Struct.new(:name, :age, keyword_init: true)
|
50
|
+
|
47
51
|
# Constant style declaration
|
48
|
-
Output =
|
52
|
+
Output = ::User
|
49
53
|
|
50
54
|
# Well, also Constant style, but using classic `class` notation
|
51
|
-
class Error
|
52
|
-
|
53
|
-
|
54
|
-
|
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.
|
59
|
-
if user.
|
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
|
-
|
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
|
data/lib/teckel.rb
CHANGED
@@ -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/
|
19
|
+
require_relative "teckel/contracts"
|
11
20
|
require_relative "teckel/result"
|
12
|
-
require_relative "teckel/operation
|
21
|
+
require_relative "teckel/operation"
|
13
22
|
require_relative "teckel/chain"
|
data/lib/teckel/chain.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
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
|
12
|
-
#
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
147
|
-
err =
|
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
|
-
|
153
|
-
|
154
|
-
|
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
|
-
|
157
|
-
|
57
|
+
if around
|
58
|
+
around.call(runner, input)
|
59
|
+
else
|
60
|
+
runner.call(input)
|
61
|
+
end
|
158
62
|
end
|
159
|
-
end
|
160
63
|
|
161
|
-
|
162
|
-
def
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
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
|
179
|
-
receiver.
|
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
|