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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5c90e25e9bc6ff181667253af48ad1b36235425c
4
- data.tar.gz: 21dfcd7b48ed82e22056e8528d196962f6c93ae8
3
+ metadata.gz: fad9b5cc25344895ee783f55c630d30b1d445abb
4
+ data.tar.gz: 73520ca797c54e54e7b6ddb919448ced2814a873
5
5
  SHA512:
6
- metadata.gz: 54f037193337876ec1bc462903f3a05fd9960ef9cc94391772ba33bf754b53405786000a84928f0673d34a56c910ec855a632aceb13d6cd903fadcc81ed072a5
7
- data.tar.gz: 68e195848fd01c5349fa81d540dbf23b28adac42fc6210e71c1282f67a6bc2e29dfebbc33408d162edd73bf5c0908f6015a09249d5654f13058f727d20f8dbc8
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
@@ -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
@@ -1,5 +1,7 @@
1
1
  require 'redis'
2
2
  require 'rails/all'
3
+ require 'awrence'
4
+ require 'plissken'
3
5
  require 'entangled/version'
4
6
  require 'entangled/helpers'
5
7
  require 'entangled/model'
@@ -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 not persisted, it should not have
87
- # any channels
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
 
@@ -1,3 +1,3 @@
1
1
  module Entangled
2
- VERSION = "1.2.0"
2
+ VERSION = "1.4.0"
3
3
  end
@@ -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(JSON.stringify(this));
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(JSON.stringify(this));
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, false otherwise. This doesn't actually
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 !!this.id;
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.created_at).toBeDefined();
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.created_at).toBeDefined();
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.created_at).toBeDefined();
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.updated_at;
100
+ var oldUpdatedAt = list.updatedAt;
101
101
 
102
102
  list.$save(function() {
103
103
  expect(list.name).toBe('new name');
104
- expect(list.updated_at).not.toEqual(oldUpdatedAt);
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.updated_at;
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.updated_at).toBe(oldUpdatedAt);
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.updated_at;
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.updated_at).not.toEqual(oldUpdatedAt);
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.updated_at;
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.updated_at).toBe(oldUpdatedAt);
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 channel" do
147
- it 'does not have any channels since it is not persisted' do
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
@@ -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.2.0
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-12 00:00:00.000000000 Z
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: