couchbase-model 0.0.1 → 0.1.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.
@@ -1,6 +1,6 @@
1
1
  before_install:
2
2
  - wget -O- http://packages.couchbase.com/ubuntu/couchbase.key | sudo apt-key add -
3
- - echo deb http://packages.couchbase.com/ubuntu lucid lucid/main | sudo tee /etc/apt/sources.list.d/couchbase.list
3
+ - echo deb http://packages.couchbase.com/preview/ubuntu lucid lucid/main | sudo tee /etc/apt/sources.list.d/couchbase.list
4
4
  - sudo apt-get update
5
5
  - sudo apt-get -y install libevent-dev libvbucket-dev libcouchbase-dev
6
6
 
@@ -1,6 +1,41 @@
1
- # Couchbase Model [![Build Status](https://secure.travis-ci.org/avsej/ruby-couchbase-model.png?branch=master)](http://travis-ci.org/avsej/ruby-couchbase-model)
1
+ # Couchbase Model
2
2
 
3
- This library allows to declare models for [couchbase gem][1]. Here are example:
3
+ This library allows to declare models for [couchbase gem][1].
4
+
5
+ ## Rails integration
6
+
7
+ To generate config you can use `rails generate couchbase:config`:
8
+
9
+ $ rails generate couchbase:config
10
+ create config/couchbase.yml
11
+
12
+ It will generate this `config/couchbase.yml` for you:
13
+
14
+ common: &common
15
+ hostname: localhost
16
+ port: 8091
17
+ username:
18
+ password:
19
+ pool: default
20
+
21
+ development:
22
+ <<: *common
23
+ bucket: couchbase_tinyurl_development
24
+
25
+ test:
26
+ <<: *common
27
+ bucket: couchbase_tinyurl_test
28
+
29
+ # set these environment variables on your production server
30
+ production:
31
+ hostname: <%= ENV['COUCHBASE_HOST'] %>
32
+ port: <%= ENV['COUCHBASE_PORT'] %>
33
+ username: <%= ENV['COUCHBASE_USERNAME'] %>
34
+ password: <%= ENV['COUCHBASE_PASSWORD'] %>
35
+ pool: <%= ENV['COUCHBASE_POOL'] %>
36
+ bucket: <%= ENV['COUCHBASE_BUCKET'] %>
37
+
38
+ ## Examples
4
39
 
5
40
  require 'couchbase/model'
6
41
 
@@ -48,4 +83,58 @@ You can define connection options on per model basis:
48
83
  connect :port => 80, :bucket => 'blog'
49
84
  end
50
85
 
86
+ ## Views (aka Map/Reduce queries to Couchbase)
87
+
88
+ Views are stored in models directory in subdirectory named after the
89
+ model (to be precious `design_document` attribute of the model class).
90
+ Here is an example of directory layout for `Link` model with three
91
+ views.
92
+
93
+ .
94
+ └── app
95
+ └── models
96
+ ├── link
97
+ │   ├── total_count
98
+ │   │   ├── map.js
99
+ │   │   └── reduce.js
100
+ │   ├── by_created_at
101
+ │   │   └── map.js
102
+ │   └── by_view_count
103
+ │   └── map.js
104
+ └── link.rb
105
+
106
+ To generate view you can use yet another generator `rails generate
107
+ couchbase:view DESIGNDOCNAME VIEWNAME`. For example how `total_count`
108
+ view could be generated:
109
+
110
+ $ rails generate link total_count
111
+
112
+ The generated files contains useful info and links about how to write
113
+ map and reduce functions, you can take a look at them in the [templates
114
+ directory][2].
115
+
116
+ In the model class you should declare accessible views:
117
+
118
+ class Post < Couchbase::Model
119
+ attribute :title
120
+ attribute :body
121
+ attribute :draft
122
+ attribute :view_count
123
+ attribute :created_at, :default => lambda { Time.now }
124
+
125
+ view :total_count, :by_created_at, :by_view_count
126
+ end
127
+
128
+ And request them later:
129
+
130
+ Post.by_created_at(:include_docs => true).each do |post|
131
+ puts post.title
132
+ end
133
+
134
+ Post.by_view_count(:include_docs => true).group_by(&:view_count) do |count, posts|
135
+ p "#{count} -> #{posts.map{|pp| pp.inspect}.join(', ')}"
136
+ end
137
+
138
+
51
139
  [1]: https://github.com/couchbase/couchbase-ruby-client/
140
+ [2]: https://github.com/couchbaselabs/ruby-couchbase-model/tree/master/lib/rails/generators/couchbase/view/templates/
@@ -5,9 +5,9 @@ require "couchbase/model/version"
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "couchbase-model"
7
7
  s.version = Couchbase::Model::VERSION
8
- s.authors = ["Sergey Avseyev"]
9
- s.email = ["sergey.avseyev@gmail.com"]
10
- s.homepage = ""
8
+ s.author = "Couchbase"
9
+ s.email = "support@couchbase.com"
10
+ s.homepage = "https://github.com/couchbaselabs/ruby-couchbase-model"
11
11
  s.summary = %q{Declarative interface to Couchbase}
12
12
  s.description = %q{ORM-like interface allows you to persist your models to Couchbase}
13
13
 
@@ -16,7 +16,7 @@ Gem::Specification.new do |s|
16
16
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
17
  s.require_paths = ["lib"]
18
18
 
19
- s.add_runtime_dependency 'couchbase', '~> 1.1.0'
19
+ s.add_runtime_dependency 'couchbase', '~> 1.2.0.dp'
20
20
 
21
21
  s.add_development_dependency 'rake', '~> 0.8.7'
22
22
  s.add_development_dependency 'minitest'
@@ -16,3 +16,8 @@
16
16
  #
17
17
 
18
18
  require 'couchbase/model'
19
+
20
+ # If we are using Rails then we will include the Couchbase railtie.
21
+ if defined?(Rails)
22
+ require "couchbase/railtie"
23
+ end
@@ -15,16 +15,22 @@
15
15
  # limitations under the License.
16
16
  #
17
17
 
18
+ require 'digest/md5'
19
+
18
20
  require 'couchbase'
19
21
  require 'couchbase/model/version'
20
22
  require 'couchbase/model/uuid'
23
+ require 'couchbase/model/configuration'
21
24
 
22
25
  module Couchbase
23
26
 
27
+ # @since 0.0.1
24
28
  class Error::MissingId < Error::Base; end
25
29
 
26
30
  # Declarative layer for Couchbase gem
27
31
  #
32
+ # @since 0.0.1
33
+ #
28
34
  # require 'couchbase/model'
29
35
  #
30
36
  # class Post < Couchbase::Model
@@ -72,14 +78,35 @@ module Couchbase
72
78
  # end
73
79
  class Model
74
80
  # Each model must have identifier
81
+ #
82
+ # @since 0.0.1
75
83
  attr_accessor :id
76
84
 
77
- # @private Container for all attributes of all subclasses
78
- @@attributes = ::Hash.new {|hash, key| hash[key] = []}
85
+ # @since 0.1.0
86
+ attr_reader :_key
87
+
88
+ # @since 0.1.0
89
+ attr_reader :_value
90
+
91
+ # @since 0.1.0
92
+ attr_reader :_doc
93
+
94
+ # @since 0.1.0
95
+ attr_reader :_meta
96
+
97
+ # @private Container for all attributes with defaults of all subclasses
98
+ @@attributes = ::Hash.new {|hash, key| hash[key] = {}}
99
+
100
+ # @private Container for all view names of all subclasses
101
+ @@views = ::Hash.new {|hash, key| hash[key] = []}
79
102
 
80
103
  # Use custom connection options
81
104
  #
82
- # @param [String, Hash, Array] options for establishing connection.
105
+ # @since 0.0.1
106
+ #
107
+ # @param [String, Hash, Array] options options for establishing
108
+ # connection.
109
+ # @return [Couchbase::Bucket]
83
110
  #
84
111
  # @see Couchbase::Bucket#initialize
85
112
  #
@@ -92,8 +119,108 @@ module Couchbase
92
119
  self.bucket = Couchbase.connect(*options)
93
120
  end
94
121
 
