perpetuity 0.7.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f8013375d75e8b9cdc7a171ff15c51624709b3ba
4
- data.tar.gz: 5f2a8546b037800548e2ec5588302552ba09323d
3
+ metadata.gz: 99bbdaa723ccda7b072e0b224d45508ee85173f2
4
+ data.tar.gz: 1b1fef3e2811ee1516d36c97b82de71d524279dc
5
5
  SHA512:
6
- metadata.gz: fbdf6dc761c95fffa557e4a430f0b711cd52bbedf1aecf8e85200d9287bd385f27620c7c61606597dc51044312929d4576ac063a9ecde7c223c087e6b2438851
7
- data.tar.gz: 101f30e6be3207d717dc3e0d2cfbbf27b20793fc6550c2bdd2c998cd34175444eb68a3acc54443c35535950709f3a25b28c256bb201809237cd98fa97067c70f
6
+ metadata.gz: 3768caff24b90ecf53787b530ac59f95fa920275a43a15fc906c2770e3d78e3a3ad07360b4ae4fb55b6976060ad5a8e8ad1f4d35bf842447bfff4c114f6e7cf8
7
+ data.tar.gz: 441477056696558e7be0fc26e789f0f0d996e48b5df471529a315d610b1891ab0cf61ef174f9c60ad5e7bac86d5664da5871fa849b7b610d23a9895b1143fd7e
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## Version 0.7.1
2
+
3
+ - Only unmarshal attributes that we marshaled to begin with. This disallows the use of false marshaled objects. — with [Kevin Sjöberg](https://github.com/KevinSjoberg)
4
+ - Allow insertion of multiple objects in `Mapper#insert`
5
+ - Alias `Retrieval#limit` as `Retrieval#take` for `Enumerable` compatibility
6
+ - Leave result cache when branching to new retrievals if previous retrieval had triggered a query
7
+ - Silence warnings (some still exist in Moped, unfortunately)
8
+ - Add finding based on attribute truthiness. For example: `mapper.find { |obj| obj.name }` finds objects whose `name` is neither `nil` nor `false`.
9
+ - When you remove an index call from the mapper DSL, `Mapper#reindex!` now removes that index from the DB
10
+ - Previously activated indexes in the DB are converted to `Perpetuity::Attribute`s rather than stored in the format specific to the DB driver
11
+
1
12
  ## Version 0.7.0
2
13
 
3
14
  - Add `Perpetuity::RailsModel`, an ActiveModel-compliant mixin
@@ -1,7 +1,7 @@
1
1
  module Perpetuity
2
2
  class Attribute
3
3
  attr_reader :name, :type
4
- def initialize(name, type, options = {})
4
+ def initialize(name, type=nil, options = {})
5
5
  @name = name
6
6
  @type = type
7
7
 
@@ -47,6 +47,12 @@ module Perpetuity
47
47
 
48
48
  def reindex!
49
49
  indexes.each { |index| data_source.activate_index! index }
50
+ (data_source.active_indexes(mapped_class) - indexes).reject do |index|
51
+ # TODO: Make this not MongoDB-specific
52
+ index.attribute.name.to_s == '_id'
53
+ end.each do |index|
54
+ data_source.remove_index index
55
+ end
50
56
  end
51
57
 
52
58
  def attributes
@@ -59,14 +65,26 @@ module Perpetuity
59
65
 
60
66
  def insert object
61
67
  raise "#{object} is invalid and cannot be persisted." unless self.class.validations.valid?(object)
62
- serializable_attributes = serialize(object)
63
- if o_id = object.instance_exec(&self.class.id)
64
- serializable_attributes[:id] = o_id
68
+ objects = Array(object)
69
+ serialized_objects = objects.map do |obj|
70
+ attributes = serialize(obj)
71
+ if o_id = obj.instance_exec(&self.class.id)
72
+ attributes[:id] = o_id
73
+ end
74
+
75
+ attributes
65
76
  end
66
77
 
67
- new_id = data_source.insert mapped_class, serializable_attributes
68
- give_id_to object, new_id
69
- new_id
78
+ new_ids = data_source.insert(mapped_class, serialized_objects)
79
+ objects.each_with_index do |obj, index|
80
+ give_id_to obj, new_ids[index]
81
+ end
82
+
83
+ if object.is_a? Array
84
+ new_ids
85
+ else
86
+ new_ids.first
87
+ end
70
88
  end
71
89
 
72
90
  def self.data_source(configuration=Perpetuity.configuration)
@@ -102,7 +120,7 @@ module Perpetuity
102
120
  end
103
121
 
104
122
  def select &block
105
- retrieve data_source.query(&block).to_db
123
+ retrieve data_source.query(&block)
106
124
  end
107
125
 
108
126
  alias :find_all :select
@@ -127,7 +145,7 @@ module Perpetuity
127
145
  alias :detect :find
128
146
 
129
147
  def reject &block
130
- retrieve data_source.negate_query(&block).to_db
148
+ retrieve data_source.negate_query(&block)
131
149
  end
132
150
 
133
151
  def delete object
@@ -192,7 +210,7 @@ module Perpetuity
192
210
  end
193
211
 
194
212
  def id_for object
195
- object.instance_variable_get(:@id)
213
+ object.instance_variable_get(:@id) if persisted?(object)
196
214
  end
197
215
 
198
216
  def self.validate &block
@@ -221,8 +239,8 @@ module Perpetuity
221
239
 
222
240
  private
223
241
 
224
- def retrieve criteria={}
225
- Perpetuity::Retrieval.new self, criteria
242
+ def retrieve query=data_source.query
243
+ Perpetuity::Retrieval.new self, query
226
244
  end
227
245
  end
228
246
  end
@@ -16,9 +16,10 @@ module Perpetuity
16
16
  unless @mappers.has_key? klass
17
17
  raise KeyError, "No mapper for #{klass}"
18
18
  end
19
+ @mappers[klass]
19
20
  end
20
21
 
21
- @mappers[klass].new(self)
22
+ mapper_class.new(self)
22
23
  end
23
24
 
24
25
  def []= klass, mapper
@@ -26,7 +27,7 @@ module Perpetuity
26
27
  end
27
28
 
28
29
  def each &block
29
- @mappers.each &block
30
+ @mappers.each(&block)
30
31
  end
31
32
 
32
33
  def load_mappers
@@ -0,0 +1,11 @@
1
+ module Perpetuity
2
+ class NilQuery
3
+ def self.new
4
+ @instance ||= allocate
5
+ end
6
+
7
+ def to_db
8
+ {}
9
+ end
10
+ end
11
+ end
@@ -1,10 +1,16 @@
1
1
  require 'perpetuity/mongodb/query_attribute'
2
+ require 'perpetuity/mongodb/nil_query'
2
3
 
3
4
  module Perpetuity
4
5
  class MongoDB
5
6
  class Query
7
+ attr_reader :query
6
8
  def initialize &block
7
- @query = block.call(self)
9
+ if block_given?
10
+ @query = block.call(self)
11
+ else
12
+ @query = NilQuery.new
13
+ end
8
14
  end
9
15
 
10
16
  def to_db
@@ -18,6 +24,10 @@ module Perpetuity
18
24
  def method_missing missing_method
19
25
  QueryAttribute.new missing_method
20
26
  end
27
+
28
+ def == other
29
+ query == other.query
30
+ end
21
31
  end
22
32
  end
23
33
  end
@@ -46,6 +46,10 @@ module Perpetuity
46
46
  name
47
47
  end
48
48
 
49
+ def to_db
50
+ ((self != false) & (self != nil)).to_db
51
+ end
52
+
49
53
  def method_missing name
50
54
  if name.to_s == 'id'
51
55
  name = :"#{self.name}.__metadata__.#{name}"
@@ -4,7 +4,7 @@ require 'perpetuity/mongodb/query_intersection'
4
4
  module Perpetuity
5
5
  class MongoDB
6
6
  class QueryExpression
7
- attr_accessor :comparator, :negated
7
+ attr_accessor :attribute, :comparator, :negated, :value
8
8
 
9
9
  def initialize attribute, comparator, value
10
10
  @attribute = attribute
@@ -82,6 +82,13 @@ module Perpetuity
82
82
  expr.negated = true
83
83
  expr
84
84
  end
85
+
86
+ def == other
87
+ attribute == other.attribute &&
88
+ comparator == other.comparator &&
89
+ value == other.value &&
90
+ negated == other.negated
91
+ end
85
92
  end
86
93
  end
87
94
  end
@@ -36,7 +36,7 @@ module Perpetuity
36
36
  elsif mapper_registry.has_mapper?(value.class)
37
37
  serialize_with_foreign_mapper(value, attrib.embedded?)
38
38
  else
39
- Marshal.dump(value)
39
+ marshal(value)
40
40
  end
41
41
 
42
42
  [attrib.name.to_s, serialized_value]
@@ -76,25 +76,26 @@ module Perpetuity
76
76
  end
77
77
 
78
78
  def unserialize_attribute data
79
- if data.is_a?(String) && data.start_with?("\u0004") # if it's marshaled
80
- Marshal.load(data)
81
- elsif data.is_a? Array
82
- data.map { |i| unserialize_attribute i }
83
- elsif data.is_a? Hash
84
- metadata = data.delete('__metadata__')
85
- if metadata
86
- klass = metadata['class'].split('::').inject(Kernel) do |scope, const_name|
87
- scope.const_get(const_name)
88
- end
89
- id = metadata['id']
79
+ return data.map { |i| unserialize_attribute i } if data.is_a? Array
80
+ return data unless data.is_a? Hash
81
+ metadata = data.fetch("__metadata__", {})
82
+ marshaled = data.fetch("__marshaled__", false)
83
+
84
+ if marshaled
85
+ value = data.fetch("value")
86
+ return unmarshal(value)
87
+ end
90
88
 
91
- if id
92
- object = Reference.new(klass, id)
93
- else
94
- object = unserialize_object(data, klass)
95
- end
89
+ if metadata.any?
90
+ klass = metadata['class'].split('::').inject(Kernel) do |scope, const_name|
91
+ scope.const_get(const_name)
92
+ end
93
+ id = metadata['id']
94
+
95
+ if id
96
+ Reference.new(klass, id)
96
97
  else
97
- data
98
+ unserialize_object(data, klass)
98
99
  end
99
100
  else
100
101
  data
@@ -131,7 +132,7 @@ module Perpetuity
131
132
  serialize_reference value
132
133
  end
133
134
  else
134
- Marshal.dump(value)
135
+ marshal value
135
136
  end
136
137
  end
137
138
  end
@@ -152,6 +153,17 @@ module Perpetuity
152
153
  }
