active_repository 0.0.5 → 0.1.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.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in active_repository.gemspec
4
+ gemspec
data/README.md CHANGED
@@ -48,16 +48,16 @@ Or install it yourself as:
48
48
 
49
49
  Firstly you must inherit ActiveRepository::Base:
50
50
 
51
- class User < ActiveHash::Base
52
- # Defines the fields of the class
53
- fields :name, :email, :birthdate
51
+ class User < ActiveHash::Base
52
+ # Defines the fields of the class
53
+ fields :name, :email, :birthdate
54
54
 
55
- # Defines the class responsible for persisting data
56
- set_model_class(Country)
55
+ # Defines the class responsible for persisting data
56
+ set_model_class(Country)
57
57
 
58
- # Set this to true in order to ignore model_class attribute and persist in memory
59
- set_save_in_memory(true)
60
- end
58
+ # Set this to true in order to ignore model_class attribute and persist in memory
59
+ set_save_in_memory(true)
60
+ end
61
61
 
62
62
  Then it is just using it as if it was your ActiveRecord model or Mongoid Document.
63
63
 
@@ -0,0 +1,43 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/active_repository/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Caio Torres"]
6
+ gem.email = ["efreesen@gmail.com"]
7
+ gem.description = %q{An implementation of repository pattern that can connect with any ORM}
8
+ gem.summary = %q{An implementation of repository pattern that can connect with any ORM}
9
+ gem.homepage = "http://github.com/efreesen/active_repository"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.test_files = gem.files.grep(%r{^(spec)/})
13
+ gem.name = "active_repository"
14
+ gem.require_paths = ["lib"]
15
+ gem.version = ActiveRepository::VERSION
16
+ gem.license = "MIT"
17
+ gem.files = [
18
+ "LICENSE",
19
+ "README.md",
20
+ "active_repository.gemspec",
21
+ Dir.glob("lib/**/*")
22
+ ].flatten
23
+ gem.test_files = [
24
+ "Gemfile",
25
+ "spec/active_repository/base_spec.rb",
26
+ "spec/active_repository/associations_spec.rb",
27
+ "spec/active_repository/sql_query_executor_spec.rb",
28
+ "spec/support/shared_examples.rb",
29
+ "spec/support/sql_query_shared_examples.rb",
30
+ "spec/spec_helper.rb"
31
+ ]
32
+
33
+ gem.add_runtime_dependency(%q<active_hash>, [">= 0.9.12"])
34
+ gem.add_runtime_dependency(%q<activemodel>, [">= 3.2.6"])
35
+ gem.add_development_dependency(%q<rspec>, [">= 2.2.0"])
36
+ gem.add_development_dependency(%q<activerecord>)
37
+ gem.add_development_dependency(%q<mongoid>)
38
+ gem.add_development_dependency('rake')
39
+ gem.add_development_dependency(%q<sqlite3>) unless RUBY_PLATFORM == 'java'
40
+ gem.add_development_dependency(%q<jdbc-sqlite3>) if RUBY_PLATFORM == 'java'
41
+ gem.add_development_dependency(%q<jruby-openssl>) if RUBY_PLATFORM == 'java'
42
+ gem.add_development_dependency(%q<activerecord-jdbcsqlite3-adapter>) if RUBY_PLATFORM == 'java'
43
+ end
@@ -0,0 +1,110 @@
1
+ # Defines the relations between ActiveRepository objects and/or ActiveRecord Models.
2
+ #
3
+ # Author:: Caio Torres (mailto:efreesen@gmail.com)
4
+ # License:: MIT
5
+
6
+ module ActiveRepository
7
+ module Associations
8
+
9
+ #:nodoc:
10
+ module ActiveRecordExtensions
11
+
12
+
13
+ # Defines belongs to type relation between ActiveRepository objects and ActivRecord Models.
14
+ def belongs_to_active_repository(association_id, options = {})
15
+ options = {
16
+ :class_name => association_id.to_s.classify,
17
+ :foreign_key => association_id.to_s.foreign_key
18
+ }.merge(options)
19
+
20
+ define_method(association_id) do
21
+ options[:class_name].constantize.find_by_id(send(options[:foreign_key]))
22
+ end
23
+
24
+ define_method("#{association_id}=") do |new_value|
25
+ send "#{options[:foreign_key]}=", new_value ? new_value.id : nil
26
+ end
27
+
28
+ create_reflection(
29
+ :belongs_to,
30
+ association_id.to_sym,
31
+ options,
32
+ options[:class_name].constantize
33
+ )
34
+ end
35
+
36
+ end
37
+
38
+ #:nodoc:
39
+ def self.included(base)
40
+ base.extend Methods
41
+ end
42
+
43
+ #:nodoc:
44
+ module Methods
45
+ # Defines "has many" type relation between ActiveRepository objects
46
+ def has_many(association_id, options = {})
47
+ define_method(association_id) do
48
+ options = {
49
+ :class_name => association_id.to_s.classify,
50
+ :foreign_key => self.class.to_s.foreign_key
51
+ }.merge(options)
52
+
53
+ klass = options[:class_name].constantize
54
+ objects = []
55
+
56
+ if klass.respond_to?(:scoped)
57
+ objects = klass.scoped(:conditions => {options[:foreign_key] => id})
58
+ else
59
+ objects = klass.send("find_all_by_#{options[:foreign_key]}", id)
60
+ end
61
+ end
62
+ end
63
+
64
+ # Defines "has one" type relation between ActiveRepository objects
65
+ def has_one(association_id, options = {})
66
+ define_method(association_id) do
67
+ options = {
68
+ :class_name => association_id.to_s.classify,
69
+ :foreign_key => self.class.to_s.foreign_key
70
+ }.merge(options)
71
+
72
+ scope = options[:class_name].constantize
73
+
74
+ if scope.respond_to?(:scoped) && options[:conditions]
75
+ scope = scope.scoped(:conditions => options[:conditions])
76
+ end
77
+ scope.send("find_by_#{options[:foreign_key]}", id)
78
+ end
79
+ end
80
+
81
+ # Defines "belongs to" type relation between ActiveRepository objects
82
+ def belongs_to(association_id, options = {})
83
+
84
+ options = {
85
+ :class_name => association_id.to_s.classify,
86
+ :foreign_key => association_id.to_s.foreign_key
87
+ }.merge(options)
88
+
89
+ field options[:foreign_key].to_sym
90
+
91
+ define_method(association_id) do
92
+ klass = options[:class_name].constantize
93
+ id = send(options[:foreign_key])
94
+
95
+ if id.present?
96
+ object = klass.find_by_id(id)
97
+ else
98
+ nil
99
+ end
100
+ end
101
+
102
+ define_method("#{association_id}=") do |new_value|
103
+ attributes[options[:foreign_key].to_sym] = new_value ? new_value.id : nil
104
+ end
105
+
106
+ end
107
+ end
108
+
109
+ end
110
+ end
@@ -0,0 +1,216 @@
1
+ require 'active_repository/associations'
2
+ require 'active_repository/uniqueness'
3
+ require 'active_repository/write_support'
4
+ require 'active_repository/sql_query_executor'
5
+ require 'active_repository/finders'
6
+ require 'active_repository/writers'
7
+
8
+ module ActiveRepository
9
+
10
+ # Base class for ActiveRepository gem.
11
+ # Extends it in order to use it.
12
+ #
13
+ # == Options
14
+ #
15
+ # There are 2 class attributes to help configure your ActiveRepository class:
16
+ #
17
+ # * +class_model+: Use it to specify the class that is responsible for the
18
+ # persistence of the objects. Default is self, so it is always saving in
19
+ # memory by default.
20
+ #
21
+ # * +save_in_memory+: Used to ignore the class_model attribute, you can use
22
+ # it in your test suite, this way all your tests will be saved in memory.
23
+ # Default is set to true so it saves in memory by default.
24
+ #
25
+ #
26
+ # == Examples
27
+ #
28
+ # Using ActiveHash to persist objects in memory:
29
+ #
30
+ # class SaveInMemoryTest < ActiveRepository::Base
31
+ # end
32
+ #
33
+ # Using ActiveRecord/Mongoid to persist objects:
34
+ #
35
+ # class SaveInORMOrODMTest < ActiveRepository::Base
36
+ # SaveInORMOrODMTest.set_model_class(ORMOrODMModelClass)
37
+ # SaveInORMOrODMTest.set_save_in_memory(false)
38
+ # end
39
+ #
40
+ # Author:: Caio Torres (mailto:efreesen@gmail.com)
41
+ # License:: MIT
42
+ class Base < ActiveHash::Base
43
+ extend ActiveModel::Callbacks
44
+ extend ActiveRepository::Finders
45
+ extend ActiveRepository::Writers
46
+ include ActiveModel::Validations
47
+ include ActiveModel::Validations::Callbacks
48
+ include ActiveRepository::Associations
49
+ include ActiveRepository::Writers::InstanceMethods
50
+
51
+ class_attribute :model_class, :save_in_memory, :instance_writer => false
52
+
53
+ before_validation :set_timestamps
54
+
55
+ fields :created_at, :updated_at
56
+
57
+ # Returns all persisted objects
58
+ def self.all
59
+ self == get_model_class ? super : get_model_class.all.map { |object| serialize!(object.attributes) }
60
+ end
61
+
62
+ # Constantize class name
63
+ def self.constantize
64
+ self.to_s.constantize
65
+ end
66
+
67
+ # Deletes all persisted objects
68
+ def self.delete_all
69
+ self == get_model_class ? super : get_model_class.delete_all
70
+ end
71
+
72
+ # Checks the existence of a persisted object with the specified id
73
+ def self.exists?(id)
74
+ if self == get_model_class
75
+ !find_by_id(id).nil?
76
+ else
77
+ if mongoid?
78
+ find_by_id(id).present?
79
+ else
80
+ get_model_class.exists?(id)
81
+ end
82
+ end
83
+ end
84
+
85
+ # Returns the Class responsible for persisting the objects
86
+ def self.get_model_class
87
+ return self if self.save_in_memory.nil?
88
+ save_in_memory? ? self : self.model_class
89
+ end
90
+
91
+ # Converts Persisted object(s) to it's ActiveRepository counterpart
92
+ def self.serialize!(other)
93
+ case other.class.to_s
94
+ when "Hash" then self.new.serialize!(other)
95
+ when "Array" then other.map { |o| serialize!(o.attributes) }
96
+ when "Moped::BSON::Document" then self.new.serialize!(other)
97
+ else self.new.serialize!(other.attributes)
98
+ end
99
+ end
100
+
101
+ # Returns a array with the field names of the Class
102
+ def self.serialized_attributes
103
+ field_names.map &:to_s
104
+ end
105
+
106
+ # Sets the class attribute model_class, responsible to persist the ActiveRepository objects
107
+ def self.set_model_class(value)
108
+ self.model_class = value if model_class.nil?
109
+
110
+ field_names.each do |field_name|
111
+ define_custom_find_by_field(field_name)
112
+ define_custom_find_all_by_field(field_name)
113
+ end
114
+ end
115
+
116
+ # Sets the class attribute save_in_memory, set it to true to ignore model_class attribute
117
+ # and persist objects in memory
118
+ def self.set_save_in_memory(value)
119
+ self.save_in_memory = value if save_in_memory.nil?
120
+ end
121
+
122
+ # Searches persisted objects that matches the criterias in the parameters.
123
+ # Can be used in ActiveRecord/Mongoid way or in SQL like way.
124
+ #
125
+ # Example:
126
+ #
127
+ # * RelatedClass.where(:name => "Peter")
128
+ # * RelatedClass.where("name = 'Peter'")
129
+ def self.where(*args)
130
+ raise ArgumentError.new("wrong number of arguments (0 for 1)") if args.empty?
131
+ if self == get_model_class
132
+ query = ActiveHash::SQLQueryExecutor.args_to_query(args)
133
+ super(query)
134
+ else
135
+ objects = []
136
+ args = args.first.is_a?(Hash) ? args.first : args
137
+ get_model_class.where(args).each do |object|
138
+ objects << self.serialize!(object.attributes)
139
+ end
140
+
141
+ objects
142
+ end
143
+ end
144
+
145
+ # Persists the object using the class defined on the model_class attribute, if none defined it
146
+ # is saved in memory.
147
+ def persist
148
+ if self.valid?
149
+ save_in_memory? ? save : self.convert
150
+ end
151
+ end
152
+
153
+ # Gathers the persisted object from database and updates self with it's attributes.
154
+ def reload
155
+ serialize! self.class.get_model_class.find(self.id).attributes
156
+ end
157
+
158
+ # Returns the value of the save_in_memory class attribute
159
+ def save_in_memory?
160
+ self.save_in_memory.nil? ? true : save_in_memory
161
+ end
162
+
163
+ # Updates attributes from self with the attributes from the parameters
164
+ def serialize!(attributes)
165
+ unless attributes.nil?
166
+ self.attributes = attributes
167
+ end
168
+
169
+ self
170
+ end
171
+
172
+ protected
173
+ # Find related object on the database and updates it with attributes in self, if it didn't
174
+ # find it on database it creates a new one.
175
+ def convert(attribute="id")
176
+ klass = self.class.get_model_class
177
+ object = klass.where(attribute.to_sym => self.send(attribute)).first
178
+
179
+ object ||= self.class.get_model_class.new
180
+
181
+ attributes = self.attributes
182
+
183
+ attributes.delete(:id)
184
+
185
+ object.attributes = attributes
186
+
187
+ object.save
188
+
189
+ self.id = object.id
190
+
191
+ object
192
+ end
193
+
194
+ # Returns the value of the model_class attribute.
195
+ def model_class
196
+ self.model_class
197
+ end
198
+
199
+ private
200
+ # Checks if model_class is a Mongoid model
201
+ def self.mongoid?
202
+ get_model_class.included_modules.include?(Mongoid::Document)
203
+ end
204
+
205
+ # Checks if model_class is a Mongoid model
206
+ def mongoid?
207
+ self.class.mongoid?
208
+ end
209
+
210
+ # Updates created_at and updated_at
211
+ def set_timestamps
212
+ self.created_at = DateTime.now.utc if self.new_record?
213
+ self.updated_at = DateTime.now.utc
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,107 @@
1
+ # Module containing methods responsible for searching ActiveRepository objects
2
+ module ActiveRepository #:nodoc:
3
+ module Finders #:nodoc:
4
+ # Defines fiend_by_field methods for the Class
5
+ def define_custom_find_by_field(field_name)
6
+ method_name = :"find_all_by_#{field_name}"
7
+ the_meta_class.instance_eval do
8
+ define_method(method_name) do |*args|
9
+ object = nil
10
+
11
+ object = self.find_by_field(field_name.to_sym, args)
12
+
13
+ object.nil? ? nil : serialize!(object.attributes)
14
+ end
15
+ end
16
+ end
17
+
18
+ # Defines fiend_all_by_field methods for the Class
19
+ def define_custom_find_all_by_field(field_name)
20
+ method_name = :"find_all_by_#{field_name}"
21
+ the_meta_class.instance_eval do
22
+ define_method(method_name) do |*args|
23
+ objects = []
24
+
25
+ objects = self.find_all_by_field(field_name.to_sym, args)
26
+
27
+ objects.empty? ? [] : objects.map{ |object| serialize!(object.attributes) }
28
+ end
29
+ end
30
+ end
31
+
32
+ # Searches for a object containing the id in #id
33
+ def find(id)
34
+ begin
35
+ if self == get_model_class
36
+ super(id)
37
+ else
38
+ object = (id == :all) ? all : get_model_class.find(id)
39
+
40
+ serialize!(object)
41
+ end
42
+ rescue Exception => e
43
+ message = "Couldn't find #{self} with ID=#{id}"
44
+ message = "Couldn't find all #{self} objects with IDs (#{id.join(', ')})" if id.is_a?(Array)
45
+
46
+ raise ActiveHash::RecordNotFound.new(message)
47
+ end
48
+ end
49
+
50
+ # Searches all objects that matches #field_name field with the #args value(s)
51
+ def find_all_by_field(field_name, args)
52
+ objects = []
53
+
54
+ if self == get_model_class
55
+ objects = self.where(field_name.to_sym => args.first)
56
+ else
57
+ if mongoid?
58
+ objects = get_model_class.where(field_name.to_sym => args.first)
59
+ else
60
+ method_name = :"find_all_by_#{field_name}"
61
+ objects = get_model_class.send(method_name, args)
62
+ end
63
+ end
64
+
65
+ objects
66
+ end
67
+
68
+ # Searches first object that matches #field_name field with the #args value(s)
69
+ def find_by_field(field_name, args)
70
+ self.find_all_by_field(field_name, args).first
71
+ end
72
+
73
+ # Searches for an object that has id with #id value, if none is found returns nil
74
+ def find_by_id(id)
75
+ if self == get_model_class
76
+ super(id)
77
+ else
78
+ object = nil
79
+
80
+ if mongoid?
81
+ object = get_model_class.where(:id => id).entries.first
82
+ else
83
+ object = get_model_class.find_by_id(id)
84
+ end
85
+
86
+ object.nil? ? nil : serialize!(object.attributes)
87
+ end
88
+ end
89
+
90
+ # Returns first persisted object
91
+ def first
92
+ self == get_model_class ? super : get(:first)
93
+ end
94
+
95
+ # Returns last persisted object
96
+ def last
97
+ self == get_model_class ? super : get(:last)
98
+ end
99
+
100
+ private
101
+ # Returns the object in the position specified in #position
102
+ def get(position)
103
+ object = get_model_class.send(position)
104
+ serialize! object.attributes
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,147 @@
1
+ # Simulates a SQL where clause to filter objects from the database
2
+ module ActiveHash #:nodoc:
3
+ class SQLQueryExecutor #:nodoc:
4
+ class << self #:nodoc:
5
+ # Prepares query by replacing all ? by it's real values in #args
6
+ def args_to_query(args)
7
+ return args.first if args.size == 1
8
+
9
+ query = args.first
10
+ param = args.delete(args[1])
11
+
12
+ param = convert_param(param)
13
+
14
+ args[0] = query.sub("?", param)
15
+
16
+ args_to_query(args)
17
+ end
18
+
19
+ # Recursive method that divides the query in sub queries, executes each part individually
20
+ # and finally relates its results as specified in the query.
21
+ def execute(klass, query)
22
+ @operator, @sub_query, @objects = process_first(klass, query, query.split(" ")[1])
23
+
24
+ @operator.nil? ? @objects : @objects.send(@operator, execute(klass, @sub_query)).sort_by{ |o| o.id }
25
+ end
26
+
27
+ private
28
+ # Splits the first sub query from the rest of the query and returns it.
29
+ def divide_query
30
+ array = @query.split(" ")
31
+ case @operator
32
+ when "between"
33
+ array[0..5]
34
+ when "is"
35
+ size = array[2].downcase == "not" ? 4 : 3
36
+ array[0..size]
37
+ else
38
+ array[0..3]
39
+ end
40
+ end
41
+
42
+ # Replaces white spaces for underscores inside quotes in order to avoid getting parameters
43
+ # split into separate components of the query.
44
+ def convert_attrs(field, *attrs)
45
+ attrs.each_with_index do |attribute, i|
46
+ attribute = attribute.gsub("_", " ")
47
+ attrs[i] = field.is_a?(Integer) ? attribute.to_i : attribute
48
+ end
49
+
50
+ field = field.is_a?(Integer) ? field : field.to_s
51
+
52
+ [field, attrs].flatten
53
+ end
54
+
55
+ # Returns converted #param based on its Class, so it can be used on the query
56
+ def convert_param(param)
57
+ case param.class.name
58
+ when "String"
59
+ param = "'#{param}'"
60
+ when "Date"
61
+ param = "'#{param.strftime("%Y-%m-%d")}'"
62
+ when "Time"
63
+ param = "'#{param.strftime("%Y-%m-%d %H:%M:%S %z")}'"
64
+ else
65
+ param = param.to_s
66
+ end
67
+ end
68
+
69
+ # Execute SQL between filter
70
+ def execute_between(klass, sub_query)
71
+ klass.all.select do |o|
72
+ field, first_attr, second_attr = convert_attrs(o.send(sub_query.first), sub_query[2], sub_query[4])
73
+
74
+ (field >= first_attr && field <= second_attr)
75
+ end
76
+ end
77
+
78
+ # Executes SQL is filter
79
+ def execute_is(klass, sub_query)
80
+ klass.all.select do |o|
81
+ field = o.send(sub_query.first).blank?
82
+
83
+ sub_query.size == 3 ? field : !field
84
+ end
85
+ end
86
+
87
+ # Executes the #sub_quey defined operator filter
88
+ def execute_operator(klass, sub_query)
89
+ klass.all.select do |o|
90
+ field, attribute = convert_attrs(o.send(sub_query.first), sub_query[2])
91
+
92
+ field.blank? ? false : field.send(@operator, attribute)
93
+ end
94
+ end
95
+
96
+ # Executes the #sub_query
97
+ def execute_sub_query(klass, sub_query)
98
+ case @operator
99
+ when "between"
100
+ execute_between(klass, sub_query)
101
+ when "is"
102
+ execute_is(klass, sub_query)
103
+ else
104
+ execute_operator(klass, sub_query)
105
+ end
106
+ end
107
+
108
+ # Converts SQL where clause sub query operator to its Ruby Array counterpart
109
+ def get_operator(attributes)
110
+ operator = attributes.size >= 4 ? attributes.last : nil
111
+
112
+ case operator
113
+ when "or" then "+"
114
+ when "and" then "&"
115
+ else nil
116
+ end
117
+ end
118
+
119
+ # Processes the first sub query in query
120
+ def process_first(klass, query, operator)
121
+ @operator = operator == "=" ? "==" : operator
122
+ @query = sanitize_query(query)
123
+ sub_query = divide_query
124
+
125
+ binding_operator = get_operator(sub_query)
126
+
127
+ objects = execute_sub_query(klass, sub_query)
128
+
129
+ sub_query = query.gsub(sub_query.join(" "), "")
130
+
131
+ [binding_operator, sub_query, objects]
132
+ end
133
+
134
+ # Removes all accents and other non default characters
135
+ def sanitize_query(query)
136
+ new_query = query
137
+ params = query.scan(/([\"'])(.*?)\1/)
138
+
139
+ params.each do |quote, param|
140
+ new_query = new_query.gsub(quote,"").gsub(param, param.gsub(" ", "_"))
141
+ end
142
+
143
+ new_query
144
+ end
145
+ end
146
+ end
147
+ end
@@ -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.1.0"
3
+ end
@@ -0,0 +1,95 @@
1
+ require 'active_hash'
2
+ require 'active_repository/sql_query_executor'
3
+
4
+ # Changes made in order to make write support in ActiveHash.
5
+
6
+ begin
7
+ klass = Module.const_get(ActiveRecord::Rollback)
8
+ unless klass.is_a?(Class)
9
+ raise "Not defined"
10
+ end
11
+ rescue
12
+ module ActiveRecord
13
+ class ActiveRecordError < StandardError
14
+ end
15
+ class Rollback < ActiveRecord::ActiveRecordError
16
+ end
17
+ end
18
+ end
19
+
20
+ module ActiveHash
21
+ class Base
22
+ def self.insert(record)
23
+ record_id = record.id.to_s
24
+ record_hash = record.hash
25
+
26
+ if self.all.map(&:hash).include?(record_hash)
27
+ record_index.delete(record_id)
28
+ self.all.delete(record)
29
+ end
30
+
31
+ if record_index[record_id].nil? || !self.all.map(&:hash).include?(record_hash)
32
+ insert_record(record)
33
+ end
34
+ end
35
+
36
+ def self.where(query)
37
+ if query.is_a?(String)
38
+ return ActiveHash::SQLQueryExecutor.execute(self, query)
39
+ else
40
+ (@records || []).select do |record|
41
+ query.all? { |col, match| record[col] == match }
42
+ end
43
+ end
44
+ end
45
+
46
+ def self.validate_unique_id(record)
47
+ raise IdError.new("Duplicate Id found for record #{record.attributes}") if record_index.has_key?(record.id.to_s)
48
+ end
49
+
50
+ def update_attribute(key, value)
51
+ self.send("#{key}=", value)
52
+ self.save(:validate => false)
53
+ end
54
+
55
+ def readonly?
56
+ false
57
+ end
58
+
59
+ def save(*args)
60
+ record = self.class.find_by_id(self.id)
61
+
62
+ self.class.insert(self) if record.nil? && record != self
63
+ true
64
+ end
65
+
66
+ def to_param
67
+ id.present? ? id.to_s : nil
68
+ end
69
+
70
+ def persisted?
71
+ other = self.class.find_by_id(id)
72
+ other.present?
73
+ end
74
+
75
+ def eql?(other)
76
+ (other.instance_of?(self.class) || other.instance_of?(self.class.get_model_class)) && id.present? && (id == other.id) && (created_at == other.created_at)
77
+ end
78
+
79
+ alias == eql?
80
+
81
+ private
82
+ def self.insert_record(record)
83
+ @records ||= []
84
+ record.attributes[:id] ||= next_id
85
+
86
+ validate_unique_id(record) if dirty
87
+ mark_dirty
88
+
89
+ if record.valid?
90
+ add_to_record_index({ record.id.to_s => @records.length })
91
+ @records << record
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,75 @@
1
+ # Module containing writing methods of the ActiveRepository::Base class
2
+ module ActiveRepository
3
+ module Writers
4
+ # Creates an object and persists it.
5
+ def create(attributes={})
6
+ object = get_model_class.new(attributes)
7
+
8
+ object.id = nil if exists?(object.id)
9
+
10
+ if get_model_class == self
11
+ object.save
12
+ else
13
+ repository = serialize!(object.attributes)
14
+ repository.valid? ? (object = get_model_class.create(attributes)) : false
15
+ end
16
+
17
+ serialize!(object.attributes) unless object.class.name == self
18
+ end
19
+
20
+ # Searches for an object that matches the attributes on the parameter, if none is found
21
+ # it creates one with the defined attributes.
22
+ def find_or_create(attributes)
23
+ object = get_model_class.where(attributes).first
24
+
25
+ object = model_class.create(attributes) if object.nil?
26
+
27
+ serialize!(object.attributes)
28
+ end
29
+
30
+ #:nodoc:
31
+ module InstanceMethods
32
+ # Assigns new_attributes parameter to the attributes in self.
33
+ def attributes=(new_attributes)
34
+ new_attributes.each do |k,v|
35
+ self.send("#{k.to_s == '_id' ? 'id' : k.to_s}=", v)
36
+ end
37
+ end
38
+
39
+ # Updates #key attribute with #value value.
40
+ def update_attribute(key, value)
41
+ if self.class == self.class.get_model_class
42
+ super(key,value)
43
+ else
44
+ object = self.class.get_model_class.find(self.id)
45
+
46
+ if mongoid?
47
+ super(key,value)
48
+ key = key.to_s == 'id' ? '_id' : key.to_s
49
+ end
50
+
51
+ object.update_attribute(key, value)
52
+ object.save
53
+ end
54
+
55
+ self.reload
56
+ end
57
+
58
+ # Updates attributes in self with the attributes in the parameter
59
+ def update_attributes(attributes)
60
+ object = nil
61
+ if mongoid?
62
+ object = self.class.get_model_class.find(self.id)
63
+ else
64
+ object = self.class.get_model_class.find(self.id)
65
+ end
66
+
67
+ attributes.each do |k,v|
68
+ object.update_attribute("#{k.to_s}", v) unless k == :id
69
+ end
70
+
71
+ self.reload
72
+ end
73
+ end
74
+ end
75
+ 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'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_repository
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-12-10 00:00:00.000000000 Z
12
+ date: 2012-12-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: active_hash
@@ -130,14 +130,25 @@ executables: []
130
130
  extensions: []
131
131
  extra_rdoc_files: []
132
132
  files:
133
- - README.md
134
133
  - LICENSE
135
- - spec/active_repository/associations_spec.rb
134
+ - README.md
135
+ - active_repository.gemspec
136
+ - lib/active_repository/associations.rb
137
+ - lib/active_repository/base.rb
138
+ - lib/active_repository/finders.rb
139
+ - lib/active_repository/sql_query_executor.rb
140
+ - lib/active_repository/uniqueness.rb
141
+ - lib/active_repository/version.rb
142
+ - lib/active_repository/write_support.rb
143
+ - lib/active_repository/writers.rb
144
+ - lib/active_repository.rb
145
+ - Gemfile
136
146
  - spec/active_repository/base_spec.rb
147
+ - spec/active_repository/associations_spec.rb
137
148
  - spec/active_repository/sql_query_executor_spec.rb
138
- - spec/spec_helper.rb
139
149
  - spec/support/shared_examples.rb
140
150
  - spec/support/sql_query_shared_examples.rb
151
+ - spec/spec_helper.rb
141
152
  homepage: http://github.com/efreesen/active_repository
142
153
  licenses:
143
154
  - MIT
@@ -164,9 +175,11 @@ signing_key:
164
175
  specification_version: 3
165
176
  summary: An implementation of repository pattern that can connect with any ORM
166
177
  test_files:
167
- - spec/active_repository/associations_spec.rb
178
+ - Gemfile
168
179
  - spec/active_repository/base_spec.rb
180
+ - spec/active_repository/associations_spec.rb
169
181
  - spec/active_repository/sql_query_executor_spec.rb
170
- - spec/spec_helper.rb
171
182
  - spec/support/shared_examples.rb
172
183
  - spec/support/sql_query_shared_examples.rb
184
+ - spec/spec_helper.rb
185
+ has_rdoc: