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 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