extjs-mvc 0.3.4 → 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -62,7 +62,7 @@ E.g. with the following definition:
62
62
  class User < ActiveRecord::Base
63
63
  include ExtJS::Model
64
64
 
65
- extjs_fieldset :grid, [:name, :description, :company => [:name, :description]]
65
+ extjs_fieldset :grid, fields => [:name, :description, :company => [:name, :description]]
66
66
  extjs_fieldset :combo, [:full_name]
67
67
 
68
68
  def full_name
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.4
1
+ 0.3.5
data/lib/extjs-mvc.rb CHANGED
@@ -16,6 +16,8 @@ module ExtJS
16
16
  require 'model/data_mapper'
17
17
  elsif defined?(MongoMapper)
18
18
  require 'model/mongo_mapper'
19
+ else
20
+ raise StandardError.new("extjs-mvc could not detect an ORM framework. Be sure to include your ORM framework before initializing extjs-mvc Gem.")
19
21
  end
20
22
 
21
23
  # Rails-style Array#extract_options! used heavily
@@ -68,21 +68,17 @@ module ExtJS
68
68
  # @return {Array}
69
69
  #
70
70
  def extjs_associations
71
- #if @extjs_associations.nil?
72
- extjs_associations = {}
73
- self.reflections.keys.each do |key|
74
- assn = self.reflections[key]
75
- type = (assn.macro === :has_many || assn.macro === :has_and_belongs_to_many) ? :many : assn.macro
76
- extjs_associations[key.to_sym] = {
77
- :name => key.to_sym,
78
- :type => type,
79
- :class => assn.options[:polymorphic] ? nil : assn.class_name.constantize,
80
- :foreign_key => assn.association_foreign_key.to_sym,
81
- :is_polymorphic => !!assn.options[:polymorphic]
82
- }
83
- end
84
- #end
85
- extjs_associations
71
+ @extjs_associations ||= self.reflections.inject({}) do |memo, (key, assn)|
72
+ type = (assn.macro === :has_many || assn.macro === :has_and_belongs_to_many) ? :many : assn.macro
73
+ memo[key.to_sym] = {
74
+ :name => key.to_sym,
75
+ :type => type,
76
+ :class => assn.options[:polymorphic] ? nil : assn.class_name.constantize,
77
+ :foreign_key => assn.association_foreign_key.to_sym,
78
+ :is_polymorphic => !!assn.options[:polymorphic]
79
+ }
80
+ memo
81
+ end
86
82
  end
87
83
  end
88
84
  end
data/lib/model/base.rb CHANGED
@@ -21,23 +21,40 @@ module ExtJS
21
21
 
22
22
  ##
23
23
  # Converts a model instance to a record compatible with ExtJS
24
- # @params {Mixed} params A list of fields to use instead of this Class's extjs_record_fields
25
24
  #
25
+ # The first parameter should be the fieldset for which the record will be returned.
26
+ # If no parameter is provided, then the default fieldset will be choosen
27
+ # Alternativly the first parameter can be a Hash with a :fields member to directly specify
28
+ # the fields to use for the record.
29
+ #
30
+ # All these are valid calls:
31
+ #
32
+ # user.to_record # returns record for :default fieldset
33
+ # # (fieldset is autmatically defined, if not set)
34
+ #
35
+ # user.to_record :fieldset # returns record for :fieldset fieldset
36
+ # # (fieldset is autmatically defined, if not set)
37
+ #
38
+ # user.to_record :fields => [:id, :password]
39
+ # # returns record for the fields 'id' and 'password'
40
+ #
41
+ # For even more valid options for this method (which all should not be neccessary to use)
42
+ # have a look at ExtJS::Model::Util.extract_fieldset_and_options
26
43
  def to_record(*params)
27
- fieldset, params = self.class.extjs_extract_fieldset! params
44
+ fieldset, options = Util.extract_fieldset_and_options params
28
45
 
29
46
  fields = []
30
- if params.empty?
47
+ if options[:fields].empty?
31
48
  fields = self.class.extjs_get_fields_for_fieldset(fieldset)
32
49
  else
33
- fields = self.class.process_fields(*params)
50
+ fields = self.class.process_fields(*options[:fields])
34
51
  end
