light_mongo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ coverage
2
+ rdoc
3
+ pkg
4
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Elliot Crosby-McCullough
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,134 @@
1
+ LightMongo
2
+ ==========
3
+ LightMongo is a lightweight Mongo object persistence layer for Ruby which makes use of Mongo's features rather than trying to emulate ActiveRecord.
4
+
5
+ Status
6
+ -----------------
7
+ LightMongo is only a few days old, so while most of the features demo'd below work, there's no strong integration testing yet, so I wouldn't use it for data you particularly care about until I release a gem.
8
+
9
+ Check out the development roadmap for an idea of my current priorities.
10
+
11
+ The problem
12
+ -----------
13
+ Developers occasionally encounter a domain which defies simple modelling in an ActiveRecord relational style, and look to some of the nosql databases for a solution. They find Mongo, a document database, and feel it might provide the flexibility they need. After a bit of research they pick out a persistence library which seems popular and well maintained. It even emulates most of ActiveRecord's behaviour, style and relational philosophy. Great!
14
+
15
+ Hang on a minute, wasn't it ActiveRecord's behaviour, style and relational philosophy they moved to Mongo to get away from?
16
+
17
+ The solution
18
+ ------------
19
+ + Ruby instances store their state in instance variables. Why do we need to hide this in the persistence layer?
20
+ + Ruby has quite the heap of array management operators. Why do we need explicit relationships and relationship proxies?
21
+ + Objects of the same class can perform a number of different roles or be related to other classes in lots of ways. Why do we need to jump through complicated and restrictive hoops to do something we do in pure Ruby domains all the time?
22
+
23
+ Mongo is a flexible database. We can make use of that flexibility to allow our persistence layer to make decisions on how to best serialise and deserialise our objects. It's our responsibility to make sure our domain is correct. It's the library's responsibility to store those domain objects.
24
+
25
+ We're Ruby developers. Let's act like it.
26
+
27
+ An example
28
+ ----------
29
+ class Article
30
+ include LightMongo::Document
31
+ end
32
+
33
+ geology_article = Article.new(:title => 'Fluid Physics in Geology', :abstract => 'Lorem ipsum dolor..')
34
+ geology_article.save
35
+
36
+ Article.find.first
37
+ => #<Article:0x101647448 @_id="4b93c1e97bc7697187000001" @title="Fluid Physics in Geology" @abstract="Lorem upsum dolor...">
38
+
39
+ No tables. No database. Save your migrations for when you actually have some data to shift around.
40
+
41
+ Slightly more complex
42
+ ---------------------
43
+ Plain Ruby objects stored in your Documents will be serialised along with the Document and embedded in the Mongo document.
44
+
45
+ class Article
46
+ include LightMongo::Document
47
+
48
+ attr_accessor :title, :comments
49
+
50
+ def initialize(*args)
51
+ @comments = []
52
+ super
53
+ end
54
+ end
55
+
56
+ class Comment
57
+ attr_accessor :author_name, :text
58
+ end
59
+
60
+ geology_article = Article.create(:title => 'Fluid Physics in Geology')
61
+ comment = Comment.new
62
+ comment.author_name = 'Dave'
63
+ comment.text = "Cool article!"
64
+
65
+ geology_article.comments << comment
66
+ geology_article.save
67
+
68
+ first_article = Article.find.first
69
+
70
+ first_article.title
71
+ => "Fluid Physics in Geology"
72
+
73
+ first_article.comments
74
+ => [#<Comment:0x101664138 @author_name="Dave" @text="Cool article!">]
75
+
76
+ Dynamic finders
77
+ ---------------
78
+ It's not generally a good idea to do much searching on keys that haven't been indexed (as in most databases), so LightMongo will only set up dynamic finders for attributes you've asked to have indexed. If you really want an unindexed finder, they're not difficult to write.
79
+
80
+ class Article
81
+ include LightMongo::Document
82
+ attr_reader :page_length
83
+ index :title
84
+ index :abstract, :as => :precis
85
+ end
86
+
87
+ geology_article = Article.create(:title => 'Fluid Physics in Geology',
88
+ :abstract => 'A study in geological fluid physics',
89
+ :page_length => 367)
90
+
91
+ Article.find_by_title('Fluid Physics in Geology').first == geology_article
92
+ => true
93
+
94
+ Article.find_by_precis('A study in geological fluid physics').first == geology_article
95
+ => true
96
+
97
+ The aliasing option is not required, but is recommended if you want dynamic finders for indexed keys that can't be represented in a standard Ruby method name (for example, a finder will not be created for a complex multi-level Mongo key index. See the Mongo manual for more information).
98
+
99
+ Cross-collection relationships
100
+ ------------------------------
101
+ LightMongo uses its Document mixin to signify a collection, so if you embed a LightMongo::Document inside another LightMongo::Document, the serialisation engine will consider this a cross-collection relationship and behave accordingly.
102
+
103
+ class Article
104
+ include LightMongo::Document
105
+ attr_reader :author
106
+ end
107
+
108
+ class Person
109
+ include LightMongo::Document
110
+ end
111
+
112
+ dave = Person.new(:name => 'Dave')
113
+ fluid_physics = Article.create(:title => 'Fluid Physics in Geology', :author => dave)
114
+
115
+ Person.find.first
116
+ => #<Person:0x101664138 @_id="4b93cf9397bc7697187000001" @name="Dave">
117
+
118
+ Article.find.first.author == Person.find.first
119
+ => true
120
+
121
+ Roadmap
122
+ -------
123
+ 1. Improved testbed to allow stronger integration testing.
124
+ 2. More intelligent and efficient object serialisation.
125
+ 3. Proper deserialisation of cross-collection objects (currently they go in, don't come back out).
126
+ 4. Nested hash serialisation.
127
+ 4. Migrations (e.g. when you rename classes or modify their collection style).
128
+ 5. Some kind of validations, perhaps.
129
+
130
+
131
+
132
+
133
+
134
+
data/Rakefile ADDED
@@ -0,0 +1,45 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "light_mongo"
8
+ gem.summary = %Q{A lightweight Ruby object persistence library for Mongo DB}
9
+ gem.description = %Q{LightMongo is a lightweight Mongo object persistence layer for Ruby which makes use of Mongo's features rather than trying to emulate ActiveRecord.}
10
+ gem.email = "elliot.cm@gmail.com"
11
+ gem.homepage = "http://github.com/elliotcm/light_mongo"
12
+ gem.authors = ["Elliot Crosby-McCullough"]
13
+ gem.add_development_dependency "rspec", ">= 1.3.0"
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+
18
+ # check_dependencies is defined by jeweler
19
+ task :spec => :check_dependencies
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
22
+ end
23
+
24
+ require 'spec/rake/spectask'
25
+ Spec::Rake::SpecTask.new(:spec) do |spec|
26
+ spec.libs << 'lib' << 'spec'
27
+ spec.spec_files = FileList['spec/**/*_spec.rb']
28
+ spec.spec_opts = ['--color']
29
+ end
30
+
31
+ Spec::Rake::SpecTask.new(:integration) do |spec|
32
+ spec.libs << 'lib' << 'integration'
33
+ spec.spec_files = FileList['integration/**/*_spec.rb']
34
+ spec.spec_opts = ['--color']
35
+ end
36
+
37
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
38
+ spec.libs << 'lib' << 'spec'
39
+ spec.pattern = 'spec/**/*_spec.rb'
40
+ spec.rcov = true
41
+ spec.spec_opts = ['--color']
42
+ end
43
+
44
+
45
+ task :default => [:spec, :integration]
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,21 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/support/integration_helper')
2
+
3
+ class Article
4
+ include LightMongo::Document
5
+ end
6
+
7
+ describe 'The basic storage cycle' do
8
+ before(:each) do
9
+ @geology_article = Article.new(:title => (@title = 'Fluid Physics in Geology'),
10
+ :abstract => (@abstract = 'Lorem ipsum dolor..'))
11
+ end
12
+
13
+ it "allows the storage and retrieval of documents." do
14
+ @geology_article.save
15
+ stored_article = Article.find.first
16
+ stored_article.instance_variable_get('@title').should == @title
17
+ stored_article.instance_variable_get('@abstract').should == @abstract
18
+ end
19
+
20
+ db_teardown
21
+ end
@@ -0,0 +1,29 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/support/integration_helper')
2
+
3
+ LightMongo.slow_serialization = true
4
+
5
+ class Article
6
+ include LightMongo::Document
7
+ attr_reader :author
8
+ end
9
+
10
+ class Person
11
+ include LightMongo::Document
12
+ end
13
+
14
+ describe 'Embedding a LightMongo::Document within another LightMongo::Document' do
15
+ before(:each) do
16
+ @dave = Person.new(:name => 'Dave')
17
+ @geology_article = Article.create(:title => 'Fluid Physics in Geology', :author => @dave)
18
+ end
19
+
20
+ it "allows independent access to the embedded document via its collection." do
21
+ Person.find.first.should == @dave
22
+ end
23
+
24
+ it "retains a reference to the embedded document in its container document." do
25
+ Article.find.first.author.should == @dave
26
+ end
27
+
28
+ db_teardown
29
+ end
@@ -0,0 +1,33 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/support/integration_helper')
2
+
3
+ LightMongo.slow_serialization = true
4
+
5
+ class Article
6
+ include LightMongo::Document
7
+
8
+ attr_reader :page_length
9
+ index :title
10
+ index :abstract, :as => :precis
11
+ end
12
+
13
+ describe 'Indexing an attribute' do
14
+ before(:each) do
15
+ @geology_article = Article.create(:title => 'Fluid Physics in Geology',
16
+ :abstract => 'A study in geological fluid physics',
17
+ :page_length => 367)
18
+ end
19
+
20
+ context "where an alias is not given" do
21
+ it "creates a finder using the key name" do
22
+ Article.find_by_title('Fluid Physics in Geology').first.should == @geology_article
23
+ end
24
+ end
25
+
26
+ context "where an alias has been given" do
27
+ it "creates a finder using the alias name" do
28
+ Article.find_by_precis('A study in geological fluid physics').first.should == @geology_article
29
+ end
30
+ end
31
+
32
+ db_teardown
33
+ end
@@ -0,0 +1,20 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/support/integration_helper')
2
+
3
+ describe 'Integration tests' do
4
+ db_teardown
5
+
6
+ before(:each) do
7
+ class Integration
8
+ include LightMongo::Document
9
+ end
10
+ end
11
+
12
+ after(:all) do
13
+ Integration.find.should be_empty
14
+ end
15
+
16
+ it "tears down the database after each run" do
17
+ Integration.create :name => 'Database teardown example'
18
+ Integration.find.first.should be_a(Integration)
19
+ end
20
+ end
@@ -0,0 +1,42 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/support/integration_helper')
2
+
3
+ LightMongo.slow_serialization = true
4
+
5
+ class Article
6
+ include LightMongo::Document
7
+
8
+ attr_accessor :title, :comments
9
+
10
+ def initialize(*args)
11
+ @comments = []
12
+ super
13
+ end
14
+ end
15
+
16
+ class Comment
17
+ attr_accessor :author_name, :text
18
+ end
19
+
20
+ describe 'Embedding a Ruby object within your LightMongo::Document' do
21
+ before(:each) do
22
+ @geology_article = Article.create(:title => (@title = 'Fluid Physics in Geology'))
23
+
24
+ @comment = Comment.new
25
+ @comment.author_name = 'Dave'
26
+ @comment.text = "Cool article!"
27
+
28
+ @geology_article.comments << @comment
29
+ @geology_article.save
30
+ end
31
+
32
+ it "is represented as embedded MongoDB documents and eager restored." do
33
+ stored_article = Article.find.first
34
+ stored_article.title.should == @title
35
+
36
+ stored_comment = stored_article.comments.first
37
+ stored_comment.author_name.should == @comment.author_name
38
+ stored_comment.text.should == @comment.text
39
+ end
40
+
41
+ db_teardown
42
+ end
@@ -0,0 +1,9 @@
1
+ require File.dirname(__FILE__) + '/../../lib/light_mongo'
2
+
3
+ LightMongo.database = 'light_mongo_test'
4
+
5
+ def db_teardown
6
+ after(:each) do
7
+ LightMongo.connection.drop_database(LightMongo.database.name)
8
+ end
9
+ end
@@ -0,0 +1,84 @@
1
+ module LightMongo
2
+ # Connection and database getters/setters hoofed from jnunemaker's MongoMapper
3
+ def self.connection
4
+ @@connection ||= Mongo::Connection.new
5
+ end
6
+
7
+ def self.connection=(new_connection)
8
+ @@connection = new_connection
9
+ end
10
+
11
+ def self.database=(name)
12
+ @@database = nil
13
+ @@database_name = name
14
+ end
15
+
16
+ def self.database
17
+ unless defined?(@@database_name) and !Util.blank?(@@database_name)
18
+ raise 'You forgot to set the default database name: LightMongo.database = "foobar"'
19
+ end
20
+
21
+ @@database ||= LightMongo.connection.db(@@database_name)
22
+ end
23
+
24
+ module Document
25
+ module Persistence
26
+ def self.included(document_class)
27
+ document_class.extend ClassMethods
28
+ document_class.collection = Mongo::Collection.new(LightMongo.database, document_class.name)
29
+ end
30
+
31
+ def collection
32
+ self.class.collection
33
+ end
34
+
35
+ def save
36
+ @_id = collection.save(self.to_hash)
37
+ end
38
+
39
+ def id
40
+ @_id
41
+ end
42
+
43
+ def ==(other)
44
+ self.id == other.id
45
+ end
46
+
47
+
48
+ module ClassMethods
49
+ attr_accessor :collection
50
+
51
+ def create(params)
52
+ new_object = new(params)
53
+ new_object.save
54
+ return new_object
55
+ end
56
+
57
+ def index(key_name, options={})
58
+ return if Util.blank?(key_name)
59
+
60
+ method_name = 'find_by_'+(options[:as] or key_name).to_s
61
+ if viable_method_name(method_name)
62
+ (class << self; self; end).class_eval do
63
+ define_method method_name.to_sym do |value|
64
+ collection.find(key_name.to_sym => value).map{|bson_hash| new(bson_hash)}
65
+ end
66
+ end
67
+ end
68
+
69
+ collection.create_index(key_name)
70
+ end
71
+
72
+ def find(query=nil)
73
+ query = {'_id' => query} unless query.nil? or query.is_a?(Hash)
74
+ collection.find(query).map{|bson_hash| new(bson_hash)}
75
+ end
76
+
77
+ def viable_method_name(method_name)
78
+ method_name =~ /^\w+[!?]?$/
79
+ end
80
+ end
81
+
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,74 @@
1
+ require 'mongo'
2
+
3
+ module LightMongo
4
+ module Document
5
+ module Serialization
6
+
7
+ class HashSerializer
8
+ class <<self
9
+ def dump(object_to_serialize, current_depth=0)
10
+ case object_to_serialize
11
+ when Array
12
+ return serialize_array(object_to_serialize, current_depth)
13
+ when Hash
14
+ return serialize_hash(object_to_serialize, current_depth)
15
+ else
16
+ return serialize_object(object_to_serialize, current_depth)
17
+ end
18
+ end
19
+
20
+ def serialize_array(object_to_serialize, current_depth)
21
+ object_to_serialize.map do |entry|
22
+ Serializer.serialize(entry, current_depth + 1)
23
+ end
24
+ end
25
+
26
+ def serialize_hash(object_to_serialize, current_depth)
27
+ outbound_hash = {}
28
+ object_to_serialize.each_pair do |key, entry|
29
+ outbound_hash[key] = Serializer.serialize(entry, current_depth + 1)
30
+ end
31
+ outbound_hash
32
+ end
33
+
34
+ def serialize_object(object_to_serialize, current_depth)
35
+ return object_to_serialize if natively_embeddable?(object_to_serialize)
36
+
37
+ return object_to_serialize.export if object_to_serialize.is_a? LightMongo::Document and current_depth > 0
38
+
39
+ return hashify(object_to_serialize, current_depth)
40
+ end
41
+
42
+ def hashify(object_to_serialize, current_depth)
43
+ hashed_object = {}
44
+ hashed_object['_class_name'] = object_to_serialize.class.name if current_depth > 0
45
+
46
+ object_to_serialize.instance_variables.each do |attribute_name|
47
+ new_hash_key = attribute_name.sub(/^@/, '')
48
+ nested_object = object_to_serialize.instance_variable_get(attribute_name)
49
+ hashed_object[new_hash_key] = Serializer.serialize(nested_object, current_depth + 1)
50
+ end
51
+
52
+ return hashed_object
53
+ end
54
+
55
+ def natively_embeddable?(object)
56
+ begin
57
+ raise_unless_natively_embeddable(object)
58
+ rescue Mongo::InvalidDocument => e
59
+ return false
60
+ end
61
+
62
+ return true
63
+ end
64
+
65
+ def raise_unless_natively_embeddable(object)
66
+ BSON_RUBY.new.bson_type(object)
67
+ end
68
+
69
+ end
70
+ end
71
+
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,76 @@
1
+ require File.dirname(__FILE__) + '/hash_serializer'
2
+
3
+ module LightMongo
4
+ module Document
5
+ module Serialization
6
+ class Serializer
7
+ attr_accessor :depth
8
+
9
+ class <<self
10
+ def deserialize(object_to_deserialize)
11
+ return array_deserialize(object_to_deserialize) if object_to_deserialize.is_a?(Array)
12
+ if object_to_deserialize.is_a?(Hash)
13
+ if object_to_deserialize.has_key?('_data')
14
+ return Marshal.load(object_to_deserialize['_data'])
15
+ end
16
+
17
+ if object_to_deserialize.has_key?('_class_name')
18
+ class_name = object_to_deserialize.delete('_class_name')
19
+
20
+ if !object_to_deserialize.has_key?('_id')
21
+ object = Object.const_get(class_name).new
22
+ object_to_deserialize.each_pair do |attr_name, attr_value|
23
+ object.instance_variable_set '@'+attr_name, attr_value
24
+ end
25
+
26
+ return object
27
+ end
28
+
29
+ if object_to_deserialize.has_key?('_embed') and object_to_deserialize['_embed'] == true
30
+ return Object.const_get(class_name).find(object_to_deserialize['_id']).first
31
+ end
32
+ end
33
+
34
+ return hash_deserialize(object_to_deserialize)
35
+ end
36
+
37
+ return object_to_deserialize
38
+ end
39
+
40
+ def serialize(object_to_serialize, depth=0)
41
+ serializer = Serializer.new(object_to_serialize, depth)
42
+ return serializer.hash_serialize if LightMongo.slow_serialization or depth < LightMongo.marshal_depth
43
+ serializer.marshal
44
+ end
45
+
46
+ def array_deserialize(array)
47
+ array.map do |entry|
48
+ deserialize(entry)
49
+ end
50
+ end
51
+
52
+ def hash_deserialize(hash)
53
+ deserialized_hash = {}
54
+ hash.each_pair do |key, value|
55
+ deserialized_hash[key] = deserialize(value)
56
+ end
57
+ return deserialized_hash
58
+ end
59
+ end
60
+
61
+ def initialize(object_to_serialize, depth=0)
62
+ @object_to_serialize = object_to_serialize
63
+ @depth = depth
64
+ end
65
+
66
+ def marshal
67
+ {'_data' => Marshal.dump(@object_to_serialize)}
68
+ end
69
+
70
+ def hash_serialize
71
+ HashSerializer.dump(@object_to_serialize, @depth)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,45 @@
1
+ require File.dirname(__FILE__) + '/serialization/serializer'
2
+
3
+ module LightMongo
4
+ def self.slow_serialization=(boolean)
5
+ @@slow_serialization = boolean
6
+ end
7
+
8
+ def self.slow_serialization
9
+ @@slow_serialization = nil unless defined?(@@slow_serialization)
10
+ @@slow_serialization ||= false
11
+ end
12
+
13
+ def self.marshal_depth=(depth)
14
+ @@marshal_depth = depth
15
+ end
16
+
17
+ def self.marshal_depth
18
+ @@marshal_depth = nil unless defined?(@@marshal_depth)
19
+ @@marshal_depth ||= 3
20
+ end
21
+
22
+ module Document
23
+ module Serialization
24
+ def initialize(params={})
25
+ self.from_hash(params)
26
+ end
27
+
28
+ def to_hash(current_depth=0)
29
+ Serializer.serialize(self, current_depth)
30
+ end
31
+
32
+ def from_hash(hash)
33
+ Serializer.deserialize(hash).each_pair do |attr_name, attr_value|
34
+ self.instance_variable_set '@'+attr_name.to_s, attr_value
35
+ end
36
+ end
37
+
38
+ def export
39
+ return self unless self.class.include?(LightMongo::Document::Persistence)
40
+ self.save
41
+ {'_class_name' => self.class.name, '_id' => self.id, '_embed' => true}
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/document.rb ADDED
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+ require 'mongo'
3
+
4
+ def require_document(lib)
5
+ require File.dirname(__FILE__) + "/document/" + lib
6
+ end
7
+
8
+ require_document 'serialization'
9
+ require_document 'persistence'
10
+
11
+ module LightMongo
12
+ module Document
13
+ def self.included(document_class)
14
+ document_class.class_eval %{
15
+ include Serialization
16
+ include Persistence
17
+ }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ root = File.dirname(__FILE__)
2
+ require root+'/util'
3
+ require root+'/document'