teckel 0.1.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 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
|
-
[![
|
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
|
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
|