35
52
 
36
53
  assns = self.class.extjs_associations
37
54
  pk = self.class.extjs_primary_key
38
55
 
39
56
  # build the initial field data-hash
40
- data = extjs_prepare_data(pk)
57
+ data = {pk => self.send(pk)}
41
58
 
42
59
  fields.each do |field|
43
60
  next if data.has_key? field[:name] # already processed (e.g. explicit mentioning of :id)
@@ -45,25 +62,34 @@ module ExtJS
45
62
  value = nil
46
63
  if association_reflection = assns[field[:name]] # if field is an association
47
64
  association = self.send(field[:name])
65
+
66
+ # skip this association if we already visited it
67
+ # otherwise we could end up in a cyclic reference
68
+ next if options[:visited_classes].include? association.class
69
+
48
70
  case association_reflection[:type]
49
- when :belongs_to
71
+ when :belongs_to, :has_one
50
72
  if association.respond_to? :to_record
51
73
  assn_fields = field[:fields]
52
74
  if assn_fields.nil?
53
75
  assn_fields = association.class.extjs_get_fields_for_fieldset(field.fetch(:fieldset, fieldset))
54
76
  end
55
- value = association.to_record *assn_fields
77
+
78
+ value = association.to_record :fields => assn_fields,
79
+ :visited_classes => options[:visited_classes] + [self.class]
56
80
  else
57
81
  value = {}
58
82
  (field[:fields]||[]).each do |sub_field|
59
83
  value[sub_field[:name]] = association.send(sub_field[:name]) if association.respond_to? sub_field[:name]
60
84
  end
61
85
  end
62
- # Append associations foreign_key to data
63
- data[association_reflection[:foreign_key]] = self.send(association_reflection[:foreign_key])
64
- if association_reflection[:is_polymorphic]
65
- foreign_type = self.class.extjs_polymorphic_type(association_reflection[:foreign_key])
66
- data[foreign_type] = self.send(foreign_type)
86
+ if association_reflection[:type] == :belongs_to
87
+ # Append associations foreign_key to data
88
+ data[association_reflection[:foreign_key]] = self.send(association_reflection[:foreign_key])
89
+ if association_reflection[:is_polymorphic]
90
+ foreign_type = self.class.extjs_polymorphic_type(association_reflection[:foreign_key])
91
+ data[foreign_type] = self.send(foreign_type)
92
+ end
67
93
  end
68
94
  when :many
69
95
  value = association.collect { |r| r.to_record } # use carefully, can get HUGE
@@ -76,15 +102,6 @@ module ExtJS
76
102
  end
77
103
  data
78
104
  end
79
-
80
- ##
81
- # prepares the initial data-hash. Had to implement this to fix a MongoMapper issue where pk
82
- # is an Object. Messed things up when converting to JSON. Perhaps a better way is possible.
83
- # Any adapter can override this but typical relational dbs with Integer pks won't need to.
84
- #
85
- def extjs_prepare_data(pk)
86
- {pk => self.send(pk)}
87
- end
88
105
  end
89
106
 
90
107
  ##
@@ -95,12 +112,31 @@ module ExtJS
95
112
  # render AR columns to Ext.data.Record.create format
96
113
  # eg: {name:'foo', type: 'string'}
97
114
  #
98
- def extjs_record(*fields)
99
- fieldset, fields = self.extjs_extract_fieldset! fields
100
- if fields.empty?
115
+ # The first parameter should be the fieldset for which the record definition will be returned.
116
+ # If no parameter is provided, then the default fieldset will be choosen
117
+ # Alternativly the first parameter can be a Hash with a :fields member to directly specify
118
+ # the fields to use for the record config.
119
+ #
120
+ # All these are valid calls:
121
+ #
122
+ # User.extjs_record # returns record config for :default fieldset
123
+ # # (fieldset is autmatically defined, if not set)
124
+ #
125
+ # User.extjs_record :fieldset # returns record config for :fieldset fieldset
126
+ # # (fieldset is autmatically defined, if not set)
127
+ #
128
+ # User.extjs_record :fields => [:id, :password]
129
+ # # returns record config for the fields 'id' and 'password'
130
+ #
131
+ # For even more valid options for this method (which all should not be neccessary to use)
132
+ # have a look at ExtJS::Model::Util.extract_fieldset_and_options
133
+ def extjs_record(*params)
134
+ fieldset, options = Util.extract_fieldset_and_options params
135
+
136
+ if options[:fields].empty?
101
137
  fields = self.extjs_get_fields_for_fieldset(fieldset)
102
138
  else
103
- fields = self.process_fields(*fields)
139
+ fields = self.process_fields(*options[:fields])
104
140
  end
105
141
 
106
142
  associations = self.extjs_associations
@@ -109,17 +145,23 @@ module ExtJS
109
145
  rs = []
110
146
 
111
147
  fields.each do |field|
148
+
112
149
  field = Marshal.load(Marshal.dump(field)) # making a deep copy
113
150
 
114
151
  if col = columns[field[:name]] # <-- column on this model
115
152
  rs << self.extjs_field(field, col)
116
153
  elsif assn = associations[field[:name]]
154
+ # skip this association if we already visited it
155
+ # otherwise we could end up in a cyclic reference
156
+ next if options[:visited_classes].include? assn[:class]
157
+
117
158
  assn_fields = field[:fields]
118
159
  if assn[:class].respond_to?(:extjs_record) # <-- exec extjs_record on assn Model.
119
160
  if assn_fields.nil?
120
161
  assn_fields = assn[:class].extjs_get_fields_for_fieldset(field.fetch(:fieldset, fieldset))
121
162
  end
122
- record = assn[:class].extjs_record(field.fetch(:fieldset, fieldset), assn_fields)
163
+
164
+ record = assn[:class].extjs_record(field.fetch(:fieldset, fieldset), { :visited_classes => options[:visited_classes] + [self], :fields => assn_fields})
123
165
  rs.concat(record[:fields].collect { |assn_field|
124
166
  self.extjs_field(assn_field, :parent_trail => field[:name], :mapping => field[:name], :allowBlank => true) # <-- allowBlank on associated data?
125
167
  })
@@ -170,12 +212,9 @@ module ExtJS
170
212
  # end
171
213
  #
172
214
  def extjs_fieldset(*params)
173
- fieldset, params = self.extjs_extract_fieldset! params
174
- # creates a method storing the fieldset-to-field association
175
- # using a method and not an class-level variable because the latter
176
- # is bogus when we deal with inheritance
215
+ fieldset, options = Util.extract_fieldset_and_options params
177
216
  var_name = :"@extjs_fieldsets__#{fieldset}"
178
- self.instance_variable_set( var_name, self.process_fields(*params) )
217
+ self.instance_variable_set( var_name, self.process_fields(*options[:fields]) )
179
218
  end
180
219
 
181
220
  def extjs_get_fields_for_fieldset(fieldset)
@@ -194,7 +233,9 @@ module ExtJS
194
233
  # shortcut to define the default fieldset. For backwards-compatibility.
195
234
  #
196
235
  def extjs_fields(*params)
197
- self.extjs_fieldset(:default, params)
236
+ self.extjs_fieldset(:default, {
237
+ :fields => params
238
+ })
198
239
  end
199
240
 
200
241
  ##
@@ -229,9 +270,10 @@ module ExtJS
229
270
  elsif f.has_key?(:name) # already a valid Hash, just copy it over
230
271
  fields << f
231
272
  else
232
- raise ArgumentError, "encountered a Hash that I don't know anyting to do with `#{f.inspect}:#{f.class}`"
273
+ raise ArgumentError, "encountered a Hash that I don't know anything to do with `#{f.inspect}:#{f.class}`"
233
274
  end
234
275
  else # should be a String or Symbol
276
+ raise ArgumentError, "encountered a fields Array that I don't understand: #{params.inspect} -- `#{f.inspect}:#{f.class}` is not a Symbol or String" unless f.is_a?(Symbol) || f.is_a?(String)
235
277
  fields << {:name => f.to_sym}
236
278
  end
237
279
  end
@@ -239,20 +281,6 @@ module ExtJS
239
281
  fields
240
282
  end
241
283
 
242
- ##
243
- # returns the fieldset from the arguments.
244
- # @return [{Symbol}, Array]
245
- def extjs_extract_fieldset! arguments
246
- fieldset = :default
247
- if arguments.size > 1 && arguments[0].is_a?(Symbol) && arguments[1].is_a?(Array)
248
- fieldset = arguments.shift
249
- arguments = arguments[0]
250
- elsif arguments.size == 1 && arguments[0].is_a?(Symbol)
251
- fieldset = arguments.shift
252
- end
253
- [fieldset, arguments]
254
- end
255
-
256
284
  ##
257
285
  # Render a column-config object
258
286
  # @param {Hash/Column} field Field-configuration Hash, probably has :name already set and possibly Ext.data.Field options.
@@ -302,6 +330,39 @@ module ExtJS
302
330
  # @extjs_used_associations
303
331
  # end
304
332
  end
333
+
334
+ module Util
335
+
336
+ ##
337
+ # returns the fieldset from the arguments and normalizes the options.
338
+ # @return [{Symbol}, {Hash}]
339
+ def self.extract_fieldset_and_options arguments
340
+ orig_args = arguments
341
+ fieldset = :default
342
+ options = { # default options
343
+ :visited_classes => [],
344
+ :fields => []
345
+ }
346
+ if arguments.size > 2 || (arguments.size == 2 && !arguments[0].is_a?(Symbol))
347
+ raise ArgumentError, "Don't know how to handle #{arguments.inspect}"
348
+ elsif arguments.size == 2 && arguments[0].is_a?(Symbol)
349
+ fieldset = arguments.shift
350
+ if arguments[0].is_a?(Array)
351
+ options.update({
352
+ :fields => arguments[0]
353
+ })
354
+ elsif arguments[0].is_a?(Hash)
355
+ options.update(arguments[0])
356
+ end
357
+ elsif arguments.size == 1 && arguments[0].is_a?(Symbol)
358
+ fieldset = arguments.shift
359
+ elsif arguments.size == 1 && arguments[0].is_a?(Hash)
360
+ fieldset = arguments[0].delete(:fieldset) || :default
361
+ options.update(arguments[0])
362
+ end
363
+ [fieldset, options]
364
+ end
365
+ end
305
366
  end
306
367
  end
307
368
 
@@ -4,12 +4,6 @@
4
4
 
5
5
  module ExtJS
6
6
  module Model
7
- module InstanceMethods
8
- def extjs_prepare_data(pk)
9
- {pk => self.send(pk).to_s}
10
- end
11
- end
12
-
13
7
  ##
14
8
  # ClassMethods
15
9
  #
@@ -28,19 +22,16 @@ module ExtJS
28
22
  end
29
23
 
30
24
  def extjs_associations
31
- if @extjs_associations.nil?
32
- @extjs_associations = {}
33
- self.associations.keys.each do |key|
34
- @extjs_associations[key.to_sym] = {
35
- :name => key,
36
- :type => self.associations[key].type,
37
- :class => self.associations[key].class_name.constantize,
38
- :foreign_key => self.associations[key].foreign_key,
39
- :is_polymorphic => false # <-- no impl. for MM is_polymorhpic yet. Anyone care to implement this?
40
- }
41
- end
25
+ @extjs_associations ||= self.associations.inject({}) do |memo, (key, assn)|
26
+ memo[key.to_sym] = {
27
+ :name => key.to_sym,
28
+ :type => assn.type,
29
+ :class => assn.class_name.constantize,
30
+ :foreign_key => assn.foreign_key,
31
+ :is_polymorphic => false
32
+ }
33
+ memo
42
34
  end
43
- @extjs_associations
44
35
  end
45
36
 
46
37
  def extjs_type(col)
data/test/model_test.rb CHANGED
@@ -166,6 +166,48 @@ class ModelTest < Test::Unit::TestCase
166
166
  end
167
167
  end
168
168
 
