Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codefresh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ steps:
title: 'Build image'
type: build
image_name: codequest/pipes
registry: cfcr

test_library:
title: 'Test the gem'
Expand Down
2 changes: 1 addition & 1 deletion codequest_pipes.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Gem::Specification.new do |spec|
spec.name = 'codequest_pipes'
spec.version = '0.3.1.1'
spec.version = '0.3.2'

spec.author = 'codequest'
spec.email = 'hello@codequest.com'
Expand Down
1 change: 1 addition & 0 deletions lib/codequest_pipes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
module Pipes
class MissingCallMethod < ::Exception; end
class MissingContext < ::Exception; end
class InvalidType < ::Exception; end
end # module Pipes
53 changes: 39 additions & 14 deletions lib/codequest_pipes/context.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
require 'codequest_pipes/context/error_collector'

module Pipes
# Context is an object used to pass data between Pipes. It behaves like an
# OpenStruct except you can write a value only once - this way we prevent
# context keys from being overwritten.
class Context
attr_reader :error

# Override is an exception raised when an attempt is made to override an
# existing Context property.
class Override < ::StandardError; end
Expand All @@ -18,7 +18,7 @@ class ExecutionTerminated < ::StandardError; end
# @param values [Hash]
def initialize(values = {})
add(values)
@error = nil
@error_collector = ErrorCollector.new
end

# Method `add` allows adding new properties (as a Hash) to the Context.
Expand All @@ -32,31 +32,28 @@ def add(values)
end
end

# Quietly fail the pipe, allowing the error to be saved and accessed from
# the Context.
# Quietly fail the pipe. The error will be passed to the error_collector
# and stored in the :base errors collection.
#
# @param error [Any] Error to be set.
## @param error [String]
def halt(error = 'Execution stopped')
@error = error
add_errors(base: error)
end

# Explicitly fail the pipe, allowing the error to be saved and accessed from
# the Context.
#
# @param error [Any] Error to be set.
# Explicitly fail the pipe.
#
# @raise [ExecutionTerminated]
def terminate(error)
halt(error)
fail ExecutionTerminated, error
fail ExecutionTerminated
end

# Check if the Context finished successfully.
# This method smells of :reek:NilCheck
#
# @return [Boolean] Success status.
def success?
@error.nil?
errors.empty?
end

# Check if the Context failed.
Expand All @@ -73,9 +70,37 @@ def failure?
def inspect
keys = methods - Object.methods - Pipes::Context.instance_methods
fields = keys.map { |key| "#{key}=#{public_send(key).inspect}" }
fields << "@error=#{@error.inspect}"
fields << "@errors=#{@errors.inspect}"
object_id_hex = '%x' % (object_id << 1)
"#<Pipes::Context:0x00#{object_id_hex} #{fields.join(', ')}>"
end

# Return errors from ErrorCollector object.
#
# @return [Hash]
def errors
error_collector.errors
end

# This method is added to maintain backwards compatibility - previous
# versions implemented a single @error instance variable of String for error
# storage.
#
# @return [String]
def error
errors[:base]&.first
end

# Add errors to ErrorCollector object.
# It doesn't fail the pipe as opposed to `halt` and `terminate` methods.
#
# @param collectable_errors [Hash]
def add_errors(collectable_errors)
error_collector.add(collectable_errors)
end

private

attr_reader :error_collector
end # class Context
end # module Pipes
20 changes: 20 additions & 0 deletions lib/codequest_pipes/context/error_collector.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Pipes
class Context
# ErrorCollector is Context's companion object for storing non-critical
# errors.
class ErrorCollector
attr_reader :errors

def initialize
@errors = {}
end

def add(errors_hash)
errors_hash.map do |key, errors|
@errors[key] ||= []
@errors[key] = @errors[key] | Array(errors)
end
end
end # class ErrorColletor
end # class Context
end # module Pipes
42 changes: 32 additions & 10 deletions lib/codequest_pipes/pipe.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ def self.|(other)
end

