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