restfulness 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,111 @@
1
+ module Restfulness
2
+
3
+ class Resource
4
+
5
+ attr_reader :request, :response
6
+
7
+ def initialize(request, response)
8
+ @request = request
9
+ @response = response
10
+ end
11
+
12
+ # Options is the only HTTP method support by default
13
+ def options
14
+ response.headers['Allow'] = self.class.supported_methods.map{ |m|
15
+ m.to_s.upcase
16
+ }.join(', ')
17
+ nil
18
+ end
19
+
20
+ def call
21
+ send(request.action)
22
+ end
23
+
24
+ # Callbacks
25
+
26
+ def method_allowed?
27
+ self.class.supported_methods.include?(request.action)
28
+ end
29
+
30
+ def exists?
31
+ true
32
+ end
33
+
34
+ def authorized?
35
+ true
36
+ end
37
+
38
+ def allowed?
39
+ true
40
+ end
41
+
42
+ def last_modified
43
+ nil
44
+ end
45
+
46
+ def etag
47
+ nil
48
+ end
49
+
50
+ def check_callbacks
51
+ # Access control
52
+ raise HTTPException.new(405) unless method_allowed?
53
+ raise HTTPException.new(401) unless authorized?
54
+ raise HTTPException.new(403) unless allowed?
55
+
56
+ # The following callbacks only make sense for certain methods
57
+ if [:head, :get, :put, :delete].include?(request.action)
58
+
59
+ raise HTTPException.new(404) unless exists?
60
+
61
+ if [:get, :head].include?(request.action)
62
+ # Resource status
63
+ check_etag if etag
64
+ check_if_modified if last_modified
65
+ end
66
+ end
67
+ end
68
+
69
+ ##
70
+
71
+
72
+ protected
73
+
74
+ def error(code, payload = nil, opts = {})
75
+ raise HTTPException.new(code, payload, opts)
76
+ end
77
+
78
+ def logger
79
+ Restfulness.logger
80
+ end
81
+
82
+
83
+ private
84
+
85
+ def check_if_modified
86
+ date = request.headers[:if_modified_since]
87
+ if date && date == last_modified.to_s
88
+ raise HTTPException.new(304)
89
+ end
90
+ response.headers['Last-Modified'] = last_modified
91
+ end
92
+
93
+ def check_etag
94
+ tag = request.headers[:if_none_match]
95
+ if tag && tag == etag.to_s
96
+ raise HTTPException.new(304)
97
+ end
98
+ response.headers['ETag'] = etag
99
+ end
100
+
101
+ class << self
102
+
103
+ def supported_methods
104
+ @_actions ||= (instance_methods & [:get, :put, :post, :delete, :head, :patch, :options])
105
+ end
106
+
107
+ end
108
+
109
+ end
110
+
111
+ end
@@ -0,0 +1,57 @@
1
+
2
+ module Restfulness
3
+
4
+ class Response
5
+
6
+ # Incoming data
7
+ attr_reader :request
8
+
9
+ # Outgoing data
10
+ attr_reader :code, :headers, :payload
11
+
12
+
13
+ def initialize(request)
14
+ @request = request
15
+
16
+ # Default headers
17
+ @headers = {'Content-Type' => 'application/json; charset=utf-8'}
18
+ end
19
+
20
+ def run
21
+ logger.info("Responding to #{request.action.to_s.upcase} #{request.uri.to_s} from #{request.remote_ip}")
22
+
23
+ route = request.route
24
+ if route
25
+ logger.info("Using resource: #{route.resource_name}")
26
+ resource = route.build_resource(request, self)
27
+
28
+ # run callbacks, if any fail, they'll raise an error
29
+ resource.check_callbacks
30
+
31
+ # Perform the actual work
32
+ result = resource.call
33
+
34
+ @code ||= (result ? 200 : 204)
35
+ @payload = MultiJson.encode(result)
36
+ else
37
+ logger.error("No route found")
38
+ # This is not something we can deal with, pass it on
39
+ @code = 404
40
+ @payload = ""
41
+ end
42
+ update_content_length
43
+ end
44
+
45
+ def logger
46
+ Restfulness.logger
47
+ end
48
+
49
+ protected
50
+
51
+ def update_content_length
52
+ @headers['Content-Length'] = @payload.bytesize.to_s
53
+ end
54
+
55
+ end
56
+
57
+ end
@@ -0,0 +1,48 @@
1
+ module Restfulness
2
+
3
+ class Route
4
+
5
+ # The path array of eliments, :id always on end!
6
+ attr_accessor :path
7
+
8
+ # Reference to the class that will handle requests for this route
9
+ attr_accessor :resource_name
10
+
11
+ def initialize(*args)
12
+ self.resource_name = args.pop.to_s
13
+ self.path = args.reject{|arg| arg == :id}
14
+
15
+ if resource_name.empty? || resource.nil? # Try to load the resource
16
+ raise ArgumentError, "Please provide a resource!"
17
+ end
18
+ end
19
+
20
+ def build_path(path)
21
+ Path.new(self, path)
22
+ end
23
+
24
+ def handles?(parts)
25
+ # Make sure same length (accounting for id)
26
+ diff = parts.length - path.length
27
+ return false if diff != 0 && diff != 1
28
+
29
+ # Compare the pairs
30
+ path.each_with_index do |slug, i|
31
+ if slug.is_a?(String) or slug.is_a?(Numeric)
32
+ return false if parts[i] != slug.to_s
33
+ end
34
+ end
35
+ true
36
+ end
37
+
38
+ def resource
39
+ resource_name.constantize
40
+ end
41
+
42
+ def build_resource(request, response)
43
+ resource.new(request, response)
44
+ end
45
+
46
+ end
47
+
48
+ end
@@ -0,0 +1,27 @@
1
+
2
+ module Restfulness
3
+
4
+ class Router
5
+
6
+ attr_accessor :routes
7
+
8
+ def initialize(&block)
9
+ self.routes = []
10
+ instance_eval(&block) if block_given?
11
+ end
12
+
13
+ def add(*args)
14
+ routes << Route.new(*args)
15
+ end
16
+
17
+ def route_for(path)
18
+ parts = path.gsub(/^\/|\/$/, '').split(/\//)
19
+ routes.each do |route|
20
+ return route if route.handles?(parts)
21
+ end
22
+ nil
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1,71 @@
1
+ module Restfulness
2
+
3
+ #
4
+ # List of standard HTTP statuses blatently stolen from Ruby RestClient.
5
+ #
6
+ # https://github.com/rest-client/rest-client
7
+ #
8
+
9
+ STATUSES = {
10
+ 100 => 'Continue',
11
+ 101 => 'Switching Protocols',
12
+ 102 => 'Processing', #WebDAV
13
+
14
+ 200 => 'OK',
15
+ 201 => 'Created',
16
+ 202 => 'Accepted',
17
+ 203 => 'Non-Authoritative Information', # http/1.1
18
+ 204 => 'No Content',
19
+ 205 => 'Reset Content',
20
+ 206 => 'Partial Content',
21
+ 207 => 'Multi-Status', #WebDAV
22
+
23
+ 300 => 'Multiple Choices',
24
+ 301 => 'Moved Permanently',
25
+ 302 => 'Found',
26
+ 303 => 'See Other', # http/1.1
27
+ 304 => 'Not Modified',
28
+ 305 => 'Use Proxy', # http/1.1
29
+ 306 => 'Switch Proxy', # no longer used
30
+ 307 => 'Temporary Redirect', # http/1.1
31
+
32
+ 400 => 'Bad Request',
33
+ 401 => 'Unauthorized',
34
+ 402 => 'Payment Required',
35
+ 403 => 'Forbidden',
36
+ 404 => 'Resource Not Found',
37
+ 405 => 'Method Not Allowed',
38
+ 406 => 'Not Acceptable',
39
+ 407 => 'Proxy Authentication Required',
40
+ 408 => 'Request Timeout',
41
+ 409 => 'Conflict',
42
+ 410 => 'Gone',
43
+ 411 => 'Length Required',
44
+ 412 => 'Precondition Failed',
45
+ 413 => 'Request Entity Too Large',
46
+ 414 => 'Request-URI Too Long',
47
+ 415 => 'Unsupported Media Type',
48
+ 416 => 'Requested Range Not Satisfiable',
49
+ 417 => 'Expectation Failed',
50
+ 418 => 'I\'m A Teapot',
51
+ 421 => 'Too Many Connections From This IP',
52
+ 422 => 'Unprocessable Entity', #WebDAV
53
+ 423 => 'Locked', #WebDAV
54
+ 424 => 'Failed Dependency', #WebDAV
55
+ 425 => 'Unordered Collection', #WebDAV
56
+ 426 => 'Upgrade Required',
57
+ 449 => 'Retry With', #Microsoft
58
+ 450 => 'Blocked By Windows Parental Controls', #Microsoft
59
+
60
+ 500 => 'Internal Server Error',
61
+ 501 => 'Not Implemented',
62
+ 502 => 'Bad Gateway',
63
+ 503 => 'Service Unavailable',
64
+ 504 => 'Gateway Timeout',
65
+ 505 => 'HTTP Version Not Supported',
66
+ 506 => 'Variant Also Negotiates',
67
+ 507 => 'Insufficient Storage', #WebDAV
68
+ 509 => 'Bandwidth Limit Exceeded', #Apache
69
+ 510 => 'Not Extended'
70
+ }
71
+ end
@@ -0,0 +1,3 @@
1
+ module Restfulness
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'restfulness/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "restfulness"
8
+ spec.version = Restfulness::VERSION
9
+ spec.authors = ["Sam Lown"]
10
+ spec.email = ["me@samlown.com"]
11
+ spec.description = %q{Simple REST server that focuses on resources instead of routes.}
12
+ spec.summary = %q{Use to create a powerful, yet simple REST API in your application.}
13
+ spec.homepage = "https://github.com/samlown/restfulness"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "rack", "~> 1.4"
22
+ spec.add_dependency "multi_json", "~> 1.8"
23
+ spec.add_dependency "activesupport", ">= 3.1"
24
+ spec.add_dependency "mono_logger", "~> 1.0"
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.3"
27
+ spec.add_development_dependency "rake"
28
+ spec.add_development_dependency "rspec"
29
+ end
@@ -0,0 +1,13 @@
1
+
2
+ require 'rubygems'
3
+ require 'bundler/setup'
4
+
5
+ require 'restfulness' # and any other gems you need
6
+
7
+ RSpec.configure do |config|
8
+ config.color_enabled = true
9
+ end
10
+
11
+ # Disable any logger output
12
+ Restfulness.logger.formatter = proc {|severity, datetime, progname, msg| ""}
13
+
@@ -0,0 +1,91 @@
1
+
2
+ require 'spec_helper'
3
+
4
+ describe Restfulness::Application do
5
+
6
+ let :klass do
7
+ Class.new(Restfulness::Application) do
8
+ routes do
9
+ # nothing
10
+ end
11
+ end
12
+ end
13
+
14
+ describe "#router" do
15
+ it "should access class's router" do
16
+ obj = klass.new
17
+ obj.router.should eql(klass.router)
18
+ end
19
+ end
20
+
21
+ describe "#call" do
22
+ it "should build rack app and call with env" do
23
+ env = {}
24
+ obj = klass.new
25
+ app = double(:app)
26
+ app.should_receive(:call).with(env)
27
+ obj.should_receive(:build_rack_app).and_return(app)
28
+ obj.call(env)
29
+ end
30
+ end
31
+
32
+ describe "#build_rack_app (protected)" do
33
+ it "should build a new rack app with middlewares" do
34
+ obj = klass.new
35
+ app = obj.send(:build_rack_app)
36
+ app.should be_a(Rack::Builder)
37
+ # Note, this might brake if Rack changes!
38
+ app.instance_variable_get(:@use).first.call.should be_a(klass.middlewares.first)
39
+ app.instance_variable_get(:@run).should be_a(Restfulness::Dispatchers::Rack)
40
+ end
41
+ end
42
+
43
+ describe ".routes" do
44
+
45
+ context "basic usage" do
46
+ it "should build a new router with block" do
47
+ klass.router.should_not be_nil
48
+ klass.router.should be_a(Restfulness::Router)
49
+ end
50
+
51
+ it "should be accessable from instance" do
52
+ obj = klass.new
53
+ obj.router.should eql(klass.router)
54
+ end
55
+
56
+ it "should pass block to Router instance" do
57
+ block = lambda { }
58
+ Restfulness::Router.should_receive(:new).with(&block)
59
+ Class.new(Restfulness::Application) do
60
+ routes &block
61
+ end
62
+ end
63
+ end
64
+
65
+ end
66
+
67
+ describe ".middlewares" do
68
+ it "should provide simple array of middlewares" do
69
+ klass.middlewares.should be_a(Array)
70
+ klass.middlewares.should include(Rack::ShowExceptions)
71
+ end
72
+ end
73
+
74
+ describe ".logger" do
75
+ it "should return main logger" do
76
+ klass.logger.should eql(Restfulness.logger)
77
+ end
78
+ end
79
+
80
+ describe ".logger=" do
81
+ it "should set main logger" do
82
+ orig = Restfulness.logger
83
+ logger = double(:Logger)
84
+ klass.logger = logger
85
+ Restfulness.logger.should eql(logger)
86
+ Restfulness.logger = orig
87
+ end
88
+ end
89
+
90
+
91
+ end