codequest_pipes 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5de4743e4f4364742d5f485f244167f604394777
4
+ data.tar.gz: 76c3e6315db9137dac15c76a0263b177855295c3
5
+ SHA512:
6
+ metadata.gz: f5059e69133b70d4c618334e1fef64eb603e54b90e86df911ed22aa4b7f05a61542089d85f86d42f22789ece92ff0ccf9e83ddcb69cc94af27da479fe2bc4660
7
+ data.tar.gz: b3589d64194030087e2df1a7115e65956683a4b55e4dfcfde8b144aa320c27139baaa897efce8743c38b30c399d1598759f7db9e41d9e8852db0da8c9da5a07d
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.0
4
+ - 2.1.0
5
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'rspec', '~> 3.1'
7
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 code quest
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # Pipes [![Build Status](https://travis-ci.org/codequest-eu/codequest_pipes.svg?branch=master)](https://travis-ci.org/codequest-eu/codequest_pipes) [![codebeat badge](https://codebeat.co/badges/73f1bb7f-516f-4fc5-b241-daea42c7badd)](https://codebeat.co/projects/codequest_pipes-master-ab1a7c5f-ad5f-425a-a0f0-e56e13a04876)
2
+
3
+ Pipes provide a Unix-like way to chain business objects (interactors) in Ruby.
4
+
5
+ ## Installation
6
+
7
+ To start using Pipes, add the library to your Gemfile. This project is currently
8
+ not available on RubyGems.
9
+
10
+ ```ruby
11
+ gem "codequest_pipes", github: "codequest-eu/codequest_pipes"
12
+ ```
13
+
14
+ ## High-level usage example
15
+
16
+ ```ruby
17
+ FLOW = Projects::Match | # NOTE: each of the elements must inherit from
18
+ Projects::Validate | # Pipes::Pipe!
19
+ Projects::UpdatePayment |
20
+ Projects::SaveWithReport
21
+ context = Pipes::Context.new(project: p)
22
+ FLOW.call(context)
23
+ ```
24
+
25
+ ## Pipe
26
+
27
+ Pipes provide a way to describe business transactions in a stateless
28
+ and reusable way. Let's create a few pipes from plain Ruby classes.
29
+
30
+ ```ruby
31
+ class PaymentPipe < Pipes::Pipe
32
+ require_context :user # flow will fail if precondition not met
33
+ provide_context :transaction # flow will fail if postcondition not met
34
+
35
+ def call
36
+ result = PaymentService.create_transaction(user)
37
+ add(transaction: result.transaction)
38
+ end
39
+ end
40
+ ```
41
+
42
+ Note how we've only had to implement the `call` method for the magic to start happening. When calling these objects you'd be using the class method `call` instead and passing a `Pipes::Context` objects to it. All unknown messages (like `user`) in two examples above are passed to the `context` which is an instance variable of every object inheriting from `Pipes::Pipe`.
43
+
44
+ ## Context
45
+
46
+ Each Pipe requires an instance `Pipes::Context` to be passed on `.call` invokation. It provides append-only data container for Pipes: you can add data to a context at any time using the `add` method but the same call will raise an error if you try to modify an existing key.
47
+
48
+ Made with <3 by [code quest](http://www.codequest.com)
49
+
50
+
51
+
52
+
53
+
54
+
55
+
56
+
57
+
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ begin
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+ rescue LoadError
8
+ # no rspec available
9
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'codequest_pipes'
5
+ spec.version = '0.2.0'
6
+
7
+ spec.author = 'codequest'
8
+ spec.email = 'hello@codequest.com'
9
+ spec.description = 'Pipes provides a Unix-like way to chain business objects'
10
+ spec.summary = 'Unix-like way to chain business objects (interactors)'
11
+ spec.homepage = 'https://github.com/codequest-eu/codequest_pipes'
12
+ spec.license = 'MIT'
13
+
14
+ spec.files = `git ls-files`.split($RS)
15
+ spec.test_files = spec.files.grep(/^spec/)
16
+
17
+ spec.add_development_dependency 'bundler', '>= 1.6.9'
18
+ spec.add_development_dependency 'rake', '~> 10.3'
19
+ end
@@ -0,0 +1,9 @@
1
+ module Pipes
2
+ # Closure provides a quick and dirty way of turning a block into an example of
3
+ # Pipes::Pipe.
4
+ class Closure
5
+ def self.define(&block)
6
+ Class.new(Pipe) { define_method(:call, block) }
7
+ end
8
+ end # class Closure
9
+ end # module Pipes
@@ -0,0 +1,61 @@
1
+ module Pipes
2
+ # Context is an object used to pass data between Pipes. It behaves like an
3
+ # OpenStruct except you can write a value only once - this way we prevent
4
+ # context keys from being overwritten.
5
+ class Context
6
+ attr_reader :error
7
+
8
+ # Override is an exception raised when an attempt is made to override an
9
+ # existing Context property.
10
+ class Override < ::StandardError; end
11
+
12
+ # ExecutionTerminated is an exception raised when the `fail` method is
13
+ # explicitly called on the Context. This terminates the flow of a pipe.
14
+ class ExecutionTerminated < ::StandardError; end
15
+
16
+ # Context constructor.
17
+ #
18
+ # @param values [Hash]
19
+ def initialize(values = {})
20
+ add(values)
21
+ @error = nil
22
+ end
23
+
24
+ # Method `add` allows adding new properties (as a Hash) to the Context.
25
+ #
26
+ # @param values [Hash]
27
+ def add(values)
28
+ values.each do |key, val|
29
+ k_sym = key.to_sym
30
+ fail Override, "Property :#{key} already present" if respond_to?(k_sym)
31
+ define_singleton_method(k_sym) { val }
32
+ end
33
+ end
34
+
35
+ # Explicitly fail the pipe, allowing the error to be saved and accessed from
36
+ # the Context.
37
+ #
38
+ # @param error [Any] Error to be set.
39
+ #
40
+ # @raise [ExecutionTerminated]
41
+ def terminate(error)
42
+ @error = error
43
+ fail ExecutionTerminated, error
44
+ end
45
+
46
+ # Check if the Context finished successfully.
47
+ # This method smells of :reek:NilCheck
48
+ #
49
+ # @return [Boolean] Success status.
50
+ def success?
51
+ @error.nil?
52
+ end
53
+
54
+ # Check if the Context failed.
55
+ #
56
+ # @return [Boolean] Failure status.
57
+ def failure?
58
+ !success?
59
+ end
60
+ end # class Context
61
+ end # module Pipes
@@ -0,0 +1,76 @@
1
+ module Pipes
2
+ # Pipe is a mix-in which turns a class into a Pipes building block (Pipe).
3
+ # A Pipe can only have class methods since it can't be instantiated.
4
+ class Pipe
5
+ attr_reader :context
6
+
7
+ def initialize(ctx)
8
+ @context = ctx
9
+ end
10
+
11
+ def call
12
+ fail MissingCallMethod
13
+ end
14
+
15
+ def self.|(other)
16
+ this = self
17
+ Class.new(Pipe) do
18
+ _combine(this, other)
19
+ end
20
+ end
21
+
22
+ def self.call(ctx)
23
+ _validate_ctx(_required_context_elements, ctx)
24
+ new(ctx).call
25
+ _validate_ctx(_provided_context_elements, ctx)
26
+ ctx
27
+ end
28
+
29
+ def self.require_context(*args)
30
+ _required_context_elements.push(*args)
31
+ end
32
+
33
+ def self.provide_context(*args)
34
+ _provided_context_elements.push(*args)
35
+ end
36
+
37
+ def self._combine(first, second)
38
+ _check_interface(first)
39
+ _check_interface(second)
40
+ define_singleton_method(:call) do |ctx|
41
+ first.call(ctx)
42
+ second.call(ctx)
43
+ end
44
+ end
45
+ private_class_method :_combine
46
+
47
+ def self._check_interface(klass)
48
+ fail MissingCallMethod unless klass.instance_methods.include?(:call)
49
+ end
50
+ private_class_method :_check_interface
51
+
52
+ def self._required_context_elements
53
+ @required_context_elements ||= []
54
+ end
55
+ private_class_method :_required_context_elements
56
+
57
+ def self._provided_context_elements
58
+ @provided_context_elements ||= []
59
+ end
60
+ private_class_method :_provided_context_elements
61
+
62
+ def self._validate_ctx(collection, ctx)
63
+ collection.each do |element|
64
+ next if ctx.respond_to?(element)
65
+ fail MissingContext, "context does not respond to '#{element}'"
66
+ end
67
+ end
68
+ private_class_method :_validate_ctx
69
+
70
+ private
71
+
72
+ def method_missing(name, *args, &block)
73
+ context.send(name, *args, &block)
74
+ end
75
+ end # class Pipe
76
+ end # module Pipes
@@ -0,0 +1,8 @@
1
+ require 'codequest_pipes/closure'
2
+ require 'codequest_pipes/context'
3
+ require 'codequest_pipes/pipe'
4
+
5
+ module Pipes
6
+ class MissingCallMethod < ::Exception; end
7
+ class MissingContext < ::Exception; end
8
+ end # module Pipes
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pipes::Context do
4
+ let(:ctx) { Pipes::Context.new }
5
+
6
+ describe '#add' do
7
+ it 'allows adding new fields' do
8
+ ctx.add(key: 'val')
9
+ expect(ctx.key).to eq('val')
10
+ end
11
+
12
+ it 'does not allow rewriting existing fields' do
13
+ ctx.add(key: 'val')
14
+ expect { ctx.add(key: 'other_val') }
15
+ .to raise_error(Pipes::Context::Override)
16
+ end
17
+ end
18
+ end # describe Context
data/spec/pipe_spec.rb ADDED
@@ -0,0 +1,127 @@
1
+ require 'spec_helper'
2
+
3
+ # Parent is a dummy test class.
4
+ class Parent < Pipes::Pipe
5
+ require_context :flow
6
+
7
+ def call
8
+ flow.push(self.class.name)
9
+ end
10
+ end # class Parent
11
+
12
+ class Child < Parent; end
13
+ class Grandchild < Parent; end
14
+ class GrandGrandchild < Parent; end
15
+
16
+ # BadApple will break.
17
+ class BadApple < Pipes::Pipe
18
+ def call
19
+ fail StandardError
20
+ end
21
+ end
22
+
23
+ # NoMethodPipe will break with NoMethodError.
24
+ class NoMethodPipe < Pipes::Pipe; end
25
+
26
+ describe Pipes::Pipe do
27
+ let(:ctx) { Pipes::Context.new(flow: []) }
28
+
29
+ subject { pipe.call(ctx) }
30
+
31
+ describe 'normal flow' do
32
+ let(:pipe) { Parent | Child | Grandchild }
33
+
34
+ it 'executes the pipe left to right' do
35
+ expect { subject }.to change { ctx.flow }
36
+ .from([]).to %w(Parent Child Grandchild)
37
+ end
38
+ end # describe 'normal flow'
39
+
40
+ describe 'pipe raising an exception' do
41
+ let(:pipe) { Parent | BadApple | Child }
42
+
43
+ it 'raises StandardError' do
44
+ expect { pipe.call(ctx) }.to raise_error StandardError
45
+ end
46
+
47
+ it 'stores the result of execution so far in the context' do
48
+ # rubocop:disable Style/RescueModifier
49
+ expect { pipe.call(ctx) rescue nil }
50
+ .to change { ctx.flow }
51
+ .from([]).to(['Parent'])
52
+ # rubocop:enable Style/RescueModifier
53
+ end
54
+ end # describe 'pipe raising an exception'
55
+
56
+ describe '.provide_context' do
57
+ context 'when context element provided' do
58
+ class ProvidingChild < Parent
59
+ provide_context :bacon
60
+
61
+ def call
62
+ super
63
+ add(bacon: true)
64
+ end
65
+ end # class ProvideChild
66
+
67
+ let(:pipe) { Parent | ProvidingChild }
68
+
69
+ it 'does not raise' do
70
+ expect { subject }.to_not raise_error
71
+ end
72
+ end # context 'when context element provided'
73
+
74
+ context 'when context element not provided' do
75
+ class NotProvidingChild < Parent
76
+ provide_context :bacon
77
+ end
78
+
79
+ let(:pipe) { Parent | NotProvidingChild }
80
+
81
+ it 'raises MissingContext' do
82
+ expect { subject }.to raise_error Pipes::MissingContext
83
+ end
84
+ end # context 'when context element not provided'
85
+ end # describe '.provide_context'
86
+
87
+ describe 'pipes declared using Pipe::Closure' do
88
+ let(:dynamic_grandchild) do
89
+ Pipes::Closure.define { context.flow << 'bacon' }
90
+ end
91
+ let(:pipe) { Parent | dynamic_grandchild | Child }
92
+
93
+ it 'behaves as with normal pipes' do
94
+ expect { subject }
95
+ .to change { ctx.flow }
96
+ .from([]).to %w(Parent bacon Child)
97
+ end
98
+ end # describe 'pipes declared using Pipe::Closure'
99
+
100
+ describe 'pipe with a missing `call` method' do
101
+ let(:pipe) { Parent | Child | NoMethodPipe }
102
+
103
+ it 'raises a Pipes::MissingCallMethod error' do
104
+ expect { subject }.to raise_error Pipes::MissingCallMethod
105
+ end
106
+ end # describe 'pipe with a missing `call` method'
107
+
108
+ describe 'combined pipes' do
109
+ let(:first) { Parent | Child }
110
+ let(:second) { Grandchild | GrandGrandchild }
111
+ let(:pipe) { first | second }
112
+
113
+ it 'behaves as with normal pipes' do
114
+ expect { subject }
115
+ .to change { ctx.flow }
116
+ .from([]).to %w(Parent Child Grandchild GrandGrandchild)
117
+ end
118
+
119
+ describe 'broken combination' do
120
+ let(:second) { NoMethodPipe | Grandchild }
121
+
122
+ it 'raises error from a broken pipe' do
123
+ expect { subject }.to raise_error Pipes::MissingCallMethod
124
+ end
125
+ end # describe 'broken combination'
126
+ end # describe 'combined pipes'
127
+ end # describe Pipes::Pipe
@@ -0,0 +1,4 @@
1
+ require 'bundler/setup'
2
+ Bundler.setup
3
+
4
+ require 'codequest_pipes'
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: codequest_pipes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - codequest
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-02-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.6.9
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.6.9
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.3'
41
+ description: Pipes provides a Unix-like way to chain business objects
42
+ email: hello@codequest.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - ".gitignore"
48
+ - ".rspec"
49
+ - ".travis.yml"
50
+ - Gemfile
51
+ - LICENSE
52
+ - README.md
53
+ - Rakefile
54
+ - codequest_pipes.gemspec
55
+ - lib/codequest_pipes.rb
56
+ - lib/codequest_pipes/closure.rb
57
+ - lib/codequest_pipes/context.rb
58
+ - lib/codequest_pipes/pipe.rb
59
+ - spec/context_spec.rb
60
+ - spec/pipe_spec.rb
61
+ - spec/spec_helper.rb
62
+ homepage: https://github.com/codequest-eu/codequest_pipes
63
+ licenses:
64
+ - MIT
65
+ metadata: {}
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubyforge_project:
82
+ rubygems_version: 2.5.1
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: Unix-like way to chain business objects (interactors)
86
+ test_files:
87
+ - spec/context_spec.rb
88
+ - spec/pipe_spec.rb
89
+ - spec/spec_helper.rb