def self.call(ctx)
return ctx if ctx.error
return ctx if ctx.errors.any?
_validate_ctx(_required_context_elements, ctx)
new(ctx).call
_validate_ctx(_provided_context_elements, ctx)
end

def self.require_context(*args)
_required_context_elements.push(*args)
def self.require_context(*args, **kwargs)
_merge_context_elements(_required_context_elements, args, kwargs)
end

def self.provide_context(*args)
_provided_context_elements.push(*args)
def self.provide_context(*args, **kwargs)
_merge_context_elements(_provided_context_elements, args, kwargs)
end

def self._combine(first, second)
Expand All @@ -50,23 +50,45 @@ def self._check_interface(klass)
private_class_method :_check_interface

def self._required_context_elements
@required_context_elements ||= []
@required_context_elements ||= {}
end
private_class_method :_required_context_elements

def self._provided_context_elements
@provided_context_elements ||= []
@provided_context_elements ||= {}
end
private_class_method :_provided_context_elements

def self._validate_ctx(collection, ctx)
collection.each do |element|
next if ctx.respond_to?(element)
fail MissingContext, "context does not respond to '#{element}'"
collection.each do |element, klass|
_validate_value_presence(ctx, element)
_validate_value_type(ctx, element, klass) if klass
end
end
private_class_method :_validate_ctx

def self._validate_value_presence(ctx, element)
return if ctx.respond_to?(element)
raise MissingContext, "context does not respond to '#{element}'"
end
private_class_method :_validate_value_presence

def self._validate_value_type(ctx, element, klass)
obj = ctx.public_send(element)
return if obj.is_a?(klass)
raise InvalidType,
"'#{element}' has invalid type #{obj.class} (expected: #{klass})"
end
private_class_method :_validate_value_type

def self._merge_context_elements(elements, args, kwargs)
elements.merge!(
**args.map { |a| [a, nil] }.to_h,
**kwargs
)
end
private_class_method :_merge_context_elements

private

def method_missing(name, *args, &block)
Expand Down
35 changes: 28 additions & 7 deletions spec/context_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
expect(subject.key).to eq('val')
end

it 'can add multiple fields' do
subject.add(bacon: 'yum', raisins: 'bleh')
expect(subject.bacon).to eq('yum')
expect(subject.raisins).to eq('bleh')
end

it 'does not allow rewriting existing fields' do
subject.add(key: 'val')
expect { subject.add(key: 'other_val') }
Expand All @@ -15,16 +21,31 @@
end # describe '#add'

describe '#inspect' do
it 'lists all fields' do
subject.add(bacon: 'yum', raisins: 'bleh')
expect(subject.inspect)
.to match(/bacon=\"yum\", raisins=\"bleh\", @error=nil/)
end

it 'lists nested contexts' do
subject.add(nested: Pipes::Context.new(foo: 'bar'))
expect(subject.inspect)
.to match(/nested=#<Pipes::Context:0x\w+ foo="bar", @error=nil>,/)
.to match(/nested=#<Pipes::Context:0x\w+ foo="bar", @errors=nil>,/)
end
end # describe '#inspect'

describe '#add_errors' do
it 'adds error to error_collector' do
subject.add_errors(base: 'Error message')
subject.add_errors(
base: ['Another error message'],
user: 'User error message'
)
expect(subject.errors).to eq(
base: ['Error message', 'Another error message'],
user: ['User error message']
)
end
end # describe '#add_errors'

describe '#halt' do
it 'adds error to error collector :base' do
subject.halt('Some error')
expect(subject.error).to eq('Some error')
end
end # describe '#halt'
end # describe Pipes::Context
2 changes: 1 addition & 1 deletion spec/matcher_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
shared_examples_for 'fails_with_message' do |message|
it 'fails' do
expected_message =
message || /expected #<Pipes::Context:.+ @error=nil> to match/
message || /expected #<Pipes::Context:.+ @errors=nil> to match/
expect { expect(ctx).to match(pipe_context(expected)) }
.to fail_with(expected_message)
end
Expand Down
Loading