range_validator 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source :rubygems
2
+
3
+ gem "activemodel", ">= 3.0.0"
4
+ gem "activesupport", ">= 3.0.0"
5
+
6
+ group :development do
7
+ gem "jeweler"
8
+ gem "rspec", ">= 2.5.0"
9
+ gem "ruby-debug"
10
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Chris Baker
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,105 @@
1
+ = range_validator
2
+
3
+ Range validator for ActiveModel
4
+
5
+ validates :field, :range => true
6
+ validates :field, :range => { :overlapping => :other_records }
7
+ validates :field, :range => { :not_overlapping => Proc.new{ |record| record.other_records } }
8
+
9
+ == Examples
10
+
11
+ Suppose we have an Event model and want to ensure its duration is a range.
12
+
13
+ class Event
14
+ include ActiveModel::Validations
15
+ attr_accessor :duration
16
+
17
+ validates :duration, :range => true
18
+ end
19
+
20
+ event = Event.new
21
+ event.valid? # => false
22
+ event.errors # => #<OrderedHash {:duration=>["is not a range"]}>
23
+
24
+ event.duration = 1..10
25
+ event.valid? # => true
26
+
27
+ We can also validate that the range does or does not overlap with other
28
+ records. This is easiest to demonstrate using ActiveRecord. First we create a
29
+ migration for a Meeting model.
30
+
31
+ class CreateMeetings < ActiveRecord::Migration
32
+ def self.up
33
+ create_table :meetings do |t|
34
+ t.datetime :meeting_start
35
+ t.datetime :meeting_end
36
+ t.integer :room
37
+ end
38
+ end
39
+
40
+ def self.down
41
+ drop_table :meetings
42
+ end
43
+ end
44
+
45
+ We can use the range validator to ensure that two meetings cannot be booked in
46
+ the same room at the same time.
47
+
48
+ class Meeting < ActiveRecord::Base
49
+ validates :duration, :range => { :not_overlapping => Proc.new{ |m| Meeting.where(:room => m.room) } }
50
+
51
+ def duration
52
+ meeting_start..meeting_end
53
+ end
54
+ end
55
+
56
+ Now meetings will validate like so:
57
+
58
+ meeting = Meeting.new
59
+ meeting.meeting_start = DateTime.now
60
+ meeting.meeting_end = DateTime.now + 1.hour
61
+ meeting.room = 12
62
+
63
+ meeting.valid? # => true
64
+ meeting.save
65
+
66
+ other_meeting = Meeting.new
67
+ other_meeting.meeting_start = DateTime.now
68
+ other_meeting.meeting_end = DateTime.now + 1.hour
69
+ other_meeting.room = 12
70
+
71
+ other_meeting.valid? # => false
72
+ other_meeting.errors # => #<OrderedHash {:duration=>["overlaps"]}>
73
+
74
+ other_meeting.room = 4
75
+ other_meeting.valid? # => true
76
+
77
+ Constructing complex procs in the validates method can become messy fast. We
78
+ can move that logic into an instance method on the Meeting class and pass
79
+ a symbol to the range validator instead.
80
+
81
+ class Meeting < ActiveRecord::Base
82
+ validates :duration, :range => { :not_overlapping => :meetings_in_same_room }
83
+
84
+ def duration
85
+ meeting_start..meeting_end
86
+ end
87
+
88
+ def meetings_in_same_room
89
+ Meeting.where :room => room
90
+ end
91
+ end
92
+
93
+ == Contributing to range_validator
94
+
95
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
96
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
97
+ * Fork the project
98
+ * Start a feature/bugfix branch
99
+ * Commit and push until you are happy with your contribution
100
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
101
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
102
+
103
+ == Copyright
104
+
105
+ Copyright (c) 2011 Chris Baker. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,45 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ gem.name = "range_validator"
15
+ gem.homepage = "http://github.com/chrisb87/range_validator"
16
+ gem.license = "MIT"
17
+ gem.summary = %Q{ActiveModel validator for ranges}
18
+ gem.description = %Q{ActiveModel validator for ranges}
19
+ gem.email = "baker.chris.3@gmail.com"
20
+ gem.authors = ["Chris Baker"]
21
+ end
22
+ Jeweler::RubygemsDotOrgTasks.new
23
+
24
+ require 'rspec/core'
25
+ require 'rspec/core/rake_task'
26
+ RSpec::Core::RakeTask.new(:spec) do |spec|
27
+ spec.pattern = FileList['spec/**/*_spec.rb']
28
+ end
29
+
30
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
31
+ spec.pattern = 'spec/**/*_spec.rb'
32
+ spec.rcov = true
33
+ end
34
+
35
+ task :default => :spec
36
+
37
+ require 'rake/rdoctask'
38
+ Rake::RDocTask.new do |rdoc|
39
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
40
+
41
+ rdoc.rdoc_dir = 'rdoc'
42
+ rdoc.title = "range_validator #{version}"
43
+ rdoc.rdoc_files.include('README*')
44
+ rdoc.rdoc_files.include('lib/**/*.rb')
45
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.1
@@ -0,0 +1,79 @@
1
+ require 'active_model'
2
+ require 'active_support/core_ext'
3
+ require 'active_support/i18n'
4
+
5
+ I18n.load_path += Dir[File.expand_path(File.join(File.dirname(__FILE__), '../locales', '*.yml')).to_s]
6
+
7
+ module ActiveModel
8
+
9
+ # == Active Model Range Validator
10
+ module Validations
11
+ class RangeValidator < ActiveModel::EachValidator
12
+ OPTIONS = [:overlapping, :not_overlapping].freeze
13
+
14
+ def check_validity!
15
+ options.each do |option, option_value|
16
+ next if option_value.is_a?(Symbol) || option_value.is_a?(Proc)
17
+ raise ArgumentError, ":#{option} must be a symbol or a proc"
18
+ end
19
+ end
20
+
21
+ def validate_each(record, attribute, value)
22
+ unless value.is_a? Range
23
+ record.errors.add(attribute, :not_a_range)
24
+ return
25
+ end
26
+
27
+ options.slice(*OPTIONS).each do |option, option_value|
28
+ other_records = retrieve_other_records(record, option_value)
29
+
30
+ if option == :overlapping && other_records.blank?
31
+ record.errors.add(attribute, :no_overlap)
32
+ end
33
+
34
+ other_records.each do |other_record|
35
+ overlap = value.overlaps? other_record.send(attribute)
36
+
37
+ if option == :overlapping && !overlap
38
+ record.errors.add(attribute, :no_overlap)
39
+ elsif option == :not_overlapping && overlap
40
+ record.errors.add(attribute, :overlap)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ protected
47
+
48
+ def retrieve_other_records(record, lookup)
49
+ if lookup.is_a?(Symbol)
50
+ other_records = record.send(lookup)
51
+ elsif lookup.is_a?(Proc)
52
+ other_records = lookup.call(record)
53
+ end
54
+
55
+ responds_to_key = record.respond_to?(:to_key) && !record.to_key.blank?
56
+
57
+ (other_records || []).reject do |other_record|
58
+ other_record.equal?(record) || (responds_to_key && other_record.to_key == record.to_key)
59
+ end
60
+ end
61
+ end
62
+
63
+ module HelperMethods
64
+ # Validates that the specified attributes are valid ranges and, optionally, that they do
65
+ # or do not overlap with ranges in other models. Examples:
66
+ #
67
+ # validates :field, :range => true
68
+ # validates :field, :range => { :overlapping => Proc.new{ |record| record.other_records } }
69
+ # validates :field, :range => { :not_overlapping => :other_records }
70
+ #
71
+ # When passing a symbol to :overlapping or :not_overlapping, the object must respond_to that
72
+ # message with a (possibly empty) list of objects that have the same fields.
73
+ #
74
+ def validates_range_of(*attr_names)
75
+ validates_with RangeValidator, _merge_attributes(attr_names)
76
+ end
77
+ end
78
+ end
79
+ end
data/locales/en.yml ADDED
@@ -0,0 +1,6 @@
1
+ en:
2
+ errors:
3
+ messages:
4
+ not_a_range: "is not a range"
5
+ overlap: "overlaps"
6
+ no_overlap: "does not overlap"
@@ -0,0 +1,67 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{range_validator}
8
+ s.version = "0.2.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Chris Baker"]
12
+ s.date = %q{2011-03-25}
13
+ s.description = %q{ActiveModel validator for ranges}
14
+ s.email = %q{baker.chris.3@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".rspec",
21
+ "Gemfile",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "lib/range_validator.rb",
27
+ "locales/en.yml",
28
+ "range_validator.gemspec",
29
+ "spec/range_validator_spec.rb",
30
+ "spec/spec_helper.rb"
31
+ ]
32
+ s.homepage = %q{http://github.com/chrisb87/range_validator}
33
+ s.licenses = ["MIT"]
34
+ s.require_paths = ["lib"]
35
+ s.rubygems_version = %q{1.3.7}
36
+ s.summary = %q{ActiveModel validator for ranges}
37
+ s.test_files = [
38
+ "spec/range_validator_spec.rb",
39
+ "spec/spec_helper.rb"
40
+ ]
41
+
42
+ if s.respond_to? :specification_version then
43
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
44
+ s.specification_version = 3
45
+
46
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
47
+ s.add_runtime_dependency(%q<activemodel>, [">= 3.0.0"])
48
+ s.add_runtime_dependency(%q<activesupport>, [">= 3.0.0"])
49
+ s.add_development_dependency(%q<jeweler>, [">= 0"])
50
+ s.add_development_dependency(%q<rspec>, [">= 2.5.0"])
51
+ s.add_development_dependency(%q<ruby-debug>, [">= 0"])
52
+ else
53
+ s.add_dependency(%q<activemodel>, [">= 3.0.0"])
54
+ s.add_dependency(%q<activesupport>, [">= 3.0.0"])
55
+ s.add_dependency(%q<jeweler>, [">= 0"])
56
+ s.add_dependency(%q<rspec>, [">= 2.5.0"])
57
+ s.add_dependency(%q<ruby-debug>, [">= 0"])
58
+ end
59
+ else
60
+ s.add_dependency(%q<activemodel>, [">= 3.0.0"])
61
+ s.add_dependency(%q<activesupport>, [">= 3.0.0"])
62
+ s.add_dependency(%q<jeweler>, [">= 0"])
63
+ s.add_dependency(%q<rspec>, [">= 2.5.0"])
64
+ s.add_dependency(%q<ruby-debug>, [">= 0"])
65
+ end
66
+ end
67
+
@@ -0,0 +1,117 @@
1
+ require 'range_validator'
2
+ require 'spec_helper'
3
+
4
+ describe "RangeValidator" do
5
+
6
+ before(:each) do
7
+ TestModel.reset_callbacks(:validate)
8
+ @ranges = { :january => Date.civil(2011,1,1)..Date.civil(2011,1,31),
9
+ :february => Date.civil(2011,2,1)..Date.civil(2011,2,28),
10
+ :january_to_february => Date.civil(2011,1,1)..Date.civil(2011,2,28)}
11
+ end
12
+
13
+ it "should be valid when the field is a range" do
14
+ TestModel.validates :duration, :range => true
15
+ record = TestModel.new({:duration => 0..1})
16
+
17
+ record.should be_valid
18
+ end
19
+
20
+ it "should not be valid when the field is not a range" do
21
+ TestModel.validates :name, :range => true
22
+ record = TestModel.new({:name => "name"})
23
+
24
+ record.should_not be_valid
25
+ record.errors.should == {:name => ["is not a range"]}
26
+ end
27
+
28
+ describe "overlapping" do
29
+
30
+ before(:each) do
31
+ TestModel.validates :date_range, :range => { :overlapping => :other_records }
32
+ end
33
+
34
+ it "should be valid when there is overlap" do
35
+ record1 = TestModel.new({:date_range => @ranges[:january]})
36
+ record2 = TestModel.new({:date_range => @ranges[:january_to_february], :other_records => [record1]})
37
+
38
+ record2.should be_valid
39
+ end
40
+
41
+ it "should not be valid when there is no overlap" do
42
+ record1 = TestModel.new({:date_range => @ranges[:january]})
43
+ record2 = TestModel.new({:date_range => @ranges[:february], :other_records => [record1]})
44
+
45
+ record2.should_not be_valid
46
+ record2.errors.should == {:date_range => ["does not overlap"]}
47
+ end
48
+
49
+ it "should not be valid when overlapping an empty set" do
50
+ record = TestModel.new({:date_range => @ranges[:january], :other_records => []})
51
+ record.should_not be_valid
52
+ record.errors.should == {:date_range => ["does not overlap"]}
53
+ end
54
+
55
+ it "should not be valid when overlapping nil" do
56
+ record = TestModel.new({:date_range => @ranges[:january], :other_records => nil})
57
+ record.should_not be_valid
58
+ record.errors.should == {:date_range => ["does not overlap"]}
59
+ end
60
+
61
+ it "should throw an error when given a bad argument" do
62
+ expect {
63
+ TestModel.validates :duration, :range => { :overlapping => nil }
64
+ }.to raise_error ArgumentError, ":overlapping must be a symbol or a proc"
65
+ end
66
+
67
+ end
68
+
69
+ describe "not_overlapping" do
70
+
71
+ before(:each) do
72
+ TestModel.validates :date_range, :range => { :not_overlapping => :other_records }
73
+ end
74
+
75
+ it "should be valid when there is no overlap" do
76
+ record1 = TestModel.new({:date_range => @ranges[:january]})
77
+ record2 = TestModel.new({:date_range => @ranges[:february], :other_records => [record1]})
78
+
79
+ record2.should be_valid
80
+ end
81
+
82
+ it "should not be valid when there is an overlap" do
83
+ record1 = TestModel.new({:date_range => @ranges[:january]})
84
+ record2 = TestModel.new({:date_range => @ranges[:january_to_february], :other_records => [record1]})
85
+
86
+ record2.should_not be_valid
87
+ record2.errors.should == {:date_range => ["overlaps"]}
88
+ end
89
+
90
+ it "should be valid when overlapping nil" do
91
+ record = TestModel.new({:date_range => @ranges[:january], :other_records => nil})
92
+ record.should be_valid
93
+ end
94
+
95
+ it "should be valid when overlapping an empty set" do
96
+ record = TestModel.new({:date_range => @ranges[:january], :other_records => []})
97
+ record.should be_valid
98
+ end
99
+
100
+ it "should not detect overlap with other records with the same object_id" do
101
+ record = TestModel.new({:date_range => @ranges[:january]})
102
+ record.other_records = [record]
103
+
104
+ record.should be_valid
105
+ end
106
+
107
+ it "should not detect overlap with other records with the same to_key value" do
108
+ record1 = TestModel.new({:date_range => @ranges[:january], :to_key => [1]})
109
+ record2 = TestModel.new({:date_range => @ranges[:january], :to_key => [1]})
110
+ record2.other_records = [record1]
111
+
112
+ record2.should be_valid
113
+ end
114
+
115
+ end
116
+
117
+ end
@@ -0,0 +1,7 @@
1
+ require 'ostruct'
2
+ require 'active_model'
3
+
4
+ class TestModel < OpenStruct
5
+ include ActiveModel::Validations
6
+ end
7
+
metadata ADDED
@@ -0,0 +1,154 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: range_validator
3
+ version: !ruby/object:Gem::Version
4
+ hash: 21
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 1
10
+ version: 0.2.1
11
+ platform: ruby
12
+ authors:
13
+ - Chris Baker
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-03-25 00:00:00 -07:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ type: :runtime
23
+ prerelease: false
24
+ name: activemodel
25
+ version_requirements: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ hash: 7
31
+ segments:
32
+ - 3
33
+ - 0
34
+ - 0
35
+ version: 3.0.0
36
+ requirement: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ type: :runtime
39
+ prerelease: false
40
+ name: activesupport
41
+ version_requirements: &id002 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ hash: 7
47
+ segments:
48
+ - 3
49
+ - 0
50
+ - 0
51
+ version: 3.0.0
52
+ requirement: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ type: :development
55
+ prerelease: false
56
+ name: jeweler
57
+ version_requirements: &id003 !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ hash: 3
63
+ segments:
64
+ - 0
65
+ version: "0"
66
+ requirement: *id003
67
+ - !ruby/object:Gem::Dependency
68
+ type: :development
69
+ prerelease: false
70
+ name: rspec
71
+ version_requirements: &id004 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ hash: 27
77
+ segments:
78
+ - 2
79
+ - 5
80
+ - 0
81
+ version: 2.5.0
82
+ requirement: *id004
83
+ - !ruby/object:Gem::Dependency
84
+ type: :development
85
+ prerelease: false
86
+ name: ruby-debug
87
+ version_requirements: &id005 !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ hash: 3
93
+ segments:
94
+ - 0
95
+ version: "0"
96
+ requirement: *id005
97
+ description: ActiveModel validator for ranges
98
+ email: baker.chris.3@gmail.com
99
+ executables: []
100
+
101
+ extensions: []
102
+
103
+ extra_rdoc_files:
104
+ - LICENSE
105
+ - README.rdoc
106
+ files:
107
+ - .rspec
108
+ - Gemfile
109
+ - LICENSE
110
+ - README.rdoc
111
+ - Rakefile
112
+ - VERSION
113
+ - lib/range_validator.rb
114
+ - locales/en.yml
115
+ - range_validator.gemspec
116
+ - spec/range_validator_spec.rb
117
+ - spec/spec_helper.rb
118
+ has_rdoc: true
119
+ homepage: http://github.com/chrisb87/range_validator
120
+ licenses:
121
+ - MIT
122
+ post_install_message:
123
+ rdoc_options: []
124
+
125
+ require_paths:
126
+ - lib
127
+ required_ruby_version: !ruby/object:Gem::Requirement
128
+ none: false
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ hash: 3
133
+ segments:
134
+ - 0
135
+ version: "0"
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ hash: 3
142
+ segments:
143
+ - 0
144
+ version: "0"
145
+ requirements: []
146
+
147
+ rubyforge_project:
148
+ rubygems_version: 1.3.7
149
+ signing_key:
150
+ specification_version: 3
151
+ summary: ActiveModel validator for ranges
152
+ test_files:
153
+ - spec/range_validator_spec.rb
154
+ - spec/spec_helper.rb