restfulness 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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/example/config.ru ADDED
@@ -0,0 +1,5 @@
1
+
2
+ require './app'
3
+
4
+ run ExampleApp.new
5
+
@@ -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,14 @@
1
+
2
+ module Restfulness
3
+
4
+ class Dispatcher
5
+
6
+ attr_accessor :app
7
+
8
+ def initialize(app)
9
+ self.app = app
10
+ end
11
+
12
+ end
13
+
14
+ 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,7 @@
1
+ module Restfulness
2
+ class QuietFormatter
3
+ def call(serverity, datetime, progname, msg)
4
+ ""
5
+ end
6
+ end
7
+ 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