mongo_doc 0.3.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/.document +5 -0
- data/.gitignore +7 -0
- data/LICENSE +20 -0
- data/README.textile +174 -0
- data/Rakefile +135 -0
- data/TODO +31 -0
- data/VERSION +1 -0
- data/data/.gitignore +2 -0
- data/examples/simple_document.rb +35 -0
- data/examples/simple_object.rb +30 -0
- data/features/finders.feature +76 -0
- data/features/mongodb.yml +7 -0
- data/features/mongodoc_base.feature +128 -0
- data/features/new_record.feature +36 -0
- data/features/partial_updates.feature +105 -0
- data/features/removing_documents.feature +68 -0
- data/features/saving_an_object.feature +15 -0
- data/features/scopes.feature +66 -0
- data/features/step_definitions/collection_steps.rb +14 -0
- data/features/step_definitions/document_steps.rb +149 -0
- data/features/step_definitions/documents.rb +30 -0
- data/features/step_definitions/finder_steps.rb +15 -0
- data/features/step_definitions/json_steps.rb +9 -0
- data/features/step_definitions/object_steps.rb +50 -0
- data/features/step_definitions/objects.rb +24 -0
- data/features/step_definitions/partial_update_steps.rb +32 -0
- data/features/step_definitions/query_steps.rb +54 -0
- data/features/step_definitions/removing_documents_steps.rb +14 -0
- data/features/step_definitions/scope_steps.rb +18 -0
- data/features/step_definitions/util_steps.rb +7 -0
- data/features/support/support.rb +10 -0
- data/features/using_criteria.feature +128 -0
- data/lib/mongo_doc/associations/collection_proxy.rb +105 -0
- data/lib/mongo_doc/associations/document_proxy.rb +56 -0
- data/lib/mongo_doc/associations/hash_proxy.rb +98 -0
- data/lib/mongo_doc/associations/proxy_base.rb +53 -0
- data/lib/mongo_doc/attributes.rb +140 -0
- data/lib/mongo_doc/bson.rb +45 -0
- data/lib/mongo_doc/collection.rb +55 -0
- data/lib/mongo_doc/connection.rb +88 -0
- data/lib/mongo_doc/contexts/enumerable.rb +128 -0
- data/lib/mongo_doc/contexts/ids.rb +41 -0
- data/lib/mongo_doc/contexts/mongo.rb +232 -0
- data/lib/mongo_doc/contexts.rb +25 -0
- data/lib/mongo_doc/criteria.rb +38 -0
- data/lib/mongo_doc/cursor.rb +32 -0
- data/lib/mongo_doc/document.rb +216 -0
- data/lib/mongo_doc/ext/array.rb +5 -0
- data/lib/mongo_doc/ext/binary.rb +7 -0
- data/lib/mongo_doc/ext/boolean_class.rb +11 -0
- data/lib/mongo_doc/ext/date.rb +16 -0
- data/lib/mongo_doc/ext/date_time.rb +13 -0
- data/lib/mongo_doc/ext/dbref.rb +7 -0
- data/lib/mongo_doc/ext/hash.rb +7 -0
- data/lib/mongo_doc/ext/nil_class.rb +5 -0
- data/lib/mongo_doc/ext/numeric.rb +17 -0
- data/lib/mongo_doc/ext/object.rb +17 -0
- data/lib/mongo_doc/ext/object_id.rb +7 -0
- data/lib/mongo_doc/ext/regexp.rb +5 -0
- data/lib/mongo_doc/ext/string.rb +5 -0
- data/lib/mongo_doc/ext/symbol.rb +5 -0
- data/lib/mongo_doc/ext/time.rb +5 -0
- data/lib/mongo_doc/finders.rb +49 -0
- data/lib/mongo_doc/matchers.rb +35 -0
- data/lib/mongo_doc/query.rb +7 -0
- data/lib/mongo_doc/scope.rb +64 -0
- data/lib/mongo_doc/validations/macros.rb +11 -0
- data/lib/mongo_doc/validations/validates_embedded.rb +13 -0
- data/lib/mongo_doc.rb +19 -0
- data/lib/mongoid/contexts/paging.rb +42 -0
- data/lib/mongoid/criteria.rb +247 -0
- data/lib/mongoid/criterion/complex.rb +21 -0
- data/lib/mongoid/criterion/exclusion.rb +65 -0
- data/lib/mongoid/criterion/inclusion.rb +92 -0
- data/lib/mongoid/criterion/optional.rb +136 -0
- data/lib/mongoid/extensions/hash/criteria_helpers.rb +20 -0
- data/lib/mongoid/extensions/symbol/inflections.rb +36 -0
- data/lib/mongoid/matchers/all.rb +11 -0
- data/lib/mongoid/matchers/default.rb +26 -0
- data/lib/mongoid/matchers/exists.rb +13 -0
- data/lib/mongoid/matchers/gt.rb +11 -0
- data/lib/mongoid/matchers/gte.rb +11 -0
- data/lib/mongoid/matchers/in.rb +11 -0
- data/lib/mongoid/matchers/lt.rb +11 -0
- data/lib/mongoid/matchers/lte.rb +11 -0
- data/lib/mongoid/matchers/ne.rb +11 -0
- data/lib/mongoid/matchers/nin.rb +11 -0
- data/lib/mongoid/matchers/size.rb +11 -0
- data/mongo_doc.gemspec +205 -0
- data/mongod.example.yml +2 -0
- data/mongodb.example.yml +14 -0
- data/perf/mongo_doc_runner.rb +90 -0
- data/perf/ruby_driver_runner.rb +64 -0
- data/script/console +8 -0
- data/spec/associations/collection_proxy_spec.rb +200 -0
- data/spec/associations/document_proxy_spec.rb +42 -0
- data/spec/associations/hash_proxy_spec.rb +163 -0
- data/spec/attributes_spec.rb +273 -0
- data/spec/bson_matchers.rb +54 -0
- data/spec/bson_spec.rb +196 -0
- data/spec/collection_spec.rb +161 -0
- data/spec/connection_spec.rb +147 -0
- data/spec/contexts/enumerable_spec.rb +274 -0
- data/spec/contexts/ids_spec.rb +49 -0
- data/spec/contexts/mongo_spec.rb +198 -0
- data/spec/contexts_spec.rb +28 -0
- data/spec/criteria_spec.rb +33 -0
- data/spec/cursor_spec.rb +91 -0
- data/spec/document_ext.rb +9 -0
- data/spec/document_spec.rb +664 -0
- data/spec/embedded_save_spec.rb +109 -0
- data/spec/finders_spec.rb +73 -0
- data/spec/hash_matchers.rb +27 -0
- data/spec/matchers_spec.rb +342 -0
- data/spec/mongodb.yml +6 -0
- data/spec/mongodb_pairs.yml +8 -0
- data/spec/new_record_spec.rb +128 -0
- data/spec/query_spec.rb +12 -0
- data/spec/scope_spec.rb +79 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +13 -0
- metadata +290 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
When /^I remove '(.+)'$/ do |doc_name|
|
|
2
|
+
doc = instance_variable_get("@#{doc_name}")
|
|
3
|
+
doc.remove
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
Then /^the document '(.+)' is not found$/ do |doc_name|
|
|
7
|
+
doc = instance_variable_get("@#{doc_name}")
|
|
8
|
+
doc.class.find_one(doc.id)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
Then /^an exception is raised if I remove '(.+)'$/ do |doc_name|
|
|
12
|
+
doc = instance_variable_get("@#{doc_name}")
|
|
13
|
+
lambda { doc.remove }.should raise_error
|
|
14
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
def scope_query=(scope)
|
|
2
|
+
@query = scope
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
When /^I query (.*) with scope '(.*)'$/ do |doc, scope|
|
|
6
|
+
self.scope_query = klass(doc).send(scope)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
When /^I query (.*) with scopes '(.*)'$/ do |doc, scopes|
|
|
10
|
+
self.scope_query = scopes.split(',').inject(klass(doc)) do |result, scope|
|
|
11
|
+
result.send(scope.strip)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
When /^I query (.*) with lambda scope '(.*)' with parameters '(.*)'$/ do |doc, scope, params_text|
|
|
16
|
+
params = params_text.split(',').map(&:strip)
|
|
17
|
+
self.scope_query = klass(doc).send(scope, *params)
|
|
18
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
|
|
2
|
+
require 'cucumber'
|
|
3
|
+
require 'spec/expectations'
|
|
4
|
+
require 'spec/bson_matchers'
|
|
5
|
+
require 'mongo_doc'
|
|
6
|
+
|
|
7
|
+
MongoDoc::Connection.env = 'cucumber'
|
|
8
|
+
MongoDoc::Connection.config_path = './features/mongodb.yml'
|
|
9
|
+
|
|
10
|
+
World(BsonMatchers)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Feature: MongoDoc::Base
|
|
2
|
+
|
|
3
|
+
Background:
|
|
4
|
+
Given an empty Contact document collection
|
|
5
|
+
And a Contact document named 'hashrocket' :
|
|
6
|
+
| Name | Type |
|
|
7
|
+
| Hashrocket | company |
|
|
8
|
+
And 'hashrocket' has interests, an array of:
|
|
9
|
+
| Interest |
|
|
10
|
+
| ruby |
|
|
11
|
+
| rails |
|
|
12
|
+
| employment |
|
|
13
|
+
| contract work |
|
|
14
|
+
| restaurants |
|
|
15
|
+
| hotels |
|
|
16
|
+
| flights |
|
|
17
|
+
| car rentals |
|
|
18
|
+
And 'hashrocket' has many addresses :
|
|
19
|
+
| Street | City | State | Zip Code |
|
|
20
|
+
| 320 First Street North | Jacksonville Beach | FL | 32250 |
|
|
21
|
+
| 1 Lake Michigan Street | Chicago | IL | 60611 |
|
|
22
|
+
| 1 Main Street | Santiago | Chile | |
|
|
23
|
+
And I save the document 'hashrocket'
|
|
24
|
+
And a Contact document named 'rocketeer' :
|
|
25
|
+
| Name |
|
|
26
|
+
| Rocketeer Mike |
|
|
27
|
+
And 'rocketeer' has interests, an array of:
|
|
28
|
+
| Interest |
|
|
29
|
+
| ruby |
|
|
30
|
+
| rails |
|
|
31
|
+
| restaurants |
|
|
32
|
+
| employment |
|
|
33
|
+
And 'rocketeer' has many addresses :
|
|
34
|
+
| Street | City | State | Zip Code |
|
|
35
|
+
| 1 Main Street | Atlantic Beach | FL | 32233 |
|
|
36
|
+
And I save the document 'rocketeer'
|
|
37
|
+
And a Contact document named 'contractor' :
|
|
38
|
+
| Name |
|
|
39
|
+
| Contractor Joe |
|
|
40
|
+
And 'contractor' has interests, an array of:
|
|
41
|
+
| Interest |
|
|
42
|
+
| ruby |
|
|
43
|
+
| rails |
|
|
44
|
+
| contract work |
|
|
45
|
+
| flights |
|
|
46
|
+
| car rentals |
|
|
47
|
+
| hotels |
|
|
48
|
+
| restaurants |
|
|
49
|
+
And 'contractor' has many addresses :
|
|
50
|
+
| Street | City | State | Zip Code |
|
|
51
|
+
| 1 Main St. | Jacksonville | FL | 32218 |
|
|
52
|
+
And I save the document 'contractor'
|
|
53
|
+
And an empty Place document collection
|
|
54
|
+
And a Place document named 'one_ocean' :
|
|
55
|
+
| Name | Type |
|
|
56
|
+
| One Ocean | hotel |
|
|
57
|
+
And 'one_ocean' has one Address as address :
|
|
58
|
+
| Street | City | State | Zip Code |
|
|
59
|
+
| 1 Ocean Street | Atlantic Beach | FL | 32233 |
|
|
60
|
+
And I save the document 'one_ocean'
|
|
61
|
+
And a Place document named 'sea_horse' :
|
|
62
|
+
| Name | Type |
|
|
63
|
+
| Sea Horse | hotel |
|
|
64
|
+
And 'sea_horse' has one Address as address :
|
|
65
|
+
| Street | City | State | Zip Code |
|
|
66
|
+
| 1401 Atlantic Blvd | Neptune Beach | FL | 32266 |
|
|
67
|
+
And I save the document 'sea_horse'
|
|
68
|
+
And a Place document named 'jax' :
|
|
69
|
+
| Name | Type |
|
|
70
|
+
| Jacksonville International Airport | airport |
|
|
71
|
+
And 'jax' has one Address as address :
|
|
72
|
+
| Street | City | State | Zip Code |
|
|
73
|
+
| | Jacksonville | FL | 32218 |
|
|
74
|
+
And I save the document 'jax'
|
|
75
|
+
|
|
76
|
+
Scenario: Counting results
|
|
77
|
+
When I query contacts with criteria all('interests' => ['ruby', 'rails', 'employment'])
|
|
78
|
+
Then the query result has 2 documents
|
|
79
|
+
|
|
80
|
+
Scenario: Finding contacts with interests in ruby and rails
|
|
81
|
+
When I query contacts with criteria all('interests' => ['ruby', 'rails', 'employment'])
|
|
82
|
+
Then one of the query results is the document 'rocketeer'
|
|
83
|
+
|
|
84
|
+
Scenario: Finding contacts with interests in restaurants or hotels
|
|
85
|
+
When I query contacts with criteria in('interests' => ['restaurants', 'hotels'])
|
|
86
|
+
Then one of the query results is the document 'contractor'
|
|
87
|
+
|
|
88
|
+
Scenario: Aggregating Places
|
|
89
|
+
When I query places with criteria only('type').where('address.state' => 'FL').aggregate
|
|
90
|
+
Then the query result with "type" == "hotel" has a count of 2
|
|
91
|
+
|
|
92
|
+
Scenario: Excluding places in Neptune Beach
|
|
93
|
+
When I query places with criteria only('type').where('address.city' => 'Neptune Beach').aggregate
|
|
94
|
+
Then the query result with "type" == "hotel" has a count of 1
|
|
95
|
+
|
|
96
|
+
Scenario: Using extras to limit results
|
|
97
|
+
When I query contacts with criteria all('interests' => ['ruby', 'rails', 'employment']).limit(1)
|
|
98
|
+
Then the size of the query result is 1
|
|
99
|
+
|
|
100
|
+
Scenario: Finding the first result
|
|
101
|
+
When I query contacts with criteria all('interests' => ['ruby', 'rails', 'employment']).first
|
|
102
|
+
Then the query result is equal to the document 'hashrocket'
|
|
103
|
+
|
|
104
|
+
Scenario: Grouping places by type
|
|
105
|
+
When I query places with criteria only('type').where('type' => 'hotel').group
|
|
106
|
+
Then the query result with "type" == "hotel" has the document 'one_ocean'
|
|
107
|
+
|
|
108
|
+
Scenario: Selecting contacts with in operator
|
|
109
|
+
When I query contacts with criteria in('interests' => ['ruby', 'rails', 'employment'])
|
|
110
|
+
Then the query result has 3 documents
|
|
111
|
+
|
|
112
|
+
Scenario: Selecting a contact with the id operator
|
|
113
|
+
When I query contacts with the 'hashrocket' id
|
|
114
|
+
Then the query result has 1 documents
|
|
115
|
+
And the query result is the document 'hashrocket'
|
|
116
|
+
|
|
117
|
+
Scenario: Selecting contacts with not in operator
|
|
118
|
+
When I query contacts with criteria not_in('interests' => ['contract work', 'employment'])
|
|
119
|
+
Then the query result has 0 documents
|
|
120
|
+
|
|
121
|
+
Scenario: Ordering contacts
|
|
122
|
+
When I query contacts with criteria in('interests' => ['ruby', 'rails']).order_by([[:name, :asc]]).entries
|
|
123
|
+
Then the first query result is the document 'contractor'
|
|
124
|
+
And the last query result is the document 'rocketeer'
|
|
125
|
+
|
|
126
|
+
Scenario: Using skip on results
|
|
127
|
+
When I query contacts with criteria all('interests' => ['ruby', 'rails']).skip(1)
|
|
128
|
+
Then the size of the query result is 2
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Thanks Sandro!
|
|
2
|
+
# http://github.com/sandro
|
|
3
|
+
module MongoDoc
|
|
4
|
+
module Associations
|
|
5
|
+
class CollectionProxy
|
|
6
|
+
include ProxyBase
|
|
7
|
+
|
|
8
|
+
# List of array methods (that are not in +Object+) that need to be
|
|
9
|
+
# delegated to +collection+.
|
|
10
|
+
ARRAY_METHODS = (Array.instance_methods - Object.instance_methods).map { |n| n.to_s }
|
|
11
|
+
|
|
12
|
+
# List of additional methods that must be delegated to +collection+.
|
|
13
|
+
MUST_DEFINE = %w[to_a to_ary inspect to_bson ==]
|
|
14
|
+
|
|
15
|
+
DO_NOT_DEFINE = %w[concat insert replace]
|
|
16
|
+
|
|
17
|
+
(ARRAY_METHODS + MUST_DEFINE - DO_NOT_DEFINE).uniq.each do |method|
|
|
18
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
19
|
+
def #{method}(*args, &block) # def each(*args, &block)
|
|
20
|
+
collection.send(:#{method}, *args, &block) # collection.send(:each, *args, &block)
|
|
21
|
+
end # end
|
|
22
|
+
RUBY
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
attr_reader :collection
|
|
26
|
+
|
|
27
|
+
def _root=(root)
|
|
28
|
+
@_root = root
|
|
29
|
+
collection.each do |item|
|
|
30
|
+
item._root = root if is_document?(item)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(options)
|
|
35
|
+
super
|
|
36
|
+
@collection = []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
alias _append <<
|
|
40
|
+
def <<(item)
|
|
41
|
+
attach(item)
|
|
42
|
+
_append item
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
alias push <<
|
|
46
|
+
|
|
47
|
+
alias add []=
|
|
48
|
+
def []=(index, item)
|
|
49
|
+
attach(item)
|
|
50
|
+
add(index, item)
|
|
51
|
+
end
|
|
52
|
+
alias insert []=
|
|
53
|
+
|
|
54
|
+
def build(attrs)
|
|
55
|
+
item = assoc_class.new(attrs)
|
|
56
|
+
push(item)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def concat(array)
|
|
60
|
+
array.each do |item|
|
|
61
|
+
push(item)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Lie about our class. Borrowed from Rake::FileList
|
|
66
|
+
# Note: Does not work for case equality (<tt>===</tt>)
|
|
67
|
+
def is_a?(klass)
|
|
68
|
+
klass == Array || super(klass)
|
|
69
|
+
end
|
|
70
|
+
alias kind_of? is_a?
|
|
71
|
+
|
|
72
|
+
def replace(other)
|
|
73
|
+
clear
|
|
74
|
+
concat(other)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
alias _unshift unshift
|
|
78
|
+
def unshift(item)
|
|
79
|
+
attach(item)
|
|
80
|
+
_unshift(item)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def valid?
|
|
84
|
+
all? do |child|
|
|
85
|
+
if is_document?(child)
|
|
86
|
+
child.valid?
|
|
87
|
+
else
|
|
88
|
+
true
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
protected
|
|
94
|
+
|
|
95
|
+
def annotated_keys(src, attrs)
|
|
96
|
+
assoc_path = "#{assoc_name}.#{index(src)}"
|
|
97
|
+
annotated = {}
|
|
98
|
+
attrs.each do |(key, value)|
|
|
99
|
+
annotated["#{assoc_path}.#{key}"] = value
|
|
100
|
+
end
|
|
101
|
+
annotated
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module MongoDoc
|
|
2
|
+
module Associations
|
|
3
|
+
class DocumentProxy
|
|
4
|
+
include ProxyBase
|
|
5
|
+
|
|
6
|
+
attr_reader :document
|
|
7
|
+
|
|
8
|
+
delegate :to_bson, :id, :to => :document
|
|
9
|
+
|
|
10
|
+
def _root=(root)
|
|
11
|
+
@_root = root
|
|
12
|
+
document._root = root if is_document?(document)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def ==(other)
|
|
16
|
+
if self.class === other
|
|
17
|
+
document == other.document
|
|
18
|
+
else
|
|
19
|
+
document == other
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def build(attrs)
|
|
24
|
+
item = assoc_class.new(attrs)
|
|
25
|
+
self.document = item
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def document=(doc)
|
|
29
|
+
attach(doc)
|
|
30
|
+
@document = doc
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def valid?
|
|
34
|
+
if is_document?(document)
|
|
35
|
+
document.valid?
|
|
36
|
+
else
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def method_missing(method, *args)
|
|
44
|
+
unless document.respond_to?(method)
|
|
45
|
+
raise NoMethodError, "undefined method `#{method.to_s}' for proxied \"#{document}\":#{document.class.to_s}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if block_given?
|
|
49
|
+
document.send(method, *args) { |*block_args| yield(*block_args) }
|
|
50
|
+
else
|
|
51
|
+
document.send(method, *args)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
module MongoDoc
|
|
2
|
+
class InvalidEmbeddedHashKey < RuntimeError; end
|
|
3
|
+
|
|
4
|
+
module Associations
|
|
5
|
+
class HashProxy
|
|
6
|
+
include ProxyBase
|
|
7
|
+
|
|
8
|
+
HASH_METHODS = (Hash.instance_methods - Object.instance_methods).map { |n| n.to_s }
|
|
9
|
+
|
|
10
|
+
MUST_DEFINE = %w[to_a inspect to_bson ==]
|
|
11
|
+
|
|
12
|
+
DO_NOT_DEFINE = %w[merge! replace store update]
|
|
13
|
+
|
|
14
|
+
(HASH_METHODS + MUST_DEFINE - DO_NOT_DEFINE).uniq.each do |method|
|
|
15
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
16
|
+
def #{method}(*args, &block) # def each(*args, &block)
|
|
17
|
+
hash.send(:#{method}, *args, &block) # hash.send(:each, *args, &block)
|
|
18
|
+
end # end
|
|
19
|
+
RUBY
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :hash
|
|
23
|
+
|
|
24
|
+
def _root=(root)
|
|
25
|
+
@_root = root
|
|
26
|
+
hash.each do |key, value|
|
|
27
|
+
value._root = root if is_document?(value)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(options)
|
|
32
|
+
super
|
|
33
|
+
@hash = {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
alias put []=
|
|
37
|
+
def []=(key, value)
|
|
38
|
+
raise InvalidEmbeddedHashKey.new("Key name [#{key}] must be a valid element name, see http://www.mongodb.org/display/DOCS/BSON#BSON-noteonelementname") unless valid_key?(key)
|
|
39
|
+
attach(value)
|
|
40
|
+
put(key, value)
|
|
41
|
+
end
|
|
42
|
+
alias store []=
|
|
43
|
+
|
|
44
|
+
def build(key, attrs)
|
|
45
|
+
item = assoc_class.new(attrs)
|
|
46
|
+
store(key, item)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Lie about our class. Borrowed from Rake::FileList
|
|
50
|
+
# Note: Does not work for case equality (<tt>===</tt>)
|
|
51
|
+
def is_a?(klass)
|
|
52
|
+
klass == Hash || super(klass)
|
|
53
|
+
end
|
|
54
|
+
alias kind_of? is_a?
|
|
55
|
+
|
|
56
|
+
def merge!(other)
|
|
57
|
+
other.each_pair do |key, value|
|
|
58
|
+
self[key] = if block_given?
|
|
59
|
+
yield key, [key], value
|
|
60
|
+
else
|
|
61
|
+
value
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
alias update merge!
|
|
66
|
+
|
|
67
|
+
def replace(other)
|
|
68
|
+
clear
|
|
69
|
+
merge!(other)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def valid?
|
|
73
|
+
values.all? do |child|
|
|
74
|
+
if is_document?(child)
|
|
75
|
+
child.valid?
|
|
76
|
+
else
|
|
77
|
+
true
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
protected
|
|
83
|
+
|
|
84
|
+
def annotated_keys(src, attrs)
|
|
85
|
+
assoc_path = "#{assoc_name}.#{index(src)}"
|
|
86
|
+
annotated = {}
|
|
87
|
+
attrs.each do |(key, value)|
|
|
88
|
+
annotated["#{assoc_path}.#{key}"] = value
|
|
89
|
+
end
|
|
90
|
+
annotated
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def valid_key?(key)
|
|
94
|
+
(String === key or Symbol === key) and key.to_s !~ /(_id|query|\$.*|.*\..*)/
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module MongoDoc
|
|
2
|
+
module Associations
|
|
3
|
+
module ProxyBase
|
|
4
|
+
def self.included(klass)
|
|
5
|
+
klass.class_eval do
|
|
6
|
+
attr_reader :assoc_name, :assoc_class, :_parent, :_root
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def _parent=(parent)
|
|
11
|
+
@_parent = parent
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def _path_to_root(src, attrs)
|
|
15
|
+
_parent._path_to_root(src, annotated_keys(src, attrs))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def _root=(root)
|
|
19
|
+
@_root = root
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(options)
|
|
23
|
+
@assoc_name = options[:assoc_name]
|
|
24
|
+
@assoc_class = options[:assoc_class]
|
|
25
|
+
@_root = options[:root]
|
|
26
|
+
@_parent = options[:parent]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def attach(item)
|
|
30
|
+
if is_document?(item)
|
|
31
|
+
item._parent = self
|
|
32
|
+
item._root = _root
|
|
33
|
+
_root.send(:register_save_observer, item)
|
|
34
|
+
end
|
|
35
|
+
item
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
protected
|
|
39
|
+
|
|
40
|
+
def annotated_keys(src, hash)
|
|
41
|
+
annotated = {}
|
|
42
|
+
hash.each do |(key, value)|
|
|
43
|
+
annotated["#{assoc_name}.#{key}"] = value
|
|
44
|
+
end
|
|
45
|
+
annotated
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def is_document?(object)
|
|
49
|
+
object.respond_to?(:_parent)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
require 'mongo_doc/associations/proxy_base'
|
|
2
|
+
require 'mongo_doc/associations/collection_proxy'
|
|
3
|
+
require 'mongo_doc/associations/document_proxy'
|
|
4
|
+
require 'mongo_doc/associations/hash_proxy'
|
|
5
|
+
|
|
6
|
+
module MongoDoc
|
|
7
|
+
module Attributes
|
|
8
|
+
def self.included(klass)
|
|
9
|
+
klass.class_eval do
|
|
10
|
+
class_inheritable_array :_keys
|
|
11
|
+
self._keys = []
|
|
12
|
+
class_inheritable_array :_associations
|
|
13
|
+
self._associations = []
|
|
14
|
+
|
|
15
|
+
attr_accessor :_parent
|
|
16
|
+
attr_accessor :_id
|
|
17
|
+
|
|
18
|
+
extend ClassMethods
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def _root
|
|
23
|
+
@_root
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def _root=(root)
|
|
27
|
+
@_root = root
|
|
28
|
+
_associations.each do|a|
|
|
29
|
+
association = send(a)
|
|
30
|
+
association._root = root if association
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def _path_to_root(src, attrs)
|
|
35
|
+
return attrs unless _parent
|
|
36
|
+
_parent._path_to_root(self, attrs)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
module ClassMethods
|
|
40
|
+
def _attributes
|
|
41
|
+
_keys + _associations
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def key(*args)
|
|
45
|
+
args.each do |name|
|
|
46
|
+
_keys << name unless _keys.include?(name)
|
|
47
|
+
attr_accessor name
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def has_one(*args)
|
|
52
|
+
options = args.extract_options!
|
|
53
|
+
assoc_class = if class_name = options.delete(:class_name)
|
|
54
|
+
self.class_from_name(class_name)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
args.each do |name|
|
|
58
|
+
_associations << name unless _associations.include?(name)
|
|
59
|
+
|
|
60
|
+
attr_reader name
|
|
61
|
+
|
|
62
|
+
define_method("#{name}=") do |value|
|
|
63
|
+
association = instance_variable_get("@#{name}")
|
|
64
|
+
unless association
|
|
65
|
+
association = Associations::DocumentProxy.new(:root => _root || self, :parent => self, :assoc_name => name, :assoc_class => assoc_class || self.class.class_from_name(name))
|
|
66
|
+
instance_variable_set("@#{name}", association)
|
|
67
|
+
end
|
|
68
|
+
association.document = value
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
validates_embedded name, :if => Proc.new { !send(name).nil? }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def has_many(*args)
|
|
76
|
+
options = args.extract_options!
|
|
77
|
+
assoc_class = if class_name = options.delete(:class_name)
|
|
78
|
+
self.class_from_name(class_name)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
args.each do |name|
|
|
82
|
+
_associations << name unless _associations.include?(name)
|
|
83
|
+
|
|
84
|
+
define_method("#{name}") do
|
|
85
|
+
association = instance_variable_get("@#{name}")
|
|
86
|
+
unless association
|
|
87
|
+
association = Associations::CollectionProxy.new(:root => _root || self, :parent => self, :assoc_name => name, :assoc_class => assoc_class || self.class.class_from_name(name))
|
|
88
|
+
instance_variable_set("@#{name}", association)
|
|
89
|
+
end
|
|
90
|
+
association
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
validates_embedded name
|
|
94
|
+
|
|
95
|
+
define_method("#{name}=") do |arrayish|
|
|
96
|
+
proxy = send("#{name}")
|
|
97
|
+
proxy.clear
|
|
98
|
+
Array.wrap(arrayish).each do|item|
|
|
99
|
+
proxy << item
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def has_hash(*args)
|
|
106
|
+
options = args.extract_options!
|
|
107
|
+
assoc_class = if class_name = options.delete(:class_name)
|
|
108
|
+
self.class_from_name(class_name)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
args.each do |name|
|
|
112
|
+
_associations << name unless _associations.include?(name)
|
|
113
|
+
|
|
114
|
+
define_method("#{name}") do
|
|
115
|
+
association = instance_variable_get("@#{name}")
|
|
116
|
+
unless association
|
|
117
|
+
association = Associations::HashProxy.new(:root => _root || self, :parent => self, :assoc_name => name, :assoc_class => assoc_class || self.class.class_from_name(name))
|
|
118
|
+
instance_variable_set("@#{name}", association)
|
|
119
|
+
end
|
|
120
|
+
association
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
validates_embedded name
|
|
124
|
+
|
|
125
|
+
define_method("#{name}=") do |hash|
|
|
126
|
+
send("#{name}").replace(hash)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def class_from_name(name)
|
|
132
|
+
type_name_with_module(name.to_s.classify).constantize rescue nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def type_name_with_module(type_name)
|
|
136
|
+
(/^::/ =~ type_name) ? type_name : "#{parent}::#{type_name}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require 'mongo_doc/ext/array'
|
|
2
|
+
require 'mongo_doc/ext/binary'
|
|
3
|
+
require 'mongo_doc/ext/boolean_class'
|
|
4
|
+
require 'mongo_doc/ext/date'
|
|
5
|
+
require 'mongo_doc/ext/date_time'
|
|
6
|
+
require 'mongo_doc/ext/dbref'
|
|
7
|
+
require 'mongo_doc/ext/hash'
|
|
8
|
+
require 'mongo_doc/ext/nil_class'
|
|
9
|
+
require 'mongo_doc/ext/numeric'
|
|
10
|
+
require 'mongo_doc/ext/object'
|
|
11
|
+
require 'mongo_doc/ext/object_id'
|
|
12
|
+
require 'mongo_doc/ext/regexp'
|
|
13
|
+
require 'mongo_doc/ext/string'
|
|
14
|
+
require 'mongo_doc/ext/symbol'
|
|
15
|
+
require 'mongo_doc/ext/time'
|
|
16
|
+
|
|
17
|
+
module MongoDoc
|
|
18
|
+
module BSON
|
|
19
|
+
CLASS_KEY = "json_class"
|
|
20
|
+
|
|
21
|
+
def self.decode(bson, options = {})
|
|
22
|
+
return bson if options[:raw_json]
|
|
23
|
+
case bson
|
|
24
|
+
when Hash
|
|
25
|
+
bson_create(bson, options)
|
|
26
|
+
when Array
|
|
27
|
+
array_create(bson, options)
|
|
28
|
+
else
|
|
29
|
+
bson
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.bson_create(bson_hash, options = {})
|
|
34
|
+
return bson_hash if options[:raw_json]
|
|
35
|
+
klass = bson_hash.delete(CLASS_KEY)
|
|
36
|
+
return bson_hash.each_pair {|key, value| bson_hash[key] = decode(value, options)} unless klass
|
|
37
|
+
klass.constantize.bson_create(bson_hash, options)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.array_create(bson_array, options = {})
|
|
41
|
+
return bson_array if options[:raw_json]
|
|
42
|
+
bson_array.map {|item| decode(item, options)}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|