From bcdfc0389551c1a4fb3e9ddc76f5e450f7376715 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemek=20Bieli=C5=84ski?= Date: Thu, 10 Oct 2019 12:40:33 +0200 Subject: [PATCH 1/6] Add ErrorCollector object to Pipes::Context (#12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Przemek BieliƄski * ErrorCollector for Pipes::Context * Store error passed to 'halt' in the error collectors errors hash in the :base key. Add 'error' method returning this error for backwards compatibility. * Update gem version --- codequest_pipes.gemspec | 2 +- lib/codequest_pipes/context.rb | 53 ++++++++++++++----- .../context/error_collector.rb | 20 +++++++ lib/codequest_pipes/pipe.rb | 2 +- spec/context_spec.rb | 25 ++++++++- spec/matcher_spec.rb | 2 +- 6 files changed, 85 insertions(+), 19 deletions(-) create mode 100644 lib/codequest_pipes/context/error_collector.rb diff --git a/codequest_pipes.gemspec b/codequest_pipes.gemspec index 0cdda07..e312bcb 100644 --- a/codequest_pipes.gemspec +++ b/codequest_pipes.gemspec @@ -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' diff --git a/lib/codequest_pipes/context.rb b/lib/codequest_pipes/context.rb index 0fcc7ba..155c1e1 100644 --- a/lib/codequest_pipes/context.rb +++ b/lib/codequest_pipes/context.rb @@ -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 @@ -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. @@ -32,23 +32,20 @@ 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. @@ -56,7 +53,7 @@ def terminate(error) # # @return [Boolean] Success status. def success? - @error.nil? + errors.empty? end # Check if the Context failed. @@ -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) "#" 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 diff --git a/lib/codequest_pipes/context/error_collector.rb b/lib/codequest_pipes/context/error_collector.rb new file mode 100644 index 0000000..60a048c --- /dev/null +++ b/lib/codequest_pipes/context/error_collector.rb @@ -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 diff --git a/lib/codequest_pipes/pipe.rb b/lib/codequest_pipes/pipe.rb index d2eff11..4a75b24 100644 --- a/lib/codequest_pipes/pipe.rb +++ b/lib/codequest_pipes/pipe.rb @@ -20,7 +20,7 @@ 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) diff --git a/spec/context_spec.rb b/spec/context_spec.rb index bd632cc..0982f67 100644 --- a/spec/context_spec.rb +++ b/spec/context_spec.rb @@ -18,13 +18,34 @@ it 'lists all fields' do subject.add(bacon: 'yum', raisins: 'bleh') expect(subject.inspect) - .to match(/bacon=\"yum\", raisins=\"bleh\", @error=nil/) + .to match(/bacon=\"yum\", raisins=\"bleh\", @errors=nil/) end it 'lists nested contexts' do subject.add(nested: Pipes::Context.new(foo: 'bar')) expect(subject.inspect) - .to match(/nested=#,/) + .to match(/nested=#,/) 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 diff --git a/spec/matcher_spec.rb b/spec/matcher_spec.rb index 71f2c2d..3ad7b55 100644 --- a/spec/matcher_spec.rb +++ b/spec/matcher_spec.rb @@ -13,7 +13,7 @@ shared_examples_for 'fails_with_message' do |message| it 'fails' do expected_message = - message || /expected # to match/ + message || /expected # to match/ expect { expect(ctx).to match(pipe_context(expected)) } .to fail_with(expected_message) end From d07c533323a55825f1277e20dfe8058b17c14b8a Mon Sep 17 00:00:00 2001 From: pjanek Date: Thu, 10 Oct 2019 12:42:12 +0200 Subject: [PATCH 2/6] Strong context elements (#13) * strong context elements v1-simple * tests for strong context elements * extract _merge_context_elements method * rename 'cls' to 'klass' * reformat _validate_ctx * reformat tests and add tests for require_context --- lib/codequest_pipes.rb | 1 + lib/codequest_pipes/pipe.rb | 40 ++++++++++---- spec/pipe_spec.rb | 102 +++++++++++++++++++++++++++++++----- 3 files changed, 121 insertions(+), 22 deletions(-) diff --git a/lib/codequest_pipes.rb b/lib/codequest_pipes.rb index 1072217..ef3a4a7 100755 --- a/lib/codequest_pipes.rb +++ b/lib/codequest_pipes.rb @@ -5,4 +5,5 @@ module Pipes class MissingCallMethod < ::Exception; end class MissingContext < ::Exception; end + class InvalidType < ::Exception; end end # module Pipes diff --git a/lib/codequest_pipes/pipe.rb b/lib/codequest_pipes/pipe.rb index 4a75b24..0817580 100644 --- a/lib/codequest_pipes/pipe.rb +++ b/lib/codequest_pipes/pipe.rb @@ -26,12 +26,12 @@ def self.call(ctx) _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) @@ -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) diff --git a/spec/pipe_spec.rb b/spec/pipe_spec.rb index 426536b..2dc8bb3 100644 --- a/spec/pipe_spec.rb +++ b/spec/pipe_spec.rb @@ -20,6 +20,45 @@ def call end end +class ProvidingChild < Parent + provide_context :bacon + + def call + super + add(bacon: true) + end +end # class ProvidingChild + +class ProvidingNumericChild < Parent + provide_context bacon: Numeric + + def call + super + add(bacon: 4) + end +end # class ProvidingNumericChild + +class NotProvidingChild < Parent + provide_context :bacon +end # class NotProvidingChild + +class ProvidingInvalidChild < Parent + provide_context bacon: Numeric + + def call + super + add(bacon: "yes, please") + end +end # class ProvidingInvalidChild + +class RequiringChild < Parent + require_context :bacon +end # class RequiringChild + +class RequiringNumericChild < Parent + require_context bacon: Numeric +end # class RequiringNumericChild + # NoMethodPipe will break with NoMethodError. class NoMethodPipe < Pipes::Pipe; end @@ -55,15 +94,6 @@ class NoMethodPipe < Pipes::Pipe; end describe '.provide_context' do context 'when context element provided' do - class ProvidingChild < Parent - provide_context :bacon - - def call - super - add(bacon: true) - end - end # class ProvideChild - let(:pipe) { Parent | ProvidingChild } it 'does not raise' do @@ -72,16 +102,62 @@ def call end # context 'when context element provided' context 'when context element not provided' do - class NotProvidingChild < Parent - provide_context :bacon - end - let(:pipe) { Parent | NotProvidingChild } it 'raises MissingContext' do expect { subject }.to raise_error Pipes::MissingContext end end # context 'when context element not provided' + + context 'when context element with invalid type provided' do + let(:pipe) { Parent | ProvidingInvalidChild } + + it 'raises InvalidType' do + expect { subject }.to raise_error Pipes::InvalidType + end + end # context 'when context element with invalid type provided' + + context 'when context element with valid type provided' do + let(:pipe) { Parent | ProvidingNumericChild } + + it 'does not raise' do + expect { subject }.to_not raise_error + end + end # context 'when context element with valid type provided' + end # describe '.provide_context' + + describe '.require_context' do + context 'when required context element present' do + let(:pipe) { ProvidingChild | RequiringChild } + + it 'does not raise' do + expect { subject }.to_not raise_error + end + end # context 'when required context element present' + + context 'when required context element missing' do + let(:pipe) { Parent | RequiringChild } + + it 'raises MissingContext' do + expect { subject }.to raise_error Pipes::MissingContext + end + end # context 'when context element missing' + + context 'when required context element present with invalid type' do + let(:pipe) { ProvidingInvalidChild | RequiringNumericChild } + + it 'raises InvalidType' do + expect { subject }.to raise_error Pipes::InvalidType + end + end # context 'when context element present with invalid type' + + context 'when required context element present with valid type' do + let(:pipe) { ProvidingNumericChild | RequiringNumericChild } + + it 'raises InvalidType' do + expect { subject }.to_not raise_error + end + end # context 'when context element present with valid type' end # describe '.provide_context' describe 'pipes declared using Pipe::Closure' do From ff2e5bfce819ac1f506fca0f705bbe8206914a3b Mon Sep 17 00:00:00 2001 From: Jan Taras Date: Tue, 10 Nov 2020 15:56:07 +0100 Subject: [PATCH 3/6] [typed context] Fixed wrong description in the spec --- spec/pipe_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/pipe_spec.rb b/spec/pipe_spec.rb index 2dc8bb3..366dfbf 100644 --- a/spec/pipe_spec.rb +++ b/spec/pipe_spec.rb @@ -154,7 +154,7 @@ class NoMethodPipe < Pipes::Pipe; end context 'when required context element present with valid type' do let(:pipe) { ProvidingNumericChild | RequiringNumericChild } - it 'raises InvalidType' do + it 'does not raise' do expect { subject }.to_not raise_error end end # context 'when context element present with valid type' From 9eeb4ae350e9bd4181c1125249d9b54eef4bcadf Mon Sep 17 00:00:00 2001 From: Jan Taras Date: Tue, 10 Nov 2020 17:36:29 +0100 Subject: [PATCH 4/6] [release/1.0.0] Simplified multikey addition spec --- spec/context_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/context_spec.rb b/spec/context_spec.rb index 0982f67..e058c32 100644 --- a/spec/context_spec.rb +++ b/spec/context_spec.rb @@ -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') } @@ -15,12 +21,6 @@ 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\", @errors=nil/) - end - it 'lists nested contexts' do subject.add(nested: Pipes::Context.new(foo: 'bar')) expect(subject.inspect) From f6f816dd2019d3efedb562f6a282d0ac4f679dcc Mon Sep 17 00:00:00 2001 From: Jan Taras Date: Mon, 16 Nov 2020 13:50:05 +0100 Subject: [PATCH 5/6] [release/1.0.0] Added missing param to codefresh.yaml --- codefresh.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/codefresh.yml b/codefresh.yml index b695740..560ba7c 100644 --- a/codefresh.yml +++ b/codefresh.yml @@ -6,6 +6,7 @@ steps: title: 'Build image' type: build image_name: codequest/pipes + registry: r.cfcr.io/codequest test_library: title: 'Test the gem' From b168756d82dd50a946f412da20720564589835a9 Mon Sep 17 00:00:00 2001 From: Jan Taras Date: Mon, 16 Nov 2020 14:04:42 +0100 Subject: [PATCH 6/6] [release/1.0.0] Added missing param to codefresh.yaml pt.2 --- codefresh.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codefresh.yml b/codefresh.yml index 560ba7c..53f8a96 100644 --- a/codefresh.yml +++ b/codefresh.yml @@ -6,7 +6,7 @@ steps: title: 'Build image' type: build image_name: codequest/pipes - registry: r.cfcr.io/codequest + registry: cfcr test_library: title: 'Test the gem'