hyper-operation 0.5.1 → 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +18 -616
- data/lib/hyper-operation.rb +1 -0
- data/lib/hyper-operation/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ae0c4ee46f852a8120a9116a6e90ca9198f131ac
|
4
|
+
data.tar.gz: 6cb26e37bcbe79c1a6c639f5a6f50c5f6c3dfa95
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7834032b2c71bcdcb0f295aecc7aa9878f276cd9770e6a9e04754a93b9e3c89d8871129a929901f52a0a0b7ecc4a9000e15e258f34fe8d9ca3888d2c8c23199b
|
7
|
+
data.tar.gz: b8ee589621f040707daff0c1d70117f5e495727409b746aa2d8e919470ac3d736ee9fb6dee2ead40cfc5e407816f48e597bc635da1c3caaacb7d7e89b51e6621
|
data/README.md
CHANGED
@@ -15,14 +15,20 @@ Operations encapsulate business logic. In a traditional MVC architecture, Operat
|
|
15
15
|
+ Please see the [ruby-hyperloop.io](http://ruby-hyperloop.io/) website for documentation.
|
16
16
|
+ Join the Hyperloop [gitter.io](https://gitter.im/ruby-hyperloop/chat) chat for help and support.
|
17
17
|
|
18
|
-
##
|
18
|
+
## Installation and Setup
|
19
19
|
|
20
|
-
|
20
|
+
**Note: Operations require Rails currently.**
|
21
21
|
|
22
|
-
|
23
|
-
### Installation
|
22
|
+
### Easy Installation
|
24
23
|
|
25
|
-
|
24
|
+
The easiest way to install is to use the `hyper-rails` generator.
|
25
|
+
|
26
|
+
1. Add `gem 'hyper-rails'` to your Rails `Gemfile` development section.
|
27
|
+
2. Install the Gem: `bundle install`
|
28
|
+
3. Run the generator: `bundle exec rails g hyperloop:install --all`
|
29
|
+
4. Update the bundle: `bundle update`
|
30
|
+
|
31
|
+
### Manual Installation
|
26
32
|
|
27
33
|
Add `gem 'hyper-operation'` to your Gemfile
|
28
34
|
Add `//= require hyperloop-loader` to your application.rb
|
@@ -82,627 +88,23 @@ end
|
|
82
88
|
|
83
89
|
See the [Channels](#channels) section for more details on authorization.
|
84
90
|
|
85
|
-
###
|
86
|
-
=======
|
87
|
-
1. Add `gem 'hyper-rails'` to your Rails `Gemfile` development section.
|
88
|
-
2. Install the Gem: `bundle install`
|
89
|
-
3. Run the generator: `bundle exec rails g hyperloop:install --all`
|
90
|
-
4. Update the bundle: `bundle update`
|
91
|
-
>>>>>>> 14990fb3321e5a8b1cc1cb2d859d747695ffd907
|
92
|
-
|
93
|
-
Your Isomorphic Operations live in a `hyperloop/operations` folder and your server only Operations in `app/operations`
|
94
|
-
|
95
|
-
You will also find an `app/policies` folder with a simple access policy suited for development. Policies are how you will provide detailed access control to your Isomorphic models.
|
96
|
-
|
97
|
-
## Contributing
|
98
|
-
|
99
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/ruby-hyperloop/hyper-operation. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Code of Conduct](https://github.com/ruby-hyperloop/hyper-operation/blob/master/CODE_OF_CONDUCT.md) code of conduct.
|
100
|
-
|
101
|
-
## License
|
102
|
-
|
103
|
-
<<<<<<< HEAD
|
104
|
-
```ruby
|
105
|
-
class AddItemToCart < Hyperloop::Operation
|
106
|
-
param :sku, type: String
|
107
|
-
param qty: 1, type: Integer, min: 1
|
108
|
-
end
|
109
|
-
|
110
|
-
class Cart < Hyperloop::Store
|
111
|
-
receives AddItemToCart do
|
112
|
-
mutate.items[params.sku] += params.qty
|
113
|
-
end
|
114
|
-
end
|
115
|
-
```
|
116
|
-
|
117
|
-
In addition unlike Hyperloop::Component params, Operation params are *not* reactive, and so you can assign to them as well:
|
118
|
-
```ruby
|
119
|
-
params.some_value = 12
|
120
|
-
```
|
121
|
-
|
122
|
-
The parameter filter types and options are taken from the [Mutations](https://github.com/cypriss/mutations) gem with the following changes:
|
123
|
-
|
124
|
-
+ In Hyperloop::Operations all params are declared with the param macro.
|
125
|
-
+ The type *can* be specified using the `type:` option.
|
126
|
-
+ Array and hash types can be shortened to `[]` and `{}`
|
127
|
-
+ Optional params either have the default value associated with the param name, or by having the `default` option present.
|
128
|
-
+ All other [Mutation filter options](https://github.com/cypriss/mutations/wiki/Filtering-Input) (such as `:min`) will work the same.
|
129
|
-
|
130
|
-
```ruby
|
131
|
-
# required param (does not have a default value)
|
132
|
-
param :sku, type: String
|
133
|
-
# equivalent Mutation syntax
|
134
|
-
# required { string :sku }
|
135
|
-
|
136
|
-
# optional params (does have a default value)
|
137
|
-
param qty: 1, min: 1
|
138
|
-
# alternative syntax
|
139
|
-
param :qty, default: 1, min: 1
|
140
|
-
# equivalent Mutation syntax
|
141
|
-
# optional { integer :qty, default: 1, min: 1 }
|
142
|
-
```
|
143
|
-
|
144
|
-
All incoming params are validated against the param declarations, and any errors are posted to the `@errors` instance variable. Extra params are ignored, but missing params unless they have a default value will cause a validation error.
|
145
|
-
|
146
|
-
### Defining Execution Steps
|
147
|
-
|
148
|
-
Operations may define a sequence of steps to be executed when the operation is run, using the `step`, `failed` and `async` callback macros.
|
149
|
-
|
150
|
-
```ruby
|
151
|
-
class Reset < Hyperloop::Operation
|
152
|
-
step { HTTP.post('/logout') }
|
153
|
-
end
|
154
|
-
```
|
155
|
-
|
156
|
-
+ `step`: runs a callback - each step is run in order.
|
157
|
-
+ `failed`: runs a callback if a previous `step` or validation has failed.
|
158
|
-
+ `async`: will be explained below.
|
159
|
-
|
160
|
-
```ruby
|
161
|
-
step { } # do something
|
162
|
-
step { } # do something else once above step is done
|
163
|
-
failed { } # do this if anything above has failed
|
164
|
-
step { } # do a third thing, unless we are on the failed track
|
165
|
-
failed { } # do this if anything above has failed
|
166
|
-
```
|
167
|
-
|
168
|
-
Together `step` and `failed` form two *railway tracks*. Initially execution proceeds down the success track until something goes wrong, then
|
169
|
-
execution switches to the failure track starting at the next `failed` statement. Once on the failed track execution continues performing each
|
170
|
-
`failed` callback and skipping any `step` callbacks.
|
171
|
-
|
172
|
-
Failure occurs when either an exception is raised or a promise fails (more on this in the next section.) The Ruby `fail` keyword can be used as a simple way to switch to the failed track.
|
173
|
-
|
174
|
-
Both `step` and `failed` can receive any results delivered by the previous step. If the previous step raised an exception (outside a promise) the failure track will receive the exception object.
|
175
|
-
|
176
|
-
The callback may be provided to `step` and `failed` either as a block, a symbol (which will name a method), a proc, a lambda, or an Operation.
|
177
|
-
|
178
|
-
```ruby
|
179
|
-
step { puts 'hello' }
|
180
|
-
step :say_hello
|
181
|
-
step -> () { puts 'hello' }
|
182
|
-
step Proc.new { puts 'hello' }
|
183
|
-
step SayHello # your params will be passed along to SayHello
|
184
|
-
```
|
185
|
-
|
186
|
-
FYI: You can also use the Ruby `next` keyword as expected to leave the current step and move to the next one.
|
187
|
-
|
188
|
-
### Promises and Operations
|
189
|
-
|
190
|
-
Within the browser, code does not wait for asynchronous methods (such as HTTP requests or timers) to complete. Operations use Opal's [Promise library](http://opalrb.org/docs/api/v0.10.3/stdlib/Promise.html) to deal with these situations cleanly. A Promise is an object that has three states: It is either still pending, or has been rejected (i.e. failed), or has been successfully resolved. A promise can have callbacks attached to either the failed or resolved state, and these callbacks will be executed once the promise is resolved or rejected.
|
191
|
-
|
192
|
-
If a `step` or `failed` callback returns a pending promise then the execution of the operation is suspended, and the Operation will return the promise to the caller. If there is more track ahead, then execution will resume on the next step when the promise is resolved. Likewise if the pending promise is rejected execution will resume on the next `failed` callback. Because of the way promises work, the operation steps will all be completed before the resolved state is passed along to caller, so everything will execute in its original order.
|
193
|
-
|
194
|
-
Likewise the Operation's dispatch occurs when the promise resolves as well.
|
195
|
-
|
196
|
-
The `async` method can be used to override the waiting behavior. If a `step` returns a promise, and there is an `async` callback farther down the track, execution will immediately pick up at the `async`. Any steps in between will still be run when the promise resolves, but their results will not be passed outside of the operation.
|
197
|
-
|
198
|
-
These features make it easy to organize, understand and compose asynchronous code:
|
199
|
-
|
200
|
-
```ruby
|
201
|
-
class AddItemToCart < Hyperloop::Operation
|
202
|
-
step { HTTP.get('/inventory/#{params.sku}/qty') }
|
203
|
-
# previous step returned a promise so next step
|
204
|
-
# will execute when that promise resolves
|
205
|
-
step { |response| fail if params.qty > response.to_i }
|
206
|
-
# once we are sure we have inventory we will dispatch
|
207
|
-
# to any listening stores.
|
208
|
-
end
|
209
|
-
```
|
210
|
-
|
211
|
-
Operations will *always* return a *Promise*. If an Operation has no steps that return a promise the value of the last step will be wrapped in a resolved promise. This lets you easily chain Operations, regardless of their internal implementation:
|
212
|
-
|
213
|
-
```ruby
|
214
|
-
class QuickCheckout < Hyperloop::Operation
|
215
|
-
param :sku, type: String
|
216
|
-
param qty: 1, type: Integer, minimum: 1
|
217
|
-
|
218
|
-
step { AddItemToCart(params) }
|
219
|
-
step ValidateUserDefaultCC
|
220
|
-
step Checkout
|
221
|
-
end
|
222
|
-
```
|
223
|
-
|
224
|
-
You can also use `Promise#when` if you don't care about the order of Operations
|
225
|
-
|
226
|
-
```ruby
|
227
|
-
class DoABunchOStuff < Hyperloop::Operation
|
228
|
-
step { Promise.when(SomeOperation.run, SomeOtherOperation.run) }
|
229
|
-
# dispatch when both operations complete
|
230
|
-
end
|
231
|
-
```
|
232
|
-
|
233
|
-
### Early Exits with `abort!` and `succeed!`
|
234
|
-
|
235
|
-
In any `step` or `failed` callback, you may do an immediate exit from the Operation using the `abort!` and `succeed!` methods. The `abort!` method returns a failed Promise with any supplied parameters. The `succeed!` method does an immediate dispatch, and returns a resolved Promise with any supplied parameters. If `succeed!` is used in a `failed` callback, it will override the failed status of the Operation. This is especially useful if you want to dispatch in spite of failures:
|
236
|
-
|
237
|
-
```ruby
|
238
|
-
class Pointless < Hyperloop::Operation
|
239
|
-
step { fail } # go to failure track
|
240
|
-
failed { succeed! } # dispatch and exit
|
241
|
-
end
|
242
|
-
```
|
243
|
-
|
244
|
-
### The `validate` and `add_error` methods
|
245
|
-
|
246
|
-
An Operation can also have a number of `validate` callbacks which will run before the first step. This is a handy place to put any additional validations. In the validate method you can add validation type messages using the `add_error` method, and these will be passed along like any other param validation failures.
|
247
|
-
|
248
|
-
```ruby
|
249
|
-
class UpdateProfile < Hyperloop::Operation
|
250
|
-
param :first_name, type: String
|
251
|
-
param :last_name, type: String
|
252
|
-
param :password, type: String, nils: true
|
253
|
-
param :password_confirmation, type: String, nils: true
|
254
|
-
|
255
|
-
validate do
|
256
|
-
add_error(
|
257
|
-
:password_confirmation,
|
258
|
-
:doesnt_match,
|
259
|
-
"Your new password and confirmation do not match"
|
260
|
-
) unless params.password == params.confirmation
|
261
|
-
end
|
262
|
-
|
263
|
-
# or more simply:
|
264
|
-
|
265
|
-
add_error :password_confirmation, :doesnt_match, "Your new password and confirmation do not match" do
|
266
|
-
params.password != params.confirmation
|
267
|
-
end
|
268
|
-
|
269
|
-
...
|
270
|
-
end
|
271
|
-
```
|
272
|
-
|
273
|
-
If the validate method returns a promise, then execution will wait until the promise resolves. If the promise fails, then the current validation fails.
|
274
|
-
|
275
|
-
You may also call `abort!` from within `validate` or `add_error` to immediately exit the Operation. Otherwise all validations will be run and collected together and the Operation will move onto the `failed` track. If `abort!` is called within an `add_error` callback the error will be added before aborting.
|
276
|
-
|
277
|
-
You can also raise an exception directly in validate if appropriate. If a `Hyperloop::AccessViolation` exception is raised the Operation will immediately abort, otherwise just the current validation fails.
|
278
|
-
|
279
|
-
If you want to avoid further validations if there are any failures in the basic parameter validations you can add do add this
|
280
|
-
```ruby
|
281
|
-
validate { abort! if has_errors? }
|
282
|
-
```
|
283
|
-
before the first `validate` or `add_error` call.
|
284
|
-
|
285
|
-
### Handling Failed Operations
|
286
|
-
|
287
|
-
Because Operations always return a promise, you can use the Promise's `fail` method on the Operation's result to detect failures.
|
288
|
-
|
289
|
-
```ruby
|
290
|
-
QuickCheckout(sku: selected_item, qty: selected_qty)
|
291
|
-
.then do
|
292
|
-
# show confirmation
|
293
|
-
end
|
294
|
-
.fail do |exception|
|
295
|
-
# whatever exception was raised is passed to the fail block
|
296
|
-
end
|
297
|
-
```
|
298
|
-
Failures to validate params result in `Hyperloop::ValidationException` which contains a [Mutations error object](https://github.com/cypriss/mutations#what-about-validation-errors).
|
299
|
-
```ruby
|
300
|
-
MyOperation.run.fail do |e|
|
301
|
-
if e.is_a? Hyperloop::ValidationException
|
302
|
-
e.errors.symbolic # hash: each key is a parameter that failed validation,
|
303
|
-
# value is a symbol representing the reason
|
304
|
-
e.errors.message # same as symbolic but message is in English
|
305
|
-
e.errors.message_list # array of messages where failed parameter is
|
306
|
-
# combined with the message
|
307
|
-
end
|
308
|
-
end
|
309
|
-
```
|
310
|
-
|
311
|
-
### Running Operations
|
312
|
-
|
313
|
-
You can run an Operation by using ...
|
314
|
-
+ the Operation class name as a method:
|
315
|
-
```ruby
|
316
|
-
MyOperation(...params...)
|
317
|
-
```
|
318
|
-
+ the `run` method:
|
319
|
-
```ruby
|
320
|
-
MyOperation.run ...params...
|
321
|
-
```
|
322
|
-
+ the `then` and `fail` methods, which will dispatch the operation and attach a promise handler:
|
323
|
-
```ruby
|
324
|
-
MyOperation.then(...params...) { alert 'operation completed' }
|
325
|
-
```
|
326
|
-
|
327
|
-
### The `Hyperloop::ServerOp` class
|
328
|
-
|
329
|
-
Operations will run on the client or the server. Some Operations like `ValidateUserDefaultCC` probably need to check information server side, and make secure API calls to our credit card processor. Rather than build an API and controller to "validate the user credentials" you simply specify that the operation must run on the server by using the `Hyperloop::ServerOp` class.
|
330
|
-
|
331
|
-
```ruby
|
332
|
-
class ValidateUserCredentials < Hyperloop::ServerOp
|
333
|
-
param :acting_user
|
334
|
-
add_error :acting_user, :no_valid_default_cc, "No valid default credit card" do
|
335
|
-
!params.acting_user.has_default_cc?
|
336
|
-
end
|
337
|
-
end
|
338
|
-
```
|
339
|
-
|
340
|
-
A Server Operation will always run on the server even if invoked on the client. When invoked from the client Server Operations will receive the `acting_user` param with the current value that your ApplicationController's `acting_user` method returns. Typically the `acting_user` method will return either some User model, or nil (if there is no logged in user.) Its up to you to define how `acting_user` is computed, but this is easily done with any of the popular authentication gems. Note that unless you explicitly add `nils: true` to the param declaration, nil will not be accepted.
|
341
|
-
|
342
|
-
As shown above you can also define a validation to further insure that the acting user (with perhaps other parameters) is allowed to perform the operation. In the above case that is the only purpose of the Operation. Another typical use would be to make sure the current acting user has the correct role to perform the operation:
|
343
|
-
|
344
|
-
```ruby
|
345
|
-
...
|
346
|
-
validate { raise Hyperloop::AccessViolation unless params.acting_user.admin? }
|
347
|
-
...
|
348
|
-
```
|
349
|
-
|
350
|
-
You can bake this kind logic into a superclass:
|
91
|
+
### Add the engine
|
351
92
|
|
352
93
|
```ruby
|
353
|
-
|
354
|
-
|
355
|
-
validate { raise Hyperloop::AccessViolation unless params.acting_user.admin? }
|
356
|
-
end
|
357
|
-
|
358
|
-
class DeleteUser < AdminOnlyOp
|
359
|
-
param :user
|
360
|
-
add_error :user, :cant_delete_user, "Can't delete yourself, or the last admin user" do
|
361
|
-
params.user == params.acting_user || (params.user.admin? && AdminUsers.count == 1)
|
362
|
-
end
|
363
|
-
end
|
364
|
-
```
|
365
|
-
|
366
|
-
Because Operations always return a promise, there is nothing to change on the client to call a Server Operation. A Server Operation will return a promise that will be resolved (or rejected) when the Operation completes (or fails) on the server.
|
367
|
-
|
368
|
-
### Dispatching From Server Operations
|
369
|
-
|
370
|
-
You can also broadcast the dispatch from Server Operations to all authorized clients. The `dispatch_to` will determine a list of *channels* to broadcast the dispatch to:
|
371
|
-
|
372
|
-
```ruby
|
373
|
-
class Announcement < Hyperloop::ServerOp
|
374
|
-
# no acting_user because we don't want clients to invoke the Operation
|
375
|
-
param :message
|
376
|
-
param :duration, type: Float, nils: true
|
377
|
-
# dispatch to the builtin Hyperloop::Application Channel
|
378
|
-
dispatch_to Hyperloop::Application
|
379
|
-
end
|
380
|
-
|
381
|
-
class CurrentAnnouncements < Hyperloop::Store
|
382
|
-
state_reader all: [], scope: :class
|
383
|
-
receives Announcement do
|
384
|
-
mutate.all << params.message
|
385
|
-
after(params.duration) { delete params.message } if params.duration
|
386
|
-
end
|
387
|
-
def self.delete(message)
|
388
|
-
mutate.all.delete message
|
389
|
-
end
|
390
|
-
end
|
391
|
-
```
|
392
|
-
|
393
|
-
#### Channels
|
394
|
-
|
395
|
-
As seen above broadcasting is done over a *Channel*. Any Ruby class (including Operations) can be used as *class channel*. Any Ruby class that responds to the `id` method can be used as an *instance channel.*
|
396
|
-
|
397
|
-
For example the `User` active record model could be a used as channel to broadcast to *all* users. Each user instance could also be a separate instance channel that would be used to broadcast to a specific user.
|
398
|
-
|
399
|
-
The purpose of having channels is to restrict what gets broadcast to who, therefore typically channels represent *connections* to
|
400
|
-
|
401
|
-
+ the application (represented by the `Hyperloop::Application` class)
|
402
|
-
+ or some function within the application (like an Operation)
|
403
|
-
+ or some class which is *authenticated* like a User or Administrator,
|
404
|
-
+ instances of those classes,
|
405
|
-
+ or instances of classes in some relationship - like a `team` that a `user` belongs to.
|
406
|
-
|
407
|
-
You create a channel by including the `Hyperloop::Policy::Mixin`,
|
408
|
-
which gives you three class methods: `regulate_class_connection` `always_allow_connection` and `regulate_instance_connections`. For example:
|
409
|
-
|
410
|
-
```ruby
|
411
|
-
class User < ActiveRecord::Base
|
412
|
-
include Hyperloop::Policy::Mixin
|
413
|
-
regulate_class_connection { self }
|
414
|
-
regulate_instance_connection { self }
|
415
|
-
end
|
416
|
-
```
|
417
|
-
|
418
|
-
will attach the current acting user to the `User` channel (which is shared with all users) and to that user's private channel.
|
419
|
-
|
420
|
-
Both blocks execute with `self` set to the current acting user, but the return value has a different meaning. If `regulate_class_connection` returns any truthy value, then the class level connection will be made on behalf of the acting user. On the other hand if `regulate_instance_connection` returns an array (possibly nested) or Active Record relationship then an instance connection is made with each object in the list. So for example you could add:
|
421
|
-
|
422
|
-
```ruby
|
423
|
-
class User < ActiveRecord::Base
|
424
|
-
has_many chat_rooms
|
425
|
-
regulate_instance_connection { chat_rooms }
|
426
|
-
# we will connect to all the chat room channels we are members of
|
427
|
-
end
|
428
|
-
```
|
429
|
-
|
430
|
-
Now if we want to broadcast to all users our Operation would have
|
431
|
-
|
432
|
-
```ruby
|
433
|
-
dispatch_to { User } # dispatch to the User class channel
|
434
|
-
```
|
435
|
-
|
436
|
-
or to send an announcement to a specific user
|
437
|
-
|
438
|
-
```ruby
|
439
|
-
class PrivateAnnouncement < Hyperloop::ServerOp
|
440
|
-
param :receiver
|
441
|
-
param :message
|
442
|
-
# dispatch_to can take a block if we need to
|
443
|
-
# dynamically compute the channels
|
444
|
-
dispatch_to { params.receiver }
|
445
|
-
end
|
446
|
-
...
|
447
|
-
# somewhere else in the server
|
448
|
-
PrivateAnnouncement(receiver: User.find_by_login(login), message: 'log off now!')
|
449
|
-
```
|
450
|
-
|
451
|
-
The above will work if `PrivateAnnouncement` is invoked from the server, but usually some other client would be sending the message so the operation could look like this:
|
452
|
-
|
453
|
-
```ruby
|
454
|
-
class PrivateAnnouncement < Hyperloop::ServerOp
|
455
|
-
param :acting_user
|
456
|
-
param :receiver
|
457
|
-
param :message
|
458
|
-
validate { raise Hyperloop::AccessViolation unless params.acting_user.admin? }
|
459
|
-
validate { params.receiver = User.find_by_login(receiver) }
|
460
|
-
dispatch_to { params.receiver }
|
461
|
-
end
|
462
|
-
```
|
463
|
-
|
464
|
-
Now on the client we can say:
|
465
|
-
|
466
|
-
```ruby
|
467
|
-
PrivateAnnouncement(receiver: login_name, message: 'log off now!').fail do
|
468
|
-
alert('message could not be sent')
|
469
|
-
end
|
470
|
-
```
|
471
|
-
|
472
|
-
and elsewhere in the client code we would have a component like this:
|
473
|
-
|
474
|
-
```ruby
|
475
|
-
class Alerts < Hyperloop::Component
|
476
|
-
include Hyperloop::Store::Mixin
|
477
|
-
# for simplicity we are going to merge our store with the component
|
478
|
-
state alert_messages: [] scope: :class
|
479
|
-
receives PrivateAnnouncement { |params| mutate.alert_messages << params.message }
|
480
|
-
render(DIV, class: :alerts) do
|
481
|
-
UL do
|
482
|
-
state.alert_messages.each do |message|
|
483
|
-
LI do
|
484
|
-
SPAN { message }
|
485
|
-
BUTTON { 'dismiss' }.on(:click) { mutate.alert_messages.delete(message) }
|
486
|
-
end
|
487
|
-
end
|
488
|
-
end
|
489
|
-
end
|
490
|
-
end
|
491
|
-
```
|
492
|
-
|
493
|
-
This will (in only 28 lines of code)
|
494
|
-
+ associate a channel with each logged in user
|
495
|
-
+ invoke the PrivateAnnouncement Operation on the server (remotely from the client)
|
496
|
-
+ validate that there is a logged in user at that client
|
497
|
-
+ validate that we have a non-nil, non-blank receiver and message
|
498
|
-
+ validate that the acting_user is an admin
|
499
|
-
+ lookup the receiver in the database under their login name
|
500
|
-
+ dispatch the parameters back to any clients where the receiver is logged in
|
501
|
-
+ those clients will update their alert_messages state and
|
502
|
-
+ display the message
|
503
|
-
|
504
|
-
|
505
|
-
The `dispatch_to` callback takes a list of classes, representing *Channels.* The Operation will be dispatched to all clients connected on those Channels. Alternatively `dispatch_to` can take a block, a symbol (indicating a method to call) or a proc. The block, proc or method should return a single Channel, or an array of Channels, which the Operation will be dispatched to. The dispatch_to callback has access to the params object. For example we can add an optional `to` param to our Operation, and use this to select which Channel we will broadcast to.
|
506
|
-
|
507
|
-
```ruby
|
508
|
-
class Announcement < Hyperloop::Operation
|
509
|
-
param :message
|
510
|
-
param :duration
|
511
|
-
param to: nil, type: User
|
512
|
-
# dispatch to the Users channel only if specified otherwise announcement is application wide
|
513
|
-
dispatch_to { params.to || Hyperloop::Application }
|
514
|
-
end
|
515
|
-
```
|
516
|
-
|
517
|
-
### Defining Connections in ServerOps
|
518
|
-
|
519
|
-
The policy methods `always_allow_connection` and `regulate_class_connection` may be used directly in a ServerOp class. This will define a channel dedicated to that class, and will also dispatch to that channel when the Operation completes.
|
520
|
-
|
521
|
-
```ruby
|
522
|
-
class Announcement < HyperLoop::ServerOp
|
523
|
-
# all clients will have a Announcement Channel which will
|
524
|
-
# receive all dispatches from the Annoucement Operation
|
525
|
-
always_allow_connection
|
526
|
-
end
|
94
|
+
# config/routes.rb
|
95
|
+
mount Hyperloop::Engine => '/hyperloop'
|
527
96
|
```
|
528
97
|
|
529
|
-
|
530
|
-
class AdminOps < HyperLoop::ServerOp
|
531
|
-
# subclasses can be invoked from the client if an admin is logged in
|
532
|
-
# and all other clients that have a logged in admin will receive the dispatch
|
533
|
-
regulate_class_connection { acting_user.admin? }
|
534
|
-
param :acting_user
|
535
|
-
validate { param.acting_user.admin? }
|
536
|
-
end
|
537
|
-
```
|
538
|
-
|
539
|
-
### Regulating Dispatches in Policy Classes
|
540
|
-
|
541
|
-
Regulations and dispatch lists can be grouped and specified in Policy files, which are by convention kept in the Rails `app/policies` directory.
|
542
|
-
|
543
|
-
```ruby
|
544
|
-
# app/policies/announcement_policy.rb
|
545
|
-
class AnnouncementPolicy
|
546
|
-
always_allow_connection
|
547
|
-
dispatch_to { params.acting_user }
|
548
|
-
end
|
549
|
-
|
550
|
-
# app/policies/user_policy.rb
|
551
|
-
class UserPolicy
|
552
|
-
regulate_instance_connection { self }
|
553
|
-
end
|
554
|
-
```
|
555
|
-
|
556
|
-
### Serialization
|
557
|
-
|
558
|
-
If you need to control serialization and deserialization across the wire you can define the following *class* methods:
|
559
|
-
|
560
|
-
```ruby
|
561
|
-
def self.serialize_params(hash)
|
562
|
-
# receives param_name -> value pairs
|
563
|
-
# return an object ready for to_json
|
564
|
-
# default is just return the input hash
|
565
|
-
end
|
98
|
+
### Operation Folder Structure
|
566
99
|
|
567
|
-
|
568
|
-
# recieves whatever was returned from serialize_to_server
|
569
|
-
# (param_name => value pairs by default)
|
570
|
-
# must return a hash of param_name => value pairs
|
571
|
-
# by default this returns object
|
572
|
-
end
|
573
|
-
|
574
|
-
def self.serialize_response(object)
|
575
|
-
# receives the object ready for to_json
|
576
|
-
# by default this returns object
|
577
|
-
end
|
578
|
-
|
579
|
-
def self.deserialize_response(object)
|
580
|
-
# receives whatever was returned from serialize_response
|
581
|
-
# by default this returns object
|
582
|
-
end
|
583
|
-
|
584
|
-
def self.serialize_dispatch(hash)
|
585
|
-
# input is always key - value pairs
|
586
|
-
# return an object ready for to_json
|
587
|
-
# default is just return the input hash
|
588
|
-
end
|
589
|
-
|
590
|
-
def self.deserialize_dispatch(object)
|
591
|
-
# recieves whatever was returned from serialize_to_server
|
592
|
-
# (param_name => value pairs by default)
|
593
|
-
# must return a hash of param_name => value pairs
|
594
|
-
# by default this returns object
|
595
|
-
end
|
596
|
-
```
|
597
|
-
|
598
|
-
### Isomorphic Operations
|
599
|
-
|
600
|
-
Unless the Operation is a Server Operation it will run where it was invoked. This can be handy if you have an Operation that needs to run on both the server and the client. For example an Operation that calculates the customers discount, will want to run on the client so the user gets immediate feedback, and then will be run again on the server when the order is submitted as a double check.
|
601
|
-
|
602
|
-
### Dispatching With New Parameters
|
603
|
-
|
604
|
-
The `dispatch` method sends the `params` object on to any registered receivers. Sometimes it's useful for the to add additional outbound params before dispatching. Additional params can be declared using the `outbound` macro:
|
605
|
-
|
606
|
-
```ruby
|
607
|
-
class AddItemToCart < Hyperloop::Operation
|
608
|
-
param :sku, type: String
|
609
|
-
param qty: 1, type: Integer, minimum: 1
|
610
|
-
outbound :available
|
100
|
+
Your Isomorphic Operations live in a `app/hyperloop/operations` folder and your server only Operations in `app/operations`
|
611
101
|
|
612
|
-
|
613
|
-
step { |response| params.available = response.to_i }
|
614
|
-
step { fail if params.qty > params.available }
|
615
|
-
dispatch
|
616
|
-
end
|
617
|
-
```
|
618
|
-
|
619
|
-
### Instance Verses Class Execution Context
|
620
|
-
|
621
|
-
Normally the Operation's steps are declared and run in the context of an instance of the Operation. An instance of the Operation is created, runs and is thrown away.
|
622
|
-
|
623
|
-
Sometimes it's useful to run a step (or other macro such as `validate`) in the context of the class. This is useful especially for caching values between calls to the Operation. You can do this by defining the steps in the class context, or by providing the option `scope: :class` to the step.
|
624
|
-
|
625
|
-
Note that the primary use should be in interfacing to outside APIs. Don't hide your application state inside an Operation - Move it to a Store.
|
626
|
-
|
627
|
-
```ruby
|
628
|
-
class GetRandomGithubUser < Hyperloop::Operation
|
629
|
-
def self.reload_users
|
630
|
-
@promise = HTTP.get("https://api.github.com/users?since=#{rand(500)}").then do |response|
|
631
|
-
@users = response.json.collect do |user|
|
632
|
-
{ name: user[:login], website: user[:html_url], avatar: user[:avatar_url] }
|
633
|
-
end
|
634
|
-
end
|
635
|
-
end
|
636
|
-
self.class.step do # as one big step
|
637
|
-
return @users.delete_at(rand(@users.length)) unless @users.blank?
|
638
|
-
reload_users unless @promise && @promise.pending?
|
639
|
-
@promise.then { run }
|
640
|
-
end
|
641
|
-
end
|
642
|
-
# or
|
643
|
-
class GetRandomGithubUser < Hyperloop::Operation
|
644
|
-
class << self # as 4 steps - whatever you like
|
645
|
-
step { succeed! @users.delete_at(rand(@users.length)) unless @users.blank? }
|
646
|
-
step { succeed! @promise.then { run } if @promise && @promise.pending? }
|
647
|
-
step { self.class.reload_users }
|
648
|
-
async { @promise.then { run } }
|
649
|
-
end
|
650
|
-
end
|
651
|
-
```
|
652
|
-
|
653
|
-
An instance of the operation is always created to hold the current parameter values, dispatcher, etc. The first parameter to a class level `step` block or method (if it takes parameters) will always be the instance.
|
654
|
-
|
655
|
-
```ruby
|
656
|
-
class Interesting < Hyperloop::Operation
|
657
|
-
param :increment
|
658
|
-
param :multiply
|
659
|
-
outbound :result
|
660
|
-
outbound :total
|
661
|
-
step scope: :class { @total ||= 0 }
|
662
|
-
step scope: :class { |op| op.params.result = op.params.increment * op.params.multiply }
|
663
|
-
step scope: :class { |op| op.params.total = (@total += op.params.result) }
|
664
|
-
dispatch
|
665
|
-
end
|
666
|
-
```
|
667
|
-
|
668
|
-
### The `Hyperloop::Application::Boot` Operation
|
669
|
-
|
670
|
-
Hyperloop includes one predefined Operation, `Hyperloop::Application::Boot`, that runs at system initialization. Stores can receive `Hyperloop::Application::Boot` to initialize their state. To reset the state of the application you can simply execute `Hyperloop::Application::Boot`
|
671
|
-
|
672
|
-
### Flux and Operations
|
673
|
-
|
674
|
-
Hyperloop is a merger of the concepts of the Flux pattern, the [Mutation Gem](https://github.com/cypriss/mutations), and Trailblazer Operations.
|
675
|
-
|
676
|
-
We chose the name `Operation` rather than `Action` or `Mutation` because we feel it best captures all the capabilities of a `Hyperloop::Operation`. Nevertheless Operations are fully compatible with the Flux Pattern.
|
677
|
-
|
678
|
-
| Flux | HyperLoop |
|
679
|
-
|-----| --------- |
|
680
|
-
| Action | Hyperloop::Operation subclass |
|
681
|
-
| ActionCreator | `Hyperloop::Operation.step/failed/async` methods |
|
682
|
-
| Action Data | Hyperloop::Operation parameters |
|
683
|
-
| Dispatcher | `Hyperloop::Operation#dispatch` method |
|
684
|
-
| Registering a Store | `Store.receives` |
|
685
|
-
|
686
|
-
In addition Operations have the following capabilities:
|
687
|
-
|
688
|
-
+ Can easily be chained because they always return promises.
|
689
|
-
+ Clearly declare both their parameters, and what they will dispatch.
|
690
|
-
+ Parameters can be validated and type checked.
|
691
|
-
+ Can run remotely on the server.
|
692
|
-
+ Can be dispatched from the server to all authorized clients.
|
693
|
-
+ Can hold their own state data when appropriate.
|
694
|
-
|
695
|
-
## Documentation and Help
|
696
|
-
|
697
|
-
+ Please see the [ruby-hyperloop.io](http://ruby-hyperloop.io/) website for documentation.
|
698
|
-
+ Join the Hyperloop [gitter.io](https://gitter.im/ruby-hyperloop/chat) chat for help and support.
|
102
|
+
You will also find an `app/policies` folder with a simple access policy suited for development. Policies are how you will provide detailed access control to your Isomorphic models.
|
699
103
|
|
700
104
|
## Contributing
|
701
105
|
|
702
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/ruby-hyperloop/hyper-
|
106
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ruby-hyperloop/hyper-operation. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Code of Conduct](https://github.com/ruby-hyperloop/hyper-operation/blob/master/CODE_OF_CONDUCT.md) code of conduct.
|
703
107
|
|
704
108
|
## License
|
705
109
|
|
706
|
-
=======
|
707
|
-
>>>>>>> 14990fb3321e5a8b1cc1cb2d859d747695ffd907
|
708
110
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/lib/hyper-operation.rb
CHANGED