Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

merge master

  • Loading branch information...
commit 360e28b9aaaae0c345934665449f7f733381814b 2 parents 8ea448c + be04426
dan sinclair dj2 authored
Showing with 4,717 additions and 593 deletions.
  1. 0  .gemtest
  2. +13 −9 .gitignore
  3. +0 −3  Gemfile
  4. +64 −0 HISTORY.md
  5. +21 −13 README.md
  6. +1 −0  Rakefile
  7. +1 −5 examples/activerecord/srv.rb
  8. +28 −0 examples/api_proxy.rb
  9. +85 −0 examples/async_aroundware_demo.rb
  10. +1 −6 examples/async_upload.rb
  11. +265 −0 examples/auth_and_rate_limit.rb
  12. +37 −0 examples/chunked_streaming.rb
  13. +1 −4 examples/conf_test.rb
  14. +33 −0 examples/config/auth_and_rate_limit.rb
  15. +29 −0 examples/config/content_stream.rb
  16. +7 −5 examples/config/http_log.rb
  17. +8 −0 examples/config/template.rb
  18. +39 −0 examples/content_stream.rb
  19. +37 −0 examples/early_abort.rb
  20. +9 −11 examples/echo.rb
  21. +20 −0 examples/env_use_statements.rb
  22. +40 −0 examples/favicon.rb
  23. +1 −3 examples/gziped.rb
  24. +2 −2 examples/http_log.rb
  25. BIN  examples/public/favicon.ico
  26. +296 −0 examples/public/stylesheets/style.css
  27. +84 −3 examples/rack_routes.rb
  28. +15 −0 examples/rasterize/rasterize.js
  29. +37 −0 examples/rasterize/rasterize.rb
  30. +42 −0 examples/rasterize/rasterize_and_shorten.rb
  31. BIN  examples/rasterize/thumb/f7ad4cb03e5bfd0e2c43db8e598fb3cd.png
  32. +2 −2 examples/stream.rb
  33. +48 −0 examples/template.rb
  34. +125 −0 examples/test_rig.rb
  35. +4 −6 examples/valid.rb
  36. +4 −0 examples/views/debug.haml
  37. +13 −0 examples/views/joke.markdown
  38. +12 −0 examples/views/layout.erb
  39. +39 −0 examples/views/layout.haml
  40. +28 −0 examples/views/root.haml
  41. +18 −9 goliath.gemspec
  42. +0 −36 lib/goliath.rb
  43. +155 −23 lib/goliath/api.rb
  44. +71 −21 lib/goliath/application.rb
  45. +13 −11 lib/goliath/connection.rb
  46. +3 −1 lib/goliath/constants.rb
  47. +133 −0 lib/goliath/deprecated/async_aroundware.rb
  48. +84 −0 lib/goliath/deprecated/mongo_receiver.rb
  49. +97 −0 lib/goliath/deprecated/response_receiver.rb
  50. +52 −1 lib/goliath/env.rb
  51. +30 −15 lib/goliath/goliath.rb
  52. +2 −2 lib/goliath/headers.rb
  53. +8 −2 lib/goliath/plugins/latency.rb
  54. +23 −0 lib/goliath/rack.rb
  55. +115 −0 lib/goliath/rack/async_middleware.rb
  56. +228 −0 lib/goliath/rack/barrier_aroundware.rb
  57. +60 −0 lib/goliath/rack/barrier_aroundware_factory.rb
  58. +58 −0 lib/goliath/rack/builder.rb
  59. +3 −15 lib/goliath/rack/default_response_format.rb
  60. +11 −0 lib/goliath/rack/formatters.rb
  61. +2 −18 lib/goliath/rack/formatters/html.rb
  62. +2 −17 lib/goliath/rack/formatters/json.rb
  63. +32 −0 lib/goliath/rack/formatters/plist.rb
  64. +23 −31 lib/goliath/rack/formatters/xml.rb
  65. +27 −0 lib/goliath/rack/formatters/yaml.rb
  66. +8 −5 lib/goliath/rack/heartbeat.rb
  67. +1 −13 lib/goliath/rack/jsonp.rb
  68. +61 −10 lib/goliath/rack/params.rb
  69. +13 −22 lib/goliath/rack/render.rb
  70. +114 −0 lib/goliath/rack/simple_aroundware.rb
  71. +121 −0 lib/goliath/rack/simple_aroundware_factory.rb
  72. +357 −0 lib/goliath/rack/templates.rb
  73. +11 −12 lib/goliath/rack/tracer.rb
  74. +12 −0 lib/goliath/rack/validation.rb
  75. +0 −2  lib/goliath/rack/validation/default_params.rb
  76. +11 −2 lib/goliath/rack/validation/numeric_range.rb
  77. +3 −2 lib/goliath/rack/validation/request_method.rb
  78. +21 −12 lib/goliath/rack/validation/required_param.rb
  79. +11 −15 lib/goliath/rack/validation/required_value.rb
  80. +51 −0 lib/goliath/rack/validator.rb
  81. +52 −21 lib/goliath/request.rb
  82. +3 −2 lib/goliath/response.rb
  83. +28 −16 lib/goliath/runner.rb
  84. +19 −11 lib/goliath/server.rb
  85. +53 −29 lib/goliath/test_helper.rb
  86. +2 −0  lib/goliath/validation.rb
  87. +0 −16 lib/goliath/{rack/validation_error.rb → validation/error.rb}
  88. +31 −0 lib/goliath/validation/standard_http_errors.rb
  89. +1 −1  lib/goliath/version.rb
  90. +0 −2  spec/integration/async_request_processing.rb
  91. +50 −0 spec/integration/early_abort_spec.rb
  92. +37 −2 spec/integration/echo_spec.rb
  93. +20 −0 spec/integration/empty_body_spec.rb
  94. +138 −0 spec/integration/http_log_spec.rb
  95. +2 −4 spec/integration/keepalive_spec.rb
  96. +2 −4 spec/integration/pipelining_spec.rb
  97. +169 −0 spec/integration/rack_routes_spec.rb
  98. +43 −0 spec/integration/reloader_spec.rb
  99. +56 −0 spec/integration/template_spec.rb
  100. +23 −0 spec/integration/trace_spec.rb
  101. +21 −2 spec/integration/valid_spec.rb
  102. +8 −0 spec/spec_helper.rb
  103. +30 −0 spec/unit/api_spec.rb
  104. +2 −2 spec/unit/env_spec.rb
  105. +40 −0 spec/unit/rack/builder_spec.rb
  106. +2 −2 spec/unit/rack/formatters/json_spec.rb
  107. +51 −0 spec/unit/rack/formatters/plist_spec.rb
  108. +3 −3 spec/unit/rack/formatters/xml_spec.rb
  109. +53 −0 spec/unit/rack/formatters/yaml_spec.rb
  110. +12 −2 spec/unit/rack/heartbeat_spec.rb
  111. +88 −3 spec/unit/rack/params_spec.rb
  112. +11 −6 spec/unit/rack/render_spec.rb
  113. +3 −3 spec/unit/rack/validation/default_params_spec.rb
  114. +11 −4 spec/unit/rack/validation/numeric_range_spec.rb
  115. +9 −9 spec/unit/rack/validation/request_method_spec.rb
  116. +31 −17 spec/unit/rack/validation/required_param_spec.rb
  117. +12 −15 spec/unit/rack/validation/required_value_spec.rb
  118. +0 −40 spec/unit/rack/validation_error_spec.rb
  119. +23 −4 spec/unit/request_spec.rb
  120. +13 −0 spec/unit/runner_spec.rb
  121. +8 −4 spec/unit/server_spec.rb
  122. +21 −0 spec/unit/validation/standard_http_errors_spec.rb
0  .gemtest
View
No changes.
22 .gitignore
View
@@ -1,15 +1,19 @@
-Makefile
-mkmf.log
-*.o
-*.bundle
+Gemfile.lock
+
+.bundle/
doc/
pkg/
-examples/log
-*.swp
-Gemfile.lock
+_site
+
.yardoc
.livereload
+.rvmrc
+
+*.swp
*.watchr
*.rbc
-.rvmrc
-_site
+
+examples/log
+examples/goliath.log*
+examples/goliath.pid
+examples/rasterize/thumb
3  Gemfile
View
@@ -1,6 +1,3 @@
source "http://rubygems.org"
-gem 'em-websocket', :git => 'http://github.com/dj2/em-websocket', :branch => 'factory_change'
-gem 'http_parser.rb', :git => 'http://github.com/dj2/http_parser.rb', :branch => 'upgrade_data'
-
gemspec
64 HISTORY.md
View
@@ -0,0 +1,64 @@
+# HISTORY
+
+## v0.9.3 (Oct 16, 2011)
+
+ - new router DSL - much improved, see examples
+ - refactored async_aroundware
+ - make jruby friendlier (removed 1.9 req in gemspec)
+ - enable epoll
+ - SSL support
+ - unix socket support
+ - reload config on HUP
+ - and a number of small bugfixes + other improvements..
+ - See full list @ https://github.com/postrank-labs/goliath/compare/v0.9.2...v0.9.3
+
+## v0.9.2 (July 21, 2011)
+
+ - See full list @ https://github.com/postrank-labs/goliath/compare/v0.9.1...v0.9.2
+
+## v0.9.1 (Apr 12, 2011)
+
+ - Added extra messaging around the class not matching the file name (Carlos Brando)
+
+ - Fix issue with POST parameters not being parsed by Goliath::Rack::Params
+ - Added support for multipart encoded POST bodies
+ - Added support for parsing nested query string parameters (Nolan Evans)
+ - Added support for parsing application/json POST bodies
+ - Content-Types outside of multipart, urlencoded and application/json will not be parsed automatically.
+
+ - added 'run as user' option
+ - SERVER_NAME and SERVER_PORT are set to values in HOST header
+
+ - Cleaned up spec examples (Justin Ko)
+
+ - moved logger into 'rack.logger' key to be more Rack compliant (Env#logger added to
+ keep original API consistent)
+ - add command line option for specifying config file
+ - HTTP_CONTENT_LENGTH and HTTP_CONTENT_TYPE were changed to CONTENT_TYPE and CONTENT_LENGTH
+ to be more Rack compliant
+ - fix issue with loading config file in development mode
+
+ - Rack::Reloader will be loaded automatically by the framework in development mode.
+
+
+## v0.9.0 (Mar 9, 2011)
+
+(Initial Public Release)
+
+Goliath is an open source version of the non-blocking (asynchronous) Ruby web server framework
+powering PostRank. It is a lightweight framework designed to meet the following goals: bare
+metal performance, Rack API and middleware support, simple configuration, fully asynchronous
+processing, and readable and maintainable code (read: no callbacks).
+
+The framework is powered by an EventMachine reactor, a high-performance HTTP parser and Ruby 1.9
+runtime. One major advantage Goliath has over other asynchronous frameworks is the fact that by
+leveraging Ruby fibers, it can untangle the complicated callback-based code into a format we are
+all familiar and comfortable with: linear execution, which leads to more maintainable and readable code.
+
+While MRI is the recommend platform, Goliath has been tested to run on JRuby and Rubinius.
+
+Goliath has been in production at PostRank for over a year, serving a sustained 500 requests/s for
+internal and external applications. Many of the Goliath processes have been running for months at
+a time (read: no memory leaks) and have served hundreds of gigabytes of data without restarts. To
+scale up and provide failover and redundancy, our individual Goliath servers at PostRank are usually
+deployed behind a reverse proxy (such as HAProxy).
34 README.md
View
@@ -20,21 +20,23 @@ Each HTTP request within Goliath is executed in its own Ruby fiber and all async
## Getting Started: Hello World
- require 'goliath'
+```ruby
+require 'goliath'
- class Hello < Goliath::API
- # reload code on every request in dev environment
- use ::Rack::Reloader, 0 if Goliath.dev?
+class Hello < Goliath::API
+ def response(env)
+ [200, {}, "Hello World"]
+ end
+end
- def response(env)
- [200, {}, "Hello World"]
- end
- end
+> ruby hello.rb -sv
+> [97570:INFO] 2011-02-15 00:33:51 :: Starting server on 0.0.0.0:9000 in development mode. Watch out for stones.
+```
- > ruby hello.rb -sv
- > [97570:INFO] 2011-02-15 00:33:51 :: Starting server on 0.0.0.0:9000 in development mode. Watch out for stones.
+See examples directory for more, hands-on examples of building Goliath powered web-services. Are you new to EventMachine, or want a detailed walk-through of building a Goliath powered API? You're in luck, we have two super-awesome peepcode screencasts which will teach you all you need to know:
-See examples directory for more, hands-on examples of building Goliath powered web-services.
+* [Meet EventMachine: Part 1](http://peepcode.com/products/eventmachine) - introduction to EM, Fibers, etc.
+* [Meet EventMachine: Part 2](http://peepcode.com/products/eventmachine-ii) - building an API with Goliath
## Performance: MRI, JRuby, Rubinius
@@ -55,7 +57,7 @@ Goliath has been in production at PostRank for over a year, serving a sustained
* Mongrel is a threaded web-server, and both Passenger and Unicorn fork an entire VM to isolate each request from each other. By contrast, Goliath builds a single instance of the Rack app and runs all requests in parallel through a single VM, which leads to a much smaller memory footprint and less overhead.
* How do I deploy Goliath in production?
- * We recommend deploying Goliath behind a reverse proxy such as HAProxy, Nginx or equivalent. Using one of the above, you can easily run multiple instances of the same application and load balance between them within the reverse proxy.
+ * We recommend deploying Goliath behind a reverse proxy such as HAProxy ([sample config](https://github.com/postrank-labs/goliath/wiki/HAProxy)), Nginx or equivalent. Using one of the above, you can easily run multiple instances of the same application and load balance between them within the reverse proxy.
## Guides
@@ -66,6 +68,7 @@ Goliath has been in production at PostRank for over a year, serving a sustained
### Hands-on applications:
+* [Peepcode](http://peepcode.com/products/eventmachine) [screencasts](http://peepcode.com/products/eventmachine-ii)
* [Asynchronous HTTP, MySQL, etc](https://github.com/postrank-labs/goliath/wiki/Asynchronous-Processing)
* [Response streaming with Goliath](https://github.com/postrank-labs/goliath/wiki/Streaming)
* [Examples](https://github.com/postrank-labs/goliath/tree/master/examples)
@@ -74,6 +77,10 @@ Goliath has been in production at PostRank for over a year, serving a sustained
* [Goliath: Non-blocking, Ruby 1.9 Web Server](http://www.igvita.com/2011/03/08/goliath-non-blocking-ruby-19-web-server)
* [Stage left: Enter Goliath - HTTP Proxy + MongoDB](http://everburning.com/news/stage-left-enter-goliath/)
+* [InfoQ: Meet the Goliath of Ruby Application Servers](http://www.infoq.com/articles/meet-goliath)
+* [Node.jsはコールバック・スパゲティを招くか](http://el.jibun.atmarkit.co.jp/rails/2011/03/nodejs-d123.html)
+* [Goliath on LinuxFr.org (french)](http://linuxfr.org/news/en-vrac-spécial-ruby-jruby-sinatra-et-goliath)
+* [Goliath et ses amis (slides in french)](http://nono.github.com/Presentations/20110416_Goliath/)
## Discussion and Support
@@ -83,4 +90,5 @@ Goliath has been in production at PostRank for over a year, serving a sustained
## License & Acknowledgments
-Goliath is distributed under the MIT license, for full details please see the LICENSE file.
+Goliath is distributed under the MIT license, for full details please see the LICENSE file.
+Rock favicon CC-BY from [Douglas Feer](http://www.favicon.cc/?action=icon&file_id=375421)
1  Rakefile
View
@@ -5,6 +5,7 @@ require 'yard'
require 'rspec/core/rake_task'
task :default => [:spec]
+task :test => [:spec]
desc "run spec tests"
RSpec::Core::RakeTask.new('spec') do |t|
6 examples/activerecord/srv.rb
View
@@ -19,13 +19,9 @@ class User < ActiveRecord::Base
end
class Srv < Goliath::API
- use ::Rack::Reloader, 0 if Goliath.dev?
-
use Goliath::Rack::Params
use Goliath::Rack::DefaultMimeType
- use Goliath::Rack::Formatters::JSON
- use Goliath::Rack::Render
- use Goliath::Rack::ValidationError
+ use Goliath::Rack::Render, 'json'
use Goliath::Rack::Validation::RequiredParam, {:key => 'id', :type => 'ID'}
use Goliath::Rack::Validation::NumericRange, {:key => 'id', :min => 1}
28 examples/api_proxy.rb
View
@@ -0,0 +1,28 @@
+#!/usr/bin/env ruby
+
+# Rewrites and proxies requests to a third-party API, with HTTP basic authentication.
+
+require 'goliath'
+require 'em-synchrony/em-http'
+
+class TwilioResponse < Goliath::API
+ use Goliath::Rack::Params
+ use Goliath::Rack::JSONP
+
+ HEADERS = { authorization: ENV.values_at("TWILIO_SID","TWILIO_AUTH_TOKEN") }
+ BASE_URL = "https://api.twilio.com/2010-04-01/Accounts/#{ENV['TWILIO_SID']}/AvailablePhoneNumbers/US"
+
+ def response(env)
+ url = "#{BASE_URL}#{env['REQUEST_PATH']}?#{env['QUERY_STRING']}"
+ logger.debug "Proxying #{url}"
+
+ http = EM::HttpRequest.new(url).get head: HEADERS
+ logger.debug "Received #{http.response_header.status} from Twilio"
+
+ [200, {'X-Goliath' => 'Proxy','Content-Type' => 'application/javascript'}, http.response]
+ end
+end
+
+class Twilio < Goliath::API
+ get %r{^/(Local|TollFree)}, TwilioResponse
+end
85 examples/async_aroundware_demo.rb
View
@@ -0,0 +1,85 @@
+#!/usr/bin/env ruby
+$: << File.dirname(__FILE__)+'/../lib'
+
+require 'goliath'
+require 'em-synchrony/em-http'
+require 'yajl/json_gem'
+
+#
+# Here's a way to make an asynchronous request in the middleware, and only
+# proceed with the response when both the endpoint and our middleware's
+# responses have completed.
+#
+# To run this, start the 'test_rig.rb' server on port 9002:
+#
+# bundle exec ./examples/test_rig.rb -sv -p 9002
+#
+# And then start this server on port 9000:
+#
+# bundle exec ./examples/barrier_aroundware_demo.rb -sv -p 9000
+#
+# Now curl the async_aroundware_demo_multi:
+#
+# $ time curl 'http://127.0.0.1:9000/?delay_1=1.0&delay_2=1.5'
+# { "results": {
+# "sleep_2": { "delay": 1.5, "actual": 1.5085558891296387 },
+# "sleep_1": { "delay": 1.0, "actual": 1.0098700523376465 }
+# } }
+#
+# The requests are run concurrently:
+#
+# $ ./examples/async_aroundware_demo.rb -sv -p 9000 -e prod &
+# [68463:INFO] 2011-05-03 23:13:03 :: Starting server on 0.0.0.0:9000 in production mode. Watch out for stones.
+# $ ab -n10 -c10 'http://127.0.0.1:9000/?delay_1=1.5&delay_2=2.0'
+# Connection Times (ms)
+# min mean[+/-sd] median max
+# Connect: 0 0 0.1 0 0
+# Processing: 2027 2111 61.6 2112 2204
+# Waiting: 2027 2111 61.5 2112 2204
+# Total: 2027 2112 61.5 2113 2204
+#
+#
+
+BASE_URL = 'http://localhost:9002/'
+
+class RemoteRequestBarrier
+ include Goliath::Rack::BarrierAroundware
+ attr_accessor :sleep_1
+
+ def pre_process
+ # Request with delay_1 and drop_1 -- note: 'aget', because we want execution to continue
+ req = EM::HttpRequest.new(BASE_URL).aget(:query => { :delay => env.params['delay_1'], :drop => env.params['drop_1'] })
+ enqueue :sleep_1, req
+ return Goliath::Connection::AsyncResponse
+ end
+
+ def post_process
+ # unify the results with the results of the API call
+ if successes.include?(:sleep_1) then body[:results][:sleep_1] = JSON.parse(sleep_1.response)
+ else body[:errors][:sleep_1] = sleep_1.error ; end
+ [status, headers, JSON.pretty_generate(body)]
+ end
+end
+
+class BarrierAroundwareDemo < Goliath::API
+ use Goliath::Rack::Params
+ use Goliath::Rack::Validation::NumericRange, {:key => 'delay_1', :default => 1.0, :max => 5.0, :min => 0.0, :as => Float}
+ use Goliath::Rack::Validation::NumericRange, {:key => 'delay_2', :default => 0.5, :max => 5.0, :min => 0.0, :as => Float}
+ #
+ use Goliath::Rack::BarrierAroundwareFactory, RemoteRequestBarrier
+
+ def response(env)
+ # Request with delay_2 and drop_2 -- note: 'get', because we want execution to proceed linearly
+ resp = EM::HttpRequest.new(BASE_URL).get(:query => { :delay => env.params['delay_2'], :drop => env.params['drop_2'] })
+
+ body = { :results => {}, :errors => {} }
+
+ if resp.response_header.status.to_i != 0
+ body[:results][:sleep_2] = JSON.parse(resp.response) rescue 'parsing failed'
+ else
+ body[:errors ][:sleep_2] = resp.error
+ end
+
+ [200, { }, body]
+ end
+end
7 examples/async_upload.rb
View
@@ -5,14 +5,9 @@
require 'yajl'
class AsyncUpload < Goliath::API
-
- # reload code on every request in dev environment
- use ::Rack::Reloader, 0 if Goliath.dev?
-
use Goliath::Rack::Params # parse & merge query and body parameters
use Goliath::Rack::DefaultMimeType # cleanup accepted media types
- use Goliath::Rack::Formatters::JSON # JSON output formatter
- use Goliath::Rack::Render # auto-negotiate response format
+ use Goliath::Rack::Render, 'json' # auto-negotiate response format
def on_headers(env, headers)
env.logger.info 'received headers: ' + headers.inspect
265 examples/auth_and_rate_limit.rb
View
@@ -0,0 +1,265 @@
+#!/usr/bin/env ruby
+$: << File.join(File.dirname(__FILE__), '../lib')
+require 'goliath'
+require 'em-mongo'
+require 'em-http'
+require 'em-synchrony/em-http'
+require 'em-synchrony/em-mongo'
+require 'yajl/json_gem'
+
+require File.join(File.dirname(__FILE__), 'http_log') # Use the HttpLog as our actual endpoint, but include this in the middleware
+
+#
+# Usage:
+#
+# First launch the test rig:
+# bundle exec ./examples/test_rig.rb -sv -p 8080 -e prod &
+#
+# Then launch this script
+# bundle exec ./examples/auth_and_rate_limit.rb -sv -p 9000 --config $PWD/examples/config/auth_and_rate_limit.rb
+#
+# The auth info is returned in the headers:
+#
+# curl -vv 'http://127.0.0.1:9000/?_apikey=i_am_busy&drop=false' ; echo
+# ...snip...
+# < X-RateLimit-MaxRequests: 1000
+# < X-RateLimit-Requests: 999
+# < X-RateLimit-Reset: 1312059600
+#
+# This user will hit the rate limit after 10 requests:
+#
+# for foo in 1 2 3 4 5 6 7 8 9 10 11 12 ; do echo -ne $foo "\t" ; curl 'http://127.0.0.1:9000/?_apikey=i_am_limited' ; echo ; done
+# 1 {"Special":"Header","Params":"_apikey: i_am_awesome|drop: false","Path":"/","Headers":"User-Agent: ...
+# ...
+# 11 [:error, "Your request rate (11) is over your limit (10)"]
+#
+# You can test the barrier (both delays are in fractional seconds):
+# * drop=true will drop the request at the remote host
+# * auth_db_delay will fake a slow response from the mongo
+# * delay will cause a slow response from the remote host
+#
+# time curl -vv 'http://127.0.0.1:9000/?_apikey=i_am_awesome&drop=false&delay=0.4&auth_db_delay=0.3'
+# ...
+# X-Tracer: ... received_usage_info: 0.06, received_sleepy: 299.52, received_downstream_resp: 101.67, ..., total: 406.09
+# ...
+# real 0m0.416s user 0m0.002s sys 0m0.003s pct 1.24
+#
+# This shows the mongodb response returning quickly, the fake DB delay returning
+# after 300ms, and the downstream response returning after an additional 101 ms.
+# The total request took 416ms of wall-clock time
+#
+# This will hold up even in the face of many concurrent connections. Relaunch in
+# production (you may have to edit the config/auth_and_rate_limit scripts):
+#
+# bundle exec ./examples/auth_and_rate_limit.rb -sv -p 9000 -e prod --config $PWD/examples/config/auth_and_rate_limit.rb
+#
+# On my laptop, with 20 concurrent requests (each firing two db gets, a 400 ms
+# http get, and two db writes), the median/90%ile times were 431ms / 457ms:
+#
+# time ab -c20 -n20 'http://127.0.0.1:9000/?_apikey=i_am_awesome&drop=false&delay=0.4&auth_db_delay=0.3'
+# ...
+# Percentage of the requests served within a certain time (ms)
+# 50% 431
+# 90% 457
+# real 0m0.460s user 0m0.001s sys 0m0.003s pct 0.85
+#
+# With 100 concurrent requests, the request latency starts to drop but the
+# throughput and variance stand up:
+#
+# time ab -c100 -n100 'http://127.0.0.1:9000/?_apikey=i_am_awesome&drop=false&delay=0.4&auth_db_delay=0.3'
+# ...
+# Percentage of the requests served within a certain time (ms)
+# 50% 640
+# 90% 673
+# real 0m0.679s user 0m0.002s sys 0m0.007s pct 1.33
+#
+
+# Tracks and enforces account and rate limit policies.
+#
+# This is like a bouncer who lets townies order a drink while he checks their
+# ID, but who's a jerk to college kids.
+#
+# On GET or HEAD requests, it proxies the request and gets account/usage info
+# concurrently; authorizing the account doesn't delay the response.
+#
+# On a POST or other non-idempotent request, it checks the account/usage info
+# *before* allowing the request to fire. This takes longer, but is necessary and
+# tolerable.
+#
+# The magic of BarrierAroundware:
+#
+# 1) In pre_process (before the request):
+# * validate an apikey was given; if not, raise (returning directly)
+# * launch requests for the account and rate limit usage
+#
+# 2) On a POST or other non-GET non-HEAD, we issue `perform`, which barriers
+# (allowing other requests to proceed) until the two pending requests
+# complete. It then checks the account exists and is valid, and that the rate
+# limit is OK
+#
+# 3) If the auth check fails, we raise an error (later caught by a safely{}
+# block and turned into the right 4xx HTTP response.
+#
+# 4) If the auth check succeeds, or the request is a GET or HEAD, we return
+# Goliath::Connection::AsyncResponse, and BarrierAroundwareFactory passes the
+# request down the middleware chain
+#
+# 5) post_process resumes only when both proxied request & auth info are complete
+# (it already has of course in the non-lazy scenario)
+#
+# 6) If we were lazy, the post_process method now checks authorization
+#
+class AuthBarrier
+ include Goliath::Rack::BarrierAroundware
+ include Goliath::Validation
+ attr_reader :db
+ attr_accessor :account_info, :usage_info
+
+ # time period to aggregate stats over, in seconds
+ TIMEBIN_SIZE = 60 * 60
+
+ class MissingApikeyError < BadRequestError ; end
+ class RateLimitExceededError < ForbiddenError ; end
+ class InvalidApikeyError < UnauthorizedError ; end
+
+ def initialize(env, db_name)
+ @db = env.config[db_name]
+ super(env)
+ end
+
+ def pre_process
+ env.trace('pre_process_beg')
+ validate_apikey!
+
+ # the results of the afirst deferrable will be set right into account_info (and the request into successes)
+ enqueue_mongo_request(:account_info, { :_id => apikey })
+ enqueue_mongo_request(:usage_info, { :_id => usage_id })
+ maybe_fake_delay!
+
+ # On non-GET non-HEAD requests, we have to check auth now.
+ unless lazy_authorization?
+ perform # yield execution until user_info has arrived
+ charge_usage
+ check_authorization!
+ end
+
+ env.trace('pre_process_end')
+ return Goliath::Connection::AsyncResponse
+ end
+
+ def post_process
+ env.trace('post_process_beg')
+ # [:account_info, :usage_info, :status, :headers, :body].each{|attr| env.logger.info(("%23s\t%s" % [attr, self.send(attr).inspect[0..200]])) }
+
+ inject_headers
+
+ # We have to check auth now, we skipped it before
+ if lazy_authorization?
+ charge_usage
+ check_authorization!
+ end
+
+ env.trace('post_process_end')
+ [status, headers, body]
+ end
+
+ def lazy_authorization?
+ (env['REQUEST_METHOD'] == 'GET') || (env['REQUEST_METHOD'] == 'HEAD')
+ end
+
+ if defined?(EM::Mongo::Cursor)
+ # em-mongo > 0.3.6 gives us a deferrable back. nice and clean.
+ def enqueue_mongo_request(handle, query)
+ enqueue handle, db.collection(handle).afirst(query)
+ end
+ else
+ # em-mongo <= 0.3.6 makes us fake a deferrable response.
+ def enqueue_mongo_request(handle, query)
+ enqueue_acceptor(handle) do |acc|
+ db.collection(handle).afind(query){|resp| acc.succeed(resp.first) }
+ end
+ end
+ end
+
+ # Fake out a delay in the database response if auth_db_delay is given
+ def maybe_fake_delay!
+ if (auth_db_delay = env.params['auth_db_delay'].to_f) > 0
+ enqueue_acceptor(:sleepy){|acc| EM.add_timer(auth_db_delay){ acc.succeed } }
+ end
+ end
+
+ def accept_response(handle, *args)
+ env.trace("received_#{handle}")
+ super(handle, *args)
+ end
+
+ # ===========================================================================
+
+ def check_authorization!
+ check_apikey!
+ check_rate_limit!
+ end
+
+ def validate_apikey!
+ if apikey.to_s.empty?
+ raise MissingApikeyError
+ end
+ end
+
+ def check_apikey!
+ unless account_info && (account_info['valid'] == true)
+ raise InvalidApikeyError
+ end
+ end
+
+ def check_rate_limit!
+ self.usage_info ||= {}
+ rate = usage_info['calls'].to_i + 1
+ limit = account_info['max_call_rate'].to_i
+ return true if rate <= limit
+ raise RateLimitExceededError, "Your request rate (#{rate}) is over your limit (#{limit})"
+ end
+
+ def charge_usage
+ EM.next_tick do
+ safely(env){ db.collection(:usage_info).update({ :_id => usage_id },
+ { '$inc' => { :calls => 1 } }, :upsert => true) }
+ end
+ end
+
+ def inject_headers
+ headers.merge!({
+ 'X-RateLimit-MaxRequests' => account_info['max_call_rate'].to_s,
+ 'X-RateLimit-Requests' => usage_info['calls'].to_i.to_s,
+ 'X-RateLimit-Reset' => timebin_end.to_s,
+ })
+ end
+
+ # ===========================================================================
+
+ def apikey
+ env.params['_apikey']
+ end
+
+ def usage_id
+ "#{apikey}-#{timebin}"
+ end
+
+ def timebin
+ @timebin ||= timebin_beg
+ end
+
+ def timebin_beg
+ ((Time.now.to_i / TIMEBIN_SIZE).floor * TIMEBIN_SIZE)
+ end
+
+ def timebin_end
+ timebin_beg + TIMEBIN_SIZE
+ end
+end
+
+class AuthAndRateLimit < HttpLog
+ use Goliath::Rack::Tracer, 'X-Tracer'
+ use Goliath::Rack::Params # parse & merge query and body parameters
+ use Goliath::Rack::BarrierAroundwareFactory, AuthBarrier, 'api_auth_db'
+end
37 examples/chunked_streaming.rb
View
@@ -0,0 +1,37 @@
+#!/usr/bin/env ruby
+$:<< '../lib' << 'lib'
+
+#
+# A simple HTTP streaming API which returns a 200 response for any GET request
+# and then emits numbers 1 through 10 in 1 second intervals using Chunked
+# transfer encoding, and finally closes the connection.
+#
+# Chunked transfer streaming works transparently with both browsers and
+# streaming consumers.
+#
+
+require 'goliath'
+
+class ChunkedStreaming < Goliath::API
+ def on_close(env)
+ env.logger.info "Connection closed."
+ end
+
+ def response(env)
+ i = 0
+ pt = EM.add_periodic_timer(1) do
+ env.chunked_stream_send("#{i}\n")
+ i += 1
+ end
+
+ EM.add_timer(10) do
+ pt.cancel
+
+ env.chunked_stream_send("!! BOOM !!\n")
+ env.chunked_stream_close
+ end
+
+ headers = { 'Content-Type' => 'text/plain', 'X-Stream' => 'Goliath' }
+ chunked_streaming_response(200, headers)
+ end
+end
5 examples/conf_test.rb
View
@@ -10,11 +10,8 @@
require 'goliath'
class ConfTest < Goliath::API
-
use Goliath::Rack::Params
- use Goliath::Rack::DefaultMimeType
- use Goliath::Rack::Formatters::JSON
- use Goliath::Rack::Render
+ use Goliath::Rack::Render, 'json'
def options_parser(opts, options)
options[:test] = 0
33 examples/config/auth_and_rate_limit.rb
View
@@ -0,0 +1,33 @@
+import 'http_log'
+
+environment(:development) do
+
+ config['api_auth_db'] = EventMachine::Synchrony::ConnectionPool.new(:size => 20) do
+ conn = EM::Mongo::Connection.new('localhost', 27017, 1, {:reconnect_in => 1})
+ conn.db('buzzkill_test')
+ end
+
+ # for demo purposes, some dummy accounts
+ timebin = ((Time.now.to_i / 3600).floor * 3600)
+
+ # This user's calls should all go through
+ config['api_auth_db'].collection(:account_info).save({
+ :_id => 'i_am_awesome', 'valid' => true, 'max_call_rate' => 1_000_000 })
+
+ # this user's account is disabled
+ config['api_auth_db'].collection(:account_info).save({
+ :_id => 'i_am_lame', 'valid' => false, 'max_call_rate' => 1_000 })
+
+ # this user has not been seen, but will very quickly hit their limit
+ config['api_auth_db'].collection(:account_info).save({
+ :_id => 'i_am_limited', 'valid' => true, 'max_call_rate' => 10 })
+ config['api_auth_db'].collection(:usage_info).save({
+ :_id => "i_am_limited-#{timebin}", 'calls' => 0 })
+
+ # fakes a user with a bunch of calls already made this hour -- two more = no yuo
+ config['api_auth_db'].collection(:account_info).save({
+ :_id => 'i_am_busy', 'valid' => true, 'max_call_rate' => 1_000 })
+ config['api_auth_db'].collection(:usage_info).save({
+ :_id => "i_am_busy-#{timebin}", 'calls' => 999 })
+
+end
29 examples/config/content_stream.rb
View
@@ -0,0 +1,29 @@
+require 'amqp'
+
+config['channel'] = EM::Channel.new
+
+amqp_config = {
+ :host => 'localhost',
+ :user => 'test',
+ :pass => 'test',
+ :vhost => '/test'
+}
+
+conn = AMQP.connect(amqp_config)
+xchange = AMQP::Channel.new(conn).fanout('stream')
+
+q = AMQP::Channel.new(conn).queue('stream/StreamAPI')
+q.bind(xchange)
+
+def handle_message(metadata, payload)
+ config['channel'].push(payload)
+end
+
+q.subscribe(&method(:handle_message))
+
+# push data into the stream. (Just so we have stuff going in)
+count = 0
+EM.add_periodic_timer(2) do
+ xchange.publish("Iteration #{count}\n")
+ count += 1
+end
12 examples/config/http_log.rb
View
@@ -1,7 +1,9 @@
config['forwarder'] = 'http://localhost:8080'
-config['mongo'] = EventMachine::Synchrony::ConnectionPool.new(size: 20) do
- # Need to deal with this just never connecting ... ?
- conn = EM::Mongo::Connection.new('localhost', 27017, 1, {:reconnect_in => 1})
- conn.db('http_log').collection('aggregators')
-end
+environment(:development) do
+ config['mongo'] = EventMachine::Synchrony::ConnectionPool.new(size: 20) do
+ # Need to deal with this just never connecting ... ?
+ conn = EM::Mongo::Connection.new('localhost', 27017, 1, {:reconnect_in => 1})
+ conn.db('http_log').collection('aggregators')
+ end
+end
8 examples/config/template.rb
View
@@ -0,0 +1,8 @@
+config[:template] = {
+ :layout_engine => :haml,
+}
+config[:template_engines] = {
+ :haml => {
+ :escape_html => true
+ }
+}
39 examples/content_stream.rb
View
@@ -0,0 +1,39 @@
+#!/usr/bin/env ruby
+$:<< '../lib' << 'lib'
+
+require 'goliath'
+
+# This example assumes you have an AMQP server up and running with the
+# following config (using rabbit-mq as an example)
+#
+# rabbitmq-server start
+# rabbitmqctl add_vhost /test
+# rabbitmqctl add_user test test
+# rabbitmqctl set_permissions -p /test test ".*" ".*" ".*"
+
+class ContentStream < Goliath::API
+ use Goliath::Rack::Params
+
+ use Goliath::Rack::Render, 'json'
+ use Goliath::Rack::Heartbeat
+ use Goliath::Rack::Validation::RequestMethod, %w(GET)
+
+ def on_close(env)
+ # This is just to make sure if the Heartbeat fires we don't try
+ # to close a connection.
+ return unless env['subscription']
+
+ env.channel.unsubscribe(env['subscription'])
+ env.logger.info "Stream connection closed."
+ end
+
+ def response(env)
+ env.logger.info "Stream connection opened"
+
+ env['subscription'] = env.channel.subscribe do |msg|
+ env.stream_send(msg)
+ end
+
+ [200, {}, Goliath::Response::STREAMING]
+ end
+end
37 examples/early_abort.rb
View
@@ -0,0 +1,37 @@
+#!/usr/bin/env ruby
+$:<< '../lib' << 'lib'
+require 'goliath'
+
+class EarlyAbort < Goliath::API
+ include Goliath::Validation
+ MAX_SIZE = 10
+ TEST_FILE = "/tmp/goliath-test-error.log"
+
+ def on_headers(env, headers)
+ env.logger.info 'received headers: ' + headers.inspect
+ env['async-headers'] = headers
+
+ if env['HTTP_X_CRASH'] && env['HTTP_X_CRASH'] == 'true'
+ raise Goliath::Validation::NotImplementedError.new("Can't handle requests with X-Crash: true.")
+ end
+ end
+
+ def on_body(env, data)
+ env.logger.info 'received data: ' + data
+ (env['async-body'] ||= '') << data
+ size = env['async-body'].size
+
+ if size >= MAX_SIZE
+ raise Goliath::Validation::BadRequestError.new("Payload size can't exceed #{MAX_SIZE} bytes. Received #{size.inspect} bytes.")
+ end
+ end
+
+ def on_close(env)
+ env.logger.info 'closing connection'
+ end
+
+ def response(env)
+ File.open(TEST_FILE, "w+") { |f| f << "response that should not be here"}
+ [200, {}, "OK"]
+ end
+end
20 examples/echo.rb 100755 → 100644
View
@@ -4,23 +4,21 @@
require 'goliath'
require 'goliath/plugins/latency'
-# Goliath uses multi-jon, so pick your favorite JSON serializer
+# Goliath uses multi-json, so pick your favorite JSON serializer
# require 'json'
require 'yajl'
class Echo < Goliath::API
-
- # reload code on every request in dev environment
- use ::Rack::Reloader, 0 if Goliath.dev?
-
- use Goliath::Rack::Params # parse & merge query and body parameters
+ use Goliath::Rack::Tracer # log trace statistics
use Goliath::Rack::DefaultMimeType # cleanup accepted media types
- use Goliath::Rack::Formatters::JSON # JSON output formatter
- use Goliath::Rack::Render # auto-negotiate response format
+ use Goliath::Rack::Render, 'json' # auto-negotiate response format
+ use Goliath::Rack::Params # parse & merge query and body parameters
use Goliath::Rack::Heartbeat # respond to /status with 200, OK (monitoring, etc)
- use Goliath::Rack::ValidationError # catch and render validation errors
- use Goliath::Rack::Validation::RequestMethod, %w(GET) # allow GET requests only
+ # If you are using Golaith version <=0.9.1 you need to Goliath::Rack::ValidationError
+ # to prevent the request from remaining open after an error occurs
+ #use Goliath::Rack::ValidationError
+ use Goliath::Rack::Validation::RequestMethod, %w(GET POST) # allow GET and POST requests only
use Goliath::Rack::Validation::RequiredParam, {:key => 'echo'} # must provide ?echo= query or body param
plugin Goliath::Plugin::Latency # output reactor latency every second
@@ -34,4 +32,4 @@ def process_request
def response(env)
[200, {}, process_request]
end
-end
+end
20 examples/env_use_statements.rb
View
@@ -0,0 +1,20 @@
+#!/usr/bin/env ruby
+$:<< '../lib' << 'lib'
+
+require 'goliath'
+require 'yajl'
+
+# API must be started with -e [production, development, ...]
+# or set your ENV['RACK_ENV'] to specify the environemtn
+
+class EnvUseStatements < Goliath::API
+ if Goliath.dev?
+ use Goliath::Rack::Render, 'json'
+ elsif Goliath.prod?
+ use Goliath::Rack::Render, 'xml'
+ end
+
+ def response(env)
+ [200, {}, {'Test' => 'Response'}]
+ end
+end
40 examples/favicon.rb
View
@@ -0,0 +1,40 @@
+#!/usr/bin/env ruby
+require 'time'
+
+#
+# Reads a favicon.ico statically at load time, renders it on any request for
+# '/favicon.ico', and sends every other request on downstream.
+#
+# If you will be serving even one more file than this one, you should instead
+# use Rack::Static:
+#
+# use(Rack::Static, # render static files from ./public
+# :root => Goliath::Application.app_path("public"),
+# :urls => ["/favicon.ico", '/stylesheets', '/javascripts', '/images'])
+#
+class Favicon
+ def initialize(app, filename)
+ @@favicon = File.read(filename)
+ @@last_mod = File.mtime(filename).utc.rfc822
+ @@expires = Time.at(Time.now + 604800).utc.rfc822 # 1 week from now
+ @app = app
+ end
+
+ def call(env, *args)
+ if env['REQUEST_PATH'] == '/favicon.ico'
+ return [200, {"Last-Modified"=> @@last_mod.to_s, "Expires" => @@expires, "Content-Type"=>"image/vnd.microsoft.icon"}, @@favicon]
+ else
+ return @app.call(env)
+ end
+ end
+end
+
+if File.expand_path($0) == File.expand_path(__FILE__)
+ $:<< '../lib' << 'lib'
+ require 'goliath'
+ puts "starting hello world!"
+ class HelloWorld < Goliath::API
+ HelloWorld.use(Favicon, File.expand_path(File.dirname(__FILE__)+"/public/favicon.ico"))
+ end
+ require(File.dirname(__FILE__)+'/hello_world.rb')
+end
4 examples/gziped.rb
View
@@ -27,9 +27,7 @@ class Gziped < Goliath::API
end
use Goliath::Rack::Params # parse & merge query and body parameters
- use Goliath::Rack::Formatters::JSON # JSON output formatter
- use Goliath::Rack::Render # auto-negotiate response format
- use Goliath::Rack::ValidationError # catch and render validation errors
+ use Goliath::Rack::Render, 'json' # auto-negotiate response format
use Goliath::Rack::Validation::RequestMethod, %w(GET) # allow GET requests only
use Goliath::Rack::Validation::RequiredParam, {:key => 'echo'} # must provide ?echo= query or body param
4 examples/http_log.rb
View
@@ -15,7 +15,6 @@
require 'pp'
class HttpLog < Goliath::API
- use ::Rack::Reloader, 0 if Goliath.dev?
use Goliath::Rack::Params
def on_headers(env, headers)
@@ -57,6 +56,7 @@ def to_http_header(k)
# Write the request information into mongo
def record(process_time, resp, client_headers, response_headers)
e = env
+ e.trace('http_log_record')
EM.next_tick do
doc = {
request: {
@@ -82,4 +82,4 @@ def record(process_time, resp, client_headers, response_headers)
e.mongo.insert(doc)
end
end
-end
+end
BIN  examples/public/favicon.ico
View
Binary file not shown
296 examples/public/stylesheets/style.css
View
@@ -0,0 +1,296 @@
+
+/* ==== Scroll down to find where to put your styles :) ==== */
+
+/* HTML5 - Boilerplate */
+
+html, body, div, span, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp,
+small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section, summary,
+time, mark, audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline;
+}
+
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section {
+ display: block;
+}
+
+blockquote, q { quotes: none; }
+blockquote:before, blockquote:after,
+q:before, q:after { content: ''; content: none; }
+ins { background-color: #ff9; color: #000; text-decoration: none; }
+mark { background-color: #ff9; color: #000; font-style: italic; font-weight: bold; }
+del { text-decoration: line-through; }
+abbr[title], dfn[title] { border-bottom: 1px dotted; cursor: help; }
+table { border-collapse: collapse; border-spacing: 0; }
+hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; }
+input, select { vertical-align: middle; }
+
+body { font:13px/1.231 sans-serif; *font-size:small; }
+select, input, textarea, button { font:99% sans-serif; }
+pre, code, kbd, samp { font-family: monospace, sans-serif; }
+
+html { overflow-y: scroll; }
+a:hover, a:active { outline: none; }
+ul, ol { margin-left: 2em; }
+ol { list-style-type: decimal; }
+nav ul, nav li { margin: 0; list-style:none; list-style-image: none; }
+small { font-size: 85%; }
+strong, th { font-weight: bold; }
+td { vertical-align: top; }
+
+sub, sup { font-size: 75%; line-height: 0; position: relative; }
+sup { top: -0.5em; }
+sub { bottom: -0.25em; }
+
+pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; padding: 15px; }
+textarea { overflow: auto; }
+.ie6 legend, .ie7 legend { margin-left: -7px; }
+input[type="radio"] { vertical-align: text-bottom; }
+input[type="checkbox"] { vertical-align: bottom; }
+.ie7 input[type="checkbox"] { vertical-align: baseline; }
+.ie6 input { vertical-align: text-bottom; }
+label, input[type="button"], input[type="submit"], input[type="image"], button { cursor: pointer; }
+button, input, select, textarea { margin: 0; }
+input:valid, textarea:valid { }
+input:invalid, textarea:invalid { border-radius: 1px; -moz-box-shadow: 0px 0px 5px red; -webkit-box-shadow: 0px 0px 5px red; box-shadow: 0px 0px 5px red; }
+.no-boxshadow input:invalid, .no-boxshadow textarea:invalid { background-color: #f0dddd; }
+
+::-moz-selection{ background: #eFdEe9; color:#fff; text-shadow: none; }
+::selection { background:#eFdEe9; color:#fff; text-shadow: none; }
+a:link { -webkit-tap-highlight-color: #eFdEe9; }
+
+button { width: auto; overflow: visible; }
+.ie7 img { -ms-interpolation-mode: bicubic; }
+
+body, select, input, textarea { color: #444; }
+h1, h2, h3, h4, h5, h6 { font-weight: bold; }
+a, a:active, a:visited { color: #607890; }
+a:hover { color: #036; }
+
+/*
+ // ========================================== \\
+ || ||
+ || Your styles ! ||
+ || ||
+ \\ ========================================== //
+*/
+
+
+body{
+ font-family:Helvetica, Helvetica Neue, Arial, sans-serif;
+}
+
+.wrapper{
+ margin:auto;
+ width:960px;
+}
+
+#logo {
+ float: left;
+ margin-top: 10px;
+ margin-right: 20px;
+}
+
+#header-container{
+ background-color:#e4edf6;
+ height:80px;
+ border-bottom:20px solid #f1e5d9;
+ margin-bottom:50px;
+}
+
+#title{
+ font-weight:normal;
+ font-size: 50px;
+ color:white;
+ padding: 15px 5px 10px 0;
+ float:left;
+ clear: none;
+}
+#title a{ text-decoration: none }
+
+h1 { font-size: 185%; padding-top: 14px; padding-bottom: 5px; }
+h2 { font-size: 154%; padding-top: 20px; padding-bottom: 4px; }
+h3 { font-size: 139%; padding-top: 6px; padding-bottom: 4px; }
+h4 { font-size: 116%; padding-top: 4px; padding-bottom: 2px; }
+
+nav{
+ float:right;
+ margin-top:10px;
+}
+
+nav ul, nav ul li{
+ display:inline;
+}
+
+nav a, nav a:visited, nav a:active{
+ padding:10px 20px;
+ color:#666;
+ text-decoration:none;
+ background-color:#d4dde6;
+}
+
+aside{
+ color:#444;
+ padding:20px;
+ float:right;
+ height:500px;
+ width:200px;
+ background-color:#e4edf6;
+ border-bottom:20px solid #e44d26;
+ margin-bottom:50px;
+}
+
+#main {
+ min-height: 550px;
+}
+
+#main p{
+ font:16px/26px Helvetica, Helvetica Neue, Arial;
+ width:620px;
+ text-shadow:none;
+}
+
+#main header h2{
+ padding-bottom:30px;
+}
+
+article header{
+ margin-bottom:50px;
+ padding-bottom:30px;
+ width:700px;
+}
+
+#footer-container{
+ background-color:#e4edf6;
+ height:240px;
+ border-top:40px solid #e4edf6;
+ margin-top:50px;
+}
+
+#footer-container footer{
+ color:#888;
+ text-align:right;
+}
+#footer-container footer a, #footer-container footer a:active, #footer-container footer a:visited { color: #88a; }
+
+.info{
+ position:absolute;
+ top:5px;
+ background-color:white;
+ padding:10px;
+}
+
+#jquery-test{
+ top:45px;
+}
+
+.ir { display: block; text-indent: -999em; overflow: hidden; background-repeat: no-repeat; text-align: left; direction: ltr; }
+.hidden { display: none; visibility: hidden; }
+.visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; }
+.visuallyhidden.focusable:active,
+.visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; }
+.invisible { visibility: hidden; }
+.clearfix:before, .clearfix:after { content: "\0020"; display: block; height: 0; overflow: hidden; }
+.clearfix:after { clear: both; }
+.clearfix { zoom: 1; }
+
+
+.meter {
+ margin: 3em;
+ background-color: #eee;
+ padding: 2em;
+ font-size: 154%;
+ }
+
+
+/* Nice table styling for greater dashboard justice */
+table {
+ text-align: left;
+}
+table th, table td {
+ text-align: left;
+}
+table th {
+ font-weight: bold;
+}
+table th, table td {
+ min-width: 30px;
+ padding: 4px 0 4px 10px;
+}
+table th:first-child, table td:first-child {
+ padding-left: 0;
+}
+table tbody tr:hover {
+ background-color: #e8f0ff;
+}
+
+table.full {
+ width: 960px;
+}
+table.horizontal th {
+ border-bottom: 2px solid #6678b1;
+}
+table.horizontal th, table.horizontal td {
+ min-width: 70px;
+}
+
+table.vertical {
+ margin: 16px 0;
+}
+table.vertical th {
+ width: 180px;
+ padding: 4px 5px 3px 5px;
+ border-bottom: 1px solid #51ded8;
+}
+table.vertical td {
+ padding: 4px 0 3px 10px;
+ border-bottom: 1px solid #f1eee8;
+}
+table.vertical th:first-child {
+ white-space: nowrap;
+}
+table.vertical tr:last-child {
+ border: none;
+}
+
+
+
+@media all and (orientation:portrait) {
+
+}
+
+@media all and (orientation:landscape) {
+
+}
+
+@media screen and (max-device-width: 480px) {
+
+ /* html { -webkit-text-size-adjust:none; -ms-text-size-adjust:none; } */
+}
+
+
+@media print {
+ * { background: transparent !important; color: black !important; text-shadow: none !important; filter:none !important;
+ -ms-filter: none !important; }
+ a, a:visited { color: #444 !important; text-decoration: underline; }
+ a[href]:after { content: " (" attr(href) ")"; }
+ abbr[title]:after { content: " (" attr(title) ")"; }
+ .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; }
+ pre, blockquote { border: 1px solid #999; page-break-inside: avoid; }
+ thead { display: table-header-group; }
+ tr, img { page-break-inside: avoid; }
+ @page { margin: 0.5cm; }
+ p, h2, h3 { orphans: 3; widows: 3; }
+ h2, h3{ page-break-after: avoid; }
+}
87 examples/rack_routes.rb
View
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# encoding: utf-8
$:<< '../lib' << 'lib'
require 'goliath'
@@ -19,26 +20,106 @@ def response(env)
end
end
+class PostHelloWorld < Goliath::API
+ def response(env)
+ [200, {}, "hello post world!"]
+ end
+end
+
+class HeaderCollector < Goliath::API
+ def on_headers(env, header)
+ @headers ||= {}
+ @headers.merge!(header)
+ end
+
+ def response(env)
+ [200, {}, "headers: #{@headers.inspect}"]
+ end
+end
+
+class HelloNumber < Goliath::API
+ use Goliath::Rack::Params
+ def response(env)
+ [200, {}, "number #{params[:number]}!"]
+ end
+end
+
+class BigNumber < Goliath::API
+ use Goliath::Rack::Params
+ def response(env)
+ [200, {}, "big number #{params[:number]}!"]
+ end
+end
+
class Bonjour < Goliath::API
def response(env)
[200, {}, "bonjour!"]
end
end
+class Hola < Goliath::API
+ use Goliath::Rack::Params
+ use Goliath::Rack::Validation::RequiredParam, {:key => "foo"}
+
+ def response(env)
+ [200, {}, "hola!"]
+ end
+end
+
+class Aloha < Goliath::API
+ use Goliath::Rack::Validation::RequestMethod, %w(GET)
+
+ def response(env)
+ [200, {}, "Aloha"]
+ end
+end
+
class RackRoutes < Goliath::API
map '/version' do
run Proc.new { |env| [200, {"Content-Type" => "text/html"}, ["Version 0.1"]] }
end
- map "/hello_world" do
+ post "/hello_world" do
+ run PostHelloWorld.new
+ end
+
+ get "/hello_world" do
+ run HelloWorld.new
+ end
+
+ head "/hello_world" do
run HelloWorld.new
end
+ map "/headers", HeaderCollector do
+ use Goliath::Rack::Validation::RequestMethod, %w(GET)
+ end
+
map "/bonjour" do
run Bonjour.new
end
- map '/' do
- run Proc.new { |env| [404, {"Content-Type" => "text/html"}, ["Try /version /hello_world or /bonjour"]] }
+ map "/hola" do
+ use Goliath::Rack::Validation::RequestMethod, %w(GET)
+ run Hola.new
+ end
+
+ map "/aloha", Aloha
+
+ map "/:number", :number => /\d+/ do
+ if params[:number].to_i > 100
+ run BigNumber.new
+ else
+ run HelloNumber.new
+ end
+ end
+
+ not_found('/') do
+ run Proc.new { |env| [404, {"Content-Type" => "text/html"}, ["Try /version /hello_world, /bonjour, or /hola"]] }
+ end
+
+ # You must use either maps or response, but never both!
+ def response(env)
+ raise RuntimeException.new("#response is ignored when using maps, so this exception won't raise. See spec/integration/rack_routes_spec.")
end
end
15 examples/rasterize/rasterize.js
View
@@ -0,0 +1,15 @@
+if (phantom.state.length === 0) {
+ if (phantom.args.length !== 2) {
+ console.log('Usage: rasterize.js URL filename');
+ phantom.exit();
+ } else {
+ var address = phantom.args[0];
+ phantom.state = 'rasterize';
+ phantom.viewportSize = { width: 800, height: 600 };
+ phantom.open(address);
+ }
+} else {
+ var output = phantom.args[1];
+ phantom.render(output);
+ phantom.exit();
+}
37 examples/rasterize/rasterize.rb
View
@@ -0,0 +1,37 @@
+#!/usr/bin/env ruby
+$: << File.dirname(__FILE__)+'/../../lib'
+require 'goliath'
+require 'postrank-uri'
+require File.dirname(__FILE__)+'/../favicon'
+
+# Install phantomjs: http://code.google.com/p/phantomjs/wiki/QuickStart
+# $> ruby rasterize.rb -sv
+# $> curl http://localhost:9000/?url=http://www.google.com (or rather, visit in the browser!)
+
+class Rasterize < Goliath::API
+ use Goliath::Rack::Params
+ use Favicon, File.expand_path(File.dirname(__FILE__)+"/../public/favicon.ico")
+ use Goliath::Rack::Validation::RequestMethod, %w(GET)
+ use Goliath::Rack::Validation::RequiredParam, {:key => 'url'}
+
+ def response(env)
+ url = PostRank::URI.clean(params['url'])
+ hash = PostRank::URI.hash(url, :clean => false)
+
+ if !File.exists? filename(hash)
+ fiber = Fiber.current
+ EM.system('phantomjs rasterize.js ' + url.to_s + ' ' + filename(hash)) do |output, status|
+ env.logger.info "Phantom exit status: #{status}"
+ fiber.resume
+ end
+
+ Fiber.yield
+ end
+
+ [202, {'X-Phantom' => 'Goliath'}, IO.read(filename(hash))]
+ end
+
+ def filename(hash)
+ "thumb/#{hash}.png"
+ end
+end
42 examples/rasterize/rasterize_and_shorten.rb
View
@@ -0,0 +1,42 @@
+#!/usr/bin/env ruby
+$: << File.dirname(__FILE__)+'/../../lib'
+require File.dirname(__FILE__)+'/rasterize'
+require File.dirname(__FILE__)+'/../favicon'
+
+require 'goliath'
+require 'em-synchrony/em-http'
+require 'postrank-uri'
+
+#
+# Aroundware: while the Rasterize API is processing, this uses http://is.gd to
+# generate a shortened link, stuffing it in the header. Both requests happen
+# simultaneously.
+#
+class ShortenURL
+ include Goliath::Rack::BarrierAroundware
+ SHORTENER_URL_BASE = 'http://is.gd/create.php'
+ attr_accessor :shortened_url
+
+ def pre_process
+ target_url = PostRank::URI.clean(env.params['url'])
+ shortener_request = EM::HttpRequest.new(SHORTENER_URL_BASE).aget(:query => { :format => 'simple', :url => target_url })
+ enqueue :shortened_url, shortener_request
+ return Goliath::Connection::AsyncResponse
+ end
+
+ def post_process
+ if shortened_url
+ headers['X-Shortened-URI'] = shortened_url.response
+ end
+ [status, headers, body]
+ end
+end
+
+class RasterizeAndShorten < Rasterize
+ use Goliath::Rack::Params
+ use Favicon, File.expand_path(File.dirname(__FILE__)+"/../public/favicon.ico")
+ use Goliath::Rack::Validation::RequestMethod, %w(GET)
+ use Goliath::Rack::Validation::RequiredParam, {:key => 'url'}
+ #
+ use Goliath::Rack::BarrierAroundwareFactory, ShortenURL
+end
BIN  examples/rasterize/thumb/f7ad4cb03e5bfd0e2c43db8e598fb3cd.png
View
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 examples/stream.rb
View
@@ -21,7 +21,7 @@ def on_close(env)
def response(env)
i = 0
pt = EM.add_periodic_timer(1) do
- env.stream_send("#{i} ")
+ env.stream_send("#{i}\n")
i += 1
end
@@ -32,6 +32,6 @@ def response(env)
env.stream_close
end
- [200, {}, Goliath::Response::STREAMING]
+ streaming_response(202, {'X-Stream' => 'Goliath'})
end
end
48 examples/template.rb
View
@@ -0,0 +1,48 @@
+#!/usr/bin/env ruby
+$:<< '../lib' << 'lib'
+
+# A simple dashboard for goliath
+# See
+# examples/views -- templates
+# examples/public -- static files
+# examples/config/template.rb -- configuration
+#
+# The templating is based on, but not as fancy-pants as, Sinatra's. Notably,
+# your template's extension must match the engine (foo.markdown, not foo.md)
+
+require 'tilt'
+# use bluecloth as default markdown renderer
+require 'bluecloth'
+Tilt.register 'markdown', Tilt::BlueClothTemplate
+require 'yajl/json_gem'
+
+require 'goliath'
+require 'goliath/rack/templates'
+require 'goliath/plugins/latency'
+
+class Template < Goliath::API
+ include Goliath::Rack::Templates # render templated files from ./views
+
+ use(Rack::Static, # render static files from ./public
+ :root => Goliath::Application.app_path("public"),
+ :urls => ["/favicon.ico", '/stylesheets', '/javascripts', '/images'])
+
+ plugin Goliath::Plugin::Latency # ask eventmachine reactor to track its latency
+
+ def recent_latency
+ Goliath::Plugin::Latency.recent_latency
+ end
+
+ def response(env)
+ case env['PATH_INFO']
+ when '/' then [200, {}, haml(:root)]
+ when '/debug' then [200, {}, haml(:debug)]
+ when '/oops' then [200, {}, haml(:no_such_template)] # will 500
+ when '/joke' then
+ [200, {}, markdown(:joke, :locals => {:title => "HERE IS A JOKE"})]
+ when '/erb_me' then
+ [200, {}, markdown(:joke, :layout_engine => :erb, :locals => {:title => "HERE IS A JOKE"})]
+ else raise Goliath::Validation::NotFoundError
+ end
+ end
+end
125 examples/test_rig.rb
View
@@ -0,0 +1,125 @@
+#!/usr/bin/env ruby
+$:<< '../lib' << 'lib'
+
+require 'goliath'
+
+#
+# A test endpoint that will:
+# * with 'delay' parameter, take the given time to respond
+# * with 'drop' parameter, drop connection before responding
+# * with 'fail' parameter, raise an error of the given type (eg 400 raises a BadRequestError)
+# * with 'echo_status', 'echo_headers', or 'echo_body' parameter, replace the given component directly.
+#
+
+# If the delay param is given, sleep for that many seconds
+#
+# Note that though this is non-blocking, the call chain does *not* proceed in parallel
+class Delay
+ include Goliath::Rack::AsyncMiddleware
+
+ def post_process(env, status, headers, body)
+ if delay = env.params['delay']
+ delay = [0, [delay.to_f, 5].min].max
+ EM::Synchrony.sleep(delay)
+ body.merge!(:delay => delay, :actual => (Time.now.to_f - env[:start_time]))
+ end
+ [status, headers, body]
+ end
+end
+
+# if the middleware_failure parameter is given, raise an error causing that
+# status code
+class MiddlewareFailure
+ include Goliath::Rack::AsyncMiddleware
+
+ def call(env)
+ if code = env.params['fail']
+ raise Goliath::Validation::Error.new(code.to_i, "Middleware error #{code}")
+ end
+ super
+ end
+end
+
+# if the drop_pre parameter is given, close the connection before headers are sent
+# This works, but probably does awful awful things to Goliath's innards
+class DropConnection
+ include Goliath::Rack::AsyncMiddleware
+
+ def call(env)
+ if env.params['drop'].to_s == 'true'
+ env.logger.info "Dropping connection"
+ env.stream_close
+ [0, {}, '']
+ else
+ super
+ end
+ end
+end
+
+# if echo_status, echo_headers or echo_body are given, blindly substitute their
+# value, clobbering whatever was there.
+#
+# If you are going to use echo_headers you probably need to use a JSON post body:
+# curl -v -H "Content-Type: application/json" --data-ascii '{"echo_headers":{"X-Question":"What is brown and sticky"},"echo_body":{"answer":"a stick"}}' 'http://127.0.0.1:9001/'
+#
+class Echo
+ include Goliath::Rack::AsyncMiddleware
+
+ def post_process env, status, headers, body
+ if env.params['echo_status']
+ status = env.params['echo_status'].to_i
+ end
+ if env.params['echo_headers']
+ headers = env.params['echo_headers']
+ end
+ if env.params['echo_body']
+ body = env.params['echo_body']
+ end
+ [status, headers, body]
+ end
+end
+
+# Rescue validation errors and send them up the chain as normal non-200 responses
+class ExceptionHandler
+ include Goliath::Rack::AsyncMiddleware
+ include Goliath::Rack::Validator
+
+ def call(env)
+ begin
+ super
+ rescue Goliath::Validation::Error => e
+ validation_error(e.status_code, e.message)
+ end
+ end
+end
+
+class TestRig < Goliath::API
+ use Goliath::Rack::Tracer # log trace statistics
+ use Goliath::Rack::Params # parse & merge query and body parameters
+ #
+ use Goliath::Rack::DefaultMimeType # cleanup accepted media types
+ use Goliath::Rack::Render, 'json' # auto-negotiate response format
+ #
+ use ExceptionHandler # turn raised errors into HTTP responses
+ use MiddlewareFailure # make response fail if 'fail' param
+ use DropConnection # drop connection if 'drop' param
+ use Echo # replace status, headers or body if 'echo_status' etc given
+ use Delay # make response take X seconds if 'delay' param
+
+ def on_headers(env, headers)
+ env['client-headers'] = headers
+ end
+
+ def response(env)
+ query_params = env.params.collect { |param| param.join(": ") }
+ query_headers = env['client-headers'].collect { |param| param.join(": ") }
+
+ headers = {
+ "Special" => "Header",
+ "Params" => query_params.join("|"),
+ "Path" => env[Goliath::Request::REQUEST_PATH],
+ "Headers" => query_headers.join("|"),
+ "Method" => env[Goliath::Request::REQUEST_METHOD]}
+ [200, headers, headers.dup]
+ end
+end
10 examples/valid.rb
View
@@ -4,15 +4,13 @@
require 'goliath'
class Valid < Goliath::API
-
- # reload code on every request in dev environment
- use ::Rack::Reloader, 0 if Goliath.dev?
-
use Goliath::Rack::Params
- use Goliath::Rack::ValidationError
-
use Goliath::Rack::Validation::RequiredParam, {:key => 'test'}
+ # If you are using Golaith version <=0.9.1 you need to use Goliath::Rack::ValidationError
+ # to prevent the request from remaining open after an error occurs
+ #use Goliath::Rack::ValidationError
+
def response(env)
[200, {}, 'OK']
end
4 examples/views/debug.haml
View
@@ -0,0 +1,4 @@
+%h1 Debug
+
+%pre
+ = JSON.pretty_generate(env)
13 examples/views/joke.markdown
View
@@ -0,0 +1,13 @@
+# Pirate Joke
+
+Pirate walks into a bar with a steering wheel half-in, half-out of his pants.
+
+
+Bartender says
+
+ "Hey Pirate, What's With The Steering Wheel?"
+
+
+Pirate says
+
+ "Arr, I dunno matey -- but it's drivin' me nuts!"
12 examples/views/layout.erb
View
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title><%= title || 'Goliath' %></title>
+ </head>
+ <body lang='en'>
+ <h1 id='title'><%= title || 'Goliath' %> - I AM ERB</h1>
+
+<%= yield %>
+
+ </body>
+</html>
39 examples/views/layout.haml
View
@@ -0,0 +1,39 @@
+- title ||= 'Goliath'
+!!! 5
+%html
+ %head
+ %meta{ :charset => "utf-8" }/
+ %meta{ :content => "IE=edge,chrome=1", "http-equiv" => "X-UA-Compatible" }/
+
+ %title= title
+ <link href="data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABmJLR0T///////8JWPfcAAAACXBIWXMAAABIAAAASABGyWs+AAAAF0lEQVRIx2NgGAWjYBSMglEwCkbBSAcACBAAAeaR9cIAAAAASUVORK5CYII=" rel="icon" type="image/x-icon" />
+ %link{ :href => "/stylesheets/style.css", :rel => "stylesheet", :type => "text/css", :media => "all" }
+
+ %body{ :lang => 'en' }
+ #header-container
+ %header.wrapper
+ %h1#title
+ == <a href="/">#{title}</a>
+
+ %nav
+ %ul.menu
+ %li.item
+ <a href="/">Home</a>
+ %li.item
+ <a href="/debug">Debug</a>
+ %li &nbsp;
+
+ #main.wrapper{ :role => 'main' }
+ != yield
+
+ #footer-container
+ %footer.wrapper
+ .copyright
+ %p Copyright &copy; #{Time.now.strftime("%Y")}
+
+ %script{ :src => "http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js", :type => "text/javascript" }/
+ :javascript
+ !window.jQuery && document.write(unescape('%3Cscript src="/javascripts/jquery.min.js"%3E%3C/script%3E'))
+
+ /
+ haml layout
28 examples/views/root.haml
View
@@ -0,0 +1,28 @@
+%h2 Routes
+
+%ul
+ %li <a href="/joke">Tell me a joke</a>
+ %li <a href="/erb_me">Tell me a joke, but with a boring .erb layout</a>
+ %li <a href="/debug">debug info</a>
+ %li <a href="/oops">Template missing</a>
+
+%h2 Dashboard
+
+%table.vertical
+ %tr
+ %th Script
+ %td= $0
+ %tr
+ %th Server Port
+ %td= env['SERVER_PORT']
+ %tr
+ %th Latency
+ %td= recent_latency
+ %tr
+ %th Request took
+ %td= (Time.now.to_f - env[:start_time]).round(3)
+ %tr
+ %th Config
+ %td
+ %pre
+ = "\n"+JSON.pretty_generate(env.config)
27 goliath.gemspec
View
@@ -8,35 +8,44 @@ Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.authors = ['dan sinclair', 'Ilya Grigorik']
s.email = ['dj2@everburning.com', 'ilya@igvita.com']
- s.homepage = 'http://labs.postrank.com/'
- s.summary = 'Framework for writing API servers'
+ s.homepage = 'http://goliath.io/'
+ s.summary = 'Async framework for writing API servers'
s.description = s.summary
s.required_ruby_version = '>=1.9.2'
s.add_dependency 'eventmachine', '>= 1.0.0.beta.1'
s.add_dependency 'em-synchrony', '>= 0.3.0.beta.1'
- # s.add_dependency 'em-websocket'
- # s.add_dependency 'http_parser.rb'
+ s.add_dependency 'em-websocket'
+ s.add_dependency 'http_parser.rb'
s.add_dependency 'log4r'
- s.add_dependency 'rack'
+ s.add_dependency 'rack', '>=1.2.2'
s.add_dependency 'rack-contrib'
s.add_dependency 'rack-respond_to'
s.add_dependency 'async-rack'
s.add_dependency 'multi_json'
+ s.add_dependency 'http_router', '~> 0.9.0'
+ s.add_development_dependency 'rake', '>=0.8.7'
s.add_development_dependency 'rspec', '>2.0'
s.add_development_dependency 'nokogiri'
- s.add_development_dependency 'em-http-request', '>= 1.0.0.beta.1'
- s.add_development_dependency 'em-mongo'
+ s.add_development_dependency 'em-http-request', '>=1.0.0'
+ s.add_development_dependency 'em-mongo', '~> 0.4.0'
s.add_development_dependency 'yajl-ruby'
s.add_development_dependency 'rack-rewrite'
+ s.add_development_dependency 'multipart_body'
+ s.add_development_dependency 'amqp', '>=0.7.1'
+ s.add_development_dependency 'tilt', '>=1.2.2'
+ s.add_development_dependency 'haml', '>=3.0.25'
s.add_development_dependency 'yard'
s.add_development_dependency 'bluecloth'
- s.files = `git ls-files`.split("\n")
- s.test_files = `git ls-files -- spec/*`.split("\n")
+ ignores = File.readlines(".gitignore").grep(/\S+/).map {|s| s.chomp }
+ dotfiles = [".gemtest", ".gitignore", ".rspec", ".yardopts"]
+
+ s.files = Dir["**/*"].reject {|f| File.directory?(f) || ignores.any? {|i| File.fnmatch(i, f) } } + dotfiles
+ s.test_files = s.files.grep(/^spec\//)
s.require_paths = ['lib']
end
36 lib/goliath.rb
View
@@ -1,38 +1,2 @@
-require 'eventmachine'
-require 'http/parser'
-require 'async_rack'
-require 'stringio'
-
-require 'goliath/version'
-require 'goliath/goliath'
-require 'goliath/runner'
-require 'goliath/server'
-require 'goliath/constants'
-require 'goliath/connection'
-require 'goliath/request'
-require 'goliath/response'
-require 'goliath/headers'
-require 'goliath/http_status_codes'
-
-require 'goliath/rack/default_response_format'
-require 'goliath/rack/heartbeat'
-require 'goliath/rack/params'
-require 'goliath/rack/render'
-require 'goliath/rack/default_mime_type'
-require 'goliath/rack/tracer'
-require 'goliath/rack/validation_error'
-require 'goliath/rack/formatters/json'
-require 'goliath/rack/formatters/html'
-require 'goliath/rack/formatters/xml'
-require 'goliath/rack/jsonp'
-
-require 'goliath/rack/validation/request_method'
-require 'goliath/rack/validation/required_param'
-require 'goliath/rack/validation/required_value'
-require 'goliath/rack/validation/numeric_range'
-require 'goliath/rack/validation/default_params'
-require 'goliath/rack/validation/boolean_value'
-
require 'goliath/api'
-
require 'goliath/application'
178 lib/goliath/api.rb
View
@@ -1,5 +1,9 @@
+require 'http_router'
+require 'goliath/goliath'
require 'goliath/response'
require 'goliath/request'
+require 'goliath/rack'
+require 'goliath/validation'
module Goliath
# All Goliath APIs subclass Goliath::API. All subclasses _must_ override the
@@ -15,13 +19,35 @@ module Goliath
# end
#
class API
+ include Goliath::Constants
+ include Goliath::Rack::Validator
+
class << self
+ # Catches the userland class which inherits the Goliath API
+ #
+ # In case of further subclassing, the very last class encountered is used.
+ def inherited(subclass)
+ Goliath::Application.app_class = subclass.name if defined?(Goliath::Application)
+ end
+
# Retrieves the middlewares defined by this API server
#
# @return [Array] array contains [middleware class, args, block]
def middlewares
- @middlewares ||= [[::Rack::ContentLength, nil, nil],
- [Goliath::Rack::DefaultResponseFormat, nil, nil]]
+ @middlewares ||= []
+
+ unless @loaded_default_middlewares
+ @middlewares.unshift([::Goliath::Rack::DefaultResponseFormat, nil, nil])
+ @middlewares.unshift([::Rack::ContentLength, nil, nil])
+
+ if Goliath.dev? && !@middlewares.detect {|mw| mw.first == ::Rack::Reloader}
+ @middlewares.unshift([::Rack::Reloader, 0, nil])
+ end
+
+ @loaded_default_middlewares = true
+ end
+
+ @middlewares
end
# Specify a middleware to be used by the API
@@ -36,8 +62,17 @@ def middlewares
# @param name [Class] The middleware class to use
# @param args Any arguments to pass to the middeware
# @param block A block to pass to the middleware
- def use(name, args = nil, &block)
- middlewares.push([name, args, block])
+ def use(name, *args, &block)
+ @middlewares ||= []
+
+ if name == Goliath::Rack::Render
+ [args].flatten.each do |type|
+ type = Goliath::Rack::Formatters.const_get type.upcase
+ @middlewares << [type, nil, nil]
+ end
+ end
+
+ @middlewares << [name, args, block]
end
# Returns the plugins configured for this API
@@ -60,11 +95,15 @@ def plugin(name, *args)
# Returns the router maps configured for the API
#
- # @return [Array] array contains [path, block]
+ # @return [Array] array contains [path, klass, block]
def maps
@maps ||= []
end
+ def maps?
+ !maps.empty?
+ end
+
# Specify a router map to be used by the API
#
# @example
@@ -72,10 +111,50 @@ def maps
# run Proc.new {|env| [200, {"Content-Type" => "text/html"}, ["Version 0.1"]] }
# end
#
- # @param name [String] The URL path to map
+ # @example
+ # map '/user/:id', :id => /\d+/ do
+ # # params[:id] will be a number
+ # run Proc.new {|env| [200, {"Content-Type" => "text/html"}, ["Loading user #{params[:id]}"]] }
+ # end
+ #
+ # @param name [String] The URL path to map.
+ # Optional parts are supported via <tt>(.:format)</tt>, variables as <tt>:var</tt> and globs via <tt>*remaining_path</tt>.
+ # Variables can be validated by supplying a Regexp.
+ # @param klass [Class] The class to retrieve the middlewares from
# @param block The code to execute
- def map(name, &block)
- maps.push([name, block])
+ def map(name, *args, &block)
+ opts = args.last.is_a?(Hash) ? args.pop : {}
+ klass = args.first
+ maps.push([name, klass, opts, block])
+ end
+
+ [:get, :post, :head, :put, :delete].each do |http_method|
+ class_eval <<-EOT, __FILE__, __LINE__ + 1
+ def #{http_method}(name, *args, &block)
+ opts = args.last.is_a?(Hash) ? args.pop : {}
+ klass = args.first
+ opts[:conditions] ||= {}
+ opts[:conditions][:request_method] = [#{http_method.to_s.upcase.inspect}]
+ map(name, klass, opts, &block)
+ end
+ EOT
+ end
+
+ def router
+ unless @router
+ @router = HttpRouter.new
+ @router.default(proc{ |env|
+ @router.routes.last.dest.call(env)
+ })
+ end
+ @router
+ end
+
+ # Use to define the 404 routing logic. As well, define any number of other paths to also run the not_found block.
+ def not_found(*other_paths, &block)
+ app = ::Rack::Builder.new(&block).to_app
+ router.default(app)
+ other_paths.each {|path| router.add(path).to(app) }
end
end
@@ -98,7 +177,7 @@ def options_parser(opts, options)
#
# @return [Goliath::Env] The current environment data for the request
def env
- Thread.current[Goliath::Constants::GOLIATH_ENV]
+ Thread.current[GOLIATH_ENV]
end
# The API will proxy missing calls to the env object if possible.
@@ -119,32 +198,42 @@ def method_missing(name, *args, &blk)
end
end
+ # @param name [Symbol] The method to check if we respond to it.
+ # @return [Boolean] True if the API's method_missing responds to the method
+ def respond_to_missing?(name, *)
+ env.respond_to? name
+ end
+
# {#call} is executed automatically by the middleware chain and will setup
# the environment for the {#response} method to execute. This includes setting
- # up a new Fiber, handing any execptions thrown from the API and executing
+ # up a new Fiber, handing any exceptions thrown from the API and executing
# the appropriate callback method for the API.
#
# @param env [Goliath::Env] The request environment
# @return [Goliath::Connection::AsyncResponse] An async response.
def call(env)
- Fiber.new {
- begin
- Thread.current[Goliath::Constants::GOLIATH_ENV] = env
- status, headers, body = response(env)
+ begin
+ Thread.current[GOLIATH_ENV] = env
+ status, headers, body = response(env)
+ if status
if body == Goliath::Response::STREAMING
- env[Goliath::Constants::STREAM_START].call(status, headers)
+ env[STREAM_START].call(status, headers)
else
- env[Goliath::Constants::ASYNC_CALLBACK].call([status, headers, body])
+ env[ASYNC_CALLBACK].call([status, headers, body])
end
+ end
- rescue Exception => e
- env.logger.error(e.message)
- env.logger.error(e.backtrace.join("\n"))
+ rescue Goliath::Validation::Error => e
+ env[RACK_EXCEPTION] = e
+ env[ASYNC_CALLBACK].call(validation_error(e.status_code, e.message))
- env[Goliath::Constants::ASYNC_CALLBACK].call([400, {}, {:error => e.message}])
- end
- }.resume
+ rescue Exception => e
+ env.logger.error(e.message)
+ env.logger.error(e.backtrace.join("\n"))
+ env[RACK_EXCEPTION] = e
+ env[ASYNC_CALLBACK].call(validation_error(500, e.message))
+ end
Goliath::Connection::AsyncResponse