moped 0.0.0.beta → 1.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of moped might be problematic. Click here for more details.

Files changed (50) hide show
  1. data/MIT_LICENSE +19 -0
  2. data/README.md +323 -0
  3. data/lib/moped.rb +19 -0
  4. data/lib/moped/bson.rb +25 -0
  5. data/lib/moped/bson/binary.rb +68 -0
  6. data/lib/moped/bson/code.rb +61 -0
  7. data/lib/moped/bson/document.rb +16 -0
  8. data/lib/moped/bson/extensions.rb +81 -0
  9. data/lib/moped/bson/extensions/array.rb +44 -0
  10. data/lib/moped/bson/extensions/boolean.rb +14 -0
  11. data/lib/moped/bson/extensions/false_class.rb +15 -0
  12. data/lib/moped/bson/extensions/float.rb +23 -0
  13. data/lib/moped/bson/extensions/hash.rb +49 -0
  14. data/lib/moped/bson/extensions/integer.rb +37 -0
  15. data/lib/moped/bson/extensions/nil_class.rb +20 -0
  16. data/lib/moped/bson/extensions/regexp.rb +40 -0
  17. data/lib/moped/bson/extensions/string.rb +35 -0
  18. data/lib/moped/bson/extensions/symbol.rb +25 -0
  19. data/lib/moped/bson/extensions/time.rb +21 -0
  20. data/lib/moped/bson/extensions/true_class.rb +15 -0
  21. data/lib/moped/bson/max_key.rb +21 -0
  22. data/lib/moped/bson/min_key.rb +21 -0
  23. data/lib/moped/bson/object_id.rb +123 -0
  24. data/lib/moped/bson/timestamp.rb +15 -0
  25. data/lib/moped/bson/types.rb +67 -0
  26. data/lib/moped/cluster.rb +193 -0
  27. data/lib/moped/collection.rb +67 -0
  28. data/lib/moped/cursor.rb +60 -0
  29. data/lib/moped/database.rb +76 -0
  30. data/lib/moped/errors.rb +61 -0
  31. data/lib/moped/indexes.rb +93 -0
  32. data/lib/moped/logging.rb +25 -0
  33. data/lib/moped/protocol.rb +20 -0
  34. data/lib/moped/protocol/command.rb +27 -0
  35. data/lib/moped/protocol/commands.rb +11 -0
  36. data/lib/moped/protocol/commands/authenticate.rb +54 -0
  37. data/lib/moped/protocol/delete.rb +92 -0
  38. data/lib/moped/protocol/get_more.rb +79 -0
  39. data/lib/moped/protocol/insert.rb +92 -0
  40. data/lib/moped/protocol/kill_cursors.rb +61 -0
  41. data/lib/moped/protocol/message.rb +320 -0
  42. data/lib/moped/protocol/query.rb +131 -0
  43. data/lib/moped/protocol/reply.rb +90 -0
  44. data/lib/moped/protocol/update.rb +107 -0
  45. data/lib/moped/query.rb +230 -0
  46. data/lib/moped/server.rb +73 -0
  47. data/lib/moped/session.rb +253 -0
  48. data/lib/moped/socket.rb +201 -0
  49. data/lib/moped/version.rb +4 -0
  50. metadata +108 -46
@@ -0,0 +1,54 @@
1
+ module Moped
2
+ module Protocol
3
+ module Commands
4
+
5
+ # Implementation of the authentication command for Mongo. See:
6
+ # http://www.mongodb.org/display/DOCS/Implementing+Authentication+in+a+Driver
7
+ # for details.
8
+ #
9
+ # @example
10
+ # socket.write Command.new :admin, getnonce: 1
11
+ # reply = Reply.deserialize socket
12
+ # socket.write Authenticate.new :admin, "username", "password",
13
+ # reply.documents[0]["nonce"]
14
+ # Reply.deserialize(socket).documents[0]["ok"] # => 1.0
15
+ class Authenticate < Command
16
+
17
+ # Create a new authentication command.
18
+ #
19
+ # @param [String] database the database to authenticate against
20
+ # @param [String] username
21
+ # @param [String] password
22
+ # @param [String] nonce the nonce returned from running the getnonce
23
+ # command.
24
+ def initialize(database, username, password, nonce)
25
+ super database, build_auth_command(username, password, nonce)
26
+ end
27
+
28
+ # @param [String] username
29
+ # @param [String] password
30
+ # @param [String] nonce
31
+ # @return [String] the mongo digest of the username, password, and
32
+ # nonce.
33
+ def digest(username, password, nonce)
34
+ Digest::MD5.hexdigest(
35
+ nonce + username + Digest::MD5.hexdigest(username + ":mongo:" + password)
36
+ )
37
+ end
38
+
39
+ # @param [String] username
40
+ # @param [String] password
41
+ # @param [String] nonce
42
+ def build_auth_command(username, password, nonce)
43
+ {
44
+ authenticate: 1,
45
+ user: username,
46
+ nonce: nonce,
47
+ key: digest(username, password, nonce)
48
+ }
49
+ end
50
+
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,92 @@
1
+ module Moped
2
+ module Protocol
3
+
4
+ # The Protocol class for deleting documents from a collection.
5
+ #
6
+ # @example Delete all people named John
7
+ # delete = Delete.new "moped", "people", { name: "John" }
8
+ #
9
+ # @example Delete the first person named John
10
+ # delete = Delete.new "moped", "people", { name: "John" },
11
+ # flags: [:remove_first]
12
+ #
13
+ # @example Setting the request id
14
+ # delete = Delete.new "moped", "people", { name: "John" },
15
+ # request_id: 123
16
+ class Delete
17
+ include Message
18
+
19
+ # @attribute
20
+ # @return [Number] the length of the message
21
+ int32 :length
22
+
23
+ # @attribute
24
+ # @return [Number] the request id of the message
25
+ int32 :request_id
26
+
27
+ int32 :response_to
28
+
29
+ # @attribute
30
+ # @return [Number] the operation code of this message
31
+ int32 :op_code
32
+
33
+ int32 :reserved # reserved for future use
34
+
35
+ # @attribute
36
+ # @return [String] the full collection name
37
+ cstring :full_collection_name
38
+
39
+ # @attribute
40
+ # @param [Array] the flags for the message
41
+ # @return [Array] the flags for the message
42
+ flags :flags, remove_first: 2 ** 0
43
+
44
+ # @attribute
45
+ # @return [Hash] the query to use when deleting documents
46
+ document :selector
47
+
48
+ # @return [String, Symbol] the database to delete from
49
+ attr_reader :database
50
+
51
+ # @return [String, Symbol] the collection to delete from
52
+ attr_reader :collection
53
+
54
+ # Create a new delete command. The +database+ and +collection+ arguments
55
+ # are joined together to set the +full_collection_name+.
56
+ #
57
+ # @example
58
+ # Delete.new "moped", "users", { condition: true },
59
+ # flags: [:remove_first],
60
+ # request_id: 123
61
+ #
62
+ # @param [String, Symbol] database the database to delete from
63
+ # @param [String, Symbol] collection the collection to delete from
64
+ # @param [Hash] selector the selector for which documents to delete
65
+ # @param [Hash] options additional options
66
+ # @option options [Number] :request_id the command's request id
67
+ # @option options [Array] :flags the flags for insertion. Supported
68
+ # flags: +:remove_first+
69
+ def initialize(database, collection, selector, options = {})
70
+ @database = database
71
+ @collection = collection
72
+
73
+ @full_collection_name = "#{database}.#{collection}"
74
+ @selector = selector
75
+ @request_id = options[:request_id]
76
+ @flags = options[:flags]
77
+ end
78
+
79
+ # @return [Number] OP_DELETE operation code (2006)
80
+ def op_code
81
+ 2006
82
+ end
83
+
84
+ def log_inspect
85
+ type = "DELETE"
86
+
87
+ "%-12s database=%s collection=%s selector=%s flags=%s" % [type, database, collection, selector.inspect, flags.inspect]
88
+ end
89
+
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,79 @@
1
+ module Moped
2
+ module Protocol
3
+
4
+ # The Protocol class for retrieving more documents from a cursor.
5
+ #
6
+ # @example Get more results using database default limit
7
+ # insert = GetMore.new "moped", "people", 29301021, 0
8
+ #
9
+ # @example Get more results using custom limit
10
+ # insert = Insert.new "moped", "people", 29301021, 10
11
+ #
12
+ # @example Setting the request id
13
+ # insert = Insert.new "moped", "people", 29301021, 10,
14
+ # request_id: 123
15
+ class GetMore
16
+ include Message
17
+
18
+ # @attribute
19
+ # @return [Number] the length of the message
20
+ int32 :length
21
+
22
+ # @attribute
23
+ # @return [Number] the request id of the message
24
+ int32 :request_id
25
+
26
+ int32 :response_to
27
+
28
+ # @attribute
29
+ # @return [Number] the operation code of this message
30
+ int32 :op_code
31
+
32
+ int32 :reserved # reserved for future use
33
+
34
+ # @attribute
35
+ # @return [String] the namespaced collection name
36
+ cstring :full_collection_name
37
+
38
+ # @attribute
39
+ # @return [Number] the number of documents to return
40
+ int32 :limit
41
+
42
+ # @attribute
43
+ # @return [Number] the id of the cursor to get more documents from
44
+ int64 :cursor_id
45
+
46
+ # @return [Number] OP_GETMORE operation code (2005)
47
+ def op_code
48
+ 2005
49
+ end
50
+
51
+ # @return [String, Symbol] the database this insert targets
52
+ attr_reader :database
53
+
54
+ # @return [String, Symbol] the collection this insert targets
55
+ attr_reader :collection
56
+
57
+ # Create a new +GetMore+ command. The +database+ and +collection+ arguments
58
+ # are joined together to set the +full_collection_name+.
59
+ #
60
+ # @example
61
+ # GetMore.new "moped", "users", 29301021, 10, request_id: 123
62
+ def initialize(database, collection, cursor_id, limit, options = {})
63
+ @database = database
64
+ @collection = collection
65
+
66
+ @full_collection_name = "#{database}.#{collection}"
67
+ @cursor_id = cursor_id
68
+ @limit = limit
69
+ @request_id = options[:request_id]
70
+ end
71
+
72
+ def log_inspect
73
+ type = "GET_MORE"
74
+
75
+ "%-12s database=%s collection=%s limit=%s cursor_id=%s" % [type, database, collection, limit, cursor_id]
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,92 @@
1
+ module Moped
2
+ module Protocol
3
+
4
+ # The Protocol class for inserting documents into a collection.
5
+ #
6
+ # @example
7
+ # insert = Insert.new "moped", "people", [{ name: "John" }]
8
+ #
9
+ # @example Continuing after an error on batch insert
10
+ # insert = Insert.new "moped", "people",
11
+ # [{ unique_field: 1 }, { unique_field: 1 }, { unique_field: 2 }],
12
+ # flags: [:continue_on_error]
13
+ #
14
+ # @example Setting the request id
15
+ # insert = Insert.new "moped", "people", [{ name: "John" }],
16
+ # request_id: 123
17
+ class Insert
18
+ include Message
19
+
20
+ # @attribute
21
+ # @return [Number] the length of the message
22
+ int32 :length
23
+
24
+ # @attribute
25
+ # @return [Number] the request id of the message
26
+ int32 :request_id
27
+
28
+ int32 :response_to
29
+
30
+ # @attribute
31
+ # @return [Number] the operation code of this message
32
+ int32 :op_code
33
+
34
+ # @attribute
35
+ # The flags for the query. Supported flags are: +:continue_on_error+.
36
+ #
37
+ # @param [Array] flags the flags for this message
38
+ # @return [Array] the flags for this message
39
+ flags :flags, continue_on_error: 2 ** 0
40
+
41
+ # @attribute
42
+ # @return [String] the namespaced collection name
43
+ cstring :full_collection_name
44
+
45
+ # @attribute
46
+ # @return [Array<Hash>] the documents to insert
47
+ document :documents, type: :array
48
+
49
+ # @return [Number] OP_INSERT operation code (2002)
50
+ def op_code
51
+ 2002
52
+ end
53
+
54
+ # @return [String, Symbol] the database this insert targets
55
+ attr_reader :database
56
+
57
+ # @return [String, Symbol] the collection this insert targets
58
+ attr_reader :collection
59
+
60
+ # Create a new insert command. The +database+ and +collection+ arguments
61
+ # are joined together to set the +full_collection_name+.
62
+ #
63
+ # @example
64
+ # Insert.new "moped", "users", [{ name: "John" }],
65
+ # flags: [:continue_on_error],
66
+ # request_id: 123
67
+ #
68
+ # @param [String, Symbol] database the database to insert into
69
+ # @param [String, Symbol] collection the collection to insert into
70
+ # @param [Array<Hash>] documents the documents to insert
71
+ # @param [Hash] options additional options
72
+ # @option options [Number] :request_id the command's request id
73
+ # @option options [Array] :flags the flags for insertion. Supported
74
+ # flags: +:continue_on_error+
75
+ def initialize(database, collection, documents, options = {})
76
+ @database = database
77
+ @collection = collection
78
+
79
+ @full_collection_name = "#{database}.#{collection}"
80
+ @documents = documents
81
+ @request_id = options[:request_id]
82
+ @flags = options[:flags]
83
+ end
84
+
85
+ def log_inspect
86
+ type = "INSERT"
87
+
88
+ "%-12s database=%s collection=%s documents=%s flags=%s" % [type, database, collection, documents.inspect, flags.inspect]
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,61 @@
1
+ module Moped
2
+ module Protocol
3
+
4
+ # The Protocol class for killing active cursors.
5
+ #
6
+ # @example
7
+ # command = KillCursors.new [123, 124, 125]
8
+ #
9
+ # @example Setting the request id
10
+ # command = KillCursors.new [123, 124, 125], request_id: 456
11
+ class KillCursors
12
+ include Message
13
+
14
+ # @attribute
15
+ # @return [Number] the length of the message
16
+ int32 :length
17
+
18
+ # @attribute
19
+ # @return [Number] the request id of the message
20
+ int32 :request_id
21
+
22
+ int32 :response_to
23
+
24
+ # @attribute
25
+ # @return [Number] the operation code of this message
26
+ int32 :op_code
27
+
28
+ int32 :reserved # reserved for future use
29
+
30
+ # @attribute
31
+ # @return [Number] the number of cursor ids
32
+ int32 :number_of_cursor_ids
33
+
34
+ # @attribute
35
+ # @return [Array] the cursor ids to kill
36
+ int64 :cursor_ids, type: :array
37
+
38
+ # @return [Number] OP_KILL_CURSORS operation code (2007)
39
+ def op_code
40
+ 2007
41
+ end
42
+
43
+ # Create a new command to kill cursors.
44
+ #
45
+ # @param [Array] cursor_ids an array of cursor ids to kill
46
+ # @param [Hash] options additional options
47
+ # @option options [Number] :request_id the command's request id
48
+ def initialize(cursor_ids, options = {})
49
+ @cursor_ids = cursor_ids
50
+ @number_of_cursor_ids = cursor_ids.length
51
+ @request_id = options[:request_id]
52
+ end
53
+
54
+ def log_inspect
55
+ type = "KILL_CURSORS"
56
+
57
+ "%-12s cursor_ids=%s" % [type, cursor_ids.inspect]
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,320 @@
1
+ module Moped
2
+ module Protocol
3
+
4
+ # The base class for building all messages needed to implement the Mongo
5
+ # Wire Protocol. It provides a minimal DSL for defining typed fields for
6
+ # serialization and deserialization over the wire.
7
+ #
8
+ # @example
9
+ #
10
+ # class KillCursors < Moped::Protocol::Message
11
+ # # header fields
12
+ # int32 :length
13
+ # int32 :request_id
14
+ # int32 :response_to
15
+ # int32 :op_code
16
+ #
17
+ # # message fields
18
+ # int32 :reserved
19
+ # int32 :number_of_cursors
20
+ # int64 :cursor_ids, type: :array
21
+ #
22
+ # # Customize field reader
23
+ # def number_of_cursors
24
+ # cursor_ids.length
25
+ # end
26
+ # end
27
+ #
28
+ # Note that all messages *must* implement the header fields required by the
29
+ # Mongo Wire Protocol, namely:
30
+ #
31
+ # int32 :length
32
+ # int32 :request_id
33
+ # int32 :response_to
34
+ # int32 :op_code
35
+ #
36
+ module Message
37
+
38
+ class << self
39
+
40
+ # Extends the including class with +ClassMethods+.
41
+ #
42
+ # @param [Class] subclass the inheriting class
43
+ def included(base)
44
+ super
45
+
46
+ base.extend ClassMethods
47
+ end
48
+
49
+ private :included
50
+ end
51
+
52
+ # Provides a DSL for defining struct-like fields for building messages
53
+ # for the Mongo Wire.
54
+ #
55
+ # @example
56
+ # class Command
57
+ # extend Message::ClassMethods
58
+ #
59
+ # int32 :length
60
+ # end
61
+ #
62
+ # Command.fields # => [:length]
63
+ # command = Command.new
64
+ # command.length = 12
65
+ # command.serialize_length("") # => "\f\x00\x00\x00"
66
+ module ClassMethods
67
+
68
+ # @return [Array] the fields defined for this message
69
+ def fields
70
+ @fields ||= []
71
+ end
72
+
73
+ # Declare a null terminated string field.
74
+ #
75
+ # @example
76
+ # class Query < Message
77
+ # cstring :collection
78
+ # end
79
+ #
80
+ # @param [String] name the name of this field
81
+ def cstring(name)
82
+ attr_accessor name
83
+
84
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
85
+ def serialize_#{name}(buffer)
86
+ buffer << #{name}
87
+ buffer << 0
88
+ end
89
+ RUBY
90
+
91
+ fields << name
92
+ end
93
+
94
+ # Declare a BSON Document field.
95
+ #
96
+ # @example
97
+ # class Update < Message
98
+ # document :selector
99
+ # end
100
+ #
101
+ # @example optional document field
102
+ # class Query < Message
103
+ # document :selector
104
+ # document :fields, optional: true
105
+ # end
106
+ #
107
+ # @example array of documents
108
+ # class Reply < Message
109
+ # document :documents, type: :array
110
+ # end
111
+ #
112
+ # @param [String] name the name of this field
113
+ # @param [Hash] options the options for this field
114
+ # @option options [:array] :type specify an array of documents
115
+ # @option options [Boolean] :optional specify this field as optional
116
+ def document(name, options = {})
117
+ attr_accessor name
118
+
119
+ if options[:optional]
120
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
121
+ def serialize_#{name}(buffer)
122
+ BSON::Document.serialize(#{name}, buffer) if #{name}
123
+ end
124
+ RUBY
125
+ elsif options[:type] == :array
126
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
127
+ def serialize_#{name}(buffer)
128
+ #{name}.each do |document|
129
+ BSON::Document.serialize(document, buffer)
130
+ end
131
+ end
132
+ RUBY
133
+ else
134
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
135
+ def serialize_#{name}(buffer)
136
+ BSON::Document.serialize(#{name}, buffer)
137
+ end
138
+ RUBY
139
+ end
140
+
141
+ fields << name
142
+ end
143
+
144
+ # Declare a flag field (32 bit signed integer)
145
+ #
146
+ # @example
147
+ # class Update < Message
148
+ # flags :flags, upsert: 2 ** 0,
149
+ # multi: 2 ** 1
150
+ # end
151
+ #
152
+ # @param [String] name the name of this field
153
+ # @param [Hash{Symbol => Number}] flags the flags for this flag field
154
+ def flags(name, flag_map = {})
155
+ attr_writer name
156
+
157
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
158
+ def #{name}
159
+ @#{name} ||= []
160
+ end
161
+
162
+ def #{name}=(flags)
163
+ if flags.is_a? Numeric
164
+ @#{name} = #{name}_from_int(flags)
165
+ else
166
+ @#{name} = flags
167
+ end
168
+ end
169
+
170
+ def #{name}_as_int
171
+ bits = 0
172
+ flags = self.#{name}
173
+ #{flag_map.map { |flag, value| "bits |= #{value} if flags.include? #{flag.inspect}" }.join "\n"}
174
+ bits
175
+ end
176
+
177
+ def #{name}_from_int(bits)
178
+ flags = []
179
+ #{flag_map.map { |flag, value| "flags << #{flag.inspect} if #{value} & bits == #{value}" }.join "\n"}
180
+ flags
181
+ end
182
+
183
+ def serialize_#{name}(buffer)
184
+ buffer << [#{name}_as_int].pack('l<')
185
+ end
186
+
187
+ def deserialize_#{name}(buffer)
188
+ bits, = buffer.read(4).unpack('l<')
189
+
190
+ self.#{name} = bits
191
+ end
192
+ RUBY
193
+
194
+ fields << name
195
+ end
196
+
197
+ # Declare a 32 bit signed integer field.
198
+ #
199
+ # @example
200
+ # class Query < Message
201
+ # int32 :length
202
+ # end
203
+ #
204
+ # @param [String] name the name of this field
205
+ def int32(name)
206
+ attr_writer name
207
+
208
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
209
+ def #{name}
210
+ @#{name} ||= 0
211
+ end
212
+
213
+ def serialize_#{name}(buffer)
214
+ buffer << [#{name}].pack('l<')
215
+ end
216
+
217
+ def deserialize_#{name}(buffer)
218
+ self.#{name}, = buffer.read(4).unpack('l<')
219
+ end
220
+ RUBY
221
+
222
+ fields << name
223
+ end
224
+
225
+ # Declare a 64 bit signed integer field.
226
+ #
227
+ # @example
228
+ # class Query < Message
229
+ # int64 :cursor_id
230
+ # end
231
+ #
232
+ # @example with array type
233
+ # class KillCursors < Message
234
+ # int64 :cursor_ids, type: :array
235
+ # end
236
+ #
237
+ # @param [String] name the name of this field
238
+ # @param [Hash] options the options for this field
239
+ # @option options [:array] :type specify an array of 64 bit ints
240
+ def int64(name, options = {})
241
+ attr_writer name
242
+
243
+ if options[:type] == :array
244
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
245
+ def #{name}
246
+ @#{name} ||= []
247
+ end
248
+
249
+ def serialize_#{name}(buffer)
250
+ buffer << #{name}.pack('q*<')
251
+ end
252
+
253
+ def deserialize_#{name}(buffer)
254
+ raise NotImplementedError
255
+ end
256
+ RUBY
257
+ else
258
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
259
+ def #{name}
260
+ @#{name} ||= 0
261
+ end
262
+
263
+ def serialize_#{name}(buffer)
264
+ buffer << [#{name}].pack('q<')
265
+ end
266
+
267
+ def deserialize_#{name}(buffer)
268
+ self.#{name}, = buffer.read(8).unpack('q<')
269
+ end
270
+ RUBY
271
+ end
272
+
273
+ fields << name
274
+ end
275
+
276
+ private
277
+
278
+ # This ensures that subclasses of the primary wire message classes have
279
+ # identical fields.
280
+ def inherited(subclass)
281
+ super
282
+
283
+ subclass.fields.replace fields
284
+ end
285
+
286
+ end
287
+
288
+ # Serializes the message and all of its fields to a new buffer or to the
289
+ # provided buffer.
290
+ #
291
+ # @param [String] buffer a buffer to serialize to
292
+ # @return [String] the result of serliazing this message
293
+ def serialize(buffer = "")
294
+ buffer.tap do
295
+ start = buffer.length
296
+
297
+ self.class.fields.each do |field|
298
+ __send__ :"serialize_#{field}", buffer
299
+ end
300
+
301
+ self.length = buffer.length - start
302
+
303
+ buffer[start, 4] = serialize_length("")
304
+ end
305
+ end
306
+
307
+ alias to_s serialize
308
+
309
+ # @return [String] the nicely formatted version of the message
310
+ def inspect
311
+ fields = self.class.fields.map do |field|
312
+ "@#{field}=" + __send__(field).inspect
313
+ end
314
+ "#<#{self.class.name}\n" <<
315
+ " #{fields * "\n "}>"
316
+ end
317
+
318
+ end
319
+ end
320
+ end