require 'spec_helper' module OperationMatchers class HaveValidParent def matches?(target) stack = [] stack << [ target, target.operands ] if target.respond_to?(:operands) while node = stack.pop @expected, operands = node operands.each do |operand| @target = operand @actual = @target.parent return false unless @actual.equal?(@expected) if @target.respond_to?(:operands) stack << [ @target, @target.operands ] end end end true end def failure_message <<-OUTPUT expected: #{@expected.inspect} got: #{@actual.inspect} OUTPUT end end def have_operands_with_valid_parent HaveValidParent.new end end shared_examples_for 'DataMapper::Query::Conditions::AbstractOperation' do before :all do module ::Blog class Article include DataMapper::Resource property :id, Serial property :title, String, :required => true end end DataMapper.finalize @model = Blog::Article end before do class ::OtherOperation < DataMapper::Query::Conditions::AbstractOperation slug :other end end before do @comparison = DataMapper::Query::Conditions::Comparison.new(:eql, @model.properties[:title], 'A title') @and_operation = DataMapper::Query::Conditions::Operation.new(:and) @or_operation = DataMapper::Query::Conditions::Operation.new(:or) @not_operation = DataMapper::Query::Conditions::Operation.new(:not) @null_operation = DataMapper::Query::Conditions::Operation.new(:null) @other = OtherOperation.new end it { @operation.class.should respond_to(:new) } describe '.new' do describe 'with no arguments' do subject { @operation.class.new } it { should be_kind_of(@operation.class) } end describe 'with arguments' do subject { @operation.class.new(@comparison) } it { should be_kind_of(@operation.class) } end end it { @operation.class.should respond_to(:slug) } describe '.slug' do describe 'with no arguments' do subject { @operation.class.slug } it { should == @slug } end describe 'with an argument' do subject { @operation.class.slug(:other) } it { should == :other } # reset the AndOperation slug after { @operation.class.slug(@slug) } end end it { should respond_to(:==) } describe '#==' do describe 'when the other AbstractOperation is equal' do # artificially modify the object so #== will throw an # exception if the equal? branch is not followed when heckling before { @operation.singleton_class.send(:undef_method, :slug) } subject { @operation == @operation } it { should be(true) } end describe 'when the other AbstractOperation is the same class' do subject { @operation == DataMapper::Query::Conditions::Operation.new(@slug) } it { should be(true) } end describe 'when the other AbstractOperation is a different class, with the same slug' do before { @other.class.slug(@slug) } subject { @operation == @other } it { should be(false) } # reset the OtherOperation slug after { @other.class.slug(:other) } end describe 'when the other AbstractOperation is the same class, with different operands' do subject { @operation == DataMapper::Query::Conditions::Operation.new(@slug, @comparison) } it { should be(false) } end end it { should respond_to(:<<) } describe '#<<' do describe 'with a NullOperation' do subject { @operation << @null_operation } it { should equal(@operation) } it 'should merge the operand' do subject.to_a.should == [ @null_operation.class.new ] end it { should have_operands_with_valid_parent } end describe 'with a duplicate operand' do before { @operation << @comparison.dup } subject { @operation << @comparison.dup } it { should equal(@operation) } it 'should have unique operands' do subject.to_a.should == [ @comparison ] end it { should have_operands_with_valid_parent } end describe 'with an invalid operand' do subject { @operation << '' } it { method(:subject).should raise_error(ArgumentError, '+operand+ should be DataMapper::Query::Conditions::AbstractOperation or DataMapper::Query::Conditions::AbstractComparison or Array, but was String') } end end it { should respond_to(:children) } describe '#children' do subject { @operation.children } it { should be_kind_of(Set) } it { should be_empty } it { should equal(@operation.operands) } end it { should respond_to(:clear) } describe '#clear' do before do @operation << @other @operation.should_not be_empty end subject { @operation.clear } it { should equal(@operation) } it 'should clear the operands' do subject.should be_empty end end [ :difference, :- ].each do |method| it { should respond_to(method) } describe "##{method}" do subject { @operation.send(method, @comparison) } it { should eql(@not_operation.class.new(@comparison)) } end end it { should respond_to(:dup) } describe '#dup' do subject { @operation.dup } it { should_not equal(@operation) } it { subject.to_a.should == @operation.to_a } end it { should respond_to(:each) } describe '#each' do before do @yield = [] @operation << @other end subject { @operation.each { |operand| @yield << operand } } it { should equal(@operation) } it 'should yield to every operand' do subject @yield.should == [ @other ] end end it { should respond_to(:eql?) } describe '#eql?' do describe 'when the other AbstractOperation is equal' do # artificially modify the object so #eql? will throw an # exception if the equal? branch is not followed when heckling before { @operation.singleton_class.send(:undef_method, :slug) } subject { @operation.eql?(@operation) } it { should be(true) } end describe 'when the other AbstractOperation is the same class' do subject { @operation.eql?(DataMapper::Query::Conditions::Operation.new(@slug)) } it { should be(true) } end describe 'when the other AbstractOperation is a different class' do subject { @operation.eql?(DataMapper::Query::Conditions::Operation.new(:other)) } it { should be(false) } end describe 'when the other AbstractOperation is the same class, with different operands' do subject { @operation.eql?(DataMapper::Query::Conditions::Operation.new(@slug, @comparison)) } it { should be(false) } end describe 'when operations contain more than one operand' do before do @operation << DataMapper::Query::Conditions::Operation.new(:and, @comparison, @other) @other = @operation.dup end subject { @operation.eql?(@other) } it { should be(true) } end end it { should respond_to(:hash) } describe '#hash' do describe 'with operands' do before do @operation << @comparison end subject { @operation.hash } it 'should match the same AbstractOperation with the same operands' do should == DataMapper::Query::Conditions::Operation.new(@slug, @comparison.dup).hash end it 'should not match the same AbstractOperation with different operands' do should_not == DataMapper::Query::Conditions::Operation.new(@slug).hash end it 'should not match a different AbstractOperation with the same operands' do should_not == @other.class.new(@comparison.dup).hash end it 'should not match a different AbstractOperation with different operands' do should_not == DataMapper::Query::Conditions::Operation.new(:or).hash end end end [ :intersection, :& ].each do |method| it { should respond_to(method) } describe "##{method}" do subject { @operation.send(method, @other) } it { should eql(@and_operation) } end end it { should respond_to(:merge) } describe '#merge' do describe 'with a NullOperation' do subject { @operation.merge([ @null_operation ]) } it { should equal(@operation) } it 'should merge the operand' do subject.to_a.should == [ @null_operation.class.new ] end it { should have_operands_with_valid_parent } end describe 'with a duplicate operand' do before { @operation << @comparison.dup } subject { @operation.merge([ @comparison.dup ]) } it { should equal(@operation) } it 'should have unique operands' do subject.to_a.should == [ @comparison ] end it { should have_operands_with_valid_parent } end describe 'with an invalid operand' do subject { @operation.merge([ '' ]) } it { method(:subject).should raise_error(ArgumentError) } end end it { should respond_to(:operands) } describe '#operands' do subject { @operation.operands } it { should be_kind_of(Set) } it { should be_empty } it { should equal(@operation.children) } end it { should respond_to(:parent) } describe '#parent' do describe 'when there is no parent' do subject { @operation.parent } it { should be_nil } end describe 'when there is a parent' do before { @other << @operation } subject { @operation.parent } it { should equal(@other) } end end it { should respond_to(:parent=) } describe '#parent=' do subject { @operation.parent = @other } it { should equal(@other) } it 'should change the parent' do method(:subject).should change(@operation, :parent). from(nil). to(@other) end end [ :union, :|, :+ ].each do |method| it { should respond_to(method) } describe "##{method}" do subject { @operation.send(method, @null_operation) } it { should eql(@null_operation) } end end it { should respond_to(:valid?) } describe '#valid?' do subject { @operation.valid? } describe 'with no operands' do it { should be(false) } end describe 'with an operand that responds to #valid?' do describe 'and is valid' do before do @operation << @comparison end it { should be(true) } end describe 'and is not valid' do before do @operation << @or_operation.dup end it { should be(false) } end end describe 'with an operand that does not respond to #valid?' do before do @operation << [ 'raw = 1' ] end it { should be(true) } end end end describe DataMapper::Query::Conditions::Operation do it { DataMapper::Query::Conditions::Operation.should respond_to(:new) } describe '.new' do { :and => DataMapper::Query::Conditions::AndOperation, :or => DataMapper::Query::Conditions::OrOperation, :not => DataMapper::Query::Conditions::NotOperation, :null => DataMapper::Query::Conditions::NullOperation, }.each do |slug, klass| describe "with a slug #{slug.inspect}" do subject { DataMapper::Query::Conditions::Operation.new(slug) } it { should be_kind_of(klass) } it { subject.should be_empty } end end describe 'with an invalid slug' do subject { DataMapper::Query::Conditions::Operation.new(:invalid) } it { method(:subject).should raise_error(ArgumentError, 'No Operation class for :invalid has been defined') } end describe 'with operands' do before { @or_operation = DataMapper::Query::Conditions::Operation.new(:or) } subject { DataMapper::Query::Conditions::Operation.new(:and, @or_operation) } it { should be_kind_of(DataMapper::Query::Conditions::AndOperation) } it 'should set the operands' do subject.to_a.should == [ @or_operation ] end end end end describe DataMapper::Query::Conditions::AndOperation do include OperationMatchers it_should_behave_like 'DataMapper::Query::Conditions::AbstractOperation' before do @operation = @and_operation @slug = @operation.slug end it { should respond_to(:<<) } describe '#<<' do [ DataMapper::Query::Conditions::AndOperation, DataMapper::Query::Conditions::OrOperation, DataMapper::Query::Conditions::NotOperation, ].each do |klass| describe "with an #{klass.name.split('::').last}" do before do @other = klass.new(@comparison) end subject { @operation << @other } it { should equal(@operation) } if klass == DataMapper::Query::Conditions::AndOperation it 'should flatten and merge the operand' do subject.to_a.should == @other.operands.to_a end else it 'should merge the operand' do subject.to_a.should == [ @other ] end end it { should have_operands_with_valid_parent } end end end it { should respond_to(:negated?) } describe '#negated?' do describe 'with a negated parent' do before do @not_operation.class.new(@operation) end subject { @operation.negated? } it { should be(true) } end describe 'with a not negated parent' do before do @or_operation.class.new(@operation) end subject { @operation.negated? } it { should be(false) } end describe 'after memoizing the negation, and switching parents' do before do @or_operation.class.new(@operation) @operation.should_not be_negated @not_operation.class.new(@operation) end subject { @operation.negated? } it { should be(true) } end end it { should respond_to(:matches?) } describe '#matches?' do before do @operation << @comparison << @comparison.class.new(@model.properties[:id], 1) end supported_by :all do describe 'with a matching Hash' do subject { @operation.matches?('title' => 'A title', 'id' => 1) } it { should be(true) } end describe 'with a not matching Hash' do subject { @operation.matches?('title' => 'Not matching', 'id' => 1) } it { should be(false) } end describe 'with a matching Resource' do subject { @operation.matches?(@model.new(:title => 'A title', :id => 1)) } it { should be(true) } end describe 'with a not matching Resource' do subject { @operation.matches?(@model.new(:title => 'Not matching', :id => 1)) } it { should be(false) } end describe 'with a raw condition' do before do @operation = @operation.class.new([ 'title = ?', 'Another title' ]) end subject { @operation.matches?('title' => 'A title', 'id' => 1) } it { should be(true) } end end end it { should respond_to(:minimize) } describe '#minimize' do subject { @operation.minimize } describe 'with one empty operand' do before do @operation << @other end it { should equal(@operation) } it { subject.should be_empty } end describe 'with more than one operation' do before do @operation.merge([ @comparison, @not_operation.class.new(@comparison) ]) end it { should equal(@operation) } it { subject.to_a.should =~ [ @comparison, @not_operation.class.new(@comparison) ] } end describe 'with one non-empty operand' do before do @operation << @comparison end it { should == @comparison } end describe 'with one null operation' do before do @operation << @null_operation end it { should eql(@null_operation) } end describe 'with one null operation and one non-null operation' do before do @operation.merge([ @null_operation, @comparison ]) end it { should eql(@comparison) } end end it { should respond_to(:merge) } describe '#merge' do [ DataMapper::Query::Conditions::AndOperation, DataMapper::Query::Conditions::OrOperation, DataMapper::Query::Conditions::NotOperation, ].each do |klass| describe "with an #{klass.name.split('::').last}" do before do @other = klass.new(@comparison) end subject { @operation.merge([ @other ]) } it { should equal(@operation) } if klass == DataMapper::Query::Conditions::AndOperation it 'should flatten and merge the operand' do subject.to_a.should == @other.operands.to_a end else it 'should merge the operand' do subject.to_a.should == [ @other ] end end it { should have_operands_with_valid_parent } end end end it { should respond_to(:to_s) } describe '#to_s' do describe 'with no operands' do subject { @operation.to_s } it { should eql('') } end describe 'with operands' do before do @not_operation << @comparison.dup @operation << @comparison << @not_operation end subject { @operation.to_s } it { should eql('(NOT(title = "A title") AND title = "A title")') } end end it { should respond_to(:valid?) } describe '#valid?' do describe 'with one valid operand, and one invalid operand' do before do @operation << @comparison @operation << DataMapper::Query::Conditions::Comparison.new(:in, @model.properties[:id], []) end subject { @operation.valid? } it { should be(false) } end describe 'with one invalid operand' do before do @operation << DataMapper::Query::Conditions::Comparison.new(:in, @model.properties[:id], []) end subject { @operation.valid? } it { should be(false) } end end end describe DataMapper::Query::Conditions::OrOperation do include OperationMatchers it_should_behave_like 'DataMapper::Query::Conditions::AbstractOperation' before do @operation = @or_operation @slug = @operation.slug end it { should respond_to(:<<) } describe '#<<' do [ DataMapper::Query::Conditions::AndOperation, DataMapper::Query::Conditions::OrOperation, DataMapper::Query::Conditions::NotOperation, ].each do |klass| describe "with an #{klass.name.split('::').last}" do before do @other = klass.new(@comparison) end subject { @operation << @other } it { should equal(@operation) } if klass == DataMapper::Query::Conditions::OrOperation it 'should flatten and merge the operand' do subject.to_a.should == @other.operands.to_a end else it 'should merge the operand' do subject.to_a.should == [ @other ] end end it { should have_operands_with_valid_parent } end end end it { should respond_to(:negated?) } describe '#negated?' do describe 'with a negated parent' do before do @not_operation.class.new(@operation) end subject { @operation.negated? } it { should be(true) } end describe 'with a not negated parent' do before do @and_operation.class.new(@operation) end subject { @operation.negated? } it { should be(false) } end describe 'after memoizing the negation, and switching parents' do before do @or_operation.class.new(@operation) @operation.should_not be_negated @not_operation.class.new(@operation) end subject { @operation.negated? } it { should be(true) } end end it { should respond_to(:matches?) } describe '#matches?' do before do @operation << @comparison << @comparison.class.new(@model.properties[:id], 1) end supported_by :all do describe 'with a matching Hash' do subject { @operation.matches?('title' => 'A title', 'id' => 2) } it { should be(true) } end describe 'with a not matching Hash' do subject { @operation.matches?('title' => 'Not matching', 'id' => 2) } it { should be(false) } end describe 'with a matching Resource' do subject { @operation.matches?(@model.new(:title => 'A title', :id => 2)) } it { should be(true) } end describe 'with a not matching Resource' do subject { @operation.matches?(@model.new(:title => 'Not matching', :id => 2)) } it { should be(false) } end describe 'with a raw condition' do before do @operation = @operation.class.new([ 'title = ?', 'Another title' ]) end subject { @operation.matches?('title' => 'A title', 'id' => 2) } it { should be(true) } end end end it { should respond_to(:minimize) } describe '#minimize' do subject { @operation.minimize } describe 'with one empty operand' do before do @operation << @other end it { should equal(@operation) } it { subject.should be_empty } end describe 'with more than one operation' do before do @operation.merge([ @comparison, @not_operation.class.new(@comparison) ]) end it { should equal(@operation) } it { subject.to_a.should =~ [ @comparison, @not_operation.class.new(@comparison) ] } end describe 'with one non-empty operand' do before do @operation << @comparison end it { should == @comparison } end describe 'with one null operation' do before do @operation << @null_operation end it { should eql(@null_operation) } end end it { should respond_to(:merge) } describe '#merge' do [ DataMapper::Query::Conditions::AndOperation, DataMapper::Query::Conditions::OrOperation, DataMapper::Query::Conditions::NotOperation, ].each do |klass| describe "with an #{klass.name.split('::').last}" do before do @other = klass.new(@comparison) end subject { @operation.merge([ @other ]) } it { should equal(@operation) } if klass == DataMapper::Query::Conditions::OrOperation it 'should flatten and merge the operand' do subject.to_a.should == @other.operands.to_a end else it 'should merge the operand' do subject.to_a.should == [ @other ] end end it { should have_operands_with_valid_parent } end end end it { should respond_to(:valid?) } describe '#valid?' do describe 'with one valid operand, and one invalid operand' do before do @operation << @comparison @operation << DataMapper::Query::Conditions::Comparison.new(:in, @model.properties[:id], []) end subject { @operation.valid? } it { should be(true) } end describe 'with one invalid operand' do before do @operation << DataMapper::Query::Conditions::Comparison.new(:in, @model.properties[:id], []) end subject { @operation.valid? } it { should be(false) } end end end describe DataMapper::Query::Conditions::NotOperation do include OperationMatchers it_should_behave_like 'DataMapper::Query::Conditions::AbstractOperation' before do @operation = @not_operation @slug = @operation.slug end it { should respond_to(:<<) } describe '#<<' do [ DataMapper::Query::Conditions::AndOperation, DataMapper::Query::Conditions::OrOperation, DataMapper::Query::Conditions::NotOperation, ].each do |klass| describe "with an #{klass.name.split('::').last}" do before do @other = klass.new(@comparison) end subject { @operation << @other } it { should equal(@operation) } it 'should merge the operand' do subject.to_a.should == [ @other ] end it { should have_operands_with_valid_parent } end end describe 'with more than one operand' do subject { @operation << @comparison << @other } it { method(:subject).should raise_error(ArgumentError) } end describe 'with self as an operand' do subject { @operation << @operation } it { method(:subject).should raise_error(ArgumentError, 'cannot append operand to itself') } end end it { should respond_to(:negated?) } describe '#negated?' do describe 'with a negated parent' do before do @not_operation.class.new(@operation) end subject { @operation.negated? } it { should be(false) } end describe 'with a not negated parent' do before do @or_operation.class.new(@operation) end subject { @operation.negated? } it { should be(true) } end describe 'after memoizing the negation, and switching parents' do before do @or_operation.class.new(@operation) @operation.should be_negated @not_operation.class.new(@operation) end subject { @operation.negated? } it { should be(false) } end end it { should respond_to(:matches?) } describe '#matches?' do before do @operation << @comparison.class.new(@model.properties[:id], 1) end supported_by :all do describe 'with a matching Hash' do subject { @operation.matches?('id' => 2) } it { should be(true) } end describe 'with a not matching Hash' do subject { @operation.matches?('id' => 1) } it { should be(false) } end describe 'with a matching Resource' do subject { @operation.matches?(@model.new(:id => 2)) } it { should be(true) } end describe 'with a not matching Hash' do subject { @operation.matches?(@model.new(:id => 1)) } it { should be(false) } end describe 'with a raw condition' do before do @operation = @operation.class.new([ 'title = ?', 'Another title' ]) end subject { @operation.matches?('id' => 2) } it { should be(true) } end end end it { should respond_to(:merge) } describe '#merge' do [ DataMapper::Query::Conditions::AndOperation, DataMapper::Query::Conditions::OrOperation, DataMapper::Query::Conditions::NotOperation, ].each do |klass| describe "with an #{klass.name.split('::').last}" do before do @other = klass.new(@comparison) end subject { @operation.merge([ @other ]) } it { should equal(@operation) } it 'should merge the operand' do subject.to_a.should == [ @other ] end it { should have_operands_with_valid_parent } end end describe 'with more than one operand' do subject { @operation.merge([ @comparison, @other ]) } it { method(:subject).should raise_error(ArgumentError) } end end it { should respond_to(:minimize) } describe '#minimize' do subject { @operation.minimize } describe 'when no operand' do it { should equal(@operation) } end describe 'when operand is a NotOperation' do before do @operation << @not_operation.class.new(@comparison) end it 'should remove the double negative' do should eql(@comparison) end end describe 'when operand is not a NotOperation' do before do @operation << @comparison end it { should equal(@operation) } it { subject.to_a.should == [ @comparison ] } end describe 'when operand is an empty operation' do before do @operation << @and_operation end it { should equal(@operation) } it { subject.should be_empty } end describe 'when operand is an operation containing a Comparison' do before do @operation << @and_operation.class.new(@comparison) end it { should equal(@operation) } it { subject.to_a.should == [ @comparison ] } end end it { should respond_to(:to_s) } describe '#to_s' do describe 'with no operands' do subject { @operation.to_s } it { should eql('') } end describe 'with operands' do before do @operation << @comparison end subject { @operation.to_s } it { should eql('NOT(title = "A title")') } end end it { should respond_to(:valid?) } describe '#valid?' do describe 'with one invalid operand' do before do @operation << @not_operation.class.new( DataMapper::Query::Conditions::Comparison.new(:eql, @model.properties[:id], nil) ) end subject { @operation.valid? } it { should be(false) } end end end describe DataMapper::Query::Conditions::NullOperation do include OperationMatchers before :all do module ::Blog class Article include DataMapper::Resource property :id, Serial property :title, String, :required => true end end @model = Blog::Article end before do @null_operation = DataMapper::Query::Conditions::Operation.new(:null) @operation = @null_operation @slug = @operation.slug end it { should respond_to(:slug) } describe '#slug' do subject { @operation.slug } it { should == :null } end it { should respond_to(:matches?) } describe '#matches?' do describe 'with a Hash' do subject { @operation.matches?({}) } it { should be(true) } end describe 'with a Resource' do subject { @operation.matches?(Blog::Article.new) } it { should be(true) } end describe 'with any other Object' do subject { @operation.matches?(Object.new) } it { should be(false) } end end it { should respond_to(:minimize) } describe '#minimize' do subject { @operation.minimize } it { should equal(@operation) } end it { should respond_to(:valid?) } describe '#valid?' do subject { @operation.valid? } it { should be(true) } end it { should respond_to(:nil?) } describe '#nil?' do subject { @operation.nil? } it { should be(true) } end it { should respond_to(:inspect) } describe '#inspect' do subject { @operation.inspect } it { should == 'nil' } end end