restfulness 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +342 -0
- data/Rakefile +1 -0
- data/example/Gemfile +5 -0
- data/example/README.md +42 -0
- data/example/app.rb +42 -0
- data/example/config.ru +5 -0
- data/lib/restfulness.rb +36 -0
- data/lib/restfulness/application.rb +83 -0
- data/lib/restfulness/dispatcher.rb +14 -0
- data/lib/restfulness/dispatchers/rack.rb +91 -0
- data/lib/restfulness/exceptions.rb +17 -0
- data/lib/restfulness/log_formatters/quiet_formatter.rb +7 -0
- data/lib/restfulness/log_formatters/verbose_formatter.rb +16 -0
- data/lib/restfulness/path.rb +46 -0
- data/lib/restfulness/request.rb +81 -0
- data/lib/restfulness/resource.rb +111 -0
- data/lib/restfulness/response.rb +57 -0
- data/lib/restfulness/route.rb +48 -0
- data/lib/restfulness/router.rb +27 -0
- data/lib/restfulness/statuses.rb +71 -0
- data/lib/restfulness/version.rb +3 -0
- data/restfulness.gemspec +29 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/unit/application_spec.rb +91 -0
- data/spec/unit/dispatcher_spec.rb +14 -0
- data/spec/unit/dispatchers/rack_spec.rb +34 -0
- data/spec/unit/exceptions_spec.rb +21 -0
- data/spec/unit/path_spec.rb +91 -0
- data/spec/unit/request_spec.rb +129 -0
- data/spec/unit/resource_spec.rb +245 -0
- data/spec/unit/response_spec.rb +65 -0
- data/spec/unit/route_spec.rb +160 -0
- data/spec/unit/router_spec.rb +103 -0
- metadata +189 -0
data/example/app.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
|
2
|
+
require 'restfulness'
|
3
|
+
|
4
|
+
$projects = []
|
5
|
+
|
6
|
+
Project = Class.new(HashWithIndifferentAccess)
|
7
|
+
|
8
|
+
class ProjectResource < Restfulness::Resource
|
9
|
+
def exists?
|
10
|
+
!project.nil?
|
11
|
+
end
|
12
|
+
def get
|
13
|
+
project
|
14
|
+
end
|
15
|
+
def post
|
16
|
+
$projects << Project.new(request.params)
|
17
|
+
end
|
18
|
+
def put
|
19
|
+
project.update(request.params)
|
20
|
+
end
|
21
|
+
def delete
|
22
|
+
$projects.delete(project)
|
23
|
+
end
|
24
|
+
protected
|
25
|
+
def project
|
26
|
+
$projects.find{|p| p[:id] == request.path[:id]}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class ProjectsResource < Restfulness::Resource
|
31
|
+
def get
|
32
|
+
$projects
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
class ExampleApp < Restfulness::Application
|
38
|
+
routes do
|
39
|
+
add 'project', ProjectResource
|
40
|
+
add 'projects', ProjectsResource
|
41
|
+
end
|
42
|
+
end
|
data/lib/restfulness.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
|
2
|
+
require 'uri'
|
3
|
+
require 'multi_json'
|
4
|
+
require 'mono_logger'
|
5
|
+
require 'active_support/core_ext'
|
6
|
+
require 'active_support/dependencies'
|
7
|
+
require 'rack/utils'
|
8
|
+
require 'rack/showexceptions'
|
9
|
+
require 'rack/builder'
|
10
|
+
|
11
|
+
require "restfulness/application"
|
12
|
+
require "restfulness/dispatcher"
|
13
|
+
require "restfulness/exceptions"
|
14
|
+
require "restfulness/path"
|
15
|
+
require "restfulness/request"
|
16
|
+
require "restfulness/resource"
|
17
|
+
require "restfulness/response"
|
18
|
+
require "restfulness/route"
|
19
|
+
require "restfulness/router"
|
20
|
+
require "restfulness/statuses"
|
21
|
+
require "restfulness/version"
|
22
|
+
|
23
|
+
require "restfulness/dispatchers/rack"
|
24
|
+
|
25
|
+
require "restfulness/log_formatters/quiet_formatter"
|
26
|
+
require "restfulness/log_formatters/verbose_formatter"
|
27
|
+
|
28
|
+
module Restfulness
|
29
|
+
extend self
|
30
|
+
|
31
|
+
attr_accessor :logger
|
32
|
+
end
|
33
|
+
|
34
|
+
Restfulness.logger = MonoLogger.new(STDOUT)
|
35
|
+
Restfulness.logger.formatter = Restfulness::VerboseFormatter.new
|
36
|
+
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Restfulness
|
2
|
+
|
3
|
+
#
|
4
|
+
# The Restulness::Application is the starting point. It'll deal with
|
5
|
+
# defining the initial configuration, and handle incoming requests
|
6
|
+
# from rack.
|
7
|
+
#
|
8
|
+
# Build your own Restfulness applications by inheriting from this class:
|
9
|
+
#
|
10
|
+
# class MyApp < Restfulness::Application
|
11
|
+
#
|
12
|
+
# routes do
|
13
|
+
# scope 'api' do
|
14
|
+
# add 'journey', JourneyResource
|
15
|
+
# add 'journeys', JourneyCollectionResource
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
class Application
|
22
|
+
|
23
|
+
def router
|
24
|
+
self.class.router
|
25
|
+
end
|
26
|
+
|
27
|
+
# Rack Handling.
|
28
|
+
# Forward rack call to dispatcher
|
29
|
+
def call(env)
|
30
|
+
@app ||= build_rack_app
|
31
|
+
@app.call(env)
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def build_rack_app
|
37
|
+
this = self
|
38
|
+
dispatcher = Dispatchers::Rack.new(self)
|
39
|
+
Rack::Builder.new do
|
40
|
+
this.class.middlewares.each do |middleware|
|
41
|
+
use middleware
|
42
|
+
end
|
43
|
+
run dispatcher
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class << self
|
48
|
+
|
49
|
+
attr_accessor :router, :middlewares
|
50
|
+
|
51
|
+
def routes(&block)
|
52
|
+
# Store the block so we can parse it at run time (autoload win!)
|
53
|
+
@router = Router.new(&block)
|
54
|
+
end
|
55
|
+
|
56
|
+
# A simple array of rack middlewares that will be applied
|
57
|
+
# before handling the request in Restfulness.
|
58
|
+
#
|
59
|
+
# Probably most useful for adding the ActiveDispatch::Reloader
|
60
|
+
# as used by Rails to reload on each request. e.g.
|
61
|
+
#
|
62
|
+
# middlewares << ActiveDispatch::Reloader
|
63
|
+
#
|
64
|
+
def middlewares
|
65
|
+
@middlewares ||= [
|
66
|
+
Rack::ShowExceptions
|
67
|
+
]
|
68
|
+
end
|
69
|
+
|
70
|
+
# Quick access to the Restfulness logger.
|
71
|
+
def logger
|
72
|
+
Restfulness.logger
|
73
|
+
end
|
74
|
+
|
75
|
+
# Override the default Restfulness logger.
|
76
|
+
def logger=(logger)
|
77
|
+
Restfulness.logger = logger
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Restfulness
|
2
|
+
|
3
|
+
module Dispatchers
|
4
|
+
|
5
|
+
class Rack < Dispatcher
|
6
|
+
|
7
|
+
def call(env)
|
8
|
+
rack_req = ::Rack::Request.new(env)
|
9
|
+
|
10
|
+
# Make sure we understand the request
|
11
|
+
request = Request.new(app)
|
12
|
+
prepare_request(env, rack_req, request)
|
13
|
+
|
14
|
+
|
15
|
+
# Prepare a suitable response
|
16
|
+
response = Response.new(request)
|
17
|
+
response.run
|
18
|
+
|
19
|
+
|
20
|
+
# No need to provide an empty response
|
21
|
+
log_response(response.code)
|
22
|
+
[response.code, response.headers, [response.payload || ""]]
|
23
|
+
|
24
|
+
rescue HTTPException => e
|
25
|
+
log_response(e.code)
|
26
|
+
[e.code, {}, [e.payload || ""]]
|
27
|
+
|
28
|
+
#rescue Exception => e
|
29
|
+
# log_response(500)
|
30
|
+
# puts
|
31
|
+
# puts e.message
|
32
|
+
# puts e.backtrace
|
33
|
+
# # Something unknown went wrong
|
34
|
+
# [500, {}, [STATUSES[500]]]
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
def prepare_request(env, rack_req, request)
|
40
|
+
request.uri = rack_req.url
|
41
|
+
request.action = parse_action(rack_req.request_method)
|
42
|
+
request.query = rack_req.GET
|
43
|
+
request.body = rack_req.body
|
44
|
+
request.remote_ip = rack_req.ip
|
45
|
+
request.headers = prepare_headers(env)
|
46
|
+
|
47
|
+
# Sometimes rack removes content type from headers
|
48
|
+
request.headers[:content_type] ||= rack_req.content_type
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_action(action)
|
52
|
+
case action
|
53
|
+
when 'DELETE'
|
54
|
+
:delete
|
55
|
+
when 'GET'
|
56
|
+
:get
|
57
|
+
when 'HEAD'
|
58
|
+
:head
|
59
|
+
when 'POST'
|
60
|
+
:post
|
61
|
+
when 'PUT'
|
62
|
+
:put
|
63
|
+
when 'OPTIONS'
|
64
|
+
:options
|
65
|
+
else
|
66
|
+
raise HTTPException.new(501)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def log_response(code)
|
71
|
+
logger.info("Completed #{code} #{STATUSES[code]}")
|
72
|
+
end
|
73
|
+
|
74
|
+
def prepare_headers(env)
|
75
|
+
res = {}
|
76
|
+
env.each do |k,v|
|
77
|
+
next unless k =~ /^HTTP_/
|
78
|
+
res[k.sub(/^HTTP_/, '').downcase.gsub(/-/, '_').to_sym] = v
|
79
|
+
end
|
80
|
+
res
|
81
|
+
end
|
82
|
+
|
83
|
+
def logger
|
84
|
+
Restfulness.logger
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
|
2
|
+
module Restfulness
|
3
|
+
|
4
|
+
class HTTPException < ::StandardError
|
5
|
+
|
6
|
+
attr_accessor :code, :payload, :headers
|
7
|
+
|
8
|
+
def initialize(code, payload = nil, opts = {})
|
9
|
+
@code = code
|
10
|
+
@payload = payload
|
11
|
+
@headers = opts[:headers]
|
12
|
+
super(opts[:message] || STATUSES[code])
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Restfulness
|
2
|
+
class VerboseFormatter
|
3
|
+
def call(serverity, datetime, progname, msg)
|
4
|
+
time = Time.now.strftime('%Y-%m-%d %H:%M:%S.%L')
|
5
|
+
sym = case serverity
|
6
|
+
when 'ERROR'
|
7
|
+
'EE'
|
8
|
+
when 'INFO'
|
9
|
+
'--'
|
10
|
+
else
|
11
|
+
'**'
|
12
|
+
end
|
13
|
+
"#{sym} #{time}: #{msg}\n"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
|
2
|
+
module Restfulness
|
3
|
+
|
4
|
+
# The Path object is provided in request objects to provide easy access
|
5
|
+
# to parameters included in the URI's path.
|
6
|
+
class Path
|
7
|
+
|
8
|
+
attr_accessor :route, :components, :params
|
9
|
+
|
10
|
+
def initialize(route, string)
|
11
|
+
self.route = route
|
12
|
+
self.params = {}
|
13
|
+
parse(string)
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
'/' + components.join('/')
|
18
|
+
end
|
19
|
+
|
20
|
+
def [](index)
|
21
|
+
if index.is_a?(Integer)
|
22
|
+
components[index]
|
23
|
+
else
|
24
|
+
params[index]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def parse(string)
|
31
|
+
self.components = string.gsub(/^\/|\/$/, '').split(/\//)
|
32
|
+
|
33
|
+
# Make sure we have the id available when parsing
|
34
|
+
path = route.path + [:id]
|
35
|
+
|
36
|
+
# Parametize values that need it
|
37
|
+
path.each_with_index do |value, i|
|
38
|
+
if value.is_a?(Symbol)
|
39
|
+
params[value] = components[i]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Restfulness
|
2
|
+
|
3
|
+
# Simple, indpendent, request interface for dealing with the incoming information
|
4
|
+
# in a request.
|
5
|
+
#
|
6
|
+
# Currently wraps around the information provided in a Rack Request object.
|
7
|
+
class Request
|
8
|
+
|
9
|
+
# Who does this request belong to?
|
10
|
+
attr_reader :app
|
11
|
+
|
12
|
+
# The HTTP action being handled
|
13
|
+
attr_accessor :action
|
14
|
+
|
15
|
+
# Hash of HTTP headers. Keys always normalized to lower-case symbols with underscore.
|
16
|
+
attr_accessor :headers
|
17
|
+
|
18
|
+
# Ruby URI object
|
19
|
+
attr_reader :uri
|
20
|
+
|
21
|
+
# Path object of the current URL being accessed
|
22
|
+
attr_accessor :path
|
23
|
+
|
24
|
+
# The route, determined from the path, if available!
|
25
|
+
attr_accessor :route
|
26
|
+
|
27
|
+
# Query parameters included in the URL
|
28
|
+
attr_accessor :query
|
29
|
+
|
30
|
+
# Raw HTTP body, for POST and PUT requests.
|
31
|
+
attr_accessor :body
|
32
|
+
|
33
|
+
# IP address of requester
|
34
|
+
attr_accessor :remote_ip
|
35
|
+
|
36
|
+
def initialize(app)
|
37
|
+
@app = app
|
38
|
+
|
39
|
+
# Prepare basics
|
40
|
+
self.action = nil
|
41
|
+
self.headers = {}
|
42
|
+
self.body = nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def uri=(uri)
|
46
|
+
@uri = URI(uri)
|
47
|
+
end
|
48
|
+
|
49
|
+
def path
|
50
|
+
@path ||= (route ? route.build_path(uri.path) : nil)
|
51
|
+
end
|
52
|
+
|
53
|
+
def route
|
54
|
+
# Determine the route from the uri
|
55
|
+
@route ||= app.router.route_for(uri.path)
|
56
|
+
end
|
57
|
+
|
58
|
+
def query
|
59
|
+
@query ||= HashWithIndifferentAccess.new(
|
60
|
+
::Rack::Utils.parse_nested_query(uri.query)
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
def params
|
65
|
+
return @params if @params || body.nil?
|
66
|
+
case headers[:content_type]
|
67
|
+
when 'application/json'
|
68
|
+
@params = MultiJson.decode(body)
|
69
|
+
else
|
70
|
+
raise HTTPException.new(406)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
[:get, :post, :put, :delete, :head, :options].each do |m|
|
75
|
+
define_method("#{m}?") do
|
76
|
+
action == m
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|