169
+ context "Person with User association (has_one relationship)" do
170
+ setup do
171
+ clean_all
172
+ User.extjs_fields(:id, :password)
173
+ Person.extjs_fields(:id, :user)
174
+ end
175
+ should "produce a valid store config" do
176
+ fields = Person.extjs_record[:fields]
177
+ assert_array_has_item(fields, 'has id') {|f| f[:name] === "id" }
178
+ assert_array_has_item(fields, 'has user_id') {|f| f[:name] === "user_id" and f[:mapping] == 'user.id' }
179
+ assert_array_has_item(fields, 'has user_password') {|f| f[:name] === "user_password"and f[:mapping] == 'user.password' }
180
+ end
181
+ should "produce a valid to_record record" do
182
+ person = Person.create!(:first => 'first', :last => 'last', :email => 'email')
183
+ user = User.create!(:person_id => person.id, :password => 'password')
184
+ record = person.reload.to_record
185
+ assert_equal(person.id, record[:id])
186
+ assert_equal(user.id, record[:user][:id])
187
+ assert_equal('password', record[:user][:password])
188
+ end
189
+ end
190
+
191
+ context "Person with User association (has_one/belongs_to relationship) cyclic reference" do
192
+ setup do
193
+ clean_all
194
+ User.extjs_fields(:id, :person)
195
+ Person.extjs_fields(:id, :user)
196
+ end
197
+ should "produce a valid store config for Person" do
198
+ fields = Person.extjs_record[:fields]
199
+ assert_array_has_item(fields, 'has id') {|f| f[:name] === "id" }
200
+ assert_array_has_item(fields, 'has user_id') {|f| f[:name] === "user_id" and f[:mapping] == 'user.id' }
201
+ end
202
+ should "produce a valid to_record record for Person" do
203
+ person = Person.create!(:first => 'first', :last => 'last', :email => 'email')
204
+ user = User.create!(:person_id => person.id, :password => 'password')
205
+ record = person.reload.to_record
206
+ assert_equal(person.id, record[:id])
207
+ assert_equal(user.id, record[:user][:id])
208
+ end
209
+ end
210
+
169
211
  context "Fields should render with correct, ExtJS-compatible data-types" do
170
212
  setup do
171
213
  clean_all
@@ -204,7 +246,7 @@ class ModelTest < Test::Unit::TestCase
204
246
  end
205
247
  end
206
248
 
207
- context "polymorphic assosiations" do
249
+ context "polymorphic associations" do
208
250
  setup do
209
251
  clean_all
210
252
  end
@@ -220,7 +262,7 @@ class ModelTest < Test::Unit::TestCase
220
262
  assert_array_has_item(fields, 'addressable_type') {|f| f[:name] === 'addressable_type' && !f[:mapping] }
221
263
  end
222
264
 
223
- should "create the right store config when including members of the polymorpic association" do
265
+ should "create the right store config when including members of the polymorphic association" do
224
266
  Address.extjs_fields :street, :addressable => [:name]
225
267
  fields = Address.extjs_record[:fields]
226
268
  assert_array_has_item(fields, "has addressable_name") {|f| f[:name] === 'addressable_name' && f[:mapping] === 'addressable.name'}
@@ -246,13 +288,13 @@ class ModelTest < Test::Unit::TestCase
246
288
  clean_all
247
289
  end
248
290
 
249
- should "fieldsets should be accessible from decendants" do
291
+ should "fieldsets should be accessible from descendants" do
250
292
  Location.extjs_fieldset :on_location, [:street]
251
293
  fields = House.extjs_record(:on_location)[:fields]
252
294
  assert_array_has_item(fields, 'has street') {|f| f[:name] === 'street' }
253
295
  assert_array_has_not_item(fields, 'has name') {|f| f[:name] === 'name' }
254
296
  end
255
- should "fieldsets should be overrideable from decendants" do
297
+ should "fieldsets should be overrideable from descendants" do
256
298
  Location.extjs_fieldset :override, [:street]
257
299
  House.extjs_fieldset :override, [:name]
258
300
  fields = House.extjs_record(:override)[:fields]
@@ -261,11 +303,11 @@ class ModelTest < Test::Unit::TestCase
261
303
  end
262
304
  end
263
305
 
264
- context "ExtJS::Model::ClassMethods" do
265
-
266
- context "#extjs_extract_fieldset! default" do
306
+ context "ExtJS::Model::Util" do
307
+ context "#extract_fieldset_and_options default" do
267
308
  setup do
