entangled 0.0.25 → 0.0.26

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: 8ca5362f5aca57e992be81a4856c54006173ff27
4
- data.tar.gz: 63181710cbfdb8c0b984a19e684e9d39a519ffaa
3
+ metadata.gz: 7687c95a8fad6d954ca7b9754d6ad17a40de5919
4
+ data.tar.gz: 37a20749306f1c7dac1a6f86bc92e1974b54306f
5
5
  SHA512:
6
- metadata.gz: ea9d8bac58de5cd03d45fc35b253f1a124baf29b6dce187039b149d1151d777365d5453c660952ac3d7103fec5843f2e7f643bcf1028b4e62bddba7e4b36acda
7
- data.tar.gz: 84ab4fa51ccbf7eb5058a3ac780bd7552b00c099684ef38997eab065994a59b37bde85a95d06158c56194f5643500234f32db24ba0261fe800e4fc36a0a50ad5
6
+ metadata.gz: cc6c25b58eb6e18956a9571e88f3933ee96a1b191f440ef3a895ac8bd4bf27cc0fd2bf81bde259584bb7079ce8ffa24b0d58e38c8e63fdb9b49a6bbb6e0a380b
7
+ data.tar.gz: ded5776561605f68d9a20b839645a4b1388b3a6209812db939c35287787f0208942a2fc09f19599840b8c00a235140edffb8ff5fb530bc9b759fc585f63a0d91
data/README.md CHANGED
@@ -6,7 +6,7 @@ Real time is important. Users have come to expect real time behavior from every
6
6
 
7
7
  Entangled stores and syncs data from ActiveRecord instantly across every device. It is a layer behind your models and controllers that pushes updates to all connected clients in real time. It is cross-browser compatible and offers real time validations.
8
8
 
