little_frankie 1.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/Performance.md +65 -0
- data/README.md +167 -0
- data/Rakefile +1 -0
- data/benchmarks/filters/frankie.rb +18 -0
- data/benchmarks/filters/sinatra.rb +17 -0
- data/benchmarks/helpers/frankie.rb +19 -0
- data/benchmarks/helpers/sinatra.rb +18 -0
- data/benchmarks/simple/frankie.rb +10 -0
- data/benchmarks/simple/sinatra.rb +9 -0
- data/benchmarks/url_pattern/frankie.rb +10 -0
- data/benchmarks/url_pattern/sinatra.rb +8 -0
- data/examples/active_record/.gitignore +1 -0
- data/examples/active_record/Gemfile +14 -0
- data/examples/active_record/Rakefile +51 -0
- data/examples/active_record/config/database.yml +0 -0
- data/examples/active_record/database.rb +12 -0
- data/examples/active_record/db/migrate/20130606133756_add_shouts.rb +12 -0
- data/examples/active_record/models/shout.rb +3 -0
- data/examples/active_record/server.rb +43 -0
- data/examples/json_api.rb +21 -0
- data/examples/templates/server.rb +27 -0
- data/examples/templates/views/index.haml +1 -0
- data/examples/web_sockets/public/FABridge.js +604 -0
- data/examples/web_sockets/public/WebSocketMain.swf +0 -0
- data/examples/web_sockets/public/index.html +76 -0
- data/examples/web_sockets/public/swfobject.js +4 -0
- data/examples/web_sockets/public/web_socket.js +388 -0
- data/examples/web_sockets/server.rb +60 -0
- data/lib/little_frankie.rb +9 -0
- data/lib/little_frankie/app.rb +66 -0
- data/lib/little_frankie/class_level_api.rb +40 -0
- data/lib/little_frankie/primitives.rb +25 -0
- data/lib/little_frankie/request_scope.rb +60 -0
- data/lib/little_frankie/route_signature.rb +44 -0
- data/lib/little_frankie/version.rb +3 -0
- data/little_frankie.gemspec +24 -0
- data/spec/app_spec.rb +144 -0
- data/spec/class_level_api_spec.rb +39 -0
- data/spec/primitives_spec.rb +26 -0
- data/spec/request_scope_spec.rb +71 -0
- data/spec/spec_helper.rb +42 -0
- metadata +138 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
#!ruby -I ../../lib -I lib
|
2
|
+
require 'frankie'
|
3
|
+
require 'faye/websocket'
|
4
|
+
|
5
|
+
#
|
6
|
+
# Open localhost:9000/public/index.html in the browser
|
7
|
+
#
|
8
|
+
|
9
|
+
Faye::WebSocket.load_adapter('thin')
|
10
|
+
|
11
|
+
class WebSockets
|
12
|
+
def initialize app=nil, opts={}, &blk
|
13
|
+
@app = app
|
14
|
+
@path = opts.fetch :path, '/'
|
15
|
+
@blk = blk
|
16
|
+
end
|
17
|
+
|
18
|
+
def call env
|
19
|
+
return @app.call(env) unless env['PATH_INFO'] == @path
|
20
|
+
|
21
|
+
if Faye::WebSocket.websocket?(env)
|
22
|
+
ws = Faye::WebSocket.new(env)
|
23
|
+
|
24
|
+
if @blk
|
25
|
+
Proc.new(&@blk).call ws
|
26
|
+
else
|
27
|
+
handle ws
|
28
|
+
end
|
29
|
+
|
30
|
+
ws.rack_response
|
31
|
+
else
|
32
|
+
@app.call(env)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def handle ws
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class App < Frankie::App
|
41
|
+
#Serve static assets from public folder
|
42
|
+
use Rack::Static, :urls => ["/public"]
|
43
|
+
|
44
|
+
use WebSockets, :path => '/websocket' do |ws|
|
45
|
+
ws.on :message do |event|
|
46
|
+
ws.send(event.data)
|
47
|
+
end
|
48
|
+
|
49
|
+
ws.on :close do |event|
|
50
|
+
p [:close, event.code, event.reason]
|
51
|
+
ws = nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
get '/frankie' do
|
56
|
+
'yep, you can still use frankie'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
App.run! 9000
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module LittleFrankie
|
2
|
+
class App
|
3
|
+
extend ClassLevelApi
|
4
|
+
|
5
|
+
RouteNotFoundError = Class.new StandardError
|
6
|
+
|
7
|
+
def initialize app=nil
|
8
|
+
@app = app || lambda {|env| Response.new '', 404 }
|
9
|
+
build_middleware_chain
|
10
|
+
end
|
11
|
+
|
12
|
+
def build_middleware_chain
|
13
|
+
@top = self.class.middlewares.reverse.reduce (self) do |prev, entry|
|
14
|
+
klass, args, blk = entry
|
15
|
+
klass.new prev, *args, &blk
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.run! port=9292
|
20
|
+
middlewares.unshift Rack::ShowExceptions, Rack::CommonLogger
|
21
|
+
|
22
|
+
begin
|
23
|
+
Rack::Handler::Thin
|
24
|
+
rescue LoadError
|
25
|
+
Rack::Handler::WEBrick
|
26
|
+
end.run new, :Port => port
|
27
|
+
end
|
28
|
+
|
29
|
+
def handler_for_path method, path
|
30
|
+
self.class.routes.fetch(method.downcase.to_sym).each do |sig, h|
|
31
|
+
params = sig.match path
|
32
|
+
return [h, params] if params
|
33
|
+
end
|
34
|
+
|
35
|
+
raise RouteNotFoundError
|
36
|
+
end
|
37
|
+
|
38
|
+
def route req
|
39
|
+
begin
|
40
|
+
handler, params = handler_for_path req.request_method, req.path
|
41
|
+
req.params.merge! params
|
42
|
+
RequestScope.new(self, req).apply_to &handler
|
43
|
+
rescue KeyError, RouteNotFoundError
|
44
|
+
@app.call req.env
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def _call env
|
49
|
+
route Request.new(env)
|
50
|
+
end
|
51
|
+
|
52
|
+
def call env
|
53
|
+
if @top == self
|
54
|
+
_call env
|
55
|
+
else
|
56
|
+
if not @initialized_chain
|
57
|
+
@initialized_chain = true
|
58
|
+
@top.call(env)
|
59
|
+
else
|
60
|
+
@initialized_chain = false
|
61
|
+
_call env
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module LittleFrankie
|
2
|
+
module ClassLevelApi
|
3
|
+
HTTP_VERBS = [:delete, :get, :head, :options, :patch, :post, :put, :trace]
|
4
|
+
HTTP_VERBS.each do |method|
|
5
|
+
define_method method do |str, &blk|
|
6
|
+
(routes[method] ||= {})[RouteSignature.new(str)] = Proc.new &blk
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def middlewares; @middlewares ||= [] end
|
11
|
+
def routes; @routes ||= {} end
|
12
|
+
def before_hooks; @before_hooks ||= [] end
|
13
|
+
def after_hooks; @after_hooks ||= [] end
|
14
|
+
|
15
|
+
def use_protection! args={}
|
16
|
+
begin
|
17
|
+
require 'rack/protection'
|
18
|
+
middlewares.unshift [Rack::Protection, args]
|
19
|
+
rescue LoadError
|
20
|
+
puts "WARN: to use protection, you must install 'rack-protection' gem"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def before &blk
|
25
|
+
before_hooks << Proc.new(&blk)
|
26
|
+
end
|
27
|
+
|
28
|
+
def after &blk
|
29
|
+
after_hooks << Proc.new(&blk)
|
30
|
+
end
|
31
|
+
|
32
|
+
def use middleware, *args, &block
|
33
|
+
middlewares << [middleware, args, block]
|
34
|
+
end
|
35
|
+
|
36
|
+
def helpers *args
|
37
|
+
args.each {|m| RequestScope.add_helper_module m }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module LittleFrankie
|
2
|
+
class Request < Rack::Request
|
3
|
+
end
|
4
|
+
|
5
|
+
class Response < Rack::Response
|
6
|
+
attr_reader :raw_body
|
7
|
+
|
8
|
+
def initialize body=[], status=200, header={}
|
9
|
+
@raw_body = body
|
10
|
+
super body.to_s, status, header
|
11
|
+
end
|
12
|
+
|
13
|
+
def body= value
|
14
|
+
@raw_body = value
|
15
|
+
@body = []
|
16
|
+
@length = 0
|
17
|
+
|
18
|
+
if value.respond_to? :to_str
|
19
|
+
write value.to_str
|
20
|
+
elsif value.respond_to?(:each)
|
21
|
+
value.each {|part| write part.to_s }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module LittleFrankie
|
2
|
+
class RequestScope
|
3
|
+
attr_reader :request, :app, :response
|
4
|
+
|
5
|
+
def self.add_helper_module m
|
6
|
+
include m
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize app, req
|
10
|
+
@app = app
|
11
|
+
@headers = {'Content-Type' => 'text/html'}
|
12
|
+
@status = 200
|
13
|
+
@request = req
|
14
|
+
end
|
15
|
+
|
16
|
+
def params
|
17
|
+
request.params
|
18
|
+
end
|
19
|
+
|
20
|
+
def headers hash={}
|
21
|
+
@headers.merge! hash
|
22
|
+
end
|
23
|
+
|
24
|
+
def session
|
25
|
+
request.session
|
26
|
+
end
|
27
|
+
|
28
|
+
def cookies
|
29
|
+
request.cookies
|
30
|
+
end
|
31
|
+
|
32
|
+
def status code
|
33
|
+
@status = code
|
34
|
+
end
|
35
|
+
|
36
|
+
def halt status, headers={}, body=''
|
37
|
+
@halt_response = Response.new body, status, @headers.merge(headers)
|
38
|
+
end
|
39
|
+
|
40
|
+
def redirect_to path
|
41
|
+
@redirect = path
|
42
|
+
end
|
43
|
+
|
44
|
+
def apply_to &handler
|
45
|
+
params.default_proc = proc {|h,k| h[k.to_s] || h[k.to_sym]}
|
46
|
+
app.class.before_hooks.each {|h| instance_eval &h }
|
47
|
+
|
48
|
+
@response = @halt_response || begin
|
49
|
+
Response.new instance_eval(&handler), @status, @headers
|
50
|
+
end
|
51
|
+
|
52
|
+
cookies.each {|k,v| @response.set_cookie k,v }
|
53
|
+
@response.redirect(@redirect) if @redirect
|
54
|
+
|
55
|
+
app.class.after_hooks.each {|h| instance_eval &h }
|
56
|
+
@response.finish
|
57
|
+
@response
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module LittleFrankie
|
2
|
+
class RouteSignature
|
3
|
+
NAME_PATTERN = /:(\S+)/
|
4
|
+
|
5
|
+
attr_reader :pattern
|
6
|
+
def initialize route
|
7
|
+
@pattern = if route.is_a? Regexp
|
8
|
+
route
|
9
|
+
else
|
10
|
+
pattern_for route.dup
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def pattern_for string
|
15
|
+
return string unless string.include? ':'
|
16
|
+
string = "/#{string}" unless string.start_with? '/'
|
17
|
+
parts = string.split '/'
|
18
|
+
|
19
|
+
groups = parts.map do |part|
|
20
|
+
next part if part.empty?
|
21
|
+
next part unless part.start_with? ':'
|
22
|
+
name = NAME_PATTERN.match(part)[1]
|
23
|
+
%Q{(?<#{name}>\\S+)}
|
24
|
+
end.select {|s| !s.empty? }
|
25
|
+
|
26
|
+
%r(\/#{groups.join('\/')})
|
27
|
+
end
|
28
|
+
|
29
|
+
def match path
|
30
|
+
return (pattern == path ? {} : nil) if pattern.is_a?(String)
|
31
|
+
data = pattern.match path
|
32
|
+
|
33
|
+
if data
|
34
|
+
if pattern.respond_to? :names
|
35
|
+
Hash[data.names.map {|n| [n.to_sym, URI.unescape(data[n])]}]
|
36
|
+
else
|
37
|
+
{}
|
38
|
+
end
|
39
|
+
else
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'little_frankie/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "little_frankie"
|
8
|
+
spec.version = LittleFrankie::VERSION
|
9
|
+
spec.authors = ["Andrei Lisnic"]
|
10
|
+
spec.email = ["andrei.lisnic@gmail.com"]
|
11
|
+
spec.description = %q{sinatra's little brother}
|
12
|
+
spec.summary = %q{sinatra's little brother}
|
13
|
+
spec.homepage = "https://github.com/alisnic/frankie"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
end
|
data/spec/app_spec.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe App do
|
4
|
+
let (:app) { mock_app {} }
|
5
|
+
|
6
|
+
describe '.run!' do
|
7
|
+
#
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should have the class methods included' do
|
11
|
+
extended_modules_for(App).should include(ClassLevelApi)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should return a rack response on call' do
|
15
|
+
response = app.get '/'
|
16
|
+
response.should be_a(Rack::Response)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should return 404 for non-matched routes' do
|
20
|
+
response = app.get random_url
|
21
|
+
response.status.should == 404
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should match a route for any supported verbs' do
|
25
|
+
url = random_url
|
26
|
+
verb = ClassLevelApi::HTTP_VERBS.sample
|
27
|
+
|
28
|
+
app = mock_app do
|
29
|
+
send verb, url do
|
30
|
+
'foo'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
res = app.send verb, url
|
35
|
+
res.body.should == 'foo'
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should support route patterns' do
|
39
|
+
app = mock_app do
|
40
|
+
get '/:name' do
|
41
|
+
"hello #{params[:name]}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
res = app.get '/foo'
|
46
|
+
res.body.should == "hello foo"
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should support adding before filers' do
|
50
|
+
app = mock_app do
|
51
|
+
before do
|
52
|
+
request.should_not == nil
|
53
|
+
end
|
54
|
+
|
55
|
+
get '/' do
|
56
|
+
"hello"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
app.get('/')
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'does not maintain state between requests' do
|
64
|
+
app = mock_app do
|
65
|
+
get '/state' do
|
66
|
+
@foo ||= "new"
|
67
|
+
body = "Foo: #{@foo}"
|
68
|
+
@foo = 'discard'
|
69
|
+
body
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
2.times do
|
74
|
+
response = app.get('/state')
|
75
|
+
response.should be_ok
|
76
|
+
'Foo: new'.should == response.body
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'acts well as a middleware' do
|
81
|
+
app = lambda do |env|
|
82
|
+
[210, {}, ['Hello from downstream']]
|
83
|
+
end
|
84
|
+
|
85
|
+
app_class = frankie_app do
|
86
|
+
get '/' do
|
87
|
+
'hello'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
frankie = app_class.new(app)
|
92
|
+
req = Rack::MockRequest.new frankie
|
93
|
+
res = req.get '/'
|
94
|
+
res.body.should == 'hello'
|
95
|
+
|
96
|
+
res2 = req.get '/ither'
|
97
|
+
res2.body.should == 'Hello from downstream'
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'should support adding after filers' do
|
101
|
+
app = mock_app do
|
102
|
+
after do
|
103
|
+
response.should_not == nil
|
104
|
+
end
|
105
|
+
|
106
|
+
get '/' do
|
107
|
+
"hello"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
app.get '/'
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'should be able to set cookies' do
|
114
|
+
app_class = Class.new(App) do
|
115
|
+
post '/write' do
|
116
|
+
cookies.merge! params
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
req = Rack::MockRequest.env_for '/write?foo=bar', :method => :post
|
121
|
+
res = app_class.new.call(req)
|
122
|
+
res.headers['Set-Cookie'].should == 'foo=bar'
|
123
|
+
end
|
124
|
+
|
125
|
+
describe '.run!' do
|
126
|
+
before do
|
127
|
+
handler = begin
|
128
|
+
Rack::Handler::Thin
|
129
|
+
rescue LoadError
|
130
|
+
Rack::Handler::WEBrick
|
131
|
+
end
|
132
|
+
handler.stub :run
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'should include the default middleware on top' do
|
136
|
+
kls = frankie_app do
|
137
|
+
end
|
138
|
+
|
139
|
+
kls.run!
|
140
|
+
kls.middlewares.first.should == Rack::ShowExceptions
|
141
|
+
kls.middlewares[1].should == Rack::CommonLogger
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|