transflow 0.2.0 → 0.3.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 +16 -0
- data/Gemfile +1 -0
- data/README.md +40 -9
- data/lib/transflow/errors.rb +30 -0
- data/lib/transflow/flow_dsl.rb +17 -3
- data/lib/transflow/publisher.rb +38 -4
- data/lib/transflow/step_dsl.rb +9 -3
- data/lib/transflow/transaction.rb +24 -29
- data/lib/transflow/version.rb +1 -1
- data/transflow.gemspec +2 -1
- metadata +19 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c021e3e6b56f1b856d4fe83400829b642a685fbc
|
4
|
+
data.tar.gz: c2462b4a2bcf77dc345c9fc847715ac626ca813d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f507660968d9a83111394311e18c1d2711019a6fb47801361cfc2cc7df5707336824cc77d53f355d3d7328a0b72662ae0b72949de37e453758ff71e1de5ff53
|
7
|
+
data.tar.gz: b9561cfc4d1cdc0091567aa367ebf03d783d3176b2e1a6cb18043e1cd43cb7fbaa39698b1cebc6b6f4777151d930a562f83b485bb4802f2d5d6d548a9922ab9a
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,19 @@
|
|
1
|
+
# 0.3.0 2015-08-19
|
2
|
+
|
3
|
+
## Added
|
4
|
+
|
5
|
+
- Support for steps that return [kleisli](https://github.com/txus/kleisli) monads (solnic)
|
6
|
+
- Support for setting default step options via flow DSL (solnic)
|
7
|
+
- Support for subscribing many listeners to a single step (solnic)
|
8
|
+
- Support for subscribing one listener to all steps (solnic)
|
9
|
+
|
10
|
+
## Changed
|
11
|
+
|
12
|
+
- Now step objects are wrapped using `Step` decorator that uses `dry-pipeline` gem (solnic)
|
13
|
+
- Only `Transflow::StepError` errors can cause transaction failure (solnic)
|
14
|
+
|
15
|
+
[Compare v0.2.0...v0.3.0](https://github.com/solnic/transflow/compare/v0.2.0...v0.3.0)
|
16
|
+
|
1
17
|
# 0.2.0 2015-08-18
|
2
18
|
|
3
19
|
## Added
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -44,13 +44,6 @@ handled by a pub/sub interface.
|
|
44
44
|
It's a clean and simple way of encapsulating complex business logic in your application
|
45
45
|
using simple, stateless objects.
|
46
46
|
|
47
|
-
## Error Handling
|
48
|
-
|
49
|
-
This will be the tricky part - there are scenarios where we need to aggregate
|
50
|
-
errors from multiple steps without stopping the processing. It's not implemented
|
51
|
-
yet but *probably* using pub/sub for that will do the work as we can register an
|
52
|
-
error listener that can simply gather errors and return it as a result.
|
53
|
-
|
54
47
|
## Synopsis
|
55
48
|
|
56
49
|
Using Transflow is ridiculously simple as it doesn't make much assumptions about
|
@@ -63,7 +56,7 @@ to `#call(input)` and return output or raise an error if something went wrong.
|
|
63
56
|
DB = []
|
64
57
|
|
65
58
|
container = {
|
66
|
-
validate: -> input { input[:name].nil? ? raise("name nil") : input },
|
59
|
+
validate: -> input { input[:name].nil? ? raise(Transflow::StepError.new("name nil")) : input },
|
67
60
|
persist: -> input { DB << input[:name] }
|
68
61
|
}
|
69
62
|
|
@@ -126,7 +119,7 @@ DB = []
|
|
126
119
|
operations = {
|
127
120
|
preprocess_input: -> input { { name: input['name'], email: input['email'] } },
|
128
121
|
# let's say this one needs additional argument called `email`
|
129
|
-
validate_input: -> email, input { input[:email] == email ? input : raise('ops') },
|
122
|
+
validate_input: -> email, input { input[:email] == email ? input : raise(Transflow::StepError.new('ops')) },
|
130
123
|
persist_input: -> input { DB << input[:name] }
|
131
124
|
}
|
132
125
|
|
@@ -147,6 +140,44 @@ puts DB.inspect
|
|
147
140
|
# ["Jane"]
|
148
141
|
```
|
149
142
|
|
143
|
+
### Kleisli Integration
|
144
|
+
|
145
|
+
You can use monads from [kleisli](https://github.com/txus/kleisli) gem in your
|
146
|
+
steps to achieve a nice control-flow without exceptions:
|
147
|
+
|
148
|
+
``` ruby
|
149
|
+
|
150
|
+
DB = []
|
151
|
+
|
152
|
+
validate = -> input do
|
153
|
+
if input[:email]
|
154
|
+
Right(input)
|
155
|
+
else
|
156
|
+
Left("what about the email?")
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
persist = -> input do
|
161
|
+
input.fmap do |values|
|
162
|
+
DB << values
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
container = { validate: validate, persist: persist }
|
167
|
+
|
168
|
+
transflow = Transflow(container: container) do
|
169
|
+
monadic true
|
170
|
+
|
171
|
+
steps :validate, :persist
|
172
|
+
end
|
173
|
+
|
174
|
+
transflow[name: 'Jane', email: 'jane@doe.org']
|
175
|
+
# Right([{:name=>"Jane", :email=>"jane@doe.org"}])
|
176
|
+
|
177
|
+
transflow[name: 'Jane']
|
178
|
+
# Left("what about the email?")
|
179
|
+
```
|
180
|
+
|
150
181
|
## Installation
|
151
182
|
|
152
183
|
Add this line to your application's Gemfile:
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Transflow
|
2
|
+
class TransactionFailedError < StandardError
|
3
|
+
attr_reader :transaction
|
4
|
+
|
5
|
+
attr_reader :original_error
|
6
|
+
|
7
|
+
def initialize(transaction, original_error)
|
8
|
+
@transaction = transaction
|
9
|
+
@original_error = original_error
|
10
|
+
|
11
|
+
super("#{transaction} failed [#{original_error.class}: #{original_error.message}]")
|
12
|
+
|
13
|
+
set_backtrace(original_error.backtrace)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class StepError < StandardError
|
18
|
+
attr_reader :original_error
|
19
|
+
|
20
|
+
def initialize(input = nil)
|
21
|
+
if input.kind_of?(StandardError)
|
22
|
+
@original_error = input
|
23
|
+
super(@original_error.message)
|
24
|
+
set_backtrace(original_error.backtrace)
|
25
|
+
else
|
26
|
+
super(input)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/transflow/flow_dsl.rb
CHANGED
@@ -13,22 +13,36 @@ module Transflow
|
|
13
13
|
# @api private
|
14
14
|
attr_reader :step_map
|
15
15
|
|
16
|
+
# @api private
|
17
|
+
attr_reader :step_options
|
18
|
+
|
16
19
|
# @api private
|
17
20
|
def initialize(options, &block)
|
18
21
|
@options = options
|
19
22
|
@container = options.fetch(:container)
|
20
23
|
@step_map = {}
|
24
|
+
@step_options = {}
|
21
25
|
instance_exec(&block)
|
22
26
|
end
|
23
27
|
|
24
|
-
# @api
|
28
|
+
# @api public
|
25
29
|
def steps(*names)
|
26
30
|
names.reverse_each { |name| step(name) }
|
27
31
|
end
|
28
32
|
|
29
|
-
# @api
|
33
|
+
# @api public
|
30
34
|
def step(name, options = {}, &block)
|
31
|
-
StepDSL.new(name, options, container, step_map, &block).call
|
35
|
+
StepDSL.new(name, step_options.merge(options), container, step_map, &block).call
|
36
|
+
end
|
37
|
+
|
38
|
+
# @api public
|
39
|
+
def monadic(value)
|
40
|
+
step_options.update(monadic: value)
|
41
|
+
end
|
42
|
+
|
43
|
+
# @api public
|
44
|
+
def publish(value)
|
45
|
+
step_options.update(publish: value)
|
32
46
|
end
|
33
47
|
|
34
48
|
# @api private
|
data/lib/transflow/publisher.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
1
|
require 'wisper'
|
2
|
+
require 'kleisli'
|
3
|
+
|
4
|
+
require 'transflow/errors'
|
2
5
|
|
3
6
|
module Transflow
|
4
7
|
class Publisher
|
@@ -8,6 +11,24 @@ module Transflow
|
|
8
11
|
|
9
12
|
attr_reader :op
|
10
13
|
|
14
|
+
def self.[](name, op, options = {})
|
15
|
+
type =
|
16
|
+
if options[:monadic]
|
17
|
+
Monadic
|
18
|
+
else
|
19
|
+
self
|
20
|
+
end
|
21
|
+
type.new(name, op)
|
22
|
+
end
|
23
|
+
|
24
|
+
class Monadic < Publisher
|
25
|
+
def call(*args)
|
26
|
+
op.(*args)
|
27
|
+
.or { |result| broadcast_failure(*args, result) and Left(result) }
|
28
|
+
.>-> value { broadcast_success(value) and Right(value) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
11
32
|
class Curried < Publisher
|
12
33
|
attr_reader :publisher
|
13
34
|
|
@@ -52,12 +73,25 @@ module Transflow
|
|
52
73
|
|
53
74
|
def call(*args)
|
54
75
|
result = op.call(*args)
|
55
|
-
|
76
|
+
broadcast_success(result)
|
56
77
|
result
|
57
|
-
rescue => err
|
58
|
-
|
59
|
-
raise err
|
78
|
+
rescue StepError => err
|
79
|
+
broadcast_failure(*args, err) and raise(err)
|
60
80
|
end
|
61
81
|
alias_method :[], :call
|
82
|
+
|
83
|
+
def subscribe(listeners, *args)
|
84
|
+
Array(listeners).each { |listener| super(listener, *args) }
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def broadcast_success(result)
|
90
|
+
broadcast(:"#{name}_success", result)
|
91
|
+
end
|
92
|
+
|
93
|
+
def broadcast_failure(*args, err)
|
94
|
+
broadcast(:"#{name}_failure", *args, err)
|
95
|
+
end
|
62
96
|
end
|
63
97
|
end
|
data/lib/transflow/step_dsl.rb
CHANGED
@@ -4,6 +4,8 @@ module Transflow
|
|
4
4
|
class StepDSL
|
5
5
|
attr_reader :name
|
6
6
|
|
7
|
+
attr_reader :options
|
8
|
+
|
7
9
|
attr_reader :handler
|
8
10
|
|
9
11
|
attr_reader :container
|
@@ -12,17 +14,21 @@ module Transflow
|
|
12
14
|
|
13
15
|
attr_reader :publish
|
14
16
|
|
17
|
+
attr_reader :monadic
|
18
|
+
|
15
19
|
def initialize(name, options, container, steps, &block)
|
16
20
|
@name = name
|
21
|
+
@options = options
|
17
22
|
@handler = options.fetch(:with, name)
|
18
23
|
@publish = options.fetch(:publish, false)
|
24
|
+
@monadic = options.fetch(:monadic, false)
|
19
25
|
@container = container
|
20
26
|
@steps = steps
|
21
27
|
instance_exec(&block) if block
|
22
28
|
end
|
23
29
|
|
24
|
-
def step(
|
25
|
-
self.class.new(
|
30
|
+
def step(name, new_options = {}, &block)
|
31
|
+
self.class.new(name, options.merge(new_options), container, steps, &block).call
|
26
32
|
end
|
27
33
|
|
28
34
|
def call
|
@@ -30,7 +36,7 @@ module Transflow
|
|
30
36
|
|
31
37
|
step =
|
32
38
|
if publish
|
33
|
-
Publisher
|
39
|
+
Publisher[name, operation, monadic: monadic]
|
34
40
|
else
|
35
41
|
operation
|
36
42
|
end
|
@@ -1,21 +1,7 @@
|
|
1
|
-
require '
|
1
|
+
require 'dry-pipeline'
|
2
|
+
require 'transflow/errors'
|
2
3
|
|
3
4
|
module Transflow
|
4
|
-
class TransactionFailedError < StandardError
|
5
|
-
attr_reader :transaction
|
6
|
-
|
7
|
-
attr_reader :original_error
|
8
|
-
|
9
|
-
def initialize(transaction, original_error)
|
10
|
-
@transaction = transaction
|
11
|
-
@original_error = original_error
|
12
|
-
|
13
|
-
super("#{transaction} failed [#{original_error.class}: #{original_error.message}]")
|
14
|
-
|
15
|
-
set_backtrace(original_error.backtrace)
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
5
|
# Transaction encapsulates calling individual steps registered within a transflow
|
20
6
|
# constructor.
|
21
7
|
#
|
@@ -27,11 +13,20 @@ module Transflow
|
|
27
13
|
#
|
28
14
|
# @api public
|
29
15
|
class Transaction
|
30
|
-
#
|
16
|
+
# Step wrapper object which adds `>>` operator
|
31
17
|
#
|
32
18
|
# @api private
|
33
|
-
|
34
|
-
|
19
|
+
class Step
|
20
|
+
include Dry::Pipeline::Mixin
|
21
|
+
|
22
|
+
# @api private
|
23
|
+
def self.[](op)
|
24
|
+
if op.respond_to?(:>>)
|
25
|
+
op
|
26
|
+
else
|
27
|
+
Step.new(op)
|
28
|
+
end
|
29
|
+
end
|
35
30
|
end
|
36
31
|
|
37
32
|
# @attr_reader [Hash<Symbol => Proc,#call>] steps The step map
|
@@ -77,7 +72,11 @@ module Transflow
|
|
77
72
|
#
|
78
73
|
# @api public
|
79
74
|
def subscribe(listeners)
|
80
|
-
listeners.
|
75
|
+
if listeners.is_a?(Hash)
|
76
|
+
listeners.each { |step, listener| steps[step].subscribe(listener) }
|
77
|
+
else
|
78
|
+
steps.each { |(_, step)| step.subscribe(listeners) }
|
79
|
+
end
|
81
80
|
self
|
82
81
|
end
|
83
82
|
|
@@ -108,10 +107,10 @@ module Transflow
|
|
108
107
|
#
|
109
108
|
# @api public
|
110
109
|
def call(input, options = {})
|
111
|
-
handler = handler_steps(options).map(&method(:
|
110
|
+
handler = handler_steps(options).map(&method(:step)).reduce(:>>)
|
112
111
|
handler.call(input)
|
113
|
-
rescue
|
114
|
-
raise TransactionFailedError.new(self, err
|
112
|
+
rescue StepError => err
|
113
|
+
raise TransactionFailedError.new(self, err)
|
115
114
|
end
|
116
115
|
alias_method :[], :call
|
117
116
|
|
@@ -159,12 +158,8 @@ module Transflow
|
|
159
158
|
# @param [#call]
|
160
159
|
#
|
161
160
|
# @api private
|
162
|
-
def
|
163
|
-
|
164
|
-
obj
|
165
|
-
else
|
166
|
-
Registry[obj]
|
167
|
-
end
|
161
|
+
def step(obj)
|
162
|
+
Step[obj]
|
168
163
|
end
|
169
164
|
end
|
170
165
|
end
|
data/lib/transflow/version.rb
CHANGED
data/transflow.gemspec
CHANGED
@@ -18,8 +18,9 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.add_runtime_dependency '
|
21
|
+
spec.add_runtime_dependency 'dry-pipeline'
|
22
22
|
spec.add_runtime_dependency 'wisper'
|
23
|
+
spec.add_runtime_dependency 'kleisli'
|
23
24
|
|
24
25
|
spec.add_development_dependency "bundler", "~> 1.10"
|
25
26
|
spec.add_development_dependency "rake", "~> 10.0"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: transflow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Piotr Solnica
|
@@ -11,25 +11,19 @@ cert_chain: []
|
|
11
11
|
date: 2015-08-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: dry-pipeline
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "~>"
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0.3'
|
20
17
|
- - ">="
|
21
18
|
- !ruby/object:Gem::Version
|
22
|
-
version: 0
|
19
|
+
version: '0'
|
23
20
|
type: :runtime
|
24
21
|
prerelease: false
|
25
22
|
version_requirements: !ruby/object:Gem::Requirement
|
26
23
|
requirements:
|
27
|
-
- - "~>"
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
version: '0.3'
|
30
24
|
- - ">="
|
31
25
|
- !ruby/object:Gem::Version
|
32
|
-
version: 0
|
26
|
+
version: '0'
|
33
27
|
- !ruby/object:Gem::Dependency
|
34
28
|
name: wisper
|
35
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -44,6 +38,20 @@ dependencies:
|
|
44
38
|
- - ">="
|
45
39
|
- !ruby/object:Gem::Version
|
46
40
|
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: kleisli
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
47
55
|
- !ruby/object:Gem::Dependency
|
48
56
|
name: bundler
|
49
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -104,6 +112,7 @@ files:
|
|
104
112
|
- bin/console
|
105
113
|
- bin/setup
|
106
114
|
- lib/transflow.rb
|
115
|
+
- lib/transflow/errors.rb
|
107
116
|
- lib/transflow/flow_dsl.rb
|
108
117
|
- lib/transflow/publisher.rb
|
109
118
|
- lib/transflow/step_dsl.rb
|