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 CHANGED
@@ -4,4 +4,5 @@ testing/
4
4
  log
5
5
  tmp
6
6
  pkg
7
- .bundle
7
+ .bundle
8
+ Gemfile.lock
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - ree
5
+ - 1.9.2
6
+ - 1.9.3
7
+ notifications:
8
+ email: false
9
+ bundler_args: --without guard
@@ -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
- group(:development) do
5
- gem 'guard'
6
- gem 'guard-rspec'
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
- gem 'rake', '~> 0.8.7'
11
- gem 'rspec', '~> 2.3'
12
- gem 'timecop', '~> 0.3.5'
13
- gem 'tzinfo', '~> 0.3.23'
14
- gem 'log_buddy', '~> 0.5.0'
15
- gem 'rack-test'
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
- gem 'bson'
18
- gem 'bson_ext'
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
- See [examples/](https://github.com/jnunemaker/toystore/tree/master/examples) for potential direction.
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