Skip to content

Instantly share code, notes, and snippets.

@JoshCheek
Last active February 1, 2018 17:19
Show Gist options
  • Save JoshCheek/121e587101d397cbfbf3 to your computer and use it in GitHub Desktop.
Save JoshCheek/121e587101d397cbfbf3 to your computer and use it in GitHub Desktop.
A small Sinatra clone

A small clone of Sinatra

For the purpose of learning.

Run the example app

$ gem install rack
$ rackup config.ru -p 9292

Get/Post + params

Now go to http://localhost:9292/greet and you'll be presented with a form. When you submit it, it will post form data to '/greet'. Refresh to get the form again.

Redirecting

Go to http://localhost:9292/turing and you will be redirected to http://www.turing.io

Content Type

Go to http://localhost:9292/the-jsons?this-is-a-key=this-is-a-value&abc=123, you will see the params presented in a JSON object, with the content type set correctly (if you have a fancy JS viewer, it will be triggered by the content-type and display the JSON all fancy-like).

Mildly related

License

Do what the fuck you want to

require_relative 'fake_sinatra'
class MyApp < FakeSinatra::Base
# I've also written some erb parsers https://gist.github.com/JoshCheek/2b30b052560337522f94
# but didn't bring them in for this app
def layout_around(html)
"
<DOCTYPE html!>
<html>
<head><title>Welcome!</title></head>
<body>
#{html}
</body>
</html>
"
end
get '/greet' do
layout_around '
<form action="/greet" method="post">
<label for="name_field">Name:</label>
<input type="text" id="name_field" name="name" />
</form>
'
end
post '/greet' do
layout_around(params[:name] && "<p>Hello, #{params[:name]}</p>")
end
get '/turing' do
redirect 'http://www.turing.io'
end
get '/the-jsons' do
require 'json'
content_type 'application/json'
{params: params}.to_json
end
end
run MyApp
module FakeSinatra
class Base
def self.call(env)
controller = controller_for env['REQUEST_METHOD'].downcase.intern, env['PATH_INFO']
new(env, controller).rack_response
end
def self.controller_for(request_method, request_path)
routes.each do |method, path, controller|
return controller if request_method == method && request_path == path
end
lambda { status 404 }
end
def self.params_from(env)
params = Hash.new { |_, key| params[key.to_s] if key.kind_of? Symbol }
params.merge! parse_params(env['QUERY_STRING'])
params.merge! parse_params(env['rack.input'].read(env['CONTENT_LENGTH'].to_i)) if params_in_body? env
params
end
def self.params_in_body?(env)
env['CONTENT_LENGTH'] && env['REQUEST_METHOD'] != 'GET' && env['CONTENT_TYPE'] == 'application/x-www-form-urlencoded'
end
def self.parse_params(urlencoded)
urlencoded.split('&').map { |kv| kv.split '=', 2 }.to_h
end
def self.routes
@routes ||= []
end
def self.get(path, &controller)
routes << [:get, path, controller]
end
def self.post(path, &controller)
routes << [:post, path, controller]
end
def self.put(path, &controller)
routes << [:put, path, controller]
end
def self.patch(path, &controller)
routes << [:patch, path, controller]
end
def self.delete(path, &controller)
routes << [:delete, path, controller]
end
attr_reader :controller, :env, :params
attr_accessor :response_status, :response_headers, :response_body
def initialize(env, controller)
@controller, @env, @params = controller, env, self.class.params_from(env)
self.response_headers, self.response_body = {}, []
status 200
content_type 'text/html'
end
def run
catch(:finished) { body instance_exec &controller }
end
def status(code)
self.response_status = code
end
def body(value)
return unless value.kind_of? String
self.response_body = [value]
end
def content_type(type)
response_headers['Content-Type'] = type
end
def redirect(location)
status 302
response_headers['Location'] = location
throw :finished
end
def rack_response
run
[response_status, response_headers, response_body]
end
end
end
require_relative 'fake_sinatra'
require 'rack/test'
require 'stringio'
RSpec.describe FakeSinatra do
def assert_request(app, method, path, assertions, &overrides)
env = Rack::MockRequest.env_for path, method: method
overrides.call env if overrides
status, headers, body = app.call(env)
assertions.each do |name, expectation|
case name
when :body
expect(body.join).to eq expectation
when :status
expect(status).to eq expectation
when :content_type
expect(headers['Content-Type']).to eq expectation
when :location
expect(headers['Location']).to eq expectation
else
raise "Unexpected assertion: #{name.inspect}"
end
end
end
describe 'routing to a block' do
it 'routes based on the method (get/post/put/patch/delete)' do
app = Class.new FakeSinatra::Base do
get('/') { 'get request to /' }
post('/') { 'post request to /' }
put('/') { 'put request to /' }
patch('/') { 'patch request to /' }
delete('/') { 'delete request to /' }
end
assert_request app, :get, '/', body: 'get request to /'
assert_request app, :post, '/', body: 'post request to /'
assert_request app, :put, '/', body: 'put request to /'
assert_request app, :patch, '/', body: 'patch request to /'
assert_request app, :delete, '/', body: 'delete request to /'
end
it 'routes based on the path' do
app = Class.new FakeSinatra::Base do
get('/a') { 'first' }
get('/b') { 'second' }
end
assert_request app, :get, '/a', body: 'first'
assert_request app, :get, '/b', body: 'second'
end
it 'routes based on both of these together' do
app = Class.new FakeSinatra::Base do
get('/a') { 'first' }
get('/b') { 'second' }
post('/a') { 'third' }
post('/b') { 'fourth' }
end
assert_request app, :get, '/a', body: 'first'
assert_request app, :post, '/a', body: 'third'
assert_request app, :get, '/b', body: 'second'
assert_request app, :post, '/b', body: 'fourth'
end
it 'returns a 404 when it can\'t find a match' do
app = Class.new(FakeSinatra::Base) { get('/a') { '' } }
assert_request app, :get, '/a', status: 200
assert_request app, :get, '/b', status: 404
end
end
describe 'routed code' do
it 'returns the result as the body' do
app = Class.new(FakeSinatra::Base) { get('/') { 'the body' } }
assert_request app, :get, '/', body: 'the body'
end
it 'has an empty body if the block evaluates to a non-string' do
app = Class.new(FakeSinatra::Base) { get('/') { } }
assert_request app, :get, '/', body: ''
end
end
describe 'the block of code' do
it 'defaults the content-type to text/html, but allows it to be overridden' do
app = Class.new FakeSinatra::Base do
get('/a') { }
get('/b') { content_type 'text/plain' }
end
assert_request app, :get, '/a', content_type: 'text/html'
assert_request app, :get, '/b', content_type: 'text/plain'
end
it 'allows the status to be set' do
app = Class.new FakeSinatra::Base do
get('/a') { }
get('/b') { status 400 }
end
assert_request app, :get, '/a', status: 200
assert_request app, :get, '/b', status: 400
end
it 'has access to the params' do
app = Class.new FakeSinatra::Base do
get('/a') { "params: #{params.inspect}" }
end
assert_request app, :get, '/a?b=c', body: 'params: {"b"=>"c"}'
end
it 'has a convenience method "redirect", which sets the status, location, and halts execution' do
app = Class.new FakeSinatra::Base do
get('/a') do
redirect 'http://www.example.com'
raise "should not get here"
end
end
assert_request app, :get, '/a', status: 302, location: 'http://www.example.com', body: ''
end
end
it 'gives access to the env' do
app = Class.new FakeSinatra::Base do
get('/a') { "REQUEST_METHOD: #{env['REQUEST_METHOD']}" }
end
assert_request app, :get, '/a?b=c&d=e', body: 'REQUEST_METHOD: GET'
end
describe 'params' do
describe 'parsing params with a Content-Type of application/x-www-form-urlencoded' do
def assert_parses(urlencoded, expected)
actual = FakeSinatra::Base.parse_params urlencoded
expect(actual).to eq expected
end
it 'splits them on "&"' do
assert_parses 'a=b&c=d', {'a' => 'b', 'c' => 'd'}
end
it 'splits keys and values on the first "="' do
assert_parses 'a=b=c', {'a' => 'b=c'}
end
end
it 'includes query parms' do
app = Class.new FakeSinatra::Base do
get('/a') { "params: #{params.inspect}" }
end
assert_request app, :get, '/a?b=c&d=e', body: 'params: {"b"=>"c", "d"=>"e"}'
end
context 'from form data' do
let(:app) do
Class.new FakeSinatra::Base do
get('/a') { "params: #{params.inspect}" }
post('/a') { "params: #{params.inspect}" }
end
end
it 'does not read the form data when the request is GET' do
assert_request app, :get, '/a', body: 'params: {}' do |env|
env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
env['CONTENT_LENGTH'] = '7'
env['rack.input'].string = 'a=1'
end
end
it 'does not read the form data when the CONTENT_TYPE is not application/x-www-form-urlencoded' do
assert_request app, :post, '/a', body: 'params: {}' do |env|
env['CONTENT_TYPE'] = 'application/json'
env['CONTENT_LENGTH'] = '7'
env['rack.input'].string = 'a=1'
end
end
it 'does not read the form data when there is no CONTENT_LENGTH' do
assert_request app, :post, '/a', body: 'params: {}' do |env|
env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
env['CONTENT_LENGTH'] = nil
env['rack.input'].string = 'a=1'
end
end
it 'only reads the form data as far as the CONTENT_LENGTH says it should' do
assert_request app, :post, '/a', body: 'params: {"b"=>"c", "d"=>"e"}' do |env|
env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
env['CONTENT_LENGTH'] = '7'
env['rack.input'].string = "b=c&d=eTHIS IS NOT READ"
end
end
end
it 'memoizes the params' do
app = Class.new FakeSinatra::Base do
get('/') { params['a'] = 'b'; params['a'] }
end
assert_request app, :get, '/', body: 'b'
end
it 'returns nil if the param doesn\'t exist' do
app = Class.new FakeSinatra::Base do
get('/') { "nonexistent: #{params['nonexistent'].inspect}" }
end
assert_request app, :get, '/', body: 'nonexistent: nil'
end
it 'allows the params to be accessed with a string or a symbol' do
app = Class.new FakeSinatra::Base do
get('/') { "#{params['key']} #{params[:key]}" }
end
assert_request app, :get, '/?key=value', body: 'value value'
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment