-
Notifications
You must be signed in to change notification settings - Fork 18
00 FINAL
Build interactive Web applications quickly. Hyperloop encourages rapid development with clean, pragmatic design. With developer productivity as our highest goal, Hyperloop takes care of much of the hassle of Web development, so you can focus on innovation and delivering end-user value.
One language. One model. One set of tests. The same business logic and domain models running on the clients and the server. Hyperloop is fully integrated with Rails and also gives you unfettered access to the complete universe of JavaScript libraries (including React) from within your Ruby code. Hyperloop lets you build beautiful interactive user interfaces in Ruby.
Everything has a place in our architecture. Components deliver interactive user experiences, Operations encapsulate business logic, Models magically synchronize data between clients and servers, Policies govern authorization and Stores hold local state.
- Hyperloop Components ---> HyperReact Gem
- Hyperloop Models ---> HyperMesh Gem
- Hyperloop Stores ---> HyperStore Gem
- Hyperloop Operations & Policies ---> HyperOperation Gem
- Hyperloop Specs ---> HyperSpec Gem
- Hyperloop Rails Installer ---> HyperRails Gem
Note on terminology: It's very refreshing to speak about Hyperloop Components, Stores, and Models instead of HyperReact components or HyperMesh models. Outside of the Hyperloop qualifier (which is rarely needed) you just flow into Components, Stores, and Models. I believe we need to update everything to read that way - only referencing the Gem by name when it is necessary to do so “Components are found in the HyperReact Gem, Stores are defined in the HyperStore Gem”, but outside of that the Gem names do not need to be a part of the description or text. This feels like it is working well, and unless anyone disagrees I am updating everything to read that way.
Hyperloop's primary goal is to allow you to enjoy quickly building modern interactive web applications.
Hyperloop includes full access to the entire Rails ecosystem and universe of front-end JavaScript libraries like React and jQuery - all using one great language - Ruby.
Hyperloop lets you write code that is directed toward solving the user's needs in the most straightforward manner, without redundant code, unnecessary APIs, or artificial separation between client and server.
Hyperloop is an Isomorphic Web Application Framework consisting of Components, Operations, Models, Policies, and Stores. This structure is analogous to and replaces the older MVC architecture, but with a more logical and finer grained division of labor.
- Components describe how the UI will display the current application state and how it will handle user actions. React Components automatically rerender parts of the display as state changes due to local or remote activities.
- Operations encapsulate activities. In an MVC architecture operations end up either in controllers, models or some other secondary construct such as service objects, helpers, or concerns. Here they are first class objects.
- Models can now focus on one thing and that is the structure of the data as it is persisted. Any business logic is moved to Operations.
- Policies keep authorization logic out of models, and operations, and also allows the isomorphic transport mechanism to know what and when to communicate between client and server.
- Stores hold application state. Stores are Ruby classes that keep the dynamic parts of the state in special state variables.
The only remaining function of controllers - acting as the HTTP end point - is automatically handled by the Hyperloop framework. Both Models and Operations will automatically work across the wire using the Policies to determine access rights, and reactive Components to update the display as data changes.
This overview will take you through all the main Hyperloop architectural layers.
You build your UI using React Components described as Ruby classes. Within your Components, you can display other components, change state, access models, or communicate with third party APIs. Typically you will want to use Operations to encapsulate these activities. Here is a simple example using an AddBookToBasket
operation, which will define later.
class BookList < React::Component::Base
# Display each book in our catalog unless it's already in the cart basket.
# When the user clicks on a book, add it to the Basket.
render(UL) do
Book.all.each do |book|
LI { "Add #{book.name}" }.on(:click) do
AddBookToBasket(book: book)
end unless acting_user.basket.include? book
end
end
end
Notice how our component directly scopes the Book
model and reads the name
attribute. Models are dynamically synchronized to all connected and authorized clients using ActionCable, pusher.com or polling. The synchronization is completely automatic and magical to behold.
Hyperloop recommends that only scopes, relations, and validations are described in model classes. All business logic can be encapsulated in reusable isomorphic Operations that do not complicate your Models or Components. Each Operation is a self-contained piece of logic that does one simple thing, as defined by a series of steps.
class AddToActingUsersWatchList < Hyperloop::ServerOp
# Add a book to the current acting_user's watch list, and
# send an initial email about the book.
param :acting_user
param :book, type: Book
# Operations will automatically be supplied with the
# current 'acting_user'.
step { succeed! if params.acting_user.watch_list.include? params.book }
step { WatchList.create(user: params.acting_user, book: params.book) }
step { |watch_list| WatchListMailer.new_book_email watch_list }
end
Pretty simple. Typically code like this might be found in a controller which makes it hard to reuse or in a model which makes maintenance difficult when business logic changes. Placing it in its own Operation
makes it easy to maintain, reuse and test.
Of course, Operations can invoke other Operations:
class AddBookToBasket < Hyperloop::ServerOp
# Add a book to the basket and add to users watchlist
param :acting_user
param :book, type: Book
step { params.acting_user.basket << book }
step { AddToActingUsersWatchList(params) }
end
Hyperloop uses Rails ActiveRecord for data persistence. This allows easy integration with existing Rails apps. Hyperloop Models are implemented in the HyperMesh Gem.
HyperMesh gives you full access to the ActiveRecord models on the client or the server which means we can use the models directly within our Components without needing the abstraction of an API:
class BookList < React::Component::Base
# Display each book in the catalog
render(UL) do
Book.in_catalog.each do |book|
LI { book.name }
end
end
end
Changes made to Models on a client or server are automatically synchronized to all other authorized connected clients using ActionCable, pusher.com or polling. The synchronization is completely automatic and magical to behold.
While communication between the client and server is automatic it does need to be authorized. This is accomplished by regulations which can be grouped into pundit style Policy classes. This allows your access rules to be described separately from your Models.
class BookPolicy
regulate_broadcast do |policy|
# allow the entire application to see all book attributes
# except the 'unit_cost'.
policy.send_all_but(:unit_cost).to(Application)
end
# but only acting_user's who are admins can make changes to Books
allow_change(on: [:create, :update]) { acting_user.admin? }
end
Stores are where the state of your Application lives.
Anything but a completely static web page will have dynamic states that change because of user inputs, the passage of time, or other external events.
Stores are Ruby classes that keep the dynamic parts of the state in special state variables
For example here is Store that keeps track of time at a given location:
class WorldClock < Hyperloop::Store
# Keep track of the time at multiple locations
attr_reader :name
attr_reader :lattitude
attr_reader :longitude
attr_reader :time_zone_offset
def current_time
WorldClock.gmt+time_zone_offset
end
def initialize(name, lattitude, longitude, time_zone_offset)
@name, @lattitude, @longitude, @time_zone_offset =
[name, lattitude, longitude, time_zone_offset]
end
def WorldClock.gmt
unless state.gmt
every(1) { mutate.gmt Time.now.gmt }
mutate.gmt Time.now
end
state.gmt
end
end
Now we can create a clock and post the time to the console every minute like this:
new_york = WorldClock.new('New York', 40.7128, -74.0059, 5.hours)
every(1.minute) { puts new_york.current_time }
But because it is a Reactive Store
we can also say this:
# assume we have a div with id='new-york' some place in our code
Element['div#new-york'].render do
"The time in #{new_york.name} is #{new_york.current_time}"
end
This will automatically rerender the contents of the 'new-york' DIV whenever the store changes
How about dispatching, and receiving dispatches ?
- The COMP architecture encapsulates functionality for clean, predictable, testable code
- One language - Ruby everywhere - reduces complexity and lets developers build solutions quickly
- Encourages client-side execution for distributed processing and a rich interactive user experience
- Full power of Rails, React and the entire JavaScript universe
- Transparent, automatic and secure client-server communication built into Models and Operations