restfulness 0.3.2 → 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +6 -5
- data/README.md +98 -80
- data/lib/restfulness.rb +3 -0
- data/lib/restfulness/dispatchers/rack.rb +1 -0
- data/lib/restfulness/headers/accept.rb +66 -0
- data/lib/restfulness/headers/media_type.rb +127 -0
- data/lib/restfulness/request.rb +43 -15
- data/lib/restfulness/version.rb +1 -1
- data/restfulness.gemspec +1 -1
- data/spec/spec_helper.rb +1 -1
- data/spec/unit/application_spec.rb +14 -14
- data/spec/unit/dispatcher_spec.rb +1 -1
- data/spec/unit/dispatchers/rack_spec.rb +21 -20
- data/spec/unit/exceptions_spec.rb +5 -5
- data/spec/unit/headers/accept_spec.rb +70 -0
- data/spec/unit/headers/media_type_spec.rb +262 -0
- data/spec/unit/http_authentication/basic_spec.rb +7 -7
- data/spec/unit/path_spec.rb +19 -19
- data/spec/unit/request_spec.rb +96 -44
- data/spec/unit/requests/authorization_header_spec.rb +8 -8
- data/spec/unit/requests/authorization_spec.rb +3 -3
- data/spec/unit/resource_spec.rb +28 -28
- data/spec/unit/resources/authentication_spec.rb +2 -2
- data/spec/unit/resources/events_spec.rb +1 -1
- data/spec/unit/response_spec.rb +53 -53
- data/spec/unit/route_spec.rb +24 -24
- data/spec/unit/router_spec.rb +29 -29
- data/spec/unit/sanitizer_spec.rb +9 -9
- metadata +13 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ce9b42d05b92e2107f89ce16f36a5f1bbfabe6e7
|
4
|
+
data.tar.gz: b9cb7bdc241661ab063b45f20a81a967f43eeee6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7809aad851643407a59b3d376a75d39c4a1f703cf18cc647817e018971434e547dd8fbfcbd6b01e6dafb3aa7e510eee7233ca5a75ecf839239b52596e5bac5ca
|
7
|
+
data.tar.gz: f8fbe0c48f0a05bd5f50f48ccb56545afed3408ed5d4941ffd7db5303dbb107203b6656de0c7f13cf937bb88ee716dd2b7d8a7499681abdfbdc2492dc3f14206
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -6,93 +6,97 @@ Because REST APIs are all about resources, not routes.
|
|
6
6
|
|
7
7
|
## Introduction
|
8
8
|
|
9
|
-
Restfulness is
|
9
|
+
Restfulness is a simple Ruby library for creating REST APIs. Each endpoint defined in the routing configuration refers to a resource class containing HTTP actions and callbacks. When an HTTP request is received, the callbacks are checked, and the appropriate action is called to provide a response.
|
10
10
|
|
11
|
-
|
11
|
+
When creating Restfulness, we had a set of objectives we wanted to achieve:
|
12
12
|
|
13
|
-
|
13
|
+
* A true "resource" orientated interface.
|
14
|
+
* Simple routing.
|
15
|
+
* Fast.
|
16
|
+
* JSON only responses.
|
17
|
+
* Take advantage of HTTP flow control using callbacks.
|
18
|
+
* Simple error handling, and "instant abort" exceptions.
|
14
19
|
|
15
|
-
|
20
|
+
Here's a code example of what the restfulness side of a rack application might look like:
|
16
21
|
|
17
22
|
```ruby
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
desc "Return a public timeline."
|
27
|
-
get :public_timeline do
|
28
|
-
Status.limit(20)
|
29
|
-
end
|
30
|
-
|
31
|
-
desc "Return a personal timeline."
|
32
|
-
get :home_timeline do
|
33
|
-
authenticate!
|
34
|
-
current_user.statuses.limit(20)
|
35
|
-
end
|
36
|
-
|
37
|
-
desc "Return a status."
|
38
|
-
params do
|
39
|
-
requires :id, type: Integer, desc: "Status id."
|
40
|
-
end
|
41
|
-
route_param :id do
|
42
|
-
get do
|
43
|
-
Status.find(params[:id])
|
44
|
-
end
|
23
|
+
# The API definition, this matches incoming request paths to resources
|
24
|
+
class TwitterAPI < Restfullness::Application
|
25
|
+
routes do
|
26
|
+
scope 'api' do
|
27
|
+
add 'task', Tasks::ItemResource
|
28
|
+
scope 'tasks' do
|
29
|
+
add 'public', Tasks::PublicResource
|
30
|
+
add 'private', Tasks::PrivateResource
|
45
31
|
end
|
46
|
-
|
47
32
|
end
|
48
|
-
|
49
33
|
end
|
50
34
|
end
|
51
35
|
|
52
|
-
|
36
|
+
# Modules are always a good idea to group resources
|
37
|
+
module Tasks
|
38
|
+
# A simple resource for returning tasks
|
39
|
+
class ItemResource < Restfulness::Resource
|
40
|
+
# Callback to see if task exsists
|
41
|
+
def exists?
|
42
|
+
!!task
|
43
|
+
end
|
53
44
|
|
54
|
-
|
45
|
+
# Provide the task, if the #exists? call worked
|
46
|
+
def get
|
47
|
+
task
|
48
|
+
end
|
55
49
|
|
56
|
-
|
50
|
+
# Create a new task, this will bypass the #exits? call
|
51
|
+
def post
|
52
|
+
Task.create(request.params)
|
53
|
+
end
|
57
54
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
add 'status', StatusResource
|
62
|
-
scope 'timeline' do
|
63
|
-
add 'public', Timelines::PublicResource
|
64
|
-
add 'home', Timelines::HomeResource
|
55
|
+
# Update the task, and raise an error with error response if failed
|
56
|
+
def patch
|
57
|
+
task.update_attributes(request.params) || forbidden!(task.errors)
|
65
58
|
end
|
66
|
-
end
|
67
|
-
end
|
68
59
|
|
69
|
-
|
70
|
-
|
71
|
-
|
60
|
+
protected
|
61
|
+
|
62
|
+
def task
|
63
|
+
@task ||= Task.find(request.path[:id])
|
64
|
+
end
|
72
65
|
end
|
73
|
-
end
|
74
66
|
|
75
|
-
|
67
|
+
# Simple resource that provides list of public tasks, that may be empty
|
76
68
|
class PublicResource < Restfulness::Resource
|
77
69
|
def get
|
78
|
-
|
70
|
+
Task.public.limit(20)
|
79
71
|
end
|
80
72
|
end
|
81
73
|
|
82
|
-
#
|
83
|
-
class
|
74
|
+
# Authorization requires additional code to authenticate the user
|
75
|
+
class PrivateResource < Restfulness::Resource
|
76
|
+
# If this fails, abort and return 401 Unauthorized response
|
84
77
|
def authorized?
|
85
|
-
|
78
|
+
!current_user
|
86
79
|
end
|
80
|
+
|
81
|
+
# Assuming authorized, attept to load tasks
|
87
82
|
def get
|
88
|
-
current_user.
|
83
|
+
current_user.tasks.limit(20)
|
84
|
+
end
|
85
|
+
|
86
|
+
protected
|
87
|
+
|
88
|
+
# Very simple example of authentication
|
89
|
+
def current_user
|
90
|
+
@current_user ||= authenticate_with_http_basic do |username, password|
|
91
|
+
User.authenticate(username, password)
|
92
|
+
end
|
89
93
|
end
|
90
94
|
end
|
91
95
|
end
|
92
96
|
|
93
97
|
```
|
94
98
|
|
95
|
-
|
99
|
+
Checkout the rest of this document for more of the details on the api, integration with your existing apps, and additional features.
|
96
100
|
|
97
101
|
|
98
102
|
## Installation
|
@@ -159,7 +163,7 @@ The aim of routes in Restfulness are to be stupid simple. These are the basic ru
|
|
159
163
|
* Order is important.
|
160
164
|
* Strings are matched directly.
|
161
165
|
* Symbols match anything, and are accessible as path attributes.
|
162
|
-
* Every route
|
166
|
+
* Every route automatically gets an :id parameter at the end, that may or may not have a null value.
|
163
167
|
* Scopes save repeating shared route array entries.
|
164
168
|
|
165
169
|
Lets see a few examples:
|
@@ -204,7 +208,7 @@ end
|
|
204
208
|
|
205
209
|
### Resources
|
206
210
|
|
207
|
-
Resources are like
|
211
|
+
Resources are like controllers in a Rails project. They handle the basic HTTP actions using methods that match the same name as the action. The result of an action is serialized into a JSON object automatically. The actions supported by a resource are:
|
208
212
|
|
209
213
|
* `get`
|
210
214
|
* `head`
|
@@ -212,7 +216,7 @@ Resources are like Controllers in a Rails project. They handle the basic HTTP ac
|
|
212
216
|
* `patch`
|
213
217
|
* `put`
|
214
218
|
* `delete`
|
215
|
-
* `options` - this is the only action
|
219
|
+
* `options` - this is the only action provided by default
|
216
220
|
|
217
221
|
When creating your resource, simply define the methods you'd like to use and ensure each has a result:
|
218
222
|
|
@@ -286,11 +290,11 @@ end
|
|
286
290
|
|
287
291
|
#### I18n in Resources
|
288
292
|
|
289
|
-
Restfulness uses the [http_accept_language](https://github.com/iain/http_accept_language) gem to automatically handle the `Accept-Language` header
|
293
|
+
Restfulness uses the [http_accept_language](https://github.com/iain/http_accept_language) gem to automatically handle the `Accept-Language` header received from a client. After trying to make a match between the available locales, it will automatically set the `I18n.locale`. You can access the http_accept_language parser via the `request.http_accept_language` method.
|
290
294
|
|
291
295
|
For most APIs this should work great, especially for mobile applications where this header is automatically set by the phone. There may however be situations where you need a bit more control. If a user has a preferred language setting for example.
|
292
296
|
|
293
|
-
Resources contain two protected methods that can be overwritten
|
297
|
+
Resources contain two protected methods that can be overwritten. This is what they look like in the Restfulness code:
|
294
298
|
|
295
299
|
```ruby
|
296
300
|
protected
|
@@ -302,16 +306,15 @@ end
|
|
302
306
|
def set_locale
|
303
307
|
I18n.locale = locale
|
304
308
|
end
|
305
|
-
```
|
309
|
+
```
|
306
310
|
|
307
311
|
The `Resource#set_locale` method is called before any of the other callbacks are handled. This is important as it allows the locale to be set before returning any translatable error messages.
|
308
312
|
|
309
|
-
Most users will probably just want to override the `Resource#locale` method and provide the appropriate locale for the request. If you are using a User object or similar, double check your authentication process as the default `authorized?` method will be called *after* the locale is prepared.
|
310
|
-
|
313
|
+
Most users will probably just want to override the `Resource#locale` method and provide the appropriate locale for the request. If you are using a User object or similar, double check your authentication process as the default `authorized?` method will be called *after* the locale is prepared so that error responses are always in the requested language.
|
311
314
|
|
312
315
|
#### Authentication in Resources
|
313
316
|
|
314
|
-
Restfulness
|
317
|
+
Restfulness provides basic support for [HTTP Basic Authentication](http://en.wikipedia.org/wiki/Basic_access_authentication). To use, simply call the `authenticate_with_http_basic` method in your resource definition.
|
315
318
|
|
316
319
|
Here's an example with the authentication details in the code, you'd obviously want to use something a bit more advanced than this in production:
|
317
320
|
|
@@ -332,9 +335,9 @@ def authorized?
|
|
332
335
|
end
|
333
336
|
```
|
334
337
|
|
335
|
-
|
338
|
+
Digest authentication is not currently supported, but your contributions would be more than welcome. Checkout the [HttpAuthentication/basic.rb](blob/master/lib/restfulness/http_authentication/basic.rb) source for an example.
|
336
339
|
|
337
|
-
Restfulness doesn't make any provisions for requesting authentication from the client as most APIs don't
|
340
|
+
Restfulness doesn't make any provisions for requesting authentication from the client as in our experience most APIs don't need to offer this functionality. You can achieve the same effect however by providing the `WWW-Authenticate` header in the response. For example:
|
338
341
|
|
339
342
|
```ruby
|
340
343
|
def authorized?
|
@@ -353,12 +356,11 @@ def request_authentication
|
|
353
356
|
end
|
354
357
|
```
|
355
358
|
|
356
|
-
|
357
359
|
### Requests
|
358
360
|
|
359
|
-
All resource instances have access to a `Request` object via the
|
361
|
+
All resource instances have access to a `Request` object via the `Resource#request` method, much like you'd find in a Rails project. It provides access to the details including in the HTTP request: headers, the request URL, path entries, the query, body and/or parameters.
|
360
362
|
|
361
|
-
Restfulness takes a slightly different approach to handling paths, queries, and parameters. Rails and Sinatra
|
363
|
+
Restfulness takes a slightly different approach to handling paths, queries, and parameters, as each has their own independent method. Rails and Sinatra will typically mash everything together into a `params` hash. While this is convenient for use cases involving a browser, it is less useful for APIs when body parameters should only contain attributes of the model managed by the resource. If you've ever used Models from Backbone.js or similar Javascript library you appreciate this. When saving a Model, Backbone.js assumes by default that attributes will be provided without a prefix in the POST body.
|
362
364
|
|
363
365
|
The following key methods are provided in a request object:
|
364
366
|
|
@@ -387,7 +389,13 @@ request.query[:page] # 1
|
|
387
389
|
request.body # "{'key':'value'}" - string payload
|
388
390
|
|
389
391
|
# Request params
|
390
|
-
request.params # {'key' => 'value'} - usually a JSON
|
392
|
+
request.params # {'key' => 'value'} - usually a JSON de-serialized object
|
393
|
+
|
394
|
+
# Accept header object (nil if none!)
|
395
|
+
request.accept.version # For "Accept: application/vnd.example.api+json;version=3", returns "3"
|
396
|
+
|
397
|
+
# Content Type object (nil if none!)
|
398
|
+
request.content_type.to_s # Something like "application/json"
|
391
399
|
```
|
392
400
|
|
393
401
|
### Logging
|
@@ -408,7 +416,7 @@ Restfulness.sensitive_params = [:password, :secretkey]
|
|
408
416
|
|
409
417
|
## Error Handling
|
410
418
|
|
411
|
-
If you
|
419
|
+
If you'd like your application to return anything other than a 200 (or 202) status, you have a couple of options that allow you to send codes back to the client.
|
412
420
|
|
413
421
|
One of the easiest approaches is to update the `response` code. Take the following example where we set a 403 response and the model's errors object in the payload:
|
414
422
|
|
@@ -470,7 +478,7 @@ end
|
|
470
478
|
|
471
479
|
This can be a really nice way to mold your errors into a standard format. All HTTP exceptions generated inside resources will pass through `error!`, even those that a triggered by a callback. It gives a great way to provide your own JSON error payload, or even just resort to a simple string.
|
472
480
|
|
473
|
-
The currently built in
|
481
|
+
The currently built in exception methods are:
|
474
482
|
|
475
483
|
* `not_modified!`
|
476
484
|
* `bad_request!`
|
@@ -499,19 +507,19 @@ We're all used to the way Rails projects magically reload files so you don't hav
|
|
499
507
|
|
500
508
|
Using Restfulness in Rails is the easiest way to take advantage support reloading.
|
501
509
|
|
502
|
-
The
|
510
|
+
The recommended approach is to create two directories in your Rails projects `/app` path:
|
503
511
|
|
504
512
|
* `/app/apis` can be used for defining your API route files, and
|
505
513
|
* `/app/resources` for defining a tree of resource definition files.
|
506
514
|
|
507
|
-
Add the two paths to your rails
|
515
|
+
Add the two paths to your rails auto-loading configuration in `/config/application.rb`, there will already be a sample in your config provided by Rails:
|
508
516
|
|
509
517
|
```ruby
|
510
518
|
# Custom directories with classes and modules you want to be autoloadable.
|
511
519
|
config.autoload_paths += %W( #{config.root}/app/resources #{config.root}/app/apis )
|
512
520
|
```
|
513
521
|
|
514
|
-
Your Resource and API files will now be
|
522
|
+
Your Resource and API files will now be auto-loadable from your Rails project. The next step is to update the Rails router to be able to find our API. Modify the `/config/routes.rb` file so that it includes the mount method call:
|
515
523
|
|
516
524
|
```ruby
|
517
525
|
YourRailsApp::Application.routes.draw do
|
@@ -526,7 +534,7 @@ end
|
|
526
534
|
|
527
535
|
```
|
528
536
|
|
529
|
-
You'll see in the code sample that we're only loading the Restfulness API during development. Our recommendation is to use Restfulness as close to Rack as possible and avoid any of the Rails overhead. To support
|
537
|
+
You'll see in the code sample that we're only loading the Restfulness API during development. Our recommendation is to use Restfulness as close to Rack as possible and avoid any of the Rails overhead. To support requests in production, you'll need to update your `/config.rb` so that it looks something like the following:
|
530
538
|
|
531
539
|
```ruby
|
532
540
|
# This file is used by Rack-based servers to start the application.
|
@@ -546,8 +554,7 @@ Thats all there is to it! You'll now have auto-reloading in Rails, and fast requ
|
|
546
554
|
|
547
555
|
### The Rack Way
|
548
556
|
|
549
|
-
If you're using Restfulness as a standalone project, we recommend using a rack extension like [Shotgun](https://github.com/rtomayko/shotgun).
|
550
|
-
|
557
|
+
If you're using Restfulness as a standalone project, we recommend using a rack extension like [Shotgun](https://github.com/rtomayko/shotgun) to automatically reload on changes.
|
551
558
|
|
552
559
|
## Writing Tests
|
553
560
|
|
@@ -649,6 +656,7 @@ Restfulness was created by Sam Lown <me@samlown.com> as a solution for building
|
|
649
656
|
The project is now awesome, thanks to contributions by:
|
650
657
|
|
651
658
|
* [Adam Williams](https://github.com/awilliams)
|
659
|
+
* [Laura Morillo](https://github.com/lauramorillo)
|
652
660
|
|
653
661
|
|
654
662
|
## Caveats and TODOs
|
@@ -659,10 +667,20 @@ Restfulness is still a work in progress but at Cabify we are using it in product
|
|
659
667
|
* Support path methods for automatic URL generation.
|
660
668
|
* Support redirect exceptions.
|
661
669
|
* Needs more functional testing.
|
662
|
-
* Support for before and after filters in resources, although I'm slightly
|
670
|
+
* Support for before and after filters in resources, although I'm slightly apprehensive about this.
|
663
671
|
|
664
672
|
## History
|
665
673
|
|
674
|
+
### 0.3.3 - January 19, 2016
|
675
|
+
|
676
|
+
* Basic support for handling large request bodies received as Tempfile (@lauramorillo)
|
677
|
+
* Providing human readable payload for invalid JSON.
|
678
|
+
* Added support for Accept and Content-Type header handling. (@samlown)
|
679
|
+
* Better handling of IO objects from `rack.input`, such as `Puma::NullIO`. (@samlown)
|
680
|
+
* Upgrading to latest version of RSpec (@samlown)
|
681
|
+
* Adding `request.env` accessor to Rack env (@amuino)
|
682
|
+
* Removing support for Ruby 1.9 (@samlown)
|
683
|
+
|
666
684
|
### 0.3.2 - February 9, 2015
|
667
685
|
|
668
686
|
* Added support for application/x-www-form-urlencoded parameter decoding (@samlown)
|
data/lib/restfulness.rb
CHANGED
@@ -20,6 +20,9 @@ require "restfulness/resources/authentication"
|
|
20
20
|
require "restfulness/requests/authorization"
|
21
21
|
require "restfulness/requests/authorization_header"
|
22
22
|
|
23
|
+
require "restfulness/headers/media_type"
|
24
|
+
require "restfulness/headers/accept"
|
25
|
+
|
23
26
|
require "restfulness/application"
|
24
27
|
require "restfulness/dispatcher"
|
25
28
|
require "restfulness/exceptions"
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Restfulness
|
2
|
+
module Headers
|
3
|
+
|
4
|
+
# The Accept header handler provides an array of Media Types that the
|
5
|
+
# client is willing to accept.
|
6
|
+
#
|
7
|
+
# Based on a simplified RFC2616 implementation, each media type is stored
|
8
|
+
# and ordered.
|
9
|
+
#
|
10
|
+
# Restfulness does not currently deal with the special 'q' paremeter defined
|
11
|
+
# in the standard as quality is not something APIs normally need to handle.
|
12
|
+
#
|
13
|
+
# Aside from media type detection, a useful feature of the accept header is to
|
14
|
+
# provide the desired version of content to provide in the response. This class
|
15
|
+
# offers a helper method that will attempt to determine the version.
|
16
|
+
#
|
17
|
+
# Given the HTTP header:
|
18
|
+
#
|
19
|
+
# Accept: application/com.example.api+json;version=1
|
20
|
+
#
|
21
|
+
# The resource instace has access to the version via:
|
22
|
+
#
|
23
|
+
# request.accept.version == "1"
|
24
|
+
#
|
25
|
+
class Accept
|
26
|
+
|
27
|
+
# The -ordered- array of media types provided in the headers
|
28
|
+
attr_accessor :media_types
|
29
|
+
|
30
|
+
def initialize(str = "")
|
31
|
+
self.media_types = []
|
32
|
+
parse(str) unless str.empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse(str)
|
36
|
+
types = str.split(',').map{|t| t.strip}
|
37
|
+
|
38
|
+
# Attempt to crudely determine order based on length, and store
|
39
|
+
types.sort{|a,b| b.length <=> a.length}.each do |t|
|
40
|
+
media_types << MediaType.new(t)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Request the version, always assumes that the first media type is the most relevant
|
45
|
+
def version
|
46
|
+
media_types.first.version
|
47
|
+
end
|
48
|
+
|
49
|
+
def json?
|
50
|
+
media_types.each do |mt|
|
51
|
+
return true if mt.json?
|
52
|
+
end
|
53
|
+
false
|
54
|
+
end
|
55
|
+
|
56
|
+
def xml?
|
57
|
+
media_types.each do |mt|
|
58
|
+
return true if mt.xml?
|
59
|
+
end
|
60
|
+
false
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|