perpetuity 0.7.0 → 0.7.1

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 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