268
- @fieldset, @fields = BogusModel.extjs_extract_fieldset! [:one, :two, :three]
309
+ @fieldset, @options = ExtJS::Model::Util.extract_fieldset_and_options [:fields => [:one, :two, :three]]
310
+ @fields = @options[:fields]
269
311
  end
270
312
  should "return :default when no fieldset provided" do
271
313
  assert_equal(:'default', @fieldset)
@@ -275,9 +317,10 @@ class ModelTest < Test::Unit::TestCase
275
317
  end
276
318
  end
277
319
 
278
- context "#extjs_extract_fieldset! with explicit fieldset definition" do
320
+ context "#extract_fieldset_and_options with explicit fieldset definition and array with fields" do
279
321
  setup do
280
- @fieldset, @fields = BogusModel.extjs_extract_fieldset! [:explicit, [:one, :two, :three]]
322
+ @fieldset, @options = ExtJS::Model::Util.extract_fieldset_and_options [:explicit, [:one, :two, :three]]
323
+ @fields = @options[:fields]
281
324
  end
282
325
  should "return :default when no fieldset provided" do
283
326
  assert_equal(:'explicit', @fieldset)
@@ -287,18 +330,55 @@ class ModelTest < Test::Unit::TestCase
287
330
  end
288
331
  end
289
332
 
290
- context "#extjs_extract_fieldset! edge cases" do
333
+ context "#extract_fieldset_and_options with explicit fieldset definition and hash with fields" do
334
+ setup do
335
+ @fieldset, @options = ExtJS::Model::Util.extract_fieldset_and_options [:explicit, {:fields => [:one, :two, :three]}]
336
+ @fields = @options[:fields]
337
+ end
338
+ should "return :default when no fieldset provided" do
339
+ assert_equal(:'explicit', @fieldset)
340
+ end
341
+ should "not alter the fields array" do
342
+ assert_equal([:one, :two, :three], @fields)
343
+ end
344
+ end
345
+
346
+ context "#extract_fieldset_and_options with only a hash" do
347
+ setup do
348
+ @fieldset, @options = ExtJS::Model::Util.extract_fieldset_and_options [{:fieldset => :explicit, :fields => [:one, :two, :three]}]
349
+ @fields = @options[:fields]
350
+ end
351
+ should "return :default when no fieldset provided" do
352
+ assert_equal(:'explicit', @fieldset)
353
+ end
354
+ should "not alter the fields array" do
355
+ assert_equal([:one, :two, :three], @fields)
356
+ end
357
+ end
358
+
359
+ context "#extract_fieldset_and_options edge cases" do
291
360
  should "called without arguments" do
292
- @fieldset, @fields = BogusModel.extjs_extract_fieldset! []
361
+ @fieldset, @options = ExtJS::Model::Util.extract_fieldset_and_options []
362
+ @fields = @options[:fields]
293
363
  assert_equal(:'default', @fieldset)
294
364
  assert_equal([], @fields)
295
365
  end
296
366
  should "called with only the fieldset and no field arguments" do
297
- @fieldset, @fields = BogusModel.extjs_extract_fieldset! [:explicit]
367
+ @fieldset, @options = ExtJS::Model::Util.extract_fieldset_and_options [:explicit]
368
+ @fields = @options[:fields]
298
369
  assert_equal(:'explicit', @fieldset)
299
370
  assert_equal([], @fields)
300
371
  end
372
+ should "raise error when called with more than 2 arguments" do
373
+ assert_raise(ArgumentError) { ExtJS::Model::Util.extract_fieldset_and_options [:explicit, :some, {}] }
374
+ end
375
+ should "raise error when called with 2 arguments and the first one is no symbol" do
376
+ assert_raise(ArgumentError) { ExtJS::Model::Util.extract_fieldset_and_options [{ :fields => [] }, :explicit] }
377
+ end
301
378
  end
379
+ end
380
+
381
+ context "ExtJS::Model::ClassMethods" do
302
382
 
303
383
  context "#process_fields" do
304
384
  should "handle a simple Array of Symbols" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: extjs-mvc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Scott
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-02-20 00:00:00 -05:00
12
+ date: 2010-02-21 00:00:00 -05:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency