WABuR is a Web Application Builder using Ruby. It strives to be simple to use and maintain not only for the trivial case but for more advanced applications as well. It utilizes a Mode/View/Controller pattern with well defined APIs between each element of the MVC approach.
One of the more interesting benefits of well defined APIs between MVC
components is that it allows multiple Views without changing any other
components. A View can be a single page JavaScript application as is used in
the WABuR reference implementation or it can simply be a curl
command used
to send HTTP requests as is used in later lessons in the tutorial.
The architecture provides many options but keeps clean and clear API between components. This pluggable design allows for unit test drivers and various levels of deployment options from straight Ruby to a high performance C runner that handles HTTP and data storage.
Three configuration are available. One uses a Runner that calls to the Ruby
core controller through pipes on $stdin
and $stdout
. A second is
to implement a runner in Ruby (bin/wabur). The third is to use a C Runner with
embedded Ruby (OpO-Rub).
The WAB architecture is a Mode View Controller with clear APIs between each part of the MVC. The design allows the non-business related tasks such as the HTTP server and data store to be treated as service to the Controller which contains the business logic.
A Runner that spawns (forks) and runs a Ruby Controller makes use of the
::WAB::IO::Shell
.
The Ruby Runner and C Runner with embedded ruby follow the same architecture.
Access to data can follow two paths. A direct access to the data is possible as portrayed by the red line that flows from HTTP server to the runner and onto the Model. The other path is to dive down into the Ruby Controller and allow the Controller to modify and control what is returned by a request. The Benchmark results in the (benchmarks/README.md)[benchmarks/README.md] includes the latest results.
Simplified the logical view is:
The Model View Controller pattern is a well known and widely accepted design pattern. It is also the pattern used by Rails. WAB adheres to this model with well defined APIs that are used exclusively.
The MVC pattern has many variants with some functionality being in different components depending on how the lines are drawn between those components. No matter what the separation, if it is clear many issues can be avoided.
The Model component is responsible for persisting data, providing search and retrieval capabilities, and assuring stored consistency. The Model does not assure business logic consistency nor does it enforce relationships between data elements. It is a data store only.
The data in the system is follows a JSON structural model. This make a NoSQL database an ideal store. It does mean that the Model data is unstructured in that there is no schema enforced by the data store. It does not mean that relationships do not exist in the data store.
With well defined APIs between the Model and the Controller almost any data store can be used as long as an adapter is written. This allows options such as MongoDB, Redis, OpO, a file based store, or even an in memory store for testing.
One feature that may not be supported by all stores is the ability to push changes from the data store to the Controller and then up to the View to real time feeds.
An HTTP server provides the View environment. In addition to serving HTML, CSS, images, and other files typically served by a web server the View server also supports exchanging JSON data through a REST API. A WebSocket and SSE capability is also expected and designed for in the API.
Javascript is the suggested language to use for web pages but any approach to accepting and delivering JSON is fine. JaveScript helpers are provided that support the REST API as well as the WebSocket and SSE push APIs.
With two approaches to connecting the Controller to the View and Model there are deployment options that allow trading off latency for throughput. The Controller code is isolated from those deployment choices except for the choice of language. If the Controller is written in Ruby then it can be either embedded in the shell or access as an external application.
The Controller is a bridge between the View and Model and implements the business logic as needed. In the case of a fetch, create, or update options are available to bypass or use the default Controller. The bypass allows a more direct conduit between the View and the Model with the Controller just passing the data along to the model and vice versa.
The Controller can also receive callbacks on changes in the Model which can then be forwarded to the View if a subscription to that data has been made.
All APIs are described with Ruby code as the Controller is expected to be Ruby. If an external Controller is to be used then the external glue API is used. This API is a text based API over a pipe (Unix socket). Events can arrive at the Controller from either the View or Model interface.
Data is loosely represented as JSON. There are some expectations that can be relaxed if desired.
The data exchanged between components follows the JSON model for primitives with a few optional additions. The JSON types can be used exclusively and are:
null
(nil)- boolean (
true
|false
) - string (String)
- number (Integer | Float | BigDecimal)
- object (Hash)
- array (Array)
All string must be UTF-8 or Unicode.
Some other types are also allowed. Each has a defined JSON representation that can be used instead.
- time (Ruby Time encoded in RFC 3339 format if a string)
- UUID (WAB::UUID encoded as defined by RFC 4122)
- IRI (WAB::IRI encoded as defined by RFC 3987)
These types are represented by the WAB::Data class.
While object can be any JSON or WAB::Data they are encouraged to be JSON Objects (Hash) types. I addition one attribute should be used to identify the type. The default is the 'kind' attribute. That attribute is not required to be the 'kind' attribute but can be anywhere in the data tree as long as it is consistent across all types. As an example it could be in 'meta.kind' where that key represents a path with a 'kind' element in a 'meta' object (Hash).
Each object or for that matter every node in a Data tree is assigned a system wide unique identifier. That is used to identfiy each object when using the API. From the view perspective a REST over HTTP is used.
WABuR keeps data and behavior separate. The data portion of a system is
represented by WAB::Data
class. Behavior is implemented by support
classes. This alows extensions to be added without conflict as the Data
class is not modified by adding new features. This avoids the conflicts
encountered with Rails when it monkey patches core classes to add to_json
which conflicts with the JSON gem and locks out other better performing
extensions.
The reasoning behind the separation of data and behavior to improve extendability is that different features often need similar but different features. As an example data might need to be stored in multiple data stores or be encoded for a view, external processing, or writing to a file. If the behavior is encoded in the data object then care must be taken to avoid using the same method names which is difficult if separate authors are contributing or open source gems are used.
The View/Controller API is predominanty from View to Controller but the ability for the Controller to push data to the View is also part of the design. That allows pages to take advantage of WebSockets or SSEs to display data changes as they occur.
See the Controller class documentation for further details.
The Contoller/Model API is driven mostly from Controller to Model with the Model responding to queries. Like the View/Controller API the Model can also push changes to the Controller.
The Model supports basic CRUD operations as well as a query API that uses TQL in both the friendly and JSON formats. Support for GraphQL is anticipated but not for the first iteration.
See the Model class documentation for further details.
As noted in the architecture diagram, the WAB Shell can be either the C or Ruby shell. The Ruby shell is intended for development and possibly small installations. The C WAB Shell or Shells if more thna one is implemented are intended to be high performance environments that the Controller code resides in either as embedded code or through a piped connections.
A C WAB Shell with an embedded Controller utilizes the View and Model APIs directly through the Ruby library. This approach gives more control to the Shell in regard to utilizing threads and C libraries outside of Ruby. An alternative approach is to call C extensions from Ruby but that approach is left for future if it makes sense.
The Ruby WAB Shell implements an HTTP server as well as either an in memory data store or a file based data store.
The APIs are designed to allow relatively direct modifications to the data store if the store supports such.
The embedded Shell should have the lowest latency but may sacrifice throughput depending on the complexity of the Controller code.
Running external Controllers is implemented by the WAB Shell either spawning the Controller application or connecting to an existing one. Multiple Controllers can be active to allow parallel Controller processing. The approach taken is the same as that used Piper Push Cache with the process flow Spawn Actor.
To run a Controller written in Ruby the External Glue is used to bridge the gap between the WAB external API and the Controller APIs. This is a light weught layer that converts to and from the text based pipe API and the Ruby APIs.
With the ability to make use of multiple Controller instances which may reside on different machines the throughput is expected to be higher than the embedded Shell but with a degradation of the latency.
A straight Ruby WAB Shell is the choice for testing and for small installations. The Ruby shell is the first choice when getting started.
Outside of this project WAB C or other language shells can be written. The first is expected to be a derivative of OpO as it takes shape. This WAB Shell will also draw on the libraries use by Piper Push Cache to provide WebSocket and SSE support.
Knowing what the concepts involved in the WABuR architecture and design does not make it obvious where all the parts described reside in the repository. The following directory overlay attempts to provide some of that mapping.
wabur
├── bench
| Benchmark results and code.
├── bin
| Scripts available from the gem such as the wabur app.
├── export
| Files exported by the gem. This is used as an alternate file system
| for the web servers if a filer has not been added by the developer.
├── lib
| └── wab
| | The core module that provides duck-types for Shell and Data. It
| | also contains the default Controller bahavior and some utilities
| | and helpers.
| ├── impl
| | The Impl module provides an implementation of the duck-type
| | found in the wab directory. The wabur pure Ruby runner is
| | implemented in this directory.
| ├── io
| | Implements a stdio shell that is used with a runner that forks
| | a WAB::IO::Shell that then acts as a mechanism for exchanging
| | messages between the Controller and the Runner which
| | implements the Model and provides an HTTP API for Views.
| └── ui
| The reference UI makes use of configuration data to define
| displays and transitions between displays. Displays are
| described mostly by HTML generated by the Ruby code in this
| directory. There is also a Controller class that provides the
| UI configuration just as anu other Controller does for data
| from the model or database.
├── pages
| Additional documentation.
├── script
| Scripts for building and tools for developers.
├── test
| Unit and compatibility tests.
├── tutorial
| Tutorial for building a WABuR application from the ground up.
└── view
| Source code for JavaScript, CSS, fonts, and other View related
| components. This is where the reference UI implemenation sources
| reside or at least the non-configuration defining portions.
├── ui
| ├── js
| | JavaScript sources for the UI reference implementation.
| ├── styles
| | Sass sources for generating the wab.css file.
| └── wabfont
| A font used to provide the icons in the list view.
└── wab
└── wab.js
JavaScript file that implements API wrappers for calls to the
Controllers