perpetuity 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
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