entangled 1.2.0 → 1.4.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 +11 -4
- data/entangled.gemspec +6 -0
- data/lib/entangled.rb +2 -0
- data/lib/entangled/model.rb +7 -5
- data/lib/entangled/version.rb +1 -1
- data/spec/dummy/public/app/entangled/entangled.js +42 -4
- data/spec/dummy/public/test/services/entangled_test.js +87 -11
- data/spec/models/channels_spec.rb +11 -2
- data/spec/models/list_spec.rb +13 -0
- metadata +30 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fad9b5cc25344895ee783f55c630d30b1d445abb
|
4
|
+
data.tar.gz: 73520ca797c54e54e7b6ddb919448ced2814a873
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f50e3e90e995d47e4dc85b47eddfe44edf1aac69050571663afb69c1e1467210048b4607ddaec9b8abf5b9a595b6a88292b860b8a7019fa0a32fcaba220258a4
|
7
|
+
data.tar.gz: f8cfa4d6a52154aa26b6da60e7d0f149a38f6e5f2277f932487b3a1852b8894b93073ac48c745bcf1614f93eccfbc5fd0f1179c259fd3c98ee329b64951151e5
|
data/README.md
CHANGED
@@ -261,6 +261,13 @@ Pick if you want to use Entangled with plain JavaScript or with Angular:
|
|
261
261
|
- [entangled-js](https://github.com/dchacke/entangled-js)
|
262
262
|
- [entangled-angular](https://github.com/dchacke/entangled-angular)
|
263
263
|
|
264
|
+
## A Note On Cases
|
265
|
+
The case conventions differ in Ruby and JavaScript. `snake_case` is the standard in Ruby, whereas `camelCase` is the standard in JavaScript.
|
266
|
+
|
267
|
+
To comply with both standards, Entangled automatically converts attribute names to camel case before sending them from the server to the client to comply with JS conventions, and back to snake case before sending them from the client back to the server to comply with Ruby conventions.
|
268
|
+
|
269
|
+
All this means for you is that this enables you to use the conventional case for both environments. For example, a `sender_name` attribute on your model in Rails will turn into a `senderName` attribute in the browser, and vice versa. It would be weird to write camel case in Ruby.
|
270
|
+
|
264
271
|
## Planning Your Infrastructure
|
265
272
|
This gem is best used for Rails apps that serve as APIs only and are not concerned with rendering views, since Entangled controllers cannot render views. A front end separate from your Rails app is recommended, either in your Rails app's public directory, or a separate front end app altogether.
|
266
273
|
|
@@ -270,9 +277,12 @@ The gem relies heavily on convention over configuration and currently only works
|
|
270
277
|
## Development Priorities
|
271
278
|
The following features are to be implemented next:
|
272
279
|
|
280
|
+
- Parse tubesock message `m` if available and assign to params in other controller actions before yield
|
281
|
+
- Make broadcast method non-blocking using [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby); update Readme and repo description accordingly
|
282
|
+
- Add compatibility table for Ruby/Angular/JS versions
|
273
283
|
- Make prefix of create path `create_message` instead of `create_messages`
|
274
284
|
- Support `belongsTo` in front end
|
275
|
-
- Support `has_one` association in back end and front end
|
285
|
+
- Support `has_one` association in back end and front end and route helper for single resource
|
276
286
|
- Add offline capabilities
|
277
287
|
- Add authentication - with JWT?
|
278
288
|
- On Heroku, tasks are always in different order depending on which ones are checked off and not
|
@@ -282,10 +292,7 @@ The following features are to be implemented next:
|
|
282
292
|
- Contact Jessy to tweet about it!
|
283
293
|
- Handle errors gracefully (e.g. finding a non-existent id, etc, authorization error in the back end, timeouts, etc)
|
284
294
|
- Test controllers (see https://github.com/ngauthier/tubesock/issues/41)
|
285
|
-
- Freeze destroyed object
|
286
|
-
- Set `$persisted()` to false on a destroyed object
|
287
295
|
- Add `.destroyAll()` function to `Resources`
|
288
|
-
- Add support for plain JavaScript usage (without Angular) and add section about that to Readme
|
289
296
|
|
290
297
|
## Contributing
|
291
298
|
1. [Fork it](https://github.com/dchacke/entangled/fork) - you will notice that the repo comes with a back end and a front end part to test both parts of the gem
|
data/entangled.gemspec
CHANGED
@@ -31,4 +31,10 @@ Gem::Specification.new do |s|
|
|
31
31
|
s.add_dependency 'tubesock', '~> 0.2'
|
32
32
|
s.add_dependency 'rails', '~> 4.0'
|
33
33
|
s.add_dependency 'redis', '~> 3.2'
|
34
|
+
|
35
|
+
# To convert hashes from snake case to camel case
|
36
|
+
s.add_dependency 'awrence', '~> 0.1'
|
37
|
+
|
38
|
+
# To convert hashes from camel case to snake case
|
39
|
+
s.add_dependency 'plissken', '~> 0.2.0'
|
34
40
|
end
|
data/lib/entangled.rb
CHANGED
data/lib/entangled/model.rb
CHANGED
@@ -68,9 +68,12 @@ module Entangled
|
|
68
68
|
# JSON representation of the resource includes
|
69
69
|
# its errors. This is necessary so that errors
|
70
70
|
# are sent back to the client along with the
|
71
|
-
# resource on create and update
|
71
|
+
# resource on create and update. Furthermore,
|
72
|
+
# keys are converted to camel case, to comply
|
73
|
+
# with JavaScript conventions on the client
|
72
74
|
def as_json(options = nil)
|
73
|
-
super(options || attributes).merge(errors: errors).as_json
|
75
|
+
super(options || attributes).merge(errors: errors).as_json.
|
76
|
+
to_camelback_keys
|
74
77
|
end
|
75
78
|
|
76
79
|
# Build channels. Channels always at least include
|
@@ -83,9 +86,8 @@ module Entangled
|
|
83
86
|
def channels(tail = '')
|
84
87
|
channels = []
|
85
88
|
|
86
|
-
# If the record is
|
87
|
-
|
88
|
-
return channels unless persisted?
|
89
|
+
# If the record is new, it should not have any channels
|
90
|
+
return channels if new_record?
|
89
91
|
|
90
92
|
plural_name = self.class.name.underscore.pluralize
|
91
93
|
|
data/lib/entangled/version.rb
CHANGED
@@ -39,7 +39,7 @@ angular.module('entangled', [])
|
|
39
39
|
// Update
|
40
40
|
var socket = new WebSocket(this.webSocketUrl + '/' + this.id + '/update');
|
41
41
|
socket.onopen = function() {
|
42
|
-
socket.send(
|
42
|
+
socket.send(this.asSnakeJSON());
|
43
43
|
}.bind(this);
|
44
44
|
|
45
45
|
// Receive updated resource from server
|
@@ -71,7 +71,7 @@ angular.module('entangled', [])
|
|
71
71
|
|
72
72
|
// Send attributes to server
|
73
73
|
socket.onopen = function() {
|
74
|
-
socket.send(
|
74
|
+
socket.send(this.asSnakeJSON());
|
75
75
|
}.bind(this);
|
76
76
|
|
77
77
|
// Receive saved resource from server
|
@@ -133,6 +133,13 @@ angular.module('entangled', [])
|
|
133
133
|
this[key] = data.resource[key];
|
134
134
|
}
|
135
135
|
}
|
136
|
+
|
137
|
+
// Mark resource as destroyed
|
138
|
+
this.destroyed = true;
|
139
|
+
|
140
|
+
// Freeze resource so as to prevent future
|
141
|
+
// modifications
|
142
|
+
Object.freeze(this);
|
136
143
|
}
|
137
144
|
|
138
145
|
if (callback) callback(this);
|
@@ -140,7 +147,7 @@ angular.module('entangled', [])
|
|
140
147
|
};
|
141
148
|
|
142
149
|
// $valid() checks if any errors are attached to the object
|
143
|
-
// and return false if so,
|
150
|
+
// and return false if so, true otherwise. This doesn't actually
|
144
151
|
// invoke server side validations, so it should only be used
|
145
152
|
// after calling $save() to check if the record was successfully
|
146
153
|
// stored in the database
|
@@ -156,7 +163,38 @@ angular.module('entangled', [])
|
|
156
163
|
// $persisted() checks if the record was successfully stored
|
157
164
|
// in the back end's database
|
158
165
|
Resource.prototype.$persisted = function() {
|
159
|
-
return
|
166
|
+
return !(this.$newRecord() || this.$destroyed());
|
167
|
+
};
|
168
|
+
|
169
|
+
// $newRecord() checks if the record was just instantiated
|
170
|
+
Resource.prototype.$newRecord = function() {
|
171
|
+
return !this.id;
|
172
|
+
};
|
173
|
+
|
174
|
+
// $destroyed() checks if the record has been destroyed
|
175
|
+
Resource.prototype.$destroyed = function() {
|
176
|
+
return !!this.destroyed;
|
177
|
+
};
|
178
|
+
|
179
|
+
// asSnakeJSON returns a JSON object that looks just like the
|
180
|
+
// resource, except that the keys are snake case to comply
|
181
|
+
// with Ruby conventions once sent to the server
|
182
|
+
Resource.prototype.asSnakeJSON = function() {
|
183
|
+
var newKey,
|
184
|
+
that = this,
|
185
|
+
newObject = {};
|
186
|
+
|
187
|
+
Object.keys(this).forEach(function(key) {
|
188
|
+
if (that.hasOwnProperty(key)) {
|
189
|
+
newKey = key.match(/[A-Za-z][a-z]*/g).map(function(char) {
|
190
|
+
return char.toLowerCase();
|
191
|
+
}).join("_");
|
192
|
+
|
193
|
+
newObject[newKey] = that[key];
|
194
|
+
}
|
195
|
+
});
|
196
|
+
|
197
|
+
return JSON.stringify(newObject);
|
160
198
|
};
|
161
199
|
|
162
200
|
// Resources wraps all individual Resource objects
|
@@ -42,7 +42,7 @@ describe('Entangled', function() {
|
|
42
42
|
it('creates a list', function(done) {
|
43
43
|
List.create({ name: 'foo' }, function(list) {
|
44
44
|
expect(list.id).toBeDefined();
|
45
|
-
expect(list.
|
45
|
+
expect(list.createdAt).toBeDefined();
|
46
46
|
done();
|
47
47
|
});
|
48
48
|
});
|
@@ -62,7 +62,7 @@ describe('Entangled', function() {
|
|
62
62
|
List.create({ name: 'foo' }, function(list) {
|
63
63
|
List.find(list.id, function(list) {
|
64
64
|
expect(list.id).toBeDefined();
|
65
|
-
expect(list.
|
65
|
+
expect(list.createdAt).toBeDefined();
|
66
66
|
done();
|
67
67
|
});
|
68
68
|
});
|
@@ -76,7 +76,7 @@ describe('Entangled', function() {
|
|
76
76
|
|
77
77
|
list.$save(function() {
|
78
78
|
expect(list.id).toBeDefined();
|
79
|
-
expect(list.
|
79
|
+
expect(list.createdAt).toBeDefined();
|
80
80
|
done();
|
81
81
|
});
|
82
82
|
});
|
@@ -97,11 +97,11 @@ describe('Entangled', function() {
|
|
97
97
|
it('updates an existing list', function(done) {
|
98
98
|
List.create({ name: 'foo' }, function(list) {
|
99
99
|
list.name = 'new name';
|
100
|
-
var oldUpdatedAt = list.
|
100
|
+
var oldUpdatedAt = list.updatedAt;
|
101
101
|
|
102
102
|
list.$save(function() {
|
103
103
|
expect(list.name).toBe('new name');
|
104
|
-
expect(list.
|
104
|
+
expect(list.updatedAt).not.toEqual(oldUpdatedAt);
|
105
105
|
done();
|
106
106
|
});
|
107
107
|
});
|
@@ -113,12 +113,12 @@ describe('Entangled', function() {
|
|
113
113
|
// empty string, causing model validations
|
114
114
|
// in ActiveRecord to fail
|
115
115
|
list.name = '';
|
116
|
-
var oldUpdatedAt = list.
|
116
|
+
var oldUpdatedAt = list.updatedAt;
|
117
117
|
|
118
118
|
list.$save(function() {
|
119
119
|
// Assert that the list was not updated
|
120
120
|
// by the server
|
121
|
-
expect(list.
|
121
|
+
expect(list.updatedAt).toBe(oldUpdatedAt);
|
122
122
|
expect(list.errors.name.indexOf("can't be blank") > -1).toBeTruthy();
|
123
123
|
done();
|
124
124
|
});
|
@@ -130,11 +130,11 @@ describe('Entangled', function() {
|
|
130
130
|
describe('#$update', function() {
|
131
131
|
it('updates a list in place', function(done) {
|
132
132
|
List.create({ name: 'foo' }, function(list) {
|
133
|
-
var oldUpdatedAt = list.
|
133
|
+
var oldUpdatedAt = list.updatedAt;
|
134
134
|
|
135
135
|
list.$update({ name: 'new name' }, function() {
|
136
136
|
expect(list.name).toBe('new name');
|
137
|
-
expect(list.
|
137
|
+
expect(list.updatedAt).not.toEqual(oldUpdatedAt);
|
138
138
|
done();
|
139
139
|
});
|
140
140
|
});
|
@@ -142,7 +142,7 @@ describe('Entangled', function() {
|
|
142
142
|
|
143
143
|
it('receives validation messages', function(done) {
|
144
144
|
List.create({ name: 'foo' }, function(list) {
|
145
|
-
var oldUpdatedAt = list.
|
145
|
+
var oldUpdatedAt = list.updatedAt;
|
146
146
|
|
147
147
|
// Make invalid by setting the name to an
|
148
148
|
// empty string, causing model validations
|
@@ -150,7 +150,7 @@ describe('Entangled', function() {
|
|
150
150
|
list.$update({ name: '' }, function() {
|
151
151
|
// Assert that the list was not updated
|
152
152
|
// by the server
|
153
|
-
expect(list.
|
153
|
+
expect(list.updatedAt).toBe(oldUpdatedAt);
|
154
154
|
expect(list.errors.name.indexOf("can't be blank") > -1).toBeTruthy();
|
155
155
|
done();
|
156
156
|
});
|
@@ -172,6 +172,24 @@ describe('Entangled', function() {
|
|
172
172
|
});
|
173
173
|
});
|
174
174
|
});
|
175
|
+
|
176
|
+
it('marks the record as destroyed', function(done) {
|
177
|
+
List.create({ name: 'foo' }, function(list) {
|
178
|
+
list.$destroy(function() {
|
179
|
+
expect(list.destroyed).toBeTruthy();
|
180
|
+
done();
|
181
|
+
});
|
182
|
+
});
|
183
|
+
});
|
184
|
+
|
185
|
+
it('freezes the record', function(done) {
|
186
|
+
List.create({ name: 'foo' }, function(list) {
|
187
|
+
list.$destroy(function() {
|
188
|
+
expect(Object.isFrozen(list)).toBeTruthy();
|
189
|
+
done();
|
190
|
+
});
|
191
|
+
});
|
192
|
+
});
|
175
193
|
});
|
176
194
|
|
177
195
|
describe('#$valid', function() {
|
@@ -243,6 +261,64 @@ describe('Entangled', function() {
|
|
243
261
|
expect(list.$persisted()).not.toBeTruthy();
|
244
262
|
});
|
245
263
|
});
|
264
|
+
|
265
|
+
describe('destroyed record', function() {
|
266
|
+
it('is false', function() {
|
267
|
+
list.id = 1;
|
268
|
+
list.destroyed = true;
|
269
|
+
expect(list.$persisted()).not.toBeTruthy();
|
270
|
+
});
|
271
|
+
});
|
272
|
+
});
|
273
|
+
|
274
|
+
describe('#$newRecord', function() {
|
275
|
+
var list;
|
276
|
+
|
277
|
+
beforeEach(function() {
|
278
|
+
list = List.new();
|
279
|
+
});
|
280
|
+
|
281
|
+
describe('new record', function() {
|
282
|
+
it('is true', function() {
|
283
|
+
expect(list.$newRecord()).toBeTruthy();
|
284
|
+
});
|
285
|
+
});
|
286
|
+
|
287
|
+
describe('persisted record', function() {
|
288
|
+
it('is false', function() {
|
289
|
+
list.id = 1;
|
290
|
+
expect(list.$newRecord()).not.toBeTruthy();
|
291
|
+
});
|
292
|
+
});
|
293
|
+
});
|
294
|
+
|
295
|
+
describe('#$destroyed', function() {
|
296
|
+
var list;
|
297
|
+
|
298
|
+
beforeEach(function() {
|
299
|
+
list = List.new();
|
300
|
+
});
|
301
|
+
|
302
|
+
describe('destroyed record', function() {
|
303
|
+
it('is true', function() {
|
304
|
+
list.destroyed = true;
|
305
|
+
expect(list.$destroyed()).toBeTruthy();
|
306
|
+
});
|
307
|
+
});
|
308
|
+
|
309
|
+
describe('non-destroyed record', function() {
|
310
|
+
it('is false', function() {
|
311
|
+
expect(list.$destroyed()).not.toBeTruthy();
|
312
|
+
});
|
313
|
+
});
|
314
|
+
});
|
315
|
+
|
316
|
+
describe('#asSnakeJSON', function() {
|
317
|
+
it('returns the resource as JSON with snake case keys', function() {
|
318
|
+
var list = List.new();
|
319
|
+
list.fooBar = 'bar';
|
320
|
+
expect(JSON.parse(list.asSnakeJSON()).foo_bar).toEqual('bar');
|
321
|
+
});
|
246
322
|
});
|
247
323
|
|
248
324
|
describe('Associations', function() {
|
@@ -24,6 +24,9 @@ RSpec.describe 'Channels', type: :model do
|
|
24
24
|
# Child that's not persisted
|
25
25
|
let!(:fetus) { Child.new }
|
26
26
|
|
27
|
+
# Child that's been destroyed
|
28
|
+
let(:dead_body) { child.destroy }
|
29
|
+
|
27
30
|
describe "grandmother's channels" do
|
28
31
|
it 'has two channels' do
|
29
32
|
expect(grandmother.channels.size).to eq 2
|
@@ -143,9 +146,15 @@ RSpec.describe 'Channels', type: :model do
|
|
143
146
|
end
|
144
147
|
end
|
145
148
|
|
146
|
-
describe "fetus's
|
147
|
-
it 'does not have any channels since it is
|
149
|
+
describe "fetus's channels" do
|
150
|
+
it 'does not have any channels since it is a new record' do
|
148
151
|
expect(fetus.channels).to be_empty
|
149
152
|
end
|
150
153
|
end
|
154
|
+
|
155
|
+
describe "dead body's channels" do
|
156
|
+
it 'still has all channels even though it has been destroyed' do
|
157
|
+
expect(dead_body.channels.size).to eq 8
|
158
|
+
end
|
159
|
+
end
|
151
160
|
end
|
data/spec/models/list_spec.rb
CHANGED
@@ -119,10 +119,23 @@ RSpec.describe List, type: :model do
|
|
119
119
|
|
120
120
|
describe '#as_json' do
|
121
121
|
let(:list) { List.create }
|
122
|
+
let(:persisted_list) do
|
123
|
+
list = List.new
|
124
|
+
list.save(validate: false)
|
125
|
+
list
|
126
|
+
end
|
122
127
|
|
123
128
|
it 'includes errors' do
|
124
129
|
expect(list.as_json["errors"][:name]).to include "can't be blank"
|
125
130
|
end
|
131
|
+
|
132
|
+
it 'converts the attributes to camel case' do
|
133
|
+
expect(persisted_list.as_json["created_at"]).to be_nil
|
134
|
+
expect(persisted_list.as_json["createdAt"]).to be_present
|
135
|
+
|
136
|
+
expect(persisted_list.as_json["updated_at"]).to be_nil
|
137
|
+
expect(persisted_list.as_json["updatedAt"]).to be_present
|
138
|
+
end
|
126
139
|
end
|
127
140
|
end
|
128
141
|
|
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.4.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-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -164,6 +164,34 @@ dependencies:
|
|
164
164
|
- - "~>"
|
165
165
|
- !ruby/object:Gem::Version
|
166
166
|
version: '3.2'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: awrence
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - "~>"
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0.1'
|
174
|
+
type: :runtime
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - "~>"
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0.1'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: plissken
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - "~>"
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: 0.2.0
|
188
|
+
type: :runtime
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - "~>"
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: 0.2.0
|
167
195
|
description: Makes Rails real time through websockets as a gem in the backend and
|
168
196
|
as an Angular library in the front end.
|
169
197
|
email:
|