nyny 1.0.0.pre1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/nyny.rb +9 -0
- data/lib/nyny/app.rb +66 -0
- data/lib/nyny/class_level_api.rb +40 -0
- data/lib/nyny/primitives.rb +25 -0
- data/lib/nyny/request_scope.rb +60 -0
- data/lib/nyny/route_signature.rb +44 -0
- data/lib/nyny/version.rb +3 -0
- data/nyny.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
|
data/lib/nyny.rb
ADDED
data/lib/nyny/app.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
module NYNY
|
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 NYNY
|
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 NYNY
|
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 NYNY
|
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 NYNY
|
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
|
data/lib/nyny/version.rb
ADDED
data/nyny.gemspec
ADDED
@@ -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 'nyny/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "nyny"
|
8
|
+
spec.version = NYNY::VERSION
|
9
|
+
spec.authors = ["Andrei Lisnic"]
|
10
|
+
spec.email = ["andrei.lisnic@gmail.com"]
|
11
|
+
spec.description = %q{New York, New York.}
|
12
|
+
spec.summary = %q{sinatra's little brother}
|
13
|
+
spec.homepage = "https://github.com/alisnic/nyny"
|
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
|