perpetuity-mongodb 1.0.0.beta
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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +36 -0
- data/Rakefile +1 -0
- data/lib/perpetuity/mongodb.rb +231 -0
- data/lib/perpetuity/mongodb/index.rb +52 -0
- data/lib/perpetuity/mongodb/nil_query.rb +11 -0
- data/lib/perpetuity/mongodb/query.rb +33 -0
- data/lib/perpetuity/mongodb/query_attribute.rb +66 -0
- data/lib/perpetuity/mongodb/query_expression.rb +94 -0
- data/lib/perpetuity/mongodb/query_intersection.rb +16 -0
- data/lib/perpetuity/mongodb/query_union.rb +16 -0
- data/lib/perpetuity/mongodb/serializer.rb +174 -0
- data/lib/perpetuity/mongodb/version.rb +5 -0
- data/perpetuity-mongodb.gemspec +26 -0
- data/spec/perpetuity/mongodb/index_spec.rb +44 -0
- data/spec/perpetuity/mongodb/query_attribute_spec.rb +58 -0
- data/spec/perpetuity/mongodb/query_expression_spec.rb +67 -0
- data/spec/perpetuity/mongodb/query_intersection_spec.rb +16 -0
- data/spec/perpetuity/mongodb/query_spec.rb +79 -0
- data/spec/perpetuity/mongodb/query_union_spec.rb +16 -0
- data/spec/perpetuity/mongodb/serializer_spec.rb +212 -0
- data/spec/perpetuity/mongodb_spec.rb +218 -0
- data/spec/support/test_classes/book.rb +8 -0
- data/spec/support/test_classes/car.rb +5 -0
- data/spec/support/test_classes/user.rb +7 -0
- metadata +152 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'perpetuity/mongodb/query_union'
|
2
|
+
require 'perpetuity/mongodb/query_intersection'
|
3
|
+
|
4
|
+
module Perpetuity
|
5
|
+
class MongoDB
|
6
|
+
class QueryExpression
|
7
|
+
attr_accessor :attribute, :comparator, :negated, :value
|
8
|
+
|
9
|
+
def initialize attribute, comparator, value
|
10
|
+
@attribute = attribute
|
11
|
+
@comparator = comparator
|
12
|
+
@value = value
|
13
|
+
@negated = false
|
14
|
+
|
15
|
+
@attribute = @attribute.to_sym if @attribute.respond_to? :to_sym
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_db
|
19
|
+
public_send @comparator
|
20
|
+
end
|
21
|
+
|
22
|
+
def equals
|
23
|
+
if @negated
|
24
|
+
{ @attribute => { '$ne' => @value } }
|
25
|
+
else
|
26
|
+
{ @attribute => @value }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def function func
|
31
|
+
f = { func => @value }
|
32
|
+
|
33
|
+
if @negated
|
34
|
+
{ @attribute => { '$not' => f } }
|
35
|
+
else
|
36
|
+
{ @attribute => f }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def less_than
|
41
|
+
function '$lt'
|
42
|
+
end
|
43
|
+
|
44
|
+
def lte
|
45
|
+
function '$lte'
|
46
|
+
end
|
47
|
+
|
48
|
+
def greater_than
|
49
|
+
function '$gt'
|
50
|
+
end
|
51
|
+
|
52
|
+
def gte
|
53
|
+
function '$gte'
|
54
|
+
end
|
55
|
+
|
56
|
+
def not_equal
|
57
|
+
function '$ne'
|
58
|
+
end
|
59
|
+
|
60
|
+
def in
|
61
|
+
function '$in'
|
62
|
+
end
|
63
|
+
|
64
|
+
def matches
|
65
|
+
if @negated
|
66
|
+
{ @attribute => { '$not' => @value } }
|
67
|
+
else
|
68
|
+
{ @attribute => @value }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def | other
|
73
|
+
QueryUnion.new(self, other)
|
74
|
+
end
|
75
|
+
|
76
|
+
def & other
|
77
|
+
QueryIntersection.new(self, other)
|
78
|
+
end
|
79
|
+
|
80
|
+
def negate
|
81
|
+
expr = dup
|
82
|
+
expr.negated = true
|
83
|
+
expr
|
84
|
+
end
|
85
|
+
|
86
|
+
def == other
|
87
|
+
attribute == other.attribute &&
|
88
|
+
comparator == other.comparator &&
|
89
|
+
value == other.value &&
|
90
|
+
negated == other.negated
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'perpetuity/data_injectable'
|
2
|
+
require 'perpetuity/reference'
|
3
|
+
|
4
|
+
module Perpetuity
|
5
|
+
class MongoDB
|
6
|
+
class Serializer
|
7
|
+
include DataInjectable
|
8
|
+
|
9
|
+
attr_reader :mapper, :mapper_registry
|
10
|
+
|
11
|
+
def initialize(mapper)
|
12
|
+
@mapper = mapper
|
13
|
+
@class = mapper.mapped_class
|
14
|
+
@mapper_registry = mapper.mapper_registry
|
15
|
+
end
|
16
|
+
|
17
|
+
def attribute_for object, attribute_name
|
18
|
+
object.instance_variable_get("@#{attribute_name}")
|
19
|
+
end
|
20
|
+
|
21
|
+
def has_attribute? object, attribute_name
|
22
|
+
object.instance_variable_defined? "@#{attribute_name}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def serialize object
|
26
|
+
attrs = mapper.attribute_set.map do |attrib|
|
27
|
+
next unless has_attribute? object, attrib.name
|
28
|
+
|
29
|
+
value = attribute_for object, attrib.name
|
30
|
+
|
31
|
+
serialized_value = if value.is_a? Reference
|
32
|
+
serialize_reference value
|
33
|
+
elsif value.is_a? Array
|
34
|
+
serialize_array(value, attrib.embedded?)
|
35
|
+
elsif mapper.data_source.can_serialize? value
|
36
|
+
value
|
37
|
+
elsif mapper_registry.has_mapper?(value.class)
|
38
|
+
serialize_with_foreign_mapper(value, attrib.embedded?)
|
39
|
+
else
|
40
|
+
marshal(value)
|
41
|
+
end
|
42
|
+
|
43
|
+
[attrib.name.to_s, serialized_value]
|
44
|
+
end
|
45
|
+
|
46
|
+
Hash[attrs.compact]
|
47
|
+
end
|
48
|
+
|
49
|
+
def serialize_changes changed, original
|
50
|
+
Hash[Array(serialize(changed)) - Array(serialize(original))]
|
51
|
+
end
|
52
|
+
|
53
|
+
def unserialize data
|
54
|
+
if data.is_a? Array
|
55
|
+
unserialize_object_array data
|
56
|
+
else
|
57
|
+
object = unserialize_object(data)
|
58
|
+
give_id_to object
|
59
|
+
object
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def unserialize_object data, klass=@class
|
64
|
+
if data.is_a? Hash
|
65
|
+
object = klass.allocate
|
66
|
+
data.each do |attr, value|
|
67
|
+
inject_attribute object, attr, unserialize_attribute(value)
|
68
|
+
end
|
69
|
+
object
|
70
|
+
else
|
71
|
+
unserialize_attribute data
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def unserialize_object_array objects
|
76
|
+
objects.map do |data|
|
77
|
+
object = unserialize_object data
|
78
|
+
give_id_to object
|
79
|
+
object
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def unserialize_attribute data
|
84
|
+
return data.map { |i| unserialize_attribute i } if data.is_a? Array
|
85
|
+
return data unless data.is_a? Hash
|
86
|
+
metadata = data.fetch("__metadata__", {})
|
87
|
+
marshaled = data.fetch("__marshaled__", false)
|
88
|
+
|
89
|
+
if marshaled
|
90
|
+
value = data.fetch("value")
|
91
|
+
return unmarshal(value)
|
92
|
+
end
|
93
|
+
|
94
|
+
if metadata.any?
|
95
|
+
klass = metadata['class'].split('::').inject(Kernel) do |scope, const_name|
|
96
|
+
scope.const_get(const_name)
|
97
|
+
end
|
98
|
+
id = metadata['id']
|
99
|
+
|
100
|
+
if id
|
101
|
+
Reference.new(klass, id)
|
102
|
+
else
|
103
|
+
unserialize_object(data, klass)
|
104
|
+
end
|
105
|
+
else
|
106
|
+
data
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def serialize_with_foreign_mapper value, embedded = false
|
111
|
+
if embedded
|
112
|
+
value_mapper = mapper_registry[value.class]
|
113
|
+
value_serializer = Serializer.new(value_mapper)
|
114
|
+
attr = value_serializer.serialize(value)
|
115
|
+
attr.merge '__metadata__' => { 'class' => value.class }
|
116
|
+
else
|
117
|
+
serialize_reference(value)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def serialize_array enum, embedded
|
122
|
+
enum.map do |value|
|
123
|
+
if value.is_a? Reference
|
124
|
+
serialize_reference value
|
125
|
+
elsif value.is_a? Array
|
126
|
+
serialize_array(value)
|
127
|
+
elsif mapper.data_source.can_serialize? value
|
128
|
+
value
|
129
|
+
elsif mapper_registry.has_mapper?(value.class)
|
130
|
+
if embedded
|
131
|
+
{
|
132
|
+
'__metadata__' => {
|
133
|
+
'class' => value.class.to_s
|
134
|
+
}
|
135
|
+
}.merge mapper_registry[value.class].serialize(value)
|
136
|
+
else
|
137
|
+
serialize_reference value
|
138
|
+
end
|
139
|
+
else
|
140
|
+
marshal value
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def serialize_reference value
|
146
|
+
if value.is_a? Reference
|
147
|
+
reference = value
|
148
|
+
else
|
149
|
+
unless mapper.persisted? value
|
150
|
+
mapper_registry[value.class].insert value
|
151
|
+
end
|
152
|
+
reference = Reference.new(value.class.to_s, mapper.id_for(value))
|
153
|
+
end
|
154
|
+
{
|
155
|
+
'__metadata__' => {
|
156
|
+
'class' => reference.klass.to_s,
|
157
|
+
'id' => reference.id
|
158
|
+
}
|
159
|
+
}
|
160
|
+
end
|
161
|
+
|
162
|
+
def marshal value
|
163
|
+
{
|
164
|
+
'__marshaled__' => true,
|
165
|
+
'value' => Marshal.dump(value)
|
166
|
+
}
|
167
|
+
end
|
168
|
+
|
169
|
+
def unmarshal value
|
170
|
+
Marshal.load(value)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'perpetuity/mongodb/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "perpetuity-mongodb"
|
8
|
+
spec.version = Perpetuity::Mongodb::VERSION
|
9
|
+
spec.authors = ["Jamie Gaskins"]
|
10
|
+
spec.email = ["jgaskins@gmail.com"]
|
11
|
+
spec.description = %q{MongoDB adapter for Perpetuity}
|
12
|
+
spec.summary = %q{MongoDB adapter for Perpetuity}
|
13
|
+
spec.homepage = "https://github.com/jgaskins/perpetuity-mongodb"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rspec"
|
23
|
+
spec.add_development_dependency "rake"
|
24
|
+
spec.add_runtime_dependency "perpetuity", "~> 1.0.0.beta"
|
25
|
+
spec.add_runtime_dependency "moped"
|
26
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'perpetuity/mongodb/index'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
class MongoDB
|
5
|
+
describe Index do
|
6
|
+
let(:attribute) { double(name: 'name') }
|
7
|
+
let(:index) { Index.new(Object, attribute) }
|
8
|
+
|
9
|
+
it 'is not active by default' do
|
10
|
+
index.should_not be_active
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'can be activated' do
|
14
|
+
index.activate!
|
15
|
+
index.should be_active
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'can be unique' do
|
19
|
+
index = Index.new(Object, attribute, unique: true)
|
20
|
+
index.should be_unique
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'is not unique by default' do
|
24
|
+
index.should_not be_unique
|
25
|
+
end
|
26
|
+
|
27
|
+
describe 'index ordering' do
|
28
|
+
it 'can be ordered in ascending order' do
|
29
|
+
index = Index.new(Object, attribute, order: :ascending)
|
30
|
+
index.order.should be :ascending
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'is ordered ascending by default' do
|
34
|
+
index.order.should be :ascending
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'can be ordered in descending order' do
|
38
|
+
index = Index.new(Object, attribute, order: :descending)
|
39
|
+
index.order.should be :descending
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'perpetuity/mongodb/query_attribute'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
describe MongoDB::QueryAttribute do
|
5
|
+
let(:attribute) { MongoDB::QueryAttribute.new :attribute_name }
|
6
|
+
subject { attribute }
|
7
|
+
|
8
|
+
its(:name) { should == :attribute_name }
|
9
|
+
|
10
|
+
it 'allows checking subattributes' do
|
11
|
+
attribute.title.name.should == :'attribute_name.title'
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'wraps .id subattribute in metadata' do
|
15
|
+
attribute.id.name.should == :'attribute_name.__metadata__.id'
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'wraps .klass subattribute in metadata' do
|
19
|
+
attribute.klass.name.should == :'attribute_name.__metadata__.class'
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'checks for equality' do
|
23
|
+
(attribute == 1).should be_a MongoDB::QueryExpression
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'checks for less than' do
|
27
|
+
(attribute < 1).should be_a MongoDB::QueryExpression
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'checks for <=' do
|
31
|
+
(attribute <= 1).should be_a MongoDB::QueryExpression
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'checks for greater than' do
|
35
|
+
(attribute > 1).should be_a MongoDB::QueryExpression
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'checks for >=' do
|
39
|
+
(attribute >= 1).should be_a MongoDB::QueryExpression
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'checks for inequality' do
|
43
|
+
(attribute != 1).should be_a MongoDB::QueryExpression
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'checks for regexp matches' do
|
47
|
+
(attribute =~ /value/).should be_a MongoDB::QueryExpression
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'checks for inclusion' do
|
51
|
+
(attribute.in [1, 2, 3]).should be_a MongoDB::QueryExpression
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'checks for its own truthiness' do
|
55
|
+
attribute.to_db.should == ((attribute != false) & (attribute != nil)).to_db
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'perpetuity/mongodb/query_expression'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
describe MongoDB::QueryExpression do
|
5
|
+
let(:expression) { MongoDB::QueryExpression.new :attribute, :equals, :value }
|
6
|
+
subject { expression }
|
7
|
+
|
8
|
+
describe 'translation to Mongo expressions' do
|
9
|
+
it 'equality expression' do
|
10
|
+
expression.to_db.should == { attribute: :value }
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'less-than expression' do
|
14
|
+
expression.comparator = :less_than
|
15
|
+
expression.to_db.should == { attribute: { '$lt' => :value } }
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'less-than-or-equal-to expression' do
|
19
|
+
expression.comparator = :lte
|
20
|
+
expression.to_db.should == { attribute: { '$lte' => :value } }
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'greater-than expression' do
|
24
|
+
expression.comparator = :greater_than
|
25
|
+
expression.to_db.should == { attribute: { '$gt' => :value } }
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'greater-than-or-equal-to expression' do
|
29
|
+
expression.comparator = :gte
|
30
|
+
expression.to_db.should == { attribute: { '$gte' => :value } }
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'not-equal' do
|
34
|
+
expression.comparator = :not_equal
|
35
|
+
expression.to_db.should == { attribute: { '$ne' => :value } }
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'checks for inclusion' do
|
39
|
+
expression.comparator = :in
|
40
|
+
expression.to_db.should == { attribute: { '$in' => :value } }
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'checks for regexp matching' do
|
44
|
+
expression.comparator = :matches
|
45
|
+
expression.to_db.should == { attribute: :value }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'unions' do
|
50
|
+
let(:lhs) { MongoDB::QueryExpression.new :first, :equals, :one }
|
51
|
+
let(:rhs) { MongoDB::QueryExpression.new :second, :equals, :two }
|
52
|
+
|
53
|
+
it 'converts | to an $or query' do
|
54
|
+
(lhs | rhs).to_db.should == { '$or' => [{first: :one}, {second: :two}] }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe 'intersections' do
|
59
|
+
let(:lhs) { MongoDB::QueryExpression.new :first, :equals, :one }
|
60
|
+
let(:rhs) { MongoDB::QueryExpression.new :second, :equals, :two }
|
61
|
+
|
62
|
+
it 'converts & to an $and query' do
|
63
|
+
(lhs & rhs).to_db.should == { '$and' => [{first: :one}, {second: :two}] }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|