couch_tomato 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/MIT-LICENSE.txt +19 -0
  2. data/README.md +96 -0
  3. data/init.rb +3 -0
  4. data/lib/core_ext/date.rb +10 -0
  5. data/lib/core_ext/duplicable.rb +43 -0
  6. data/lib/core_ext/extract_options.rb +14 -0
  7. data/lib/core_ext/inheritable_attributes.rb +222 -0
  8. data/lib/core_ext/object.rb +5 -0
  9. data/lib/core_ext/string.rb +19 -0
  10. data/lib/core_ext/symbol.rb +15 -0
  11. data/lib/core_ext/time.rb +12 -0
  12. data/lib/couch_tomato/database.rb +279 -0
  13. data/lib/couch_tomato/js_view_source.rb +182 -0
  14. data/lib/couch_tomato/migration.rb +52 -0
  15. data/lib/couch_tomato/migrator.rb +235 -0
  16. data/lib/couch_tomato/persistence/base.rb +62 -0
  17. data/lib/couch_tomato/persistence/belongs_to_property.rb +58 -0
  18. data/lib/couch_tomato/persistence/callbacks.rb +60 -0
  19. data/lib/couch_tomato/persistence/dirty_attributes.rb +27 -0
  20. data/lib/couch_tomato/persistence/json.rb +48 -0
  21. data/lib/couch_tomato/persistence/magic_timestamps.rb +15 -0
  22. data/lib/couch_tomato/persistence/properties.rb +58 -0
  23. data/lib/couch_tomato/persistence/simple_property.rb +97 -0
  24. data/lib/couch_tomato/persistence/validation.rb +18 -0
  25. data/lib/couch_tomato/persistence.rb +85 -0
  26. data/lib/couch_tomato/replicator.rb +50 -0
  27. data/lib/couch_tomato.rb +46 -0
  28. data/lib/tasks/couch_tomato.rake +128 -0
  29. data/rails/init.rb +7 -0
  30. data/spec/callbacks_spec.rb +271 -0
  31. data/spec/comment.rb +8 -0
  32. data/spec/create_spec.rb +22 -0
  33. data/spec/custom_view_spec.rb +134 -0
  34. data/spec/destroy_spec.rb +29 -0
  35. data/spec/fixtures/address.rb +9 -0
  36. data/spec/fixtures/person.rb +6 -0
  37. data/spec/property_spec.rb +103 -0
  38. data/spec/spec_helper.rb +40 -0
  39. data/spec/unit/attributes_spec.rb +26 -0
  40. data/spec/unit/callbacks_spec.rb +33 -0
  41. data/spec/unit/create_spec.rb +58 -0
  42. data/spec/unit/customs_views_spec.rb +15 -0
  43. data/spec/unit/database_spec.rb +38 -0
  44. data/spec/unit/dirty_attributes_spec.rb +113 -0
  45. data/spec/unit/string_spec.rb +13 -0
  46. data/spec/unit/view_query_spec.rb +9 -0
  47. data/spec/update_spec.rb +40 -0
  48. data/test/test_helper.rb +63 -0
  49. data/test/unit/database_test.rb +285 -0
  50. data/test/unit/js_view_test.rb +362 -0
  51. data/test/unit/property_test.rb +193 -0
  52. metadata +133 -0
