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.
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