9
- Currently, Entangled runs on Rails 4.2 and Ruby 2.0 in the back end, and Angular 1.3 in the front end.
9
+ Currently, Entangled runs on Rails 4.2 and Ruby 2.0. In the front end, libraries are available in [plain JavaScript](https://github.com/dchacke/entangled-js) and for [Angular](https://github.com/dchacke/entangled-angular).
10
10
 
11
11
  ## Installation
12
12
  Add this line to your application's Gemfile:
@@ -100,6 +100,18 @@ entangle only: :create
100
100
  entangle only: [:create, :update]
101
101
  ```
102
102
 
103
+ Calling `entangled` creates the following channels (sticking with the example of a `Message` model):
104
+
105
+ ```ruby
106
+ # For the collection
107
+ "/messages"
108
+
109
+ # For a member, e.g. /messages/1
110
+ "/messages/:id"
111
+ ```
112
+
113
+ `:id` being the record's id, just as with routes.
114
+
103
115
  ### Controllers
104
116
  Your controllers will be a little more lightweight than in a standard restful Rails app. A restful-style controller is expected and should look like this:
105
117
 
@@ -152,6 +164,7 @@ Note the following:
152
164
  - The `show`, `create`, `update`, and `destroy` actions will expect an instance variable with the singular name of your controller (e.g. `@message` in a `MessagesController`)
153
165
  - The instance variables are sent to clients as stringified JSON
154
166
  - Strong parameters are expected
167
+ - The path to your controllers' index action has to match the model's channel for the collection, and the path to your controller's show action has to match the model's channel for a single member (which it will automatically if you stay RESTful)
155
168
 
156
169
  ### Server
157
170
 
@@ -161,160 +174,14 @@ Remember to run Redis whenever you run your server:
161
174
  $ redis-server
162
175
  ```
163
176
 
164
- Otherwise the channels won't work.
177
+ Redis is needed to subscribe and publish to the channels that are created by Entangled internally to communicate over websockets.
165
178
 
166
179
  If you store your Redis instance in `$redis` or `REDIS` (e.g. in an initializer), Entangled will use that assigned instance so that you can configure Redis just like you're used to. Otherwise, Entangled will instantiate Redis itself and use its default settings.
167
180
 
168
- ## The Client
169
- You will need to configure your client to create Websockets and understand incoming requests on those sockets. In order to use the helper methods for the front end provided by the Entangled Angular library, you must use Angular in your front end. The use of Angular as counterpart of this gem is highly recommended, since its inherent two way data binding complements the real time functionality of this gem nicely.
170
-
171
- ### Installation
172
- You can either download or reference the file `entangled.js` from this repository, or simply install it with Bower:
173
-
174
- ```shell
175
- $ bower install entangled
176
- ```
177
-
178
- Then include it in your HTML.
179
-
180
- Lastly, add the Entangled module as a dependency to your Angular app:
181
-
182
- ```javascript
183
- angular.module('appName', ['entangled']);
184
- ```
185
-
186
- ### Usage
187
- Entangled is best used within Angular services. For example, consider a `Message` service for a chat app:
188
-
189
- ```javascript
190
- app.factory('Message', function(Entangled) {
191
- return new Entangled('ws://localhost:3000/messages');
192
- });
193
- ```
194
-
195
- In the above example, first we inject Entangled into our service, then instantiate a new Entangled object and return it. The Entangled object takes one argument when instantiated: the URL of your resource's index action (in this case, `/messages`). Note that the socket URL looks just like a standard restful URL with http, except that the protocol part has been switched with `ws` to use the websocket protocol. Also note that you need to use `wss` instead if you want to use SSL.
196
-
197
- The Entangled service comes with these functions:
198
-
199
- - `new(params)`
200
- - `create(params, callback)`
201
- - `find(id, callback)`
202
- - `all(callback)`
203
-
204
- ...and the following functions on returned objects:
205
-
206
- - `$save(callback)`
207
- - `$update(params, callback)`
208
- - `$destroy(callback)`
209
-
210
- They're just like class and instance methods in Active Record.
211
-
212
- In your controller, you could then inject that `Message` service and use it like so:
213
-
214
- ```javascript
215
- // To instantiate a blank message, e.g. for a form;
216
- // You can optionally pass in an object to new() to
217
- // set some default values
218
- $scope.message = Message.new();
219
-
220
- // To instantiate and save a message in one go
221
- Message.create({ body: 'text' }, function(message) {
222
- $scope.$apply(function() {
223
- $scope.message = message;
224
- });
225
- });
226
-
227
- // To retrieve a specific message from the server
228
- // with id 1 and subscribe to its channel
229
- Message.find(1, function(message) {
230
- $scope.$apply(function() {
231
- $scope.message = message;
232
- });
233
- });
234
-
235
- // To retrieve all messages from the server and
236
- // subscribe to the collection's channel
237
- Message.all(function(messages) {
238
- $scope.$apply(function() {
239
- $scope.messages = messages;
240
- });
241
- });
242
-
243
- // To store a newly instantiated or update an existing message.
244
- // If saved successfully, $scope.message is updated in place
245
- // with the attributes id, created_at and updated_at
246
- $scope.message.body = 'new body';
247
- $scope.message.$save(function() {
248
- // Do stuff after save
249
- });
250
-
251
- // To update a newly instantiated or existing message in place.
252
- // If updated successfully, $scope.message is updated in place
253
- // with the attributes id, created_at and updated_at
254
- $scope.message.$update({ body: 'new body' }, function() {
255
- // Do stuff after update
256
- });
257
-
258
- // To destroy a message
259
- $scope.message.$destroy(function() {
260
- // Do stuff after destroy
261
- });
262
- ```
263
-
264
- All functions above will interact with your server's controllers in real time. Your scope variables will always reflect your server's most current data.
265
-
266
- #### Validations
267
- Objects from the Entangled service automatically receive ActiveRecord's error messages from your model when you `$save()`. An additional property called `errors` containing the error messages is available, formatted the same way you're used to from calling `.errors` on a model in Rails.
268
-
269
- For example, consider the following scenario:
270
-
271
- ```ruby
272
- # Message model (Rails)
273
- validates :body, presence: true
274
- ```
275
-
276
- ```javascript
277
- // Controller (Angular)
278
- $scope.message.$save(function() {
279
- console.log($scope.message.errors);
280
- // => { body: ["can't be blank"] }
281
- });
282
- ```
283
-
284
- You could then display these error messages to your users.
285
-
286
- To check if a resource is valid, you can use `$valid()` and `$invalid()`. Both functions return booleans. For example:
287
-
288
- ```javascript
289
- $scope.message.$save(function() {
290
- // Check if record has no errors
291
- if ($scope.message.$valid()) { // similar to ActiveRecord's .valid?
292
- alert('Yay!');
293
- }
294
-
295
- // Check if record errors
296
- if ($scope.message.$invalid()) { // similar to ActiveRecord's .invalid?
297
- alert('Nay!');
298
- }
299
- });
300
- ```
301
-
302
- Note that `$valid()` and `$invalid()` should only be used after $saving a resource, i.e. in the callback of `$save`, since they don't actually invoke server side validations. They only check if a resource contains errors.
303
-
304
- #### Persistence
305
- Just as with ActiveRecord's `persisted?` method, you can use `$persisted()` on an object to check if it was successfully stored in the database.
306
-
307
- ```javascript
308
- $scope.message.$persisted();
309
- // => true or false
310
- ```
311
-
312
- #### Associations
181
+ ### Associations
313
182
  What if you want to only fetch and subscribe to children that belong to a specific parent? Or maybe you want to create a child in your front end and assign it to a specific parent?
314
183
 
315
- Entangled currently supports one `belongs_to` association per model.
316
-
317
- For example, imagine the following Parent > Children relationship in your models:
184
+ Imagine the following Parent > Children relationship in your models:
318
185
 
319
186
  ```ruby
320
187
  class Parent < ActiveRecord::Base
@@ -332,7 +199,25 @@ class Child < ActiveRecord::Base
332
199
  end
333
200
  ```
334
201
 
335
- To reflect this in your front end, you just need to add three things to your app:
202
+ Entangled takes note of every `belongs_to` association and creates two additional channels for each `belongs_to` association in the child model:
203
+
204
+ ```ruby
205
+ "/parents/:parent_id/children"
206
+ "/parents/:parent_id/children/:id"
207
+ ```
208
+
209
+ So in total, the `Child` model will have all of the following channels:
210
+
211
+ ```ruby
212
+ "/children"
213
+ "/children/:id"
214
+ "/parents/:parent_id/children"
215
+ "/parents/:parent_id/children/:id"
216
+ ```
217
+
218
+ Channels are deeply nested for child/parent/grandparent etc associations. There is no limit. To get a list of all available channels on a record, you can call the method `channels` on any entangled instance.
219
+
220
+ To reflect associations in your front end, you just need to add three things to your app:
336
221
 
337
222
  - Nest your routes so that they resemble the parent/child relationship:
338
223
 
@@ -358,7 +243,9 @@ class ChildrenController < ApplicationController
358
243
  # Create child of specific parent
359
244
  def create
360
245
  broadcast do
361
- @child = Parent.find(params[:parent_id]).children.create(child_params)
246
+ @child = Child.new(child_params)
247
+ @child.parent_id = params[:parent_id]
248
+ @child.save
362
249
  end
363
250
  end
364
251
 
@@ -366,56 +253,24 @@ class ChildrenController < ApplicationController
366
253
  end
367
254
  ```
368
255
 
369
- - Lastly, inform your Angular parent service about the association:
370
-
371
- ```javascript
372
- app.factory('Parent', function(Entangled) {
373
- // Instantiate Entangled service
374
- var Parent = new Entangled('ws://localhost:3000/parents');
375
-
376
- // Set up association
377
- Parent.hasMany('children');
378
-
379
- return Parent;
380
- });
381
- ```
382
-
383
- This makes a `children()` function available on your parent records on which you can chain all other functions to fetch/manipulate data:
384
-
385
- ```javascript
386
- Parent.find(1, function(parent) {
387
- parent.children().all(function(children) {
388
- // children here all belong to parent with id 1
389
- });
390
-
391
- parent.children().find(1, function(child) {
392
- // child has id 1 and belongs to parent with id 1
393
- });
394
-
395
- parent.children().create({ foo: 'bar' }, function(child) {
396
- // child has been persisted and associated with parent
397
- });
256
+ Check out the JavaScript guides to implement associations on the client.
398
257
 
399
- // etc
400
- });
401
- ```
402
-
403
- This is the way to go if you want to fetch records that only belong to a certain record, or create records that should belong to a parent record. In short, it is ideal to scope records to parent records.
258
+ ## The Client
259
+ Pick if you want to use Entangled with plain JavaScript or with Angular:
404
260
 
405
- Naturally, all nested records are also synced in real time across all connected clients.
261
+ - [entangled-js](https://github.com/dchacke/entangled-js)
262
+ - [entangled-angular](https://github.com/dchacke/entangled-angular)
406
263
 
407
264
  ## Planning Your Infrastructure
408
265
  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.
409
266
 
410
267
  ## Limitations
411
- 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, such as associations, authentication, and more.
268
+ 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.
412
269
 
413
270
  ## Development Priorities
414
271
  The following features are to be implemented next:
415
272
 
416
- - Allow for more than one level of nesting of `#channels` in `Entangled::Model`
417
273
  - Support `belongsTo` in front end
418
- - Support deeply nested `belongs_to`, e.g. `Parent > Child > Grandchild`
419
274
  - Support `has_one` association in back end and front end
420
275
  - Add offline capabilities
421
276
  - Add authentication - with JWT?
@@ -77,40 +77,38 @@ module Entangled
77
77
  # a collection channel, i.e. /tacos, and a member
78
78
  # channel, i.e. /tacos/1, for direct access.
79
79
  #
80
- # If the model belongs_to other models, two nested
81
- # channels are added for each belongs_to association.
82
- # E.g., if child belongs_to parent, the two channels
83
- # that are added are parents/1/children, and
84
- # parents/1/children/1, leaving a total of four channels
85
- def channels
80
+ # If the model belongs_to other models, nested channels
81
+ # are created for all parents, grand parents, etc
82
+ # recursively
83
+ def channels(tail = '')
86
84
  channels = []
87
85
  plural_name = self.class.name.underscore.pluralize
88
86
 
89
- # Add collection's channel
90
- channels << "/#{plural_name}"
91
-
92
- # Add member's channel
93
- channels << "/#{plural_name}/#{to_param}"
87
+ # Add collection channel for child only. If the tails
88
+ # is not empty, the function is being called recursively
89
+ # for one of the parents, for which only member channels
90
+ # are needed
91
+ if tail.empty?
92
+ collection_channel = "/#{plural_name}" + tail
93
+ channels << collection_channel
94
+ end
94
95
 
95
- # Find parent names from belongs_to associations
96
- parents = self.class.reflect_on_all_associations(:belongs_to)
96
+ # Add member channel
97
+ member_channel = "/#{plural_name}/#{to_param}" + tail
98
+ channels << member_channel
97
99
 
98
100
  # Add nested channels for each parent
99
- parents.map(&:name).each do |parent_name|
100
- # Get parent record from name
101
- parent = send(parent_name)
102
-
103
- # Get parent class's plural underscore name
104
- parent_plural_name = parent_name.to_s.underscore.pluralize
105
-
106
- # Add collection's channel nested under parent's member channel
107
- channels << "/#{parent_plural_name}/#{parent.to_param}/#{plural_name}"
101
+ parents.each do |parent|
102
+ # Only recusively add collection channel
103
+ # for child
104
+ if tail.empty?
105
+ channels << parent.channels(collection_channel)
106
+ end
108
107
 
109
- # Add member's channel nested under parent's member channel
110
- channels << "/#{parent_plural_name}/#{parent.to_param}/#{plural_name}/#{to_param}"
108
+ channels << parent.channels(member_channel)
111
109
  end
112
110
 
113
- channels
111
+ channels.flatten
114
112
  end
115
113
 
116
114
  private
@@ -132,6 +130,13 @@ module Entangled
132
130
  resource: self
133
131
  }.to_json
134
132
  end
133
+
134
+ # Find parent classes from belongs_to associations
135
+ def parents
136
+ self.class.
137
+ reflect_on_all_associations(:belongs_to).
138
+ map{ |a| send(a.name) }
139
+ end
135
140
  end
136
141
 
137
142
  def self.included(receiver)
@@ -1,3 +1,3 @@
1
1
  module Entangled
2
- VERSION = "0.0.25"
2
+ VERSION = "0.0.26"
3
3
  end
@@ -0,0 +1,6 @@
1
+ class Child < ActiveRecord::Base
2
+ include Entangled::Model
3
+ entangle
4
+
5
+ belongs_to :parent, required: true
6
+ end
@@ -0,0 +1,6 @@
1
+ class Grandfather < ActiveRecord::Base
2
+ include Entangled::Model
3
+ entangle
4
+
5
+ has_many :parents, dependent: :destroy
6
+ end
@@ -0,0 +1,6 @@
1
+ class Grandmother < ActiveRecord::Base
2
+ include Entangled::Model
3
+ entangle
4
+
5
+ has_many :parents, dependent: :destroy
6
+ end
@@ -3,4 +3,6 @@ class Item < ActiveRecord::Base
3
3
  entangle
4
4
 
5
5
  belongs_to :list, required: true
6
+
7
+ validates :name, presence: true
6
8
  end
@@ -0,0 +1,8 @@
1
+ class Parent < ActiveRecord::Base
2
+ include Entangled::Model
3
+ entangle
4
+
5
+ has_many :children, dependent: :destroy
6
+ belongs_to :grandmother, required: true
7
+ belongs_to :grandfather, required: true
8
+ end
@@ -0,0 +1,9 @@
1
+ class CreateChildren < ActiveRecord::Migration
2
+ def change
3
+ create_table :children do |t|
4
+ t.integer :parent_id
5
+
6
+ t.timestamps null: false
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ class CreateParents < ActiveRecord::Migration
2
+ def change
3
+ create_table :parents do |t|
4
+ t.integer :grandmother_id
5
+ t.integer :grandfather_id
6
+
7
+ t.timestamps null: false
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ class CreateGrandmothers < ActiveRecord::Migration
2
+ def change
3
+ create_table :grandmothers do |t|
4
+
5
+ t.timestamps null: false
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ class CreateGrandfathers < ActiveRecord::Migration
2
+ def change
3
+ create_table :grandfathers do |t|
4
+
5
+ t.timestamps null: false
6
+ end
7
+ end
8
+ end
@@ -11,7 +11,7 @@
11
11
  #
12
12
  # It's strongly recommended that you check this file into your version control system.
13
13
 
14
- ActiveRecord::Schema.define(version: 20150316034305) do
14
+ ActiveRecord::Schema.define(version: 20150329034923) do
15
15
 
16
16
  create_table "barfoos", force: :cascade do |t|
17
17
  t.text "body"
@@ -25,6 +25,12 @@ ActiveRecord::Schema.define(version: 20150316034305) do
25
25
  t.datetime "updated_at", null: false
26
26
  end
27
27
 
28
+ create_table "children", force: :cascade do |t|
29
+ t.integer "parent_id"
30
+ t.datetime "created_at", null: false
31
+ t.datetime "updated_at", null: false
32
+ end
33
+
28
34
  create_table "foobars", force: :cascade do |t|
29
35
  t.text "body"
30
36
  t.datetime "created_at", null: false
@@ -37,6 +43,16 @@ ActiveRecord::Schema.define(version: 20150316034305) do
37
43
  t.datetime "updated_at", null: false
38
44
  end
39
45
 
46
+ create_table "grandfathers", force: :cascade do |t|
47
+ t.datetime "created_at", null: false
48
+ t.datetime "updated_at", null: false
49
+ end
50
+
51
+ create_table "grandmothers", force: :cascade do |t|
52
+ t.datetime "created_at", null: false
53
+ t.datetime "updated_at", null: false
54
+ end
55
+
40
56
  create_table "items", force: :cascade do |t|
41
57
  t.string "name"
42
58
  t.boolean "complete", default: false
@@ -51,4 +67,11 @@ ActiveRecord::Schema.define(version: 20150316034305) do
51
67
  t.datetime "updated_at", null: false
52
68
  end
53
69
 
70
+ create_table "parents", force: :cascade do |t|
71
+ t.integer "grandmother_id"
72
+ t.integer "grandfather_id"
73
+ t.datetime "created_at", null: false
74
+ t.datetime "updated_at", null: false
75
+ end
76
+
54
77
  end
@@ -13,7 +13,6 @@ angular.module('entangledTest')
13
13
  });
14
14
 
15
15
  $scope.item = $scope.list.items().new();
16
- console.log($scope.item);
17
16
  });
18
17
  });
19
18
 
@@ -9,8 +9,8 @@ angular.module('entangled', [])
9
9
  // methods $save(), $destroy, and others. A Resource also
10
10
  // stores the socket's URL it was retrieved from so it
11
11
  // can be reused for other requests.
