Project

sansom

0.0
No commit activity in last 3 years
No release in over 3 years
Scientific, philosophical, abstract web 'picowork' named after Sansom street in Philly, near where it was made. It's under 200 lines of code & it's lightning fast. It uses tree-based route resolution.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 1.6

Runtime

~> 1
 Project Readme

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:

  1. The route matching the path and verb. Routes have a sub-order:
    1. "Static" paths
    2. Wildcards (see below)
      1. Full mappings (kinda a non-compete)
      2. Partial mappings
      3. Splats
    3. The first Subsansom that matches the route & verb
    4. 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 any Object methods, including initialize
  • 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++)

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.**