Sansom
No-nonsense web 'picowork' named after Sansom street in Philly, near where it was made.
Philosophy
A framework should not limit you to one way of thinking.
-
You can write a
Sansomable
for each logical unit of your API, but you also don't have to. -
You can also mount existing Rails/Sinatra/Rack apps in your
Sansomable
. But you also don't have to. -
You can write one
Sansomable
for your entire API.
Fuck it.
A tool should do one thing, and do it well. (Unix philosophy)
A web framework is, fundamentally, a tool to connect code to a URL's path. Therefore, a web framework doesn't provide an ORM, template rendering, shortcuts, nor security patches.
A web framework shall remain a framework
No single tool should get so powerful that it can overcome its master. Rails and Sinatra have been doing this: modifying the language beyond recognition. Rails has activerecord and Sinatra's blocks aren't really blocks.
Installation
gem install sansom
Or, you can clone this repo and use gem build sansom.gemspec
to build the gem.
Writing a Sansom app
Writing a one-file application is trivial with Sansom:
# config.ru
require "sansom"
class MyAPI
include Sansomable
def routes
# define routes here
end
end
run MyAPI.new
Defining Routes
Routes are defined through (dynamically resolved) instance methods that correspond to HTTP verbs. They take a path and a block. The block must be able to accept (at least) one argument.
You can either write
require "sansom"
class MyAPI
include Sansomable
def routes
get "/" do |r|
# r is a Rack::Request object
[200, {}, ["hello world!"]]
end
post "/form" do |r|
# return a Rack response
end
end
end
Routes can also be defined like so:
s = MyAPI.new
s.get "/" do |r| # r is a Rack::Request
[200, {}, ["Return a Rack response."]]
end
But let's say you have an existing Sinatra/Rails/Sansom (Rack) app. It's simple: mount them. For example, mounting existing applications can be used to easily version an app:
# config.ru
require "sansom"
class Versioning
include Sansomable
end
s = Versioning.new
s.mount "/v1", MyAPI.new
s.mount "/v2", MyNewAPI.new
run s
Sansom routes vs Sinatra routes
Sansom routes remain true blocks: When a route is mapped, the same block you use is called when a route is matched. It's the same object every time.
Sinatra routes become methods behind the scenes: When a route is matched, Sinatra looks up the method and calls it.
It's a common idiom in Sinatra to use return
to terminate execution of a route prematurely (since Sinatra routes aren't blocks). You must use next
instead (you can relplace all instances of return
with next
).
Before filters
You can write before filters to try to preëmpt request processing. If the block returns anything (other than nil) the request is preëmpted. In that case, the response from the before block is the response for the request.
# app.rb
require "sansom"
s = Sansom.new
s.before do |r|
[200, {}, ["Preëmpted."]] if some_condition
end
You could use this for request statistics, caching, auth, etc.
After filters
Called after a route is called. If they return a non-nil response, that response is used instead of the response from a route. After filters are not called if a before filter preëmpted route execution.
# app.rb
require "sansom"
s = Sansom.new
s.after do |req,res| # req is a Rack::Request and res is the response generated by a route.
next [200, {}, ["Postëmpted."]] if some_condition
end
Errors
Error blocks allow for the app to return something parseable when an error is raised.
require "sansom"
require "json"
s = Sansom.new
s.error do |err,r| # err is the error, r is a Rack::Request
[500, {"yo" => "headers"}, [{ :message => err.message }.to_json]]
end
There is also a unique error 404 handler:
require "sansom"
require "json"
s = Sansom.new
s.not_found do |r| # r is a Rack::Request
[404, {"yo" => "shit"}, [{ :message => "not found" }.to_json]]
end
Matching
Sansom
uses trees to match routes. It follows a certain set of rules:
- The route matching the path and verb. Routes have a sub-order:
- "Static" paths
- Wildcards (see below)
- Full mappings (kinda a non-compete)
- Partial mappings
- Splats
- The first Subsansom that matches the route & verb
- The first mounted non-
Sansom
rack app matching the route
Wildcards
Sansom supports multiple wildcards:
/path/to/:resource/:action
- Full mapping
/path/to/resource.<format>
- Partial mapping
/path/to/*.json
- Splat
/path/to/*.<format>.<compression>
- You can mix them.
Mappings map part of the route (for example format
above) to the corresponding part of the matched path (for /resource.<format>
and /resource.json
yields a mapping of format
:json
).
Mappings (full and partial) are available in Rack::Request#params
by name, and splats are available under the key splats
in Rack::Request#params
.
See the Matching section of this readme for wildcard precedence.
Notes
-
Sansom
does not pollute anyObject
methods, includinginitialize
- No regexes are used in the entire project.
- Has one dependency:
rack
-
Sansom
is under 400 lines of code at the time of writing. This includes- Rack conformity & the DSL (
sansom.rb
) (~90 lines) - Custom tree-based routing (
sanom/pine.rb
) (~150 lines) - libpatternmatch (~150 lines of C++)
- Rack conformity & the DSL (
Speed
Well, that's great and all, but how fast is "hello world" example in comparision to Rack or Sinatra?
Rack: 11ms
Sansom: 14ms*†
Sinatra: 28ms
Rails: 34ms**
(results are measured locally using Puma and are rounded down)
Hey Konstantine, put that in your pipe and smoke it.
* Uncached. If a tree lookup is cached, it takes the same time as Rack.
† Sansom's speed (compared to Sinatra) may be because it doesn't load any middleware by default.
** Rails loads a rich welcome page which may contribute to its slowness
Todo
- Multiple return types for routes
If you have any ideas, let me know!
Contributing
You know the drill. But ** make sure you don't add tons and tons of code. Part of Sansom
's beauty is its brevity.**