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 +4 -0
- data/README.md +8 -8
- data/active_repository.gemspec +43 -0
- data/lib/active_repository/associations.rb +110 -0
- data/lib/active_repository/base.rb +216 -0
- data/lib/active_repository/finders.rb +107 -0
- data/lib/active_repository/sql_query_executor.rb +147 -0
- data/lib/active_repository/uniqueness.rb +196 -0
- data/lib/active_repository/version.rb +3 -0
- data/lib/active_repository/write_support.rb +95 -0
- data/lib/active_repository/writers.rb +75 -0
- data/lib/active_repository.rb +15 -0
- metadata +20 -7
data/Gemfile
ADDED
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
|
-
|
52
|
-
|
53
|
-
|
51
|
+
class User < ActiveHash::Base
|
52
|
+
# Defines the fields of the class
|
53
|
+
fields :name, :email, :birthdate
|
54
54
|
|
55
|
-
|
56
|
-
|
55
|
+
# Defines the class responsible for persisting data
|
56
|
+
set_model_class(Country)
|
57
57
|
|
58
|
-
|
59
|
-
|
60
|
-
|
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,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
|
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-
|
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
|
-
-
|
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
|
-
-
|
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:
|