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 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