entangled 1.4.1 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +23 -5
- data/lib/entangled/controller.rb +160 -134
- data/lib/entangled/version.rb +1 -1
- data/spec/dummy/public/app/controllers/list.js +2 -2
- data/spec/dummy/public/app/controllers/lists.js +1 -1
- data/spec/dummy/public/app/entangled/entangled.js +104 -19
- data/spec/dummy/public/test/services/entangled_test.js +286 -34
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: da1eee63cbd14ff868400bd98f77d164b7625df0
|
4
|
+
data.tar.gz: 97a8f2215ebd469f203ad179d61f8beb9872aa63
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 69972bd90800fdbd7bf64c96a27b25eca95b633971116351c604d0049ec164a9c5d4696d5a9278f990ee7b5fccf47ff804d2f325aefd88272f2798cd3e863c58
|
7
|
+
data.tar.gz: 8cf42b1b3bf5f5a6bbc1009d87c4e657c429cd7392ea1acb7895f411fbb422c05f04de7e1998332358c99594b8bcd2b4f4d60e2c759f6f3b11a2af864ea7c69b
|
data/README.md
CHANGED
@@ -268,6 +268,13 @@ Pick if you want to use Entangled with plain JavaScript or with Angular:
|
|
268
268
|
- [entangled-js](https://github.com/dchacke/entangled-js)
|
269
269
|
- [entangled-angular](https://github.com/dchacke/entangled-angular)
|
270
270
|
|
271
|
+
The following versions are compatible:
|
272
|
+
|
273
|
+
| entangled.gem | entangled-js.js | entangled-angular.js |
|
274
|
+
|---------------|-----------------|----------------------|
|
275
|
+
| 1.4.1 | 1.3.1 | 1.3.1 |
|
276
|
+
| 1.4.1 | 1.3.0 | 1.3.0 |
|
277
|
+
|
271
278
|
## A Note On Cases
|
272
279
|
The case conventions differ in Ruby and JavaScript. `snake_case` is the standard in Ruby, whereas `camelCase` is the standard in JavaScript.
|
273
280
|
|
@@ -281,22 +288,33 @@ This gem is best used for Rails apps that serve as APIs only and are not concern
|
|
281
288
|
## Limitations
|
282
289
|
The gem relies heavily on convention over configuration and currently only works with restful style controllers as shown above. More features will be available soon. See the list of development priorities below.
|
283
290
|
|
291
|
+
## Debugging Websockets
|
292
|
+
To debug websockets from your terminal, you can use curl. For example, to do a handshake with a socket at `/messages` (a route you need to have set up), you can do the following:
|
293
|
+
|
294
|
+
```shell
|
295
|
+
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Host: echo.websocket.org" -H "Origin: http://localhost:3000" http://localhost:3000/messages
|
296
|
+
```
|
297
|
+
|
298
|
+
More information [here](http://www.thenerdary.net/post/24889968081/debugging-websockets-with-curl).
|
299
|
+
|
284
300
|
## Development Priorities
|
285
301
|
The following features are to be implemented next:
|
286
302
|
|
287
|
-
-
|
288
|
-
-
|
303
|
+
- Throw error if parent id not set on child when trying to fetch parent
|
304
|
+
- Support multiple `hasMany` and `belongsTo` associations in front end
|
305
|
+
- Put up example application (the todo list?)
|
289
306
|
- Make prefix of create path `create_message` instead of `create_messages`
|
290
|
-
- Support `belongsTo` in front end
|
291
307
|
- Support `has_one` association in back end and front end and route helper for single resource
|
292
|
-
-
|
308
|
+
- Support scoping in back end
|
309
|
+
- Add support for scopes and where clauses in front end once back end can do scopes à la [Spyke](https://github.com/balvig/spyke)
|
310
|
+
- Display results of interactions to client immediately without going through the server; add server interactions to queue and constantly dequeue; if result from server conflicts with client state, update client accordingly
|
311
|
+
- Add offline capabilities, i.e. only dequeue server interactions once internet connection established
|
293
312
|
- Add authentication - with JWT?
|
294
313
|
- On Heroku, tasks are always in different order depending on which ones are checked off and not
|
295
314
|
- Add `$onChange` function to objects - or could a simple $watch and $watchCollection suffice?
|
296
315
|
- Add diagram on how it works to Readme
|
297
316
|
- GNU instead of MIT? Or something else? How to switch?
|
298
317
|
- Contact Jessy to tweet about it!
|
299
|
-
- Handle errors gracefully (e.g. finding a non-existent id, etc, authorization error in the back end, timeouts, etc)
|
300
318
|
- Test controllers (see https://github.com/ngauthier/tubesock/issues/41)
|
301
319
|
- Add `.destroyAll()` function to `Resources`
|
302
320
|
|
data/lib/entangled/controller.rb
CHANGED
@@ -61,165 +61,170 @@ module Entangled
|
|
61
61
|
def broadcast(&block)
|
62
62
|
# Use hijack to handle sockets
|
63
63
|
hijack do |tubesock|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
64
|
+
begin
|
65
|
+
# Assuming restful controllers, the behavior of
|
66
|
+
# this method has to change depending on the action
|
67
|
+
# it's being used in
|
68
|
+
case action_name
|
69
|
+
|
70
|
+
# If the controller action is 'index', a collection
|
71
|
+
# of records should be broadcast
|
72
|
+
when 'index'
|
73
|
+
execute_action(tubesock, block)
|
74
|
+
|
75
|
+
# The following code will run if an instance
|
76
|
+
# variable with the plural resource name has been
|
77
|
+
# assigned in yield. For example, if a
|
78
|
+
# TacosController's index action looked something
|
79
|
+
# like this:
|
80
|
+
|
81
|
+
# def index
|
82
|
+
# broadcast do
|
83
|
+
# @tacos = Taco.all
|
84
|
+
# end
|
85
|
+
# end
|
86
|
+
|
87
|
+
# ...then @tacos will be broadcast to all connected
|
88
|
+
# clients. The variable name, in this example,
|
89
|
+
# has to be "@tacos"
|
90
|
+
if collection
|
91
|
+
redis_thread = Thread.new do
|
92
|
+
redis.subscribe channel do |on|
|
93
|
+
# Broadcast messages to all connected clients
|
94
|
+
on.message do |channel, message|
|
95
|
+
tubesock.send_data message
|
96
|
+
end
|
97
|
+
|
98
|
+
# Send message to whoever just subscribed
|
99
|
+
tubesock.send_data({
|
100
|
+
resources: collection
|
101
|
+
}.to_json)
|
102
|
+
|
103
|
+
close_db_connection
|
95
104
|
end
|
105
|
+
end
|
96
106
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
}.to_json)
|
101
|
-
|
102
|
-
close_db_connection
|
107
|
+
# When client disconnects, kill the thread
|
108
|
+
tubesock.onclose do
|
109
|
+
redis_thread.kill
|
103
110
|
end
|
104
111
|
end
|
105
112
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
113
|
+
# If the controller's action name is 'show', a single record
|
114
|
+
# should be broadcast
|
115
|
+
when 'show'
|
116
|
+
execute_action(tubesock, block)
|
117
|
+
|
118
|
+
# The following code will run if an instance variable
|
119
|
+
# with the singular resource name has been assigned in
|
120
|
+
# yield. For example, if a TacosController's show action
|
121
|
+
# looked something like this:
|
122
|
+
|
123
|
+
# def show
|
124
|
+
# broadcast do
|
125
|
+
# @taco = Taco.find(params[:id])
|
126
|
+
# end
|
127
|
+
# end
|
111
128
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
# ...then @taco will be broadcast to all connected clients.
|
129
|
-
# The variable name, in this example, has to be "@taco"
|
130
|
-
if member
|
131
|
-
redis_thread = Thread.new do
|
132
|
-
redis.subscribe channel do |on|
|
133
|
-
# Broadcast messages to all connected clients
|
134
|
-
on.message do |channel, message|
|
135
|
-
tubesock.send_data message
|
129
|
+
# ...then @taco will be broadcast to all connected clients.
|
130
|
+
# The variable name, in this example, has to be "@taco"
|
131
|
+
if member
|
132
|
+
redis_thread = Thread.new do
|
133
|
+
redis.subscribe channel do |on|
|
134
|
+
# Broadcast messages to all connected clients
|
135
|
+
on.message do |channel, message|
|
136
|
+
tubesock.send_data message
|
137
|
+
end
|
138
|
+
|
139
|
+
# Send message to whoever just subscribed
|
140
|
+
tubesock.send_data({
|
141
|
+
resource: member
|
142
|
+
}.to_json)
|
143
|
+
|
144
|
+
close_db_connection
|
136
145
|
end
|
146
|
+
end
|
137
147
|
|
138
|
-
|
148
|
+
# When client disconnects, kill the thread
|
149
|
+
tubesock.onclose do
|
150
|
+
redis_thread.kill
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# If the controller's action name is 'create', a record should be
|
155
|
+
# created. Before yielding, the params hash has to be prepared
|
156
|
+
# with attributes sent to the socket. The actual publishing
|
157
|
+
# happens in the model's callback
|
158
|
+
when 'create'
|
159
|
+
tubesock.onmessage do |m|
|
160
|
+
set_resource_params(m)
|
161
|
+
execute_action(tubesock, block)
|
162
|
+
|
163
|
+
# Send resource that was just created back to client. The resource
|
164
|
+
# on the client will be overridden with this one. This is important
|
165
|
+
# so that the id, created_at and updated_at and possibly other
|
166
|
+
# attributes arrive on the client
|
167
|
+
if member
|
139
168
|
tubesock.send_data({
|
140
169
|
resource: member
|
141
170
|
}.to_json)
|
142
|
-
|
143
|
-
close_db_connection
|
144
171
|
end
|
145
|
-
end
|
146
172
|
|
147
|
-
|
148
|
-
tubesock.onclose do
|
149
|
-
redis_thread.kill
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
|
-
# If the controller's action name is 'create', a record should be
|
154
|
-
# created. Before yielding, the params hash has to be prepared
|
155
|
-
# with attributes sent to the socket. The actual publishing
|
156
|
-
# happens in the model's callback
|
157
|
-
when 'create'
|
158
|
-
tubesock.onmessage do |m|
|
159
|
-
set_resource_params(m)
|
160
|
-
yield
|
161
|
-
|
162
|
-
# Send resource that was just created back to client. The resource
|
163
|
-
# on the client will be overridden with this one. This is important
|
164
|
-
# so that the id, created_at and updated_at and possibly other
|
165
|
-
# attributes arrive on the client
|
166
|
-
if member
|
167
|
-
tubesock.send_data({
|
168
|
-
resource: member
|
169
|
-
}.to_json)
|
173
|
+
close_db_connection
|
170
174
|
end
|
171
175
|
|
172
|
-
|
173
|
-
|
176
|
+
# If the controller's action name is 'update', a record should be
|
177
|
+
# updated. Before yielding, the params hash has to be prepared
|
178
|
+
# with attributes sent to the socket
|
179
|
+
when 'update'
|
180
|
+
tubesock.onmessage do |m|
|
181
|
+
set_resource_params(m)
|
182
|
+
execute_action(tubesock, block)
|
183
|
+
|
184
|
+
# Send resource that was just updated back to client. The resource
|
185
|
+
# on the client will be overridden with this one. This is important
|
186
|
+
# so that the new updated_at and possibly other attributes arrive
|
187
|
+
# on the client
|
188
|
+
if member
|
189
|
+
tubesock.send_data({
|
190
|
+
resource: member
|
191
|
+
}.to_json)
|
192
|
+
end
|
174
193
|
|
175
|
-
|
176
|
-
# updated. Before yielding, the params hash has to be prepared
|
177
|
-
# with attributes sent to the socket
|
178
|
-
when 'update'
|
179
|
-
tubesock.onmessage do |m|
|
180
|
-
set_resource_params(m)
|
181
|
-
yield
|
182
|
-
|
183
|
-
# Send resource that was just updated back to client. The resource
|
184
|
-
# on the client will be overridden with this one. This is important
|
185
|
-
# so that the new updated_at and possibly other attributes arrive
|
186
|
-
# on the client
|
187
|
-
if member
|
188
|
-
tubesock.send_data({
|
189
|
-
resource: member
|
190
|
-
}.to_json)
|
194
|
+
close_db_connection
|
191
195
|
end
|
192
196
|
|
193
|
-
|
194
|
-
|
197
|
+
when 'destroy'
|
198
|
+
tubesock.onmessage do |m|
|
199
|
+
execute_action(tubesock, block)
|
195
200
|
|
196
|
-
|
197
|
-
|
198
|
-
|
201
|
+
# Send resource that was just destroyed back to client
|
202
|
+
if member
|
203
|
+
tubesock.send_data({
|
204
|
+
resource: member
|
205
|
+
}.to_json)
|
206
|
+
end
|
199
207
|
|
200
|
-
|
201
|
-
if member
|
202
|
-
tubesock.send_data({
|
203
|
-
resource: member
|
204
|
-
}.to_json)
|
208
|
+
close_db_connection
|
205
209
|
end
|
206
210
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
# If message was sent, attach to params (rescue exception if
|
216
|
-
# message not valid JSON or message not present)
|
217
|
-
params.merge!(JSON.parse(m)) rescue nil
|
211
|
+
# For every other controller action, simply wrap whatever is
|
212
|
+
# yielded in the tubesock block to execute it in the context
|
213
|
+
# of the socket. Other custom actions can be added through this
|
214
|
+
else
|
215
|
+
tubesock.onmessage do |m|
|
216
|
+
# If message was sent, attach to params (rescue exception if
|
217
|
+
# message not valid JSON or message not present)
|
218
|
+
params.merge!(JSON.parse(m)) rescue nil
|
218
219
|
|
219
|
-
|
220
|
+
execute_action(tubesock, block)
|
220
221
|
|
221
|
-
|
222
|
+
close_db_connection
|
223
|
+
end
|
222
224
|
end
|
225
|
+
rescue Exception => e
|
226
|
+
Rails.logger.error e
|
227
|
+
close_db_connection
|
223
228
|
end
|
224
229
|
end
|
225
230
|
end
|
@@ -227,6 +232,27 @@ module Entangled
|
|
227
232
|
def set_resource_params(message_from_socket)
|
228
233
|
params[resource_name.to_sym] = JSON.parse(message_from_socket)
|
229
234
|
end
|
235
|
+
|
236
|
+
# Run the actual controller action that is passed as
|
237
|
+
# a block to the broadcast method and catch any exceptions
|
238
|
+
# as needed
|
239
|
+
def execute_action(tubesock, block)
|
240
|
+
begin
|
241
|
+
# Execute block
|
242
|
+
block.call
|
243
|
+
rescue Exception => e
|
244
|
+
# Log the error
|
245
|
+
Rails.logger.error e.message
|
246
|
+
|
247
|
+
# Print stack strace
|
248
|
+
puts e.backtrace
|
249
|
+
|
250
|
+
# Send error message to client
|
251
|
+
tubesock.send_data({
|
252
|
+
error: e.message
|
253
|
+
}.to_json)
|
254
|
+
end
|
255
|
+
end
|
230
256
|
end
|
231
257
|
|
232
258
|
def self.included(receiver)
|
data/lib/entangled/version.rb
CHANGED
@@ -3,10 +3,10 @@
|
|
3
3
|
angular.module('entangledTest')
|
4
4
|
|
5
5
|
.controller('ListCtrl', function($scope, $routeParams, List) {
|
6
|
-
List.find($routeParams.id, function(list) {
|
6
|
+
List.find($routeParams.id, function(err, list) {
|
7
7
|
$scope.$apply(function() {
|
8
8
|
$scope.list = list;
|
9
|
-
$scope.list.items().all(function(items) {
|
9
|
+
$scope.list.items().all(function(err, items) {
|
10
10
|
$scope.$apply(function() {
|
11
11
|
$scope.items = items;
|
12
12
|
});
|
@@ -7,10 +7,10 @@ angular.module('entangled', [])
|
|
7
7
|
// Every response coming from the server will be wrapped
|
8
8
|
// in a Resource constructor to represent a CRUD-able
|
9
9
|
// resource that can be saved and destroyed using the
|
10
|
-
// methods $save
|
10
|
+
// methods $save, $destroy, and others. A Resource also
|
11
11
|
// stores the socket's URL it was retrieved from so it
|
12
12
|
// can be reused for other requests.
|
13
|
-
function Resource(params, webSocketUrl, hasMany) {
|
13
|
+
function Resource(params, webSocketUrl, hasMany, belongsTo) {
|
14
14
|
// Assign properties
|
15
15
|
for (var key in params) {
|
16
16
|
// Skip inherent object properties
|
@@ -20,14 +20,50 @@ angular.module('entangled', [])
|
|
20
20
|
}
|
21
21
|
|
22
22
|
// Assign socket URL
|
23
|
-
this.
|
23
|
+
this.socket = webSocketUrl;
|
24
|
+
this.webSocketUrl = function() {
|
25
|
+
// If a parent is associated, the websocket URL looks something
|
26
|
+
// like ws://localhost:3000/parents/:parentId/children. The
|
27
|
+
// following replaces :parentId with the actual parent's id
|
28
|
+
if (belongsTo) {
|
29
|
+
var parentIdPlaceholder = ':' + belongsTo + 'Id';
|
30
|
+
var parentId = this[belongsTo + 'Id'];
|
31
|
+
var newWebSocketUrl = this.socket.replace(parentIdPlaceholder, parentId);
|
32
|
+
return newWebSocketUrl;
|
33
|
+
} else {
|
34
|
+
// If no parent is associated, just return the plain URL
|
35
|
+
return this.socket;
|
36
|
+
}
|
37
|
+
};
|
24
38
|
|
25
|
-
// Assign
|
39
|
+
// Assign parent relationship
|
26
40
|
if (hasMany) {
|
27
41
|
this[hasMany] = function() {
|
28
|
-
return new Entangled(this.webSocketUrl + '/' + this.id + '/' + hasMany);
|
42
|
+
return new Entangled(this.webSocketUrl() + '/' + this.id + '/' + hasMany);
|
29
43
|
};
|
30
44
|
}
|
45
|
+
|
46
|
+
// Assign child relationship
|
47
|
+
if (belongsTo) {
|
48
|
+
this[belongsTo] = function(callback) {
|
49
|
+
// Infer socket URL to parent from child's socket URL
|
50
|
+
var socketElements = this.webSocketUrl().split('/');
|
51
|
+
var parentSocket = socketElements.slice(socketElements.length - 6, 4).join('/');
|
52
|
+
|
53
|
+
// Instantiate new parent and get its id
|
54
|
+
var Parent = new Entangled(parentSocket);
|
55
|
+
var parentId = this[belongsTo + 'Id'];
|
56
|
+
|
57
|
+
// Find parent and pass it to callback
|
58
|
+
Parent.find(parentId, function(err, parent) {
|
59
|
+
if (err) {
|
60
|
+
callback(err)
|
61
|
+
} else {
|
62
|
+
callback(null, parent);
|
63
|
+
}
|
64
|
+
});
|
65
|
+
}.bind(this);
|
66
|
+
}
|
31
67
|
};
|
32
68
|
|
33
69
|
// $save() will send a request to the server
|
@@ -37,7 +73,7 @@ angular.module('entangled', [])
|
|
37
73
|
Resource.prototype.$save = function(callback) {
|
38
74
|
if (this.id) {
|
39
75
|
// Update
|
40
|
-
var socket = new WebSocket(this.webSocketUrl + '/' + this.id + '/update');
|
76
|
+
var socket = new WebSocket(this.webSocketUrl() + '/' + this.id + '/update');
|
41
77
|
socket.onopen = function() {
|
42
78
|
socket.send(this.asSnakeJSON());
|
43
79
|
}.bind(this);
|
@@ -47,6 +83,14 @@ angular.module('entangled', [])
|
|
47
83
|
if (event.data) {
|
48
84
|
var data = JSON.parse(event.data);
|
49
85
|
|
86
|
+
if (data.error) {
|
87
|
+
if (callback) {
|
88
|
+
callback(new Error(data.error));
|
89
|
+
}
|
90
|
+
|
91
|
+
return;
|
92
|
+
}
|
93
|
+
|
50
94
|
// Assign/override new data (such as updated_at, etc)
|
51
95
|
if (data.resource) {
|
52
96
|
for (key in data.resource) {
|
@@ -57,17 +101,19 @@ angular.module('entangled', [])
|
|
57
101
|
|
58
102
|
// Assign has many association. The association
|
59
103
|
// can only be available to a persisted record
|
60
|
-
this[this.hasMany] = new Entangled(this.webSocketUrl + '/' + this.id + '/' + this.hasMany);
|
104
|
+
this[this.hasMany] = new Entangled(this.webSocketUrl() + '/' + this.id + '/' + this.hasMany);
|
61
105
|
|
62
106
|
// Pass 'this' to callback for create
|
63
107
|
// function so this the create function
|
64
108
|
// can pass the created resource to its
|
65
109
|
// own callback; not needed for $save per se
|
66
|
-
if (callback)
|
110
|
+
if (callback) {
|
111
|
+
callback(null, this);
|
112
|
+
}
|
67
113
|
}.bind(this);
|
68
114
|
} else {
|
69
115
|
// Create
|
70
|
-
var socket = new WebSocket(this.webSocketUrl + '/create');
|
116
|
+
var socket = new WebSocket(this.webSocketUrl() + '/create');
|
71
117
|
|
72
118
|
// Send attributes to server
|
73
119
|
socket.onopen = function() {
|
@@ -79,6 +125,14 @@ angular.module('entangled', [])
|
|
79
125
|
if (event.data) {
|
80
126
|
var data = JSON.parse(event.data);
|
81
127
|
|
128
|
+
if (data.error) {
|
129
|
+
if (callback) {
|
130
|
+
callback(new Error(data.error));
|
131
|
+
}
|
132
|
+
|
133
|
+
return;
|
134
|
+
}
|
135
|
+
|
82
136
|
// Assign/override new data (such as id, created_at,
|
83
137
|
// updated_at, etc)
|
84
138
|
if (data.resource) {
|
@@ -92,7 +146,9 @@ angular.module('entangled', [])
|
|
92
146
|
// function so this the create function
|
93
147
|
// can pass the created resource to its
|
94
148
|
// own callback; not needed for $save per se
|
95
|
-
if (callback)
|
149
|
+
if (callback) {
|
150
|
+
callback(null, this)
|
151
|
+
};
|
96
152
|
}.bind(this);
|
97
153
|
}
|
98
154
|
};
|
@@ -114,7 +170,8 @@ angular.module('entangled', [])
|
|
114
170
|
// $destroy() will send a request to the server to
|
115
171
|
// destroy an existing record.
|
116
172
|
Resource.prototype.$destroy = function(callback) {
|
117
|
-
var socket = new WebSocket(this.webSocketUrl + '/' + this.id + '/destroy');
|
173
|
+
var socket = new WebSocket(this.webSocketUrl() + '/' + this.id + '/destroy');
|
174
|
+
// var socket = new WebSocket(this.webSocketUrl() + '/' + '2343345' + '/destroy');
|
118
175
|
|
119
176
|
socket.onopen = function() {
|
120
177
|
// It's fine to send an empty message since the
|
@@ -127,6 +184,14 @@ angular.module('entangled', [])
|
|
127
184
|
if (event.data) {
|
128
185
|
var data = JSON.parse(event.data);
|
129
186
|
|
187
|
+
if (data.error) {
|
188
|
+
if (callback) {
|
189
|
+
callback(new Error(data.error));
|
190
|
+
}
|
191
|
+
|
192
|
+
return;
|
193
|
+
}
|
194
|
+
|
130
195
|
// Assign/override new data
|
131
196
|
if (data.resource) {
|
132
197
|
for (key in data.resource) {
|
@@ -142,7 +207,9 @@ angular.module('entangled', [])
|
|
142
207
|
Object.freeze(this);
|
143
208
|
}
|
144
209
|
|
145
|
-
if (callback)
|
210
|
+
if (callback) {
|
211
|
+
callback(null, this);
|
212
|
+
}
|
146
213
|
}.bind(this);
|
147
214
|
};
|
148
215
|
|
@@ -223,16 +290,22 @@ angular.module('entangled', [])
|
|
223
290
|
this.webSocketUrl = webSocketUrl;
|
224
291
|
};
|
225
292
|
|
226
|
-
// hasMany() adds the appropriate association
|
227
|
-
// sets up websockets accordingly
|
228
|
-
Entangled.prototype.hasMany = function(
|
229
|
-
this.hasMany =
|
293
|
+
// hasMany() adds the appropriate parent association
|
294
|
+
// and sets up websockets accordingly
|
295
|
+
Entangled.prototype.hasMany = function(resourcesName) {
|
296
|
+
this.hasMany = resourcesName;
|
297
|
+
};
|
298
|
+
|
299
|
+
// belongsTo() adds the appropriate child association
|
300
|
+
// and sets up websockets accordingly
|
301
|
+
Entangled.prototype.belongsTo = function(resourceName) {
|
302
|
+
this.belongsTo = resourceName;
|
230
303
|
};
|
231
304
|
|
232
305
|
// Function to instantiate a new Resource, optionally
|
233
306
|
// with given parameters
|
234
307
|
Entangled.prototype.new = function(params) {
|
235
|
-
return new Resource(params, this.webSocketUrl, this.hasMany);
|
308
|
+
return new Resource(params, this.webSocketUrl, this.hasMany, this.belongsTo);
|
236
309
|
};
|
237
310
|
|
238
311
|
// Retrieve all Resources from the root of the socket's
|
@@ -246,6 +319,12 @@ angular.module('entangled', [])
|
|
246
319
|
// Convert message to JSON
|
247
320
|
var data = JSON.parse(event.data);
|
248
321
|
|
322
|
+
if (data.error) {
|
323
|
+
callback(new Error(data.error));
|
324
|
+
|
325
|
+
return;
|
326
|
+
}
|
327
|
+
|
249
328
|
// If the collection of Resources was sent
|
250
329
|
if (data.resources) {
|
251
330
|
// Store retrieved Resources in property
|
@@ -288,7 +367,7 @@ angular.module('entangled', [])
|
|
288
367
|
|
289
368
|
// Run the callback and pass in the
|
290
369
|
// resulting collection
|
291
|
-
callback(this.resources.all);
|
370
|
+
callback(null, this.resources.all);
|
292
371
|
}.bind(this);
|
293
372
|
};
|
294
373
|
|
@@ -309,6 +388,12 @@ angular.module('entangled', [])
|
|
309
388
|
// Parse message and convert to JSON
|
310
389
|
var data = JSON.parse(event.data);
|
311
390
|
|
391
|
+
if (data.error) {
|
392
|
+
callback(new Error(data.error));
|
393
|
+
|
394
|
+
return;
|
395
|
+
}
|
396
|
+
|
312
397
|
if (data.resource && !data.action) {
|
313
398
|
// If the Resource was sent from the server,
|
314
399
|
// store it
|
@@ -330,7 +415,7 @@ angular.module('entangled', [])
|
|
330
415
|
}
|
331
416
|
|
332
417
|
// Run callback with retrieved Resource
|
333
|
-
callback(this.resource);
|
418
|
+
callback(null, this.resource);
|
334
419
|
}.bind(this);
|
335
420
|
};
|
336
421
|
|
@@ -5,7 +5,8 @@ describe('Entangled', function() {
|
|
5
5
|
|
6
6
|
var $injector,
|
7
7
|
Entangled,
|
8
|
-
List
|
8
|
+
List,
|
9
|
+
Item;
|
9
10
|
|
10
11
|
beforeEach(inject(function() {
|
11
12
|
$injector = angular.injector(['entangled']);
|
@@ -13,6 +14,9 @@ describe('Entangled', function() {
|
|
13
14
|
|
14
15
|
List = new Entangled('ws://localhost:3000/lists');
|
15
16
|
List.hasMany('items');
|
17
|
+
|
18
|
+
Item = new Entangled('ws://localhost:3000/lists/:listId/items');
|
19
|
+
Item.belongsTo('list');
|
16
20
|
}));
|
17
21
|
|
18
22
|
describe('constructor', function() {
|
@@ -31,8 +35,13 @@ describe('Entangled', function() {
|
|
31
35
|
|
32
36
|
describe('.all', function() {
|
33
37
|
it('fetches all lists', function(done) {
|
34
|
-
List.all(function(lists) {
|
38
|
+
List.all(function(err, lists) {
|
39
|
+
// Assert that err is null
|
40
|
+
expect(err).toEqual(null);
|
41
|
+
|
42
|
+
// Assert that lists are set
|
35
43
|
expect(lists).toEqual(jasmine.any(Array));
|
44
|
+
|
36
45
|
done();
|
37
46
|
});
|
38
47
|
});
|
@@ -40,9 +49,14 @@ describe('Entangled', function() {
|
|
40
49
|
|
41
50
|
describe('.create', function() {
|
42
51
|
it('creates a list', function(done) {
|
43
|
-
List.create({ name: 'foo' }, function(list) {
|
52
|
+
List.create({ name: 'foo' }, function(err, list) {
|
53
|
+
// Assert that err is null
|
54
|
+
expect(err).toEqual(null);
|
55
|
+
|
56
|
+
// Assert that list was created successfully
|
44
57
|
expect(list.id).toBeDefined();
|
45
58
|
expect(list.createdAt).toBeDefined();
|
59
|
+
|
46
60
|
done();
|
47
61
|
});
|
48
62
|
});
|
@@ -50,8 +64,13 @@ describe('Entangled', function() {
|
|
50
64
|
it('receives validation messages', function(done) {
|
51
65
|
// Leave out name, causing model validations
|
52
66
|
// in ActiveRecord to fail
|
53
|
-
List.create({}, function(list) {
|
67
|
+
List.create({}, function(err, list) {
|
68
|
+
// Assert that err is null
|
69
|
+
expect(err).toEqual(null);
|
70
|
+
|
71
|
+
// Assert that validation errors are attached
|
54
72
|
expect(list.errors.name.indexOf("can't be blank") > -1).toBeTruthy();
|
73
|
+
|
55
74
|
done();
|
56
75
|
});
|
57
76
|
});
|
@@ -59,14 +78,35 @@ describe('Entangled', function() {
|
|
59
78
|
|
60
79
|
describe('.find', function() {
|
61
80
|
it('finds a list', function(done) {
|
62
|
-
List.create({ name: 'foo' }, function(list) {
|
63
|
-
|
81
|
+
List.create({ name: 'foo' }, function(err, list) {
|
82
|
+
// Assert that err is null
|
83
|
+
expect(err).toEqual(null);
|
84
|
+
|
85
|
+
List.find(list.id, function(err, list) {
|
86
|
+
// Assert that err is null
|
87
|
+
expect(err).toEqual(null);
|
88
|
+
|
89
|
+
// Assert that list was found
|
64
90
|
expect(list.id).toBeDefined();
|
65
91
|
expect(list.createdAt).toBeDefined();
|
92
|
+
|
66
93
|
done();
|
67
94
|
});
|
68
95
|
});
|
69
96
|
});
|
97
|
+
|
98
|
+
it('receives an error when looking for a list that does not exist', function(done) {
|
99
|
+
List.find('not an id', function(err, list) {
|
100
|
+
// Assert that error is set
|
101
|
+
expect(err).toEqual(jasmine.any(Error));
|
102
|
+
expect(err.message).toEqual("Couldn't find List with 'id'=not an id");
|
103
|
+
|
104
|
+
// Assert that no second parameter was passed to callback
|
105
|
+
expect(list).not.toBeDefined();
|
106
|
+
|
107
|
+
done();
|
108
|
+
})
|
109
|
+
});
|
70
110
|
});
|
71
111
|
|
72
112
|
describe('#$save', function() {
|
@@ -74,9 +114,14 @@ describe('Entangled', function() {
|
|
74
114
|
it('saves a new list', function(done) {
|
75
115
|
var list = List.new({ name: 'foo' });
|
76
116
|
|
77
|
-
list.$save(function() {
|
117
|
+
list.$save(function(err, list) {
|
118
|
+
// Assert that err is null
|
119
|
+
expect(err).toEqual(null);
|
120
|
+
|
121
|
+
// Assert that list was persisted
|
78
122
|
expect(list.id).toBeDefined();
|
79
123
|
expect(list.createdAt).toBeDefined();
|
124
|
+
|
80
125
|
done();
|
81
126
|
});
|
82
127
|
});
|
@@ -86,8 +131,13 @@ describe('Entangled', function() {
|
|
86
131
|
// in ActiveRecord to fail
|
87
132
|
var list = List.new();
|
88
133
|
|
89
|
-
list.$save(function() {
|
134
|
+
list.$save(function(err, list) {
|
135
|
+
// Assert that err is null
|
136
|
+
expect(err).toEqual(null);
|
137
|
+
|
138
|
+
// Assert that validation errors are attached
|
90
139
|
expect(list.errors.name.indexOf("can't be blank") > -1).toBeTruthy();
|
140
|
+
|
91
141
|
done();
|
92
142
|
});
|
93
143
|
});
|
@@ -95,31 +145,72 @@ describe('Entangled', function() {
|
|
95
145
|
|
96
146
|
describe('existing record', function() {
|
97
147
|
it('updates an existing list', function(done) {
|
98
|
-
List.create({ name: 'foo' }, function(list) {
|
148
|
+
List.create({ name: 'foo' }, function(err, list) {
|
149
|
+
// Assert that err is null
|
150
|
+
expect(err).toEqual(null);
|
151
|
+
|
152
|
+
// Prepare for update
|
99
153
|
list.name = 'new name';
|
154
|
+
|
155
|
+
// Remember old updatedAt to compare once saved
|
100
156
|
var oldUpdatedAt = list.updatedAt;
|
101
157
|
|
102
|
-
|
158
|
+
// Save
|
159
|
+
list.$save(function(err, list) {
|
160
|
+
// Assert that err is null
|
161
|
+
expect(err).toEqual(null);
|
162
|
+
|
163
|
+
// Assert that list was updated successfully
|
103
164
|
expect(list.name).toBe('new name');
|
104
165
|
expect(list.updatedAt).not.toEqual(oldUpdatedAt);
|
166
|
+
|
105
167
|
done();
|
106
168
|
});
|
107
169
|
});
|
108
170
|
});
|
109
171
|
|
110
172
|
it('receives validation messages', function(done) {
|
111
|
-
List.create({ name: 'foo' }, function(list) {
|
173
|
+
List.create({ name: 'foo' }, function(err, list) {
|
174
|
+
// Assert that err is null
|
175
|
+
expect(err).toEqual(null);
|
176
|
+
|
112
177
|
// Make invalid by setting the name to an
|
113
178
|
// empty string, causing model validations
|
114
179
|
// in ActiveRecord to fail
|
115
180
|
list.name = '';
|
116
181
|
var oldUpdatedAt = list.updatedAt;
|
117
182
|
|
118
|
-
list.$save(function() {
|
183
|
+
list.$save(function(err, list) {
|
184
|
+
// Assert that err is null
|
185
|
+
expect(err).toEqual(null);
|
186
|
+
|
119
187
|
// Assert that the list was not updated
|
120
188
|
// by the server
|
121
189
|
expect(list.updatedAt).toBe(oldUpdatedAt);
|
122
190
|
expect(list.errors.name.indexOf("can't be blank") > -1).toBeTruthy();
|
191
|
+
|
192
|
+
done();
|
193
|
+
});
|
194
|
+
});
|
195
|
+
});
|
196
|
+
|
197
|
+
it('receives an error when trying to save a record without being able to find it', function(done) {
|
198
|
+
List.create({ name: 'foo' }, function(err, list) {
|
199
|
+
// Assert that err is null
|
200
|
+
expect(err).toEqual(null);
|
201
|
+
|
202
|
+
// Prepare for update
|
203
|
+
list.id = 'not an id';
|
204
|
+
|
205
|
+
// Save
|
206
|
+
list.$save(function(err, list) {
|
207
|
+
// Assert that error is set
|
208
|
+
expect(err).toEqual(jasmine.any(Error));
|
209
|
+
expect(err.message).toEqual("Couldn't find List with 'id'=not an id");
|
210
|
+
|
211
|
+
// Assert that no second parameter was passed to callback
|
212
|
+
expect(list).not.toBeDefined();
|
213
|
+
|
123
214
|
done();
|
124
215
|
});
|
125
216
|
});
|
@@ -129,29 +220,69 @@ describe('Entangled', function() {
|
|
129
220
|
|
130
221
|
describe('#$update', function() {
|
131
222
|
it('updates a list in place', function(done) {
|
132
|
-
List.create({ name: 'foo' }, function(list) {
|
223
|
+
List.create({ name: 'foo' }, function(err, list) {
|
224
|
+
// Assert that err is null
|
225
|
+
expect(err).toEqual(null);
|
226
|
+
|
227
|
+
// Remember old updatedAt to compare once updated
|
133
228
|
var oldUpdatedAt = list.updatedAt;
|
134
229
|
|
135
|
-
|
230
|
+
// Update
|
231
|
+
list.$update({ name: 'new name' }, function(err, list) {
|
232
|
+
// Assert that err is null
|
233
|
+
expect(err).toEqual(null);
|
234
|
+
|
235
|
+
// Assert that list was updated successfully
|
136
236
|
expect(list.name).toBe('new name');
|
137
237
|
expect(list.updatedAt).not.toEqual(oldUpdatedAt);
|
238
|
+
|
138
239
|
done();
|
139
240
|
});
|
140
241
|
});
|
141
242
|
});
|
142
243
|
|
143
244
|
it('receives validation messages', function(done) {
|
144
|
-
List.create({ name: 'foo' }, function(list) {
|
245
|
+
List.create({ name: 'foo' }, function(err, list) {
|
246
|
+
// Assert that err is null
|
247
|
+
expect(err).toEqual(null);
|
248
|
+
|
249
|
+
// Remember old updatedAt to compare once updated
|
145
250
|
var oldUpdatedAt = list.updatedAt;
|
146
251
|
|
147
252
|
// Make invalid by setting the name to an
|
148
253
|
// empty string, causing model validations
|
149
254
|
// in ActiveRecord to fail
|
150
|
-
list.$update({ name: '' }, function() {
|
255
|
+
list.$update({ name: '' }, function(err, list) {
|
256
|
+
// Assert that err is null
|
257
|
+
expect(err).toEqual(null);
|
258
|
+
|
151
259
|
// Assert that the list was not updated
|
152
260
|
// by the server
|
153
261
|
expect(list.updatedAt).toBe(oldUpdatedAt);
|
154
262
|
expect(list.errors.name.indexOf("can't be blank") > -1).toBeTruthy();
|
263
|
+
|
264
|
+
done();
|
265
|
+
});
|
266
|
+
});
|
267
|
+
});
|
268
|
+
|
269
|
+
it('receives an error when trying to save a record without being able to find it', function(done) {
|
270
|
+
List.create({ name: 'foo' }, function(err, list) {
|
271
|
+
// Assert that err is null
|
272
|
+
expect(err).toEqual(null);
|
273
|
+
|
274
|
+
// Prepare for update
|
275
|
+
// list.id = 'not an id';
|
276
|
+
|
277
|
+
// Save
|
278
|
+
list.$update({ id: 'not an id' }, function(err, list) {
|
279
|
+
// Assert that error is set
|
280
|
+
expect(err).toEqual(jasmine.any(Error));
|
281
|
+
expect(err.message).toEqual("Couldn't find List with 'id'=not an id");
|
282
|
+
|
283
|
+
// Assert that no second parameter was passed to callback
|
284
|
+
expect(list).not.toBeDefined();
|
285
|
+
|
155
286
|
done();
|
156
287
|
});
|
157
288
|
});
|
@@ -160,13 +291,25 @@ describe('Entangled', function() {
|
|
160
291
|
|
161
292
|
describe('#$destroy', function() {
|
162
293
|
it('destroys a list', function(done) {
|
163
|
-
List.create({ name: 'foo' }, function(list) {
|
164
|
-
|
294
|
+
List.create({ name: 'foo' }, function(err, list) {
|
295
|
+
// Assert that err is null
|
296
|
+
expect(err).toEqual(null);
|
297
|
+
|
298
|
+
list.$destroy(function(err, list) {
|
299
|
+
// Assert that no error happened
|
300
|
+
expect(err).toEqual(null);
|
301
|
+
|
165
302
|
// If successfully destroyed, it won't be
|
166
303
|
// included in the collection of all records
|
167
304
|
// anymore
|
168
|
-
List.all(function(lists) {
|
305
|
+
List.all(function(err, lists) {
|
306
|
+
// Assert that no error happened
|
307
|
+
expect(err).toEqual(null);
|
308
|
+
|
309
|
+
// Assert that list not included in lists
|
310
|
+
// anymore
|
169
311
|
expect(lists.indexOf(list)).toBe(-1);
|
312
|
+
|
170
313
|
done();
|
171
314
|
});
|
172
315
|
});
|
@@ -174,22 +317,59 @@ describe('Entangled', function() {
|
|
174
317
|
});
|
175
318
|
|
176
319
|
it('marks the record as destroyed', function(done) {
|
177
|
-
List.create({ name: 'foo' }, function(list) {
|
178
|
-
|
320
|
+
List.create({ name: 'foo' }, function(err, list) {
|
321
|
+
// Assert that err is null
|
322
|
+
expect(err).toEqual(null);
|
323
|
+
|
324
|
+
list.$destroy(function(err, list) {
|
325
|
+
// Assert that no error happened
|
326
|
+
expect(err).toEqual(null);
|
327
|
+
|
328
|
+
// Asser that list is destroyed
|
179
329
|
expect(list.destroyed).toBeTruthy();
|
330
|
+
|
180
331
|
done();
|
181
332
|
});
|
182
333
|
});
|
183
334
|
});
|
184
335
|
|
185
336
|
it('freezes the record', function(done) {
|
186
|
-
List.create({ name: 'foo' }, function(list) {
|
187
|
-
|
337
|
+
List.create({ name: 'foo' }, function(err, list) {
|
338
|
+
// Assert that err is null
|
339
|
+
expect(err).toEqual(null);
|
340
|
+
|
341
|
+
list.$destroy(function(err, list) {
|
342
|
+
// Assert that no error happened
|
343
|
+
expect(err).toEqual(null);
|
344
|
+
|
345
|
+
// Assert that list is frozen
|
188
346
|
expect(Object.isFrozen(list)).toBeTruthy();
|
347
|
+
|
189
348
|
done();
|
190
349
|
});
|
191
350
|
});
|
192
351
|
});
|
352
|
+
|
353
|
+
it('has an error when an exception happens in the back end', function(done) {
|
354
|
+
// Try to destroy list that's not in the database,
|
355
|
+
// thereby triggering an exception in the back end,
|
356
|
+
// which should trigger an exception in the front end
|
357
|
+
var list = List.new({ id: 1234 });
|
358
|
+
|
359
|
+
list.$destroy(function(err, noList) {
|
360
|
+
// Assert that error is defined and has the right message
|
361
|
+
expect(err).toEqual(jasmine.any(Error));
|
362
|
+
expect(err.message).toEqual("Couldn't find List with 'id'=1234");
|
363
|
+
|
364
|
+
// Assert that list was not frozen
|
365
|
+
expect(Object.isFrozen(list)).not.toBeTruthy();
|
366
|
+
|
367
|
+
// Assert that no second parameter was sent to callback
|
368
|
+
expect(noList).not.toBeDefined();
|
369
|
+
|
370
|
+
done();
|
371
|
+
});
|
372
|
+
});
|
193
373
|
});
|
194
374
|
|
195
375
|
describe('#$valid', function() {
|
@@ -322,17 +502,89 @@ describe('Entangled', function() {
|
|
322
502
|
});
|
323
503
|
|
324
504
|
describe('Associations', function() {
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
505
|
+
describe('List', function() {
|
506
|
+
it('has many items', function(done) {
|
507
|
+
List.create({ name: 'foo' }, function(err, list) {
|
508
|
+
// Assert that err is null
|
509
|
+
expect(err).toEqual(null);
|
510
|
+
|
511
|
+
// Assert that relationship defined
|
512
|
+
expect(list.items).toBeDefined();
|
513
|
+
|
514
|
+
// Assert that relationship is also
|
515
|
+
// an instance of Entangled, meaning
|
516
|
+
// in turn that all class and instance
|
517
|
+
// methods are available on it
|
518
|
+
expect(list.items().constructor.name).toBe('Entangled');
|
519
|
+
|
520
|
+
done();
|
521
|
+
});
|
522
|
+
});
|
523
|
+
});
|
524
|
+
|
525
|
+
describe('Item', function() {
|
526
|
+
it('belongs to a list', function(done) {
|
527
|
+
// Create parent list
|
528
|
+
List.create({ name: 'foo' }, function(err, list) {
|
529
|
+
// Assert that err is null
|
530
|
+
expect(err).toEqual(null);
|
531
|
+
|
532
|
+
// Create child item
|
533
|
+
Item.create({ name: 'foo', listId: list.id }, function(err, item) {
|
534
|
+
// Assert that err is null
|
535
|
+
expect(err).toEqual(null);
|
536
|
+
|
537
|
+
// Assert that creation successful
|
538
|
+
expect(item.$persisted()).toBeTruthy();
|
539
|
+
|
540
|
+
// Assert parent relationship
|
541
|
+
// expect(item.list).toBe(list);
|
542
|
+
var originalList = list;
|
543
|
+
|
544
|
+
item.list(function(err, list) {
|
545
|
+
// Assert that err is null
|
546
|
+
expect(err).toEqual(null);
|
547
|
+
|
548
|
+
// Assert that the original list and the parent
|
549
|
+
// are the same
|
550
|
+
expect(originalList.id).toBe(list.id);
|
551
|
+
|
552
|
+
done();
|
553
|
+
});
|
554
|
+
});
|
555
|
+
});
|
556
|
+
});
|
557
|
+
|
558
|
+
it('receives an error when trying to find a parent that does not exist', function(done) {
|
559
|
+
// Create parent list
|
560
|
+
List.create({ name: 'foo' }, function(err, list) {
|
561
|
+
// Assert that err is null
|
562
|
+
expect(err).toEqual(null);
|
563
|
+
|
564
|
+
// Create child item
|
565
|
+
Item.create({ name: 'foo', listId: list.id }, function(err, item) {
|
566
|
+
// Assert that err is null
|
567
|
+
expect(err).toEqual(null);
|
568
|
+
|
569
|
+
// Assert that creation successful
|
570
|
+
expect(item.$persisted()).toBeTruthy();
|
571
|
+
|
572
|
+
// Change parent id to a non-existent id
|
573
|
+
item.listId = 'not an id';
|
574
|
+
|
575
|
+
item.list(function(err, list) {
|
576
|
+
// Assert that err is set
|
577
|
+
expect(err).toEqual(jasmine.any(Error));
|
578
|
+
expect(err.message).toEqual("Couldn't find List with 'id'=not an id");
|
579
|
+
|
580
|
+
// Assert that no second parameter was passed
|
581
|
+
// to the callback
|
582
|
+
expect(list).not.toBeDefined();
|
583
|
+
|
584
|
+
done();
|
585
|
+
});
|
586
|
+
});
|
587
|
+
});
|
336
588
|
});
|
337
589
|
});
|
338
590
|
});
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: entangled
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dennis Charles Hackethal
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-04-
|
11
|
+
date: 2015-04-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|