oz-couchrest 0.29

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.
Files changed (95) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +95 -0
  3. data/Rakefile +74 -0
  4. data/THANKS.md +18 -0
  5. data/examples/model/example.rb +144 -0
  6. data/examples/word_count/markov +38 -0
  7. data/examples/word_count/views/books/chunked-map.js +3 -0
  8. data/examples/word_count/views/books/united-map.js +1 -0
  9. data/examples/word_count/views/markov/chain-map.js +6 -0
  10. data/examples/word_count/views/markov/chain-reduce.js +7 -0
  11. data/examples/word_count/views/word_count/count-map.js +6 -0
  12. data/examples/word_count/views/word_count/count-reduce.js +3 -0
  13. data/examples/word_count/word_count.rb +46 -0
  14. data/examples/word_count/word_count_query.rb +40 -0
  15. data/examples/word_count/word_count_views.rb +26 -0
  16. data/lib/couchrest.rb +198 -0
  17. data/lib/couchrest/commands/generate.rb +71 -0
  18. data/lib/couchrest/commands/push.rb +103 -0
  19. data/lib/couchrest/core/database.rb +326 -0
  20. data/lib/couchrest/core/design.rb +91 -0
  21. data/lib/couchrest/core/document.rb +87 -0
  22. data/lib/couchrest/core/response.rb +16 -0
  23. data/lib/couchrest/core/server.rb +88 -0
  24. data/lib/couchrest/core/view.rb +4 -0
  25. data/lib/couchrest/helper/pager.rb +103 -0
  26. data/lib/couchrest/helper/streamer.rb +44 -0
  27. data/lib/couchrest/helper/upgrade.rb +51 -0
  28. data/lib/couchrest/mixins.rb +4 -0
  29. data/lib/couchrest/mixins/attachments.rb +31 -0
  30. data/lib/couchrest/mixins/callbacks.rb +483 -0
  31. data/lib/couchrest/mixins/class_proxy.rb +112 -0
  32. data/lib/couchrest/mixins/collection.rb +222 -0
  33. data/lib/couchrest/mixins/design_doc.rb +114 -0
  34. data/lib/couchrest/mixins/document_queries.rb +53 -0
  35. data/lib/couchrest/mixins/extended_attachments.rb +74 -0
  36. data/lib/couchrest/mixins/extended_document_mixins.rb +8 -0
  37. data/lib/couchrest/mixins/properties.rb +125 -0
  38. data/lib/couchrest/mixins/validation.rb +257 -0
  39. data/lib/couchrest/mixins/views.rb +211 -0
  40. data/lib/couchrest/monkeypatches.rb +112 -0
  41. data/lib/couchrest/more/casted_model.rb +29 -0
  42. data/lib/couchrest/more/extended_document.rb +232 -0
  43. data/lib/couchrest/more/property.rb +40 -0
  44. data/lib/couchrest/support/blank.rb +42 -0
  45. data/lib/couchrest/support/class.rb +176 -0
  46. data/lib/couchrest/support/rails.rb +35 -0
  47. data/lib/couchrest/validation/auto_validate.rb +161 -0
  48. data/lib/couchrest/validation/contextual_validators.rb +78 -0
  49. data/lib/couchrest/validation/validation_errors.rb +118 -0
  50. data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
  51. data/lib/couchrest/validation/validators/confirmation_validator.rb +99 -0
  52. data/lib/couchrest/validation/validators/format_validator.rb +117 -0
  53. data/lib/couchrest/validation/validators/formats/email.rb +66 -0
  54. data/lib/couchrest/validation/validators/formats/url.rb +43 -0
  55. data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
  56. data/lib/couchrest/validation/validators/length_validator.rb +134 -0
  57. data/lib/couchrest/validation/validators/method_validator.rb +89 -0
  58. data/lib/couchrest/validation/validators/numeric_validator.rb +104 -0
  59. data/lib/couchrest/validation/validators/required_field_validator.rb +109 -0
  60. data/spec/couchrest/core/couchrest_spec.rb +201 -0
  61. data/spec/couchrest/core/database_spec.rb +700 -0
  62. data/spec/couchrest/core/design_spec.rb +138 -0
  63. data/spec/couchrest/core/document_spec.rb +267 -0
  64. data/spec/couchrest/core/server_spec.rb +35 -0
  65. data/spec/couchrest/helpers/pager_spec.rb +122 -0
  66. data/spec/couchrest/helpers/streamer_spec.rb +23 -0
  67. data/spec/couchrest/more/casted_extended_doc_spec.rb +75 -0
  68. data/spec/couchrest/more/casted_model_spec.rb +177 -0
  69. data/spec/couchrest/more/extended_doc_attachment_spec.rb +135 -0
  70. data/spec/couchrest/more/extended_doc_spec.rb +563 -0
  71. data/spec/couchrest/more/extended_doc_subclass_spec.rb +98 -0
  72. data/spec/couchrest/more/extended_doc_view_spec.rb +414 -0
  73. data/spec/couchrest/more/property_spec.rb +146 -0
  74. data/spec/fixtures/attachments/README +3 -0
  75. data/spec/fixtures/attachments/couchdb.png +0 -0
  76. data/spec/fixtures/attachments/test.html +11 -0
  77. data/spec/fixtures/more/article.rb +34 -0
  78. data/spec/fixtures/more/card.rb +22 -0
  79. data/spec/fixtures/more/cat.rb +18 -0
  80. data/spec/fixtures/more/course.rb +14 -0
  81. data/spec/fixtures/more/event.rb +6 -0
  82. data/spec/fixtures/more/invoice.rb +17 -0
  83. data/spec/fixtures/more/person.rb +8 -0
  84. data/spec/fixtures/more/question.rb +6 -0
  85. data/spec/fixtures/more/service.rb +12 -0
  86. data/spec/fixtures/views/lib.js +3 -0
  87. data/spec/fixtures/views/test_view/lib.js +3 -0
  88. data/spec/fixtures/views/test_view/only-map.js +4 -0
  89. data/spec/fixtures/views/test_view/test-map.js +3 -0
  90. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  91. data/spec/spec.opts +6 -0
  92. data/spec/spec_helper.rb +37 -0
  93. data/utils/remap.rb +27 -0
  94. data/utils/subset.rb +30 -0
  95. metadata +194 -0
