openlogic-couchrest_model 1.0.0

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