153
154
  }
154
155
  end
156
+
157
+ def marshal value
158
+ {
159
+ '__marshaled__' => true,
160
+ 'value' => Marshal.dump(value)
161
+ }
162
+ end
163
+
164
+ def unmarshal value
165
+ Marshal.load(value)
166
+ end
155
167
  end
156
168
  end
157
169
  end
@@ -1,9 +1,11 @@
1
1
  require 'moped'
2
2
  require 'perpetuity/mongodb/query'
3
+ require 'perpetuity/mongodb/nil_query'
3
4
  require 'perpetuity/mongodb/index'
4
5
  require 'perpetuity/mongodb/serializer'
5
6
  require 'set'
6
7
  require 'perpetuity/exceptions/duplicate_key_error'
8
+ require 'perpetuity/attribute'
7
9
 
8
10
  module Perpetuity
9
11
  class MongoDB
@@ -45,11 +47,18 @@ module Perpetuity
45
47
  database[klass.to_s]
46
48
  end
47
49
 
48
- def insert klass, attributes
49
- attributes[:_id] = attributes.delete(:id) || Moped::BSON::ObjectId.new
50
+ def insert klass, objects
51
+ if objects.is_a? Array
52
+ objects.each do |object|
53
+ object[:_id] = object.delete(:id) || Moped::BSON::ObjectId.new
54
+ end
55
+
56
+ collection(klass).insert objects
57
+ objects.map { |object| object[:_id] }
58
+ else
59
+ insert(klass, [objects]).first
60
+ end
50
61
 
51
- collection(klass).insert attributes
52
- attributes[:_id]
53
62
  rescue Moped::Errors::OperationFailure => e
54
63
  if e.message =~ /duplicate key/
55
64
  e.message =~ /\$(\w+)_\d.*dup key: { : (.*) }/
@@ -59,8 +68,8 @@ module Perpetuity
59
68
  end
60
69
  end
61
70
 
62
- def count klass, criteria={}, &block
63
- q = block_given? ? query(&block).to_db : criteria
71
+ def count klass, criteria=nil_query, &block
72
+ q = block_given? ? query(&block).to_db : criteria.to_db
64
73
  collection(klass).find(q).count
65
74
  end
66
75
 
@@ -77,7 +86,7 @@ module Perpetuity
77
86
 
78
87
  def retrieve klass, criteria, options = {}
79
88
  # MongoDB uses '_id' as its ID field.
80
- criteria = to_bson_id(criteria)
89
+ criteria = to_bson_id(criteria.to_db)
81
90
 
82
91
  skipped = options.fetch(:skip) { 0 }
83
92
 
@@ -129,7 +138,7 @@ module Perpetuity
129
138
  end
130
139
 
131
140
  def all klass
132
- retrieve klass, {}, {}
141
+ retrieve klass, nil_query, {}
133
142
  end
134
143
 
135
144
  def delete id, klass
@@ -152,6 +161,10 @@ module Perpetuity
152
161
  Query.new(&block)
153
162
  end
154
163
 
164
+ def nil_query
165
+ NilQuery.new
166
+ end
167
+
155
168
  def negate_query &block
156
169
  Query.new(&block).negate
157
170
  end
@@ -173,7 +186,7 @@ module Perpetuity
173
186
  key = index['key'].keys.first
174
187
  direction = index['key'][key]
175
188
  unique = index['unique']
176
- Index.new(klass, key, order: Index::KEY_ORDERS[direction], unique: unique)
189
+ Index.new(klass, Attribute.new(key), order: Index::KEY_ORDERS[direction], unique: unique)
177
190
  end.to_set
178
191
  end
179
192
 
@@ -3,12 +3,12 @@ require 'perpetuity/reference'
3
3
  module Perpetuity
4
4
  class Retrieval
5
5
  include Enumerable
6
- attr_accessor :sort_attribute, :sort_direction, :result_limit, :result_page, :result_offset
6
+ attr_accessor :sort_attribute, :sort_direction, :result_limit, :result_page, :result_offset, :result_cache
7
7
 
8
- def initialize mapper, criteria
8
+ def initialize mapper, query
9
9
  @mapper = mapper
10
10
  @class = mapper.mapped_class
11
- @criteria = criteria
11
+ @query = query
12
12
  @data_source = mapper.data_source
13
13
  end
14
14
 
@@ -16,6 +16,7 @@ module Perpetuity
16
16
  retrieval = clone
17
17
  retrieval.sort_attribute = attribute
18
18
  retrieval.sort_direction = :ascending
19
+ retrieval.clear_cache
19
20
 
20
21
  retrieval
21
22
  end
@@ -23,6 +24,7 @@ module Perpetuity
23
24
  def reverse
24
25
  retrieval = clone
25
26
  retrieval.sort_direction = retrieval.sort_direction == :descending ? :ascending : :descending
27
+ retrieval.clear_cache
26
28
 
