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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 344a08b1fd7ca7f8a79f125a61a65efd0b3aba81
4
- data.tar.gz: b6117c9e5341d27179eba0ce900b0f297cf0a0d4
3
+ metadata.gz: ae0c4ee46f852a8120a9116a6e90ca9198f131ac
4
+ data.tar.gz: 6cb26e37bcbe79c1a6c639f5a6f50c5f6c3dfa95
5
5
  SHA512:
6
- metadata.gz: d548bd54f1af9565e4ecd786d754c2206964d6bbcded8a628b4b4e6fdd71b440abb7c2e8f84bf3996edcf212e056b2f5a786236a53f85c523d80dc2daaeed617
7
- data.tar.gz: 92f1934159ab8cc398b3dea11640cacbae56f2f8b92727606e844352b9295a5843ec663399eb76f1ec78149c390ef65630ca5c3fd29d5e7076917aa8b779f8f8
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
- ## Basic Installation and Setup
18
+ ## Installation and Setup
19
19
 
20
- The easiest way to install is to use the `hyper-rails` gem.
20
+ **Note: Operations require Rails currently.**
21
21
 
22
- <<<<<<< HEAD
23
- ### Installation
22
+ ### Easy Installation
24
23
 
25
- **Note: only runs with rails currently.**
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
- ### Operation Structure
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
- class AdminOnlyOp < Hyperloop::ServerOp
354
- param :acting_user
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
- ```ruby
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
- def self.deserialize_params(object)
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
- step { HTTP.get('/inventory/#{params.sku}/qty') }
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-store. 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-store/blob/master/CODE_OF_CONDUCT.md) code of conduct.
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).
@@ -1,5 +1,6 @@
1
1
  require "hyper-operation/version"
2
2
  require 'hyperloop-config'
3
+ Hyperloop.import 'browser/interval', client_only: true
3
4
  Hyperloop.import 'hyper-operation'
4
5
 
5
6
  if RUBY_ENGINE == 'opal'
@@ -1,5 +1,5 @@
1
1
  module Hyperloop
2
2
  class Operation
3
- VERSION = '0.5.1'
3
+ VERSION = '0.5.2'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hyper-operation
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - catmando