diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..a369321c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ +bundler_args: --without yard guard metrics benchmarks +branches: + only: + - /^release-.*$/ +script: "bundle exec rake spec" +rvm: + - ree + - 1.8.7 + - 1.9.2 + - 1.9.3 + - 2.0.0 + - ruby-head + - jruby-18mode + - jruby-19mode + - jruby-head + - ree + - rbx-18mode + - rbx-19mode +env: + - "GIT_BRANCH=release-1.2" +notifications: + irc: "irc.freenode.org#datamapper" + email: + - dan.kubb@gmail.com +matrix: + allow_failures: + - rvm: rbx-18mode + - rvm: rbx-19mode diff --git a/lib/dm-validations.rb b/lib/dm-validations.rb index 7b9b83c7..0a0a5300 100644 --- a/lib/dm-validations.rb +++ b/lib/dm-validations.rb @@ -159,6 +159,61 @@ def #{name} # def valid_for_signup? end end + # Create a new validator of the given klazz and push it onto the + # requested context for each of the attributes in the fields list + # @param [Hash] opts + # Options supplied to validation macro, example: + # {:context=>:default, :maximum=>50, :allow_nil=>true, :message=>nil} + # + # @param [Array] fields + # Fields given to validation macro, example: + # [:first_name, :last_name] in validates_presence_of :first_name, :last_name + # + # @param [Class] klazz + # Validator class, example: DataMapper::Validations::LengthValidator + def add_validator_to_context(opts, fields, validator_class) + fields.each do |field| + validator = validator_class.new(field, opts.dup) + + opts[:context].each do |context| + validator_contexts = validators.context(context) + next if validator_contexts.include?(validator) + validator_contexts << validator + create_context_instance_methods(context) + end + end + end + + # Automatically adds a validation to all associations so that validation + # on parent models will return false when children are invalid. This + # matches the behaviour of #save, which returns false if child models + # cannot be saved. + # + # TODO: Validations are only run on dirty models (which includes new models), + # since clean ones are assumed to be valid. This prevents validations + # from cascading down to nested child models when not required. + def has(cardinality, property, *args) + super.tap do + next if @disable_auto_validations + + add_validation = lambda do |conditional| + validates_with_block property do + value = send(property) + if conditional[value] + [false, "#{property.to_s.capitalize} must be valid"] + else + true + end + end + end + + if cardinality == 1 || (cardinality.is_a?(Range) && cardinality.max == 1) + add_validation[lambda {|val| val && val.dirty? && !val.valid? }] + else + add_validation[lambda {|val| val.loaded? && !val.map(&:valid?).all? }] + end + end + end end # module ClassMethods end # module Validations diff --git a/lib/dm-validations/validators/block_validator.rb b/lib/dm-validations/validators/block_validator.rb index eab8bd43..f5ddff86 100644 --- a/lib/dm-validations/validators/block_validator.rb +++ b/lib/dm-validations/validators/block_validator.rb @@ -50,7 +50,7 @@ def validates_with_block(*fields, &block) method_name = "__validates_with_block_#{@__validates_with_block_count}".to_sym define_method(method_name, &block) - options = fields.last.is_a?(Hash) ? fields.last.pop.dup : {} + options = fields.last.is_a?(Hash) ? fields.pop.dup : {} options[:method] = method_name fields = [method_name] if fields.empty? diff --git a/spec/fixtures/company.rb b/spec/fixtures/company.rb index 4c072aad..ace0fc43 100644 --- a/spec/fixtures/company.rb +++ b/spec/fixtures/company.rb @@ -60,6 +60,23 @@ class ProductCompany < Company validates_presence_of :title, :message => "Product company must have a name" validates_presence_of :flagship_product + + has n, :products, :child_key => [:company_id] + has 1, :profile + has 0..1, :alternate_profile, :model => "Profile" + + without_auto_validations do + has 0..1, :yet_another_profile, :model => "Profile" + end + end + + class Profile + include DataMapper::Resource + + property :id, Serial + belongs_to :product_company + property :description, Text, :required => false # Allow NULL values, enforce validation in the app + validates_presence_of :description end class Product diff --git a/spec/fixtures/g3_concert.rb b/spec/fixtures/g3_concert.rb index 0cc55787..9d328d86 100644 --- a/spec/fixtures/g3_concert.rb +++ b/spec/fixtures/g3_concert.rb @@ -14,13 +14,13 @@ class G3Concert # Attributes # - attr_accessor :year, :participants, :city + attr_accessor :year, :participants, :city, :planned # # Validations # - validates_with_block :participants do + validates_with_block :participants, :unless => :planned do if self.class.known_performances.any? { |perf| perf == self } true else diff --git a/spec/integration/block_validator/block_validator_spec.rb b/spec/integration/block_validator/block_validator_spec.rb index 2db80eba..2b747ebf 100644 --- a/spec/integration/block_validator/block_validator_spec.rb +++ b/spec/integration/block_validator/block_validator_spec.rb @@ -29,4 +29,13 @@ it_should_behave_like "valid model" end + + describe "planned concert for non-existing year/participants/city combinations" do + before :all do + @model.planned = true + @model.year = 2021 + end + + it_should_behave_like "valid model" + end end diff --git a/spec/integration/datamapper_models/association_validation_spec.rb b/spec/integration/datamapper_models/association_validation_spec.rb index 97a536e2..06984613 100644 --- a/spec/integration/datamapper_models/association_validation_spec.rb +++ b/spec/integration/datamapper_models/association_validation_spec.rb @@ -27,3 +27,70 @@ end end end + +describe 'DataMapper::Validations::Fixtures::ProductCompany' do + before :all do + @model = DataMapper::Validations::Fixtures::ProductCompany.create(:title => "Apple", :flagship_product => "Macintosh") + end + + describe 'with no products loaded' do + it 'does not load or validate it' do + @model.reload + @model.valid? + @model.products.should_not be_loaded + end + end + + describe 'with no profile loaded' do + it 'does not load or validate it' do + pending "Unsure how to test this" + @model.reload + @model.valid? + @model.profile.should_not be_loaded + end + end + + describe 'with not dirty profile' do + before :all do + # Force an invalid, yet clean model. This should not happen in real + # code, but gives us an easy way to check whether the validations + # are getting run on the profile. + profile = DataMapper::Validations::Fixtures::Profile.create!(:product_company => @model) + @model.reload + end + + it_should_behave_like "valid model" + end + + describe 'with invalid products' do + before :all do + @model.products = [DataMapper::Validations::Fixtures::Product.new] + end + + it_should_behave_like "invalid model" + + it "has a meaningful error message" do + @model.errors.on(:products).should == [ 'Products must be valid' ] + end + end + + describe 'with invalid profile' do + before :all do + @model.profile = DataMapper::Validations::Fixtures::Profile.new + end + + it_should_behave_like "invalid model" + + it "has a meaningful error message" do + @model.errors.on(:profile).should == [ 'Profile must be valid' ] + end + end + + describe 'with invalid yet_another_profile' do + before :all do + @model.yet_another_profile = DataMapper::Validations::Fixtures::Profile.new + end + + it_should_behave_like "valid model" + end +end