27
29
  retrieval
28
30
  end
@@ -32,6 +34,7 @@ module Perpetuity
32
34
  retrieval.result_limit ||= 20
33
35
  retrieval.result_page = page
34
36
  retrieval.result_offset = (page - 1) * retrieval.result_limit
37
+ retrieval.clear_cache
35
38
  retrieval
36
39
  end
37
40
 
@@ -39,6 +42,7 @@ module Perpetuity
39
42
  retrieval = clone
40
43
  retrieval.result_limit = per
41
44
  retrieval.result_offset = (retrieval.result_page - 1) * per
45
+ retrieval.clear_cache
42
46
  retrieval
43
47
  end
44
48
 
@@ -47,11 +51,11 @@ module Perpetuity
47
51
  end
48
52
 
49
53
  def to_a
50
- @results ||= @data_source.unserialize(@data_source.retrieve(@class, @criteria, options), @mapper)
54
+ @result_cache ||= @data_source.unserialize(@data_source.retrieve(@class, @query, options), @mapper)
51
55
  end
52
56
 
53
57
  def count
54
- @data_source.count(@class, @criteria)
58
+ @data_source.count(@class, @query)
55
59
  end
56
60
 
57
61
  def first
@@ -84,15 +88,22 @@ module Perpetuity
84
88
  def limit lim
85
89
  retrieval = clone
86
90
  retrieval.result_limit = lim
91
+ retrieval.clear_cache
87
92
 
88
93
  retrieval
89
94
  end
95
+ alias_method :take, :limit
90
96
 
91
97
  def drop count
92
98
  retrieval = clone
93
99
  retrieval.result_offset = count
100
+ retrieval.clear_cache
94
101
 
95
102
  retrieval
96
103
  end
104
+
105
+ def clear_cache
106
+ @result_cache = nil
107
+ end
97
108
  end
98
109
  end
@@ -1,3 +1,3 @@
1
1
  module Perpetuity
2
- VERSION = "0.7.0"
2
+ VERSION = "0.7.1"
3
3
  end
@@ -8,6 +8,13 @@ describe 'indexing' do
8
8
  index :name, unique: true
9
9
  end
10
10
  end
11
+ let(:mapper_class_without_index) do
12
+ klass = mapper_class.dup
13
+ klass.new.indexes.reject! do |index|
14
+ index.attribute.name == :name
15
+ end
16
+ klass
17
+ end
11
18
  let(:mapper) { mapper_class.new }
12
19
  let(:name_index) do
13
20
  mapper.indexes.find do |index|
@@ -39,5 +46,14 @@ describe 'indexing' do
39
46
  it 'specifies uniqueness of the index' do
40
47
  name_index.should be_unique
41
48
  end
49
+
50
+ it 'removes other indexes' do
51
+ mapper.reindex!
52
+ mapper_without_index = mapper_class_without_index.new
53
+ mapper_without_index.reindex!
54
+ mapper.data_source.active_indexes(Object).any? do |index|
55
+ index.attribute.name.to_s == 'name'
56
+ end.should be_false
57
+ end
42
58
  end
43
59
 
@@ -48,6 +48,15 @@ module Perpetuity
48
48
  its(:password) { should == password }
49
49
  end
50
50
 
51
+ it 'inserts documents into a collection' do
52
+ expect { mongo.insert klass, name: 'foo' }.to change { mongo.count klass }.by 1
53
+ end
54
+
55
+ it 'inserts multiple documents into a collection' do
56
+ expect { mongo.insert klass, [{name: 'foo'}, {name: 'bar'}] }
57
+ .to change { mongo.count klass }.by 2
58
+ end
59
+
51
60
  it 'removes all documents from a collection' do
52
61
  mongo.insert klass, {}
53
62
  mongo.delete_all klass
@@ -77,7 +86,7 @@ module Perpetuity
77
86
 
78
87
  it 'gets all of the documents in a collection' do
79
88
  values = [{value: 1}, {value: 2}]
80
- mongo.should_receive(:retrieve).with(Object, {}, {})
89
+ mongo.should_receive(:retrieve).with(Object, mongo.nil_query, {})
81
90
  .and_return(values)
82
91
  mongo.all(Object).should == values
83
92
  end
@@ -86,7 +95,7 @@ module Perpetuity
86
95
  time = Time.now.utc
87
96
  id = mongo.insert Object, {inserted: time}
88
97
 
89
- object = mongo.retrieve(Object, id: id.to_s).first
98
+ object = mongo.retrieve(Object, mongo.query{|o| o.id == id.to_s }).first
90
99
  retrieved_time = object["inserted"]
91
100
  retrieved_time.to_f.should be_within(0.001).of time.to_f
92
101
  end
@@ -148,9 +157,10 @@ module Perpetuity
148
157
  id = mongo.insert klass, count: 1
149
158
  mongo.increment klass, id, :count
150
159
  mongo.increment klass, id, :count, 10
151
- mongo.retrieve(klass, id: id).first['count'].should == 12
160
+ query = mongo.query { |o| o.id == id }
161
+ mongo.retrieve(klass, query).first['count'].should be == 12
152
162
  mongo.increment klass, id, :count, -1
153
- mongo.retrieve(klass, id: id).first['count'].should == 11
163
+ mongo.retrieve(klass, query).first['count'].should be == 11
154
164
  end
155
165
  end
156
166
 
@@ -11,6 +11,13 @@ describe 'Persistence' do
11
11
  mapper.find(mapper.id_for(article)).title.should eq 'I have a title'
12
12
  end
13
13
 
14
+ it 'persists multiple objects' do
15
+ mapper.delete_all
16
+ articles = 2.times.map { Article.new(SecureRandom.hex) }
17
+ expect { mapper.insert articles }.to change { mapper.count }.by 2
18
+ mapper.all.sort(:title).to_a.should == articles.sort_by(&:title)
19
+ end
20
+
14
21
  it 'returns the id of the persisted object' do
15
22
  article = Article.new
16
23
  mapper.insert(article).should eq mapper.id_for(article)
@@ -53,71 +53,90 @@ describe "retrieval" do
53
53
  end
54
54
 
55
55
  describe "Array-like syntax" do
56
- let(:draft) { Article.new 'Draft', 'draft content', nil, Time.now + 30 }
57
- let(:published) { Article.new 'Published', 'content', nil, Time.now - 30, 3 }
58
-
59
- let(:published_id) { mapper.id_for published }
60
- let(:draft_id) { mapper.id_for draft }
61
-
62
- before do
63
- mapper.insert draft
64
- mapper.insert published
65
- end
66
-
67
- it 'selects objects using equality' do
68
- selected = mapper.select { |article| article.title == 'Published' }
69
- ids = selected.map { |article| mapper.id_for article }
70
- ids.should include published_id
71
- ids.should_not include draft_id
72
- end
73
-
74
- it 'selects objects using greater-than' do
75
- selected = mapper.select { |article| article.published_at < Time.now }
76
- ids = selected.map { |article| mapper.id_for article }
77
- ids.should include published_id
78
- ids.should_not include draft_id
79
- end
80
-
81
- it 'selects objects using greater-than-or-equal' do
82
- selected = mapper.select { |article| article.views >= 3 }
83
- ids = selected.map { |article| mapper.id_for article }
84
- ids.should include published_id
85
- ids.should_not include draft_id
86
- end
87
-
88
- it 'selects objects using less-than' do
89
- selected = mapper.select { |article| article.views < 3 }
90
- ids = selected.map { |article| mapper.id_for article }
91
- ids.should include draft_id
92
- ids.should_not include published_id
93
- end
94
-
95
- it 'selects objects using less-than-or-equal' do
96
- selected = mapper.select { |article| article.views <= 0 }
97
- ids = selected.map { |article| mapper.id_for article }
98
- ids.should include draft_id
99
- ids.should_not include published_id
100
- end
101
-
102
- it 'selects objects using inequality' do
103
- selected = mapper.select { |article| article.title != 'Draft' }
104
- ids = selected.map { |article| mapper.id_for article }
105
- ids.should_not include draft_id
106
- ids.should include published_id
107
- end
108
-
109
- it 'selects objects using regular expressions' do
110
- selected = mapper.select { |article| article.title =~ /Pub/ }
111
- ids = selected.map { |article| mapper.id_for article }
112
- ids.should include published_id
113
- ids.should_not include draft_id
114
- end
115
-
116
- it 'selects objects using inclusion' do
117
- selected = mapper.select { |article| article.title.in %w( Published ) }
118
- ids = selected.map { |article| mapper.id_for article }
119
- ids.should include published_id
120
- ids.should_not include draft_id
56
+ describe 'using comparison operators' do
57
+ let(:draft) { Article.new 'Draft', 'draft content', nil, Time.now + 30 }
58
+ let(:published) { Article.new 'Published', 'content', nil, Time.now - 30, 3 }
59
+
60
+ let(:published_id) { mapper.id_for published }
61
+ let(:draft_id) { mapper.id_for draft }
62
+
63
+ before do
64
+ mapper.insert draft
65
+ mapper.insert published
66
+ end
67
+
68
+ it 'selects objects using equality' do
69
+ selected = mapper.select { |article| article.title == 'Published' }
70
+ ids = selected.map { |article| mapper.id_for article }
71
+ ids.should include published_id
72
+ ids.should_not include draft_id
73
+ end
74
+
75
+ it 'selects objects using greater-than' do
76
+ selected = mapper.select { |article| article.published_at < Time.now }
77
+ ids = selected.map { |article| mapper.id_for article }
78
+ ids.should include published_id
79
+ ids.should_not include draft_id
80
+ end
81
+
82
+ it 'selects objects using greater-than-or-equal' do
83
+ selected = mapper.select { |article| article.views >= 3 }
84
+ ids = selected.map { |article| mapper.id_for article }
85
+ ids.should include published_id
86
+ ids.should_not include draft_id
87
+ end
88
+
89
+ it 'selects objects using less-than' do
90
+ selected = mapper.select { |article| article.views < 3 }
91
+ ids = selected.map { |article| mapper.id_for article }
92
+ ids.should include draft_id
93
+ ids.should_not include published_id
94
+ end
95
+
96
+ it 'selects objects using less-than-or-equal' do
97
+ selected = mapper.select { |article| article.views <= 0 }
98
+ ids = selected.map { |article| mapper.id_for article }
99
+ ids.should include draft_id
100
+ ids.should_not include published_id
101
+ end
102
+
103
+ it 'selects objects using inequality' do
104
+ selected = mapper.select { |article| article.title != 'Draft' }
105
+ ids = selected.map { |article| mapper.id_for article }
106
+ ids.should_not include draft_id
107
+ ids.should include published_id
108
+ end
109
+
110
+ it 'selects objects using regular expressions' do
111
+ selected = mapper.select { |article| article.title =~ /Pub/ }
112
+ ids = selected.map { |article| mapper.id_for article }
113
+ ids.should include published_id
114
+ ids.should_not include draft_id
115
+ end
116
+
117
+ it 'selects objects using inclusion' do
118
+ selected = mapper.select { |article| article.title.in %w( Published ) }
119
+ ids = selected.map { |article| mapper.id_for article }
120
+ ids.should include published_id
121
+ ids.should_not include draft_id
122
+ end
123
+ end
124
+
125
+ it 'selects objects that are truthy' do
126
+ article_with_truthy_title = Article.new('I have a title')
127
+ article_with_false_title = Article.new(false)
128
+ article_with_nil_title = Article.new(nil)
129
+
130
+ false_id = mapper.insert article_with_false_title
131
+ truthy_id = mapper.insert article_with_truthy_title
132
+ nil_id = mapper.insert article_with_nil_title
133
+
134
+ selected = mapper.select { |article| article.title }
135
+ ids = selected.map { |article| mapper.id_for(article) }
136
+
137
+ ids.should include truthy_id
138
+ ids.should_not include false_id
139
+ ids.should_not include nil_id
121
140
  end
