couchbase-model 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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