perpetuity 0.1 → 0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -15,11 +15,9 @@ In the Data Mapper pattern, the objects you work with don't understand how to pe
15
15
  Add the following to your Gemfile and run `bundle` to install it.
16
16
 
17
17
  ```ruby
18
- gem 'perpetuity', github: 'jgaskins/perpetuity'
18
+ gem 'perpetuity'
19
19
  ```
20
20
 
21
- Once it's got enough functionality to release, you'll be able to remove the `github` parameter.
22
-
23
21
  ## Configuration
24
22
 
25
23
  The only currently supported persistence method is MongoDB. Other schemaless solutions can probably be implemented easily.
@@ -1,13 +1,17 @@
1
1
  module Perpetuity
2
2
  module DataInjectable
3
+ def inject_attribute object, attribute, value
4
+ if object.respond_to?("#{attribute}=")
5
+ object.send("#{attribute}=", value)
6
+ else
7
+ attribute = "@#{attribute}" unless attribute[0] == '@'
8
+ object.instance_variable_set(attribute, value)
9
+ end
10
+ end
11
+
3
12
  def inject_data object, data
4
13
  data.each_pair do |attribute,value|
5
- if object.respond_to?("#{attribute}=")
6
- object.send("#{attribute}=", value)
7
- else
8
- attribute = "@#{attribute}" unless attribute[0] == '@'
9
- object.instance_variable_set(attribute, value)
10
- end
14
+ inject_attribute object, attribute, value
11
15
  end
12
16
  give_id_to object if object.instance_variables.include?(:@id)
13
17
  end
@@ -39,68 +39,64 @@ module Perpetuity
39
39
  data_source.delete_all mapped_class
40
40
  end
41
41
 
42
- def serializable_types
43
- @serializable_types ||= [NilClass, TrueClass, FalseClass, Fixnum, Bignum, Float, String, Array, Hash, Time, Date]
44
- end
45
-
46
42
  def insert object
47
43
  raise "#{object} is invalid and cannot be persisted." unless validations.valid?(object)
48
- serializable_attributes = {}
44
+ serializable_attributes = serialize(object)
49
45
  if o_id = object.instance_exec(&id)
50
46
  serializable_attributes[:id] = o_id
51
47
  end
52
48
 
53
- attributes_for(object).each_pair do |attribute, value|
54
- if serializable_types.include? value.class
55
- serializable_attributes[attribute] = value
56
- elsif value.respond_to?(:id)
57
- serializable_attributes[attribute] = value.id
58
- else
59
- raise "Must persist #{attribute} (#{value.inspect}) before persisting this #{object.inspect}."
60
- end
61
- end
62
-
63
49
  new_id = data_source.insert mapped_class, serializable_attributes
64
50
  give_id_to object, new_id
65
51
  new_id
66
52
  end
67
53
 
68
- def attributes_for object
54
+ def serialize object
69
55
  attrs = {}
70
56
  attribute_set.each do |attrib|
71
57
  value = object.send(attrib.name)
72
-
73
- if attrib.type == Array
74
- new_array = []
75
- value.each do |i|
76
- if serializable_types.include? i.class
77
- new_array << i
78
- else
79
- if attrib.embedded?
80
- new_array << Marshal.dump(i)
81
- else
82
- new_array << i.id
83
- end
84
- end
58
+ attrib_name = attrib.name.to_s
59
+
60
+ if value.respond_to? :each
61
+ attrs[attrib_name] = serialize_enumerable(value)
62
+ elsif data_source.can_serialize? value
63
+ attrs[attrib_name] = value
64
+ elsif Mapper[value.class]
65
+ if attrib.embedded?
66
+ attrs[attrib_name] = Mapper[value.class].serialize(value).merge '__metadata__' => { 'class' => value.class }
67
+ else
68
+ attrs[attrib_name] = {
69
+ '__metadata__' => {
70
+ 'class' => value.class.to_s,
71
+ 'id' => value.id
72
+ }
73
+ }
85
74
  end
86
-
87
- attrs[attrib.name] = new_array
88
75
  else
89
- attrs[attrib.name] = value
76
+ if attrib.embedded?
77
+ attrs[attrib_name] = Marshal.dump(value)
78
+ end
90
79
  end
91
80
  end
81
+
92
82
  attrs
93
83
  end
94
84
 
95
- def unserialize(data)
96
- if data.is_a?(String) && data.start_with?("\u0004")
97
- Marshal.load(data)
98
- elsif data.is_a? Array
99
- data.map { |i| unserialize i }
100
- elsif data.is_a? Hash
101
- Hash[data.map{|k,v| [k, unserialize(v)]}]
102
- else
103
- data
85
+ def serialize_enumerable enum
86
+ enum.map do |value|
87
+ if value.respond_to? :each
88
+ serialize_enumerable(value)
89
+ elsif data_source.can_serialize? value
90
+ value
91
+ elsif Mapper[value.class]
92
+ {
93
+ '__metadata__' => {
94
+ 'class' => value.class.to_s
95
+ }
96
+ }.merge Mapper[value.class].serialize(value)
97
+ else
98
+ Marshal.dump(value)
99
+ end
104
100
  end
105
101
  end
106
102
 
@@ -159,12 +155,11 @@ module Perpetuity
159
155
  end
160
156
 
161
157
  def load_association! object, attribute
162
- class_name = attribute_set[attribute].type
163
- id = object.send(attribute)
158
+ reference = object.send(attribute)
159
+ klass = reference.klass
160
+ id = reference.id
164
161
 
165
- mapper = Mapper[class_name]
166
- associated_object = mapper.find(id)
167
- object.send("#{attribute}=", associated_object)
162
+ inject_attribute object, attribute, Mapper[klass].find(id)
168
163
  end
169
164
 
170
165
  def id &block
@@ -98,5 +98,14 @@ module Perpetuity
98
98
  def update klass, id, new_data
99
99
  collection(klass).update({ _id: id }, new_data)
100
100
  end
101
+
102
+ def can_serialize? value
103
+ serializable_types.include? value.class
104
+ end
105
+
106
+ private
107
+ def serializable_types
108
+ @serializable_types ||= [NilClass, TrueClass, FalseClass, Fixnum, Float, String, Array, Hash, Time]
109
+ end
101
110
  end
102
111
  end
@@ -0,0 +1,17 @@
1
+ module Perpetuity
2
+ class Reference
3
+ attr_reader :klass, :id
4
+ def initialize klass, id
5
+ @klass = klass
6
+ @id = id
7
+ end
8
+
9
+ def == other
10
+ klass == other.klass && id == other.id
11
+ end
12
+
13
+ def eql? other
14
+ self == other
15
+ end
16
+ end
17
+ end
@@ -1,5 +1,5 @@
1
1
  require 'perpetuity/data_injectable'
2
- require 'perpetuity/mapper'
2
+ require 'perpetuity/reference'
3
3
 
4
4
  module Perpetuity
5
5
  class Retrieval
@@ -53,15 +53,39 @@ module Perpetuity
53
53
  page: result_page
54
54
  }
55
55
  results = @data_source.retrieve(@class, @criteria, options)
56
- objects = []
57
- results.each do |result|
58
- object = @class.new
59
- inject_data object, Mapper.new.unserialize(result)
56
+ unserialize results
57
+ end
60
58
 
61
- objects << object
62
- end
59
+ def unserialize(data)
60
+ if data.is_a?(String) && data.start_with?("\u0004") # if it's marshaled
61
+ Marshal.load(data)
62
+ elsif data.is_a? Array
63
+ data.map { |i| unserialize i }
64
+ elsif data.is_a? Hash
65
+ metadata = data.delete('__metadata__')
66
+ if metadata
67
+ klass = Object.const_get metadata['class']
68
+ id = metadata['id']
69
+ if id
70
+ object = Reference.new(klass, id)
71
+ else
72
+ object = klass.new
73
+ data.each do |attr, value|
74
+ inject_attribute object, attr, unserialize(value)
75
+ end
76
+ end
77
+ else
78
+ object = @class.new
79
+ data.each do |attr, value|
80
+ inject_attribute object, attr, unserialize(value)
81
+ end
82
+ end
63
83
 
64
- objects
84
+ give_id_to object
85
+ object
86
+ else
87
+ data
88
+ end
65
89
  end
66
90
 
67
91
  def [] index
@@ -1,3 +1,3 @@
1
1
  module Perpetuity
2
- VERSION = "0.1"
2
+ VERSION = "0.2"
3
3
  end
data/perpetuity.gemspec CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |s|
8
8
  s.authors = ["Jamie Gaskins"]
9
9
  s.email = ["jgaskins@gmail.com"]
10
10
  s.homepage = "https://github.com/jgaskins/perpetuity.git"
11
- s.summary = %q{Persistence library allowing persistence of Ruby objects}
12
- s.description = %q{Persistence library allowing persistence of Ruby objects without adding persistence concerns to domain objects.}
11
+ s.summary = %q{Persistence library allowing serialization of Ruby objects}
12
+ s.description = %q{Persistence layer Ruby objects}
13
13
 
14
14
  s.files = `git ls-files`.split("\n")
15
15
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
@@ -7,6 +7,11 @@ module Perpetuity
7
7
 
8
8
  before { klass.extend DataInjectable }
9
9
 
10
+ it 'injects an attribute into an object' do
11
+ klass.inject_attribute object, :a, 1
12
+ object.instance_variable_get(:@a).should eq 1
13
+ end
14
+
10
15
  it 'injects data into an object' do
11
16
  klass.inject_data object, { a: 1, b: 2 }
12
17
  object.instance_variable_get(:@a).should eq 1
@@ -26,7 +26,7 @@ module Perpetuity
26
26
 
27
27
  its(:mapped_class) { should eq Object }
28
28
 
29
- context 'with unserializable attributes' do
29
+ context 'with unserializable embedded attributes' do
30
30
  let(:unserializable_object) { 1.to_c }
31
31
  let(:serialized_attrs) do
32
32
  [ Marshal.dump(unserializable_object) ]
@@ -36,15 +36,11 @@ module Perpetuity
36
36
  object = Object.new
37
37
  object.stub(sub_objects: [unserializable_object])
38
38
  mapper.attribute :sub_objects, Array, embedded: true
39
- mapper.attributes_for(object)[:sub_objects].should eq serialized_attrs
40
- end
41
-
42
- describe 'unserializes attributes' do
43
- let(:comments) { mapper.unserialize(serialized_attrs) }
44
- subject { comments.first }
39
+ data_source = double(:data_source)
40
+ mapper.stub(data_source: data_source)
41
+ data_source.should_receive(:can_serialize?).with(unserializable_object).and_return false
45
42
 
46
- it { should be_a Complex }
47
- it { should eq unserializable_object }
43
+ mapper.serialize(object)['sub_objects'].should eq serialized_attrs
48
44
  end
49
45
  end
50
46
  end
@@ -1,4 +1,5 @@
1
1
  require 'perpetuity/mongodb'
2
+ require 'date'
2
3
 
3
4
  module Perpetuity
4
5
  describe MongoDB do
@@ -83,9 +84,20 @@ module Perpetuity
83
84
 
84
85
  it 'retrieves by id if the id is a string' do
85
86
  time = Time.now.utc
86
- id = mongo.insert Article, {inserted: time}
87
- objects = mongo.retrieve(Article, id: id.to_s).to_a
87
+ id = mongo.insert Object, {inserted: time}
88
+ objects = mongo.retrieve(Object, id: id.to_s).to_a
88
89
  objects.map{|i| i["inserted"].to_f}.first.should be_within(0.001).of time.to_f
89
90
  end
91
+
92
+ describe 'serializable objects' do
93
+ let(:serializable_values) { [nil, true, false, 1, 1.2, '', [], {}, Time.now] }
94
+
95
+ it 'can insert serializable values' do
96
+ serializable_values.each do |value|
97
+ mongo.insert(Object, {value: value}).should be_a BSON::ObjectId
98
+ mongo.can_serialize?(value).should be_true
99
+ end
100
+ end
101
+ end
90
102
  end
91
103
  end
@@ -0,0 +1,26 @@
1
+ require 'perpetuity/reference'
2
+
3
+ module Perpetuity
4
+ describe Reference do
5
+ let(:reference) { Reference.new Object, 1 }
6
+ subject { reference }
7
+
8
+ its(:klass) { should be Object }
9
+ its(:id) { should be == 1 }
10
+
11
+ describe 'comparability' do
12
+ describe 'equality' do
13
+ let(:duplicate) { reference.dup }
14
+ it { should be == duplicate }
15
+ it { should eql duplicate }
16
+ end
17
+
18
+ describe 'inequality' do
19
+ it { should_not be == Reference.new(String, reference.id) }
20
+ it { should_not eql Reference.new(String, reference.id) }
21
+ it { should_not be == Reference.new(reference.klass, 2) }
22
+ it { should_not eql Reference.new(reference.klass, 2) }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -54,5 +54,15 @@ module Perpetuity
54
54
 
55
55
  results.map(&:id).should == [0]
56
56
  end
57
+
58
+ describe 'unserializes attributes' do
59
+ let(:unserializable_object) { 1.to_c }
60
+ let(:serialized_attrs) { [ Marshal.dump(unserializable_object) ] }
61
+ let(:comments) { retrieval.unserialize(serialized_attrs) }
62
+ subject { comments.first }
63
+
64
+ it { should be_a Complex }
65
+ it { should eq unserializable_object}
66
+ end
57
67
  end
58
68
  end
@@ -243,12 +243,17 @@ describe Perpetuity do
243
243
  topic_mapper.insert topic
244
244
  end
245
245
 
246
- it 'can reference other objects' do
247
- topic_mapper.find(topic.id).creator.should eq user.id
246
+ describe 'referenced relationships' do
247
+ let(:creator) { topic_mapper.find(topic.id).creator }
248
+ subject { creator }
249
+
250
+ it { should be_a Perpetuity::Reference }
251
+ its(:klass) { should be User }
252
+ its(:id) { should be == user.id }
248
253
  end
249
254
 
250
255
  it 'can retrieve associated objects' do
251
- retrieved_topic = topic_mapper.first
256
+ retrieved_topic = topic_mapper.find(topic.id)
252
257
 
253
258
  topic_mapper.load_association! retrieved_topic, :creator
254
259
  retrieved_topic.creator.name.should eq 'Flump'
@@ -290,4 +295,62 @@ describe Perpetuity do
290
295
  saved_message.instance_variable_get(:@text).should eq 'My Message!'.reverse
291
296
  saved_message.text.should eq 'My Message!'
292
297
  end
298
+
299
+ describe 'serialization' do
300
+ let(:author) { User.new 'username' }
301
+ let(:comment) { Comment.new }
302
+ let(:article) { Article.new }
303
+ let(:mapper) { Perpetuity[Article] }
304
+ let(:serialized_value) do
305
+ {
306
+ 'title' => article.title,
307
+ 'body' => article.body,
308
+ 'author' => {
309
+ '__metadata__' => {
310
+ 'class' => author.class.to_s,
311
+ 'id' => author.id
312
+ }
313
+ },
314
+ 'comments' => [
315
+ {
316
+ '__metadata__' => {
317
+ 'class' => comment.class.to_s
318
+ },
319
+ 'body' => comment.body,
320
+ 'author' => {
321
+ '__metadata__' => {
322
+ 'class' => author.class.to_s,
323
+ 'id' => author.id
324
+ }
325
+ }
326
+ },
327
+ ],
328
+ 'published_at' => article.published_at,
329
+ 'views' => article.views
330
+ }
331
+ end
332
+
333
+ before do
334
+ article.author = author
335
+ article.comments = [comment]
336
+ comment.author = author
337
+
338
+ Perpetuity[User].insert author
339
+ Perpetuity[Article].insert article
340
+ end
341
+
342
+ it 'serializes objects into hashes' do
343
+ mapper.serialize(article).should be == serialized_value
344
+ end
345
+
346
+ it 'deserializes hashes into proper objects' do
347
+ unserialized = mapper.find article.id
348
+ unserialized.should be_a Article
349
+ unserialized.title.should be == article.title
350
+ unserialized.body.should be == article.body
351
+ unserialized.comments.first.tap do |unserialized_comment|
352
+ unserialized_comment.body.should be == comment.body
353
+ end
354
+ end
355
+ end
293
356
  end
data/spec/test_classes.rb CHANGED
@@ -1,8 +1,20 @@
1
+ class User
2
+ attr_accessor :name
3
+ def initialize name="Foo"
4
+ @name = name
5
+ end
6
+ end
7
+
8
+ Perpetuity.generate_mapper_for User do
9
+ attribute :name, String
10
+ end
11
+
1
12
  class Article
2
- attr_accessor :title, :body, :comments, :published_at, :views
13
+ attr_accessor :title, :body, :author, :comments, :published_at, :views
3
14
  def initialize title="Title", body="Body", author=nil, published_at=Time.now, views=0
4
15
  @title = title
5
16
  @body = body
17
+ @author = author
6
18
  @comments = []
7
19
  @published_at = published_at
8
20
  @views = views
@@ -12,31 +24,23 @@ end
12
24
  Perpetuity.generate_mapper_for(Article) do
13
25
  attribute :title, String
14
26
  attribute :body, String
27
+ attribute :author, User
15
28
  attribute :comments, Array, embedded: true
16
29
  attribute :published_at, Time
17
30
  attribute :views, Integer
18
31
  end
19
32
 
20
33
  class Comment
21
- attr_reader :body
22
- def initialize body='Body'
34
+ attr_accessor :body, :author
35
+ def initialize body='Body', author=nil
23
36
  @body = body
37
+ @author = author
24
38
  end
25
39
  end
26
40
 
27
41
  Perpetuity.generate_mapper_for(Comment) do
28
42
  attribute :body, String
29
- end
30
-
31
- class User
32
- attr_accessor :name
33
- def initialize name="Foo"
34
- @name = name
35
- end
36
- end
37
-
38
- Perpetuity.generate_mapper_for User do
39
- attribute :name, String
43
+ attribute :author, User
40
44
  end
41
45
 
42
46
  class Book
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: perpetuity
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.1'
4
+ version: '0.2'
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-10-22 00:00:00.000000000 Z
12
+ date: 2012-11-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -91,8 +91,7 @@ dependencies:
91
91
  - - ! '>='
92
92
  - !ruby/object:Gem::Version
93
93
  version: '0'
94
- description: Persistence library allowing persistence of Ruby objects without adding
95
- persistence concerns to domain objects.
94
+ description: Persistence layer Ruby objects
96
95
  email:
97
96
  - jgaskins@gmail.com
98
97
  executables: []
@@ -117,6 +116,7 @@ files:
117
116
  - lib/perpetuity/mongodb/query.rb
118
117
  - lib/perpetuity/mongodb/query_attribute.rb
119
118
  - lib/perpetuity/mongodb/query_expression.rb
119
+ - lib/perpetuity/reference.rb
120
120
  - lib/perpetuity/retrieval.rb
121
121
  - lib/perpetuity/validations.rb
122
122
  - lib/perpetuity/validations/length.rb
@@ -133,6 +133,7 @@ files:
133
133
  - spec/perpetuity/mongodb/query_expression_spec.rb
134
134
  - spec/perpetuity/mongodb/query_spec.rb
135
135
  - spec/perpetuity/mongodb_spec.rb
136
+ - spec/perpetuity/reference_spec.rb
136
137
  - spec/perpetuity/retrieval_spec.rb
137
138
  - spec/perpetuity/validations/length_spec.rb
138
139
  - spec/perpetuity/validations/presence_spec.rb
@@ -162,7 +163,7 @@ rubyforge_project:
162
163
  rubygems_version: 1.8.24
163
164
  signing_key:
164
165
  specification_version: 3
165
- summary: Persistence library allowing persistence of Ruby objects
166
+ summary: Persistence library allowing serialization of Ruby objects
166
167
  test_files:
167
168
  - spec/perpetuity/attribute_set_spec.rb
168
169
  - spec/perpetuity/attribute_spec.rb
@@ -173,6 +174,7 @@ test_files:
173
174
  - spec/perpetuity/mongodb/query_expression_spec.rb
174
175
  - spec/perpetuity/mongodb/query_spec.rb
175
176
  - spec/perpetuity/mongodb_spec.rb
177
+ - spec/perpetuity/reference_spec.rb
176
178
  - spec/perpetuity/retrieval_spec.rb
177
179
  - spec/perpetuity/validations/length_spec.rb
178
180
  - spec/perpetuity/validations/presence_spec.rb