active_repository 0.0.1

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,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'