openlogic-couchrest_model 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. data/.gitignore +11 -0
  2. data/.rspec +4 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +176 -0
  5. data/README.md +137 -0
  6. data/Rakefile +38 -0
  7. data/THANKS.md +21 -0
  8. data/VERSION +1 -0
  9. data/benchmarks/dirty.rb +118 -0
  10. data/couchrest_model.gemspec +36 -0
  11. data/history.md +309 -0
  12. data/init.rb +1 -0
  13. data/lib/couchrest/model.rb +10 -0
  14. data/lib/couchrest/model/associations.rb +231 -0
  15. data/lib/couchrest/model/base.rb +129 -0
  16. data/lib/couchrest/model/callbacks.rb +28 -0
  17. data/lib/couchrest/model/casted_array.rb +83 -0
  18. data/lib/couchrest/model/casted_by.rb +33 -0
  19. data/lib/couchrest/model/casted_hash.rb +84 -0
  20. data/lib/couchrest/model/class_proxy.rb +135 -0
  21. data/lib/couchrest/model/collection.rb +273 -0
  22. data/lib/couchrest/model/configuration.rb +67 -0
  23. data/lib/couchrest/model/connection.rb +70 -0
  24. data/lib/couchrest/model/core_extensions/hash.rb +9 -0
  25. data/lib/couchrest/model/core_extensions/time_parsing.rb +66 -0
  26. data/lib/couchrest/model/design_doc.rb +128 -0
  27. data/lib/couchrest/model/designs.rb +91 -0
  28. data/lib/couchrest/model/designs/view.rb +513 -0
  29. data/lib/couchrest/model/dirty.rb +39 -0
  30. data/lib/couchrest/model/document_queries.rb +99 -0
  31. data/lib/couchrest/model/embeddable.rb +78 -0
  32. data/lib/couchrest/model/errors.rb +25 -0
  33. data/lib/couchrest/model/extended_attachments.rb +83 -0
  34. data/lib/couchrest/model/persistence.rb +178 -0
  35. data/lib/couchrest/model/properties.rb +228 -0
  36. data/lib/couchrest/model/property.rb +114 -0
  37. data/lib/couchrest/model/property_protection.rb +71 -0
  38. data/lib/couchrest/model/proxyable.rb +183 -0
  39. data/lib/couchrest/model/support/couchrest_database.rb +13 -0
  40. data/lib/couchrest/model/support/couchrest_design.rb +33 -0
  41. data/lib/couchrest/model/typecast.rb +154 -0
  42. data/lib/couchrest/model/validations.rb +80 -0
  43. data/lib/couchrest/model/validations/casted_model.rb +16 -0
  44. data/lib/couchrest/model/validations/locale/en.yml +5 -0
  45. data/lib/couchrest/model/validations/uniqueness.rb +69 -0
  46. data/lib/couchrest/model/views.rb +151 -0
  47. data/lib/couchrest/railtie.rb +24 -0
  48. data/lib/couchrest_model.rb +66 -0
  49. data/lib/rails/generators/couchrest_model.rb +16 -0
  50. data/lib/rails/generators/couchrest_model/config/config_generator.rb +18 -0
  51. data/lib/rails/generators/couchrest_model/config/templates/couchdb.yml +21 -0
  52. data/lib/rails/generators/couchrest_model/model/model_generator.rb +27 -0
  53. data/lib/rails/generators/couchrest_model/model/templates/model.rb +2 -0
  54. data/spec/.gitignore +1 -0
  55. data/spec/fixtures/attachments/README +3 -0
  56. data/spec/fixtures/attachments/couchdb.png +0 -0
  57. data/spec/fixtures/attachments/test.html +11 -0
  58. data/spec/fixtures/config/couchdb.yml +10 -0
  59. data/spec/fixtures/models/article.rb +36 -0
  60. data/spec/fixtures/models/base.rb +164 -0
  61. data/spec/fixtures/models/card.rb +19 -0
  62. data/spec/fixtures/models/cat.rb +23 -0
  63. data/spec/fixtures/models/client.rb +6 -0
  64. data/spec/fixtures/models/course.rb +27 -0
  65. data/spec/fixtures/models/event.rb +8 -0
  66. data/spec/fixtures/models/invoice.rb +14 -0
  67. data/spec/fixtures/models/key_chain.rb +5 -0
  68. data/spec/fixtures/models/membership.rb +4 -0
  69. data/spec/fixtures/models/person.rb +11 -0
  70. data/spec/fixtures/models/project.rb +6 -0
  71. data/spec/fixtures/models/question.rb +7 -0
  72. data/spec/fixtures/models/sale_entry.rb +9 -0
  73. data/spec/fixtures/models/sale_invoice.rb +14 -0
  74. data/spec/fixtures/models/service.rb +10 -0
  75. data/spec/fixtures/models/user.rb +22 -0
  76. data/spec/fixtures/views/lib.js +3 -0
  77. data/spec/fixtures/views/test_view/lib.js +3 -0
  78. data/spec/fixtures/views/test_view/only-map.js +4 -0
  79. data/spec/fixtures/views/test_view/test-map.js +3 -0
  80. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  81. data/spec/functional/validations_spec.rb +8 -0
  82. data/spec/spec_helper.rb +60 -0
  83. data/spec/unit/active_model_lint_spec.rb +30 -0
  84. data/spec/unit/assocations_spec.rb +242 -0
  85. data/spec/unit/attachment_spec.rb +176 -0
  86. data/spec/unit/base_spec.rb +537 -0
  87. data/spec/unit/casted_spec.rb +72 -0
  88. data/spec/unit/class_proxy_spec.rb +167 -0
  89. data/spec/unit/collection_spec.rb +86 -0
  90. data/spec/unit/configuration_spec.rb +77 -0
  91. data/spec/unit/connection_spec.rb +148 -0
  92. data/spec/unit/core_extensions/time_parsing.rb +77 -0
  93. data/spec/unit/design_doc_spec.rb +241 -0
  94. data/spec/unit/designs/view_spec.rb +831 -0
  95. data/spec/unit/designs_spec.rb +134 -0
  96. data/spec/unit/dirty_spec.rb +436 -0
  97. data/spec/unit/embeddable_spec.rb +498 -0
  98. data/spec/unit/inherited_spec.rb +33 -0
  99. data/spec/unit/persistence_spec.rb +481 -0
  100. data/spec/unit/property_protection_spec.rb +192 -0
  101. data/spec/unit/property_spec.rb +481 -0
  102. data/spec/unit/proxyable_spec.rb +376 -0
  103. data/spec/unit/subclass_spec.rb +85 -0
  104. data/spec/unit/typecast_spec.rb +521 -0
  105. data/spec/unit/validations_spec.rb +140 -0
  106. data/spec/unit/view_spec.rb +367 -0
  107. metadata +301 -0
