transflow 0.0.2 → 0.1.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 +30 -1
- data/README.md +61 -19
- data/lib/transflow.rb +29 -2
- data/lib/transflow/flow_dsl.rb +16 -22
- data/lib/transflow/step_dsl.rb +1 -1
- data/lib/transflow/transaction.rb +136 -8
- data/lib/transflow/version.rb +1 -1
- data/transflow.gemspec +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e56210712ab5748639ce2feb57913c5754102588
|
4
|
+
data.tar.gz: 7823b8b50bf6f6d258ca2ee787585341b9a72ca9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1dba14fc104d96601b42e7a73abbfcba85a2b3e7f67eb33b2863f129a6ade75bfc025e5750b2749e342f5078ea5a950aba5ee76182ca7696f7a4f4028701a1c5
|
7
|
+
data.tar.gz: 0da536341a6345f6205a5db977d57be6c1a0e8f1ae94e4d51c2494487751a67fa85ad3421f0832c32ad7fe1653a184e05392cb8f10cbcb43948260ca8fd63dd8
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,32 @@
|
|
1
|
+
# 0.1.0 2015-08-17
|
2
|
+
|
3
|
+
## Added
|
4
|
+
|
5
|
+
- `Transaction#call` will raise if options include an unknown step name (solnic)
|
6
|
+
- `Transflow` support shorter syntax for steps: `steps :one, :two, :three` (solnic)
|
7
|
+
- `step(name)` defaults to `step(name, with: name)` (solnic)
|
8
|
+
|
9
|
+
## Fixed
|
10
|
+
|
11
|
+
- `Transaction#to_s` displays steps in the order of execution (solnic)
|
12
|
+
|
13
|
+
## Internal
|
14
|
+
|
15
|
+
- Organize source code into separate files (solnic)
|
16
|
+
- Document public interface with YARD (solnic)
|
17
|
+
- Add unit specs for `Transaction` (solnic)
|
18
|
+
|
19
|
+
[Compare v0.0.2...v0.1.0](https://github.com/solnic/transflow/compare/v0.0.2...v0.1.0)
|
20
|
+
|
21
|
+
# 0.0.2 2015-08-16
|
22
|
+
|
23
|
+
## Added
|
24
|
+
|
25
|
+
- Ability to pass aditional arguments to specific operations prior calling the
|
26
|
+
whole transaction (solnic)
|
27
|
+
|
28
|
+
[Compare v0.0.2...v0.0.2](https://github.com/solnic/transflow/compare/v0.0.1...v0.0.2)
|
29
|
+
|
1
30
|
# 0.0.2 2015-08-16
|
2
31
|
|
3
32
|
## Added
|
@@ -5,7 +34,7 @@
|
|
5
34
|
- Ability to publish events from operations via `publish: true` option (solnic)
|
6
35
|
- Ability to subscribe to events via `Transflow::Transaction#subscribe` interface (solnic)
|
7
36
|
|
8
|
-
[Compare v0.0.1...v0.0.2](https://github.com/
|
37
|
+
[Compare v0.0.1...v0.0.2](https://github.com/solnic/transflow/compare/v0.0.1...v0.0.2)
|
9
38
|
|
10
39
|
# 0.0.1 2015-08-16
|
11
40
|
|
data/README.md
CHANGED
@@ -30,8 +30,8 @@ It is based on the following ideas:
|
|
30
30
|
an output without causing any side-effects
|
31
31
|
- the only interface of a an operation is `#call(input)`
|
32
32
|
- each operation provides a meaningful functionality and can be reused
|
33
|
-
- each operation can broadcast its result
|
34
|
-
- external message consumers can listen to a transaction object for specific events
|
33
|
+
- each operation can broadcast its result
|
34
|
+
- external message consumers can listen to a transaction object for specific events
|
35
35
|
|
36
36
|
## Why?
|
37
37
|
|
@@ -53,33 +53,46 @@ error listener that can simply gather errors and return it as a result.
|
|
53
53
|
|
54
54
|
## Synopsis
|
55
55
|
|
56
|
+
Using Transflow is ridiculously simple as it doesn't make much assumptions about
|
57
|
+
your code. You provide container with operations and they simply need to respond
|
58
|
+
to `#call(input)` and return output or raise an error if something went wrong.
|
59
|
+
|
60
|
+
### Defining a simple flow
|
61
|
+
|
56
62
|
``` ruby
|
57
63
|
DB = []
|
58
64
|
|
59
65
|
container = {
|
60
|
-
validate: -> input { raise
|
66
|
+
validate: -> input { input[:name].nil? ? raise("name nil") : input },
|
61
67
|
persist: -> input { DB << input[:name] }
|
62
68
|
}
|
63
69
|
|
64
|
-
my_business_flow = Transflow(container: container)
|
65
|
-
step(:validate) { step(:persist) }
|
66
|
-
end
|
70
|
+
my_business_flow = Transflow(container: container) { steps :validate, :persist }
|
67
71
|
|
68
72
|
my_business_flow[{ name: 'Jane' }]
|
69
73
|
|
70
74
|
puts DB.inspect
|
71
75
|
# ["Jane"]
|
76
|
+
```
|
77
|
+
|
78
|
+
## Defining a flow with event publishers
|
79
|
+
|
80
|
+
In many cases an individual operation may require additional behavior to be
|
81
|
+
triggered. This can be easily achieved with a pub/sub mechanism. Transflow
|
82
|
+
provides that mechanism through the wonderful `wisper` gem which is used under
|
83
|
+
the hood.
|
72
84
|
|
73
|
-
|
85
|
+
``` ruby
|
86
|
+
DB = []
|
74
87
|
|
75
|
-
NOTIFICATIONS = []
|
88
|
+
NOTIFICATIONS = [] # just for the sake of the example
|
76
89
|
|
77
|
-
class
|
78
|
-
def persist_success(user)
|
90
|
+
class UserPersistListener
|
91
|
+
def self.persist_success(user)
|
79
92
|
NOTIFICATIONS << "#{user} persisted"
|
80
93
|
end
|
81
94
|
|
82
|
-
def persist_failure(user, err)
|
95
|
+
def self.persist_failure(user, err)
|
83
96
|
# do sth about that
|
84
97
|
end
|
85
98
|
end
|
@@ -88,9 +101,7 @@ my_business_flow = Transflow(container: container) do
|
|
88
101
|
step(:validate) { step(:persist, publish: true) }
|
89
102
|
end
|
90
103
|
|
91
|
-
|
92
|
-
|
93
|
-
my_business_flow.subscribe(persist: notify)
|
104
|
+
my_business_flow.subscribe(persist: UserPersistListener)
|
94
105
|
|
95
106
|
my_business_flow[{ name: 'Jane' }]
|
96
107
|
|
@@ -101,6 +112,41 @@ puts NOTIFICATIONS.inspect
|
|
101
112
|
# ["Jane persisted"]
|
102
113
|
```
|
103
114
|
|
115
|
+
### Passing additional arguments
|
116
|
+
|
117
|
+
Another common requirement is to pass aditional arguments that we don't have in
|
118
|
+
the moment of defining our flow. Fortunately Transflow allows you to pass any
|
119
|
+
arguments in the moment you call the transaction. Those arguments will be curried
|
120
|
+
which means you must use either procs as your operation or an object that responds
|
121
|
+
to `curry`. This limitation will be removed soon.
|
122
|
+
|
123
|
+
``` ruby
|
124
|
+
DB = []
|
125
|
+
|
126
|
+
operations = {
|
127
|
+
preprocess_input: -> input { { name: input['name'], email: input['email'] } },
|
128
|
+
# let's say this one needs additional argument called `email`
|
129
|
+
validate_input: -> email, input { input[:email] == email ? input : raise('ops') },
|
130
|
+
persist_input: -> input { DB << input[:name] }
|
131
|
+
}
|
132
|
+
|
133
|
+
transflow = Transflow(container: operations) do
|
134
|
+
step :preprocess, with: :preprocess_input do
|
135
|
+
step :validate, with: :validate_input do
|
136
|
+
step :persist, with: :persist_input
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
input = { 'name' => 'Jane', 'email' => 'jane@doe.org' }
|
142
|
+
|
143
|
+
# here we say "for `validate` operation curry this additional argument
|
144
|
+
transflow[input, validate: 'jane@doe.org']
|
145
|
+
|
146
|
+
puts DB.inspect
|
147
|
+
# ["Jane"]
|
148
|
+
```
|
149
|
+
|
104
150
|
## Installation
|
105
151
|
|
106
152
|
Add this line to your application's Gemfile:
|
@@ -117,10 +163,6 @@ Or install it yourself as:
|
|
117
163
|
|
118
164
|
$ gem install transflow
|
119
165
|
|
120
|
-
## Usage
|
121
|
-
|
122
|
-
TODO: Write usage instructions here
|
123
|
-
|
124
166
|
## Development
|
125
167
|
|
126
168
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -129,4 +171,4 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
129
171
|
|
130
172
|
## Contributing
|
131
173
|
|
132
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
174
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/solnic/transflow.
|
data/lib/transflow.rb
CHANGED
@@ -1,10 +1,21 @@
|
|
1
1
|
require 'transflow/version'
|
2
2
|
require 'transflow/flow_dsl'
|
3
3
|
|
4
|
-
# Define a transaction flow
|
4
|
+
# Define a business transaction flow.
|
5
5
|
#
|
6
|
-
#
|
6
|
+
# A business transaction flow is a simple composition of callable objects that
|
7
|
+
# receive an input and produce an output. Steps are registered in the same order
|
8
|
+
# they are defined within the DSL and that's also the order of execution.
|
9
|
+
#
|
10
|
+
# Initial input is sent to the first step, its output is sent to the second step
|
11
|
+
# and so on.
|
7
12
|
#
|
13
|
+
# Every step can become a publisher, which means you can broadcast results from
|
14
|
+
# any step and subscribe event listeners to individual steps. This gives you
|
15
|
+
# a flexible way of responding to successful or failed execution of individual
|
16
|
+
# steps.
|
17
|
+
#
|
18
|
+
# @example
|
8
19
|
# container = { do_one: some_obj, do_two: some_obj }
|
9
20
|
#
|
10
21
|
# my_business_flow = Transflow(container: container) do
|
@@ -13,6 +24,22 @@ require 'transflow/flow_dsl'
|
|
13
24
|
#
|
14
25
|
# my_business_flow[some_input]
|
15
26
|
#
|
27
|
+
# # with events
|
28
|
+
#
|
29
|
+
# my_business_flow = Transflow(container: container) do
|
30
|
+
# step(:one, with: :do_one) { step(:two, with: :do_two, publish: true) }
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# class Listener
|
34
|
+
# def self.do_two_success(*args)
|
35
|
+
# puts ":do_two totally worked and produced: #{args.inspect}!"
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# my_business_flow.subscribe(do_two: Listener)
|
40
|
+
#
|
41
|
+
# my_business_flow[some_input]
|
42
|
+
#
|
16
43
|
# @options [Hash] options The option hash
|
17
44
|
#
|
18
45
|
# @api public
|
data/lib/transflow/flow_dsl.rb
CHANGED
@@ -1,45 +1,39 @@
|
|
1
|
-
require 'transproc'
|
2
|
-
|
3
1
|
require 'transflow/step_dsl'
|
4
2
|
require 'transflow/transaction'
|
5
3
|
|
6
4
|
module Transflow
|
5
|
+
# @api private
|
7
6
|
class FlowDSL
|
8
|
-
|
9
|
-
extend Transproc::Registry
|
10
|
-
end
|
11
|
-
|
12
|
-
def self.[](op)
|
13
|
-
if op.respond_to?(:>>)
|
14
|
-
op
|
15
|
-
else
|
16
|
-
Registry[op]
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
7
|
+
# @api private
|
20
8
|
attr_reader :options
|
21
9
|
|
10
|
+
# @api private
|
22
11
|
attr_reader :container
|
23
12
|
|
24
|
-
|
13
|
+
# @api private
|
14
|
+
attr_reader :step_map
|
25
15
|
|
16
|
+
# @api private
|
26
17
|
def initialize(options, &block)
|
27
18
|
@options = options
|
28
19
|
@container = options.fetch(:container)
|
29
|
-
@
|
20
|
+
@step_map = {}
|
30
21
|
instance_exec(&block)
|
31
22
|
end
|
32
23
|
|
33
|
-
|
34
|
-
|
24
|
+
# @api private
|
25
|
+
def steps(*names)
|
26
|
+
names.reverse_each { |name| step(name) }
|
35
27
|
end
|
36
28
|
|
37
|
-
|
38
|
-
|
29
|
+
# @api private
|
30
|
+
def step(name, options = {}, &block)
|
31
|
+
StepDSL.new(name, options, container, step_map, &block).call
|
39
32
|
end
|
40
33
|
|
41
|
-
|
42
|
-
|
34
|
+
# @api private
|
35
|
+
def call
|
36
|
+
Transaction.new(step_map)
|
43
37
|
end
|
44
38
|
end
|
45
39
|
end
|
data/lib/transflow/step_dsl.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'transproc'
|
2
|
+
|
1
3
|
module Transflow
|
2
4
|
class TransactionFailedError < StandardError
|
3
5
|
attr_reader :transaction
|
@@ -8,35 +10,161 @@ module Transflow
|
|
8
10
|
@transaction = transaction
|
9
11
|
@original_error = original_error
|
10
12
|
|
11
|
-
super("#{transaction} failed")
|
13
|
+
super("#{transaction} failed [#{original_error.class}: #{original_error.message}]")
|
12
14
|
|
13
15
|
set_backtrace(original_error.backtrace)
|
14
16
|
end
|
15
17
|
end
|
16
18
|
|
19
|
+
# Transaction encapsulates calling individual steps registered within a transflow
|
20
|
+
# constructor.
|
21
|
+
#
|
22
|
+
# It's responsible for calling steps in the right order and optionally currying
|
23
|
+
# arguments for specific steps.
|
24
|
+
#
|
25
|
+
# Furthermore you can subscribe event listeners to individual steps within a
|
26
|
+
# transaction.
|
27
|
+
#
|
28
|
+
# @api public
|
17
29
|
class Transaction
|
18
|
-
|
30
|
+
# Internal function factory using Transproc extension
|
31
|
+
#
|
32
|
+
# @api private
|
33
|
+
module Registry
|
34
|
+
extend Transproc::Registry
|
35
|
+
end
|
19
36
|
|
37
|
+
# @attr_reader [Hash<Symbol => Proc,#call>] steps The step map
|
38
|
+
#
|
39
|
+
# @api private
|
20
40
|
attr_reader :steps
|
21
41
|
|
22
|
-
|
42
|
+
# @attr_reader [Array<Symbol>] step_names The names of registered steps
|
43
|
+
#
|
44
|
+
# @api private
|
45
|
+
attr_reader :step_names
|
46
|
+
|
47
|
+
# @api private
|
48
|
+
def initialize(steps)
|
23
49
|
@steps = steps
|
24
|
-
@
|
50
|
+
@step_names = steps.keys.reverse
|
25
51
|
end
|
26
52
|
|
53
|
+
# Subscribe event listeners to specific steps
|
54
|
+
#
|
55
|
+
# @example
|
56
|
+
# transaction = Transflow(container: my_container) {
|
57
|
+
# step(:one) { step(:two, publish: true }
|
58
|
+
# }
|
59
|
+
#
|
60
|
+
# class MyListener
|
61
|
+
# def self.two_success(*args)
|
62
|
+
# puts 'yes!'
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# def self.two_failure(*args)
|
66
|
+
# puts 'oh noez!'
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# transaction.subscribe(two: my_listener)
|
71
|
+
#
|
72
|
+
# transaction.call(some_input)
|
73
|
+
#
|
74
|
+
# @param [Hash<Symbol => Object>] listeners The step=>listener map
|
75
|
+
#
|
76
|
+
# @return [self]
|
77
|
+
#
|
78
|
+
# @api public
|
27
79
|
def subscribe(listeners)
|
28
80
|
listeners.each { |step, listener| steps[step].subscribe(listener) }
|
81
|
+
self
|
29
82
|
end
|
30
83
|
|
31
|
-
|
32
|
-
|
84
|
+
# Call the transaction
|
85
|
+
#
|
86
|
+
# Once transaction is called it will call the first step and its result
|
87
|
+
# will be passed to the second step and so on.
|
88
|
+
#
|
89
|
+
# @example
|
90
|
+
# my_container = {
|
91
|
+
# add_one: -> i { i + 1 },
|
92
|
+
# add_two: -> j { j + 2 }
|
93
|
+
# }
|
94
|
+
#
|
95
|
+
# transaction = Transflow(container: my_container) {
|
96
|
+
# step(:one, with: :add_one) { step(:two, with: :add_two) }
|
97
|
+
# }
|
98
|
+
#
|
99
|
+
# transaction.call(1) # 4
|
100
|
+
#
|
101
|
+
# @param [Object] input The input for the first step
|
102
|
+
#
|
103
|
+
# @param [Hash] options The curry-args map, optional
|
104
|
+
#
|
105
|
+
# @return [Object]
|
106
|
+
#
|
107
|
+
# @raises TransactionFailedError
|
108
|
+
#
|
109
|
+
# @api public
|
110
|
+
def call(input, options = {})
|
111
|
+
handler = handler_steps(options).map(&method(:fn)).reduce(:>>)
|
112
|
+
handler.call(input)
|
33
113
|
rescue Transproc::MalformedInputError => err
|
34
|
-
raise TransactionFailedError.new(self, err)
|
114
|
+
raise TransactionFailedError.new(self, err.original_error)
|
35
115
|
end
|
36
116
|
alias_method :[], :call
|
37
117
|
|
118
|
+
# Coerce a transaction into string representation
|
119
|
+
#
|
120
|
+
# @return [String]
|
121
|
+
#
|
122
|
+
# @api public
|
38
123
|
def to_s
|
39
|
-
"Transaction(#{
|
124
|
+
"Transaction(#{step_names.join(' => ')})"
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
# @api private
|
130
|
+
def handler_steps(options)
|
131
|
+
if options.any?
|
132
|
+
assert_valid_options(options)
|
133
|
+
|
134
|
+
steps.map { |(name, op)|
|
135
|
+
args = options[name]
|
136
|
+
|
137
|
+
if args
|
138
|
+
op.curry.call(args)
|
139
|
+
else
|
140
|
+
op
|
141
|
+
end
|
142
|
+
}
|
143
|
+
else
|
144
|
+
steps.values
|
145
|
+
end.reverse
|
146
|
+
end
|
147
|
+
|
148
|
+
# @api private
|
149
|
+
def assert_valid_options(options)
|
150
|
+
options.each_key do |name|
|
151
|
+
unless step_names.include?(name)
|
152
|
+
raise ArgumentError, "+#{name}+ is not a valid step name"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Wrap a proc into composable transproc function
|
158
|
+
#
|
159
|
+
# @param [#call]
|
160
|
+
#
|
161
|
+
# @api private
|
162
|
+
def fn(obj)
|
163
|
+
if obj.respond_to?(:>>)
|
164
|
+
obj
|
165
|
+
else
|
166
|
+
Registry[obj]
|
167
|
+
end
|
40
168
|
end
|
41
169
|
end
|
42
170
|
end
|
data/lib/transflow/version.rb
CHANGED
data/transflow.gemspec
CHANGED
@@ -18,7 +18,7 @@ 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 'transproc', '~> 0.3', '>= 0.3.
|
21
|
+
spec.add_runtime_dependency 'transproc', '~> 0.3', '>= 0.3.2'
|
22
22
|
spec.add_runtime_dependency 'wisper'
|
23
23
|
|
24
24
|
spec.add_development_dependency "bundler", "~> 1.10"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: transflow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Piotr Solnica
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-08-
|
11
|
+
date: 2015-08-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: transproc
|
@@ -19,7 +19,7 @@ dependencies:
|
|
19
19
|
version: '0.3'
|
20
20
|
- - ">="
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: 0.3.
|
22
|
+
version: 0.3.2
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -29,7 +29,7 @@ dependencies:
|
|
29
29
|
version: '0.3'
|
30
30
|
- - ">="
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: 0.3.
|
32
|
+
version: 0.3.2
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: wisper
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|