restfulness 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +342 -0
- data/Rakefile +1 -0
- data/example/Gemfile +5 -0
- data/example/README.md +42 -0
- data/example/app.rb +42 -0
- data/example/config.ru +5 -0
- data/lib/restfulness.rb +36 -0
- data/lib/restfulness/application.rb +83 -0
- data/lib/restfulness/dispatcher.rb +14 -0
- data/lib/restfulness/dispatchers/rack.rb +91 -0
- data/lib/restfulness/exceptions.rb +17 -0
- data/lib/restfulness/log_formatters/quiet_formatter.rb +7 -0
- data/lib/restfulness/log_formatters/verbose_formatter.rb +16 -0
- data/lib/restfulness/path.rb +46 -0
- data/lib/restfulness/request.rb +81 -0
- data/lib/restfulness/resource.rb +111 -0
- data/lib/restfulness/response.rb +57 -0
- data/lib/restfulness/route.rb +48 -0
- data/lib/restfulness/router.rb +27 -0
- data/lib/restfulness/statuses.rb +71 -0
- data/lib/restfulness/version.rb +3 -0
- data/restfulness.gemspec +29 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/unit/application_spec.rb +91 -0
- data/spec/unit/dispatcher_spec.rb +14 -0
- data/spec/unit/dispatchers/rack_spec.rb +34 -0
- data/spec/unit/exceptions_spec.rb +21 -0
- data/spec/unit/path_spec.rb +91 -0
- data/spec/unit/request_spec.rb +129 -0
- data/spec/unit/resource_spec.rb +245 -0
- data/spec/unit/response_spec.rb +65 -0
- data/spec/unit/route_spec.rb +160 -0
- data/spec/unit/router_spec.rb +103 -0
- metadata +189 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ddaa3efca8063cce4459e78242665f30c7aa7beb
|
4
|
+
data.tar.gz: 39fc6f40248c0a0453ce566fc97537920977e529
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 205f618a6e3d7968e9e365e5a90abbbe335ebc994179da9030bc6d57d2bcafacfa9b001f612beea36490e18a2ed8344a2e19e772d70071b266500ba31832b7ec
|
7
|
+
data.tar.gz: f2c6c07d14351f6f05e270f34df4b23b08dea402a82b0988cc8ed4076d960d379b88c1bc2384ebcfcb9b7b80c372d681a790558d40b241b09bd5c3fea4ca9a11
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Sam Lown
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,342 @@
|
|
1
|
+
# Restfulness
|
2
|
+
|
3
|
+
Because REST APIs are all about resources, not routes.
|
4
|
+
|
5
|
+
## Introduction
|
6
|
+
|
7
|
+
Restfulness is an attempt to create a Ruby library that helps create truly REST based APIs to your services. The focus is placed on performing HTTP actions on resources via specific routes, as opposed to the current convention of assigning routes and HTTP actions to methods or blocks of code. The difference is subtle, but makes for a much more natural approach to building APIs.
|
8
|
+
|
9
|
+
The current version is very minimal, as it only support JSON content types, and does not have more advanced commonly used HTTP features like sessions or cookies. For most APIs this should be sufficient.
|
10
|
+
|
11
|
+
To try and highlight the diferences between Restfulness and other libraries, lets have a look at a couple of examples.
|
12
|
+
|
13
|
+
[Grape](https://github.com/intridea/grape) is a popular library for creating APIs in a "REST-like" manor. Here is a simplified section of code from their site:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
module Twitter
|
17
|
+
class API < Grape::API
|
18
|
+
|
19
|
+
version 'v1', using: :header, vendor: 'twitter'
|
20
|
+
format :json
|
21
|
+
|
22
|
+
resource :statuses do
|
23
|
+
|
24
|
+
desc "Return a public timeline."
|
25
|
+
get :public_timeline do
|
26
|
+
Status.limit(20)
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "Return a personal timeline."
|
30
|
+
get :home_timeline do
|
31
|
+
authenticate!
|
32
|
+
current_user.statuses.limit(20)
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "Return a status."
|
36
|
+
params do
|
37
|
+
requires :id, type: Integer, desc: "Status id."
|
38
|
+
end
|
39
|
+
route_param :id do
|
40
|
+
get do
|
41
|
+
Status.find(params[:id])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
```
|
51
|
+
|
52
|
+
The focus in Grape is to construct an API by building up a route hierarchy where each HTTP action is tied to a specific ruby block. Resources are mentioned, but they're used more for structure or route-seperation, than a meaningful object.
|
53
|
+
|
54
|
+
Restfulness takes a different approach. The following example attempts to show how you might provide a similar API:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class TwitterAPI < Restfullness::Application
|
58
|
+
routes do
|
59
|
+
add 'status', StatusResource
|
60
|
+
add 'timeline', 'public', PublicTimelineResource
|
61
|
+
add 'timeline', 'home', HomeTimelineResource
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class StatusResource < Restfulness::Resource
|
66
|
+
def get
|
67
|
+
Status.find(request.path[:id])
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class PublicTimelineResource < Restfulness::Resource
|
72
|
+
def get
|
73
|
+
Status.limit(20)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Authentication requires more cowbell, so assume the ApplicationResource is already defined
|
78
|
+
class HomeTimelineResource < ApplicationResource
|
79
|
+
def authorized?
|
80
|
+
authenticate!
|
81
|
+
end
|
82
|
+
def get
|
83
|
+
current_user.statuses.limit(20)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
```
|
88
|
+
|
89
|
+
I, for one, welcome our new resource overloads. They're a clear and consise way of separating logic between different classes, so an individual model has nothing to do with a collection of models, even if the same model may be provided in the result set.
|
90
|
+
|
91
|
+
|
92
|
+
## Installation
|
93
|
+
|
94
|
+
Add this line to your application's Gemfile:
|
95
|
+
|
96
|
+
gem 'restfulness'
|
97
|
+
|
98
|
+
And then execute:
|
99
|
+
|
100
|
+
$ bundle
|
101
|
+
|
102
|
+
Or install it yourself as:
|
103
|
+
|
104
|
+
$ gem install restfulness
|
105
|
+
|
106
|
+
## Usage
|
107
|
+
|
108
|
+
### Defining an Application
|
109
|
+
|
110
|
+
A Restfulness application is a Rack application whose main function is to define the routes that will forward requests on a specific path to a resource. Your applications inherit from the `Restfulness::Application` class. Here's a simple example:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
class MyAppAPI < Restfulness::Application
|
114
|
+
routes do
|
115
|
+
add 'project', ProjectResource
|
116
|
+
add 'projects', ProjectsResource
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
```
|
121
|
+
|
122
|
+
An application is designed to be included in your Rails, Sinatra, or other Rack project, simply include a new instance of your application in the `config.ru` file:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
run Rack::URLMap.new(
|
126
|
+
"/" => MyRailsApp::Application,
|
127
|
+
"/api" => MyAppAPI.new
|
128
|
+
)
|
129
|
+
```
|
130
|
+
|
131
|
+
By default, Restfulness comes with a Rack compatible dispatcher, but in the future it might make sense to add others.
|
132
|
+
|
133
|
+
If you want to run Restfulness standalone, simply create a `config.ru` that will load up your application:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
require 'my_app'
|
137
|
+
run MyApp.new
|
138
|
+
```
|
139
|
+
|
140
|
+
You can then run this with rackup:
|
141
|
+
|
142
|
+
```
|
143
|
+
bundle exec rackup
|
144
|
+
```
|
145
|
+
|
146
|
+
For an example, checkout the `/example` directory in the source code.
|
147
|
+
|
148
|
+
|
149
|
+
### Routes
|
150
|
+
|
151
|
+
The aim of routes in Restfulness are to be stupid simple. These are the basic rules:
|
152
|
+
|
153
|
+
* Each route is an array that forms a path when joined with `/`.
|
154
|
+
* Order is important.
|
155
|
+
* Strings are matched directly.
|
156
|
+
* Symbols match anything, and are accessible as path attributes.
|
157
|
+
* Every route automically gets an :id parameter at the end, that may or may not have a null value.
|
158
|
+
|
159
|
+
Lets see a few examples:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
routes do
|
163
|
+
# Simple route to access a project, access with:
|
164
|
+
# * PUT /project
|
165
|
+
# * GET /project/1234
|
166
|
+
add 'project', ProjectResource
|
167
|
+
|
168
|
+
# Parameters are also supported.
|
169
|
+
# Access the project id using `request.path[:project_id]`
|
170
|
+
add 'project', :project_id, 'status', ProjectStatusResource
|
171
|
+
end
|
172
|
+
```
|
173
|
+
|
174
|
+
|
175
|
+
### Resources
|
176
|
+
|
177
|
+
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:
|
178
|
+
|
179
|
+
* `get`
|
180
|
+
* `head`
|
181
|
+
* `post`
|
182
|
+
* `put`
|
183
|
+
* `delete`
|
184
|
+
* `options` - this is the only action provded by default
|
185
|
+
|
186
|
+
When creating your resource, simply define the methods you'd like to use and ensure each has a result:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
class ProjectResource < Restfulness::Resource
|
190
|
+
# Return the basic object
|
191
|
+
def get
|
192
|
+
project
|
193
|
+
end
|
194
|
+
|
195
|
+
# Update the object
|
196
|
+
def put
|
197
|
+
project.update(params)
|
198
|
+
end
|
199
|
+
|
200
|
+
protected
|
201
|
+
|
202
|
+
def project
|
203
|
+
@project ||= Project.find(request.path[:id])
|
204
|
+
end
|
205
|
+
end
|
206
|
+
```
|
207
|
+
|
208
|
+
Checking which methods are available is also possible by sending an `OPTIONS` action. Using the above resource as a base:
|
209
|
+
|
210
|
+
curl -v -X OPTIONS http://localhost:9292/project
|
211
|
+
|
212
|
+
Will include an `Allow` header that lists: "GET, PUT, OPTIONS".
|
213
|
+
|
214
|
+
Resources also have support for simple set of built-in callbacks. These have similar objectives to the callbacks used in [Ruby Webmachine](https://github.com/seancribbs/webmachine-ruby) that control the flow of the application using HTTP events.
|
215
|
+
|
216
|
+
The supported callbacks are:
|
217
|
+
|
218
|
+
* `exists?` - True by default, not called in create actions like POST.
|
219
|
+
* `authorized?` - True by default, is the current user valid?
|
220
|
+
* `allowed?` - True by default, does the current have access to the resource?
|
221
|
+
* `last_modified` - The date of last update on the model, only called for GET and HEAD requests. Validated against the `If-Modified-Since` header.
|
222
|
+
* `etag` - Unique identifier for the object, only called for GET and HEAD requests. Validated against the `If-None-Match` header.
|
223
|
+
|
224
|
+
To use them, simply override the method:
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
class ProjectResource < Restfulness::Resource
|
228
|
+
# Does the project exist? only called in GET request
|
229
|
+
def exists?
|
230
|
+
!project.nil?
|
231
|
+
end
|
232
|
+
|
233
|
+
# Return a 304 status if the client can used a cached resource
|
234
|
+
def last_modified
|
235
|
+
project.updated_at.to_s
|
236
|
+
end
|
237
|
+
|
238
|
+
# Return the basic object
|
239
|
+
def get
|
240
|
+
project
|
241
|
+
end
|
242
|
+
|
243
|
+
# Update the object
|
244
|
+
def post
|
245
|
+
Project.create(params)
|
246
|
+
end
|
247
|
+
|
248
|
+
protected
|
249
|
+
|
250
|
+
def project
|
251
|
+
@project ||= Project.find(request.path[:id])
|
252
|
+
end
|
253
|
+
end
|
254
|
+
```
|
255
|
+
|
256
|
+
|
257
|
+
### Requests
|
258
|
+
|
259
|
+
All resource instances have access to a `Request` object via the `#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.
|
260
|
+
|
261
|
+
Restfulness takes a slightly different approach to handling paths, queries, and parameters. Rails and Sinatra apps will typically mash everything together into a `params` hash. While this is convenient for most use cases, it makes it much more difficult to separate values from different contexts. The effects of this are most noticable if you've ever used Models Backbone.js or similar Javascript library. By default a Backbone Model will provide attributes without a prefix in the POST body, so to be able to differenciate between query, path and body parameters you need to ignore the extra attributes, or hack a part of your code to re-add a prefix.
|
262
|
+
|
263
|
+
The following key methods are provided in a request object:
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
# A URI object
|
267
|
+
request.uri # #<URI::HTTPS:0x00123456789 URL:https://example.com/somepath?x=y>
|
268
|
+
|
269
|
+
# Basic request path
|
270
|
+
request.path.to_s # '/project/123456'
|
271
|
+
request.path # ['project', '123456']
|
272
|
+
request.path[:id] # '123456'
|
273
|
+
request.path[0] # 'project
|
274
|
+
|
275
|
+
# More complex request path, from route: ['project', :project_id, 'task']
|
276
|
+
request.path.to_s # '/project/123456/task/234567'
|
277
|
+
request.path # ['project', '123456', 'task', '234567']
|
278
|
+
request.path[:id] # '234567'
|
279
|
+
require.path[:project_id] # '123456'
|
280
|
+
require.path[2] # 'task'
|
281
|
+
|
282
|
+
# The request query
|
283
|
+
request.query # {:page => 1} - Hash with indifferent access
|
284
|
+
request.query[:page] # 1
|
285
|
+
|
286
|
+
# Request body
|
287
|
+
request.body # "{'key':'value'}" - string payload
|
288
|
+
|
289
|
+
# Request params
|
290
|
+
request.params # {'key' => 'value'} - usually a JSON deserialized object
|
291
|
+
```
|
292
|
+
|
293
|
+
## Caveats and TODOs
|
294
|
+
|
295
|
+
Restfulness is still very much a work in progress. Here is a list of things that we'd like to improve or fix:
|
296
|
+
|
297
|
+
* Support for more serializers, not just JSON.
|
298
|
+
* Reloading is a PITA (see note below).
|
299
|
+
* Needs more functional testing.
|
300
|
+
* Support for before and after filters in resources, although I'm slightly aprehensive about this.
|
301
|
+
|
302
|
+
## Reloading
|
303
|
+
|
304
|
+
Reloading is complicated. Unfortunately we're all used to the way Rails projects magically reload changed files so you don't have to restart the server after each change.
|
305
|
+
|
306
|
+
If you're using Restfulness as a standalone project, we recommend using a rack extension like [Shotgun](https://github.com/rtomayko/shotgun).
|
307
|
+
|
308
|
+
If you're adding Restfulness to a Rails project, you can take advantage of the `ActionDispatch::Reloader` rack middleware. Simply include it in the application definition:
|
309
|
+
|
310
|
+
```ruby
|
311
|
+
class MyAPI < Restfulness::Application
|
312
|
+
if Rails.env.development?
|
313
|
+
middlewares << ActionDispatch::Relaoder
|
314
|
+
end
|
315
|
+
routes do
|
316
|
+
# etc. etc.
|
317
|
+
end
|
318
|
+
```
|
319
|
+
|
320
|
+
We're still working on ways to improve this. If you have any ideas, please send me a pull request!
|
321
|
+
|
322
|
+
## Contributing
|
323
|
+
|
324
|
+
1. Fork it
|
325
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
326
|
+
3. Write your code and test the socks off it!
|
327
|
+
4. Commit your changes (`git commit -am 'Add some feature'`)
|
328
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
329
|
+
6. Create new Pull Request
|
330
|
+
|
331
|
+
## Contributors
|
332
|
+
|
333
|
+
Restfulness was created by Sam Lown <me@samlown.com> as a solution for building simple APIs at [Cabify](http://www.cabify.com).
|
334
|
+
|
335
|
+
|
336
|
+
## History
|
337
|
+
|
338
|
+
### 0.1.0 - October 16, 2013
|
339
|
+
|
340
|
+
First release!
|
341
|
+
|
342
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/example/Gemfile
ADDED
data/example/README.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
|
2
|
+
# Restfulness Example App
|
3
|
+
|
4
|
+
Really simple example of a basic app with a couple of resources.
|
5
|
+
|
6
|
+
## Preparation
|
7
|
+
|
8
|
+
Use bundler to make sure all the dependencies are in place:
|
9
|
+
|
10
|
+
cd example
|
11
|
+
bundle install
|
12
|
+
|
13
|
+
By default, bundler will expect to find the restfulness gem provided in the parent directory.
|
14
|
+
|
15
|
+
## Running
|
16
|
+
|
17
|
+
bundle exec runit
|
18
|
+
|
19
|
+
## Testing
|
20
|
+
|
21
|
+
Curl is your friend!
|
22
|
+
|
23
|
+
# Get nothing (returns 404)
|
24
|
+
curl -v http://localhost:9292/projects
|
25
|
+
|
26
|
+
# Post a journey
|
27
|
+
curl -v -X POST http://localhost:9292/project -H "Content-Type: application/json" -d "{\"id\":\"project1\",\"name\":\"First Project\"}"
|
28
|
+
|
29
|
+
# Retrieve it
|
30
|
+
curl -v http://localhost:9292/project/project1
|
31
|
+
|
32
|
+
# Get an array of projects
|
33
|
+
curl -v http://localhost:9292/projects
|
34
|
+
|
35
|
+
# Try updating it
|
36
|
+
curl -v -X PUT http://localhost:9292/project/project1 -H "Content-Type: application/json" -d "{\"name\":\"First Updated Project\"}"
|
37
|
+
|
38
|
+
# Finally remove it and check the list is empty
|
39
|
+
curl -v -X DELETE http://localhost:9292/project/project1
|
40
|
+
curl -v http://localhost:9292/projects
|
41
|
+
|
42
|
+
|