data/MIT-LICENSE.txt ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2007 Bryan Helmkamp, Seth Fitzsimmons
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,96 @@
1
+ Couch Tomato
2
+ ============
3
+
4
+ ## TODO
5
+
6
+ ### Documentation
7
+
8
+ - Quick start
9
+ - Data migration mechanism for managing (implicit) schema and data changes between team members and deployed systems, complete with generator and Rake tasks
10
+ - Rake tasks that facilitate replication of local and remote CouchDB databases
11
+ - Removed model property dirty tracking (added significant complexity and we haven't needed it...yet?)
12
+ - Shoulda instead of RSpec
13
+ - RR for mocks/stubs
14
+
15
+ ### Code
16
+
17
+ - default "server" to `http://localhost:5984`
18
+
19
+
20
+ ## Potato/Tomato
21
+
22
+ We're huge fans of Couch Potato. We love it's advocacy for using Couch naturally (not trying to make it look like a SQL database). Originally a Couch Potato fork, Couch Tomato supports our own production needs.
23
+
24
+ ### Multi-Database Support
25
+
26
+ CouchDB makes it dead-simple to manage multiple databases. For large data-sets, it's very important to separate unrelated documents into separate databases. Couch Tomato assumes (but doesn't force) the use of multiple databases.
27
+
28
+ class UserDb < CouchTomato::Database
29
+ name "users"
30
+ server "http://#{APP_CONFIG["couchdb_address"]}:#{APP_CONFIG["couchdb_port"]}"
31
+ end
32
+
33
+ class StatDb < CouchTomato::Database
34
+ name "stats"
35
+ server "http://#{APP_CONFIG["couchdb_address"]}:#{APP_CONFIG["couchdb_port"]}"
36
+ end
37
+
38
+ UserDb.save_doc(User.new({:name => 'Joe'}))
39
+ 5_000.times { StatDb.save_doc(Stat.new({:metric => 10_000 * rand})) }
40
+
41
+ ### Each view determines the model for its values
42
+
43
+ Views return arbitrary hashes. Often a views' value is an entire document (or more correctly, utilize `emit(key, null)` combined with `:include_docs => true`). But, a views' value is also often completely independent of the structure of the underlying documents.
44
+
45
+ Define views on the database rather than inside a model (this is arguably more Couch-like). Each views declaration stipulates whether their results should be 'raw' hashes or a particular model type.
46
+
47
+ class UserDb < CouchTomato::Database
48
+ name "users"
49
+ server "http://#{APP_CONFIG["couchdb_address"]}:#{APP_CONFIG["couchdb_port"]}"
50
+
51
+ view :by_created_at, User
52
+ view :count # raw
53
+ end
54
+
55
+ ### Store view definitions on the file system
56
+
57
+ Rather than having Ruby generate JavaScript or writing JavaScript in our Ruby code as a string, define views in files on the file system:
58
+
59
+ RAILS_ROOT/couchdb/views/users/*-map.js
60
+ RAILS_ROOT/couchdb/views/users/*-reduce.js
61
+
62
+ The reduce is optional. If you want to define views in a specific design document (called 'lazy'), you can do so:
63
+
64
+ RAILS_ROOT/couchdb/views/users/lazy/*.js
65
+
66
+ There's a handy generator:
67
+
68
+ script/generate view users by_created_at
69
+ script/generate view users/lazy by_birthday
70
+ script/generate view users by_created_on map reduce
71
+
72
+ Rake tasks apply the views on the file system to Couch, skipping views that aren't dirty:
73
+
74
+ rake couch_tomato:push
75
+
76
+ You can also view the differences between the views in Couch and those on the file system:
77
+
78
+ rake couch_tomato:diff
79
+
80
+ ### Remove dynamically generated views
81
+
82
+ We almost always need to write JavaScript to get the view behavior we need, and, for both conceptual and implementation complexity reasons, we value having all the views contained in one place--the file system. This also simplifies deployment and collaboration workflows.
83
+
84
+ ### Multiple design documents per database
85
+
86
+ CouchDB supports multiple design documents per database. There's an important semantic consideration: all views in a design document are updated if any one view needs to be updated. To improve the read performance of couch views under high-volume reads and writes, you could organize views that don't need to be as timely into a separate design document named 'lazy', and always include the `stale=true` couch option in queries to views defined in the 'lazy' design document. You could then have a script that ran periodically to trigger the 'lazy' views to update.
87
+
88
+ class UserDb > CouchTomato::Database
89
+ name :users
90
+
91
+ view :by_created_at, User
92
+ view :count # raw
93
+ view 'lazy/count_created_by_date'
94
+ end
95
+
96
+ We have not had a use for this, nor have we demonstrated that the claimed performance benefit actually exists (it originated from the CouchDB docs, wiki or list or some-such). But, it is instance where this approach to representing views maps fairly directly to CouchDB functionality.
data/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ # this is for rails only
2
+
3
+ require File.dirname(__FILE__) + '/rails/init'
@@ -0,0 +1,10 @@
1
+ class Date
2
+ def to_json(*a)
3
+ %("#{strftime("%Y/%m/%d")}")
4
+ end
5
+
6
+ def self.json_create string
7
+ return nil if string.nil?
8
+ Date.parse(string)
9
+ end
10
+ end
@@ -0,0 +1,43 @@
1
+ class Object
2
+ # Can you safely .dup this object?
3
+ # False for nil, false, true, symbols, and numbers; true otherwise.
4
+ def duplicable?
5
+ true
6
+ end
7
+ end
8
+
9
+ class NilClass #:nodoc:
10
+ def duplicable?
11
+ false
12
+ end
13
+ end
14
+
15
+ class FalseClass #:nodoc:
16
+ def duplicable?
17
+ false
18
+ end
19
+ end
20
+
21
+ class TrueClass #:nodoc:
22
+ def duplicable?
23
+ false
24
+ end
25
+ end
26
+
27
+ class Symbol #:nodoc:
28
+ def duplicable?
29
+ false
30
+ end
31
+ end
32
+
33
+ class Numeric #:nodoc:
34
+ def duplicable?
35
+ false
36
+ end
37
+ end
38
+
39
+ class Class #:nodoc:
40
+ def duplicable?
41
+ false
42
+ end
43
+ end
@@ -0,0 +1,14 @@
1
+ class Array
2
+ # Extracts options from a set of arguments. Removes and returns the last
3
+ # element in the array if it's a hash, otherwise returns a blank hash.
4
+ #
5
+ # def options(*args)
6
+ # args.extract_options!
7
+ # end
8
+ #
9
+ # options(1, 2) # => {}
10
+ # options(1, 2, :a => :b) # => {:a=>:b}
11
+ def extract_options!
12
+ last.is_a?(::Hash) ? pop : {}
13
+ end
14
+ end
@@ -0,0 +1,222 @@
1
+
2
+ # Retain for backward compatibility. Methods are now included in Class.
3
+ module ClassInheritableAttributes # :nodoc:
4
+ end
5
+
6
+ # Allows attributes to be shared within an inheritance hierarchy, but where each descendant gets a copy of
7
+ # their parents' attributes, instead of just a pointer to the same. This means that the child can add elements
8
+ # to, for example, an array without those additions being shared with either their parent, siblings, or
9
+ # children, which is unlike the regular class-level attributes that are shared across the entire hierarchy.
10
+ class Class # :nodoc:
11
+ def class_inheritable_reader(*syms)
12
+ syms.each do |sym|
13
+ next if sym.is_a?(Hash)
14
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
15
+ def self.#{sym} # def self.after_add
16
+ read_inheritable_attribute(:#{sym}) # read_inheritable_attribute(:after_add)
17
+ end # end
18
+
19
+ def #{sym} # def after_add
20
+ self.class.#{sym} # self.class.after_add
21
+ end # end
22
+ EOS
23
+ end
24
+ end
25
+
26
+ def class_inheritable_writer(*syms)
27
+ options = syms.extract_options!
28
+ syms.each do |sym|
29
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
30
+ def self.#{sym}=(obj) # def self.color=(obj)
31
+ write_inheritable_attribute(:#{sym}, obj) # write_inheritable_attribute(:color, obj)
32
+ end # end
33
+ #
34
+ #{" #
35
+ def #{sym}=(obj) # def color=(obj)
36
+ self.class.#{sym} = obj # self.class.color = obj
37
+ end # end
38
+ " unless options[:instance_writer] == false } # # the writer above is generated unless options[:instance_writer] == false
39
+ EOS
40
+ end
41
+ end
42
+
43
+ def class_inheritable_array_writer(*syms)
44
+ options = syms.extract_options!
45
+ syms.each do |sym|
46
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
47
+ def self.#{sym}=(obj) # def self.levels=(obj)
48
+ write_inheritable_array(:#{sym}, obj) # write_inheritable_array(:levels, obj)
49
+ end # end
50
+ #
51
+ #{" #
52
+ def #{sym}=(obj) # def levels=(obj)
53
+ self.class.#{sym} = obj # self.class.levels = obj
54
+ end # end
55
+ " unless options[:instance_writer] == false } # # the writer above is generated unless options[:instance_writer] == false
56
+ EOS
57
+ end
58
+ end
59
+
60
+ def class_inheritable_hash_writer(*syms)
61
+ options = syms.extract_options!
62
+ syms.each do |sym|
63
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
64
+ def self.#{sym}=(obj) # def self.nicknames=(obj)
65
+ write_inheritable_hash(:#{sym}, obj) # write_inheritable_hash(:nicknames, obj)
66
+ end # end
67
+ #
68
+ #{" #
69
+ def #{sym}=(obj) # def nicknames=(obj)
70
+ self.class.#{sym} = obj # self.class.nicknames = obj
71
+ end # end
72
+ " unless options[:instance_writer] == false } # # the writer above is generated unless options[:instance_writer] == false
73
+ EOS
74
+ end
75
+ end
76
+
77
+ def class_inheritable_accessor(*syms)
78
+ class_inheritable_reader(*syms)
79
+ class_inheritable_writer(*syms)
80
+ end
81
+
82
+ def class_inheritable_array(*syms)
83
+ class_inheritable_reader(*syms)
84
+ class_inheritable_array_writer(*syms)
85
+ end
86
+
87
+ def class_inheritable_hash(*syms)
88
+ class_inheritable_reader(*syms)
89
+ class_inheritable_hash_writer(*syms)
90
+ end
91
+
92
+ def inheritable_attributes
93
+ @inheritable_attributes ||= EMPTY_INHERITABLE_ATTRIBUTES
94
+ end
95
+
96
+ def write_inheritable_attribute(key, value)
97
+ if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES)
98
+ @inheritable_attributes = {}
99
+ end
100
+ inheritable_attributes[key] = value
101
+ end
102
+
103
+ def write_inheritable_array(key, elements)
104
+ write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil?
105
+ write_inheritable_attribute(key, read_inheritable_attribute(key) + elements)
106
+ end
107
+
108
+ def write_inheritable_hash(key, hash)
109
+ write_inheritable_attribute(key, {}) if read_inheritable_attribute(key).nil?
110
+ write_inheritable_attribute(key, read_inheritable_attribute(key).merge(hash))
111
+ end
112
+
113
+ def read_inheritable_attribute(key)
114
+ inheritable_attributes[key]
115
+ end
116
+
117
+ def reset_inheritable_attributes
118
+ @inheritable_attributes = EMPTY_INHERITABLE_ATTRIBUTES
119
+ end
120
+
121
+ private
122
+ # Prevent this constant from being created multiple times
123
+ EMPTY_INHERITABLE_ATTRIBUTES = {}.freeze unless const_defined?(:EMPTY_INHERITABLE_ATTRIBUTES)
124
+
125
+ def inherited_with_inheritable_attributes(child)
126
+ inherited_without_inheritable_attributes(child) if respond_to?(:inherited_without_inheritable_attributes)
127
+
128
+ if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES)
129
+ new_inheritable_attributes = EMPTY_INHERITABLE_ATTRIBUTES
130
+ else
131
+ new_inheritable_attributes = inheritable_attributes.inject({}) do |memo, (key, value)|
132
+ memo.update(key => value.duplicable? ? value.dup : value)
133
+ end
134
+ end
135
+
136
+ child.instance_variable_set('@inheritable_attributes', new_inheritable_attributes)
137
+ end
138
+
139
+ alias inherited_without_inheritable_attributes inherited
140
+ alias inherited inherited_with_inheritable_attributes
141
+ end
142
+
143
+ class Class
144
+ # Defines class-level inheritable attribute reader. Attributes are available to subclasses,
145
+ # each subclass has a copy of parent's attribute.
146
+ #
147
+ # @param *syms<Array[#to_s]> Array of attributes to define inheritable reader for.
148
+ # @return <Array[#to_s]> Array of attributes converted into inheritable_readers.
149
+ #
150
+ # @api public
151
+ #
152
+ # @todo Do we want to block instance_reader via :instance_reader => false
153
+ # @todo It would be preferable that we do something with a Hash passed in
154
+ # (error out or do the same as other methods above) instead of silently
155
+ # moving on). In particular, this makes the return value of this function
156
+ # less useful.
157
+ def extlib_inheritable_reader(*ivars)
158
+ instance_reader = ivars.pop[:reader] if ivars.last.is_a?(Hash)
159
+
160
+ ivars.each do |ivar|
161
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
162
+ def self.#{ivar}
163
+ return @#{ivar} if self.object_id == #{self.object_id} || defined?(@#{ivar})
164
+ ivar = superclass.#{ivar}
165
+ return nil if ivar.nil? && !#{self}.instance_variable_defined?("@#{ivar}")
166
+ @#{ivar} = ivar && !ivar.is_a?(Module) && !ivar.is_a?(Numeric) && !ivar.is_a?(TrueClass) && !ivar.is_a?(FalseClass) ? ivar.dup : ivar
167
+ end
168
+ RUBY
169
+ unless instance_reader == false
170
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
171
+ def #{ivar}
172
+ self.class.#{ivar}
173
+ end
174
+ RUBY
175
+ end
176
+ end
177
+ end
178
+
179
+ # Defines class-level inheritable attribute writer. Attributes are available to subclasses,
180
+ # each subclass has a copy of parent's attribute.
181
+ #
182
+ # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
183
+ # define inheritable writer for.
184
+ # @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
185
+ # @return <Array[#to_s]> An Array of the attributes that were made into inheritable writers.
186
+ #
187
+ # @api public
188
+ #
189
+ # @todo We need a style for class_eval <<-HEREDOC. I'd like to make it
190
+ # class_eval(<<-RUBY, __FILE__, __LINE__), but we should codify it somewhere.
191
+ def extlib_inheritable_writer(*ivars)
192
+ instance_writer = ivars.pop[:writer] if ivars.last.is_a?(Hash)
193
+ ivars.each do |ivar|
194
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
195
+ def self.#{ivar}=(obj)
196
+ @#{ivar} = obj
197
+ end
198
+ RUBY
199
+ unless instance_writer == false
200
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
201
+ def #{ivar}=(obj) self.class.#{ivar} = obj end
202
+ RUBY
203
+ end
204
+
205
+ self.send("#{ivar}=", yield) if block_given?
206
+ end
207
+ end
208
+
209
+ # Defines class-level inheritable attribute accessor. Attributes are available to subclasses,
210
+ # each subclass has a copy of parent's attribute.
211
+ #
212
+ # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
213
+ # define inheritable accessor for.
214
+ # @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
215
+ # @return <Array[#to_s]> An Array of attributes turned into inheritable accessors.
216
+ #
217
+ # @api public
218
+ def extlib_inheritable_accessor(*syms, &block)
219
+ extlib_inheritable_reader(*syms)
220
+ extlib_inheritable_writer(*syms, &block)
221
+ end
222
+ end
@@ -0,0 +1,5 @@
1
+ Object.class_eval do
2
+ def try(method, *args)
3
+ self.send method, *args if self.respond_to?(method)
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ module ActiveSupportMethods
2
+ def camelize
3
+ sub(/^([a-z])/) {$1.upcase}.gsub(/_([a-z])/) do
4
+ $1.upcase
5
+ end
6
+ end
7
+
8
+ # Source
9
+ # http://github.com/rails/rails/blob/b600bf2cd728c90d50cc34456c944b2dfefe8c8d/activesupport/lib/active_support/inflector.rb
10
+ def underscore
11
+ gsub(/::/, '/').
12
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
13
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
14
+ tr("-", "_").
15
+ downcase
16
+ end
17
+ end
18
+
19
+ String.send :include, ActiveSupportMethods unless String.new.respond_to?(:underscore)
@@ -0,0 +1,15 @@
1
+ # taken from ActiveSupport 2.3.2
2
+ unless :to_proc.respond_to?(:to_proc)
3
+ class Symbol
4
+ # Turns the symbol into a simple proc, which is especially useful for enumerations. Examples:
5
+ #
6
+ # # The same as people.collect { |p| p.name }
7
+ # people.collect(&:name)
8
+ #
9
+ # # The same as people.select { |p| p.manager? }.collect { |p| p.salary }
10
+ # people.select(&:manager?).collect(&:salary)
11
+ def to_proc
12
+ Proc.new { |*args| args.shift.__send__(self, *args) }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ class Time
2
+ def to_json(*a)
3
+ self.utc
4
+ %("#{strftime("%Y/%m/%d %H:%M:%S +0000")}")
5
+ end
6
+
7
+ def self.json_create string
8
+ return nil if string.nil?
9
+ d = DateTime.parse(string).new_offset
10
+ self.utc(d.year, d.month, d.day, d.hour, d.min, d.sec)
11
+ end
12
+ end