From 533d3e4728d6fbfdbb32bb7107e7c3dd99a2cf68 Mon Sep 17 00:00:00 2001 From: Alexandr Smirnov Date: Mon, 3 Apr 2017 14:11:53 +0300 Subject: [PATCH] Ready to work * refactored logic * perfect rubocop and code style * added docs * added travis * class_builder_spec and rubocop-rspec features * added applicator_spec, renamed binding to binding_ * applciator_with_otions_spec * styles in verifier_spec * added XVerifier::VERSION spec * updated rubocop-rspec, updated styling * updated travis config - use with newest bundler - use ruby 2.3.4 for 2.3 family - add ruby 2.4.1 for 2.4 family * fixed bug with MethodExtractor on Binding binding * review fixes * specs for InstanceEvaluator#caller_line * Added readme * grammar --- .rubocop.yml | 15 ++ .travis.yml | 6 + README.md | 176 +++++++++++++++ lib/xverifier.rb | 6 +- lib/xverifier/applicator.rb | 209 ++++++++++++++---- lib/xverifier/applicator_with_options.rb | 43 ++++ lib/xverifier/class_builder.rb | 55 +++++ lib/xverifier/verifier.rb | 33 ++- .../applicator_with_options_builder.rb | 74 +++++++ spec/spec_helper.rb | 10 + spec/xverifier/applicator_spec.rb | 141 ++++++++++++ .../xverifier/applicator_with_options_spec.rb | 46 ++++ spec/xverifier/class_builder_spec.rb | 25 +++ spec/xverifier/verifier_spec.rb | 24 +- spec/xverifier_spec.rb | 14 ++ xverifier.gemspec | 2 + 16 files changed, 813 insertions(+), 66 deletions(-) create mode 100644 .travis.yml create mode 100644 README.md create mode 100644 lib/xverifier/applicator_with_options.rb create mode 100644 lib/xverifier/class_builder.rb create mode 100644 lib/xverifier/verifier/applicator_with_options_builder.rb create mode 100644 spec/xverifier/applicator_spec.rb create mode 100644 spec/xverifier/applicator_with_options_spec.rb create mode 100644 spec/xverifier/class_builder_spec.rb create mode 100644 spec/xverifier_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 484948e..65954f0 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,3 +1,6 @@ +require: + - rubocop-rspec + AllCops: TargetRubyVersion: 2.3 Exclude: @@ -14,3 +17,15 @@ Metrics/BlockLength: Lint/AmbiguousBlockAssociation: # REVIEW: why is that dangerous? Enabled: false + +RSpec/FilePath: + CustomTransform: + XVerifier: xverifier + +RSpec/MessageSpies: + # Spies are not always usable, i.e. we want to stub something we haven't created + EnforcedStyle: receive + +RSpec/NestedGroups: + # Extra nesting is good, not bad! + Enabled: false diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9870a9d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +sudo: false +language: ruby +rvm: + - 2.3.4 + - 2.4.1 +before_install: gem install bundler -v 1.14.6 diff --git a/README.md b/README.md new file mode 100644 index 0000000..01007a1 --- /dev/null +++ b/README.md @@ -0,0 +1,176 @@ +# XVerifier v0.1 +[![Build Status](https://travis-ci.org/umbrellio/xverifier.svg?branch=master)] +(https://travis-ci.org/umbrellio/xverifier) + +This gem consists of several dependent components, which all could be +used standalone. The most important one is [Verifier](#Verifier), +but understanding of [Applicator](#Applicator) and +[ApplicatorWithOptions](#ApplicatorWithOptions) helps understand it's API. +The least one, [ClassBuilder](#ClassBuilder) only used in private APIs, +but it's own API also public + +## ClassBuilder + +example: + +```lang=ruby +Abstract = Struct.new(:data) + extend XVerifier::ClassBuilder::Mixin + + class WithString < self + def self.build_class(x) + self if x.is_a?(String) + end + end + + Generic = Class.new(self) + + self.buildable_classes = [WithString, Generic] + # or, vise versa + def self.buildable_classes + [WithString, Generic] + end +end + +Abstract.build("foo") # => WithString.new("foo") +Abstract.build(:foo) # => Generic.new("foo") +``` + +or see lib/verifier/applicator.rb + +Why don't just use Uber::Builder? +([Uber](https://github.com/apotonick/uber) is cool, you should try it) +There are two reasons: firstly, it is unnecessary dependency. +We dont want npm hell, aren't we? Uber::Builder realy does not do much work, +it's just a pattern. Secondly, this implementation looks for me +to be more clear, because children instead of parent are deciding would +they handle arguments. + +So to use it you have to: + +1. Write some classes with duck type `.class_builder(*args)` + +2. Invoke `XVerifier::ClassBuilder.new([<%= array_of_classes %>]).call(*args)` + +3. ???? + +4. PROFIT + +It's simple and clear, but not very sugarish. So, otherwise, you may do +following: + +1. Write an abstract class + +2. Extend `XVerifier::ClassBuilder::Mixin` + +3. Inherit abstract class in different implementations + +4. If some implementations have common ancestors +(not including abstract class), you can implement common ancestor's +`.build_class` in terms of super (i.e. +`def self.build_class(x); super if x.is_a?(String); end`) + +5. Change `.build_class` of other classes like `self if ...`. +Don't change default implementation's `.build_class` + +6. Setup `.buildable_classes` on abstract class, mention only direct chldren +if you done step 4 + +7. Optionally redefine `.build` in abstract class, if you want +to separate `build_class` and constructor params + +8. Use `.build` instead of `new` + +## Applicator + +Applicator is designed to wrap applying of +[applicable](https://en.wikipedia.org/wiki/Sepulka) objects +to some binding in some context + +example: + +```lang=ruby +object = OpenStruct.new(foo: :bar) +Applicator.call(:foo, object, {}) # => :bar +Applicator.call('foo', object, {}) # => :bar +Applicator.call('context', object, {}) # => {} +Applicator.call(-> { foo }, object, {}) # => :bar +Applicator.call(->(context) { context[foo] }, object, bar: :baz) # => :baz +Applicator.call(true, object, {}) # => true + +foo = :bar +Applicator.call(:foo, binding, {}) # => :bar +Applicator.call('object.foo', binding, {}) # => :bar +``` + +Applicator is good, but in most case +[ApplicatorWithOptions](#ApplicatorWithOptions) would be better solution. + +## ApplicatorWithOptions + +ApplicatorWithOptions is an applicator with options. +The options are `if: ` and `unless: `. Same as in ActiveModel::Validations, +they are applied to same binding and main action would be executed +if `if: ` evaluates to truthy and `unless: ` evaluates to falsey + +See examples: + +```lang=ruby +ApplicatorWithOptions.new(:foo, if: -> { true }).call(binding, {}) # => foo + +ApplicatorWithOptions.new(:foo, if: -> (context) { context[:bar] }) + .call(binding, { bar: true }) # => foo + +ApplicatorWithOptions.new(:foo, if: { bar: true }).call(binding, :bar) # => foo + +ApplicatorWithOptions.new(:foo, unless: -> { true }) + .call(binding, {}) # => nil +ApplicatorWithOptions.new(:foo, unless: -> (context) { context[:bar] }) + .call(binding, { bar: true }) # => foo +ApplicatorWithOptions.new(:foo, unless: { bar: true }) + .call(binding, :bar) # => nil +``` + +## Verifier + +The last but most interesting component is Verifier. +Verifiers use ApplciatorWithOptions to execute generic procedures. +Procedures should call `message!` if they want to yield something. +Note, that you should implement `message!` by yourself (in terms of super) + +```lang=ruby +class MyVerifier < XVerifier::Verifier + Message = Struct.new(:text) + verify :foo, if: { foo: true } + + private + + def message!(text) + super { Message.new(text) } + end + + def foo + message!('Something is wrong') if Fixnum != Bignum + end +end +``` + +In addition to Applicator power, you also can nest your verifiers +to split some logic + +```lang=ruby +class MyVerifier < XVerifier::Verifier + Message = Struct.new(:text) + verify ChildVerifier, if: -> (context) { cotnext[:foo] } + + private + + def message!(text) + super { Message.new(text) } + end +end + +class ChildVerifier < MyVerifier + verify %q(message!("it's alive!")) +end +``` diff --git a/lib/xverifier.rb b/lib/xverifier.rb index 19b9463..3e7d693 100644 --- a/lib/xverifier.rb +++ b/lib/xverifier.rb @@ -1,9 +1,13 @@ # frozen_string_literal: true -# @todo write a good readme +# XVerifier provides several usefull classes, but most important +# is Verifier. Other provided classes are it's dependency. +# See README.md or their own documentation for more info about usage module XVerifier autoload :VERSION, 'xverifier/version' autoload :Applicator, 'xverifier/applicator' + autoload :ApplicatorWithOptions, 'xverifier/applicator_with_options' + autoload :ClassBuilder, 'xverifier/class_builder' autoload :Verifier, 'xverifier/verifier' end diff --git a/lib/xverifier/applicator.rb b/lib/xverifier/applicator.rb index d535185..586edc4 100644 --- a/lib/xverifier/applicator.rb +++ b/lib/xverifier/applicator.rb @@ -2,77 +2,204 @@ module XVerifier # @abstract implement `#call` + # Applies "applicable" objects to given bindings + # (applicable objects are named based on their use, + # currently any object is applicable). + # + # This class uses ClassBuilder system. + # When reading the code, we suggest starting from '.call' method + # @see ClassBuilder + # @attr applicable [applicable] wrapped applicable object class Applicator - attr_accessor :applicable - - def self.build_class(applicable) - if applicable.is_a?(Symbol) - MethodExtractor - elsif applicable.is_a?(String) - InstanceEvaler - elsif applicable.respond_to?(:to_proc) - ProcApplicatior - else - Quoter + # Proxy is used when applicable itself is an instance of Applicator. + # It just delegates #call method to applicable + # @example + # Applicator.call(Applicator.build(:foo), binding_, context) + # # => Applicator.call(:foo, binding_, context) + class Proxy < self + # @param applicable [Applicator] + # @return Proxy if applicable is an instance of Applicator + # @return [nil] otherwise + def self.build_class(applicable) + self if applicable.is_a?(Applicator) end - end - def self.call(applicable, binding, context) - build_class(applicable).new(applicable).call(binding, context) + # @param binding_ [#instance_exec] target to apply applicable + # @param context additional info to send it to applicable + # @return application result + def call(binding_, context) + applicable.call(binding_, context) + end end - def initialize(applicable) - self.applicable = applicable - end + # MethodExtractor is used when applicable is a symbol. + # It extracts extracts method from binding_ and executes it on binding_ + # (so it works just like send except it sends nothing + # when method arity is zero). + # @example + # Applicator.call(Applicator.build(:foo), User.new, context) + # # => User.new.foo(context) + # # or => User.new.foo, if it does not accept context + class MethodExtractor < self + # @param applicable [Symbol] + # @return MethodExtractor if applicable is Symbol + # @return [nil] otherwise + def self.build_class(applicable) + self if applicable.is_a?(Symbol) + end - # @!method call(binding, context) - # @abstract - # @param binding [#instance_exec] - # @param context + # @param binding_ [#instance_exec] target to apply applicable + # @param context additional info to send it to applicable + # @return application result + def call(binding_, context) + if binding_.is_a?(Binding) + call_on_binding(binding_, context) + else + invoke_lambda(binding_.method(applicable), binding_, context) + end + end - private + private - def invoke_lambda(lambda, binding, context) - if lambda.arity.zero? - binding.instance_exec(&lambda) - else - binding.instance_exec(context, &lambda) + # When Binding is target, we have to respect both methods and variables + # @param binding_ [Binding] target to apply applicable + # @param context additional info to send it to applicable + # @return application result + def call_on_binding(binding_, context) + if binding_.receiver.respond_to?(applicable) + invoke_lambda(binding_.receiver.method(applicable), binding_, context) + else + binding_.local_variable_get(applicable) + end end end - class MethodExtractor < self - def call(binding, context) - invoke_lambda(binding.method(applicable), binding, context) + # InstanceEvaluator is used for string. It works like instance_eval or + # Binding#eval depending on binding_ class + # @example + # Applicator.call('foo if context[:foo]', binding_, context) + # # => foo if context[:foo] + class InstanceEvaluator < self + # @param applicable [String] + # @return InstanceEvaluator if applicable is String + # @return [nil] otherwise + def self.build_class(applicable) + self if applicable.is_a?(String) end - end - class InstanceEvaler < self - def call(binding, context) - if binding.is_a?(Binding) - binding = binding.dup - binding.local_variable_set(:context, context) - binding.eval(applicable, *caller_line) + # @param binding_ [#instance_exec] target to apply applicable + # @param context additional info to send it to applicable + # @return application result + def call(binding_, context) + if binding_.is_a?(Binding) + binding_ = binding_.dup + binding_.local_variable_set(:context, context) + binding_.eval(applicable, *caller_line) else - binding.instance_eval(applicable, *caller_line) + binding_.instance_eval(applicable, *caller_line) end end + # @return [String, Integer] + # file and line where `Applicator.call` was called def caller_line - _, file, line = caller(3...4)[0].match(/\A(.+):(\d+):[^:]+\z/).to_a + offset = 2 + backtace_line = caller(offset..offset)[0] + _, file, line = backtace_line.match(/\A(.+):(\d+):[^:]+\z/).to_a [file, Integer(line)] end end + # ProcApplicatior is used when #to_proc is available. + # It works not just with procs, but also with hashes etc + # @example with proc + # Applicator.call(-> { foo }, binding_, context) # => foo + # @example with hash + # Applicator.call(Hash[foo: true], binding_, :foo) # => true + # Applicator.call(Hash[foo: true], binding_, :bar) # => nil class ProcApplicatior < self - def call(binding, context) - invoke_lambda(applicable.to_proc, binding, context) + # @param applicable [#to_proc] + # @return ProcApplicatior if applicable accepts #to_proc + # @return [nil] otherwise + def self.build_class(applicable) + self if applicable.respond_to?(:to_proc) + end + + # @param binding_ [#instance_exec] target to apply applicable + # @param context additional info to send it to applicable + # @return application result + def call(binding_, context) + invoke_lambda(applicable.to_proc, binding_, context) end end + # Quoter is used when there is no other way to apply applicatable. + # @example + # Applicator.call(true, binding_, context) # => true class Quoter < self + # @return applicable without changes def call(*) applicable end end + + extend ClassBuilder::Mixin + + self.buildable_classes = + [Proxy, MethodExtractor, InstanceEvaluator, ProcApplicatior, Quoter] + + attr_accessor :applicable + + # Applies applicable on binding_ with context + # @todo add @see #initialize when it's todo done + # @param applicable [applicable] + # see examples in defenitions of sublcasses + # @param binding_ [#instance_exec] + # where should applicable be applied. It could be either a generic object, + # where it would be `instance_exec`uted, or a binding_ + # @param context + # geneneric data you want to pass to applicable function. + # If applicable cannot accept params, context will not be sent + # @return application result + def self.call(applicable, binding_, context) + build(applicable).call(binding_, context) + end + + # Always use build instead of new + # @todo add more examples right here + # @param applicable [applicable] + # see examples in sublcasses defenitions + # @api private + def initialize(applicable) + self.applicable = applicable + end + + # @param [Applicator] other + # @return [Boolean] true if applicable matches, false otherwise + def ==(other) + applicable == other.applicable + end + + # @!method call(binding_, context) + # @abstract + # Applies applicable on binding_ with context + # @param binding_ [#instance_exec] binding to be used for applying + # @param context param that will be passed if requested + # @return application result + + private + + # invokes lambda respecting it's arity + # @param [Proc] lambda + # @param binding_ [#instance_exec] binding_ would be used in application + # @param context param would be passed if lambda arity > 0 + # @return invocation result + def invoke_lambda(lambda, binding_, context) + if lambda.arity.zero? + binding_.instance_exec(&lambda) + else + binding_.instance_exec(context, &lambda) + end + end end end diff --git a/lib/xverifier/applicator_with_options.rb b/lib/xverifier/applicator_with_options.rb new file mode 100644 index 0000000..af0f396 --- /dev/null +++ b/lib/xverifier/applicator_with_options.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module XVerifier + # An applicator with useful options + # @example if + # ApplicatorWithOptions.new(action, if: true) + # @example unless + # ApplicatorWithOptions.new(action, unless: false) + # @attr action [Applicator] + # main action to apply on call + # @attr if_condition [Applicator] + # main action only apply if this applies to truthy value + # @attr unless_condition [Applicator] + # main action only apply if this applies to falsey value + class ApplicatorWithOptions + attr_accessor :action, :if_condition, :unless_condition + + # @param action [applicable] main action + # @option options [applicable] :if + # main action only apply if this applies to truthy value + # @option options [applicable] :unless + # main action only apply if this applies to falsey value + def initialize(action, **options) + self.action = Applicator.build(action) + self.if_condition = Applicator.build(options.fetch(:if, true)) + self.unless_condition = Applicator.build(options.fetch(:unless, false)) + end + + # Applies main action if if_condition applyd to truthy value + # and unless_condition applyd to falsey value + # @param binding_ [#instance_exec] + # binding to apply (see Applicator) + # @param context + # generic context to apply (see Applicator) + # @return main action application result + # @return [nil] if condition checks failed + def call(binding_, context) + return unless if_condition.call(binding_, context) + return if unless_condition.call(binding_, context) + action.call(binding_, context) + end + end +end diff --git a/lib/xverifier/class_builder.rb b/lib/xverifier/class_builder.rb new file mode 100644 index 0000000..fcb4307 --- /dev/null +++ b/lib/xverifier/class_builder.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module XVerifier + # ClassBuilder is something like Uber::Builder, but it + # allows the child classes to decide whether they will be used. + # I find it much more OOPish + # @attr klasses [Array(Class)] + # classes to iterate during search of most suitable + class ClassBuilder + # Mixin provides useful methods to integrate builder subsystem. + # Feel free to override or just never include it. + # @attr_writer [Array(Class)] buildable_classes + # Array of classes which will be checked if they + # suite constructor arguments. Order matters + module Mixin + # Array of classes which would be checked if they + # suite constructor arguments. Order matters + # @param klasses [Array(Class)] + def buildable_classes=(klasses) + @class_builder = ClassBuilder.new(klasses).freeze + end + + # Default implementation of build_class. Feel free to change + # You also have to override it in buildable_classes + def build_class(*args, &block) + if @class_builder + @class_builder.call(*args, &block) + else + self + end + end + + # Default implementation. + # This method should be used instead of new in all cases + def build(*arguments, &block) + build_class(*arguments, &block).new(*arguments, &block) + end + end + + attr_accessor :klasses + + # @param klasses [Array(Classes)] + def initialize(klasses) + self.klasses = klasses + end + + # @return [Class] first nonzero class returned by .build_class + def call(*arguments, &block) + klasses.each do |klass| + result = klass.build_class(*arguments, &block) + return result if result + end + end + end +end diff --git a/lib/xverifier/verifier.rb b/lib/xverifier/verifier.rb index 18d66d1..a4c6aef 100644 --- a/lib/xverifier/verifier.rb +++ b/lib/xverifier/verifier.rb @@ -8,14 +8,19 @@ module XVerifier # @attr model # Generic object to be verified # @attr messages [Array] - # Array to collect all messages yielded by verifier + # Array with all messages yielded by the verifier class Verifier + autoload :ApplicatorWithOptionsBuilder, + 'xverifier/verifier/applicator_with_options_builder' + attr_accessor :model, :messages # @example with block # verify { |context| message!() if context[:foo] } # @example with proc # verify -> (context) { message!() if context[:foo] } + # @example with hash + # verify -> { message!() } if: { foo: true } # @example cotnext could be ommited from lambda params # verify -> { message!() }, if: -> (context) { context[:foo] } # @example with symbol @@ -36,13 +41,15 @@ class Verifier # call verifier if only block invocation result is falsey # @yield context on `#verfify!` calls # @return [Array] list of all verifiers already defined - def self.verify(verifier = nil, **options, &block) - verifiers << [verifier || block, **options] + def self.verify(*args, &block) + bound_applicators << + ApplicatorWithOptionsBuilder.call(self, *args, &block) end - # @return [Array] List of all verifiers - def self.verifiers - @verifiers ||= [] + # @return [Array(ApplicatorWithOptions)] + # List of applicators, bound by .verify + def self.bound_applicators + @bound_applicators ||= [] end # @param model generic model to validate @@ -58,21 +65,13 @@ def initialize(model) self.messages = [] end - # @todo fix to match style guides # @param context context in which model would be valdiated # @return [Array] list of messages yielded by verifier - def verify!(context = {}) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/LineLength + def verify!(context = {}) self.messages = [] - self.class.verifiers.each do |verifier, options| - next unless Applicator.call(options.fetch(:if, true), self, context) - next if Applicator.call(options.fetch(:unless, false), self, context) - - if verifier.is_a?(Class) && verifier < self.class - messages.concat verifier.call(model, context) - else - Applicator.call(verifier, self, context) - end + self.class.bound_applicators.each do |bound_applicator| + bound_applicator.call(self, context) end messages diff --git a/lib/xverifier/verifier/applicator_with_options_builder.rb b/lib/xverifier/verifier/applicator_with_options_builder.rb new file mode 100644 index 0000000..dec8da2 --- /dev/null +++ b/lib/xverifier/verifier/applicator_with_options_builder.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module XVerifier + class Verifier + # Builds ApplicatorWithOptions from different invocation styles. + # @api private + # @attr base [Class] + # class for which applicator_with_options should be built + # @attr args [Array] + # array of arguments Verifier.verify invoked with + # @attr block [Proc] + # block Verifier.verify invoked with + ApplicatorWithOptionsBuilder = Struct.new(:base, :args, :block) + class ApplicatorWithOptionsBuilder + # @!method self.call(base) + # transforms `verify` arguments to class attributes + # and invokes calculation + # @raise [ArgumentError] + # @return [ApplicatorWithOptions] resulting applicator_with_options + def self.call(base, *args, &block) + new(base, args, block).call + end + + # Tries different invocation styles until one matches + # @see #try_block + # @see #try_nesting + # @see #default + # @raise [ArgumentError] + # @return [ApplicatorWithOptions] resulting applicator_with_options + def call + try_block || try_nesting || default + end + + private + + # @example with options + # verify(if: true) { ... } + # @example without options + # verify { ... } + # @return [ApplicatorWithOptions] + def try_block + ApplicatorWithOptions.new(block, *args) if block + end + + # @example correct + # verify SubVerifier + # @example incorrect + # verify Class + # @raise [ArgumentError] + # @return [ApplicatorWithOptions] + def try_nesting + verifier, *rest = args + return unless verifier.is_a?(Class) + raise ArgumentError, <<~ERROR unless verifier < base + Nested verifiers should be inherited from verifier they nested are in + ERROR + + applicable = lambda do |context| + messages.concat(verifier.call(model, context)) + end + + ApplicatorWithOptions.new(applicable, *rest) + end + + # Simply passes args to ApplicatorWithOptions + # @example + # verify(:foo, unless: false) + # @return [ApplicatorWithOptions] + def default + ApplicatorWithOptions.new(*args) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c0d26fd..0feb44b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,10 +3,20 @@ require 'bundler/setup' require 'pry' +require 'rspec/its' + require 'coveralls' Coveralls.wear! require 'xverifier' RSpec.configure do |config| config.default_formatter = 'doc' if config.files_to_run.one? + + config.mock_with :rspec do |mocks| + mocks.verify_doubled_constant_names = true + end +end + +def applicator(applicable) + XVerifier::Applicator.build(applicable) end diff --git a/spec/xverifier/applicator_spec.rb b/spec/xverifier/applicator_spec.rb new file mode 100644 index 0000000..8a8960b --- /dev/null +++ b/spec/xverifier/applicator_spec.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +describe XVerifier::Applicator do + subject(:applicator) { described_class.build(applicable) } + + let(:binding_class) do + # defining #foo to use it with `instance_double`s + Class.new { define_method(:foo) { |*| } } + end + + let(:binding_) { instance_double(binding_class, :binding, foo: result) } + let(:context) { instance_double(Object, :context) } + let(:result) { instance_double(Object, :result) } + + shared_context 'call(context, binding_)' do + subject(:call!) { applicator.call(binding_, context) } + end + + shared_examples 'its call returns result' do + describe 'call(context, binding_)' do + include_context 'call(context, binding_)' + it { is_expected.to eq(result) } + end + end + + context 'Proxy' do + let(:applicable) { described_class.build(result) } + + it { is_expected.to be_a XVerifier::Applicator::Proxy } + it_behaves_like 'its call returns result' + end + + context 'MethodExtractor' do + let(:applicable) { :foo } + + it { is_expected.to be_a XVerifier::Applicator::MethodExtractor } + + describe 'call(context, binding_)' do + include_context 'call(context, binding_)' + + context 'method on generic object' do + before do + expect(binding_).to receive(:foo).with(context).and_return(result) + end + + it { is_expected.to eq(result) } + end + + context 'method on binding' do + let(:binding_) { binding } + let(:applicable) { :result } + + it { is_expected.to eq(result) } + end + + context 'variable on binding' do + let(:binding_) do + foo = result + binding + end + + it { is_expected.to eq(result) } + end + end + end + + context 'InstanceEvaluator' do + let(:applicable) { 'foo' } + + it { is_expected.to be_a XVerifier::Applicator::InstanceEvaluator } + + describe 'call(context, binding_)' do + shared_examples 'error backtrace points to this file' do + subject(:error) do + begin + call! + rescue => e + e + else + raise 'No error raised' + end + end + + let(:applicable) { 'raise' } + + its(:backtrace) do + expect(error.backtrace[0]).to include(__FILE__) + end + end + + include_context 'call(context, binding_)' + it_behaves_like 'error backtrace points to this file' + it { is_expected.to eq(result) } + + context 'when applicable = context' do + let(:applicable) { 'context' } + + it { is_expected.to eq(context) } + end + + context 'when binding_ is a Binding' do + let(:binding_) { binding } + let(:applicable) { '[result, context]' } + + it_behaves_like 'error backtrace points to this file' + it { is_expected.to eq [result, context] } + + context 'when binding_ does not have "context"' do + let(:binding_) { Object.new.send(:binding) } + let(:applicable) { 'context' } + + it { is_expected.to eq(context) } + end + end + end + end + + context 'ProcApplicatior' do + let(:applicable) { -> { foo } } + + it { is_expected.to be_a XVerifier::Applicator::ProcApplicatior } + + describe 'call(context, binding_)' do + include_context 'call(context, binding_)' + it { is_expected.to eq(result) } + + context 'when applicable requests arg' do + let(:applicable) { ->(context) { [foo, context] } } + + it { is_expected.to eq [result, context] } + end + end + end + + context 'Quoter' do + let(:applicable) { result } + + it { is_expected.to be_a XVerifier::Applicator::Quoter } + it_behaves_like 'its call returns result' + end +end diff --git a/spec/xverifier/applicator_with_options_spec.rb b/spec/xverifier/applicator_with_options_spec.rb new file mode 100644 index 0000000..1589315 --- /dev/null +++ b/spec/xverifier/applicator_with_options_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +describe XVerifier::ApplicatorWithOptions do + subject(:applicator_with_options) do + described_class.new(action, if: if_condition, unless: unless_condition) + end + + let(:action) { instance_double(Object, :action) } + let(:if_condition) { true } + let(:unless_condition) { false } + + let(:binding_) { instance_double(Object, :binding) } + let(:context) { instance_double(Object, :context) } + + describe 'default values' do + subject { described_class.new(action) } + + its(:if_condition) { is_expected.to eq applicator(true) } + its(:unless_condition) { is_expected.to eq applicator(false) } + end + + shared_examples 'it does not call action' do + it 'does not call action' do + expect(applicator_with_options.action).not_to receive(:call) + applicator_with_options.call(binding_, context) + end + end + + it 'calls action' do + expect(applicator_with_options.action) + .to receive(:call).with(binding_, context) + applicator_with_options.call(binding_, context) + end + + context 'if condition resolves to falsey' do + let(:if_condition) { -> { false } } + + it_behaves_like 'it does not call action' + end + + context 'unless condition resolves to truthy' do + let(:unless_condition) { -> { true } } + + it_behaves_like 'it does not call action' + end +end diff --git a/spec/xverifier/class_builder_spec.rb b/spec/xverifier/class_builder_spec.rb new file mode 100644 index 0000000..548f2bf --- /dev/null +++ b/spec/xverifier/class_builder_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +describe XVerifier::ClassBuilder do + let(:klass) do + # This extend is used in all specs for class_double purposes. + # You are not forced to extend mixin if you implement `.build_class` + # in all `klasses` + Class.new { extend XVerifier::ClassBuilder::Mixin } + end + + let(:unbuildable) { class_double(klass, build_class: nil) } + let(:recursive) { class_double(klass, build_class: buildable) } + let(:buildable) { class_double(klass) } + + describe 'XVerifier::ClassBuilder flow' do + subject(:class_builder) { described_class.new([unbuildable, recursive]) } + + its(:call) { is_expected.to eq buildable } + end + + specify 'XVerifier::ClassBuilder::Mixin#build_class' do + klass.buildable_classes = [unbuildable, recursive] + expect(klass.build_class).to eq(buildable) + end +end diff --git a/spec/xverifier/verifier_spec.rb b/spec/xverifier/verifier_spec.rb index 84bab3c..05fdc7c 100644 --- a/spec/xverifier/verifier_spec.rb +++ b/spec/xverifier/verifier_spec.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true describe XVerifier::Verifier do - let(:model) { double(:model) } - subject(:verifier) do Class.new(described_class) do def message!(*args) @@ -11,6 +9,8 @@ def message!(*args) end end + let(:model) { instance_double(Object, :model) } + describe 'verifiers conditioning' do let(:context) { Hash[foo: true] } @@ -36,7 +36,7 @@ def mark(name) end describe 'verifiers invocation' do - let(:context) { double(:context) } + let(:context) { instance_double(Object, :context) } specify 'with subclass' do subclass = Class.new(verifier) @@ -62,10 +62,20 @@ def mark(name) end end - specify 'with #to_proc' do - verifier.verify -> { message!(model) }, if: Hash[context => true] - expect(verifier.call(model, context)).to eq [[model]] - expect(verifier.call(model, double(:new_context))).to be_empty + context 'with #to_proc' do + before do + verifier.verify -> { message!(model) }, if: Hash[context => true] + end + + context 'with same context' do + it { expect(verifier.call(model, context)).to eq [[model]] } + end + + context 'with different contextes' do + let(:new_context) { instance_double(Object, :new_context) } + + it { expect(verifier.call(model, new_context)).to be_empty } + end end specify 'with string' do diff --git a/spec/xverifier_spec.rb b/spec/xverifier_spec.rb new file mode 100644 index 0000000..9332677 --- /dev/null +++ b/spec/xverifier_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +describe XVerifier do + describe 'VERSION' do + it 'is well-formated' do + expect(XVerifier::VERSION).to match(/\A0\.\d+\.\d+\.\d+(_.+)?\z/) + end + + it 'is released' do + pending 'WIP' + expect(XVerifier::VERSION).to match(/\A\d+\.\d+\.\d+(_.+)?\z/) + end + end +end diff --git a/xverifier.gemspec b/xverifier.gemspec index 6e90639..95b60d0 100644 --- a/xverifier.gemspec +++ b/xverifier.gemspec @@ -27,4 +27,6 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'yard', '~> 0.9.8' spec.add_development_dependency 'launchy', '~> 2.4' spec.add_development_dependency 'coveralls', '~> 0.8.19' + spec.add_development_dependency 'rubocop-rspec', '~> 1.15' + spec.add_development_dependency 'rspec-its', '~> 1.1' end