12
- var Resource = function(params, webSocketUrl, hasMany) {
13
- // Assign proerties
12
+ function Resource(params, webSocketUrl, hasMany) {
13
+ // Assign properties
14
14
  for (var key in params) {
15
15
  // Skip inherent object properties
16
16
  if (params.hasOwnProperty(key)) {
@@ -160,7 +160,7 @@ angular.module('entangled', [])
160
160
 
161
161
  // Resources wraps all individual Resource objects
162
162
  // in a collection.
163
- var Resources = function(resources, webSocketUrl, hasMany) {
163
+ function Resources(resources, webSocketUrl, hasMany) {
164
164
  this.all = [];
165
165
 
166
166
  for (var i = 0; i < resources.length; i++) {
@@ -178,9 +178,7 @@ angular.module('entangled', [])
178
178
  // Entangled is a constructor that takes the URL
179
179
  // of the index action on the server where the
180
180
  // Resources can be retrieved.
181
- var Entangled = function(webSocketUrl) {
182
- this.className = 'Entangled';
183
-
181
+ function Entangled(webSocketUrl) {
184
182
  // Store the root URL that sockets
185
183
  // will connect to
186
184
  this.webSocketUrl = webSocketUrl;
@@ -297,5 +295,6 @@ angular.module('entangled', [])
297
295
  }.bind(this);
298
296
  };
299
297
 
298
+ // Return Entangled object as Angular service
300
299
  return Entangled;
301
300
  });
@@ -15,9 +15,9 @@ describe('Entangled', function() {
15
15
  List.hasMany('items');
16
16
  }));
17
17
 
18
- describe('.className', function() {
18
+ describe('constructor', function() {
19
19
  it('is "Entangled"', function() {
20
- expect(List.className).toBe('Entangled');
20
+ expect(List.constructor.name).toBe('Entangled');
21
21
  });
22
22
  });
23
23
 
@@ -255,7 +255,7 @@ describe('Entangled', function() {
255
255
  // an instance of Entangled, meaning
256
256
  // in turn that all class and instance
257
257
  // methods are available on it
258
- expect(list.items().className).toBe('Entangled');
258
+ expect(list.items().constructor.name).toBe('Entangled');
259
259
  done();
260
260
  });
261
261
  });
@@ -0,0 +1,112 @@
1
+ require 'spec_helper'
2
+
3
+ # Test channels inferred from relationships
4
+ RSpec.describe 'Channels', type: :model do
5
+ let!(:grandmother) { Grandmother.create }
6
+ let!(:grandfather) { Grandfather.create }
7
+
8
+ let!(:parent) do
9
+ Parent.create(
10
+ grandmother_id: grandmother.id,
11
+ grandfather_id: grandfather.id
12
+ )
13
+ end
14
+
15
+ let!(:child) { Child.create(parent_id: parent.id) }
16
+
17
+ describe "grandmother's channels" do
18
+ it 'has two channels' do
19
+ expect(grandmother.channels.size).to eq 2
20
+ end
21
+
22
+ it 'has a collection channel' do
23
+ expect(grandmother.channels).to include '/grandmothers'
24
+ end
25
+
26
+ it 'has a member channel' do
27
+ expect(grandmother.channels).to include "/grandmothers/#{grandmother.to_param}"
28
+ end
29
+ end
30
+
31
+ describe "grandfather's channels" do
32
+ it 'has two channels' do
33
+ expect(grandfather.channels.size).to eq 2
34
+ end
35
+
36
+ it 'has a collection channel' do
37
+ expect(grandfather.channels).to include '/grandfathers'
38
+ end
39
+
40
+ it 'has a member channel' do
41
+ expect(grandfather.channels).to include "/grandfathers/#{grandfather.to_param}"
42
+ end
43
+ end
44
+
45
+ describe "parent's channels" do
46
+ it 'has six channels' do
47
+ expect(parent.channels.size).to eq 6
48
+ end
49
+
50
+ it 'has a collection channel' do
51
+ expect(parent.channels).to include '/parents'
52
+ end
53
+
54
+ it 'has a member channel' do
55
+ expect(parent.channels).to include "/parents/#{parent.to_param}"
56
+ end
57
+
58
+ it 'has a collection channel nested under its grandmother' do
59
+ expect(parent.channels).to include "/grandmothers/#{grandmother.to_param}/parents"
60
+ end
61
+
62
+ it 'has a member channel nested under its grandmother' do
63
+ expect(parent.channels).to include "/grandmothers/#{grandmother.to_param}/parents/#{parent.to_param}"
64
+ end
65
+
66
+ it 'has a collection channel nested under its grandfather' do
67
+ expect(parent.channels).to include "/grandfathers/#{grandfather.to_param}/parents"
68
+ end
69
+
70
+ it 'has a member channel nested under its grandfather' do
71
+ expect(parent.channels).to include "/grandfathers/#{grandfather.to_param}/parents/#{parent.to_param}"
72
+ end
73
+ end
74
+
75
+ describe "child's channels" do
76
+ it 'has eight channels' do
77
+ expect(child.channels.size).to eq 8
78
+ end
79
+
80
+ it 'has a collection channel' do
81
+ expect(child.channels).to include '/children'
82
+ end
83
+
84
+ it 'has a member channel' do
85
+ expect(child.channels).to include "/children/#{child.to_param}"
86
+ end
87
+
88
+ it 'has a collection channel nested under its parent' do
89
+ expect(child.channels).to include "/parents/#{parent.to_param}/children"
90
+ end
91
+
92
+ it 'has a member channel nested under its parent' do
93
+ expect(child.channels).to include "/parents/#{parent.to_param}/children/#{child.to_param}"
94
+ end
95
+
96
+ it 'has a collection channel nested under its parent and grandmother' do
97
+ expect(child.channels).to include "/grandmothers/#{grandmother.to_param}/parents/#{parent.to_param}/children"
98
+ end
99
+
100
+ it 'has a member channel nested under its parent and grandmother' do
101
+ expect(child.channels).to include "/grandmothers/#{grandmother.to_param}/parents/#{parent.to_param}/children/#{child.to_param}"
102
+ end
103
+
104
+ it 'has a collection channel nested under its parent and grandfather' do
105
+ expect(child.channels).to include "/grandfathers/#{grandfather.to_param}/parents/#{parent.to_param}/children"
106
+ end
107
+
108
+ it 'has a member channel nested under its parent and grandfather' do
109
+ expect(child.channels).to include "/grandfathers/#{grandfather.to_param}/parents/#{parent.to_param}/children/#{child.to_param}"
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Child, type: :model do
4
+ describe 'Associations' do
5
+ it { is_expected.to belong_to :parent }
6
+ end
7
+
8
+ describe 'Attributes' do
9
+ it { is_expected.to respond_to :parent_id }
10
+ end
11
+
12
+ describe 'Database' do
13
+ it { is_expected.to have_db_column(:parent_id).of_type(:integer) }
14
+ end
15
+
16
+ describe 'Validations' do
17
+ it { is_expected.to validate_presence_of :parent }
18
+ end
19
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Grandfather, type: :model do
4
+ describe 'Associations' do
5
+ it { is_expected.to have_many(:parents).dependent(:destroy) }
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Grandmother, type: :model do
4
+ describe 'Associations' do
5
+ it { is_expected.to have_many(:parents).dependent(:destroy) }
6
+ end
7
+ end
@@ -17,34 +17,8 @@ RSpec.describe Item, type: :model do
17
17
  it { is_expected.to belong_to :list }
18
18
  end
19
19
 
20
- describe 'Methods' do
21
- let(:list) { List.create(name: 'foo') }
22
- let(:item) { list.items.create(name: 'bar') }
23
-
24
- describe '#channels' do
25
- it 'is an array of channels' do
26
- expect(item.channels).to be_an Array
27
- end
28
-
29
- it "includes the collection's channel" do
30
- expect(item.channels).to include '/items'
31
- end
32
-
33
- it "includes the item's direct channel" do
34
- expect(item.channels).to include "/items/#{item.to_param}"
35
- end
36
-
37
- it "includes the collection's nested channel" do
38
- expect(item.channels).to include "/lists/#{list.to_param}/items"
39
- end
40
-
41
- it "includes the item's nested channel" do
42
- expect(item.channels).to include "/lists/#{list.to_param}/items/#{item.to_param}"
43
- end
44
- end
45
- end
46
-
47
20
  describe 'Validations' do
48
21
  it { is_expected.to validate_presence_of :list }
22
+ it { is_expected.to validate_presence_of :name }
49
23
  end
50
24
  end
@@ -16,132 +16,113 @@ RSpec.describe List, type: :model do
16
16
  describe 'Methods' do
17
17
  let(:list) { List.create(name: 'foo') }
18
18
 
19
- describe '#channels' do
20
- it 'is an array of channels' do
21
- expect(list.channels).to be_an Array
22
- end
23
-
24
- it "includes the collection's channel" do
25
- expect(list.channels).to include '/lists'
26
- end
27
-
28
- it "includes the member's channel" do
29
- expect(list.channels).to include "/lists/#{list.to_param}"
30
- end
31
- end
32
- end
33
-
34
- describe '#entangle' do
35
- let(:stub_redis) do
36
- mock("redis").tap do |redis|
37
- redis.stubs(:publish)
38
- Redis.stubs(:new).returns(redis)
39
- end
40
- end
41
-
42
- describe 'creation' do
43
- let(:list) { List.create(name: 'foo') }
44
-
45
- it 'broadcasts the creation to the collection channel' do
46
- redis = stub_redis
47
-
48
- list.channels.each do |channel|
49
- expect(redis).to have_received(:publish).with(
50
- channel, {
51
- action: :create,
52
- resource: list
53
- }.to_json
54
- )
19
+ describe '#entangle' do
20
+ let(:stub_redis) do
21
+ mock("redis").tap do |redis|
22
+ redis.stubs(:publish)
23
+ Redis.stubs(:new).returns(redis)
55
24
  end
56
25
  end
57
26
 
58
- it 'broadcasts the creation to the member channel' do
59
- redis = stub_redis
27
+ describe 'creation' do
28
+ it 'broadcasts the creation to the collection channel' do
29
+ redis = stub_redis
30
+
31
+ list.channels.each do |channel|
32
+ expect(redis).to have_received(:publish).with(
33
+ channel, {
34
+ action: :create,
35
+ resource: list
36
+ }.to_json
37
+ )
38
+ end
39
+ end
60
40
 
61
- list.channels.each do |channel|
62
- expect(redis).to have_received(:publish).with(
63
- channel, {
64
- action: :create,
65
- resource: list
66
- }.to_json
67
- )
41
+ it 'broadcasts the creation to the member channel' do
42
+ redis = stub_redis
43
+
44
+ list.channels.each do |channel|
45
+ expect(redis).to have_received(:publish).with(
46
+ channel, {
47
+ action: :create,
48
+ resource: list
49
+ }.to_json
50
+ )
51
+ end
68
52
  end
69
53
  end
70
- end
71
54
 
72
- describe 'update' do
73
- let!(:list) { List.create(name: 'foo') }
55
+ describe 'update' do
56
+ it 'broadcasts the update to the collection channel' do
57
+ redis = stub_redis
74
58
 
75
- it 'broadcasts the update to the collection channel' do
76
- redis = stub_redis
59
+ list.update(name: 'bar')
77
60
 
78
- list.update(name: 'bar')
79
-
80
- list.channels.each do |channel|
81
- expect(redis).to have_received(:publish).with(
82
- channel, {
83
- action: :update,
84
- resource: list
85
- }.to_json
86
- )
61
+ list.channels.each do |channel|
62
+ expect(redis).to have_received(:publish).with(
63
+ channel, {
64
+ action: :update,
65
+ resource: list
66
+ }.to_json
67
+ )
68
+ end
87
69
  end
88
- end
89
70
 
90
- it 'broadcasts the update to the member channel' do
91
- redis = stub_redis
71
+ it 'broadcasts the update to the member channel' do
72
+ redis = stub_redis
92
73
 
93
- list.update(name: 'bar')
74
+ list.update(name: 'bar')
94
75
 
95
- list.channels.each do |channel|
96
- expect(redis).to have_received(:publish).with(
97
- channel, {
98
- action: :update,
99
- resource: list
100
- }.to_json
101
- )
76
+ list.channels.each do |channel|
77
+ expect(redis).to have_received(:publish).with(
78
+ channel, {
79
+ action: :update,
80
+ resource: list
81
+ }.to_json
82
+ )
83
+ end
102
84
  end
103
85
  end
104
- end
105
86
 
106
- describe 'destruction' do
107
- let!(:list) { List.create(name: 'foo') }
87
+ describe 'destruction' do
88
+ it 'broadcasts the destruction to the collection channel' do
89
+ redis = stub_redis
108
90
 
109
- it 'broadcasts the destruction to the collection channel' do
110
- redis = stub_redis
91
+ list.destroy
111
92
 
112
- list.destroy
113
-
114
- list.channels.each do |channel|
115
- expect(redis).to have_received(:publish).with(
116
- channel, {
117
- action: :destroy,
118
- resource: list
119
- }.to_json
120
- )
93
+ list.channels.each do |channel|
94
+ expect(redis).to have_received(:publish).with(
95
+ channel, {
96
+ action: :destroy,
97
+ resource: list
98
+ }.to_json
99
+ )
100
+ end
121
101
  end
122
- end
123
102
 
124
- it 'broadcasts the destruction to the member channel' do
125
- redis = stub_redis
126
-
127
- list.destroy
128
-
129
- list.channels.each do |channel|
130
- expect(redis).to have_received(:publish).with(
131
- channel, {
132
- action: :destroy,
133
- resource: list
134
- }.to_json
135
- )
103
+ it 'broadcasts the destruction to the member channel' do
104
+ redis = stub_redis
105
+
106
+ list.destroy
107
+
108
+ list.channels.each do |channel|
109
+ expect(redis).to have_received(:publish).with(
110
+ channel, {
111
+ action: :destroy,
112
+ resource: list
113
+ }.to_json
114
+ )
115
+ end
136
116
  end
137
117
  end
138
118
  end
139
- end
140
119
 
141
- describe '#as_json' do
142
- it 'includes errors' do
143
- list = List.create
144
- expect(list.as_json["errors"][:name]).to include "can't be blank"
120
+ describe '#as_json' do
121
+ let(:list) { List.create }
122
+
123
+ it 'includes errors' do
124
+ expect(list.as_json["errors"][:name]).to include "can't be blank"
125
+ end
145
126
  end
146
127
  end
147
128
 
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Parent, type: :model do
4
+ describe 'Associations' do
5
+ it { is_expected.to have_many(:children).dependent(:destroy) }
6
+ it { is_expected.to belong_to :grandmother }
7
+ it { is_expected.to belong_to :grandfather }
8
+ end
9
+
10
+ describe 'Attributes' do
11
+ it { is_expected.to respond_to :grandmother_id }
12
+ it { is_expected.to respond_to :grandfather_id }
13
+ end
14
+
15
+ describe 'Database' do
16
+ it { is_expected.to have_db_column(:grandmother_id).of_type(:integer) }
17
+ it { is_expected.to have_db_column(:grandfather_id).of_type(:integer) }
18
+ end
19
+
20
+ describe 'Validations' do
21
+ it { is_expected.to validate_presence_of :grandmother }
22
+ it { is_expected.to validate_presence_of :grandfather }
23
+ end
24
+ end
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: 0.0.25
4
+ version: 0.0.26
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-03-22 00:00:00.000000000 Z
11
+ date: 2015-03-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -177,9 +177,7 @@ files:
177
177
  - LICENSE.txt
178
178
  - README.md
179
179
  - Rakefile
180
- - bower.json
181
180
  - entangled.gemspec
182
- - entangled.js
183
181
  - lib/entangled.rb
184
182
  - lib/entangled/controller.rb
185
183
  - lib/entangled/helpers.rb
@@ -195,12 +193,16 @@ files:
195
193
  - spec/dummy/app/models/.keep
196
194
  - spec/dummy/app/models/bar.rb
197
195
  - spec/dummy/app/models/barfoo.rb
196
+ - spec/dummy/app/models/child.rb
198
197
  - spec/dummy/app/models/concerns/.keep
199
198
  - spec/dummy/app/models/foo.rb
200
199
  - spec/dummy/app/models/foobar.rb
200
+ - spec/dummy/app/models/grandfather.rb
201
+ - spec/dummy/app/models/grandmother.rb
201
202
  - spec/dummy/app/models/item.rb
202
203
  - spec/dummy/app/models/list.rb
203
204
  - spec/dummy/app/models/message.rb
205
+ - spec/dummy/app/models/parent.rb
204
206
  - spec/dummy/bin/bundle
205
207
  - spec/dummy/bin/rails
206
208
  - spec/dummy/bin/rake
@@ -232,6 +234,10 @@ files:
232
234
  - spec/dummy/db/migrate/20150314054548_create_lists.rb
233
235
  - spec/dummy/db/migrate/20150314055847_create_items.rb
234
236
  - spec/dummy/db/migrate/20150316034305_drop_messages.rb
237
+ - spec/dummy/db/migrate/20150329034759_create_children.rb
238
+ - spec/dummy/db/migrate/20150329034900_create_parents.rb
239
+ - spec/dummy/db/migrate/20150329034915_create_grandmothers.rb
240
+ - spec/dummy/db/migrate/20150329034923_create_grandfathers.rb
235
241
  - spec/dummy/db/schema.rb
236
242
  - spec/dummy/log/.keep
237
243
  - spec/dummy/public/Gruntfile.js
@@ -248,9 +254,14 @@ files:
248
254
  - spec/dummy/public/views/lists/index.html
249
255
  - spec/dummy/public/views/lists/show.html
250
256
  - spec/helpers/redis_spec.rb
257
+ - spec/models/channels_spec.rb
258
+ - spec/models/child_spec.rb
259
+ - spec/models/grandfather_spec.rb
260
+ - spec/models/grandmother_spec.rb
251
261
  - spec/models/inclusion_exclusion_spec.rb
252
262
  - spec/models/item_spec.rb
253
263
  - spec/models/list_spec.rb
264
+ - spec/models/parent_spec.rb
254
265
  - spec/routing/inclusion_exclusion_routing_spec.rb
255
266
  - spec/routing/nested_routing_spec.rb
256
267
  - spec/spec_helper.rb
@@ -288,12 +299,16 @@ test_files:
288
299
  - spec/dummy/app/models/.keep
289
300
  - spec/dummy/app/models/bar.rb
290
301
  - spec/dummy/app/models/barfoo.rb
302
+ - spec/dummy/app/models/child.rb
291
303
  - spec/dummy/app/models/concerns/.keep
292
304
  - spec/dummy/app/models/foo.rb
293
305
  - spec/dummy/app/models/foobar.rb
306
+ - spec/dummy/app/models/grandfather.rb
307
+ - spec/dummy/app/models/grandmother.rb
294
308
  - spec/dummy/app/models/item.rb
295
309
  - spec/dummy/app/models/list.rb
296
310
  - spec/dummy/app/models/message.rb
311
+ - spec/dummy/app/models/parent.rb
297
312
  - spec/dummy/bin/bundle
298
313
  - spec/dummy/bin/rails
299
314
  - spec/dummy/bin/rake
@@ -325,6 +340,10 @@ test_files:
325
340
  - spec/dummy/db/migrate/20150314054548_create_lists.rb
326
341
  - spec/dummy/db/migrate/20150314055847_create_items.rb
327
342
  - spec/dummy/db/migrate/20150316034305_drop_messages.rb
343
+ - spec/dummy/db/migrate/20150329034759_create_children.rb
344
+ - spec/dummy/db/migrate/20150329034900_create_parents.rb
345
+ - spec/dummy/db/migrate/20150329034915_create_grandmothers.rb
346
+ - spec/dummy/db/migrate/20150329034923_create_grandfathers.rb
328
347
  - spec/dummy/db/schema.rb
329
348
  - spec/dummy/log/.keep
330
349
  - spec/dummy/public/Gruntfile.js
@@ -341,9 +360,14 @@ test_files:
341
360
  - spec/dummy/public/views/lists/index.html
342
361
  - spec/dummy/public/views/lists/show.html
343
362
  - spec/helpers/redis_spec.rb
363
+ - spec/models/channels_spec.rb
364
+ - spec/models/child_spec.rb
365
+ - spec/models/grandfather_spec.rb
366
+ - spec/models/grandmother_spec.rb
344
367
  - spec/models/inclusion_exclusion_spec.rb
345
368
  - spec/models/item_spec.rb
346
369
  - spec/models/list_spec.rb
370
+ - spec/models/parent_spec.rb
347
371
  - spec/routing/inclusion_exclusion_routing_spec.rb
348
372
  - spec/routing/nested_routing_spec.rb
349
373
  - spec/spec_helper.rb
data/bower.json DELETED
@@ -1,20 +0,0 @@
1
- {
2
- "name": "entangled",
3
- "version": "0.0.8",
4
- "authors": [
5
- "Dennis Charles Hackethal <dennis.hackethal@gmail.com>"
6
- ],
7
- "description": "Angular library that communicates with a real time backend to enable three way data binding",
8
- "license": "MIT",
9
- "homepage": "https://github.com/dchacke/entangled",
10
- "ignore": [
11
- "lib",
12
- "spec",
13
- ".gitignore",
14
- "entangled.gemspec",
15
- "Gemfile",
16
- "Gemfile.lock",
17
- "Rakefile"
18
- ],
19
- "main": "entangled.js"
20
- }
data/entangled.js DELETED
@@ -1 +0,0 @@
1
- angular.module("entangled",[]).factory("Entangled",function(){var e=function(e,t,r){for(var i in e)e.hasOwnProperty(i)&&(this[i]=e[i]);this.webSocketUrl=t,r&&(this[r]=function(){return new s(this.webSocketUrl+"/"+this.id+"/"+r)})};e.prototype.$save=function(e){if(this.id){var t=new WebSocket(this.webSocketUrl+"/"+this.id+"/update");t.onopen=function(){t.send(JSON.stringify(this))}.bind(this),t.onmessage=function(t){if(t.data){var r=JSON.parse(t.data);if(r.resource)for(key in r.resource)this[key]=r.resource[key]}this[this.hasMany]=new s(this.webSocketUrl+"/"+this.id+"/"+this.hasMany),e&&e(this)}.bind(this)}else{var t=new WebSocket(this.webSocketUrl+"/create");t.onopen=function(){t.send(JSON.stringify(this))}.bind(this),t.onmessage=function(t){if(t.data){var s=JSON.parse(t.data);if(s.resource)for(key in s.resource)this[key]=s.resource[key]}e&&e(this)}.bind(this)}},e.prototype.$update=function(e,t){for(var s in e)e.hasOwnProperty(s)&&(this[s]=e[s]);this.$save(t)},e.prototype.$destroy=function(e){var t=new WebSocket(this.webSocketUrl+"/"+this.id+"/destroy");t.onopen=function(){t.send(null)},t.onmessage=function(t){if(t.data){var s=JSON.parse(t.data);if(s.resource)for(key in s.resource)this[key]=s.resource[key]}e&&e(this)}.bind(this)},e.prototype.$valid=function(){return!(this.errors&&Object.keys(this.errors).length)},e.prototype.$invalid=function(){return!this.$valid()},e.prototype.$persisted=function(){return!!this.id};var t=function(t,s,r){this.all=[];for(var i=0;i<t.length;i++){var o=new e(t[i],s,r);this.all.push(o)}},s=function(e){this.className="Entangled",this.webSocketUrl=e};return s.prototype.hasMany=function(e){this.hasMany=e},s.prototype["new"]=function(t){return new e(t,this.webSocketUrl,this.hasMany)},s.prototype.all=function(s){var r=new WebSocket(this.webSocketUrl);r.onmessage=function(i){if(i.data.length){var o=JSON.parse(i.data);if(o.resources)this.resources=new t(o.resources,r.url,this.hasMany);else if(o.action)if("create"===o.action)this.resources.all.push(new e(o.resource,r.url,this.hasMany));else if("update"===o.action){for(var n,a=0;a<this.resources.all.length;a++)this.resources.all[a].id===o.resource.id&&(n=a);this.resources.all[n]=new e(o.resource,r.url,this.hasMany)}else if("destroy"===o.action){for(var n,a=0;a<this.resources.all.length;a++)this.resources.all[a].id===o.resource.id&&(n=a);this.resources.all.splice(n,1)}else console.log("Something else other than CRUD happened..."),console.log(o)}s(this.resources.all)}.bind(this)},s.prototype.create=function(e,t){var s=this["new"](e);s.$save(t)},s.prototype.find=function(t,s){var r=this.webSocketUrl,i=new WebSocket(r+"/"+t);i.onmessage=function(t){if(t.data.length){var i=JSON.parse(t.data);i.resource&&!i.action?this.resource=new e(i.resource,r,this.hasMany):i.action?"update"===i.action?this.resource=new e(i.resource,r,this.hasMany):"destroy"===i.action&&(this.resource=void 0):(console.log("Something else other than CRUD happened..."),console.log(i))}s(this.resource)}.bind(this)},s});