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.
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/nyny.rb +9 -0
  37. data/lib/nyny/app.rb +66 -0
  38. data/lib/nyny/class_level_api.rb +40 -0
  39. data/lib/nyny/primitives.rb +25 -0
  40. data/lib/nyny/request_scope.rb +60 -0
  41. data/lib/nyny/route_signature.rb +44 -0
  42. data/lib/nyny/version.rb +3 -0
  43. data/nyny.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
data/lib/nyny.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'uri'
2
+ require 'rack'
3
+
4
+ require "nyny/version"
5
+ require 'nyny/primitives'
6
+ require 'nyny/request_scope'
7
+ require 'nyny/route_signature'
8
+ require 'nyny/class_level_api'
9
+ require 'nyny/app'
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
@@ -0,0 +1,3 @@
1
+ module NYNY
2
+ VERSION = "1.0.0.pre1"
3
+ end
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