acts_as_dashboard 0.3.2 → 0.3.3

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.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.2
1
+ 0.3.3
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{acts_as_dashboard}
8
- s.version = "0.3.2"
8
+ s.version = "0.3.3"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Nick Hoffman"]
12
- s.date = %q{2010-08-04}
12
+ s.date = %q{2010-08-05}
13
13
  s.description = %q{acts_as_dashboard makes it easy to create dashboards in Rails apps. Very little configuration is required. Read the docs to get started.}
14
14
  s.email = %q{nick@deadorange.com}
15
15
  s.extra_rdoc_files = [
@@ -1,305 +1,307 @@
1
- // function includeJavaScriptFile(path) {{{
2
- //
3
- // Creates a JavaScript <script> element and appends it
4
- // to the <head>.
5
- //
6
- function includeJavaScriptFile(path) {
7
- // Ensure that we have a non-empty string.'
8
- if ((typeof(path) != 'string') || (!path))
9
- {return;}
10
-
11
- // Ensure that we can find the <head>.
12
- var head = jQuery('head');
13
- if (!head)
14
- {return;}
15
-
16
- var element = document.createElement('script');
17
- element.type = 'text/javascript';
18
- element.src = path;
19
-
20
- head.append(element);
21
-
22
- return true;
23
- } // }}}
24
-
25
- // function buildTheDashboard() {{{
26
- //
27
- // Determines which dashboard widgets to build,
28
- // builds them, and adds them to the DOM.
29
- //
30
- function buildTheDashboard() {
31
- var parsed_widgets = jQuery.parseJSON(json_widgets);
32
-
33
- jQuery.each(parsed_widgets, function(index, widget) {
34
- if (widget.type == 'number')
35
- {dashboard.addNumberWidget(widget);}
36
- else if (widget.type == 'short_messages')
37
- {dashboard.addShortMessagesWidget(widget);}
38
- else if (widget.type == 'line_graph')
39
- {dashboard.addLineGraphWidget(widget);}
40
- });
41
- } // }}}
42
-
43
- function createUpdateTimerFor(widget) { // {{{
44
- $(this).everyTime(widget.update_interval, widget.name, function() {
45
- widget.updateData();
46
- });
47
- } // }}}
48
-
49
- // Begin Dashboard class. {{{
50
- var Dashboard = new JS.Class({
51
- initialize: function(options) { // {{{
52
- this.basePath = options.basePath;
53
-
54
- this.numberWidgets = new Array();
55
- this.shortMessagesWidgets = new Array();
56
- this.lineGraphWidgets = new Array();
57
-
58
- this.div = jQuery('#dashboard');
59
- this.numberWidgetsDiv = this.div.find('.dashboard-numbers');
60
- this.shortMessagesWidgetsDiv = this.div.find('.dashboard-short-messages');
61
- this.lineGraphWidgetsDiv = this.div.find('.dashboard-line-graphs');
62
- }, // }}}
63
-
64
- addNumberWidget: function(options) { // {{{
65
- var widget = new NumberWidget(options);
66
- var templateDiv = this.numberWidgetsDiv.find('.widget-template');
67
-
68
- var widgetDiv = templateDiv.clone(false)
69
- .removeClass('widget-template')
70
- .addClass('number-widget')
71
- .attr('id', widget.divID())
72
- .appendTo(this.numberWidgetsDiv);
73
-
74
- // Set the new div's title.
75
- widgetDiv.find('.widget-title').html(widget.title);
76
-
77
- widget.setDataDivTo(widgetDiv.find('.widget-data'));
78
- widget.updateData();
79
-
80
- createUpdateTimerFor(widget);
81
-
82
- this.numberWidgets.push(widget);
83
-
84
- widgetDiv.show();
85
- }, // }}}
86
-
87
- addShortMessagesWidget: function(options) { // {{{
88
- var widget = new ShortMessagesWidget(options);
89
- var templateDiv = this.shortMessagesWidgetsDiv.find('.widget-template');
90
-
91
- var widgetDiv = templateDiv.clone(false)
92
- .removeClass('widget-template')
93
- .addClass('short-messages-widget')
94
- .attr('id', widget.divID())
95
- .appendTo(this.shortMessagesWidgetsDiv);
96
-
97
- // Set the new div's title.
98
- widgetDiv.find('.widget-title').html(widget.title);
99
-
100
- widget.setDataDivTo(widgetDiv.find('.widget-data'));
101
- widget.updateData();
102
-
103
- createUpdateTimerFor(widget);
104
-
105
- this.shortMessagesWidgets.push(widget);
106
-
107
- widgetDiv.show();
108
- }, // }}}
109
-
110
- addLineGraphWidget: function(options) { // {{{
111
- var widget = new LineGraphWidget(options);
112
- var templateDiv = this.lineGraphWidgetsDiv.find('.widget-template');
113
-
114
- var widgetDiv = templateDiv.clone(false)
115
- .removeClass('widget-template')
116
- .addClass('line-graph-widget')
117
- .attr('id', widget.divID())
118
- .appendTo(this.lineGraphWidgetsDiv);
119
-
120
- // Set the new div's title.
121
- widgetDiv.find('.widget-title').html(widget.title);
122
-
123
- var dataDiv = widgetDiv.find('.widget-data')
124
- .attr('id', widget.divID() + '-data');
125
-
126
- widget.setDataDivTo(dataDiv);
127
-
128
- widget.updateData();
129
-
130
- // I don't know why, but setting these properties when dataDiv
131
- // is initialized fails to work. They work here, though...
132
- dataDiv.css('height', widget.height)
133
- dataDiv.css('width', widget.width)
134
-
135
- createUpdateTimerFor(widget);
136
-
137
- this.lineGraphWidgets.push(widget);
138
-
139
- widgetDiv.show();
140
- } // }}}
141
- });
142
- // End Dashboard class. }}}
143
-
144
- // Begin DashboardWidget class. {{{
145
- var DashboardWidget = new JS.Class({
146
- initialize: function(options) {
147
- this.type = options.type;
148
- this.name = options.name;
149
- this.title = options.title;
150
- this.update_interval = options.update_interval;
151
- },
152
-
153
- dataURL: function() {
154
- return dashboard.basePath + this.name;
155
- },
156
-
157
- divID: function() {
158
- return this.name + '-' + this.type + '-widget';
159
- },
160
-
161
- setDataDivTo: function(element) {
162
- this.dataDiv = element;
163
- }
164
- });
165
- // End DashboardWidget class. }}}
166
-
167
- // Begin NumberWidget class. {{{
168
- var NumberWidget = new JS.Class(DashboardWidget, {
169
- initialize: function(options) {
170
- options['type'] = 'number';
171
- this.callSuper(options);
172
- },
173
-
174
- updateData: function() {
175
- if (!this.dataDiv)
176
- {return false;}
177
-
178
- // This is needed so that the dataDiv property is accessible with jQuery.get() .
179
- var element = this.dataDiv;
180
-
181
- jQuery.get(this.dataURL(), function(data) {
182
- element.fadeOut(400, function() {
183
- element.html(data);
184
- element.fadeIn();
185
- });
186
- }, 'text');
187
- }
188
- });
189
- // End NumberWidget class. }}}
190
-
191
- // Begin ShortMessagesWidget class. {{{
192
- var ShortMessagesWidget = new JS.Class(DashboardWidget, {
193
- initialize: function(options) {
194
- options['type'] = 'short_messages';
195
- this.callSuper(options);
196
- this.maxDataItems = 5;
197
- },
198
-
199
- firstDataItem: function() {
200
- return this.dataDiv.find('li.widget-data-item:first');
201
- },
202
-
203
- dataItemsCount: function() {
204
- return this.dataDiv.find('li.widget-data-item').length;
205
- },
206
-
207
- createDataItem: function() {
208
- return this.dataDiv.find('li.widget-data-template')
209
- .clone(false)
210
- .removeClass('widget-data-template')
211
- .addClass('widget-data-item');
212
- },
213
-
214
- updateData: function() {
215
- if (!this.dataDiv)
216
- {return false;}
217
-
218
- var new_data = '';
219
- var new_li = this.createDataItem();
220
-
221
- // We use ajax() instead of get() because the "async" option must
222
- // be false. If it isn't, we're unable to determine if data was
223
- // obtained.
224
- var get_result = jQuery.ajax({
225
- url: this.dataURL(),
226
- type: 'GET',
227
- async: false,
228
- cache: false,
229
- dataType: 'text',
230
- timeout: this.updateInterval,
231
- success: function(data) {
232
- new_li.html(data);
233
- new_data = data;
234
- }
235
- });
236
-
237
- // If no data was obtained, return. Otherwise, an empty list item
238
- // will be shown.
239
- if (new_data == '')
240
- {return false;}
241
-
242
- new_li.appendTo(this.dataDiv.find('ul'));
243
-
244
- // Hide the first list item if we've reached the maximum number of
245
- // list items to show in this widget.
246
- if (this.dataItemsCount() > this.maxDataItems) {
247
- var firstDataItem = this.firstDataItem();
248
-
249
- firstDataItem.slideUp(400, function() {
250
- firstDataItem.remove();
251
- });
252
- }
253
-
254
- new_li.slideDown();
255
- }
256
- });
257
- // End ShortMessagesWidget class. }}}
258
-
259
- // Begin LineGraphWidget class. {{{
260
- var LineGraphWidget = new JS.Class(DashboardWidget, {
261
- initialize: function(options) {
262
- this.height = options.height;
263
- this.width = options.width;
264
- this.seriesColours = options.line_colours;
265
- this.x_axis = options.x_axis;
266
-
267
- options.type = 'line_graph';
268
- this.callSuper(options);
269
- },
270
-
271
- updateData: function() {
272
- if (!this.graph)
273
- {this.createGraph();}
274
-
275
- // This is needed so that the dataDiv property is accessible with jQuery.get() .
276
- var graph = this.graph;
277
-
278
- jQuery.get(this.dataURL(), function(data) {
279
- graph.series[0].data = data;
280
- graph.replot({resetAxes: true});
281
- }, 'json');
282
- },
283
-
284
- createGraph: function() {
285
- includeJavaScriptFile('/javascripts/jqplot-0.9.7/plugins/jqplot.dateAxisRenderer.min.js');
286
-
287
- var x_axis_format = {};
288
- if (this.x_axis === 'dates') {
289
- var x_axis_format = {
290
- renderer: jQuery.jqplot.DateAxisRenderer,
291
- tickOptions: {formatString: '%Y-%m-%d'}
292
- }
293
- }
294
-
295
- this.graph = jQuery.jqplot(this.dataDiv.attr('id'), [ [] ], {
296
- title: this.title,
297
- seriesColors: this.seriesColours,
298
- axes: {
299
- xaxis: x_axis_format
300
- }
301
- });
302
- }
303
- });
304
- // End LineGraphWidget class. }}}
305
-
1
+ // function includeJavaScriptFile(path) {{{
2
+ //
3
+ // Creates a JavaScript <script> element and appends it
4
+ // to the <head>.
5
+ //
6
+ function includeJavaScriptFile(path) {
7
+ // Ensure that we have a non-empty string.'
8
+ if ((typeof(path) != 'string') || (!path))
9
+ {return;}
10
+
11
+ // Ensure that we can find the <head>.
12
+ var head = jQuery('head');
13
+ if (!head)
14
+ {return;}
15
+
16
+ var element = document.createElement('script');
17
+ element.type = 'text/javascript';
18
+ element.src = path;
19
+
20
+ head.append(element);
21
+
22
+ return true;
23
+ } // }}}
24
+
25
+ // function buildTheDashboard() {{{
26
+ //
27
+ // Determines which dashboard widgets to build,
28
+ // builds them, and adds them to the DOM.
29
+ //
30
+ function buildTheDashboard() {
31
+ var parsed_widgets = jQuery.parseJSON(json_widgets);
32
+
33
+ jQuery.each(parsed_widgets, function(index, widget) {
34
+ if (widget.type == 'number')
35
+ {dashboard.addNumberWidget(widget);}
36
+ else if (widget.type == 'short_messages')
37
+ {dashboard.addShortMessagesWidget(widget);}
38
+ else if (widget.type == 'line_graph')
39
+ {dashboard.addLineGraphWidget(widget);}
40
+ });
41
+ } // }}}
42
+
43
+ function createUpdateTimerFor(widget) { // {{{
44
+ $(this).everyTime(widget.update_interval, widget.name, function() {
45
+ widget.updateData();
46
+ });
47
+ } // }}}
48
+
49
+ // Begin Dashboard class. {{{
50
+ var Dashboard = new JS.Class({
51
+ initialize: function(options) { // {{{
52
+ this.basePath = options.basePath;
53
+
54
+ this.numberWidgets = new Array();
55
+ this.shortMessagesWidgets = new Array();
56
+ this.lineGraphWidgets = new Array();
57
+
58
+ this.div = jQuery('#dashboard');
59
+ this.numberWidgetsDiv = this.div.find('.dashboard-numbers');
60
+ this.shortMessagesWidgetsDiv = this.div.find('.dashboard-short-messages');
61
+ this.lineGraphWidgetsDiv = this.div.find('.dashboard-line-graphs');
62
+ }, // }}}
63
+
64
+ addNumberWidget: function(options) { // {{{
65
+ var widget = new NumberWidget(options);
66
+ var templateDiv = this.numberWidgetsDiv.find('.widget-template');
67
+
68
+ var widgetDiv = templateDiv.clone(false)
69
+ .removeClass('widget-template')
70
+ .addClass('number-widget')
71
+ .attr('id', widget.divID())
72
+ .appendTo(this.numberWidgetsDiv);
73
+
74
+ // Set the new div's title.
75
+ widgetDiv.find('.widget-title').html(widget.title);
76
+
77
+ widget.setDataDivTo(widgetDiv.find('.widget-data'));
78
+ widget.updateData();
79
+
80
+ createUpdateTimerFor(widget);
81
+
82
+ this.numberWidgets.push(widget);
83
+
84
+ widgetDiv.show();
85
+ }, // }}}
86
+
87
+ addShortMessagesWidget: function(options) { // {{{
88
+ var widget = new ShortMessagesWidget(options);
89
+ var templateDiv = this.shortMessagesWidgetsDiv.find('.widget-template');
90
+
91
+ var widgetDiv = templateDiv.clone(false)
92
+ .removeClass('widget-template')
93
+ .addClass('short-messages-widget')
94
+ .attr('id', widget.divID())
95
+ .appendTo(this.shortMessagesWidgetsDiv);
96
+
97
+ // Set the new div's title.
98
+ widgetDiv.find('.widget-title').html(widget.title);
99
+
100
+ widget.setDataDivTo(widgetDiv.find('.widget-data'));
101
+ widget.updateData();
102
+
103
+ createUpdateTimerFor(widget);
104
+
105
+ this.shortMessagesWidgets.push(widget);
106
+
107
+ widgetDiv.show();
108
+ }, // }}}
109
+
110
+ addLineGraphWidget: function(options) { // {{{
111
+ var widget = new LineGraphWidget(options);
112
+ var templateDiv = this.lineGraphWidgetsDiv.find('.widget-template');
113
+
114
+ var widgetDiv = templateDiv.clone(false)
115
+ .removeClass('widget-template')
116
+ .addClass('line-graph-widget')
117
+ .attr('id', widget.divID())
118
+ .appendTo(this.lineGraphWidgetsDiv);
119
+
120
+ // Set the new div's title.
121
+ widgetDiv.find('.widget-title').html(widget.title);
122
+
123
+ var dataDiv = widgetDiv.find('.widget-data')
124
+ .attr('id', widget.divID() + '-data');
125
+
126
+ widget.setDataDivTo(dataDiv);
127
+
128
+ widget.updateData();
129
+
130
+ // I don't know why, but setting these properties when dataDiv
131
+ // is initialized fails to work. They work here, though...
132
+ dataDiv.css('height', widget.height)
133
+ dataDiv.css('width', widget.width)
134
+
135
+ createUpdateTimerFor(widget);
136
+
137
+ this.lineGraphWidgets.push(widget);
138
+
139
+ widgetDiv.show();
140
+ } // }}}
141
+ });
142
+ // End Dashboard class. }}}
143
+
144
+ // Begin DashboardWidget class. {{{
145
+ var DashboardWidget = new JS.Class({
146
+ initialize: function(options) {
147
+ this.type = options.type;
148
+ this.name = options.name;
149
+ this.title = options.title;
150
+ this.update_interval = options.update_interval;
151
+ },
152
+
153
+ dataURL: function() {
154
+ return dashboard.basePath + this.name;
155
+ },
156
+
157
+ divID: function() {
158
+ return this.name + '-' + this.type + '-widget';
159
+ },
160
+
161
+ setDataDivTo: function(element) {
162
+ this.dataDiv = element;
163
+ }
164
+ });
165
+ // End DashboardWidget class. }}}
166
+
167
+ // Begin NumberWidget class. {{{
168
+ var NumberWidget = new JS.Class(DashboardWidget, {
169
+ initialize: function(options) {
170
+ options['type'] = 'number';
171
+ this.callSuper(options);
172
+ },
173
+
174
+ updateData: function() {
175
+ if (!this.dataDiv)
176
+ {return false;}
177
+
178
+ // This is needed so that the dataDiv property is accessible with jQuery.get() .
179
+ var element = this.dataDiv;
180
+
181
+ jQuery.get(this.dataURL(), function(data) {
182
+ element.fadeOut(400, function() {
183
+ element.html(data);
184
+ element.fadeIn();
185
+ });
186
+ }, 'text');
187
+ }
188
+ });
189
+ // End NumberWidget class. }}}
190
+
191
+ // Begin ShortMessagesWidget class. {{{
192
+ var ShortMessagesWidget = new JS.Class(DashboardWidget, {
193
+ initialize: function(options) {
194
+ options['type'] = 'short_messages';
195
+ this.callSuper(options);
196
+ this.maxDataItems = 5;
197
+ },
198
+
199
+ firstDataItem: function() {
200
+ return this.dataDiv.find('li.widget-data-item:first');
201
+ },
202
+
203
+ dataItemsCount: function() {
204
+ return this.dataDiv.find('li.widget-data-item').length;
205
+ },
206
+
207
+ createDataItem: function() {
208
+ return this.dataDiv.find('li.widget-data-template')
209
+ .clone(false)
210
+ .removeClass('widget-data-template')
211
+ .addClass('widget-data-item');
212
+ },
213
+
214
+ updateData: function() {
215
+ if (!this.dataDiv)
216
+ {return false;}
217
+
218
+ var new_data = '';
219
+ var new_li = this.createDataItem();
220
+
221
+ // We use ajax() instead of get() because the "async" option must
222
+ // be false. If it isn't, we're unable to determine if data was
223
+ // obtained.
224
+ var get_result = jQuery.ajax({
225
+ url: this.dataURL(),
226
+ type: 'GET',
227
+ async: false,
228
+ cache: false,
229
+ dataType: 'text',
230
+ timeout: this.updateInterval,
231
+ success: function(data) {
232
+ new_li.html(data);
233
+ new_data = data;
234
+ }
235
+ });
236
+
237
+ // If no data was obtained, return. Otherwise, an empty list item
238
+ // will be shown.
239
+ if (new_data == '')
240
+ {return false;}
241
+
242
+ // Ensure that we don't show the same data twice in a row.
243
+ if (this.dataDiv.find('li:last-child').html() != new_li.html()) {
244
+ new_li.appendTo(this.dataDiv.find('ul'));
245
+
246
+ // Hide the first list item if we've reached the maximum number of
247
+ // list items to show in this widget.
248
+ if (this.dataItemsCount() > this.maxDataItems) {
249
+ var firstDataItem = this.firstDataItem();
250
+
251
+ firstDataItem.slideUp(400, function() {
252
+ firstDataItem.remove();
253
+ });
254
+ }
255
+
256
+ new_li.slideDown();
257
+ }
258
+ }
259
+ });
260
+ // End ShortMessagesWidget class. }}}
261
+
262
+ // Begin LineGraphWidget class. {{{
263
+ var LineGraphWidget = new JS.Class(DashboardWidget, {
264
+ initialize: function(options) {
265
+ this.height = options.height;
266
+ this.width = options.width;
267
+ this.seriesColours = options.line_colours;
268
+ this.x_axis = options.x_axis;
269
+
270
+ options.type = 'line_graph';
271
+ this.callSuper(options);
272
+ },
273
+
274
+ updateData: function() {
275
+ if (!this.graph)
276
+ {this.createGraph();}
277
+
278
+ // This is needed so that the dataDiv property is accessible with jQuery.get() .
279
+ var graph = this.graph;
280
+
281
+ jQuery.get(this.dataURL(), function(data) {
282
+ graph.series[0].data = data;
283
+ graph.replot({resetAxes: true});
284
+ }, 'json');
285
+ },
286
+
287
+ createGraph: function() {
288
+ includeJavaScriptFile('/javascripts/jqplot-0.9.7/plugins/jqplot.dateAxisRenderer.min.js');
289
+
290
+ var x_axis_format = {};
291
+ if (this.x_axis === 'dates') {
292
+ var x_axis_format = {
293
+ renderer: jQuery.jqplot.DateAxisRenderer,
294
+ tickOptions: {formatString: '%Y-%m-%d'}
295
+ }
296
+ }
297
+
298
+ this.graph = jQuery.jqplot(this.dataDiv.attr('id'), [ [] ], {
299
+ seriesColors: this.seriesColours,
300
+ axes: {
301
+ xaxis: x_axis_format
302
+ }
303
+ });
304
+ }
305
+ });
306
+ // End LineGraphWidget class. }}}
307
+
@@ -2,7 +2,7 @@ module ActsAsDashboard
2
2
  module InstanceMethods
3
3
  def show
4
4
  @json_widgets = dashboard_config.widgets.map {|w| w.attributes}.to_json
5
- @dashboard_path = '/' + self.class.to_s.underscore.sub('_controller', '').singularize + '/widgets/'
5
+ @dashboard_path = "#{ApplicationController.relative_url_root}/" + self.class.to_s.underscore.sub('_controller', '').singularize + '/widgets/'
6
6
 
7
7
  # @dashboard_css_path = File.join(File.dirname(__FILE__), 'public', 'stylesheets', 'dashboard.css')
8
8
  # @dashboard_css = File.open(@dashboard_css_path).read
@@ -239,19 +239,22 @@ var ShortMessagesWidget = new JS.Class(DashboardWidget, {
239
239
  if (new_data == '')
240
240
  {return false;}
241
241
 
242
- new_li.appendTo(this.dataDiv.find('ul'));
243
-
244
- // Hide the first list item if we've reached the maximum number of
245
- // list items to show in this widget.
246
- if (this.dataItemsCount() > this.maxDataItems) {
247
- var firstDataItem = this.firstDataItem();
242
+ // Ensure that we don't show the same data twice in a row.
243
+ if (this.dataDiv.find('li:last-child').html() != new_li.html()) {
244
+ new_li.appendTo(this.dataDiv.find('ul'));
245
+
246
+ // Hide the first list item if we've reached the maximum number of
247
+ // list items to show in this widget.
248
+ if (this.dataItemsCount() > this.maxDataItems) {
249
+ var firstDataItem = this.firstDataItem();
250
+
251
+ firstDataItem.slideUp(400, function() {
252
+ firstDataItem.remove();
253
+ });
254
+ }
248
255
 
249
- firstDataItem.slideUp(400, function() {
250
- firstDataItem.remove();
251
- });
256
+ new_li.slideDown();
252
257
  }
253
-
254
- new_li.slideDown();
255
258
  }
256
259
  });
257
260
  // End ShortMessagesWidget class. }}}
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acts_as_dashboard
3
3
  version: !ruby/object:Gem::Version
4
- hash: 23
4
+ hash: 21
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 3
9
- - 2
10
- version: 0.3.2
9
+ - 3
10
+ version: 0.3.3
11
11
  platform: ruby
12
12
  authors:
13
13
  - Nick Hoffman
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-08-04 00:00:00 -04:00
18
+ date: 2010-08-05 00:00:00 -04:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency