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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 191a70cd5a82b31910a2f10b64e5058ae376f729
4
- data.tar.gz: d7ad841f948925604c5b0c176c86c66c9d4a05cf
3
+ metadata.gz: c021e3e6b56f1b856d4fe83400829b642a685fbc
4
+ data.tar.gz: c2462b4a2bcf77dc345c9fc847715ac626ca813d
5
5
  SHA512:
6
- metadata.gz: c2c407713885bd8d348fbdbd6ea3c79547d82047dd20d989da5a83f3b7f1aba0134dacb1bffd57ba73385de1c13ab75735652659c99ca203118dafd888da110a
7
- data.tar.gz: dc06637017c57a11ed654b1361490a98269aeeb1cb616751cc048b42ec67ea14195665b3582360889986605b69af8859224d4d073ce8b122de262c359fe71105
6
+ metadata.gz: 1f507660968d9a83111394311e18c1d2711019a6fb47801361cfc2cc7df5707336824cc77d53f355d3d7328a0b72662ae0b72949de37e453758ff71e1de5ff53
7
+ data.tar.gz: b9561cfc4d1cdc0091567aa367ebf03d783d3176b2e1a6cb18043e1cd43cb7fbaa39698b1cebc6b6f4777151d930a562f83b485bb4802f2d5d6d548a9922ab9a
@@ -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
@@ -4,6 +4,7 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  group :test do
7
+ gem 'transproc'
7
8
  gem 'codeclimate-test-reporter', require: false, platforms: :rbx
8
9
  end
9
10
 
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
@@ -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 private
28
+ # @api public
25
29
  def steps(*names)
26
30
  names.reverse_each { |name| step(name) }
27
31
  end
28
32
 
29
- # @api private
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
@@ -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
- broadcast(:"#{name}_success", result)
76
+ broadcast_success(result)
56
77
  result
57
- rescue => err
58
- broadcast(:"#{name}_failure", *args, err)
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
@@ -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(*args, &block)
25
- self.class.new(*args, container, steps, &block).call
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.new(name, operation)
39
+ Publisher[name, operation, monadic: monadic]
34
40
  else
35
41
  operation
36
42
  end
@@ -1,21 +1,7 @@
1
- require 'transproc'
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
- # Internal function factory using Transproc extension
16
+ # Step wrapper object which adds `>>` operator
31
17
  #
32
18
  # @api private
33
- module Registry
34
- extend Transproc::Registry
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.each { |step, listener| steps[step].subscribe(listener) }
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(:fn)).reduce(:>>)
110
+ handler = handler_steps(options).map(&method(:step)).reduce(:>>)
112
111
  handler.call(input)
113
- rescue Transproc::MalformedInputError => err
114
- raise TransactionFailedError.new(self, err.original_error)
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 fn(obj)
163
- if obj.respond_to?(:>>)
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
@@ -1,3 +1,3 @@
1
1
  module Transflow
2
- VERSION = '0.2.0'.freeze
2
+ VERSION = '0.3.0'.freeze
3
3
  end
@@ -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 'transproc', '~> 0.3', '>= 0.3.2'
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.2.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: transproc
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.3.2
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.3.2
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