transflow 0.2.0 → 0.3.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: 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