@@ -0,0 +1,112 @@
1
+ module CouchRest
2
+ module Mixins
3
+ module ClassProxy
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ # Return a proxy object which represents a model class on a
12
+ # chosen database instance. This allows you to DRY operations
13
+ # where a database is chosen dynamically.
14
+ #
15
+ # ==== Example:
16
+ #
17
+ # db = CouchRest::Database.new(...)
18
+ # articles = Article.on(db)
19
+ #
20
+ # articles.all { ... }
21
+ # articles.by_title { ... }
22
+ #
23
+ # u = articles.get("someid")
24
+ #
25
+ # u = articles.new(:title => "I like plankton")
26
+ # u.save # saved on the correct database
27
+
28
+ def on(database)
29
+ Proxy.new(self, database)
30
+ end
31
+ end
32
+
33
+ class Proxy #:nodoc:
34
+ def initialize(klass, database)
35
+ @klass = klass
36
+ @database = database
37
+ end
38
+
39
+ # ExtendedDocument
40
+
41
+ def new(*args)
42
+ doc = @klass.new(*args)
43
+ doc.database = @database
44
+ doc
45
+ end
46
+
47
+ def method_missing(m, *args, &block)
48
+ if has_view?(m)
49
+ query = args.shift || {}
50
+ view(m, query, *args, &block)
51
+ else
52
+ super
53
+ end
54
+ end
55
+
56
+ # Mixins::DocumentQueries
57
+
58
+ def all(opts = {}, &block)
59
+ @klass.all({:database => @database}.merge(opts), &block)
60
+ end
61
+
62
+ def first(opts = {})
63
+ @klass.first({:database => @database}.merge(opts))
64
+ end
65
+
66
+ def get(id)
67
+ @klass.get(id, @database)
68
+ end
69
+
70
+ # Mixins::Views
71
+
72
+ def has_view?(view)
73
+ @klass.has_view?(view)
74
+ end
75
+
76
+ def view(name, query={}, &block)
77
+ @klass.view(name, {:database => @database}.merge(query), &block)
78
+ end
79
+
80
+ def all_design_doc_versions
81
+ @klass.all_design_doc_versions(@database)
82
+ end
83
+
84
+ def model_design_doc
85
+ @klass.model_design_doc(@database)
86
+ end
87
+
88
+ def cleanup_design_docs!
89
+ @klass.cleanup_design_docs!(@database)
90
+ end
91
+
92
+ # Mixins::DesignDoc
93
+
94
+ def design_doc
95
+ @klass.design_doc
96
+ end
97
+
98
+ def design_doc_fresh
99
+ @klass.design_doc_fresh
100
+ end
101
+
102
+ def refresh_design_doc
103
+ @klass.refresh_design_doc
104
+ end
105
+
106
+ def save_design_doc
107
+ @klass.save_design_doc_on(@database)
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,222 @@
1
+ module CouchRest
2
+ module Mixins
3
+ module Collection
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ # Creates a new class method, find_all_<collection_name>, that will
12
+ # execute the view specified with the design_doc and view_name
13
+ # parameters, along with the specified view_options. This method will
14
+ # return the results of the view as an Array of objects which are
15
+ # instances of the class.
16
+ #
17
+ # This method is handy for objects that do not use the view_by method
18
+ # to declare their views.
19
+ def provides_collection(collection_name, design_doc, view_name, view_options)
20
+ class_eval <<-END, __FILE__, __LINE__ + 1
21
+ def self.find_all_#{collection_name}(options = {})
22
+ view_options = #{view_options.inspect} || {}
23
+ CollectionProxy.new(@database, "#{design_doc}", "#{view_name}", view_options.merge(options), Kernel.const_get('#{self}'))
24
+ end
25
+ END
26
+ end
27
+
28
+ # Fetch a group of objects from CouchDB. Options can include:
29
+ # :page - Specifies the page to load (starting at 1)
30
+ # :per_page - Specifies the number of objects to load per page
31
+ #
32
+ # Defaults are used if these options are not specified.
33
+ def paginate(options)
34
+ proxy = create_collection_proxy(options)
35
+ proxy.paginate(options)
36
+ end
37
+
38
+ # Iterate over the objects in a collection, fetching them from CouchDB
39
+ # in groups. Options can include:
40
+ # :page - Specifies the page to load
41
+ # :per_page - Specifies the number of objects to load per page
42
+ #
43
+ # Defaults are used if these options are not specified.
44
+ def paginated_each(options, &block)
45
+ proxy = create_collection_proxy(options)
46
+ proxy.paginated_each(options, &block)
47
+ end
48
+
49
+ # Create a CollectionProxy for the specified view and options.
50
+ # CollectionProxy behaves just like an Array, but offers support for
51
+ # pagination.
52
+ def collection_proxy_for(design_doc, view_name, view_options = {})
53
+ options = view_options.merge(:design_doc => design_doc, :view_name => view_name)
54
+ create_collection_proxy(options)
55
+ end
56
+
57
+ private
58
+
59
+ def create_collection_proxy(options)
60
+ design_doc, view_name, view_options = parse_view_options(options)
61
+ CollectionProxy.new(@database, design_doc, view_name, view_options, self)
62
+ end
63
+
64
+ def parse_view_options(options)
65
+ design_doc = options.delete(:design_doc)
66
+ raise ArgumentError, 'design_doc is required' if design_doc.nil?
67
+
68
+ view_name = options.delete(:view_name)
69
+ raise ArgumentError, 'view_name is required' if view_name.nil?
70
+
71
+ default_view_options = (design_doc.class == Design &&
72
+ design_doc['views'][view_name.to_s] &&
73
+ design_doc['views'][view_name.to_s]["couchrest-defaults"]) || {}
74
+ view_options = default_view_options.merge(options)
75
+
76
+ [design_doc, view_name, view_options]
77
+ end
78
+ end
79
+
80
+ class CollectionProxy
81
+ alias_method :proxy_respond_to?, :respond_to?
82
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
83
+
84
+ DEFAULT_PAGE = 1
85
+ DEFAULT_PER_PAGE = 30
86
+
87
+ # Create a new CollectionProxy to represent the specified view. If a
88
+ # container class is specified, the proxy will create an object of the
89
+ # given type for each row that comes back from the view. If no
90
+ # container class is specified, the raw results are returned.
91
+ #
92
+ # The CollectionProxy provides support for paginating over a collection
93
+ # via the paginate, and paginated_each methods.
94
+ def initialize(database, design_doc, view_name, view_options = {}, container_class = nil)
95
+ raise ArgumentError, "database is a required parameter" if database.nil?
96
+
97
+ @database = database
98
+ @container_class = container_class
99
+
100
+ strip_pagination_options(view_options)
101
+ @view_options = view_options
102
+
103
+ if design_doc.class == Design
104
+ @view_name = "#{design_doc.name}/#{view_name}"
105
+ else
106
+ @view_name = "#{design_doc}/#{view_name}"
107
+ end
108
+ end
109
+
110
+ # See Collection.paginate
111
+ def paginate(options = {})
112
+ page, per_page = parse_options(options)
113
+ results = @database.view(@view_name, pagination_options(page, per_page))
114
+ remember_where_we_left_off(results, page)
115
+ convert_to_container_array(results)
116
+ end
117
+
118
+ # See Collection.paginated_each
119
+ def paginated_each(options = {}, &block)
120
+ page, per_page = parse_options(options)
121
+
122
+ begin
123
+ collection = paginate({:page => page, :per_page => per_page})
124
+ collection.each(&block)
125
+ page += 1
126
+ end until collection.size < per_page
127
+ end
128
+
129
+ def respond_to?(*args)
130
+ proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
131
+ end
132
+
133
+ # Explicitly proxy === because the instance method removal above
134
+ # doesn't catch it.
135
+ def ===(other)
136
+ load_target
137
+ other === @target
138
+ end
139
+
140
+ private
141
+
142
+ def method_missing(method, *args)
143
+ if load_target
144
+ if block_given?
145
+ @target.send(method, *args) { |*block_args| yield(*block_args) }
146
+ else
147
+ @target.send(method, *args)
148
+ end
149
+ end
150
+ end
151
+
152
+ def load_target
153
+ unless loaded?
154
+ results = @database.view(@view_name, @view_options)
155
+ @target = convert_to_container_array(results)
156
+ end
157
+ @loaded = true
158
+ @target
159
+ end
160
+
161
+ def loaded?
162
+ @loaded
163
+ end
164
+
165
+ def reload
166
+ reset
167
+ load_target
168
+ self unless @target.nil?
169
+ end
170
+
171
+ def reset
172
+ @loaded = false
173
+ @target = nil
174
+ end
175
+
176
+ def inspect
177
+ load_target
178
+ @target.inspect
179
+ end
180
+
181
+ def convert_to_container_array(results)
182
+ if @container_class.nil?
183
+ results
184
+ else
185
+ results['rows'].collect { |row| @container_class.new(row['doc']) } unless results['rows'].nil?
186
+ end
187
+ end
188
+
189
+ def pagination_options(page, per_page)
190
+ view_options = @view_options.clone
191
+ if @last_key && @last_docid && @last_page == page - 1
192
+ view_options.delete(:key)
193
+ options = { :startkey => @last_key, :startkey_docid => @last_docid, :limit => per_page, :skip => 1 }
194
+ else
195
+ options = { :limit => per_page, :skip => per_page * (page - 1) }
196
+ end
197
+ view_options.merge(options)
198
+ end
199
+
200
+ def parse_options(options)
201
+ page = options.delete(:page) || DEFAULT_PAGE
202
+ per_page = options.delete(:per_page) || DEFAULT_PER_PAGE
203
+ [page.to_i, per_page.to_i]
204
+ end
205
+
206
+ def strip_pagination_options(options)
207
+ parse_options(options)
208
+ end
209
+
210
+ def remember_where_we_left_off(results, page)
211
+ last_row = results['rows'].last
212
+ if last_row
213
+ @last_key = last_row['key']
214
+ @last_docid = last_row['id']
215
+ end
216
+ @last_page = page
217
+ end
218
+ end
219
+
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,114 @@
1
+ require 'digest/md5'
2
+
3
+ module CouchRest
4
+ module Mixins
5
+ module DesignDoc
6
+
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+ attr_accessor :design_doc, :design_doc_slug_cache, :design_doc_fresh
13
+
14
+ def design_doc
15
+ @design_doc ||= Design.new(default_design_doc)
16
+
17
+ # XXX quite the hack :
18
+ # uncomment if you need to get a fresh design document from CouchDB. -- oz
19
+ #
20
+ # unless @design_doc
21
+ # @design_doc ||= Design.new(default_design_doc)
22
+ # self.refresh_design_doc
23
+ # end
24
+ # @design_doc
25
+ end
26
+
27
+ def design_doc_id
28
+ "_design/#{design_doc_slug}"
29
+ end
30
+
31
+ def design_doc_slug
32
+ return design_doc_slug_cache if (design_doc_slug_cache && design_doc_fresh)
33
+ funcs = []
34
+ design_doc['views'].each do |name, view|
35
+ funcs << "#{name}/#{view['map']}#{view['reduce']}"
36
+ end
37
+ self.design_doc_slug_cache = self.to_s
38
+ end
39
+
40
+ def default_design_doc
41
+ {
42
+ "language" => "javascript",
43
+ "views" => {
44
+ 'all' => {
45
+ 'map' => "function(doc) {
46
+ if (doc['couchrest-type'] == '#{self.to_s}') {
47
+ emit(null,1);
48
+ }
49
+ }",
50
+ 'reduce' => "function(keys, values) {
51
+ return sum(values);
52
+ }"
53
+ }
54
+ }
55
+ }
56
+ end
57
+
58
+ def refresh_design_doc
59
+ reset_design_doc
60
+ save_design_doc
61
+ end
62
+
63
+ # Save the design doc onto the default database, and update the
64
+ # design_doc attribute
65
+ def save_design_doc
66
+ reset_design_doc unless design_doc_fresh
67
+ # Don't let CouchRest update the design and views on each initialization
68
+ #self.design_doc = update_design_doc(design_doc)
69
+ end
70
+
71
+ # Save the design doc onto a target database in a thread-safe way,
72
+ # not modifying the model's design_doc
73
+ def save_design_doc_on(db)
74
+ update_design_doc(Design.new(design_doc), db)
75
+ end
76
+
77
+ private
78
+
79
+ def reset_design_doc
80
+ current = self.database.get(design_doc_id) rescue nil
81
+ design_doc['_id'] = design_doc_id
82
+ if current.nil?
83
+ design_doc.delete('_rev')
84
+ else
85
+ # Don't let CouchRest update the design and views on each initialization
86
+ #design_doc['_rev'] = current['_rev']
87
+ self.design_doc = current
88
+ end
89
+ self.design_doc_fresh = true
90
+ self.design_doc
91
+ end
92
+
93
+ # Writes out a design_doc to a given database, returning the
94
+ # updated design doc
95
+ def update_design_doc(design_doc, db = database)
96
+ saved = db.get(design_doc['_id']) rescue nil
97
+ if saved
98
+ design_doc['views'].each do |name, view|
99
+ saved['views'][name] = view
100
+ end
101
+ db.save_doc(saved)
102
+ saved
103
+ else
104
+ design_doc.database = db
105
+ design_doc.save
106
+ design_doc
107
+ end
108
+ end
109
+
110
+ end # module ClassMethods
111
+
112
+ end
113
+ end
114
+ end