toystore 0.9.0 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -1
- data/.travis.yml +9 -0
- data/Changelog.md +10 -1
- data/Gemfile +14 -13
- data/README.md +213 -2
- data/examples/plain_old_object.rb +54 -0
- data/examples/plain_old_object_on_roids.rb +160 -0
- data/lib/toy.rb +3 -1
- data/lib/toy/association_serialization.rb +50 -0
- data/lib/toy/attribute.rb +17 -2
- data/lib/toy/equality.rb +5 -1
- data/lib/toy/identity/abstract_key_factory.rb +5 -0
- data/lib/toy/identity_map.rb +1 -2
- data/lib/toy/inheritance.rb +29 -0
- data/lib/toy/inspect.rb +17 -4
- data/lib/toy/object.rb +6 -0
- data/lib/toy/querying.rb +20 -3
- data/lib/toy/reference.rb +18 -4
- data/lib/toy/reloadable.rb +2 -2
- data/lib/toy/serialization.rb +1 -40
- data/lib/toy/store.rb +2 -2
- data/lib/toy/timestamps.rb +3 -1
- data/lib/toy/version.rb +2 -2
- data/spec/helper.rb +2 -3
- data/spec/support/constants.rb +15 -15
- data/spec/toy/association_serialization_spec.rb +103 -0
- data/spec/toy/attribute_spec.rb +17 -1
- data/spec/toy/equality_spec.rb +9 -2
- data/spec/toy/extensions/array_spec.rb +2 -2
- data/spec/toy/identity/uuid_key_factory_spec.rb +35 -3
- data/spec/toy/identity_map_spec.rb +4 -0
- data/spec/toy/inheritance_spec.rb +93 -0
- data/spec/toy/inspect_spec.rb +12 -4
- data/spec/toy/object_spec.rb +47 -0
- data/spec/toy/plugins_spec.rb +4 -4
- data/spec/toy/querying_spec.rb +71 -11
- data/spec/toy/reference_spec.rb +82 -72
- data/spec/toy/serialization_spec.rb +16 -111
- data/spec/toy/store_spec.rb +14 -28
- metadata +23 -13
- data/Gemfile.lock +0 -71
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/Changelog.md
CHANGED
@@ -1,9 +1,18 @@
|
|
1
1
|
I will do my best to keep this up to date with significant changes here, starting in 0.8.3.
|
2
2
|
|
3
|
+
* 0.10.0
|
4
|
+
* [Reference proxy api changes](https://github.com/jnunemaker/toystore/pull/5) thanks to jakehow
|
5
|
+
* [Support for inheritance](https://github.com/jnunemaker/toystore/pull/4)
|
6
|
+
* [Pass model class to callable default](https://github.com/jnunemaker/toystore/commit/45eff74fb712e5b2a437e3c09b382421fc05539d)
|
7
|
+
* [Added #hash](https://github.com/jnunemaker/toystore/commit/0769f548be669ad1b456cb1b8e11e394e0fee303)
|
8
|
+
* [Added pretty inspect for classes](https://github.com/jnunemaker/toystore/commit/2fdc18b8d8428a932c1e5eeafa6a4db2269f1473)
|
9
|
+
* [Always show id first in #inspect](https://github.com/jnunemaker/toystore/commit/145312b961a519ab84b010d37be075d85fa290a2)
|
10
|
+
* [Moved object serialization into Toy::Object](https://github.com/jnunemaker/toystore/commit/d9431557f0f12c4e171fc888f3eb846fb631d4aa)
|
11
|
+
|
3
12
|
* 0.8.3 => 0.9.0
|
4
13
|
* [Changed from `store` to `adapter`](https://github.com/jnunemaker/toystore/pull/1)
|
5
14
|
* [Embedded objects were removed](https://github.com/jnunemaker/toystore/pull/2)
|
6
15
|
* [Defaulted `adapter` to memory and removed `has_adapter?`](https://github.com/jnunemaker/toystore/commit/64268705fcb22d82eb7ac3e934508770ceb1f101)
|
7
16
|
* [Introduced Toy::Object](https://github.com/jnunemaker/toystore/commit/f22fddff96b388db3bd22f36cc1cc29b28d0ae5e).
|
8
17
|
* [Default Identity Map to off](https://github.com/jnunemaker/toystore/compare/02b652b4dbd4a652bf3d788fbf8cf7d0bae805f6...5cec60be60f9bf749964d5c2d437189287d6d837)
|
9
|
-
* Removed several class methods related to identity map as well (identity_map_on/off/on?/off?/etc)
|
18
|
+
* Removed several class methods related to identity map as well (identity_map_on/off/on?/off?/etc)
|
data/Gemfile
CHANGED
@@ -1,19 +1,20 @@
|
|
1
1
|
source :rubygems
|
2
2
|
gemspec
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
gem 'guard-bundler'
|
8
|
-
gem 'growl'
|
4
|
+
gem 'rake', '~> 0.9.0'
|
5
|
+
gem 'oj', '~> 1.0.0'
|
6
|
+
gem 'multi_json', '~> 1.3.2'
|
9
7
|
|
10
|
-
|
11
|
-
gem '
|
12
|
-
gem '
|
13
|
-
gem '
|
14
|
-
gem '
|
15
|
-
|
8
|
+
group(:guard) do
|
9
|
+
gem 'guard', '~> 1.0.0'
|
10
|
+
gem 'guard-rspec', '~> 0.6.0'
|
11
|
+
gem 'guard-bundler', '~> 0.1.0'
|
12
|
+
gem 'growl', '~> 1.0.0'
|
13
|
+
end
|
16
14
|
|
17
|
-
|
18
|
-
gem '
|
15
|
+
group(:test) do
|
16
|
+
gem 'rspec', '~> 2.8.0'
|
17
|
+
gem 'timecop', '~> 0.3.0'
|
18
|
+
gem 'tzinfo', '~> 0.3.0'
|
19
|
+
gem 'rack-test', '~> 0.6.0'
|
19
20
|
end
|
data/README.md
CHANGED
@@ -1,8 +1,219 @@
|
|
1
|
-
# Toystore
|
1
|
+
# Toystore [![Build Status](https://secure.travis-ci.org/jnunemaker/toystore.png)](http://travis-ci.org/jnunemaker/toystore)
|
2
2
|
|
3
3
|
An object mapper for any [adapter](https://github.com/jnunemaker/adapter) that can read, write, delete, and clear data.
|
4
4
|
|
5
|
-
|
5
|
+
## Examples
|
6
|
+
|
7
|
+
The project comes with two main includes that you can use -- Toy::Object and Toy::Store.
|
8
|
+
|
9
|
+
**Toy::Object** comes with all the goods you need for plain old ruby objects -- attributes, dirty attribute tracking, equality, inheritance, serialization, cloning, logging and pretty inspecting.
|
10
|
+
|
11
|
+
**Toy::Store** includes Toy::Object and adds persistence through adapters, mass assignment, querying, callbacks, validations and a few simple associations (lists and references).
|
12
|
+
|
13
|
+
### Toy::Object
|
14
|
+
|
15
|
+
First, join me in a whirlwind tour of Toy::Object.
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
class Person
|
19
|
+
include Toy::Object
|
20
|
+
|
21
|
+
attribute :name, String
|
22
|
+
attribute :age, Integer
|
23
|
+
end
|
24
|
+
|
25
|
+
# Pretty class inspecting
|
26
|
+
puts Person.inspect
|
27
|
+
|
28
|
+
john = Person.new(:name => 'John', :age => 30)
|
29
|
+
steve = Person.new(:name => 'Steve', :age => 31)
|
30
|
+
|
31
|
+
# Pretty inspecting
|
32
|
+
puts john.inspect
|
33
|
+
|
34
|
+
# Attribute dirty tracking
|
35
|
+
john.name = 'NEW NAME!'
|
36
|
+
puts john.changes.inspect # {"name"=>["John", "NEW NAME!"], "age"=>[nil, 30]}
|
37
|
+
puts john.name_changed?.inspect # true
|
38
|
+
|
39
|
+
# Equality goodies
|
40
|
+
puts john.eql?(john) # true
|
41
|
+
puts john.eql?(steve) # false
|
42
|
+
puts john == john # true
|
43
|
+
puts john == steve # false
|
44
|
+
|
45
|
+
# Cloning
|
46
|
+
puts john.clone.inspect
|
47
|
+
|
48
|
+
# Inheritance
|
49
|
+
class AwesomePerson < Person
|
50
|
+
end
|
51
|
+
|
52
|
+
puts Person.attributes.keys.sort.inspect # ["age", "id", "name"]
|
53
|
+
puts AwesomePerson.attributes.keys.sort.inspect # ["age", "id", "name", "type"]
|
54
|
+
|
55
|
+
# Serialization
|
56
|
+
puts john.to_json
|
57
|
+
puts john.to_xml
|
58
|
+
```
|
59
|
+
|
60
|
+
Ok, that was definitely awesome. Please continue on your personal journey to a blown mind (very similar to a beautiful mind).
|
61
|
+
|
62
|
+
### Toy::Store
|
63
|
+
|
64
|
+
Toy::Store is a unique bird that builds on top of Toy::Object. Below is a quick sample of what it can do.
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
class Person
|
68
|
+
include Toy::Store
|
69
|
+
|
70
|
+
attribute :name, String
|
71
|
+
attribute :age, Integer, :default => 0
|
72
|
+
end
|
73
|
+
|
74
|
+
# Persistence
|
75
|
+
john = Person.create(:name => 'John', :age => 30)
|
76
|
+
pp john
|
77
|
+
pp john.persisted?
|
78
|
+
|
79
|
+
# Mass Assignment Security
|
80
|
+
Person.attribute :role, String, :default => 'guest'
|
81
|
+
Person.attr_accessible :name, :age
|
82
|
+
|
83
|
+
person = Person.new(:name => 'Hacker', :age => 13, :role => 'admin')
|
84
|
+
pp person.role # "guest"
|
85
|
+
|
86
|
+
# Querying
|
87
|
+
pp Person.get(john.id)
|
88
|
+
pp Person.get_multi(john.id)
|
89
|
+
pp Person.get('NOT HERE') # nil
|
90
|
+
pp Person.get_or_new('NOT HERE') # new person with id of 'NOT HERE'
|
91
|
+
|
92
|
+
begin
|
93
|
+
Person.get!('NOT HERE')
|
94
|
+
rescue Toy::NotFound
|
95
|
+
puts "Could not find person with id of 'NOT HERE'"
|
96
|
+
end
|
97
|
+
|
98
|
+
# Reloading
|
99
|
+
pp john.reload
|
100
|
+
|
101
|
+
# Callbacks
|
102
|
+
class Person
|
103
|
+
def add_fifty_to_age
|
104
|
+
self.age += 50
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class Person
|
109
|
+
before_create :add_fifty_to_age
|
110
|
+
end
|
111
|
+
|
112
|
+
pp Person.create(:age => 10).age # 60
|
113
|
+
|
114
|
+
# Validations
|
115
|
+
class Person
|
116
|
+
validates_presence_of :name
|
117
|
+
end
|
118
|
+
|
119
|
+
person = Person.new
|
120
|
+
pp person.valid? # false
|
121
|
+
pp person.errors[:name] # ["can't be blank"]
|
122
|
+
|
123
|
+
# Lists (array key stored as attribute)
|
124
|
+
class Skill
|
125
|
+
include Toy::Store
|
126
|
+
|
127
|
+
attribute :name, String
|
128
|
+
attribute :truth, Boolean
|
129
|
+
end
|
130
|
+
|
131
|
+
class Person
|
132
|
+
list :skills, Skill
|
133
|
+
end
|
134
|
+
|
135
|
+
john.skills = [Skill.create(:name => 'Programming', :truth => true)]
|
136
|
+
john.skills << Skill.create(:name => 'Mechanic', :truth => false)
|
137
|
+
|
138
|
+
pp john.skills.map(&:id) == john.skill_ids # true
|
139
|
+
|
140
|
+
# References (think foreign keyish)
|
141
|
+
class Person
|
142
|
+
reference :mom, Person
|
143
|
+
end
|
144
|
+
|
145
|
+
mom = Person.create(:name => 'Mum')
|
146
|
+
john.mom = mom
|
147
|
+
john.save
|
148
|
+
pp john.reload.mom_id == mom.id # true
|
149
|
+
|
150
|
+
# Identity Map
|
151
|
+
Toy::IdentityMap.use do
|
152
|
+
frank = Person.create(:name => 'Frank')
|
153
|
+
|
154
|
+
pp Person.get(frank.id).equal?(frank) # true
|
155
|
+
pp Person.get(frank.id).object_id == frank.object_id # true
|
156
|
+
end
|
157
|
+
|
158
|
+
# Or you can turn it on globally
|
159
|
+
Toy::IdentityMap.enabled = true
|
160
|
+
frank = Person.create(:name => 'Frank')
|
161
|
+
|
162
|
+
pp Person.get(frank.id).equal?(frank) # true
|
163
|
+
pp Person.get(frank.id).object_id == frank.object_id # true
|
164
|
+
|
165
|
+
# All persistence runs through an adapter.
|
166
|
+
# All of the above examples used the default in-memory adapter.
|
167
|
+
# Looks something like this:
|
168
|
+
Person.adapter :memory, {}
|
169
|
+
|
170
|
+
puts "Adapter: #{Person.adapter.inspect}"
|
171
|
+
|
172
|
+
# You can make a new adapter to your awesome new/old data store
|
173
|
+
# Always use #key_for, #encode, and #decode. Feel free to override
|
174
|
+
# them if you like, but always use them. Default encode/decode is
|
175
|
+
# most likely marshaling, but you can use anything.
|
176
|
+
Adapter.define(:append_only_array) do
|
177
|
+
def read(key)
|
178
|
+
if (record = client.reverse.detect { |row| row[0] == key_for(key) })
|
179
|
+
decode(record)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def write(key, value)
|
184
|
+
key = key_for(key)
|
185
|
+
value = encode(value)
|
186
|
+
client << [key, value]
|
187
|
+
value
|
188
|
+
end
|
189
|
+
|
190
|
+
def delete(key)
|
191
|
+
key = key_for(key)
|
192
|
+
client.delete_if { |row| row[0] == key }
|
193
|
+
end
|
194
|
+
|
195
|
+
def clear
|
196
|
+
client.clear
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
client = []
|
201
|
+
Person.adapter :append_only_array, client
|
202
|
+
|
203
|
+
pp "Client: #{Person.adapter.client.equal?(client)}"
|
204
|
+
|
205
|
+
person = Person.create(:name => 'Phil', :age => 55)
|
206
|
+
person.age = 56
|
207
|
+
person.save
|
208
|
+
|
209
|
+
pp client
|
210
|
+
|
211
|
+
pp Person.get(person.id) # Phil with age 56
|
212
|
+
```
|
213
|
+
|
214
|
+
If that doesn't excite you, nothing will. At this point, you are probably wishing for more.
|
215
|
+
|
216
|
+
Luckily, there is an entire directory full of [examples](https://github.com/jnunemaker/toystore/tree/master/examples) and I created a few power user guides, which I will kindly link next.
|
6
217
|
|
7
218
|
## ToyStore Power User Guides
|
8
219
|
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'pathname'
|
3
|
+
require 'rubygems'
|
4
|
+
require 'adapter/memory'
|
5
|
+
|
6
|
+
root_path = Pathname(__FILE__).dirname.join('..').expand_path
|
7
|
+
lib_path = root_path.join('lib')
|
8
|
+
$:.unshift(lib_path)
|
9
|
+
require 'toystore'
|
10
|
+
|
11
|
+
##################################################################
|
12
|
+
# An example of all the goodies you get by including Toy::Object #
|
13
|
+
##################################################################
|
14
|
+
|
15
|
+
class Person
|
16
|
+
include Toy::Object
|
17
|
+
|
18
|
+
attribute :name, String
|
19
|
+
attribute :age, Integer
|
20
|
+
end
|
21
|
+
|
22
|
+
# Pretty class inspecting
|
23
|
+
pp Person
|
24
|
+
|
25
|
+
john = Person.new(:name => 'John', :age => 30)
|
26
|
+
steve = Person.new(:name => 'Steve', :age => 31)
|
27
|
+
|
28
|
+
# Pretty inspecting
|
29
|
+
pp john
|
30
|
+
|
31
|
+
# Attribute dirty tracking
|
32
|
+
john.name = 'NEW NAME!'
|
33
|
+
pp john.changes # {"name"=>["John", "NEW NAME!"], "age"=>[nil, 30]}
|
34
|
+
pp john.name_changed? # true
|
35
|
+
|
36
|
+
# Equality goodies
|
37
|
+
pp john.eql?(john) # true
|
38
|
+
pp john.eql?(steve) # false
|
39
|
+
pp john == john # true
|
40
|
+
pp john == steve # false
|
41
|
+
|
42
|
+
# Cloning
|
43
|
+
pp john.clone
|
44
|
+
|
45
|
+
# Inheritance
|
46
|
+
class AwesomePerson < Person
|
47
|
+
end
|
48
|
+
|
49
|
+
pp Person.attributes.keys.sort # ["age", "id", "name"]
|
50
|
+
pp AwesomePerson.attributes.keys.sort # ["age", "id", "name", "type"]
|
51
|
+
|
52
|
+
# Serialization
|
53
|
+
puts john.to_json
|
54
|
+
puts john.to_xml
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'pathname'
|
3
|
+
require 'rubygems'
|
4
|
+
require 'adapter/memory'
|
5
|
+
|
6
|
+
root_path = Pathname(__FILE__).dirname.join('..').expand_path
|
7
|
+
lib_path = root_path.join('lib')
|
8
|
+
$:.unshift(lib_path)
|
9
|
+
require 'toystore'
|
10
|
+
|
11
|
+
#################################################################
|
12
|
+
# An example of all the goodies you get by including Toy::Store #
|
13
|
+
# Note that you also get all of the goodies in Toy::Object. #
|
14
|
+
#################################################################
|
15
|
+
|
16
|
+
class Person
|
17
|
+
include Toy::Store
|
18
|
+
|
19
|
+
attribute :name, String
|
20
|
+
attribute :age, Integer, :default => 0
|
21
|
+
end
|
22
|
+
|
23
|
+
# Persistence
|
24
|
+
john = Person.create(:name => 'John', :age => 30)
|
25
|
+
pp john
|
26
|
+
pp john.persisted?
|
27
|
+
|
28
|
+
# Mass Assignment Security
|
29
|
+
Person.attribute :role, String, :default => 'guest'
|
30
|
+
Person.attr_accessible :name, :age
|
31
|
+
|
32
|
+
person = Person.new(:name => 'Hacker', :age => 13, :role => 'admin')
|
33
|
+
pp person.role # "guest"
|
34
|
+
|
35
|
+
# Querying
|
36
|
+
pp Person.get(john.id)
|
37
|
+
pp Person.get_multi(john.id)
|
38
|
+
pp Person.get('NOT HERE') # nil
|
39
|
+
pp Person.get_or_new('NOT HERE') # new person with id of 'NOT HERE'
|
40
|
+
|
41
|
+
begin
|
42
|
+
Person.get!('NOT HERE')
|
43
|
+
rescue Toy::NotFound
|
44
|
+
puts "Could not find person with id of 'NOT HERE'"
|
45
|
+
end
|
46
|
+
|
47
|
+
# Reloading
|
48
|
+
pp john.reload
|
49
|
+
|
50
|
+
# Callbacks
|
51
|
+
class Person
|
52
|
+
def add_fifty_to_age
|
53
|
+
self.age += 50
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class Person
|
58
|
+
before_create :add_fifty_to_age
|
59
|
+
end
|
60
|
+
|
61
|
+
pp Person.create(:age => 10).age # 60
|
62
|
+
|
63
|
+
# Validations
|
64
|
+
class Person
|
65
|
+
validates_presence_of :name
|
66
|
+
end
|
67
|
+
|
68
|
+
person = Person.new
|
69
|
+
pp person.valid? # false
|
70
|
+
pp person.errors[:name] # ["can't be blank"]
|
71
|
+
|
72
|
+
# Lists (array key stored as attribute)
|
73
|
+
class Skill
|
74
|
+
include Toy::Store
|
75
|
+
|
76
|
+
attribute :name, String
|
77
|
+
attribute :truth, Boolean
|
78
|
+
end
|
79
|
+
|
80
|
+
class Person
|
81
|
+
list :skills, Skill
|
82
|
+
end
|
83
|
+
|
84
|
+
john.skills = [Skill.create(:name => 'Programming', :truth => true)]
|
85
|
+
john.skills << Skill.create(:name => 'Mechanic', :truth => false)
|
86
|
+
|
87
|
+
pp john.skills.map(&:id) == john.skill_ids # true
|
88
|
+
|
89
|
+
# References (think foreign keyish)
|
90
|
+
class Person
|
91
|
+
reference :mom, Person
|
92
|
+
end
|
93
|
+
|
94
|
+
mom = Person.create(:name => 'Mum')
|
95
|
+
john.mom = mom
|
96
|
+
john.save
|
97
|
+
pp john.reload.mom_id == mom.id # true
|
98
|
+
|
99
|
+
# Identity Map
|
100
|
+
Toy::IdentityMap.use do
|
101
|
+
frank = Person.create(:name => 'Frank')
|
102
|
+
|
103
|
+
pp Person.get(frank.id).equal?(frank) # true
|
104
|
+
pp Person.get(frank.id).object_id == frank.object_id # true
|
105
|
+
end
|
106
|
+
|
107
|
+
# Or you can turn it on globally
|
108
|
+
Toy::IdentityMap.enabled = true
|
109
|
+
frank = Person.create(:name => 'Frank')
|
110
|
+
|
111
|
+
pp Person.get(frank.id).equal?(frank) # true
|
112
|
+
pp Person.get(frank.id).object_id == frank.object_id # true
|
113
|
+
|
114
|
+
# All persistence runs through an adapter.
|
115
|
+
# All of the above examples used the default in-memory adapter.
|
116
|
+
# Looks something like this:
|
117
|
+
Person.adapter :memory, {}
|
118
|
+
|
119
|
+
puts "Adapter: #{Person.adapter.inspect}"
|
120
|
+
|
121
|
+
# You can make a new adapter to your awesome new/old data store
|
122
|
+
# Always use #key_for, #encode, and #decode. Feel free to override
|
123
|
+
# them if you like, but always use them. Default encode/decode is
|
124
|
+
# most likely marshaling, but you can use anything.
|
125
|
+
Adapter.define(:append_only_array) do
|
126
|
+
def read(key)
|
127
|
+
if (record = client.reverse.detect { |row| row[0] == key_for(key) })
|
128
|
+
decode(record)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def write(key, value)
|
133
|
+
key = key_for(key)
|
134
|
+
value = encode(value)
|
135
|
+
client << [key, value]
|
136
|
+
value
|
137
|
+
end
|
138
|
+
|
139
|
+
def delete(key)
|
140
|
+
key = key_for(key)
|
141
|
+
client.delete_if { |row| row[0] == key }
|
142
|
+
end
|
143
|
+
|
144
|
+
def clear
|
145
|
+
client.clear
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
client = []
|
150
|
+
Person.adapter :append_only_array, client
|
151
|
+
|
152
|
+
pp "Client: #{Person.adapter.client.equal?(client)}"
|
153
|
+
|
154
|
+
person = Person.create(:name => 'Phil', :age => 55)
|
155
|
+
person.age = 56
|
156
|
+
person.save
|
157
|
+
|
158
|
+
pp client
|
159
|
+
|
160
|
+
pp Person.get(person.id) # Phil with age 56
|