active_repository 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,196 @@
1
+ module ActiveModel
2
+ module Validations
3
+ class UniquenessValidator < ActiveModel::EachValidator #:nodoc:
4
+ def initialize(options)
5
+ super(options.reverse_merge(:case_sensitive => true))
6
+ end
7
+
8
+ # Unfortunately, we have to tie Uniqueness validators to a class.
9
+ def setup(klass)
10
+ @klass = klass
11
+ end
12
+
13
+ def validate_each(record, attribute, value)
14
+ finder_class = record.class.get_model_class
15
+
16
+ finder_class.all.each do |object|
17
+ if object.id != record.id && object.send(attribute) == record.send(attribute)
18
+ record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope, :conditions).merge(:value => value))
19
+ break
20
+ end
21
+ end
22
+ end
23
+
24
+ protected
25
+
26
+ # The check for an existing value should be run from a class that
27
+ # isn't abstract. This means working down from the current class
28
+ # (self), to the first non-abstract class. Since classes don't know
29
+ # their subclasses, we have to build the hierarchy between self and
30
+ # the record's class.
31
+ def find_finder_class_for(record) #:nodoc:
32
+ class_hierarchy = [record.class]
33
+
34
+ while class_hierarchy.first != @klass
35
+ class_hierarchy.prepend(class_hierarchy.first.superclass)
36
+ end
37
+
38
+ class_hierarchy.detect { |klass| klass.respond_to?(:abstract_class?) ? !klass.abstract_class? : true }
39
+ end
40
+
41
+ def build_relation(klass, table, attribute, value) #:nodoc:
42
+ reflection = klass.reflect_on_association(attribute)
43
+ if reflection
44
+ column = klass.columns_hash[reflection.foreign_key]
45
+ attribute = reflection.foreign_key
46
+ value = value.attributes[reflection.primary_key_column.name]
47
+ else
48
+ column = klass.columns_hash[attribute.to_s]
49
+ end
50
+ value = column.limit ? value.to_s[0, column.limit] : value.to_s if !value.nil? && column.text?
51
+
52
+ if !options[:case_sensitive] && value && column.text?
53
+ # will use SQL LOWER function before comparison, unless it detects a case insensitive collation
54
+ relation = klass.connection.case_insensitive_comparison(table, attribute, column, value)
55
+ else
56
+ value = klass.connection.case_sensitive_modifier(value) unless value.nil?
57
+ relation = table[attribute].eq(value)
58
+ end
59
+
60
+ relation
61
+ end
62
+ end
63
+
64
+ module ClassMethods
65
+ # Validates whether the value of the specified attributes are unique
66
+ # across the system. Useful for making sure that only one user
67
+ # can be named "davidhh".
68
+ #
69
+ # class Person < ActiveRecord::Base
70
+ # validates_uniqueness_of :user_name
71
+ # end
72
+ #
73
+ # It can also validate whether the value of the specified attributes are
74
+ # unique based on a <tt>:scope</tt> parameter:
75
+ #
76
+ # class Person < ActiveRecord::Base
77
+ # validates_uniqueness_of :user_name, scope: :account_id
78
+ # end
79
+ #
80
+ # Or even multiple scope parameters. For example, making sure that a
81
+ # teacher can only be on the schedule once per semester for a particular
82
+ # class.
83
+ #
84
+ # class TeacherSchedule < ActiveRecord::Base
85
+ # validates_uniqueness_of :teacher_id, scope: [:semester_id, :class_id]
86
+ # end
87
+ #
88
+ # It is also possible to limit the uniqueness constraint to a set of
89
+ # records matching certain conditions. In this example archived articles
90
+ # are not being taken into consideration when validating uniqueness
91
+ # of the title attribute:
92
+ #
93
+ # class Article < ActiveRecord::Base
94
+ # validates_uniqueness_of :title, conditions: where('status != ?', 'archived')
95
+ # end
96
+ #
97
+ # When the record is created, a check is performed to make sure that no
98
+ # record exists in the database with the given value for the specified
99
+ # attribute (that maps to a column). When the record is updated,
100
+ # the same check is made but disregarding the record itself.
101
+ #
102
+ # Configuration options:
103
+ #
104
+ # * <tt>:message</tt> - Specifies a custom error message (default is:
105
+ # "has already been taken").
106
+ # * <tt>:scope</tt> - One or more columns by which to limit the scope of
107
+ # the uniqueness constraint.
108
+ # * <tt>:conditions</tt> - Specify the conditions to be included as a
109
+ # <tt>WHERE</tt> SQL fragment to limit the uniqueness constraint lookup
110
+ # (e.g. <tt>conditions: where('status = ?', 'active')</tt>).
111
+ # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by
112
+ # non-text columns (+true+ by default).
113
+ # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the
114
+ # attribute is +nil+ (default is +false+).
115
+ # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the
116
+ # attribute is blank (default is +false+).
117
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
118
+ # if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
119
+ # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
120
+ # proc or string should return or evaluate to a +true+ or +false+ value.
121
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to
122
+ # determine if the validation should ot occur (e.g. <tt>unless: :skip_validation</tt>,
123
+ # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
124
+ # method, proc or string should return or evaluate to a +true+ or +false+
125
+ # value.
126
+ #
127
+ # === Concurrency and integrity
128
+ #
129
+ # Using this validation method in conjunction with ActiveRecord::Base#save
130
+ # does not guarantee the absence of duplicate record insertions, because
131
+ # uniqueness checks on the application level are inherently prone to race
132
+ # conditions. For example, suppose that two users try to post a Comment at
133
+ # the same time, and a Comment's title must be unique. At the database-level,
134
+ # the actions performed by these users could be interleaved in the following manner:
135
+ #
136
+ # User 1 | User 2
137
+ # ------------------------------------+--------------------------------------
138
+ # # User 1 checks whether there's |
139
+ # # already a comment with the title |
140
+ # # 'My Post'. This is not the case. |
141
+ # SELECT * FROM comments |
142
+ # WHERE title = 'My Post' |
143
+ # |
144
+ # | # User 2 does the same thing and also
145
+ # | # infers that his title is unique.
146
+ # | SELECT * FROM comments
147
+ # | WHERE title = 'My Post'
148
+ # |
149
+ # # User 1 inserts his comment. |
150
+ # INSERT INTO comments |
151
+ # (title, content) VALUES |
152
+ # ('My Post', 'hi!') |
153
+ # |
154
+ # | # User 2 does the same thing.
155
+ # | INSERT INTO comments
156
+ # | (title, content) VALUES
157
+ # | ('My Post', 'hello!')
158
+ # |
159
+ # | # ^^^^^^
160
+ # | # Boom! We now have a duplicate
161
+ # | # title!
162
+ #
163
+ # This could even happen if you use transactions with the 'serializable'
164
+ # isolation level. The best way to work around this problem is to add a unique
165
+ # index to the database table using
166
+ # ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the
167
+ # rare case that a race condition occurs, the database will guarantee
168
+ # the field's uniqueness.
169
+ #
170
+ # When the database catches such a duplicate insertion,
171
+ # ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid
172
+ # exception. You can either choose to let this error propagate (which
173
+ # will result in the default Rails exception page being shown), or you
174
+ # can catch it and restart the transaction (e.g. by telling the user
175
+ # that the title already exists, and asking him to re-enter the title).
176
+ # This technique is also known as optimistic concurrency control:
177
+ # http://en.wikipedia.org/wiki/Optimistic_concurrency_control
178
+ #
179
+ # The bundled ActiveRecord::ConnectionAdapters distinguish unique index
180
+ # constraint errors from other types of database errors by throwing an
181
+ # ActiveRecord::RecordNotUnique exception. For other adapters you will
182
+ # have to parse the (database-specific) exception message to detect such
183
+ # a case.
184
+ #
185
+ # The following bundled adapters throw the ActiveRecord::RecordNotUnique exception:
186
+ #
187
+ # * ActiveRecord::ConnectionAdapters::MysqlAdapter
188
+ # * ActiveRecord::ConnectionAdapters::Mysql2Adapter
189
+ # * ActiveRecord::ConnectionAdapters::SQLite3Adapter
190
+ # * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
191
+ def validates_uniqueness_of(*attr_names)
192
+ validates_with UniquenessValidator, _merge_attributes(attr_names)
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveRepository
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,76 @@
1
+ require 'active_hash'
2
+ require 'active_repository/sql_query_executor'
3
+
4
+ begin
5
+ klass = Module.const_get(ActiveRecord::Rollback)
6
+ unless klass.is_a?(Class)
7
+ raise "Not defined"
8
+ end
9
+ rescue
10
+ module ActiveRecord
11
+ class ActiveRecordError < StandardError
12
+ end
13
+ class Rollback < ActiveRecord::ActiveRecordError
14
+ end
15
+ end
16
+ end
17
+
18
+ module ActiveHash
19
+ class Base
20
+ def self.insert(record)
21
+ if self.all.map(&:to_s).include?(record.to_s)
22
+ record_index.delete(record.id.to_s)
23
+ self.all.delete(record)
24
+ end
25
+
26
+ if record_index[record.id.to_s].nil? || !self.all.map(&:to_s).include?(record.to_s)
27
+ @records ||= []
28
+ record.attributes[:id] ||= next_id
29
+
30
+ validate_unique_id(record) if dirty
31
+ mark_dirty
32
+
33
+ if record.valid?
34
+ add_to_record_index({ record.id.to_s => @records.length })
35
+ @records << record
36
+ end
37
+ end
38
+ end
39
+
40
+ def self.where(query)
41
+ if query.is_a?(String)
42
+ return ActiveHash::SQLQueryExecutor.execute(self, query)
43
+ else
44
+ (@records || []).select do |record|
45
+ query.all? { |col, match| record[col] == match }
46
+ end
47
+ end
48
+ end
49
+
50
+ def self.validate_unique_id(record)
51
+ raise IdError.new("Duplicate Id found for record #{record.attributes}") if record_index.has_key?(record.id.to_s)
52
+ end
53
+
54
+ def readonly?
55
+ false
56
+ end
57
+
58
+ def save(*args)
59
+ record = self.class.find_by_id(self.id)
60
+
61
+ self.class.insert(self) if record.nil? && record != self
62
+ true
63
+ end
64
+
65
+ def persisted?
66
+ other = self.class.find_by_id(id)
67
+ other.present? && other.created_at
68
+ end
69
+
70
+ def eql?(other)
71
+ (other.instance_of?(self.class) || other.instance_of?(self.class.get_model_class)) && id.present? && (id == other.id) && (created_at == other.created_at)
72
+ end
73
+
74
+ alias == eql?
75
+ end
76
+ end
@@ -0,0 +1,15 @@
1
+ require 'active_repository'
2
+
3
+ begin
4
+ require 'active_model'
5
+ require 'active_model/naming'
6
+ rescue LoadError
7
+ end
8
+
9
+ begin
10
+ require 'active_hash'
11
+ require 'associations/associations'
12
+ rescue LoadError
13
+ end
14
+
15
+ require 'active_repository/base'
@@ -0,0 +1,123 @@
1
+ require 'spec_helper'
2
+ require 'support/shared_examples'
3
+
4
+ require 'active_repository'
5
+ require "active_record"
6
+ require "mongoid"
7
+
8
+ describe ActiveRepository, "Base" do
9
+
10
+ before do
11
+ class Country < ActiveRepository::Base
12
+ end
13
+ end
14
+
15
+ after do
16
+ Object.send :remove_const, :Country
17
+ end
18
+
19
+ context "in_memory" do
20
+ before do
21
+ Country.fields :name, :monarch, :language
22
+ Country.set_model_class(Country)
23
+ Country.set_save_in_memory(true)
24
+
25
+ Country.create(:id => 1, :name => "US", :language => 'English')
26
+ Country.create(:id => 2, :name => "Canada", :language => 'English', :monarch => "The Crown of England")
27
+ Country.create(:id => 3, :name => "Mexico", :language => 'Spanish')
28
+ Country.create(:id => 4, :name => "UK", :language => 'English', :monarch => "The Crown of England")
29
+ Country.create(:id => 5, :name => "Brazil")
30
+ end
31
+
32
+ it_behaves_like '.update_attributes'
33
+ it_behaves_like '.all'
34
+ it_behaves_like '.where'
35
+ it_behaves_like '.exists?'
36
+ it_behaves_like '.count'
37
+ it_behaves_like '.first'
38
+ it_behaves_like '.last'
39
+ it_behaves_like '.find'
40
+ it_behaves_like '.find_by_id'
41
+ it_behaves_like 'custom finders'
42
+ it_behaves_like '#method_missing'
43
+ it_behaves_like '#attributes'
44
+ it_behaves_like 'reader_methods'
45
+ it_behaves_like 'interrogator methods'
46
+ it_behaves_like '#id'
47
+ it_behaves_like '#quoted_id'
48
+ it_behaves_like '#to_param'
49
+ it_behaves_like '#persisted?'
50
+ it_behaves_like '#eql?'
51
+ it_behaves_like '#=='
52
+ it_behaves_like '#hash'
53
+ it_behaves_like '#readonly?'
54
+ it_behaves_like '#cache_key'
55
+ it_behaves_like '#save'
56
+ it_behaves_like '.create'
57
+ it_behaves_like '#valid?'
58
+ it_behaves_like '#new_record?'
59
+ it_behaves_like '.transaction'
60
+ it_behaves_like '.delete_all'
61
+ end
62
+
63
+ context "active_record" do
64
+ before do
65
+ Country.fields :name, :monarch, :language
66
+
67
+ class CountryModel < ActiveRecord::Base
68
+ self.table_name = 'countries'
69
+ establish_connection :adapter => "sqlite3", :database => ":memory:"
70
+ connection.create_table(:countries, :force => true) do |t|
71
+ t.string :name
72
+ t.string :monarch
73
+ t.string :language
74
+ t.datetime :created_at
75
+ t.datetime :updated_at
76
+ end
77
+ end
78
+
79
+ Country.set_model_class(CountryModel)
80
+ Country.set_save_in_memory(false)
81
+
82
+ Country.create(:id => 1, :name => "US", :language => 'English')
83
+ Country.create(:id => 2, :name => "Canada", :language => 'English', :monarch => "The Crown of England")
84
+ Country.create(:id => 3, :name => "Mexico", :language => 'Spanish')
85
+ Country.create(:id => 4, :name => "UK", :language => 'English', :monarch => "The Crown of England")
86
+ Country.create(:id => 5, :name => "Brazil")
87
+ end
88
+
89
+ after do
90
+ Object.send :remove_const, :CountryModel
91
+ end
92
+
93
+ it_behaves_like '.update_attributes'
94
+ it_behaves_like '.all'
95
+ it_behaves_like '.where'
96
+ it_behaves_like '.exists?'
97
+ it_behaves_like '.count'
98
+ it_behaves_like '.first'
99
+ it_behaves_like '.last'
100
+ it_behaves_like '.find'
101
+ it_behaves_like '.find_by_id'
102
+ it_behaves_like 'custom finders'
103
+ it_behaves_like '#method_missing'
104
+ it_behaves_like '#attributes'
105
+ it_behaves_like 'reader_methods'
106
+ it_behaves_like 'interrogator methods'
107
+ it_behaves_like '#id'
108
+ it_behaves_like '#quoted_id'
109
+ it_behaves_like '#to_param'
110
+ it_behaves_like '#persisted?'
111
+ it_behaves_like '#eql?'
112
+ it_behaves_like '#=='
113
+ it_behaves_like '#hash'
114
+ it_behaves_like '#readonly?'
115
+ it_behaves_like '#cache_key'
116
+ it_behaves_like '#save'
117
+ it_behaves_like '.create'
118
+ it_behaves_like '#valid?'
119
+ it_behaves_like '#new_record?'
120
+ it_behaves_like '.transaction'
121
+ it_behaves_like '.delete_all'
122
+ end
123
+ end
@@ -0,0 +1,84 @@
1
+ require 'spec_helper'
2
+ require 'support/sql_query_shared_examples'
3
+
4
+ require 'active_repository'
5
+ require "active_record"
6
+ require "mongoid"
7
+
8
+ describe ActiveRepository, "Base" do
9
+
10
+ before do
11
+ class Country < ActiveRepository::Base
12
+ end
13
+ end
14
+
15
+ after do
16
+ Object.send :remove_const, :Country
17
+ end
18
+
19
+ context "in_memory" do
20
+ before do
21
+ Country.fields :name, :monarch, :language, :founded_at
22
+ Country.set_model_class(Country)
23
+ Country.set_save_in_memory(true)
24
+
25
+ Country.create(:id => 1, :name => "US", :language => 'English')
26
+ Country.create(:id => 2, :name => "Canada", :language => 'English', :monarch => "The Crown of England")
27
+ Country.create(:id => 3, :name => "Mexico", :language => 'Spanish')
28
+ Country.create(:id => 4, :name => "UK", :language => 'English', :monarch => "The Crown of England")
29
+ Country.create(:id => 5, :name => "Brazil", :founded_at => Time.parse('1500-04-22 13:34:25'))
30
+ end
31
+
32
+ describe ".where" do
33
+ it_behaves_like '='
34
+ it_behaves_like '>'
35
+ it_behaves_like '>='
36
+ it_behaves_like '<'
37
+ it_behaves_like '<='
38
+ it_behaves_like 'between'
39
+ it_behaves_like 'is'
40
+ end
41
+ end
42
+
43
+ context "active_record" do
44
+ before do
45
+ Country.fields :name, :monarch, :language, :founded_at
46
+
47
+ class CountryModel < ActiveRecord::Base
48
+ self.table_name = 'countries'
49
+ establish_connection :adapter => "sqlite3", :database => ":memory:"
50
+ connection.create_table(:countries, :force => true) do |t|
51
+ t.string :name
52
+ t.string :monarch
53
+ t.string :language
54
+ t.datetime :founded_at
55
+ t.datetime :created_at
56
+ t.datetime :updated_at
57
+ end
58
+ end
59
+
60
+ Country.set_model_class(CountryModel)
61
+ Country.set_save_in_memory(false)
62
+
63
+ Country.create(:id => 1, :name => "US", :language => 'English')
64
+ Country.create(:id => 2, :name => "Canada", :language => 'English', :monarch => "The Crown of England")
65
+ Country.create(:id => 3, :name => "Mexico", :language => 'Spanish')
66
+ Country.create(:id => 4, :name => "UK", :language => 'English', :monarch => "The Crown of England")
67
+ Country.create(:id => 5, :name => "Brazil", :founded_at => Time.parse('1500-04-22 13:34:25'))
68
+ end
69
+
70
+ after do
71
+ Object.send :remove_const, :CountryModel
72
+ end
73
+
74
+ describe ".where" do
75
+ it_behaves_like '='
76
+ it_behaves_like '>'
77
+ it_behaves_like '>='
78
+ it_behaves_like '<'
79
+ it_behaves_like '<='
80
+ it_behaves_like 'between'
81
+ it_behaves_like 'is'
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,6 @@
1
+ require 'rspec'
2
+ require 'rspec/autorun'
3
+
4
+ # $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+ # $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ # require 'active_repository'