122
141
  end
123
142
 
@@ -180,7 +199,7 @@ describe "retrieval" do
180
199
  user = User.new(first_name: 'foo', last_name: 'bar')
181
200
  mapper = Perpetuity[User]
182
201
  mapper.insert user
183
- users = mapper.select { |user| user.name.first_name == 'foo' }
202
+ users = mapper.select { |u| u.name.first_name == 'foo' }
184
203
  ids = users.map { |retrieved_user| mapper.id_for(retrieved_user) }
185
204
  ids.should include mapper.id_for(user)
186
205
  end
@@ -203,4 +222,13 @@ describe "retrieval" do
203
222
  articles.should include mapper.sample
204
223
  end
205
224
  end
225
+
226
+ it 'does not unmarshal objects that were saved as strings' do
227
+ fake_title = Marshal.dump(Object.new)
228
+ id = mapper.insert Article.new(fake_title)
229
+
230
+ retrieved = mapper.find(id)
231
+ retrieved.title.should be_a String
232
+ retrieved.title.should == fake_title
233
+ end
206
234
  end
@@ -53,8 +53,8 @@ describe 'updating' do
53
53
  mapper.save retrieved_book
54
54
 
55
55
  retrieved_authors = Perpetuity[Book].find(mapper.id_for retrieved_book).authors
56
- retrieved_authors.map(&:klass).should == [User, User]
57
- retrieved_authors.map(&:id).should == [mapper.id_for(dave), mapper.id_for(andy)]
56
+ retrieved_authors.map(&:klass).should be == [User, User]
57
+ retrieved_authors.map(&:id).should be == [mapper.id_for(dave), mapper.id_for(andy)]
58
58
  end
59
59
 
60
60
  describe 'atomic increments/decrements' do
@@ -30,6 +30,8 @@ module Perpetuity
30
30
 
31
31
  context 'with multiple references' do
32
32
  it 'returns the array of dereferenced objects' do
33
+ first.instance_variable_set :@id, 1
34
+ second.instance_variable_set :@id, 2
33
35
  mapper.should_receive(:select) { objects }
34
36
  derefer.load([first_ref, second_ref]).should == objects
35
37
  end
@@ -48,8 +48,8 @@ module Perpetuity
48
48
  data_source.should_receive(:can_serialize?).with('foo') { true }
49
49
  data_source.should_receive(:insert)
50
50
  .with(Object,
51
- { 'my_attribute' => 'foo' })
52
- .and_return('bar')
51
+ [{ 'my_attribute' => 'foo' }])
52
+ .and_return(['bar'])
53
53
 
54
54
  mapper.insert(obj).should be == 'bar'
55
55
  end
@@ -61,10 +61,11 @@ module Perpetuity
61
61
 
62
62
  describe 'finding a single object' do
63
63
  let(:options) { {:attribute=>nil, :direction=>nil, :limit=>1, :skip=>nil} }
64
- let(:returned_object) { double('Retrieved Object', class: Object) }
64
+ let(:returned_object) { double('Retrieved Object', class: Object, delete: nil) }
65
65
 
66
66
  it 'finds an object by ID' do
67
- criteria = { id: 1 }
67
+ returned_object.instance_variable_set :@id, 1
68
+ criteria = data_source.query { |o| o.id == 1 }
68
69
  data_source.should_receive(:retrieve)
69
70
  .with(Object, criteria, options) { [returned_object] }
70
71
 
@@ -72,26 +73,26 @@ module Perpetuity
72
73
  end
73
74
 
74
75
  it 'finds multiple objects with a block' do
