Chassis is a collection of new classes and enhancements to existing projects for building maintainable applications. I choose the name "chassis" because I'm a car guy. A chassis is a car's foundation. Every car has key components: there is an engine, transmission, differential, suspension, electrical system, and a bunch of other things. They fit together on the chassis in a certain way, there are guidelines but no one is going to stop you from building a custom front suspension on a typical chassis. And that's the point. The chassis is there to build on. It does not make decisions for you. There are also kit cars and longblock engines. Kit cars come with some components and rely on you to assemble them. Longblocks are halfway complete engines. The engine block and valve train are predecided. You must decide which fuel delivery and exhaust system to use. Then you mount it in the chassis. In all things there is a line between prepackaged DIY and turn-key solutions. This project is a combination of a chassis and long block. Some things have been predecided and others are left to you. In that sense this project is a utility belt. All the components are there, you just need to figure out how to put them together.
This project chooses an ideal gem stack for building web applications and enhancements to existing projects. It's just a enough structure to build an application. It is the chassis you build your application on.
Here's an example I put together.
Chassis is implemented with help from a few smaller libraries. A unified interface if you do not want to know about such things.
- Errors with TNT
- Object initialization with Lift
- Interchangeable objects with Interchange
Add this line to your application's Gemfile:
gem 'chassis'
And then execute:
$ bundle
Or install it yourself as:
$ gem install chassis
Right off the bat, chassis is for building web applications. It depends on other gems to make that happen. Chassis fully endorses rack & Sinatra as the best way to do this. So it contains enhancements and middleware to make that so.
Chassis::Rack::Bouncer
- takes a block. Used to bounce spam or other undesirable requests.Chassis::Rack::HealthCheck
- for load balanced applications. Takes a block to test if the applications is ready. Failures terminate the process.Chassis::Rack::Instrumentation
- use harness to instrument all request timingsChassis::Rack::NoRobots
- blocks all crawlers and bots.
Chassis::WebService
includes some of these middleware as well as
other customizations.
- requires
sinatra/json
for JSON response generation - requires
rack/contrib/bounce_favicton
because ain't no body got time for that - uses
Chassis::Rack::Bouncer
- uses
Chassis::Rack::NoRobots
- uses
Rack::Deflator
to gzip everything - uses
Rack::PostBodyContentTypeParser
to parse incoming JSON bodies enable :cors
to enable CORS with manifold.- registers error handlers for unknown exceptions coming from other chassis components.
- other misc helpers for generating JSON and handling errors.
Chassis includes a
repository using
the query pattern as well. The repository pattern is perfect because
it does not require knowledge about your persistence layer. It is the
access layer. A null, in-memory, and Redis adapter are included. You
can subclass these adapters to make your own.
Chassis::Repo::Delegation
can be included in other classes to
delegate to the repository.
Here's an example:
class CustomerRepo
extend Chassis::Repo::Delegation
end
Now there are CRUD methods available on CustomerRepo
that delegate
to the repository for Customer
objects. Chassis::Persistence
can
be included in any object. It will make the object compatible with
the matching repo.
class Customer
include Chassis::Persistence
end
Now Customer
responds to id
, save
, and repo
. repo
looks for
a repository class matching the class name (e.g. CustomerRepo
).
Override as you see if.
More on my blog here.
Virtus
and virtus-dirty_attribute
are used to create
Chassis::Form
. It includes a few minor enhancements. All assignments
go through dirty tracking to support the partial update use case.
Chassis::Form#values
will return a hash of everything that's been
assigned. Chassi::Form#attributes
returns a hash for all the
declared attributes. initialize
has been modified as well. Trying to
set an unknown attributes will raise
Chassis::Form::UnknownFieldError
instead of NoMethodError
.
Chassis::WebService
registers an error handler and returns a 400 Bad Request
in this case.
Create a new form by including Chassis.form
class SignupForm
include Chassis.form
end
Chassis uses Faraday because it's the best god damn HTTP client in ruby. Chassis includes a bunch of middleware to make it even better.
Farday.new 'http://foo.com', do |builder|
# Every request is timed with Harness into a namespaced key.
# You can pass a namespace as the second argument: IE "twilio",
# or "sendgrid"
faraday.request :instrumentation
# Send requests with `content-type: application/json` and use
# the standard library JSON to encode the body
faraday.request :encode_json
# Parse a JSON response into a hash
faraday.request :parse_json
# This is the most important one IMO. All requests 4xx and 5xx
# requests will raise a useful error with the response body
# and status code. This is much more useful than the bundled
# implementation. A 403 response will raise a HttpForbiddenError.
# This middleware also captures timeouts.
# Useful for catching failure conditions.
faraday.request :server_error_handler
# Log all requests and responses. Useful when debugging running
# applications
faraday.response :logging
end
There is also a faraday factory that will build new connections using this middleware stack.
# Just like normal, but the aforementioned middleware included.
# Any middleware you insert will come after the chassis ones.
Chassis.faraday 'http://foo.com' do |builder|
# your stuff here
end
Breaker provides the low level
implementation. Chassis::CircuitPanel
is a class for unifying
access to all the different circuits in the application. This is
useful because other parts of the code don't need to know about how
the circuit is implemented. Chassis.circuit_panel
behaves like
Struct.new
. It returns a new class.
CircuitPanel = Chassis.circuit_panel do
circuit :test, timeout: 10, retry_threshold: 6
end
panel = CircuitPanel.new
circuit = panel.test
circuit.class # => Breaker::Circuit
circuit.run do
# do your stuff here
end
Since Chassis.circuit_panel
returns a class, you can do anything you
want. Don't like to have to instantiate a new instance every time? Use
a singleton and assign that to a constant.
require 'singleton'
CircuitPanel = Chassis.circuit_panel do
include Singleton
circuit :test, timeout: 10, retry_threshold: 6
end.instance
CircuitPanel.test.run do
# your stuff here
end
A proxy object used to track assignments. Wrap an object in a dirty session to see what changed and what it changed to.
Person = Struct.new :name
adam = Person.new 'adam'
session = Chassis::DirtySession.new adam
session.clean? # => true
session.dirty? # => false
session.name = 'Adman'
session.dirty? # => true
session.clean? # => false
session.named_changed? # => true
session.changed # => set of values changed
session.new_values # => { name: 'Adman' }
session.original_values # => { name: 'adam' }
session.reset! # reset everything back to normal
Chassis includes the logger-better
gem to refine the standard
library logger. Chassis::Logger
default the logdev
argument to
Chassis.stream
. This gives a unified place to assign all output.
The log level can also be controlled by the LOG_LEVEL
environment
variable. This makes it possible to restart/boot the application with
a new log level without redeploying code.
A very simple implementation of the observer pattern. It is different from the standard library implementation for two reasons:
- you don't need to call
changed
fornotify_observers
to work. notify_obsevers
includesself
as first argument to all observers- there is only the
add_observer
method.
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request