toystore 0.9.0 → 0.10.0
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/.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 [](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
|