122
+ # Associate custom design document with the model
123
+ #
124
+ # Design document is the special document which contains views, the
125
+ # chunks of code for building map/reduce indexes. When this method
126
+ # called without argument, it just returns the effective design document
127
+ # name.
128
+ #
129
+ # @since 0.1.0
130
+ #
131
+ # @see http://www.couchbase.com/docs/couchbase-manual-2.0/couchbase-views.html
132
+ #
133
+ # @param [String, Symbol] name the name for the design document. By
134
+ # default underscored model name is used.
135
+ # @return [String] the effective design document
136
+ #
137
+ # @example Choose specific design document name
138
+ # class Post < Couchbase::Model
139
+ # design_document :my_posts
140
+ # ...
141
+ # end
142
+ def self.design_document(name = nil)
143
+ if name
144
+ @_design_doc = name.to_s
145
+ else
146
+ @_design_doc ||= begin
147
+ name = self.name.dup
148
+ name.gsub!(/::/, '_')
149
+ name.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
150
+ name.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
151
+ name.downcase!
152
+ end
153
+ end
154
+ end
155
+
156
+ # Ensure that design document is up to date.
157
+ #
158
+ # @since 0.1.0
159
+ #
160
+ # This method also cares about organizing view in separate javascript
161
+ # files. The general structure is the following (+[root]+ is the
162
+ # directory, one of the {Model::Configuration.design_documents_paths}):
163
+ #
164
+ # [root]
165
+ # |
166
+ # `- link
167
+ # | |
168
+ # | `- by_created_at
169
+ # | | |
170
+ # | | `- map.js
171
+ # | |
172
+ # | `- by_session_id
173
+ # | | |
174
+ # | | `- map.js
175
+ # | |
176
+ # | `- total_views
177
+ # | | |
178
+ # | | `- map.js
179
+ # | | |
180
+ # | | `- reduce.js
181
+ #
182
+ # The directory structure above demonstrate layout for design document
183
+ # with id +_design/link+ and three views: +by_create_at+,
184
+ # +by_session_id` and `total_views`.
185
+ def self.ensure_design_document!
186
+ unless Configuration.design_documents_paths
187
+ raise "Configuration.design_documents_path must be directory"
188
+ end
189
+
190
+ doc = {'_id' => "_design/#{design_document}", 'views' => {}}
191
+ digest = Digest::MD5.new
192
+ mtime = 0
193
+ views.each do |name|
194
+ doc['views'][name] = view = {}
195
+ ['map', 'reduce'].each do |type|
196
+ Configuration.design_documents_paths.each do |path|
197
+ ff = File.join(path, design_document.to_s, name.to_s, "#{type}.js")
198
+ if File.file?(ff)
199
+ view[type] = File.read(ff)
200
+ mtime = [mtime, File.mtime(ff).to_i].max
201
+ digest << view[type]
202
+ break # pick first matching file
203
+ end
204
+ end
205
+ end
206
+ end
207
+ doc['signature'] = digest.to_s
208
+ doc['timestamp'] = mtime
209
+ if doc['signature'] != thread_storage[:signature] && doc['timestamp'] > thread_storage[:timestamp].to_i
210
+ current_doc = bucket.design_docs[design_document.to_s]
211
+ if current_doc.nil? || (current_doc['signature'] != doc['signature'] && doc['timestamp'] > current_doc[:timestamp].to_i)
212
+ bucket.save_design_doc(doc)
213
+ current_doc = doc
214
+ end
215
+ thread_storage[:signature] = current_doc['signature']
216
+ thread_storage[:timestamp] = current_doc['timestamp'].to_i
217
+ end
218
+ end
219
+
95
220
  # Choose the UUID generation algorithms
96
221
  #
222
+ # @since 0.0.1
223
+ #
97
224
  # @param [Symbol] algorithm (:sequential) one of the available
98
225
  # algorithms.
99
226
  #
@@ -104,12 +231,16 @@ module Couchbase
104
231
  # uuid_algorithm :random
105
232
  # ...
106
233
  # end
234
+ #
235
+ # @return [Symbol]
107
236
  def self.uuid_algorithm(algorithm)
108
237
  self.thread_storage[:uuid_algorithm] = algorithm
109
238
  end
110
239
 
111
240
  # Defines an attribute for the model
112
241
  #
242
+ # @since 0.0.1
243
+ #
113
244
  # @param [Symbol, String] name name of the attribute
114
245
  #
115
246
  # @example Define some attributes for a model
@@ -122,49 +253,114 @@ module Couchbase
122
253
  # post = Post.new(:title => 'Hello world',
123
254
  # :body => 'This is the first example...',
124
255
  # :published_at => Time.now)
125
- def self.attribute(name)
126
- define_method(name) do
127
- @_attributes[name]
256
+ def self.attribute(*names)
257
+ options = {}
258
+ if names.last.is_a?(Hash)
259
+ options = names.pop
128
260
  end
129
- define_method(:"#{name}=") do |value|
130
- @_attributes[name] = value
261
+ names.each do |name|
262
+ name = name.to_sym
263
+ define_method(name) do
264
+ @_attributes[name]
265
+ end
266
+ define_method(:"#{name}=") do |value|
267
+ @_attributes[name] = value
268
+ end
269
+ attributes[name] = options[:default]
270
+ end
271
+ end
272
+
273
+ def self.view(*names)
274
+ options = {}
275
+ if names.last.is_a?(Hash)
276
+ options = names.pop
277
+ end
278
+ names.each do |name|
279
+ views << name
280
+ self.instance_eval <<-EOV, __FILE__, __LINE__ + 1
281
+ def #{name}(params = {})
282
+ View.new(bucket, "_design/\#{design_document}/_view/#{name}",
283
+ params.merge(:wrapper_class => self, :include_docs => true))
284
+ end
285
+ EOV
131
286
  end
132
- attributes << name unless attributes.include?(name)
133
287
  end
134
288
 
135
289
  # Find the model using +id+ attribute
136
290
  #
291
+ # @since 0.0.1
292
+ #
137
293
  # @param [String, Symbol] id model identificator
138
294
  # @return [Couchbase::Model] an instance of the model
295
+ # @raise [Couchbase::Error::NotFound] when given key isn't exist
139
296
  #
140
297
  # @example Find model using +id+
141
298
  # post = Post.find('the-id')
142
299
  def self.find(id)
143
- if id && (obj = bucket.get(id))
144
- new({:id => id}.merge(obj))
300
+ if id && (res = bucket.get(id, :quiet => false, :extended => true))
301
+ obj, flags, cas = res
302
+ new({:id => id, :_meta => {'flags' => flags, 'cas' => cas}}.merge(obj))
303
+ end
304
+ end
305
+
306
+ # Find the model using +id+ attribute
307
+ #
308
+ # @since 0.1.0
309
+ #
310
+ # @param [String, Symbol] id model identificator
311
+ # @return [Couchbase::Model, nil] an instance of the model or +nil+ if
312
+ # given key isn't exist
313
+ #
314
+ # @example Find model using +id+
315
+ # post = Post.find_by_id('the-id')
316
+ def self.find_by_id(id)
317
+ if id && (res = bucket.get(id, :quiet => true))
318
+ obj, flags, cas = res
319
+ new({:id => id, :_meta => {'flags' => flags, 'cas' => cas}}.merge(obj))
145
320
  end
146
321
  end
147
322
 
148
323
  # Create the model with given attributes
149
324
  #
325
+ # @since 0.0.1
326
+ #
150
327
  # @param [Hash] args attribute-value pairs for the object
151
328
  # @return [Couchbase::Model] an instance of the model
152
329
  def self.create(*args)
153
330
  new(*args).create
154
331
  end
155
332
 
156
- # Constructor for all subclasses of Couchbase::Model, which optionally
157
- # takes a Hash of attribute value pairs.
333
+ # Constructor for all subclasses of Couchbase::Model
334
+ #
335
+ # @since 0.0.1
336
+ #
337
+ # Optionally takes a Hash of attribute value pairs.
158
338
  #
159
339
  # @param [Hash] attrs attribute-value pairs
160
340
  def initialize(attrs = {})
161
- @id = nil
162
- @_attributes = ::Hash.new
163
- update_attributes(attrs)
341
+ if attrs.respond_to?(:with_indifferent_access)
342
+ attrs = attrs.with_indifferent_access
343
+ end
344
+ @id = attrs.delete(:id)
345
+ @_key = attrs.delete(:_key)
346
+ @_value = attrs.delete(:_value)
347
+ @_doc = attrs.delete(:_doc)
348
+ @_meta = attrs.delete(:_meta)
349
+ @_attributes = ::Hash.new do |h, k|
350
+ default = self.class.attributes[k]
351
+ h[k] = if default.respond_to?(:call)
352
+ default.call
353
+ else
354
+ default
355
+ end
356
+ end
357
+ update_attributes(@_doc || attrs)
164
358
  end
165
359
 
166
360
  # Create this model and assign new id if necessary
167
361
  #
362
+ # @since 0.0.1
363
+ #
168
364
  # @return [Couchbase::Model] newly created object
169
365
  #
170
366
  # @raise [Couchbase::Error::KeyExists] if model with the same +id+
@@ -175,12 +371,14 @@ module Couchbase
175
371
  # p.create
176
372
  def create
177
373
  @id ||= Couchbase::Model::UUID.generator.next(1, model.thread_storage[:uuid_algorithm])
178
- model.bucket.add(@id, attributes)
374
+ model.bucket.add(@id, attributes_with_values)
179
375
  self
180
376
  end
181
377
 
182
378
  # Create or update this object based on the state of #new?.
183
379
  #
380
+ # @since 0.0.1
381
+ #
184
382
  # @return [Couchbase::Model] The saved object
185
383
  #
186
384
  # @example Update the Post model
@@ -189,12 +387,14 @@ module Couchbase
189
387
  # p.save
190
388
  def save
191
389
  return create if new?
192
- model.bucket.set(@id, attributes)
390
+ model.bucket.set(@id, attributes_with_values)
193
391
  self
194
392
  end
195
393
 
196
394
  # Update this object, optionally accepting new attributes.
197
395
  #
396
+ # @since 0.0.1
397
+ #
198
398
  # @param [Hash] attrs Attribute value pairs to use for the updated
199
399
  # version
200
400
  # @return [Couchbase::Model] The updated object
@@ -205,6 +405,8 @@ module Couchbase
205
405
 
206
406
  # Delete this object from the bucket
207
407
  #
408
+ # @since 0.0.1
409
+ #
208
410
  # @note This method will reset +id+ attribute
209
411
  #
210
412
  # @return [Couchbase::Model] Returns a reference of itself.
@@ -221,6 +423,8 @@ module Couchbase
221
423
 
222
424
  # Check if the record have +id+ attribute
223
425
  #
426
+ # @since 0.0.1
427
+ #
224
428
  # @return [true, false] Whether or not this object has an id.
225
429
  #
226
430
  # @note +true+ doesn't mean that record exists in the database
@@ -232,6 +436,8 @@ module Couchbase
232
436
 
233
437
  # Check if the key exists in the bucket
234
438
  #
439
+ # @since 0.0.1
440
+ #
235
441
  # @param [String, Symbol] id the record identifier
236
442
  # @return [true, false] Whether or not the object with given +id+
237
443
  # presented in the bucket.
@@ -241,21 +447,40 @@ module Couchbase
241
447
 
242
448
  # Check if this model exists in the bucket.
243
449
  #
450
+ # @since 0.0.1
451
+ #
244
452
  # @return [true, false] Whether or not this object presented in the
245
453
  # bucket.
246
454
  def exists?
247
455
  model.exists?(@id)
248
456
  end
249
457
 
250
- # All the defined attributes within a class.
458
+ # All defined attributes within a class.
459
+ #
460
+ # @since 0.0.1
251
461
  #
252
462
  # @see Model.attribute
463
+ #
464
+ # @return [Hash]
253
465
  def self.attributes
254
466
  @@attributes[self]
255
467
  end
256
468
 
469
+ # All defined views within a class.
470
+ #
471
+ # @since 0.1.0
472
+ #
473
+ # @see Model.view
474
+ #
475
+ # @return [Array]
476
+ def self.views
477
+ @@views[self]
478
+ end
479
+
257
480
  # All the attributes of the current instance
258
481
  #
482
+ # @since 0.0.1
483
+ #
259
484
  # @return [Hash]
260
485
  def attributes
261
486
  @_attributes
@@ -263,18 +488,23 @@ module Couchbase
263
488
 
264
489
  # Update all attributes without persisting the changes.
265
490
  #
491
+ # @since 0.0.1
492
+ #
266
493
  # @param [Hash] attrs attribute-value pairs.
267
494
  def update_attributes(attrs)
268
495
  if id = attrs.delete(:id)
269
496
  @id = id
270
497
  end
271
498
  attrs.each do |key, value|
272
- send(:"#{key}=", value)
499
+ setter = :"#{key}="
500
+ send(setter, value) if respond_to?(setter)
273
501
  end
274
502
  end
275
503
 
276
504
  # Reload all the model attributes from the bucket
277
505
  #
506
+ # @since 0.0.1
507
+ #
278
508
  # @return [Model] the latest model state
279
509
  #
280
510
  # @raise [Error::MissingId] for records without +id+
@@ -287,46 +517,99 @@ module Couchbase
287
517
  end
288
518
 
289
519
  # @private The thread local storage for model specific stuff
520
+ #
521
+ # @since 0.0.1
290
522
  def self.thread_storage
291
523
  Couchbase.thread_storage[self] ||= {:uuid_algorithm => :sequential}
292
524
  end
293
525
 
294
526
  # @private Fetch the current connection
527
+ #
528
+ # @since 0.0.1
295
529
  def self.bucket
296
530
  self.thread_storage[:bucket] ||= Couchbase.bucket
297
531
  end
298
532
 
299
533
  # @private Set the current connection
300
534
  #
535
+ # @since 0.0.1
536
+ #
301
537
  # @param [Bucket] connection the connection instance
302
538
  def self.bucket=(connection)
303
539
  self.thread_storage[:bucket] = connection
304
540
  end
305
541
 
306
542
  # @private Get model class
543
+ #
544
+ # @since 0.0.1
307
545
  def model
308
546
  self.class
309
547
  end
310
548
 
311
- # @private Wrap the hash to the model class
312
- #
313
- # @param [Model, Hash] the Couchbase::Model subclass or the
314
- # attribute-value pairs
315
- def self.wrap(object)
316
- object.class == self ? object : new(object)
549
+ # @private Wrap the hash to the model class.
550
+ #
551
+ # @since 0.0.1
552
+ #
553
+ # @param [Bucket] bucket the reference to Bucket instance
554
+ # @param [Hash] data the Hash fetched by View, it should have at least
555
+ # +"id"+, +"key"+ and +"value"+ keys, also it could have optional
556
+ # +"doc"+ key.
557
+ #
558
+ # @return [Model]
559
+ def self.wrap(bucket, data)
560
+ doc = {
561
+ :_key => data['key'],
562
+ :_value => data['value'],
563
+ :_meta => {},
564
+ :id => data['id']
565
+ }
566
+ if doc[:_value].is_a?(Hash) && (_id = doc[:_value]['_id'])
567
+ doc[:id] = _id
568
+ end
569
+ if data['doc']
570
+ data['doc'].keys.each do |key|
571
+ if key.start_with?("$")
572
+ doc[:_meta][key.sub(/^\$/, '')] = data['doc'].delete(key)
573
+ end
574
+ end
575
+ doc.update(data['doc'])
576
+ end
577
+ new(doc)
317
578
  end
318
579
 
319
580
  # @private Returns a string containing a human-readable representation
320
581
  # of the record.
582
+ #
583
+ # @since 0.0.1
321
584
  def inspect
322
- attrs = model.attributes.sort.map do |attr|
323
- [attr, @_attributes[attr].inspect]
324
- end
585
+ attrs = model.attributes.map do |attr, default|
586
+ [attr.to_s, @_attributes[attr].inspect]
587
+ end.sort
325
588
  sprintf("#<%s:%s %s>",
326
589
  model, new? ? "?" : id,
327
- attrs.map{|a| a.join("=")}.join(" "))
590
+ attrs.map{|a| a.join("=")}.join(", "))
591
+ end
592
+
593
+ def self.inspect
594
+ buf = "#{name}"
595
+ if self != Couchbase::Model
596
+ buf << "(#{['id', attributes.map(&:first)].flatten.join(', ')})"
597
+ end
598
+ buf
328
599
  end
329
600
 
601
+ protected
602
+
603
+ # @private Returns a hash with model attributes
604
+ #
605
+ # @since 0.1.0
606
+ def attributes_with_values
607
+ ret = {:type => model.design_document}
608
+ model.attributes.keys.each do |attr|
609
+ ret[attr] = @_attributes[attr]
610
+ end
611
+ ret
612
+ end
330
613
  end
331
614
 
332
615
  end