@@ -0,0 +1,13 @@
1
+ #
2
+ # Extend CouchRest's normal database delete! method to ensure any caches are
3
+ # also emptied. Given that this is a rare event, and the consequences are not
4
+ # very severe, we just completely empty the cache.
5
+ #
6
+ CouchRest::Database.class_eval do
7
+
8
+ def delete!
9
+ Thread.current[:couchrest_design_cache] = { }
10
+ CouchRest.delete @root
11
+ end
12
+
13
+ end
@@ -0,0 +1,33 @@
1
+
2
+ CouchRest::Design.class_eval do
3
+
4
+ # Calculate and update the checksum of the Design document.
5
+ # Used for ensuring the latest version has been sent to the database.
6
+ #
7
+ # This will generate an flatterned, ordered array of all the elements of the
8
+ # design document, convert to string then generate an MD5 Hash. This should
9
+ # result in a consisitent Hash accross all platforms.
10
+ #
11
+ def checksum!
12
+ # create a copy of basic elements
13
+ base = self.dup
14
+ base.delete('_id')
15
+ base.delete('_rev')
16
+ base.delete('couchrest-hash')
17
+ result = nil
18
+ flatten =
19
+ lambda {|r|
20
+ (recurse = lambda {|v|
21
+ if v.is_a?(Hash) || v.is_a?(CouchRest::Document)
22
+ v.to_a.map{|v| recurse.call(v)}.flatten
23
+ elsif v.is_a?(Array)
24
+ v.flatten.map{|v| recurse.call(v)}
25
+ else
26
+ v.to_s
27
+ end
28
+ }).call(r)
29
+ }
30
+ self['couchrest-hash'] = Digest::MD5.hexdigest(flatten.call(base).sort.join(''))
31
+ end
32
+
33
+ end
@@ -0,0 +1,154 @@
1
+ module CouchRest
2
+ module Model
3
+ module Typecast
4
+
5
+ def typecast_value(value, property) # klass, init_method)
6
+ return nil if value.nil?
7
+ klass = property.type_class
8
+ if value.instance_of?(klass) || klass == Object
9
+ if klass == Time && !value.utc?
10
+ value.utc # Ensure Time is always in UTC
11
+ else
12
+ value
13
+ end
14
+ elsif [String, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class].include?(klass)
15
+ send('typecast_to_'+klass.to_s.downcase, value)
16
+ else
17
+ property.build(value)
18
+ end
19
+ end
20
+
21
+ protected
22
+
23
+ # Typecast a value to an Integer
24
+ def typecast_to_integer(value)
25
+ typecast_to_numeric(value, :to_i)
26
+ end
27
+
28
+ # Typecast a value to a String
29
+ def typecast_to_string(value)
30
+ value.to_s
31
+ end
32
+
33
+ # Typecast a value to a true or false
34
+ def typecast_to_trueclass(value)
35
+ if value.kind_of?(Integer)
36
+ return true if value == 1
37
+ return false if value == 0
38
+ elsif value.respond_to?(:to_s)
39
+ return true if %w[ true 1 t ].include?(value.to_s.downcase)
40
+ return false if %w[ false 0 f ].include?(value.to_s.downcase)
41
+ end
42
+ value
43
+ end
44
+
45
+ # Typecast a value to a BigDecimal
46
+ def typecast_to_bigdecimal(value)
47
+ if value.kind_of?(Integer)
48
+ value.to_s.to_d
49
+ else
50
+ typecast_to_numeric(value, :to_d)
51
+ end
52
+ end
53
+
54
+ # Typecast a value to a Float
55
+ def typecast_to_float(value)
56
+ typecast_to_numeric(value, :to_f)
57
+ end
58
+
59
+ # Match numeric string
60
+ def typecast_to_numeric(value, method)
61
+ if value.respond_to?(:to_str)
62
+ if value.strip.gsub(/,/, '.').gsub(/\.(?!\d*\Z)/, '').to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
63
+ $1.send(method)
64
+ else
65
+ value
66
+ end
67
+ elsif value.respond_to?(method)
68
+ value.send(method)
69
+ else
70
+ value
71
+ end
72
+ end
73
+
74
+ # Typecasts an arbitrary value to a DateTime.
75
+ # Handles both Hashes and DateTime instances.
76
+ # This is slow!! Use Time instead.
77
+ def typecast_to_datetime(value)
78
+ if value.is_a?(Hash)
79
+ typecast_hash_to_datetime(value)
80
+ else
81
+ DateTime.parse(value.to_s)
82
+ end
83
+ rescue ArgumentError
84
+ value
85
+ end
86
+
87
+ # Typecasts an arbitrary value to a Date
88
+ # Handles both Hashes and Date instances.
89
+ def typecast_to_date(value)
90
+ if value.is_a?(Hash)
91
+ typecast_hash_to_date(value)
92
+ elsif value.is_a?(Time) # sometimes people think date is time!
93
+ value.to_date
94
+ elsif value.to_s =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})/
95
+ # Faster than parsing the date
96
+ Date.new($1.to_i, $2.to_i, $3.to_i)
97
+ else
98
+ Date.parse(value)
99
+ end
100
+ rescue ArgumentError
101
+ value
102
+ end
103
+
104
+ # Typecasts an arbitrary value to a Time
105
+ # Handles both Hashes and Time instances.
106
+ def typecast_to_time(value)
107
+ if value.is_a?(Hash)
108
+ typecast_hash_to_time(value)
109
+ else
110
+ Time.parse_iso8601(value.to_s)
111
+ end
112
+ rescue ArgumentError
113
+ value
114
+ rescue TypeError
115
+ value
116
+ end
117
+
118
+ # Creates a DateTime instance from a Hash with keys :year, :month, :day,
119
+ # :hour, :min, :sec
120
+ def typecast_hash_to_datetime(value)
121
+ DateTime.new(*extract_time(value))
122
+ end
123
+
124
+ # Creates a Date instance from a Hash with keys :year, :month, :day
125
+ def typecast_hash_to_date(value)
126
+ Date.new(*extract_time(value)[0, 3])
127
+ end
128
+
129
+ # Creates a Time instance from a Hash with keys :year, :month, :day,
130
+ # :hour, :min, :sec
131
+ def typecast_hash_to_time(value)
132
+ Time.utc(*extract_time(value))
133
+ end
134
+
135
+ # Extracts the given args from the hash. If a value does not exist, it
136
+ # uses the value of Time.now.
137
+ def extract_time(value)
138
+ now = Time.now
139
+ [:year, :month, :day, :hour, :min, :sec].map do |segment|
140
+ typecast_to_numeric(value.fetch(segment, now.send(segment)), :to_i)
141
+ end
142
+ end
143
+
144
+ # Typecast a value to a Class
145
+ def typecast_to_class(value)
146
+ value.to_s.constantize
147
+ rescue NameError
148
+ value
149
+ end
150
+
151
+ end
152
+ end
153
+ end
154
+
@@ -0,0 +1,80 @@
1
+ # encoding: utf-8
2
+
3
+ require "couchrest/model/validations/casted_model"
4
+ require "couchrest/model/validations/uniqueness"
5
+
6
+ I18n.load_path << File.join(
7
+ File.dirname(__FILE__), "validations", "locale", "en.yml"
8
+ )
9
+
10
+ module CouchRest
11
+ module Model
12
+
13
+ # Validations may be applied to both Model::Base and Model::CastedModel
14
+ module Validations
15
+ extend ActiveSupport::Concern
16
+ include ActiveModel::Validations
17
+
18
+ # Determine if the document is valid.
19
+ #
20
+ # @example Is the document valid?
21
+ # person.valid?
22
+ #
23
+ # @example Is the document valid in a context?
24
+ # person.valid?(:create)
25
+ #
26
+ # @param [ Symbol ] context The optional validation context.
27
+ #
28
+ # @return [ true, false ] True if valid, false if not.
29
+ #
30
+ def valid?(context = nil)
31
+ super context ? context : (new? ? :create : :update)
32
+ end
33
+
34
+ module ClassMethods
35
+
36
+ # Validates the associated casted model. This method should not be
37
+ # used within your code as it is automatically included when a CastedModel
38
+ # is used inside the model.
39
+ def validates_casted_model(*args)
40
+ validates_with(CastedModelValidator, _merge_attributes(args))
41
+ end
42
+
43
+ # Validates if the field is unique for this type of document. Automatically creates
44
+ # a view if one does not already exist and performs a search for all matching
45
+ # documents.
46
+ #
47
+ # Example:
48
+ #
49
+ # class Person < CouchRest::Model::Base
50
+ # property :title, String
51
+ #
52
+ # validates_uniqueness_of :title
53
+ # end
54
+ #
55
+ # Asside from the standard options, you can specify the name of the view you'd like
56
+ # to use for the search inside the +:view+ option. The following example would search
57
+ # for the code in side the +all+ view, useful for when +unique_id+ is used and you'd
58
+ # like to check before receiving a RestClient Conflict error:
59
+ #
60
+ # validates_uniqueness_of :code, :view => 'all'
61
+ #
62
+ # A +:proxy+ parameter is also accepted if you would
63
+ # like to call a method on the document on which the view should be performed.
64
+ #
65
+ # For Example:
66
+ #
67
+ # # Same as not including proxy:
68
+ # validates_uniqueness_of :title, :proxy => 'class'
69
+ #
70
+ # # Person#company.people provides a proxy object for people
71
+ # validates_uniqueness_of :title, :proxy => 'company.people'
72
+ #
73
+ def validates_uniqueness_of(*args)
74
+ validates_with(UniquenessValidator, _merge_attributes(args))
75
+ end
76
+ end
77
+
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,16 @@
1
+ module CouchRest
2
+ module Model
3
+ module Validations
4
+ class CastedModelValidator < ActiveModel::EachValidator
5
+
6
+ def validate_each(document, attribute, value)
7
+ values = value.is_a?(Array) ? value : [value]
8
+ return if values.collect {|doc| doc.nil? || doc.valid? }.all?
9
+ error_options = { :value => value }
10
+ error_options[:message] = options[:message] if options[:message]
11
+ document.errors.add(attribute, :invalid, error_options)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ en:
2
+ errors:
3
+ messages:
4
+ taken: "has already been taken"
5
+
@@ -0,0 +1,69 @@
1
+ # encoding: utf-8
2
+
3
+ module CouchRest
4
+ module Model
5
+ module Validations
6
+
7
+ # Validates if a field is unique
8
+ class UniquenessValidator < ActiveModel::EachValidator
9
+
10
+ # Ensure we have a class available so we can check for a usable view
11
+ # or add one if necessary.
12
+ def setup(model)
13
+ @model = model
14
+ if options[:view].blank?
15
+ attributes.each do |attribute|
16
+ opts = merge_view_options(attribute)
17
+
18
+ if model.respond_to?(:has_view?) && !model.has_view?(opts[:view_name])
19
+ opts[:keys] << {:allow_nil => true}
20
+ model.view_by(*opts[:keys])
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ def validate_each(document, attribute, value)
27
+ opts = merge_view_options(attribute)
28
+
29
+ values = opts[:keys].map{|k| document.send(k)}
30
+ values = values.first if values.length == 1
31
+
32
+ model = (document.respond_to?(:model_proxy) && document.model_proxy ? document.model_proxy : @model)
33
+ # Determine the base of the search
34
+ base = opts[:proxy].nil? ? model : document.instance_eval(opts[:proxy])
35
+
36
+ if base.respond_to?(:has_view?) && !base.has_view?(opts[:view_name])
37
+ raise "View #{document.class.name}.#{opts[:view_name]} does not exist for validation!"
38
+ end
39
+
40
+ rows = base.view(opts[:view_name], :key => values, :limit => 2, :include_docs => false)['rows']
41
+ return if rows.empty?
42
+
43
+ unless document.new?
44
+ return if rows.find{|row| row['id'] == document.id}
45
+ end
46
+
47
+ if rows.length > 0
48
+ opts = options.merge(:value => value)
49
+ opts.delete(:scope) # Has meaning with I18n!
50
+ document.errors.add(attribute, :taken, opts)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def merge_view_options(attr)
57
+ keys = [attr]
58
+ keys.unshift(*options[:scope]) unless options[:scope].nil?
59
+
60
+ view_name = options[:view].nil? ? "by_#{keys.join('_and_')}" : options[:view]
61
+
62
+ options.merge({:keys => keys, :view_name => view_name})
63
+ end
64
+
65
+ end
66
+
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,151 @@
1
+ module CouchRest
2
+ module Model
3
+ module Views
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ # Define a CouchDB view. The name of the view will be the concatenation
8
+ # of <tt>by</tt> and the keys joined by <tt>_and_</tt>
9
+ #
10
+ # ==== Example views:
11
+ #
12
+ # class Post
13
+ # # view with default options
14
+ # # query with Post.by_date
15
+ # view_by :date, :descending => true
16
+ #
17
+ # # view with compound sort-keys
18
+ # # query with Post.by_user_id_and_date
19
+ # view_by :user_id, :date
20
+ #
21
+ # # view with custom map/reduce functions
22
+ # # query with Post.by_tags :reduce => true
23
+ # view_by :tags,
24
+ # :map =>
25
+ # "function(doc) {
26
+ # if (doc['model'] == 'Post' && doc.tags) {
27
+ # doc.tags.forEach(function(tag){
28
+ # emit(doc.tag, 1);
29
+ # });
30
+ # }
31
+ # }",
32
+ # :reduce =>
33
+ # "function(keys, values, rereduce) {
34
+ # return sum(values);
35
+ # }"
36
+ # end
37
+ #
38
+ # <tt>view_by :date</tt> will create a view defined by this Javascript
39
+ # function:
40
+ #
41
+ # function(doc) {
42
+ # if (doc['model'] == 'Post' && doc.date) {
43
+ # emit(doc.date, null);
44
+ # }
45
+ # }
46
+ #
47
+ # It can be queried by calling <tt>Post.by_date</tt> which accepts all
48
+ # valid options for CouchRest::Database#view. In addition, calling with
49
+ # the <tt>:raw => true</tt> option will return the view rows
50
+ # themselves. By default <tt>Post.by_date</tt> will return the
51
+ # documents included in the generated view.
52
+ #
53
+ # Calling with :database => [instance of CouchRest::Database] will
54
+ # send the query to a specific database, otherwise it will go to
55
+ # the model's default database (use_database)
56
+ #
57
+ # CouchRest::Database#view options can be applied at view definition
58
+ # time as defaults, and they will be curried and used at view query
59
+ # time. Or they can be overridden at query time.
60
+ #
61
+ # Custom views can be queried with <tt>:reduce => true</tt> to return
62
+ # reduce results. The default for custom views is to query with
63
+ # <tt>:reduce => false</tt>.
64
+ #
65
+ # Views are generated (on a per-model basis) lazily on first-access.
66
+ # This means that if you are deploying changes to a view, the views for
67
+ # that model won't be available until generation is complete. This can
68
+ # take some time with large databases. Strategies are in the works.
69
+ #
70
+ # To understand the capabilities of this view system more completely,
71
+ # it is recommended that you read the RSpec file at
72
+ # <tt>spec/couchrest/more/extended_doc_spec.rb</tt>.
73
+
74
+ def view_by(*keys)
75
+ return unless auto_update_design_doc
76
+
77
+ opts = keys.pop if keys.last.is_a?(Hash)
78
+ opts ||= {}
79
+ ducktype = opts.delete(:ducktype)
80
+
81
+ unless ducktype || opts[:map]
82
+ opts[:guards] ||= []
83
+ opts[:guards].push "(doc['#{model_type_key}'] == '#{self.to_s}')"
84
+ end
85
+ keys.push opts
86
+ design_doc.view_by(*keys)
87
+ end
88
+
89
+ # returns stored defaults if there is a view named this in the design doc
90
+ def has_view?(name)
91
+ design_doc && design_doc.has_view?(name)
92
+ end
93
+
94
+ # Check if the view can be reduced by checking to see if it has a
95
+ # reduce function.
96
+ def can_reduce_view?(name)
97
+ design_doc && design_doc.can_reduce_view?(name)
98
+ end
99
+
100
+ # Dispatches to any named view.
101
+ def view(name, query={}, &block)
102
+ query = query.dup # Modifications made on copy!
103
+ db = query.delete(:database) || database
104
+ query[:raw] = true if query[:reduce]
105
+ raw = query.delete(:raw)
106
+ save_design_doc(db)
107
+ fetch_view_with_docs(db, name, query, raw, &block)
108
+ end
109
+
110
+ # Find the first entry in the view. If the second parameter is a string
111
+ # it will be used as the key for the request, for example:
112
+ #
113
+ # Course.first_from_view('by_teacher', 'Fred')
114
+ #
115
+ # More advanced requests can be performed by providing a hash:
116
+ #
117
+ # Course.first_from_view('by_teacher', :startkey => 'bbb', :endkey => 'eee')
118
+ #
119
+ def first_from_view(name, *args)
120
+ query = {:limit => 1}
121
+ case args.first
122
+ when String, Array
123
+ query.update(args[1]) unless args[1].nil?
124
+ query[:key] = args.first
125
+ when Hash
126
+ query.update(args.first)
127
+ end
128
+ view(name, query).first
129
+ end
130
+
131
+ private
132
+
133
+ def fetch_view_with_docs(db, name, opts, raw=false, &block)
134
+ if raw || (opts.has_key?(:include_docs) && opts[:include_docs] == false)
135
+ fetch_view(db, name, opts, &block)
136
+ else
137
+ opts = opts.merge(:include_docs => true)
138
+ view = fetch_view db, name, opts, &block
139
+ view['rows'].collect{|r| build_from_database(r['doc'])} if view['rows']
140
+ end
141
+ end
142
+
143
+ def fetch_view(db, view_name, opts, &block)
144
+ raise "A view needs a database to operate on (specify :database option, or use_database in the #{self.class} class)" unless db
145
+ design_doc.view_on(db, view_name, opts, &block)
146
+ end
147
+ end # module ClassMethods
148
+
149
+ end
150
+ end
151
+ end