little_frankie 1.0.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile +5 -0
  7. data/LICENSE.txt +22 -0
  8. data/Performance.md +65 -0
  9. data/README.md +167 -0
  10. data/Rakefile +1 -0
  11. data/benchmarks/filters/frankie.rb +18 -0
  12. data/benchmarks/filters/sinatra.rb +17 -0
  13. data/benchmarks/helpers/frankie.rb +19 -0
  14. data/benchmarks/helpers/sinatra.rb +18 -0
  15. data/benchmarks/simple/frankie.rb +10 -0
  16. data/benchmarks/simple/sinatra.rb +9 -0
  17. data/benchmarks/url_pattern/frankie.rb +10 -0
  18. data/benchmarks/url_pattern/sinatra.rb +8 -0
  19. data/examples/active_record/.gitignore +1 -0
  20. data/examples/active_record/Gemfile +14 -0
  21. data/examples/active_record/Rakefile +51 -0
  22. data/examples/active_record/config/database.yml +0 -0
  23. data/examples/active_record/database.rb +12 -0
  24. data/examples/active_record/db/migrate/20130606133756_add_shouts.rb +12 -0
  25. data/examples/active_record/models/shout.rb +3 -0
  26. data/examples/active_record/server.rb +43 -0
  27. data/examples/json_api.rb +21 -0
  28. data/examples/templates/server.rb +27 -0
  29. data/examples/templates/views/index.haml +1 -0
  30. data/examples/web_sockets/public/FABridge.js +604 -0
  31. data/examples/web_sockets/public/WebSocketMain.swf +0 -0
  32. data/examples/web_sockets/public/index.html +76 -0
  33. data/examples/web_sockets/public/swfobject.js +4 -0
  34. data/examples/web_sockets/public/web_socket.js +388 -0
  35. data/examples/web_sockets/server.rb +60 -0
  36. data/lib/little_frankie.rb +9 -0
  37. data/lib/little_frankie/app.rb +66 -0
  38. data/lib/little_frankie/class_level_api.rb +40 -0
  39. data/lib/little_frankie/primitives.rb +25 -0
  40. data/lib/little_frankie/request_scope.rb +60 -0
  41. data/lib/little_frankie/route_signature.rb +44 -0
  42. data/lib/little_frankie/version.rb +3 -0
  43. data/little_frankie.gemspec +24 -0
  44. data/spec/app_spec.rb +144 -0
  45. data/spec/class_level_api_spec.rb +39 -0
  46. data/spec/primitives_spec.rb +26 -0
  47. data/spec/request_scope_spec.rb +71 -0
  48. data/spec/spec_helper.rb +42 -0
  49. 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,9 @@
1
+ require 'uri'
2
+ require 'rack'
3
+
4
+ require "little_frankie/version"
5
+ require 'little_frankie/primitives'
6
+ require 'little_frankie/request_scope'
7
+ require 'little_frankie/route_signature'
8
+ require 'little_frankie/class_level_api'
9
+ require 'little_frankie/app'
@@ -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,3 @@
1
+ module LittleFrankie
2
+ VERSION = "1.0.0.pre1"
3
+ 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