75
- criteria = { name: 'foo' }
76
+ criteria = data_source.query { |o| o.name == 'foo' }
76
77
  options = self.options.merge(limit: nil)
77
78
  data_source.should_receive(:retrieve)
78
79
  .with(Object, criteria, options) { [returned_object] }.twice
79
80
 
80
- mapper.select { |e| e.name == 'foo' }.to_a.should == [returned_object]
81
- mapper.find_all { |e| e.name == 'foo' }.to_a.should == [returned_object]
81
+ mapper.select { |e| e.name == 'foo' }.to_a.should be == [returned_object]
82
+ mapper.find_all { |e| e.name == 'foo' }.to_a.should be == [returned_object]
82
83
  end
83
84
 
84
85
  it 'finds an object with a block' do
85
- criteria = { name: 'foo' }
86
+ criteria = data_source.query { |o| o.name == 'foo' }
86
87
  data_source.should_receive(:retrieve)
87
88
  .with(Object, criteria, options) { [returned_object] }.twice
88
- mapper.find { |o| o.name == 'foo' }.should == returned_object
89
- mapper.detect { |o| o.name == 'foo' }.should == returned_object
89
+ mapper.find { |o| o.name == 'foo' }.should be == returned_object
90
+ mapper.detect { |o| o.name == 'foo' }.should be == returned_object
90
91
  end
91
92
 
92
93
  it 'caches results' do
93
94
  mapper.give_id_to returned_object, 1
94
- criteria = { id: 1 }
95
+ criteria = data_source.query { |o| o.id == 1 }
95
96
  data_source.should_receive(:retrieve)
96
97
  .with(Object, criteria, options) { [returned_object] }
97
98
  .once
@@ -101,7 +102,7 @@ module Perpetuity
101
102
  end
102
103
 
103
104
  it 'does not cache nil results' do
104
- criteria = { id: 1 }
105
+ criteria = data_source.query { |o| o.id == 1 }
105
106
  data_source.should_receive(:retrieve)
106
107
  .with(Object, criteria, options) { [] }
107
108
  .twice
@@ -50,5 +50,9 @@ module Perpetuity
50
50
  it 'checks for inclusion' do
51
51
  (attribute.in [1, 2, 3]).should be_a MongoDB::QueryExpression
52
52
  end
53
+
54
+ it 'checks for its own truthiness' do
55
+ attribute.to_db.should == ((attribute != false) & (attribute != nil)).to_db
56
+ end
53
57
  end
54
58
  end
@@ -63,11 +63,7 @@ module Perpetuity
63
63
 
64
64
  context 'with objects that have hashes as attributes' do
65
65
  let(:name_data) { {first_name: 'Jamie', last_name: 'Gaskins'} }
66
- let(:serialized_data) do
67
- {
68
- 'name' => name_data
69
- }
70
- end
66
+ let(:serialized_data) { { 'name' => name_data } }
71
67
  let(:user) { User.new(name_data) }
72
68
  let(:user_serializer) { Serializer.new(user_mapper) }
73
69
 
@@ -86,21 +82,6 @@ module Perpetuity
86
82
  end
87
83
  end
88
84
 
89
- describe 'unserializes attributes' do
90
- let(:unserializable_object) { 1.to_c }
91
- let(:serialized_attrs) { [ Marshal.dump(unserializable_object) ] }
92
- let(:objects) { serializer.unserialize(serialized_attrs) }
93
- subject { objects.first }
94
-
95
- before do
96
- user_mapper.stub(data_source: data_source)
97
- book_mapper.stub(data_source: data_source)
98
- end
99
-
100
- it { should be_a Complex }
101
- it { should eq unserializable_object}
102
- end
103
-
104
85
  describe 'with an array of references' do
105
86
  let(:author) { Reference.new(User, 1) }
106
87
  let(:title) { 'title' }
@@ -147,6 +128,73 @@ module Perpetuity
147
128
  serializer.serialize(car).should == { 'model' => car_model }
148
129
  end
149
130
  end
131
+
132
+ context 'with marshaled data' do
133
+ let(:unserializable_value) { 1..10 }
134
+
135
+ it 'stores metadata with marshal information' do
136
+ book = Book.new(unserializable_value)
137
+
138
+ book_mapper.stub(data_source: data_source)
139
+ data_source.stub(:can_serialize?).with(book.title) { false }
140
+
141
+ serializer.serialize(book).should == {
142
+ 'title' => {
143
+ '__marshaled__' => true,
144
+ 'value' => Marshal.dump(unserializable_value)
145
+ },
146
+ 'authors' => []
147
+ }
148
+ end
149
+
150
+ it 'stores marshaled attributes within arrays' do
151
+ book = Book.new([unserializable_value])
152
+ book_mapper.stub(data_source: data_source)
153
+ data_source.stub(:can_serialize?).with(book.title.first) { false }
154
+
155
+ serializer.serialize(book).should == {
156
+ 'title' => [{
157
+ '__marshaled__' => true,
158
+ 'value' => Marshal.dump(unserializable_value)
159
+ }],
160
+ 'authors' => []
161
+ }
162
+ end
163
+
164
+ it 'unmarshals data that has been marshaled by the serializer' do
165
+ data = {
166
+ 'title' => {
167
+ '__marshaled__' => true,
168
+ 'value' => Marshal.dump(unserializable_value),
169
+ }
170
+ }
171
+ serializer.unserialize(data).title.should be_a unserializable_value.class
172
+ end
173
+
174
+ it 'does not unmarshal data not marshaled by the serializer' do
175
+ data = { 'title' => Marshal.dump(unserializable_value) }
176
+
177
+ serializer.unserialize(data).title.should be_a String
178
+ end
179
+ end
180
+
181
+ it 'unserializes a hash of primitives' do
182
+ time = Time.now
183
+ serialized_data = {
184
+ 'number' => 1,
185
+ 'string' => 'hello',
186
+ 'boolean' => true,
187
+ 'float' => 7.5,
188
+ 'time' => time
189
+ }
190
+
191
+ object = serializer.unserialize(serialized_data)
192
+ object.instance_variable_get(:@number).should == 1
193
+ object.instance_variable_get(:@string).should == 'hello'
194
+ object.instance_variable_get(:@boolean).should == true
195
+ object.instance_variable_get(:@float).should == 7.5
196
+ object.instance_variable_get(:@time).should == time
197
+ end
150
198
  end
151
199
  end
152
200
  end
@@ -18,34 +18,34 @@ module Perpetuity
18
18
  end
19
19
 
20
20
  it 'returns the id as to_param' do
21
- object.to_param.should == nil
21
+ object.to_param.should be == nil
22
22
  object.id = 'foo'
23
- object.to_param.should == 'foo'
23
+ object.to_param.should be == 'foo'
24
24
  end
25
25
 
26
26
  it 'returns the keys on the object' do
27
- object.to_key.should == nil
27
+ object.to_key.should be == nil
28
28
  object.id = 'bar'
29
- object.to_key.should == ['bar']
29
+ object.to_key.should be == ['bar']
30
30
  end
31
31
 
32
32
  it 'returns the model name' do
33
- klass.model_name.should == klass
33
+ klass.model_name.should be == klass
34
34
  end
35
35
 
36
36
  it 'returns the param_key' do
37
37
  stub_const 'Foo::Bar', klass
38
- Foo::Bar.param_key.should == 'foo_bar'
38
+ Foo::Bar.param_key.should be == 'foo_bar'
39
39
  end
40
40
 
41
41
  it 'returns the route_key' do
42
42
  stub_const 'Foo::Bar', klass
43
- Foo::Bar.route_key.should == 'foo_bars'
43
+ Foo::Bar.route_key.should be == 'foo_bars'
44
44
  end
45
45
 
46
46
  it 'returns the singular_route_key' do
47
47
  stub_const 'Foo::Bar', klass
48
- Foo::Bar.singular_route_key.should == 'foo_bar'
48
+ Foo::Bar.singular_route_key.should be == 'foo_bar'
49
49
  end
50
50
  end
51
51
  end
@@ -5,7 +5,8 @@ module Perpetuity
5
5
  let(:data_source) { double('data_source') }
6
6
  let(:registry) { double('mapper_registry') }
7
7
  let(:mapper) { double(mapped_class: Object, data_source: data_source, mapper_registry: registry) }
8
- let(:retrieval) { Perpetuity::Retrieval.new mapper, {} }
8
+ let(:query) { double('Query', to_db: {}) }
9
+ let(:retrieval) { Perpetuity::Retrieval.new mapper, query }
9
10
  subject { retrieval }
10
11
 
11
12
  it "sorts the results" do
@@ -53,12 +54,18 @@ module Perpetuity
53
54
  return_object.stub(id: return_data[:id])
54
55
  options = { attribute: nil, direction: nil, limit: nil, skip: nil }
55
56
 
56
- data_source.should_receive(:retrieve).with(Object, {}, options).
57
+ data_source.should_receive(:retrieve).with(Object, query, options).
57
58
  and_return([return_data])
58
59
  data_source.should_receive(:unserialize).with([return_data], mapper) { [return_object] }
59
60
  results = retrieval.to_a
60
61
 
61
62
  results.map(&:id).should == [0]
62
63
  end
64
+
65
+ it 'clears results cache' do
66
+ retrieval.result_cache = [1,2,3]
67
+ retrieval.clear_cache
68
+ retrieval.result_cache.should be_nil
69
+ end
63
70
  end
64
71
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: perpetuity
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jamie Gaskins
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-06-21 00:00:00.000000000 Z
11
+ date: 2013-08-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -79,6 +79,7 @@ files:
79
79
  - lib/perpetuity/mapper_registry.rb
80
80
  - lib/perpetuity/mongodb.rb
81
81
  - lib/perpetuity/mongodb/index.rb
82
+ - lib/perpetuity/mongodb/nil_query.rb
82
83
  - lib/perpetuity/mongodb/query.rb
83
84
  - lib/perpetuity/mongodb/query_attribute.rb
84
85
  - lib/perpetuity/mongodb/query_expression.rb
@@ -159,7 +160,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
159
160
  version: '0'
160
161
  requirements: []
161
162
  rubyforge_project:
162
- rubygems_version: 2.0.3
163
+ rubygems_version: 2.0.5
163
164
  signing_key:
164
165
  specification_version: 4
165
166
  summary: Persistence library allowing serialization of Ruby objects