codequest_pipes 0.2.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 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