transflow 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3dde7fc2cfdb940553b3a4ddabe1004d01ca49a6
4
- data.tar.gz: 2c7b0e6c01338cd5087ac5dc332e9c5936d3a5b5
3
+ metadata.gz: e56210712ab5748639ce2feb57913c5754102588
4
+ data.tar.gz: 7823b8b50bf6f6d258ca2ee787585341b9a72ca9
5
5
  SHA512:
6
- metadata.gz: c4601d2a9a7a8dfd1d72c03199f696483fa1de7bf66f0e398cecd17115bbf6a54333c32c11740ec2998d7626bbb8dadc3aab9088b4ed580f0a71cd2bd0af803d
7
- data.tar.gz: f0ccebb883cc983ace244989008a23c649e2b234ced17692923747c95d6be1b185a4528d5e64202fdf71e829b94f5162ffb3e08611eb05286556a4865b1e5b47
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/rom-rb/rom/compare/v0.0.1...v0.0.2)
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 (TODO)
34
- - external message consumers can listen to a transaction object for specific events (TODO)
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 "name nil" if input[:name].nil? },
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) do
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
- ## The same but with events
85
+ ``` ruby
86
+ DB = []
74
87
 
75
- NOTIFICATIONS = []
88
+ NOTIFICATIONS = [] # just for the sake of the example
76
89
 
77
- class Notify
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
- notify = Notify.new
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/[USERNAME]/transflow.
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
- # @example
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
@@ -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
- module Registry
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
- attr_reader :steps
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
- @steps = {}
20
+ @step_map = {}
30
21
  instance_exec(&block)
31
22
  end
32
23
 
33
- def step(*args, &block)
34
- StepDSL.new(*args, container, steps, &block).call
24
+ # @api private
25
+ def steps(*names)
26
+ names.reverse_each { |name| step(name) }
35
27
  end
36
28
 
37
- def call
38
- Transaction.new(steps, operations.reduce(:>>))
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
- def operations
42
- steps.values.reverse.map { |op| self.class[op] }
34
+ # @api private
35
+ def call
36
+ Transaction.new(step_map)
43
37
  end
44
38
  end
45
39
  end
@@ -14,7 +14,7 @@ module Transflow
14
14
 
15
15
  def initialize(name, options, container, steps, &block)
16
16
  @name = name
17
- @handler = options.fetch(:with)
17
+ @handler = options.fetch(:with, name)
18
18
  @publish = options.fetch(:publish, false)
19
19
  @container = container
20
20
  @steps = steps
@@ -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
- attr_reader :handler
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
- def initialize(steps, handler)
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
- @handler = handler
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
- def call(*args)
32
- handler.call(*args)
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(#{steps.keys.join(' => ')})"
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
@@ -1,3 +1,3 @@
1
1
  module Transflow
2
- VERSION = '0.0.2'.freeze
2
+ VERSION = '0.1.0'.freeze
3
3
  end
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.1'
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.2
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-16 00:00:00.000000000 Z
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.1
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.1
32
+ version: 0.3.2
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: wisper
35
35
  requirement: !ruby/object:Gem::Requirement