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.
@@ -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,16 @@
1
+ module Perpetuity
2
+ class MongoDB
3
+ class QueryIntersection
4
+ attr_reader :lhs, :rhs
5
+
6
+ def initialize lhs, rhs
7
+ @lhs = lhs
8
+ @rhs = rhs
9
+ end
10
+
11
+ def to_db
12
+ { '$and' => [lhs.to_db, rhs.to_db] }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module Perpetuity
2
+ class MongoDB
3
+ class QueryUnion
4
+ attr_reader :lhs, :rhs
5
+
6
+ def initialize lhs, rhs
7
+ @lhs = lhs
8
+ @rhs = rhs
9
+ end
10
+
11
+ def to_db
12
+ { '$or' => [lhs.to_db, rhs.to_db] }
13
+ end
14
+ end
15
+ end
16
+ 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,5 @@
1
+ module Perpetuity
2
+ module Mongodb
3
+ VERSION = "1.0.0.beta"
4
+ end
5
+ 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