perpetuity 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +3 -0
- data/Guardfile +24 -0
- data/README.md +142 -0
- data/Rakefile +6 -0
- data/lib/perpetuity/attribute.rb +17 -0
- data/lib/perpetuity/attribute_set.rb +23 -0
- data/lib/perpetuity/config.rb +11 -0
- data/lib/perpetuity/data_injectable.rb +21 -0
- data/lib/perpetuity/mapper.rb +195 -0
- data/lib/perpetuity/mongodb/query.rb +19 -0
- data/lib/perpetuity/mongodb/query_attribute.rb +49 -0
- data/lib/perpetuity/mongodb/query_expression.rb +55 -0
- data/lib/perpetuity/mongodb.rb +102 -0
- data/lib/perpetuity/retrieval.rb +82 -0
- data/lib/perpetuity/validations/length.rb +36 -0
- data/lib/perpetuity/validations/presence.rb +14 -0
- data/lib/perpetuity/validations/validation_set.rb +27 -0
- data/lib/perpetuity/validations.rb +1 -0
- data/lib/perpetuity/version.rb +3 -0
- data/lib/perpetuity.rb +23 -0
- data/perpetuity.gemspec +25 -0
- data/spec/perpetuity/attribute_set_spec.rb +19 -0
- data/spec/perpetuity/attribute_spec.rb +19 -0
- data/spec/perpetuity/config_spec.rb +13 -0
- data/spec/perpetuity/data_injectable_spec.rb +26 -0
- data/spec/perpetuity/mapper_spec.rb +51 -0
- data/spec/perpetuity/mongodb/query_attribute_spec.rb +42 -0
- data/spec/perpetuity/mongodb/query_expression_spec.rb +49 -0
- data/spec/perpetuity/mongodb/query_spec.rb +37 -0
- data/spec/perpetuity/mongodb_spec.rb +91 -0
- data/spec/perpetuity/retrieval_spec.rb +58 -0
- data/spec/perpetuity/validations/length_spec.rb +53 -0
- data/spec/perpetuity/validations/presence_spec.rb +30 -0
- data/spec/perpetuity/validations_spec.rb +87 -0
- data/spec/perpetuity_spec.rb +293 -0
- data/spec/test_classes.rb +93 -0
- metadata +181 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use @perpetuity --create
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'rspec', :version => 2 do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
7
|
+
watch('spec/spec_helper.rb') { "spec" }
|
8
|
+
|
9
|
+
# Rails example
|
10
|
+
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
11
|
+
watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
|
12
|
+
watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
|
13
|
+
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
|
14
|
+
watch('config/routes.rb') { "spec/routing" }
|
15
|
+
watch('app/controllers/application_controller.rb') { "spec/controllers" }
|
16
|
+
|
17
|
+
# Capybara request specs
|
18
|
+
watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
|
19
|
+
|
20
|
+
# Turnip features and steps
|
21
|
+
watch(%r{^spec/acceptance/(.+)\.feature$})
|
22
|
+
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
|
23
|
+
end
|
24
|
+
|
data/README.md
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
# Perpetuity [![Build Status](https://secure.travis-ci.org/jgaskins/perpetuity.png)](http://travis-ci.org/jgaskins/perpetuity)
|
2
|
+
|
3
|
+
Perpetuity is a simple Ruby object persistence layer that attempts to follow Martin Fowler's Data Mapper pattern, allowing you to use plain-old Ruby objects in your Ruby apps in order to decouple your domain logic from the database as well as speed up your tests. There is no need for your model classes to inherit from another class or even include a mix-in.
|
4
|
+
|
5
|
+
Your objects will hopefully eventually be able to be persisted into whichever database you like. Right now, only MongoDB is supported. Other persistence solutions will come later.
|
6
|
+
|
7
|
+
This gem was inspired by [a blog post by Steve Klabnik](http://blog.steveklabnik.com/posts/2011-12-30-active-record-considered-harmful).
|
8
|
+
|
9
|
+
## How it works
|
10
|
+
|
11
|
+
In the Data Mapper pattern, the objects you work with don't understand how to persist themselves. They interact with other objects just as in any other object-oriented application, leaving all persistence logic to mapper objects. This decouples them from the database and allows you to write your code without it in mind.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add the following to your Gemfile and run `bundle` to install it.
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'perpetuity', github: 'jgaskins/perpetuity'
|
19
|
+
```
|
20
|
+
|
21
|
+
Once it's got enough functionality to release, you'll be able to remove the `github` parameter.
|
22
|
+
|
23
|
+
## Configuration
|
24
|
+
|
25
|
+
The only currently supported persistence method is MongoDB. Other schemaless solutions can probably be implemented easily.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
mongodb = Perpetuity::MongoDB.new host: 'mongodb.example.com', db: 'example_db'
|
29
|
+
Perpetuity.configure do
|
30
|
+
data_source mongodb
|
31
|
+
end
|
32
|
+
```
|
33
|
+
|
34
|
+
## Setting up object mappers
|
35
|
+
|
36
|
+
Object mappers are generated by the following:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
Perpetuity.generate_mapper_for MyClass do
|
40
|
+
# individual mapper configuration goes here
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
The primary mapper configuration will be configuring attributes to be persisted. This is done using the `attribute` method. Calling `attribute` will add the specified attribute and its class to the mapper's attribute set. This is how the mapper knows what to store and how to store it. Here is an example of an `Article` class, its mapper and how it can be saved to the database.
|
45
|
+
|
46
|
+
Accessing mappers after they've been generated is done through the use of the subscript operator on the `Perpetuity` module. For example, if you generate a mapper for an `Article` class, you can access it by calling `Perpetuity[Article]`.
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
class Article
|
50
|
+
attr_accessor :title, :body
|
51
|
+
end
|
52
|
+
|
53
|
+
Perpetuity.generate_mapper_for Article do
|
54
|
+
attribute :title, String
|
55
|
+
attribute :body, String
|
56
|
+
end
|
57
|
+
|
58
|
+
article = Article.new
|
59
|
+
article.title = 'New Article'
|
60
|
+
article.body = 'This is an article.'
|
61
|
+
|
62
|
+
Perpetuity[Article].insert article
|
63
|
+
```
|
64
|
+
|
65
|
+
## Loading Objects
|
66
|
+
|
67
|
+
You can load all persisted objects of a particular class by sending `all` to the mapper object. Example:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
Perpetuity[Article].all
|
71
|
+
```
|
72
|
+
|
73
|
+
You can load specific objects by calling the `find` method with an ID param on that class's mapper class and passing in the criteria. You may also specify more general criteria using the `select` method with a block similar to `Enumerable#select`.
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
article = Perpetuity[Article].find params[:id]
|
77
|
+
users = Perpetuity[User].select { email == 'me@example.com' }
|
78
|
+
articles = Perpetuity[Article].select { published_at < Time.now }
|
79
|
+
comments = Perpetuity[Comment].select { article_id.in articles.map(&:id) }
|
80
|
+
```
|
81
|
+
|
82
|
+
Unfortunately, due to limitations in the Ruby language itself, this is as close as I could get to a true `Enumerable`-style select method. Once I can override `&&` and `||`, we can put more Rubyesque code in here.
|
83
|
+
|
84
|
+
These methods will return a Perpetuity::Retrieval object, which will lazily retrieve the objects from the database. They will wait to hit the DB when you begin iterating over the objects so you can continue chaining methods.
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
article_mapper = Perpetuity[Article]
|
88
|
+
articles = article_mapper.select { published_at < Time.now }
|
89
|
+
articles = articles.sort(:published_at).reverse
|
90
|
+
articles = articles.page(2).per_page(10) # built-in pagination
|
91
|
+
|
92
|
+
articles.each do |article| # This is when the DB gets hit
|
93
|
+
# Display the pretty articles
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
## Associations with Other Objects
|
98
|
+
|
99
|
+
If an object references another object (such as an article referencing its author), it must have a relationship identifier in its mapper class. For example:
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
class User
|
103
|
+
end
|
104
|
+
|
105
|
+
class Article
|
106
|
+
attr_accessor :author
|
107
|
+
|
108
|
+
def initialize(author)
|
109
|
+
self.author = author
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
Perpetuity.generate_mapper_for User do
|
114
|
+
end
|
115
|
+
|
116
|
+
Perpetuity.generate_mapper_for Article do
|
117
|
+
attribute :author, User # Notice the author's class
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
This allows you to write the following:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
article_mapper = Perpetuity[Article]
|
125
|
+
article = article_mapper.first
|
126
|
+
article_mapper.load_association! article, :author
|
127
|
+
user = article.author
|
128
|
+
```
|
129
|
+
|
130
|
+
## Customizing persistence
|
131
|
+
|
132
|
+
Setting the ID of a record to a custom value rather than using the DB default.
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
Perpetuity.generate_mapper_for Article do
|
136
|
+
id { title.gsub(/\W+/, '-') } # use the article's parameterized title attribute as its ID
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
## Contributing
|
141
|
+
|
142
|
+
Right now, this code is pretty bare and there are possibly some design decisions that need some more refinement. You can help. If you have ideas to build on this, send some love in the form of pull requests or issues or tweets or e-mails and I'll do what I can for them.
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Perpetuity
|
2
|
+
class Attribute
|
3
|
+
attr_reader :name, :type
|
4
|
+
def initialize(name, type, options = {})
|
5
|
+
@name = name
|
6
|
+
@type = type
|
7
|
+
|
8
|
+
options.each do |option, value|
|
9
|
+
instance_variable_set "@#{option}", value
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def embedded?
|
14
|
+
@embedded
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
class AttributeSet
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@attributes = Set.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def << attribute
|
12
|
+
@attributes << attribute
|
13
|
+
end
|
14
|
+
|
15
|
+
def [] name
|
16
|
+
@attributes.find { |attr| attr.name == name }
|
17
|
+
end
|
18
|
+
|
19
|
+
def each &block
|
20
|
+
@attributes.each &block
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Perpetuity
|
2
|
+
module DataInjectable
|
3
|
+
def inject_data object, data
|
4
|
+
data.each_pair do |attribute,value|
|
5
|
+
if object.respond_to?("#{attribute}=")
|
6
|
+
object.send("#{attribute}=", value)
|
7
|
+
else
|
8
|
+
attribute = "@#{attribute}" unless attribute[0] == '@'
|
9
|
+
object.instance_variable_set(attribute, value)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
give_id_to object if object.instance_variables.include?(:@id)
|
13
|
+
end
|
14
|
+
|
15
|
+
def give_id_to object, *args
|
16
|
+
object.define_singleton_method :id do
|
17
|
+
args.first || object.instance_variable_get(:@id)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'perpetuity/attribute_set'
|
2
|
+
require 'perpetuity/attribute'
|
3
|
+
require 'perpetuity/validations'
|
4
|
+
require 'perpetuity/data_injectable'
|
5
|
+
require 'perpetuity/mongodb/query'
|
6
|
+
|
7
|
+
module Perpetuity
|
8
|
+
class Mapper
|
9
|
+
include DataInjectable
|
10
|
+
attr_accessor :object, :original_object
|
11
|
+
|
12
|
+
def initialize(klass=Object, &block)
|
13
|
+
@mapped_class = klass
|
14
|
+
instance_exec &block if block_given?
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.generate_for(klass=Object, &block)
|
18
|
+
mapper = new(klass, &block)
|
19
|
+
mappers[klass] = mapper
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.mappers
|
23
|
+
@mappers ||= {}
|
24
|
+
end
|
25
|
+
|
26
|
+
def attribute_set
|
27
|
+
@attribute_set ||= AttributeSet.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def attribute name, type, options = {}
|
31
|
+
attribute_set << Attribute.new(name, type, options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def attributes
|
35
|
+
attribute_set.map(&:name)
|
36
|
+
end
|
37
|
+
|
38
|
+
def delete_all
|
39
|
+
data_source.delete_all mapped_class
|
40
|
+
end
|
41
|
+
|
42
|
+
def serializable_types
|
43
|
+
@serializable_types ||= [NilClass, TrueClass, FalseClass, Fixnum, Bignum, Float, String, Array, Hash, Time, Date]
|
44
|
+
end
|
45
|
+
|
46
|
+
def insert object
|
47
|
+
raise "#{object} is invalid and cannot be persisted." unless validations.valid?(object)
|
48
|
+
serializable_attributes = {}
|
49
|
+
if o_id = object.instance_exec(&id)
|
50
|
+
serializable_attributes[:id] = o_id
|
51
|
+
end
|
52
|
+
|
53
|
+
attributes_for(object).each_pair do |attribute, value|
|
54
|
+
if serializable_types.include? value.class
|
55
|
+
serializable_attributes[attribute] = value
|
56
|
+
elsif value.respond_to?(:id)
|
57
|
+
serializable_attributes[attribute] = value.id
|
58
|
+
else
|
59
|
+
raise "Must persist #{attribute} (#{value.inspect}) before persisting this #{object.inspect}."
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
new_id = data_source.insert mapped_class, serializable_attributes
|
64
|
+
give_id_to object, new_id
|
65
|
+
new_id
|
66
|
+
end
|
67
|
+
|
68
|
+
def attributes_for object
|
69
|
+
attrs = {}
|
70
|
+
attribute_set.each do |attrib|
|
71
|
+
value = object.send(attrib.name)
|
72
|
+
|
73
|
+
if attrib.type == Array
|
74
|
+
new_array = []
|
75
|
+
value.each do |i|
|
76
|
+
if serializable_types.include? i.class
|
77
|
+
new_array << i
|
78
|
+
else
|
79
|
+
if attrib.embedded?
|
80
|
+
new_array << Marshal.dump(i)
|
81
|
+
else
|
82
|
+
new_array << i.id
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
attrs[attrib.name] = new_array
|
88
|
+
else
|
89
|
+
attrs[attrib.name] = value
|
90
|
+
end
|
91
|
+
end
|
92
|
+
attrs
|
93
|
+
end
|
94
|
+
|
95
|
+
def unserialize(data)
|
96
|
+
if data.is_a?(String) && data.start_with?("\u0004")
|
97
|
+
Marshal.load(data)
|
98
|
+
elsif data.is_a? Array
|
99
|
+
data.map { |i| unserialize i }
|
100
|
+
elsif data.is_a? Hash
|
101
|
+
Hash[data.map{|k,v| [k, unserialize(v)]}]
|
102
|
+
else
|
103
|
+
data
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.[] klass
|
108
|
+
mappers[klass]
|
109
|
+
end
|
110
|
+
|
111
|
+
def data_source
|
112
|
+
Perpetuity.configuration.data_source
|
113
|
+
end
|
114
|
+
|
115
|
+
def count
|
116
|
+
data_source.count mapped_class
|
117
|
+
end
|
118
|
+
|
119
|
+
def mapped_class
|
120
|
+
@mapped_class
|
121
|
+
end
|
122
|
+
|
123
|
+
def first
|
124
|
+
data = data_source.first mapped_class
|
125
|
+
object = mapped_class.new
|
126
|
+
inject_data object, data
|
127
|
+
|
128
|
+
object
|
129
|
+
end
|
130
|
+
|
131
|
+
def all
|
132
|
+
results = data_source.all mapped_class
|
133
|
+
objects = []
|
134
|
+
results.each do |result|
|
135
|
+
object = mapped_class.new
|
136
|
+
inject_data object, result
|
137
|
+
|
138
|
+
objects << object
|
139
|
+
end
|
140
|
+
|
141
|
+
objects
|
142
|
+
end
|
143
|
+
|
144
|
+
def retrieve criteria={}
|
145
|
+
Perpetuity::Retrieval.new mapped_class, criteria
|
146
|
+
end
|
147
|
+
|
148
|
+
def select &block
|
149
|
+
query = data_source.class::Query.new(&block).to_db
|
150
|
+
retrieve query
|
151
|
+
end
|
152
|
+
|
153
|
+
def find id
|
154
|
+
retrieve(id: id).first
|
155
|
+
end
|
156
|
+
|
157
|
+
def delete object
|
158
|
+
data_source.delete object, mapped_class
|
159
|
+
end
|
160
|
+
|
161
|
+
def load_association! object, attribute
|
162
|
+
class_name = attribute_set[attribute].type
|
163
|
+
id = object.send(attribute)
|
164
|
+
|
165
|
+
mapper = Mapper[class_name]
|
166
|
+
associated_object = mapper.find(id)
|
167
|
+
object.send("#{attribute}=", associated_object)
|
168
|
+
end
|
169
|
+
|
170
|
+
def id &block
|
171
|
+
if block_given?
|
172
|
+
@id = block
|
173
|
+
else
|
174
|
+
@id ||= -> { nil }
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def update object, new_data
|
179
|
+
id = object.is_a?(mapped_class) ? object.id : object
|
180
|
+
|
181
|
+
data_source.update mapped_class, id, new_data
|
182
|
+
end
|
183
|
+
|
184
|
+
def validate &block
|
185
|
+
@validations ||= ValidationSet.new
|
186
|
+
|
187
|
+
validations.instance_exec(&block)
|
188
|
+
end
|
189
|
+
|
190
|
+
def validations
|
191
|
+
@validations ||= ValidationSet.new
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'perpetuity/mongodb/query_attribute'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
class MongoDB
|
5
|
+
class Query
|
6
|
+
def initialize &block
|
7
|
+
@query = instance_exec(&block)
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_db
|
11
|
+
@query.to_db
|
12
|
+
end
|
13
|
+
|
14
|
+
def method_missing missing_method
|
15
|
+
QueryAttribute.new missing_method
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'perpetuity/mongodb/query_expression'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
class MongoDB
|
5
|
+
class QueryAttribute
|
6
|
+
attr_reader :name
|
7
|
+
|
8
|
+
def initialize name
|
9
|
+
@name = name
|
10
|
+
end
|
11
|
+
|
12
|
+
def == value
|
13
|
+
QueryExpression.new self, :equals, value
|
14
|
+
end
|
15
|
+
|
16
|
+
def < value
|
17
|
+
QueryExpression.new self, :less_than, value
|
18
|
+
end
|
19
|
+
|
20
|
+
def >= value
|
21
|
+
QueryExpression.new self, :gte, value
|
22
|
+
end
|
23
|
+
|
24
|
+
def > value
|
25
|
+
QueryExpression.new self, :greater_than, value
|
26
|
+
end
|
27
|
+
|
28
|
+
def <= value
|
29
|
+
QueryExpression.new self, :lte, value
|
30
|
+
end
|
31
|
+
|
32
|
+
def not_equal? value
|
33
|
+
QueryExpression.new self, :not_equal, value
|
34
|
+
end
|
35
|
+
|
36
|
+
def =~ regexp
|
37
|
+
QueryExpression.new self, :matches, regexp
|
38
|
+
end
|
39
|
+
|
40
|
+
def in collection
|
41
|
+
QueryExpression.new self, :in, collection
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_sym
|
45
|
+
name
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Perpetuity
|
2
|
+
class MongoDB
|
3
|
+
class QueryExpression
|
4
|
+
attr_accessor :comparator
|
5
|
+
|
6
|
+
def initialize attribute, comparator, value
|
7
|
+
@attribute = attribute
|
8
|
+
@comparator = comparator
|
9
|
+
@value = value
|
10
|
+
|
11
|
+
@attribute = @attribute.to_sym if @attribute.respond_to? :to_sym
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_db
|
15
|
+
send @comparator
|
16
|
+
end
|
17
|
+
|
18
|
+
def equals
|
19
|
+
{ @attribute => @value }
|
20
|
+
end
|
21
|
+
|
22
|
+
def function func
|
23
|
+
{ @attribute => { func => @value } }
|
24
|
+
end
|
25
|
+
|
26
|
+
def less_than
|
27
|
+
function '$lt'
|
28
|
+
end
|
29
|
+
|
30
|
+
def lte
|
31
|
+
function '$lte'
|
32
|
+
end
|
33
|
+
|
34
|
+
def greater_than
|
35
|
+
function '$gt'
|
36
|
+
end
|
37
|
+
|
38
|
+
def gte
|
39
|
+
function '$gte'
|
40
|
+
end
|
41
|
+
|
42
|
+
def not_equal
|
43
|
+
function '$ne'
|
44
|
+
end
|
45
|
+
|
46
|
+
def in
|
47
|
+
function '$in'
|
48
|
+
end
|
49
|
+
|
50
|
+
def matches
|
51
|
+
{ @attribute => @value }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'mongo'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
class MongoDB
|
5
|
+
attr_accessor :connection, :host, :port, :db, :pool_size, :username, :password
|
6
|
+
|
7
|
+
def initialize options
|
8
|
+
@host = options.fetch(:host, 'localhost')
|
9
|
+
@port = options.fetch(:port, 27017)
|
10
|
+
@db = options.fetch(:db)
|
11
|
+
@pool_size = options.fetch(:pool_size, 5)
|
12
|
+
@username = options[:username]
|
13
|
+
@password = options[:password]
|
14
|
+
@connection = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def connect
|
18
|
+
database.authenticate(@username, @password) if @username and @password
|
19
|
+
@connection ||= Mongo::Connection.new @host, @port, pool_size: @pool_size
|
20
|
+
end
|
21
|
+
|
22
|
+
def connected?
|
23
|
+
!!@connection
|
24
|
+
end
|
25
|
+
|
26
|
+
def database
|
27
|
+
connect unless connected?
|
28
|
+
@connection.db(@db)
|
29
|
+
end
|
30
|
+
|
31
|
+
def collection klass
|
32
|
+
database.collection(klass.to_s)
|
33
|
+
end
|
34
|
+
|
35
|
+
def insert klass, attributes
|
36
|
+
if attributes.has_key? :id
|
37
|
+
attributes[:_id] = attributes[:id]
|
38
|
+
attributes.delete :id
|
39
|
+
end
|
40
|
+
|
41
|
+
collection(klass).insert attributes
|
42
|
+
end
|
43
|
+
|
44
|
+
def count klass
|
45
|
+
collection(klass).count
|
46
|
+
end
|
47
|
+
|
48
|
+
def delete_all klass
|
49
|
+
database.collection(klass.to_s).remove
|
50
|
+
end
|
51
|
+
|
52
|
+
def first klass
|
53
|
+
document = database.collection(klass.to_s).find_one
|
54
|
+
document[:id] = document.delete("_id")
|
55
|
+
|
56
|
+
document
|
57
|
+
end
|
58
|
+
|
59
|
+
def retrieve klass, criteria, options = {}
|
60
|
+
objects = []
|
61
|
+
|
62
|
+
# MongoDB uses '_id' as its ID field.
|
63
|
+
if criteria.has_key?(:id)
|
64
|
+
criteria = {
|
65
|
+
'$or' => [
|
66
|
+
{ _id: BSON::ObjectId.from_string(criteria[:id].to_s) },
|
67
|
+
{ _id: criteria[:id].to_s }
|
68
|
+
]
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
sort_field = options[:attribute]
|
73
|
+
sort_direction = options[:direction]
|
74
|
+
sort_criteria = [[sort_field, sort_direction]]
|
75
|
+
other_options = { limit: options[:limit] }
|
76
|
+
if options[:page]
|
77
|
+
other_options = other_options.merge skip: (options[:page] - 1) * options[:limit]
|
78
|
+
end
|
79
|
+
|
80
|
+
database.collection(klass.to_s).find(criteria, other_options).sort(sort_criteria).each do |document|
|
81
|
+
document[:id] = document.delete("_id")
|
82
|
+
objects << document
|
83
|
+
end
|
84
|
+
|
85
|
+
objects
|
86
|
+
end
|
87
|
+
|
88
|
+
def all klass
|
89
|
+
retrieve klass, {}, {}
|
90
|
+
end
|
91
|
+
|
92
|
+
def delete object, klass=nil
|
93
|
+
id = object.respond_to?(:id) ? object.id : object
|
94
|
+
klass ||= object.class
|
95
|
+
collection(klass.to_s).remove "_id" => id
|
96
|
+
end
|
97
|
+
|
98
|
+
def update klass, id, new_data
|
99
|
+
collection(klass).update({ _id: id }, new_data)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|