restfulness 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +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
|