useless 0.0.6

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.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ Gemfile.lock
2
+ log/*.log
3
+ pkg/*
data/DOC.html ADDED
@@ -0,0 +1,28 @@
1
+ <h1 class="platform-title"><span class="the-name-of-the-website">useless.io</span> the useless platform</h1>
2
+
3
+ <p class="platform-description">a collection of APIs that are more or less useless.</p>
4
+
5
+ <h2>Platform Guidelines</h2>
6
+
7
+ <h3>Authentication</h3>
8
+
9
+ <p>Some endpoints require authentication. In order to do so, the client must
10
+ specify an <em>access token</em>. This can be done by using either the
11
+ <code>Authorization</code> request header field or the
12
+ <code>access_token</code> query string parameter. For example:</p>
13
+
14
+ <pre>
15
+ GET /photos/1 HTTP/1.1
16
+ Host: street-art.useless.io
17
+ Authorization: 7Fjfp0ZBr1KtDRbnfVdmIw
18
+ </pre>
19
+
20
+ <p>If an endpoint requires authentication and an access token is not specified
21
+ using one of these methods, a <code>401 Unauthorized</code> response will be returned. If
22
+ an invalid access token is ever specified, regardless of whether the endpoint
23
+ requires authentication, a <code>401 Unauthorized</code> response will also be returned.</p>
24
+
25
+ <h3>Dates</h3>
26
+
27
+ <p>All dates are UTC and will conform to the ISO 8601 format. If a request expects
28
+ date or time data, it should formatted in the same way. For example: <code>2011-11-13T21:23:47+00:00</code></p>
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
data/README.md ADDED
@@ -0,0 +1,23 @@
1
+ useless
2
+ =======
3
+
4
+ A lightweight Rack stack that forms the platform for useless.io.
5
+
6
+ useless's primary responsibility is to dispatch requests to the APIs
7
+ based upon subdomain. It also provides a minimal common interface that
8
+ includes:
9
+
10
+ 1. _authentication_ - if a request can be authenticated by a supported
11
+ strategy, the middleware will set the 'useless.user' environment key to the
12
+ associated user record.
13
+
14
+ 2. _persistence_ - the platform provides access to a Mongo driver instance
15
+ via the `Useless::Mongo` class. An instance is provided in the env via
16
+ 'useless.mongo'.
17
+
18
+ 3. _file storage_ - all files should be stored using the `Useless::FS` instance.
19
+ provided in the env by 'useless.fs'. Any file stored using `Useless::FS` is
20
+ assigned an ID and is served from the '/files/[ID]' endpoint.
21
+
22
+ 4. _logging_ - the platform sets up a logger appropriate for the environment.
23
+ It can be accessed using 'useless.logger' environment key.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
@@ -0,0 +1,134 @@
1
+ body {
2
+ margin: 0;
3
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
4
+ font-size: 16px;
5
+ color: #333;
6
+ line-height: 21px;
7
+ font-weight: 200;
8
+ background-color: #EEE;
9
+ }
10
+
11
+ #container {
12
+ margin: 0 auto;
13
+ width: 760px;
14
+ padding: 3em 2em;
15
+ border-left: 1px solid #DDD;
16
+ border-right: 1px solid #DDD;
17
+ background-color: #FFF;
18
+ }
19
+
20
+ h1, h2, h3, h4, h5, h6 {
21
+ font-weight: normal;
22
+ }
23
+
24
+ h1 {
25
+ margin: 0 0 .25em;
26
+ }
27
+
28
+ h1.platform-title {
29
+ color: #999;
30
+ }
31
+
32
+ h1.platform-title span.the-name-of-the-website {
33
+ color: #333;
34
+ }
35
+
36
+ h2 {
37
+ border-bottom: 1px solid #DDD;
38
+ padding-bottom: 0.25em;
39
+ }
40
+
41
+ h3.endpoint, h3.endpoint pre {
42
+ margin-bottom: 0.25em;
43
+ }
44
+
45
+ h3 {
46
+ font-size: 1.25em;
47
+ margin-top: 4em;
48
+ }
49
+
50
+ h3:first-of-type {
51
+ margin-top: inherit;
52
+ }
53
+
54
+ h4 {
55
+ font-weight: bold;
56
+ margin-bottom: 0.25em;
57
+ }
58
+
59
+ p.api-description, p.platform-description {
60
+ margin-bottom: 3em;
61
+ }
62
+
63
+ p.platform-description {
64
+ margin-top: 0;
65
+ color: #AAA;
66
+ }
67
+
68
+ p.endpoint-description {
69
+ margin-top: 0;
70
+ }
71
+
72
+ code.base-api-url {
73
+ color: #777777;
74
+ display: block;
75
+ }
76
+
77
+ ul.attributes {
78
+ overflow: hidden;
79
+ *zoom: 1;
80
+ padding: 0;
81
+ margin: 0;
82
+ list-style: none;
83
+ margin-bottom: 2em;
84
+ }
85
+
86
+ ul.attributes li {
87
+ float: left;
88
+ padding: 3px 10px;
89
+ border: 1px solid #DDD;
90
+ background-color: #EEE;
91
+ font-size: 0.8em;
92
+ }
93
+
94
+ ul.attributes li.called-out {
95
+ border-color: #AAA;
96
+ background-color: lightYellow;
97
+ }
98
+
99
+ table {
100
+ width: 100%;
101
+ border: 1px solid #DDD;
102
+ border-spacing: 0;
103
+ }
104
+
105
+ table th {
106
+ font-weight: inherit;
107
+ }
108
+
109
+ table td, th {
110
+ border-right: 1px solid #DDD;
111
+ border-bottom: 1px solid #DDD;
112
+ padding: 5px;
113
+ }
114
+
115
+ table th {
116
+ text-align: left;
117
+ color: #999;
118
+ }
119
+
120
+ table td:last-child, table th:last-child {
121
+ border-right-width: 0;
122
+ }
123
+
124
+ table tbody tr:last-child td, table tbody tr:last-child th {
125
+ border-bottom-width: 0;
126
+ }
127
+
128
+ pre.example {
129
+ margin-top: 0;
130
+ padding: 1em;
131
+ border: 1px solid #DDD;
132
+ background-color: #EEE;
133
+ overflow: auto;
134
+ }
@@ -0,0 +1,12 @@
1
+ module Useless
2
+ class Rack
3
+ class Base
4
+ # Useless::Base::Doc just calls .serve on `useless.doc`
5
+ module Doc
6
+ def self.call(env)
7
+ env['useless.doc'].serve File.expand_path('../../../../DOC.html', __FILE__)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,31 @@
1
+ module Useless
2
+ class Rack
3
+ class Base
4
+ # Useless::Base::Files simply retrieves files from Useless::FS and serves
5
+ # them up.
6
+ class Files
7
+ # Provide a helper method for file URLs to keep things consistent.
8
+ def self.url_for(id)
9
+ "http://useless.io/files/#{id}"
10
+ end
11
+
12
+ def call(env)
13
+ # The file ID is everything after the initial '/' in the path
14
+ id = env['PATH_INFO'][1..-1]
15
+
16
+ # Retrieve the file from FS
17
+ file = env['useless.fs'].get(BSON::ObjectId(id))
18
+
19
+ # and serve it up with the associated content type
20
+ return [200, {'Content-Type' => file.content_type}, file.read]
21
+
22
+ # Two things can go wrong, and they'll both raise an error:
23
+ # * the specified ID is not a valid object ID
24
+ # * there is no file corresponding to the ID in the FS
25
+ rescue BSON::InvalidObjectId, Useless::Rack::FS::FileNotFound
26
+ [404, {'Content-Type' => 'text/plain'}, "File not found: #{id}"]
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ module Useless
2
+ class Rack
3
+ # Useless::Base is the Rack endpoint for useless.io proper. It has two
4
+ # responsibilties: show documentation, and serve file from 'useless.fs'.
5
+ class Base
6
+ autoload :Doc, 'useless/rack/base/doc'
7
+ autoload :Files, 'useless/rack/base/files'
8
+
9
+ def call(env)
10
+ # Show platform documentation at root
11
+ app = ::Rack::URLMap.new '/' => Doc,
12
+
13
+ # serve files out of /files/* via the Files Rack endpoint.
14
+ '/files/' => Files.new
15
+
16
+ app.call(env)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ require 'erb'
2
+
3
+ module Useless
4
+ class Rack
5
+ # `Useless::Doc` is a module that handles serving documentation
6
+ module Doc
7
+ TEMPLATE = <<-BLOCK
8
+ <!DOCTYPE html>
9
+ <html>
10
+ <head>
11
+ <title>useless.io</title>
12
+ <link href="/application.css" media="screen" rel="stylesheet" type="text/css" />
13
+ </head>
14
+ <body>
15
+ <div id="container">
16
+ <%= body %>
17
+ </div>
18
+ </body>
19
+ </html>
20
+ BLOCK
21
+
22
+ def self.serve(file_or_path)
23
+ # Read the contents of the file or the file at the specified path.
24
+ body = file_or_path.is_a?(File) ?
25
+ file_or_path.read : File.read(file_or_path)
26
+
27
+ # Render the template,
28
+ response = ERB.new(TEMPLATE).result(binding)
29
+
30
+ # and return it as a Rack response.
31
+ [200, {'Content-Type' => 'text/html'}, [response]]
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ module Useless
2
+ class Rack
3
+ # Useless::FS is a minimal wrapper around Mongo::Grid.
4
+ class FS
5
+ class FileNotFound < StandardError; end
6
+
7
+ def initialize(mongo)
8
+ @mongo = mongo
9
+ end
10
+
11
+ def put(file, opts = {})
12
+ @mongo.grid.put(file, opts)
13
+ end
14
+
15
+ def get(id)
16
+ @mongo.grid.get(id)
17
+
18
+ # To preserve the abstraction, raise our own exception when a file isn't
19
+ # found
20
+ rescue ::Mongo::GridFileNotFound => e
21
+ raise FileNotFound, e.message
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,50 @@
1
+ require 'logger'
2
+
3
+ module Useless
4
+ class Rack
5
+ # Useless::Logger is a wrapper around Logger for the purposes of decorating.
6
+ class Logger
7
+ attr_accessor :request_id
8
+
9
+ def initialize(io = $stdout)
10
+ @logger = ::Logger.new(io)
11
+ end
12
+
13
+ def level=(level)
14
+ @logger.level = level
15
+ end
16
+
17
+ def level
18
+ @logger.level
19
+ end
20
+
21
+ def fatal(message)
22
+ add ::Logger::FATAL, message
23
+ end
24
+
25
+ def error(message)
26
+ add ::Logger::ERROR, message
27
+ end
28
+
29
+ def warn(message)
30
+ add ::Logger::WARN, message
31
+ end
32
+
33
+ def info(message)
34
+ add ::Logger::INFO, message
35
+ end
36
+
37
+ def debug(message)
38
+ add ::Logger::DEBUG, message
39
+ end
40
+
41
+ private
42
+
43
+ def add(level, message)
44
+ formatted_message = message
45
+ formatted_message = "[#{@request_id}] #{formatted_message}" if @request_id
46
+ @logger.add level, formatted_message
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,38 @@
1
+ module Useless
2
+ class Rack
3
+ module Middleware
4
+ # `Assets` maps requests to files in the assets directory.
5
+ class Assets
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ # If the path couldn't be a file in assets, just proxy the request.
12
+ unless env['PATH_INFO'] =~ /\/\w+/
13
+ return @app.call(env)
14
+ end
15
+
16
+ # The path is relative to the assets directory in the gem root
17
+ path = File.expand_path("../../../../assets#{env['PATH_INFO']}", __FILE__)
18
+
19
+ # If there's a corresponding file:
20
+ if File.exists?(path) and !File.directory?(path)
21
+ # Try to infer the type from the extension, resorting to 'plain'
22
+ # if we can't.
23
+ type = path[/\.(\w+)$/, 1] || 'plain'
24
+
25
+ # Read the file,
26
+ response = File.read(path)
27
+
28
+ # and serve it up
29
+ [200, {'Content-Type' => "text/#{type}"}, [response]]
30
+ else
31
+ # Otherwise, just pass the request along
32
+ @app.call(env)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,41 @@
1
+ module Useless
2
+ class Rack
3
+ module Middleware
4
+ module Authentication
5
+ # The `Authentication::AccessToken` module defines the behavior for access-
6
+ # token-based authentication middleware. The middlewares are responsible
7
+ # only for providing the access token via the `#access_token_for_env`
8
+ # method.
9
+ module AccessToken
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ def call(env)
15
+ # If we don't already have a user set in the environment,
16
+ unless env['useless.user']
17
+ # check to see if an access token was specified.
18
+ if access_token = access_token_for_env(env)
19
+ # If so, and a corresponding user can be found,
20
+ if user = env['useless.mongo']['users'].find_one('access_token' => access_token)
21
+ # set 'useless.user' in the environment.
22
+ env['useless.user'] = user
23
+ else
24
+ # Otherwise, return a 401 Unauthorized.
25
+ return [401, {'Content-Type' => 'text/plain'}, ["Invalid access token: #{access_token}"]]
26
+ end
27
+ end
28
+ end
29
+
30
+ @app.call(env)
31
+ end
32
+
33
+ private
34
+
35
+ def access_token_for_env(env)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,19 @@
1
+ module Useless
2
+ class Rack
3
+ module Middleware
4
+ module Authentication
5
+ # The `Authentication::QueryString` middleware attempt to retrieve the
6
+ # access token from the query string.
7
+ class QueryString
8
+ include AccessToken
9
+
10
+ private
11
+
12
+ def access_token_for_env(env)
13
+ ::Rack::Utils.parse_query(env['QUERY_STRING'])['access_token']
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module Useless
2
+ class Rack
3
+ module Middleware
4
+ module Authentication
5
+ # The `Authentication::RequestHeader` middleware attempts to retrieve the
6
+ # access token from the `Authorization` request header.
7
+ class RequestHeader
8
+ include AccessToken
9
+
10
+ private
11
+
12
+ def access_token_for_env(env)
13
+ env['HTTP_AUTHORIZATION']
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ module Useless
2
+ class Rack
3
+ module Middleware
4
+ # `Doc` simply adds Useless::Doc instance to env as useless.doc'.
5
+ class Doc
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ env['useless.doc'] = Useless::Rack::Doc
12
+ @app.call(env)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ module Useless
2
+ class Rack
3
+ module Middleware
4
+ # `Exceptions` is a Rack middleware that handles any exceptions raised by
5
+ # it's app. It has two responsibilities:
6
+ # 1. log the exception trace, if a logger is available, and
7
+ # 2. return a 500 response with the appropriate message
8
+ class Exceptions
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ def call(env)
14
+ @app.call(env)
15
+ rescue => exception
16
+ # First, format the exception trace
17
+ trace = exception.message + "\n" + exception.backtrace.join("\n")
18
+
19
+ # If a logger is available,
20
+ if env['useless.logger']
21
+ # log the trace as fatal.
22
+ env['useless.logger'].fatal trace
23
+ end
24
+
25
+ # Next, if the request is authenticated by an admin or we are in development or test,
26
+ if (env['useless.user'] and env['useless.user']['admin']) or
27
+ ENV['RACK_ENV'] == 'development'
28
+ # we will return the trace;
29
+ message = trace
30
+ else
31
+ # otherwise, return a generic message.
32
+ message = 'An internal server error occurred. Please try again later.'
33
+ end
34
+
35
+ # Finally, return the 500 error.
36
+ [500, {'Content-Type' => 'text/plain'}, [message]]
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,17 @@
1
+ module Useless
2
+ class Rack
3
+ module Middleware
4
+ # `FS` simply adds the Useless.fs instance to env as 'useless.fs'.
5
+ class FS
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ env['useless.fs'] = Useless::Rack.fs
12
+ @app.call(env)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ module Useless
2
+ class Rack
3
+ module Middleware
4
+ # `Useless::Mongo` simply adds the Useless.mongo instance to env as
5
+ # 'useless.mongo'.
6
+ class Mongo
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ env['useless.mongo'] = Useless::Rack.mongo
13
+ @app.call(env)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ require 'low/mongo'
2
+
3
+ module Useless
4
+ class Rack
5
+ module Mongo
6
+ # Returns an instance of the appropriate `Low::Mongo` helper class based upon the
7
+ # `RACK_ENV` environment variable (or the `env` parameter, if specified).
8
+ def self.for_env(env = nil)
9
+ case (env || ENV['RACK_ENV'])
10
+ when 'test' then Low::Mongo::Local.new 'useless_test'
11
+ when 'production' then Low::Mongo::Remote.new ENV['MONGOLAB_URI']
12
+ else Low::Mongo::Local.new 'useless_development'
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,69 @@
1
+ $:.push File.expand_path('../lib', __FILE__)
2
+
3
+ require 'low/rack/log_level'
4
+ require 'low/rack/rack_errors'
5
+ require 'low/rack/request_id'
6
+ require 'low/rack/request_logger'
7
+ require 'low/rack/subdomain_map'
8
+ require 'low/scoped_logger'
9
+
10
+ module Useless
11
+ class Rack
12
+ autoload :Base, 'useless/rack/base'
13
+ autoload :Doc, 'useless/rack/doc'
14
+ autoload :FS, 'useless/rack/fs'
15
+ autoload :Logger, 'useless/rack/logger'
16
+ autoload :Mongo, 'useless/rack/mongo'
17
+
18
+ module Middleware
19
+ autoload :Assets, 'useless/rack/middleware/assets'
20
+ autoload :Doc, 'useless/rack/middleware/doc'
21
+ autoload :Exceptions, 'useless/rack/middleware/exceptions'
22
+ autoload :FS, 'useless/rack/middleware/fs'
23
+ autoload :Mongo, 'useless/rack/middleware/mongo'
24
+
25
+ module Authentication
26
+ autoload :AccessToken, 'useless/rack/middleware/authentication/access_token'
27
+ autoload :QueryString, 'useless/rack/middleware/authentication/query_string'
28
+ autoload :RequestHeader, 'useless/rack/middleware/authentication/request_header'
29
+ end
30
+ end
31
+
32
+ def self.mongo
33
+ @mongo ||= Mongo.for_env
34
+ end
35
+
36
+ def self.fs
37
+ @fs ||= FS.new(mongo)
38
+ end
39
+
40
+ def initialize(map = nil, &block)
41
+ @map = map || {}
42
+ instance_eval &block if block_given?
43
+ end
44
+
45
+ def map(pair)
46
+ subdomain, app = pair.first
47
+ @map[subdomain.to_s] = app.respond_to?(:new) ? app.new : app
48
+ end
49
+
50
+ def call(env)
51
+ ::Rack::Builder.app do
52
+ use Middleware::Exceptions
53
+ use Low::Rack::RequestId
54
+ use Low::Rack::RackErrors
55
+ use Low::Rack::LogLevel
56
+ use Low::Rack::RequestLogger, key: 'useless.logger'
57
+ use ::Rack::CommonLogger
58
+ use Middleware::Assets
59
+ use Middleware::Mongo
60
+ use Middleware::FS
61
+ use Middleware::Doc
62
+ use Middleware::Authentication::QueryString
63
+ use Middleware::Authentication::RequestHeader
64
+
65
+ run Low::Rack::SubdomainMap.new(Base.new, @map)
66
+ end.call(env)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,3 @@
1
+ module Useless
2
+ VERSION = '0.0.6'
3
+ end
@@ -0,0 +1,31 @@
1
+ ENV['RACK_ENV'] = 'test'
2
+ require File.dirname(__FILE__) + '/../lib/useless/rack'
3
+
4
+ require 'rack/mock'
5
+ require 'rack/test'
6
+
7
+ RSpec.configure do |config|
8
+ config.order = :rand
9
+
10
+ def clean_database
11
+ Useless::Rack.mongo.db.collections.each do |collection|
12
+ if !(collection.name =~ /^system\./)
13
+ collection.drop
14
+ end
15
+ end
16
+ end
17
+
18
+ config.before(:suite){ clean_database }
19
+
20
+ config.after(:each) do
21
+ clean_database
22
+
23
+ # After dropping collections, we need to force a new connection on the
24
+ # next request.
25
+ Useless::Rack.mongo.reset_connection!
26
+ end
27
+
28
+ def asset_path(path)
29
+ File.dirname(__FILE__) + '/support/assets/' + path
30
+ end
31
+ end
Binary file
@@ -0,0 +1,41 @@
1
+ require File.dirname(__FILE__) + '/../../../spec_helper'
2
+
3
+ describe 'useless.io/files' do
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ Useless::Rack.new
8
+ end
9
+
10
+ describe 'GET /files/:id' do
11
+ it 'should return a file if one exists for the specified ID' do
12
+ file = File.open(asset_path('muffin-milk.jpg'))
13
+ id = Useless::Rack.fs.put(file)
14
+
15
+ get Useless::Rack::Base::Files.url_for(id)
16
+ last_response.should be_ok
17
+
18
+ # See spec/useless/fs_spec.rb
19
+ file.rewind
20
+ blob = file.read
21
+ blob.force_encoding('BINARY')
22
+ blob.should == last_response.body
23
+ end
24
+
25
+ it 'should return a 404 if the file does not exits' do
26
+ get Useless::Rack::Base::Files.url_for(BSON::ObjectId.new)
27
+ last_response.should be_not_found
28
+ end
29
+
30
+ it 'should return a 404 if the specified ID is invalid' do
31
+ get Useless::Rack::Base::Files.url_for('invalid-id')
32
+ last_response.should be_not_found
33
+ end
34
+ end
35
+
36
+ describe '.url_for' do
37
+ it 'should return the appropriate URL for the specified file ID' do
38
+ Useless::Rack::Base::Files.url_for('abc123').should == 'http://useless.io/files/abc123'
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,22 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe Useless::Rack::FS do
4
+ it 'should allow for files to be added and retrieved via .put and .get' do
5
+ original_file = File.open(asset_path('muffin-milk.jpg'))
6
+ original_file_id = Useless::Rack.fs.put(original_file)
7
+ stored_file = Useless::Rack.fs.get(original_file_id)
8
+
9
+ # Useless::Rack::FS stores the file as an un-encoded binary blob, so in order to
10
+ # compare, we must strip the encoding, i.e. force a binary encoding.
11
+ original_file.rewind
12
+ original_blob = original_file.read
13
+ original_blob.force_encoding('BINARY')
14
+ original_blob.should == stored_file.read
15
+ end
16
+
17
+ describe '.get' do
18
+ it 'should raise Useless::FS::FileNotFound if the specified ID doesn\'t exist' do
19
+ -> { Useless::Rack.fs.get('nonexistant') }.should raise_error(Useless::Rack::FS::FileNotFound)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,86 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe Useless::Rack::Logger do
4
+ describe '#fatal' do
5
+ it 'should emit a fatal message' do
6
+ io = StringIO.new
7
+ logger = Useless::Rack::Logger.new(io)
8
+ logger.fatal 'Oh noes!'
9
+
10
+ io.rewind; message = io.read
11
+ message.should =~ /FATAL/
12
+ message.should =~ /Oh noes!$/
13
+ end
14
+ end
15
+
16
+ describe '#error' do
17
+ it 'should emit an error message' do
18
+ io = StringIO.new
19
+ logger = Useless::Rack::Logger.new(io)
20
+ logger.error 'Wrong!'
21
+
22
+ io.rewind; message = io.read
23
+ message.should =~ /ERROR/
24
+ message.should =~ /Wrong!$/
25
+ end
26
+ end
27
+
28
+ describe '#warn' do
29
+ it 'should emit a warn message' do
30
+ io = StringIO.new
31
+ logger = Useless::Rack::Logger.new(io)
32
+ logger.warn 'Watchout!'
33
+
34
+ io.rewind; message = io.read
35
+ message.should =~ /WARN/
36
+ message.should =~ /Watchout!$/
37
+ end
38
+ end
39
+
40
+ describe '#info' do
41
+ it 'should emit an info message' do
42
+ io = StringIO.new
43
+ logger = Useless::Rack::Logger.new(io)
44
+ logger.info 'Such and such'
45
+
46
+ io.rewind; message = io.read
47
+ message.should =~ /INFO/
48
+ message.should =~ /Such and such$/
49
+ end
50
+ end
51
+
52
+ describe '#debug' do
53
+ it 'should emit a debug message' do
54
+ io = StringIO.new
55
+ logger = Useless::Rack::Logger.new(io)
56
+ logger.debug '101011'
57
+
58
+ io.rewind; message = io.read
59
+ message.should =~ /DEBUG/
60
+ message.should =~ /101011$/
61
+ end
62
+
63
+ it 'should emit nothing if the level is too high' do
64
+ io = StringIO.new
65
+ logger = Useless::Rack::Logger.new(io)
66
+ logger.level = ::Logger::INFO
67
+ logger.debug '101011'
68
+
69
+ io.rewind; message = io.read
70
+ message.should == ''
71
+ end
72
+ end
73
+
74
+ describe '#request_id' do
75
+ it 'should append the specified ID to all messages' do
76
+ io = StringIO.new
77
+ logger = Useless::Rack::Logger.new(io)
78
+ logger.request_id = 'abc123'
79
+ logger.info 'Yadda'
80
+
81
+ io.rewind; message = io.read
82
+ message.should =~ /abc123/
83
+ message.should =~ /Yadda$/
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,45 @@
1
+ require File.dirname(__FILE__) + '/../../../../spec_helper'
2
+
3
+ describe Useless::Rack::Middleware::Authentication::QueryString do
4
+ def authenticated_app
5
+ app = lambda do |env|
6
+ body = env['useless.user'] ?
7
+ "user authenticated: #{env['useless.user']['handle']}" :
8
+ "unauthenticated"
9
+
10
+ [200, {'Content-Type' => 'text/plain'}, [body]]
11
+ end
12
+
13
+ query_string_app = Useless::Rack::Middleware::Authentication::QueryString.new(app)
14
+ Useless::Rack::Middleware::Mongo.new(query_string_app)
15
+ end
16
+
17
+ it 'should do nothing if `useless.user` is already set' do
18
+ Useless::Rack.mongo['users'].insert({'handle' => 'dph', 'access_token' => 'def456'})
19
+ res = Rack::MockRequest.new(authenticated_app).get('/resource?access_token=def456',
20
+ 'useless.user' => {'handle' => 'khy', 'access_token' => 'abc123'})
21
+ res.should be_ok
22
+ res.body.should == 'user authenticated: khy'
23
+ end
24
+
25
+ it 'should do nothing if no access token is specified' do
26
+ res = Rack::MockRequest.new(authenticated_app).get('/resource')
27
+ res.should be_ok
28
+ res.body.should == 'unauthenticated'
29
+ end
30
+
31
+ it 'should return a 401 Unauthenticated if an access token is specified, but
32
+ does not correspond to a user' do
33
+ res = Rack::MockRequest.new(authenticated_app).get('/resource?access_token=abc123')
34
+ res.status.should == 401
35
+ res.body.should == 'Invalid access token: abc123'
36
+ end
37
+
38
+ it 'should set `useless.user` in the environment if a user can be found for
39
+ the specified access token' do
40
+ Useless::Rack.mongo['users'].insert({'handle' => 'khy', 'access_token' => 'abc123'})
41
+ res = Rack::MockRequest.new(authenticated_app).get('/resource?access_token=abc123')
42
+ res.should be_ok
43
+ res.body.should == 'user authenticated: khy'
44
+ end
45
+ end
@@ -0,0 +1,46 @@
1
+ require File.dirname(__FILE__) + '/../../../../spec_helper'
2
+
3
+ describe Useless::Rack::Middleware::Authentication::RequestHeader do
4
+ def authenticated_app
5
+ app = lambda do |env|
6
+ body = env['useless.user'] ?
7
+ "user authenticated: #{env['useless.user']['handle']}" :
8
+ "unauthenticated"
9
+
10
+ [200, {'Content-Type' => 'text/plain'}, [body]]
11
+ end
12
+
13
+ request_header_app = Useless::Rack::Middleware::Authentication::RequestHeader.new(app)
14
+ Useless::Rack::Middleware::Mongo.new(request_header_app)
15
+ end
16
+
17
+ it 'should do nothing if `useless.user` is already set' do
18
+ Useless::Rack.mongo['users'].insert({'handle' => 'dph', 'access_token' => 'def456'})
19
+ res = Rack::MockRequest.new(authenticated_app).get('/resource',
20
+ 'Authorization' => 'def456',
21
+ 'useless.user' => {'handle' => 'khy', 'access_token' => 'abc123'})
22
+ res.should be_ok
23
+ res.body.should == 'user authenticated: khy'
24
+ end
25
+
26
+ it 'should do nothing if no access token is specified' do
27
+ res = Rack::MockRequest.new(authenticated_app).get('/resource')
28
+ res.should be_ok
29
+ res.body.should == 'unauthenticated'
30
+ end
31
+
32
+ it 'should return a 401 Unauthenticated if an access token is specified, but
33
+ does not correspond to a user' do
34
+ res = Rack::MockRequest.new(authenticated_app).get('/resource', 'HTTP_AUTHORIZATION' => 'abc123')
35
+ res.status.should == 401
36
+ res.body.should == 'Invalid access token: abc123'
37
+ end
38
+
39
+ it 'should set `useless.user` in the environment if a user can be found for
40
+ the specified access token' do
41
+ Useless::Rack.mongo['users'].insert({'handle' => 'khy', 'access_token' => 'abc123'})
42
+ res = Rack::MockRequest.new(authenticated_app).get('/resource', 'HTTP_AUTHORIZATION' => 'abc123')
43
+ res.should be_ok
44
+ res.body.should == 'user authenticated: khy'
45
+ end
46
+ end
@@ -0,0 +1,46 @@
1
+ require File.dirname(__FILE__) + '/../../../spec_helper'
2
+
3
+ describe Useless::Rack::Middleware::Exceptions do
4
+ it 'should proxy transparently if nothing goes wrong in the main app' do
5
+ main_app = lambda do |env|
6
+ [200, {'Content-Type' => 'text/plain'}, ['No Problems!']]
7
+ end
8
+ exceptions_app = Useless::Rack::Middleware::Exceptions.new(main_app)
9
+
10
+ res = Rack::MockRequest.new(exceptions_app).get('http://useless.info')
11
+ res.should be_ok
12
+ res.body.should == 'No Problems!'
13
+ end
14
+
15
+ def broken_app
16
+ main_app = lambda do |env|
17
+ raise 'It\'s all broken!'
18
+ [200, {'Content-Type' => 'text/plain'}, ['No Problems!']]
19
+ end
20
+
21
+ Useless::Rack::Middleware::Exceptions.new(main_app)
22
+ end
23
+
24
+ it 'should return a 500 with a generic message if an exception is raised
25
+ in the main app and the request is not authenticated' do
26
+ res = Rack::MockRequest.new(broken_app).get('http://useless.info')
27
+ res.status.should == 500
28
+ res.body.should == 'An internal server error occurred. Please try again later.'
29
+ end
30
+
31
+ it 'should return a 500 with details of the exception if one is raised in
32
+ the main app and the request is authenticated by an admin user' do
33
+ res = Rack::MockRequest.new(broken_app).get('http://useless.info',
34
+ 'useless.user' => {'handle' => 'khy', 'admin' => true})
35
+ res.status.should == 500
36
+ res.body.should =~ /^It's all broken!/
37
+ end
38
+
39
+ it 'should return a 500 with a generic message if an exception is raised
40
+ in the main app and the request is authenticated by an non-admin user' do
41
+ res = Rack::MockRequest.new(broken_app).get('http://useless.info',
42
+ 'useless.user' => {'handle' => 'khy'})
43
+ res.status.should == 500
44
+ res.body.should == 'An internal server error occurred. Please try again later.'
45
+ end
46
+ end
@@ -0,0 +1,62 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe Useless::Rack::Mongo do
4
+ describe '.for_env' do
5
+ it 'should return a instance for \'useless_development\' if \'development\' is specified' do
6
+ mongo = Useless::Rack::Mongo.for_env('development')
7
+ mongo.database.should == 'useless_development'
8
+ end
9
+
10
+ it 'should return a instance for \'useless_development\' if RACK_ENV is \'development\'' do
11
+ original_rack_env = ENV['RACK_ENV']
12
+ begin
13
+ ENV['RACK_ENV'] = 'development'
14
+ mongo = Useless::Rack::Mongo.for_env
15
+ mongo.database.should == 'useless_development'
16
+ ensure
17
+ ENV['RACK_ENV'] = original_rack_env
18
+ end
19
+ end
20
+
21
+ it 'should return a instance for \'useless_test\' if \'test\' is specified' do
22
+ mongo = Useless::Rack::Mongo.for_env('test')
23
+ mongo.database.should == 'useless_test'
24
+ end
25
+
26
+ it 'should return a instance for \'useless_test\' if RACK_ENV is \'test\'' do
27
+ original_rack_env = ENV['RACK_ENV']
28
+ begin
29
+ ENV['RACK_ENV'] = 'test'
30
+ mongo = Useless::Rack::Mongo.for_env
31
+ mongo.database.should == 'useless_test'
32
+ ensure
33
+ ENV['RACK_ENV'] = original_rack_env
34
+ end
35
+ end
36
+
37
+ it 'should return a instance for the MONGOLAB_URI if \'production\' is specified' do
38
+ original_mongolab_uri = ENV['MONGOLAB_URI']
39
+ begin
40
+ ENV['MONGOLAB_URI'] = 'mongodb://mongo.com'
41
+ mongo = Useless::Rack::Mongo.for_env('production')
42
+ mongo.uri.should == 'mongodb://mongo.com'
43
+ ensure
44
+ ENV['MONGOLAB_URI'] = original_mongolab_uri
45
+ end
46
+ end
47
+
48
+ it 'should return a instance for the MONGOLAB_URL if RACK_ENV is \'production\'' do
49
+ original_rack_env = ENV['RACK_ENV']
50
+ original_mongolab_uri = ENV['MONGOLAB_URI']
51
+ begin
52
+ ENV['RACK_ENV'] = 'production'
53
+ ENV['MONGOLAB_URI'] = 'mongodb://mongo.com'
54
+ mongo = Useless::Rack::Mongo.for_env
55
+ mongo.uri.should == 'mongodb://mongo.com'
56
+ ensure
57
+ ENV['RACK_ENV'] = original_rack_env
58
+ ENV['MONGOLAB_URI'] = original_mongolab_uri
59
+ end
60
+ end
61
+ end
62
+ end
data/useless.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/useless/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = 'useless'
6
+ gem.version = Useless::VERSION
7
+ gem.summary = 'The useless.io Ruby library.'
8
+ gem.description = 'Library for building a useless.io-compliant APIs in Ruby.'
9
+
10
+ gem.authors = ['Kevin Hyland']
11
+ gem.email = ['khy@me.com']
12
+ gem.homepage = 'https://github.com/khy/useless.rb'
13
+
14
+ gem.files = `git ls-files`.split($\)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^spec/})
17
+ gem.require_paths = ['lib']
18
+
19
+ gem.add_runtime_dependency 'rack'
20
+ gem.add_runtime_dependency 'low', '0.0.14'
21
+ gem.add_runtime_dependency 'mongo'
22
+ gem.add_runtime_dependency 'bson'
23
+ gem.add_runtime_dependency 'bson_ext'
24
+
25
+ gem.add_development_dependency 'rspec'
26
+ gem.add_development_dependency 'rack-test'
27
+ end
metadata ADDED
@@ -0,0 +1,199 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: useless
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.6
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kevin Hyland
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-27 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rack
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: low
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - '='
36
+ - !ruby/object:Gem::Version
37
+ version: 0.0.14
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - '='
44
+ - !ruby/object:Gem::Version
45
+ version: 0.0.14
46
+ - !ruby/object:Gem::Dependency
47
+ name: mongo
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: bson
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: bson_ext
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rspec
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rack-test
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: Library for building a useless.io-compliant APIs in Ruby.
127
+ email:
128
+ - khy@me.com
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - .gitignore
134
+ - DOC.html
135
+ - Gemfile
136
+ - README.md
137
+ - Rakefile
138
+ - assets/application.css
139
+ - lib/useless/rack.rb
140
+ - lib/useless/rack/base.rb
141
+ - lib/useless/rack/base/doc.rb
142
+ - lib/useless/rack/base/files.rb
143
+ - lib/useless/rack/doc.rb
144
+ - lib/useless/rack/fs.rb
145
+ - lib/useless/rack/logger.rb
146
+ - lib/useless/rack/middleware/assets.rb
147
+ - lib/useless/rack/middleware/authentication/access_token.rb
148
+ - lib/useless/rack/middleware/authentication/query_string.rb
149
+ - lib/useless/rack/middleware/authentication/request_header.rb
150
+ - lib/useless/rack/middleware/doc.rb
151
+ - lib/useless/rack/middleware/exceptions.rb
152
+ - lib/useless/rack/middleware/fs.rb
153
+ - lib/useless/rack/middleware/mongo.rb
154
+ - lib/useless/rack/mongo.rb
155
+ - lib/useless/version.rb
156
+ - spec/spec_helper.rb
157
+ - spec/support/assets/muffin-milk.jpg
158
+ - spec/useless/rack/base/files_spec.rb
159
+ - spec/useless/rack/fs_spec.rb
160
+ - spec/useless/rack/logger_spec.rb
161
+ - spec/useless/rack/middleware/authentication/query_string_spec.rb
162
+ - spec/useless/rack/middleware/authentication/request_header_spec.rb
163
+ - spec/useless/rack/middleware/exceptions_spec.rb
164
+ - spec/useless/rack/mongo_spec.rb
165
+ - useless.gemspec
166
+ homepage: https://github.com/khy/useless.rb
167
+ licenses: []
168
+ post_install_message:
169
+ rdoc_options: []
170
+ require_paths:
171
+ - lib
172
+ required_ruby_version: !ruby/object:Gem::Requirement
173
+ none: false
174
+ requirements:
175
+ - - ! '>='
176
+ - !ruby/object:Gem::Version
177
+ version: '0'
178
+ required_rubygems_version: !ruby/object:Gem::Requirement
179
+ none: false
180
+ requirements:
181
+ - - ! '>='
182
+ - !ruby/object:Gem::Version
183
+ version: '0'
184
+ requirements: []
185
+ rubyforge_project:
186
+ rubygems_version: 1.8.23
187
+ signing_key:
188
+ specification_version: 3
189
+ summary: The useless.io Ruby library.
190
+ test_files:
191
+ - spec/spec_helper.rb
192
+ - spec/support/assets/muffin-milk.jpg
193
+ - spec/useless/rack/base/files_spec.rb
194
+ - spec/useless/rack/fs_spec.rb
195
+ - spec/useless/rack/logger_spec.rb
196
+ - spec/useless/rack/middleware/authentication/query_string_spec.rb
197
+ - spec/useless/rack/middleware/authentication/request_header_spec.rb
198
+ - spec/useless/rack/middleware/exceptions_spec.rb
199
+ - spec/useless/rack/mongo_spec.rb