norr-couchrest 0.30

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 (96) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +117 -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/history.txt +19 -0
  17. data/lib/couchrest.rb +198 -0
  18. data/lib/couchrest/commands/generate.rb +71 -0
  19. data/lib/couchrest/commands/push.rb +103 -0
  20. data/lib/couchrest/core/database.rb +320 -0
  21. data/lib/couchrest/core/design.rb +79 -0
  22. data/lib/couchrest/core/document.rb +87 -0
  23. data/lib/couchrest/core/response.rb +16 -0
  24. data/lib/couchrest/core/server.rb +88 -0
  25. data/lib/couchrest/core/view.rb +4 -0
  26. data/lib/couchrest/helper/pager.rb +103 -0
  27. data/lib/couchrest/helper/streamer.rb +44 -0
  28. data/lib/couchrest/helper/upgrade.rb +51 -0
  29. data/lib/couchrest/mixins.rb +4 -0
  30. data/lib/couchrest/mixins/attachments.rb +31 -0
  31. data/lib/couchrest/mixins/callbacks.rb +483 -0
  32. data/lib/couchrest/mixins/class_proxy.rb +112 -0
  33. data/lib/couchrest/mixins/collection.rb +220 -0
  34. data/lib/couchrest/mixins/design_doc.rb +101 -0
  35. data/lib/couchrest/mixins/document_queries.rb +53 -0
  36. data/lib/couchrest/mixins/extended_attachments.rb +74 -0
  37. data/lib/couchrest/mixins/extended_document_mixins.rb +8 -0
  38. data/lib/couchrest/mixins/properties.rb +147 -0
  39. data/lib/couchrest/mixins/validation.rb +257 -0
  40. data/lib/couchrest/mixins/views.rb +181 -0
  41. data/lib/couchrest/monkeypatches.rb +113 -0
  42. data/lib/couchrest/more/casted_model.rb +29 -0
  43. data/lib/couchrest/more/extended_document.rb +229 -0
  44. data/lib/couchrest/more/property.rb +40 -0
  45. data/lib/couchrest/support/blank.rb +42 -0
  46. data/lib/couchrest/support/class.rb +176 -0
  47. data/lib/couchrest/support/rails.rb +35 -0
  48. data/lib/couchrest/validation/auto_validate.rb +161 -0
  49. data/lib/couchrest/validation/contextual_validators.rb +78 -0
  50. data/lib/couchrest/validation/validation_errors.rb +118 -0
  51. data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
  52. data/lib/couchrest/validation/validators/confirmation_validator.rb +99 -0
  53. data/lib/couchrest/validation/validators/format_validator.rb +117 -0
  54. data/lib/couchrest/validation/validators/formats/email.rb +66 -0
  55. data/lib/couchrest/validation/validators/formats/url.rb +43 -0
  56. data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
  57. data/lib/couchrest/validation/validators/length_validator.rb +134 -0
  58. data/lib/couchrest/validation/validators/method_validator.rb +89 -0
  59. data/lib/couchrest/validation/validators/numeric_validator.rb +104 -0
  60. data/lib/couchrest/validation/validators/required_field_validator.rb +109 -0
  61. data/spec/couchrest/core/couchrest_spec.rb +201 -0
  62. data/spec/couchrest/core/database_spec.rb +700 -0
  63. data/spec/couchrest/core/design_spec.rb +138 -0
  64. data/spec/couchrest/core/document_spec.rb +267 -0
  65. data/spec/couchrest/core/server_spec.rb +35 -0
  66. data/spec/couchrest/helpers/pager_spec.rb +122 -0
  67. data/spec/couchrest/helpers/streamer_spec.rb +23 -0
  68. data/spec/couchrest/more/casted_extended_doc_spec.rb +75 -0
  69. data/spec/couchrest/more/casted_model_spec.rb +177 -0
  70. data/spec/couchrest/more/extended_doc_attachment_spec.rb +135 -0
  71. data/spec/couchrest/more/extended_doc_spec.rb +563 -0
  72. data/spec/couchrest/more/extended_doc_subclass_spec.rb +98 -0
  73. data/spec/couchrest/more/extended_doc_view_spec.rb +414 -0
  74. data/spec/couchrest/more/property_spec.rb +146 -0
  75. data/spec/fixtures/attachments/README +3 -0
  76. data/spec/fixtures/attachments/couchdb.png +0 -0
  77. data/spec/fixtures/attachments/test.html +11 -0
  78. data/spec/fixtures/more/article.rb +34 -0
  79. data/spec/fixtures/more/card.rb +22 -0
  80. data/spec/fixtures/more/cat.rb +18 -0
  81. data/spec/fixtures/more/course.rb +14 -0
  82. data/spec/fixtures/more/event.rb +6 -0
  83. data/spec/fixtures/more/invoice.rb +17 -0
  84. data/spec/fixtures/more/person.rb +8 -0
  85. data/spec/fixtures/more/question.rb +6 -0
  86. data/spec/fixtures/more/service.rb +12 -0
  87. data/spec/fixtures/views/lib.js +3 -0
  88. data/spec/fixtures/views/test_view/lib.js +3 -0
  89. data/spec/fixtures/views/test_view/only-map.js +4 -0
  90. data/spec/fixtures/views/test_view/test-map.js +3 -0
  91. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  92. data/spec/spec.opts +6 -0
  93. data/spec/spec_helper.rb +37 -0
  94. data/utils/remap.rb +27 -0
  95. data/utils/subset.rb +30 -0
  96. 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,220 @@
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
+ @last_key = last_row['key']
213
+ @last_docid = last_row['id']
214
+ @last_page = page
215
+ end
216
+ end
217
+
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,101 @@
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
+ end
17
+
18
+ def design_doc_id
19
+ "_design/#{design_doc_slug}"
20
+ end
21
+
22
+ def design_doc_slug
23
+ return design_doc_slug_cache if (design_doc_slug_cache && design_doc_fresh)
24
+ funcs = []
25
+ design_doc['views'].each do |name, view|
26
+ funcs << "#{name}/#{view['map']}#{view['reduce']}"
27
+ end
28
+ self.design_doc_slug_cache = self.to_s
29
+ end
30
+
31
+ def default_design_doc
32
+ {
33
+ "language" => "javascript",
34
+ "views" => {
35
+ 'all' => {
36
+ 'map' => "function(doc) {
37
+ if (doc['couchrest-type'] == '#{self.to_s}') {
38
+ emit(null,1);
39
+ }
40
+ }",
41
+ 'reduce' => "function(keys, values) {
42
+ return sum(values);
43
+ }"
44
+ }
45
+ }
46
+ }
47
+ end
48
+
49
+ def refresh_design_doc
50
+ reset_design_doc
51
+ save_design_doc
52
+ end
53
+
54
+ # Save the design doc onto the default database, and update the
55
+ # design_doc attribute
56
+ def save_design_doc
57
+ reset_design_doc unless design_doc_fresh
58
+ self.design_doc = update_design_doc(design_doc)
59
+ end
60
+
61
+ # Save the design doc onto a target database in a thread-safe way,
62
+ # not modifying the model's design_doc
63
+ def save_design_doc_on(db)
64
+ update_design_doc(Design.new(design_doc), db)
65
+ end
66
+
67
+ private
68
+
69
+ def reset_design_doc
70
+ current = self.database.get(design_doc_id) rescue nil
71
+ design_doc['_id'] = design_doc_id
72
+ if current.nil?
73
+ design_doc.delete('_rev')
74
+ else
75
+ design_doc['_rev'] = current['_rev']
76
+ end
77
+ self.design_doc_fresh = true
78
+ end
79
+
80
+ # Writes out a design_doc to a given database, returning the
81
+ # updated design doc
82
+ def update_design_doc(design_doc, db = database)
83
+ saved = db.get(design_doc['_id']) rescue nil
84
+ if saved
85
+ design_doc['views'].each do |name, view|
86
+ saved['views'][name] = view
87
+ end
88
+ db.save_doc(saved)
89
+ saved
90
+ else
91
+ design_doc.database = db
92
+ design_doc.save
93
+ design_doc
94
+ end
95
+ end
96
+
97
+ end # module ClassMethods
98
+
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,53 @@
1
+ module CouchRest
2
+ module Mixins
3
+ module DocumentQueries
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ # Load all documents that have the "couchrest-type" field equal to the
12
+ # name of the current class. Take the standard set of
13
+ # CouchRest::Database#view options.
14
+ def all(opts = {}, &block)
15
+ view(:all, {:reduce => false}.merge(opts), &block)
16
+ end
17
+
18
+ # Returns the number of documents that have the "couchrest-type" field
19
+ # equal to the name of the current class. Takes the standard set of
20
+ # CouchRest::Database#view options
21
+ def count(opts = {}, &block)
22
+ result = all({:reduce => true}.merge(opts), &block)['rows']
23
+ return 0 if result.empty?
24
+ result.first['value']
25
+ end
26
+
27
+ # Load the first document that have the "couchrest-type" field equal to
28
+ # the name of the current class.
29
+ #
30
+ # ==== Returns
31
+ # Object:: The first object instance available
32
+ # or
33
+ # Nil:: if no instances available
34
+ #
35
+ # ==== Parameters
36
+ # opts<Hash>::
37
+ # View options, see <tt>CouchRest::Database#view</tt> options for more info.
38
+ def first(opts = {})
39
+ first_instance = self.all(opts.merge!(:limit => 1))
40
+ first_instance.empty? ? nil : first_instance.first
41
+ end
42
+
43
+ # Load a document from the database by id
44
+ def get(id, db = database)
45
+ doc = db.get id
46
+ new(doc)
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+ end
53
+ end