state_validations 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+
19
+ *.swp
20
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 PagerDuty, Inc.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,135 @@
1
+ # StateValidations
2
+
3
+ **Current version:** 0.2.0
4
+
5
+ The StateValidations gem is a collection of two Validators that aid in
6
+ verifying the integrity of a state history association. As defined here, a
7
+ state history is a collection of entries, each with start and end time
8
+ attributes.
9
+
10
+ One validator provided is the aptly-named StateHistoryValidator. This Validator
11
+ examines a state history holistically checking for a variety of issues. These
12
+ issues include:
13
+
14
+ * Overlaps: the time ranges of two consecutive entries overlap
15
+ * Gaps: the time ranges of two consecutive entries are not continuous
16
+ * Self-transitions: two continuous consecutive entries are equivalent in state
17
+ * Simultaneous states: an entry that is not the last entry has not ended (i.e.
18
+ the entry's end time is `nil`)
19
+ * Ended state histories: the last entry has ended (i.e. last entry's end time
20
+ is `nil`)
21
+
22
+ The user of this validator can disable any combination of these checks as he or
23
+ she wishes through the `:allow` option.
24
+
25
+ The other validator provided is the StateHistoryEntryValidator. This Validator
26
+ examines each state history entry on its own, checking for these issues:
27
+
28
+ * Zero-length entries: the entry begins and ends at the same time
29
+ * Ends before starting: the entry ends before it starts (end < start)
30
+
31
+ As with the first validator, the user can disable any combination of these
32
+ checks.
33
+
34
+ ## Installation
35
+
36
+ Add this line to your application's Gemfile:
37
+
38
+ gem 'state_validations'
39
+
40
+ And then execute:
41
+
42
+ $ bundle
43
+
44
+ Or install it yourself as:
45
+
46
+ $ git clone git://github.com/PagerDuty/state-history-validator.git
47
+ $ rake build
48
+ $ gem install pkg/state_validations-(version).gem
49
+
50
+ ## Usage
51
+
52
+ ### Examples
53
+
54
+ #### StateHistoryValidator
55
+ Let's say that we want to check the integrity of `:state_history_entries`.
56
+ This is how we might call the validator:
57
+
58
+ validate_state_history :state_history_entries,
59
+ :self_transition_match => [:state_id],
60
+ :allow => [:gaps]
61
+
62
+ Here, our validator will allow for gaps in the state history entries. It
63
+ will look for self-transitions depending on whether the `:state_id`
64
+ attribute of two consecutive entries match.
65
+
66
+ #### StateHistoryEntryValidator
67
+ Let's say we want to validate an entry, but we don't particularly care
68
+ about zero-entries. In addition, our entry uses the attribute `:begin` to
69
+ indicate its start time attribute. This is how we might use the validator:
70
+
71
+ validate_state_history_entry
72
+ :start => :begin
73
+ :allow => [:zero_duration]
74
+
75
+ Note that we omitted the comma after `validate_state_history_entry`.
76
+
77
+ The StateHistoryEntryValidator requires that the receiving class validate
78
+ for the presence of the `:start` attribute. The `validate_state_history_entry`
79
+ construct, which is the recommended way to call this validator, performs this
80
+ validation automatically.
81
+
82
+ ### Interface
83
+ With these examples in mind, here is the full documentation on every option
84
+ that can be toggled.
85
+
86
+ #### StateHistoryValidator
87
+ * `:start` -- (default = `:start`) Specifies the attribute referring to the
88
+ starting time of each entry
89
+ * `:end` -- (default = `:end`) Specifies the attribute referring to the
90
+ ending time of each entry
91
+ * `:order` -- (default = `#{start} ASC, ISNULL(#{end}) ASC, #{end} ASC`)
92
+ Specifies the SQL order by which the validator will sort all entries in the
93
+ state history. This is essential in ensuring that the validator obtains the
94
+ correct chronological order of entries so that it can properly check for
95
+ issues. By default, the validator sorts in ascending order of starting
96
+ time. If the start times for two entries are equal, it will then sort by
97
+ end times (pushing `nil` ends to the bottom).
98
+ * `:self_transition_match` -- (default = `[]`) Specifies the attributes to
99
+ check two states for equality. This is used when checking for self-transitions.
100
+ If self-transitions are allowed, this option has no effect. If this option
101
+ is left to its default value of an empty array, the validator will not check
102
+ for self-transitions.
103
+ * `:allow` -- (default = `[]`) Specifies issues to be ignored by the validator.
104
+ This is specified as a list of issues. By default, all checks are performed.
105
+ The issues that can be specified are:
106
+ * `:gaps` -- Ignore gaps
107
+ * `:overlaps` -- Ignore overlaps
108
+ * `:self_transitions` -- Ignore two consecutive equal states. As a side
109
+ note, two equal states separated by a gap are not considered to be in
110
+ self-transition.
111
+ * `:active_middle_states` -- Ignore the presence of an active state in the
112
+ middle of a state history. An active state is defined by the state's `:end`
113
+ attribute being `nil`.
114
+ This excludes the final state.
115
+ * `:inactive_end_state` -- Ignore the lack of a final active ending state.
116
+ Using our earlier definition, this means that the last entry does not have
117
+ `nil` as the value of its `:end` attribute.
118
+
119
+ #### StateHistoryEntryValidator
120
+ * `:start` -- (default = `:start`) Specifies the attribute referring to the
121
+ starting time of this entry
122
+ * `:end` -- (default = `:end`) Specifies the attribute referring to the ending
123
+ time of this entry
124
+ * `:allow` -- (default = `[]`) Specifies issues to be ignored by the validator.
125
+ * `:zero_duration` -- Ignore start time equal to end time
126
+ * `:end_before_start` -- Ignore end time less than start time
127
+
128
+
129
+ ## Contributing
130
+
131
+ 1. Fork it
132
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
133
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
134
+ 4. Push to the branch (`git push origin my-new-feature`)
135
+ 5. Create new Pull Request
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rake/testtask'
4
+
5
+ desc 'Run shoulda unit tests'
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'lib'
8
+ t.libs << 'test'
9
+ t.pattern = 'test/**/*_test.rb'
10
+ t.verbose = true
11
+ end
12
+
13
+
14
+ # Thanks, jekyll, for having your Rakefile on Github
15
+
16
+ def name
17
+ @name ||= Dir['*.gemspec'].first.split('.').first
18
+ end
19
+
20
+ def version
21
+ line = File.read(gemspec_file)[/^\s*gem\.version\s*=\s*.*/]
22
+ line.match(/.*gem\.version\s*=\s*['"](.*)['"]/)[1]
23
+ end
24
+
25
+ def gemspec_file
26
+ "#{name}.gemspec"
27
+ end
28
+
29
+ def gem_file
30
+ "#{name}-#{version}.gem"
31
+ end
32
+
33
+ task :build do
34
+ sh "mkdir -p pkg"
35
+ sh "gem build #{gemspec_file}"
36
+ sh "mv #{gem_file} pkg"
37
+ end
38
+
39
+ task :push do
40
+ sh "gem push pkg/#{gem_file}"
41
+ end
@@ -0,0 +1,24 @@
1
+ require 'active_model/validations'
2
+
3
+ module ActiveModel
4
+ module Validations
5
+
6
+ module HelperMethods
7
+ def validate_state_history_entry(options = {})
8
+ start_attr = options[:start] || :start
9
+ validates start_attr, :presence => true
10
+
11
+ validates_with StateHistoryEntryValidator, options
12
+ end
13
+
14
+
15
+ def validate_state_history(association, options = {})
16
+ params = { :association => association }
17
+ params.merge!(options)
18
+ validates_with StateHistoryValidator, params
19
+ end
20
+ end
21
+
22
+ end
23
+ end
24
+
@@ -0,0 +1,40 @@
1
+ module ActiveModel
2
+ module Validations
3
+
4
+ class StateHistoryEntryValidator < ActiveModel::Validator
5
+ def initialize(options = {})
6
+ super
7
+
8
+ @start = options[:start] || :start
9
+ @end = options[:end] || :end
10
+ @allow = options[:allow] || []
11
+ end
12
+
13
+ def validate(record)
14
+ non_zero?(record) unless @allow.include? :zero_duration
15
+ start_before_end?(record) unless @allow.include? :end_before_start
16
+
17
+ return
18
+ end
19
+
20
+ private
21
+
22
+ def non_zero?(record)
23
+ start = record[@start]
24
+ _end = record[@end]
25
+ if start == _end
26
+ record.errors[:base] << "Start time (#{start}) is equal to end (#{_end})"
27
+ end
28
+ end
29
+
30
+ def start_before_end?(record)
31
+ start = record[@start]
32
+ _end = record[@end]
33
+ if !_end.blank? && start > _end
34
+ record.errors[:base] << "Start time (#{start}) is later than end time (#{_end})"
35
+ end
36
+ end
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,118 @@
1
+ module ActiveModel
2
+ module Validations
3
+
4
+ class StateHistoryValidator < ActiveModel::Validator
5
+ def initialize(options = {})
6
+ super
7
+
8
+ @association = options[:association]
9
+
10
+ @start = options[:start] || :start
11
+ @end = options[:end] || :end
12
+ @order = options[:order] || "#{@start.to_s} ASC, ISNULL(#{@end}) ASC, #{@end} ASC"
13
+ @allow = options[:allow] || []
14
+ @match = options[:self_transition_match] || []
15
+ end
16
+
17
+ def validate(record)
18
+ entries = record.send(@association)
19
+ sorted_history = entries.order(@order)
20
+
21
+ return if sorted_history.blank?
22
+
23
+ if !@allow.include? :inactive_end_state
24
+ valid = last_end_nil?(sorted_history, record.errors)
25
+ return unless valid
26
+ end
27
+
28
+ no_self_trans = no_gaps = no_overlaps = no_nils = true
29
+ sorted_history.each_cons(2) do |a|
30
+ this_entry = a[0]
31
+ next_entry = a[1]
32
+
33
+ if !@allow.include? :self_transitions
34
+ no_self_trans = no_self_transitions?(this_entry, next_entry, record.errors)
35
+ end
36
+
37
+ if !@allow.include? :gaps
38
+ no_gaps = no_gaps?(this_entry, next_entry, record.errors)
39
+ end
40
+
41
+ if !@allow.include? :overlaps
42
+ no_overlaps = no_overlaps?(this_entry, next_entry, record.errors)
43
+ end
44
+
45
+ if !@allow.include? :active_middle_states
46
+ no_nils = no_intervening_nils?(this_entry, record.errors)
47
+ end
48
+
49
+ return unless no_self_trans && no_gaps && no_overlaps && no_nils
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def last_end_nil?(history, errors)
56
+ last = history.last
57
+
58
+ if !last[@end].nil?
59
+ errors[:base] << "no nil ending"
60
+ return false
61
+ end
62
+
63
+ true
64
+ end
65
+
66
+ def no_self_transitions?(this_entry, next_entry, errors)
67
+ if matches?(this_entry, next_entry) && this_entry[@end] == next_entry[@start]
68
+ errors[:base] << "self-transition found"
69
+ return false
70
+ end
71
+
72
+ true
73
+ end
74
+
75
+ def no_gaps?(this_entry, next_entry, errors)
76
+ if !this_entry[@end].nil? && next_entry[@start] > this_entry[@end]
77
+ errors[:base] << "Gap found"
78
+ return false
79
+ end
80
+
81
+ true
82
+ end
83
+
84
+ def no_overlaps?(this_entry, next_entry, errors)
85
+ if !this_entry[@end].nil? && next_entry[@start] < this_entry[@end]
86
+ errors[:base] << "Overlap found"
87
+ return false
88
+ end
89
+
90
+ true
91
+ end
92
+
93
+ # This doesn't get invoked on the very last entry, so we should be safe
94
+ def no_intervening_nils?(this_entry, errors)
95
+ if this_entry[@end].blank?
96
+ errors[:base] << "Found nil end in middle of state history"
97
+ return false
98
+ end
99
+
100
+ true
101
+ end
102
+
103
+ # Match two entries based on the specified attributes in @match
104
+ def matches?(entry1, entry2)
105
+ return false if @match.size == 0
106
+
107
+ @match.each do |attr|
108
+ return false if entry1[attr] != entry2[attr]
109
+ end
110
+
111
+ true
112
+ end
113
+
114
+ end
115
+
116
+ end
117
+ end
118
+
@@ -0,0 +1,6 @@
1
+ require 'active_model/validations/state_history_validator'
2
+ require 'active_model/validations/state_history_entry_validator'
3
+ require 'active_model/validations/helper_methods'
4
+
5
+ module StateValidations
6
+ end
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Note to self: the module file in lib/ needs to have the same name as this file
4
+ $:.push File.expand_path("../lib", __FILE__)
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.authors = ["Daniel Ge", "PagerDuty, Inc."]
8
+ gem.email = ["daniel@pagerduty.com"]
9
+ gem.summary = "State History and State History Entry Validators"
10
+ gem.description = "A gem that verifies the integrity of a state history"
11
+ gem.homepage = "https://github.com/PagerDuty/state-history-validator"
12
+
13
+ gem.files = `git ls-files`.split($\)
14
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
15
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
16
+ gem.name = "state_validations"
17
+ gem.require_paths = ["lib"]
18
+ gem.version = "0.2.0"
19
+
20
+ # Dependencies
21
+ gem.add_runtime_dependency "activemodel", ">= 3.0.0"
22
+ gem.add_runtime_dependency "activesupport", ">= 3.0.0"
23
+
24
+ gem.add_development_dependency "rake"
25
+ gem.add_development_dependency "test-unit", "= 2.2"
26
+ gem.add_development_dependency "mocha"
27
+ gem.add_development_dependency "shoulda"
28
+ end
@@ -0,0 +1,8 @@
1
+ require 'test/unit'
2
+ require 'shoulda'
3
+ require 'mocha'
4
+ require 'mocha/integration/test_unit'
5
+ require 'active_support/core_ext'
6
+ require 'active_model'
7
+
8
+ require 'state_validations'
@@ -0,0 +1,45 @@
1
+ require 'test_helper'
2
+
3
+ class HelperMethodsTest < ActiveSupport::TestCase
4
+ context "The helper methods on a perfectly valid state history" do
5
+ setup do
6
+ now = DateTime.new(2011, 2, 3, 5, 8, 13)
7
+ @starts = [now, now + 2.months, now + 3.months, now + 5.months]
8
+ @ends = [now + 2.months, now + 3.months, now + 5.months, nil]
9
+ end
10
+
11
+ should "simply work" do
12
+ record_entries = []
13
+ @starts.each_with_index do |start, i|
14
+ _end = @ends[i]
15
+ id = i
16
+ entry = RecordEntry.new(start, _end, id)
17
+
18
+ assert entry.valid?
19
+
20
+ record_entries << entry
21
+ end
22
+
23
+ record_entries.stubs(:order).with("start ASC, ISNULL(end) ASC, end ASC").returns(record_entries)
24
+ record_entries.stubs(:all).returns(record_entries)
25
+
26
+ record = Record.new(record_entries)
27
+
28
+ assert record.valid?
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ class Record < Struct.new(:record_entries)
35
+ include ActiveModel::Validations
36
+
37
+ validate_state_history :record_entries,
38
+ :self_transition_match => [:id]
39
+ end
40
+
41
+ class RecordEntry < Struct.new(:start, :end, :id)
42
+ include ActiveModel::Validations
43
+
44
+ validate_state_history_entry
45
+ end
@@ -0,0 +1,101 @@
1
+ require 'test_helper'
2
+
3
+ module ActiveModel
4
+ module Validations
5
+
6
+ class StateHistoryEntryValidatorTest < ActiveSupport::TestCase
7
+ context "An \"perfect\" account state history entry" do
8
+ setup do
9
+ @now = DateTime.new(2011, 2, 3, 5, 8, 13)
10
+ end
11
+
12
+ should "be valid by default with no checks disabled" do
13
+ entry = ValidatableEntry.new(@now, @now + 1.days)
14
+
15
+ validator = StateHistoryEntryValidator.new
16
+ validator.validate(entry)
17
+
18
+ assert !entry.errors.any?
19
+ end
20
+
21
+ should "be valid if :start and :end are changed" do
22
+ MockEntry = Struct.new(:begin, :finish)
23
+ MockEntry.class_eval do
24
+ include ActiveModel::Validations
25
+ end
26
+ entry = MockEntry.new(@now, @now + 1.days)
27
+
28
+ validator = StateHistoryEntryValidator.new(
29
+ :start => :begin,
30
+ :end => :finish
31
+ )
32
+ validator.validate(entry)
33
+
34
+ assert !entry.errors.any?
35
+ end
36
+
37
+ should "be valid even if the end is nil" do
38
+ entry = ValidatableEntry.new(@now, nil)
39
+
40
+ validator = StateHistoryEntryValidator.new
41
+ validator.validate(entry)
42
+
43
+ assert !entry.errors.any?
44
+ end
45
+ end
46
+
47
+ context "An account state history entry where start == end" do
48
+ setup do
49
+ @now = DateTime.new(2011, 2, 3, 5, 8, 13)
50
+ @entry = ValidatableEntry.new(@now, @now)
51
+ end
52
+
53
+ should "not be valid with no checks disabled" do
54
+ validator = StateHistoryEntryValidator.new
55
+ validator.validate(@entry)
56
+
57
+ assert @entry.errors.any?
58
+ end
59
+
60
+ should "be valid with :zero_duration allowed" do
61
+ validator = StateHistoryEntryValidator.new(
62
+ :allow => [:zero_duration]
63
+ )
64
+ validator.validate(@entry)
65
+
66
+ assert !@entry.errors.any?
67
+ end
68
+ end
69
+
70
+ context "An account state history entry where start > end" do
71
+ setup do
72
+ @now = DateTime.new(2011, 2, 3, 5, 8, 13)
73
+ @entry = ValidatableEntry.new(@now + 1.days, @now)
74
+ end
75
+
76
+ should "not be valid with no checks disabled" do
77
+ validator = StateHistoryEntryValidator.new
78
+ validator.validate(@entry)
79
+
80
+ assert @entry.errors.any?
81
+ end
82
+
83
+ should "be valid with :end_before_start allowed" do
84
+ validator = StateHistoryEntryValidator.new(
85
+ :allow => [:end_before_start]
86
+ )
87
+ validator.validate(@entry)
88
+
89
+ assert !@entry.errors.any?
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ class ValidatableEntry < Struct.new(:start, :end)
96
+ include ActiveModel::Validations
97
+ end
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,308 @@
1
+ require 'test_helper'
2
+
3
+ module ActiveModel
4
+ module Validations
5
+
6
+ class StateHistoryValidatorTest < ActiveSupport::TestCase
7
+ context "A perfectly valid state history" do
8
+ setup do
9
+ now = DateTime.new(2011, 2, 3, 5, 8, 13)
10
+ @starts = [now, now + 2.months, now + 3.months, now + 5.months]
11
+ @ends = [now + 2.months, now + 3.months, now + 5.months, nil]
12
+ end
13
+
14
+ should "simply be valid with the default options" do
15
+ state_history = mock_state_history(@starts, @ends) do |entry, i|
16
+ entry[:other_id] = i + 1
17
+ end
18
+
19
+ record = Validatable.new(state_history)
20
+ validator = StateHistoryValidator.new(
21
+ :association => :state_history_entries,
22
+ :self_transition_match => [:id, :other_id]
23
+ )
24
+ validator.validate(record)
25
+
26
+ assert !record.errors.any?
27
+ end
28
+
29
+ should "be valid if we only set a different start and end name" do
30
+ MockEntry = Struct.new(:begin, :finish, :id, :other_id)
31
+ state_history = mock_state_history(@starts, @ends, nil,
32
+ {:entry_class => MockEntry,
33
+ :start => :begin,
34
+ :end => :finish}) do |entry, i|
35
+ entry[:other_id] = i + 1
36
+ end
37
+
38
+ record = Validatable.new(state_history)
39
+ validator = StateHistoryValidator.new(
40
+ :association => :state_history_entries,
41
+ :start => :begin,
42
+ :end => :finish,
43
+ :self_transition_match => [:id, :other_id]
44
+ )
45
+ validator.validate(record)
46
+
47
+ assert !record.errors.any?
48
+ end
49
+
50
+ end
51
+
52
+ context "A \"perfect\" state history with a self-transition" do
53
+ setup do
54
+ now = DateTime.new(2011, 2, 3, 5, 8, 13)
55
+ starts = [now, now + 2.months, now + 3.months, now + 5.months]
56
+ ends = [now + 2.months, now + 3.months, now + 5.months, nil]
57
+ ids = [0, 1, 1, 2]
58
+ other_id = [1, 1, 1, 2]
59
+
60
+ state_history = mock_state_history(starts, ends, ids) do |entry, i|
61
+ entry[:other_id] = other_id[i]
62
+ end
63
+
64
+ @record = Validatable.new(state_history)
65
+ end
66
+
67
+ should "be invalid with nothing allowed" do
68
+ validator = StateHistoryValidator.new(
69
+ :association => :state_history_entries,
70
+ :self_transition_match => [:id, :other_id]
71
+ )
72
+ validator.validate(@record)
73
+
74
+ assert @record.errors.any?
75
+ end
76
+
77
+ should "be valid with :self_transitions allowed" do
78
+ validator = StateHistoryValidator.new(
79
+ :association => :state_history_entries,
80
+ :self_transition_match => [:id, :other_id],
81
+ :allow => [:self_transitions]
82
+ )
83
+ validator.validate(@record)
84
+
85
+ assert !@record.errors.any?
86
+ end
87
+
88
+ should "be valid if no :self_transition_match is specified and nothing is allowed" do
89
+ validator = StateHistoryValidator.new(
90
+ :association => :state_history_entries
91
+ )
92
+ validator.validate(@record)
93
+
94
+ assert !@record.errors.any?
95
+ end
96
+ end
97
+
98
+ context "A \"perfect\" state history with a non-nil end" do
99
+ setup do
100
+ now = DateTime.new(2011, 2, 3, 5, 8, 13)
101
+ starts = [now, now + 2.months, now + 3.months, now + 5.months]
102
+ ends = [now + 2.months, now + 3.months, now + 5.months, now + 8.months]
103
+
104
+ @state_history = mock_state_history(starts, ends, nil) {}
105
+
106
+ @record = Validatable.new(@state_history)
107
+ end
108
+
109
+ should "not be valid with nothing allowed" do
110
+ validator = StateHistoryValidator.new(
111
+ :association => :state_history_entries,
112
+ :self_transition_match => [:id]
113
+ )
114
+ validator.validate(@record)
115
+
116
+ assert @record.errors.any?
117
+ end
118
+
119
+ should "be valid with :inactive_end_state allowed" do
120
+ validator = StateHistoryValidator.new(
121
+ :association => :state_history_entries,
122
+ :self_transition_match => [:id],
123
+ :allow => [:inactive_end_state]
124
+ )
125
+ validator.validate(@record)
126
+
127
+ assert !@record.errors.any?
128
+ end
129
+ end
130
+
131
+ context "A state history with overlaps" do
132
+ setup do
133
+ now = DateTime.new(2011, 2, 3, 5, 8, 13)
134
+ starts = [now, now + 2.months, now + 3.months, now + 5.months]
135
+ ends = [now + 2.months, now + 3.months, now + 6.months, nil]
136
+
137
+ @state_history = mock_state_history(starts, ends) {}
138
+
139
+ @record = Validatable.new(@state_history)
140
+ end
141
+
142
+ should "not be valid with nothing allowed" do
143
+ validator = StateHistoryValidator.new(
144
+ :association => :state_history_entries,
145
+ :self_transition_match => [:id]
146
+ )
147
+ validator.validate(@record)
148
+
149
+ assert @record.errors.any?
150
+ end
151
+
152
+ should "be valid with :overlaps allowed" do
153
+ validator = StateHistoryValidator.new(
154
+ :association => :state_history_entries,
155
+ :self_transition_match => [:id],
156
+ :allow => [:overlaps]
157
+ )
158
+ validator.validate(@record)
159
+
160
+ assert !@record.errors.any?
161
+ end
162
+ end
163
+
164
+ context "A state history with gaps" do
165
+ setup do
166
+ now = DateTime.new(2011, 2, 3, 5, 8, 13)
167
+ starts = [now, now + 2.months, now + 3.months, now + 5.months]
168
+ ends = [now + 2.months, now + 3.months, now + 4.months, nil]
169
+
170
+ state_history = mock_state_history(starts, ends) {}
171
+ @record = Validatable.new(state_history)
172
+ end
173
+
174
+ should "not be valid with nothing allowed" do
175
+ validator = StateHistoryValidator.new(
176
+ :association => :state_history_entries,
177
+ :self_transition_match => [:id]
178
+ )
179
+ validator.validate(@record)
180
+
181
+ assert @record.errors.any?
182
+ end
183
+
184
+ should "be valid with :gaps allowed" do
185
+ validator = StateHistoryValidator.new(
186
+ :association => :state_history_entries,
187
+ :self_transition_match => [:id],
188
+ :allow => [:gaps]
189
+ )
190
+ validator.validate(@record)
191
+
192
+ assert !@record.errors.any?
193
+ end
194
+ end
195
+
196
+ context "A state history with nil `:ends` in the middle" do
197
+ setup do
198
+ now = DateTime.new(2011, 2, 3, 5, 8, 13)
199
+ @starts = [now, now + 2.months, now + 3.months, now + 5.months]
200
+ @ends = [now + 2.months, nil, now + 5.months, nil]
201
+
202
+ state_history = mock_state_history(@starts, @ends) {}
203
+ @record = Validatable.new(state_history)
204
+ end
205
+
206
+ should "not be valid with nothing allowed" do
207
+ validator = StateHistoryValidator.new(
208
+ :association => :state_history_entries,
209
+ :self_transition_match => [:id]
210
+ )
211
+ validator.validate(@record)
212
+
213
+ assert @record.errors.any?
214
+ end
215
+
216
+ should "be valid with :active_middle_states allowed" do
217
+ validator = StateHistoryValidator.new(
218
+ :association => :state_history_entries,
219
+ :self_transition_match => [:id],
220
+ :allow => [:active_middle_states]
221
+ )
222
+ validator.validate(@record)
223
+
224
+ assert !@record.errors.any?
225
+ end
226
+ end
227
+
228
+ context "A state history with two consecutive equivalent states separated by a gap" do
229
+ setup do
230
+ now = DateTime.new(2011, 2, 3, 5, 8, 13)
231
+ @starts = [now, now + 2.months]
232
+ @ends = [now + 1.months, nil]
233
+ @id = [0, 0]
234
+
235
+ state_history = mock_state_history(@starts, @ends, @id) {}
236
+
237
+ @record = Validatable.new(state_history)
238
+ end
239
+
240
+ should "not be valid with nothing allowed" do
241
+ validator = StateHistoryValidator.new(
242
+ :association => :state_history_entries,
243
+ :self_transition_match => [:id]
244
+ )
245
+ validator.validate(@record)
246
+
247
+ assert @record.errors.any?
248
+ end
249
+
250
+ should "be valid with :gaps allowed" do
251
+ validator = StateHistoryValidator.new(
252
+ :association => :state_history_entries,
253
+ :self_transition_match => [:id],
254
+ :allow => [:gaps]
255
+ )
256
+ validator.validate(@record)
257
+
258
+ assert !@record.errors.any?
259
+ end
260
+ end
261
+
262
+ private
263
+
264
+ class Validatable < Struct.new(:state_history_entries)
265
+ include ActiveModel::Validations
266
+ end
267
+
268
+ class ValidatableEntry < Struct.new(:start, :end, :id, :other_id)
269
+ end
270
+
271
+ # fake the entries.order(@order).all call
272
+ def state_history_stub(state_history, options = {})
273
+ start_attr = options[:start] || :start
274
+ end_attr = options[:end] || :end
275
+ sort_dir = options[:sort_dir] || "ASC"
276
+
277
+ order_by = options[:order] ||
278
+ "#{start_attr} #{sort_dir}, ISNULL(#{end_attr}) #{sort_dir}, #{end_attr} #{sort_dir}"
279
+
280
+ state_history.stubs(:order).with(order_by).returns(state_history)
281
+ state_history.stubs(:all).returns(state_history)
282
+ end
283
+
284
+ # Set up a state_history with fake objects and then yield to allow further customization
285
+ def mock_state_history(starts, ends, ids = nil, options = {})
286
+ stub_history = (options[:stub_history].nil?) ? true : options[:stub_history]
287
+ entry_class = (options[:entry_class].nil?) ? ValidatableEntry : options[:entry_class]
288
+
289
+ state_history = []
290
+ starts.each_with_index do |start, i|
291
+ _end = ends[i]
292
+ id = (ids.nil?) ? i : ids[i]
293
+ entry = entry_class.new(start, _end, id)
294
+
295
+ yield entry, i
296
+
297
+ state_history << entry
298
+ end
299
+
300
+ state_history_stub(state_history, options) if stub_history
301
+
302
+ state_history
303
+ end
304
+
305
+ end
306
+
307
+ end
308
+ end
metadata ADDED
@@ -0,0 +1,171 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: state_validations
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
11
+ platform: ruby
12
+ authors:
13
+ - Daniel Ge
14
+ - PagerDuty, Inc.
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2012-07-18 00:00:00 Z
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ version_requirements: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ hash: 7
28
+ segments:
29
+ - 3
30
+ - 0
31
+ - 0
32
+ version: 3.0.0
33
+ prerelease: false
34
+ type: :runtime
35
+ name: activemodel
36
+ requirement: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ version_requirements: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 7
44
+ segments:
45
+ - 3
46
+ - 0
47
+ - 0
48
+ version: 3.0.0
49
+ prerelease: false
50
+ type: :runtime
51
+ name: activesupport
52
+ requirement: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ version_requirements: &id003 !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 3
60
+ segments:
61
+ - 0
62
+ version: "0"
63
+ prerelease: false
64
+ type: :development
65
+ name: rake
66
+ requirement: *id003
67
+ - !ruby/object:Gem::Dependency
68
+ version_requirements: &id004 !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - "="
72
+ - !ruby/object:Gem::Version
73
+ hash: 7
74
+ segments:
75
+ - 2
76
+ - 2
77
+ version: "2.2"
78
+ prerelease: false
79
+ type: :development
80
+ name: test-unit
81
+ requirement: *id004
82
+ - !ruby/object:Gem::Dependency
83
+ version_requirements: &id005 !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ hash: 3
89
+ segments:
90
+ - 0
91
+ version: "0"
92
+ prerelease: false
93
+ type: :development
94
+ name: mocha
95
+ requirement: *id005
96
+ - !ruby/object:Gem::Dependency
97
+ version_requirements: &id006 !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ hash: 3
103
+ segments:
104
+ - 0
105
+ version: "0"
106
+ prerelease: false
107
+ type: :development
108
+ name: shoulda
109
+ requirement: *id006
110
+ description: A gem that verifies the integrity of a state history
111
+ email:
112
+ - daniel@pagerduty.com
113
+ executables: []
114
+
115
+ extensions: []
116
+
117
+ extra_rdoc_files: []
118
+
119
+ files:
120
+ - .gitignore
121
+ - Gemfile
122
+ - LICENSE
123
+ - README.md
124
+ - Rakefile
125
+ - lib/active_model/validations/helper_methods.rb
126
+ - lib/active_model/validations/state_history_entry_validator.rb
127
+ - lib/active_model/validations/state_history_validator.rb
128
+ - lib/state_validations.rb
129
+ - state_validations.gemspec
130
+ - test/test_helper.rb
131
+ - test/unit/helper_methods_test.rb
132
+ - test/unit/state_history_entry_validator_test.rb
133
+ - test/unit/state_history_validator_test.rb
134
+ homepage: https://github.com/PagerDuty/state-history-validator
135
+ licenses: []
136
+
137
+ post_install_message:
138
+ rdoc_options: []
139
+
140
+ require_paths:
141
+ - lib
142
+ required_ruby_version: !ruby/object:Gem::Requirement
143
+ none: false
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ hash: 3
148
+ segments:
149
+ - 0
150
+ version: "0"
151
+ required_rubygems_version: !ruby/object:Gem::Requirement
152
+ none: false
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ hash: 3
157
+ segments:
158
+ - 0
159
+ version: "0"
160
+ requirements: []
161
+
162
+ rubyforge_project:
163
+ rubygems_version: 1.8.24
164
+ signing_key:
165
+ specification_version: 3
166
+ summary: State History and State History Entry Validators
167
+ test_files:
168
+ - test/test_helper.rb
169
+ - test/unit/helper_methods_test.rb
170
+ - test/unit/state_history_entry_validator_test.rb
171
+ - test/unit/state_history_validator_test.rb