restfolia 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/.travis.yml +3 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +11 -0
- data/Readme.md +101 -0
- data/ReadmeDeveloper.md +30 -0
- data/lib/restfolia/entry_point.rb +156 -0
- data/lib/restfolia/exceptions.rb +109 -0
- data/lib/restfolia/http/behaviour.rb +164 -0
- data/lib/restfolia/http/configuration.rb +74 -0
- data/lib/restfolia/http/request.rb +54 -0
- data/lib/restfolia/http.rb +135 -0
- data/lib/restfolia/resource.rb +109 -0
- data/lib/restfolia/resource_creator.rb +97 -0
- data/lib/restfolia/version.rb +3 -0
- data/lib/restfolia.rb +97 -0
- data/restfolia.gemspec +28 -0
- data/samples/changing_behaviour.rb +32 -0
- data/samples/changing_links_parse.rb +38 -0
- data/samples/cookies_options.rb +23 -0
- data/samples/headers_options.rb +27 -0
- data/samples/http_behaviour.rb +52 -0
- data/samples/using_custom_factory.rb +25 -0
- data/samples/using_custom_resource.rb +25 -0
- data/test/restfolia/entry_point_test.rb +123 -0
- data/test/restfolia/http_behaviour_test.rb +86 -0
- data/test/restfolia/http_configuration_test.rb +45 -0
- data/test/restfolia/resource_creator_test.rb +54 -0
- data/test/restfolia/resource_test.rb +89 -0
- data/test/restfolia_test.rb +10 -0
- data/test/support/json_samples.rb +41 -0
- data/test/support/stub_helpers.rb +36 -0
- data/test/test_helper.rb +13 -0
- metadata +182 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--plugin yard-tomdoc -r Readme.md - MIT-LICENSE ReadmeDeveloper.md
|
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Roger Leite http://1up4dev.org
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
data/Readme.md
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
# Restfolia [![Build Status][travis_status]][travis]
|
2
|
+
|
3
|
+
[travis_status]: https://secure.travis-ci.org/rogerleite/restfolia.png
|
4
|
+
[travis]: http://travis-ci.org/rogerleite/restfolia
|
5
|
+
|
6
|
+
REST client to consume and interact with Hypermedia API, using JSON as Media Type.
|
7
|
+
|
8
|
+
## Description
|
9
|
+
|
10
|
+
Restfolia is a REST client and it's main goal is help you **consume and interact** with Hypermedia APIs.
|
11
|
+
|
12
|
+
Against the grain, Restfolia is very opinionated about some REST's concepts:
|
13
|
+
|
14
|
+
* Aims only **JSON Media Type**.
|
15
|
+
* All responses are parsed and returned as Restfolia::Resource.
|
16
|
+
* Less is more. Restfolia is very proud to be small, easy to maintain and evolve. You can compare Restfolia's code with "Similar Projects" at page's bottom.
|
17
|
+
* Restfolia::Resource is Ruby object with attributes from JSON and can optionally contains **hypermedia links** which have to be a specific format. See the examples below.
|
18
|
+
* All code is very well documented, using [TomDoc](http://tomdoc.org style).
|
19
|
+
|
20
|
+
Obs: This is a draft version. Not ready for production (yet!).
|
21
|
+
|
22
|
+
## References
|
23
|
+
|
24
|
+
You can find more information about arquitecture REST below:
|
25
|
+
|
26
|
+
* [Roy Fielding's](http://roy.gbiv.com/untangled) see this post for example: [REST APIs must be hypertext-driven](http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven).
|
27
|
+
* [Rest in Practice](http://restinpractice.com), especially the chapter titled "Hypermedia Formats".
|
28
|
+
* [Mike Amundsen's Blog](http://amundsen.com/blog)
|
29
|
+
* ROAR - [Resource Oriented Arquitectures in Ruby](https://github.com/apotonick/roar) it seems really good to build a hypermedia API, of course you can go with Sinatra+rabl solutions too.
|
30
|
+
|
31
|
+
## Examples of use
|
32
|
+
|
33
|
+
```js
|
34
|
+
// GET http://localhost:9292/recursos/busca
|
35
|
+
{ "itens_por_pagina" : 10,
|
36
|
+
"paginal_atual" : 1,
|
37
|
+
"paginas_totais" : 1,
|
38
|
+
"query" : "",
|
39
|
+
"total_resultado" : 100,
|
40
|
+
"resultado" : [ { "id" : 1,
|
41
|
+
"name" : "Test1",
|
42
|
+
"links" : [ { "href" : "http://localhost:9292/recursos/id/1",
|
43
|
+
"rel" : "recurso",
|
44
|
+
"type" : "application/json"
|
45
|
+
} ]
|
46
|
+
},
|
47
|
+
{ "id" : 2,
|
48
|
+
"name" : "Test2",
|
49
|
+
"links" : [ { "href" : "http://localhost:9292/recursos/id/2",
|
50
|
+
"rel" : "recurso",
|
51
|
+
"type" : "application/json"
|
52
|
+
} ]
|
53
|
+
}
|
54
|
+
],
|
55
|
+
"links" : { "href" : "http://localhost:9292/recursos/busca",
|
56
|
+
"rel" : "self",
|
57
|
+
"type" : "application/json"
|
58
|
+
},
|
59
|
+
}
|
60
|
+
```
|
61
|
+
|
62
|
+
```js
|
63
|
+
// GET http://localhost:9292/recursos/id/1
|
64
|
+
{ "id" : 1,
|
65
|
+
"name" : "Test1",
|
66
|
+
"links" : { "href" : "http://localhost:9292/recursos/id/1",
|
67
|
+
"rel" : "self",
|
68
|
+
"type" : "application/json"
|
69
|
+
}
|
70
|
+
}
|
71
|
+
```
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
# getting a resource
|
75
|
+
resource = Restfolia.at('http://localhost:9292/recursos/busca').get
|
76
|
+
resource.pagina_atual # => 1
|
77
|
+
resource.resultado # => [#<Resource ...>, #<Resource ...>]
|
78
|
+
|
79
|
+
# example of hypermedia navigation
|
80
|
+
r1 = resource.resultado.first
|
81
|
+
r1 = r1.links("recurso").get # => #<Resource ...>
|
82
|
+
r1.name # => "Test1"
|
83
|
+
```
|
84
|
+
|
85
|
+
## Similar Projects
|
86
|
+
|
87
|
+
* [ROAR](https://github.com/apotonick/roar)
|
88
|
+
* [Leadlight](https://github.com/avdi/leadlight)
|
89
|
+
* [Frenetic](https://github.com/dlindahl/frenetic)
|
90
|
+
* [Restfulie](https://github.com/caelum/restfulie)
|
91
|
+
|
92
|
+
## What is "folia"?
|
93
|
+
|
94
|
+
Folia is a portuguese word and a simple translation in English can be:
|
95
|
+
|
96
|
+
> sf merry-making, merriment, revelry. que folia! what a fun!
|
97
|
+
|
98
|
+
## License
|
99
|
+
|
100
|
+
Restfolia is copyright 2012 Roger Leite and contributors. It is licensed under the MIT license. See the include MIT-LICENSE file for details.
|
101
|
+
|
data/ReadmeDeveloper.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
## Steps to generate doc
|
2
|
+
|
3
|
+
```
|
4
|
+
# dependencies
|
5
|
+
$ gem install yard
|
6
|
+
$ gem install yard-tomdoc
|
7
|
+
$ gem install redcarpet
|
8
|
+
|
9
|
+
# use params from .yardopts file
|
10
|
+
$ yard
|
11
|
+
$ open doc/index.html
|
12
|
+
```
|
13
|
+
**Obs:** ignore errors :X, until my [pull request](https://github.com/rubyworks/yard-tomdoc/pull/5) be accepted.
|
14
|
+
|
15
|
+
## TODO:
|
16
|
+
|
17
|
+
Improvements:
|
18
|
+
|
19
|
+
* Add "events" before and after request. This could be
|
20
|
+
cool to make projects that extends Restfolia features.
|
21
|
+
|
22
|
+
Resource & EntryPoint
|
23
|
+
|
24
|
+
* facilitate cache (Cache Control, ETag, Last-Modified).
|
25
|
+
This could be another project like restfolia-cazuza.
|
26
|
+
|
27
|
+
Future Ideas
|
28
|
+
|
29
|
+
* Support to JSON HAL
|
30
|
+
* Another project, to Restfolia be a "Restfulie like". Main mission: migrate old projects
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module Restfolia
|
2
|
+
|
3
|
+
|
4
|
+
# Public: Responsible for request and validate a Resource.
|
5
|
+
# Abstract details of how to deal with HTTP, like headers,
|
6
|
+
# cookies, auth etc.
|
7
|
+
# The correct form to create an EntryPoint, is using Restfolia.at
|
8
|
+
# or links from some instance of Restfolia::Resource. See the
|
9
|
+
# examples for more details.
|
10
|
+
#
|
11
|
+
# Examples
|
12
|
+
#
|
13
|
+
# ep = Restfolia.at("http://fakeurl.com/some/service")
|
14
|
+
# # => #<EntryPoint ...>
|
15
|
+
#
|
16
|
+
# resource = Restfolia.at("http://fakeurl.com/service/id/1").get
|
17
|
+
# resource.links("contacts")
|
18
|
+
# # => #<EntryPoint ...> to "contacts" from this resource
|
19
|
+
class EntryPoint
|
20
|
+
|
21
|
+
include Restfolia::HTTP::Configuration
|
22
|
+
|
23
|
+
# Public: Returns the String url of EntryPoint.
|
24
|
+
attr_reader :url
|
25
|
+
|
26
|
+
# Public: Returns String that represents the relation of EntryPoint.
|
27
|
+
attr_reader :rel
|
28
|
+
|
29
|
+
# Public: Creates an EntryPoint.
|
30
|
+
#
|
31
|
+
# url - A String address of some API service.
|
32
|
+
# rel - An optional String that represents the relation of EntryPoint.
|
33
|
+
def initialize(url, rel = nil)
|
34
|
+
@url = url
|
35
|
+
@rel = rel
|
36
|
+
end
|
37
|
+
|
38
|
+
# Public: Get the Resource from this EntryPoint's url.
|
39
|
+
#
|
40
|
+
# params - an optional query String or Hash object. String parameter
|
41
|
+
# is passed direct as query. Hash object, before mounting query,
|
42
|
+
# URI.encode is used on values.
|
43
|
+
#
|
44
|
+
# Examples
|
45
|
+
#
|
46
|
+
# # GET on http://service.com/search
|
47
|
+
# resource = Restfolia.at("http://service.com/search").get
|
48
|
+
#
|
49
|
+
# # GET on http://service.com/search?q=test
|
50
|
+
# resource = Restfolia.at("http://service.com/search").get(:q => "test")
|
51
|
+
# # or if you want to control your query, you can send a String
|
52
|
+
# resource = Restfolia.at("http://service.com/search").get("q=test")
|
53
|
+
#
|
54
|
+
# Returns depends on http code from response. For each "range"
|
55
|
+
# (ex. 2xx, 3xx ... etc) you can have a different return value.
|
56
|
+
# For 2xx range, you can expect an instance of Restfolia::Resource.
|
57
|
+
# You can see Restfolia::HttpBehaviour for more details.
|
58
|
+
#
|
59
|
+
# Raises Restfolia::ResponseError for unexpected conditions. See
|
60
|
+
# Restfolia::HTTP::Behaviour methods for more details.
|
61
|
+
# Raises URI::InvalidURIError if url attribute is invalid.
|
62
|
+
def get(params = nil)
|
63
|
+
query = if params && params.is_a?(String)
|
64
|
+
params
|
65
|
+
elsif params && params.is_a?(Hash)
|
66
|
+
params.map { |k, v| "#{k}=#{URI.encode(v)}" }.join
|
67
|
+
end
|
68
|
+
|
69
|
+
args = self.configuration.merge(:query => query)
|
70
|
+
http_resp = Restfolia::HTTP::Request.do_request(:get, self.url, args)
|
71
|
+
Restfolia::HTTP.response_by_status_code(http_resp)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Public: Post data to EntryPoint's url.
|
75
|
+
#
|
76
|
+
# params - Hash object with data to be encoded as JSON and send as
|
77
|
+
# request body.
|
78
|
+
#
|
79
|
+
# Examples
|
80
|
+
#
|
81
|
+
# # Expecting API respond with 201 and Location header
|
82
|
+
# data = {:name => "Test"}
|
83
|
+
# resource = Restfolia.at("http://service.com/resource/1").post(data)
|
84
|
+
#
|
85
|
+
# Returns depends on http code from response. For each "range"
|
86
|
+
# (ex. 2xx, 3xx ... etc) you can have a different return value.
|
87
|
+
# For 2xx range, you can expect an instance of Restfolia::Resource.
|
88
|
+
# You can see Restfolia::HttpBehaviour for more details.
|
89
|
+
#
|
90
|
+
# Raises Restfolia::ResponseError for unexpected conditions. See
|
91
|
+
# Restfolia::HTTP::Behaviour methods for more details.
|
92
|
+
# Raises URI::InvalidURIError if url attribute is invalid.
|
93
|
+
def post(params)
|
94
|
+
body = MultiJson.dump(params)
|
95
|
+
|
96
|
+
args = self.configuration.merge(:body => body)
|
97
|
+
http_resp = Restfolia::HTTP::Request.do_request(:post, self.url, args)
|
98
|
+
Restfolia::HTTP.response_by_status_code(http_resp)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Public: Put data to EntryPoint's url.
|
102
|
+
#
|
103
|
+
# params - Hash object with data to be encoded as JSON and send as
|
104
|
+
# request body.
|
105
|
+
#
|
106
|
+
# Examples
|
107
|
+
#
|
108
|
+
# # Expecting API respond with 201 and Location header
|
109
|
+
# data = {:name => "Test"}
|
110
|
+
# resource = Restfolia.at("http://service.com/resource/1").put(data)
|
111
|
+
#
|
112
|
+
# Returns depends on http code from response. For each "range"
|
113
|
+
# (ex. 2xx, 3xx ... etc) you can have a different return value.
|
114
|
+
# For 2xx range, you can expect an instance of Restfolia::Resource.
|
115
|
+
# You can see Restfolia::HttpBehaviour for more details.
|
116
|
+
#
|
117
|
+
# Raises Restfolia::ResponseError for unexpected conditions. See
|
118
|
+
# Restfolia::HTTP::Behaviour methods for more details.
|
119
|
+
# Raises URI::InvalidURIError if url attribute is invalid.
|
120
|
+
def put(params)
|
121
|
+
body = MultiJson.dump(params)
|
122
|
+
|
123
|
+
args = self.configuration.merge(:body => body)
|
124
|
+
http_resp = Restfolia::HTTP::Request.do_request(:put, self.url, args)
|
125
|
+
Restfolia::HTTP.response_by_status_code(http_resp)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Public: Send Delete verb to EntryPoint's url.
|
129
|
+
#
|
130
|
+
# Examples
|
131
|
+
#
|
132
|
+
# # Expecting API respond with 204 and empty body
|
133
|
+
# resource = Restfolia.at("http://service.com/resource/1").delete
|
134
|
+
#
|
135
|
+
# Returns depends on http code from response. For each "range"
|
136
|
+
# (ex. 2xx, 3xx ... etc) you can have a different return value.
|
137
|
+
# For 2xx range, you can expect an instance of Restfolia::Resource.
|
138
|
+
# You can see Restfolia::HttpBehaviour for more details.
|
139
|
+
#
|
140
|
+
# Raises Restfolia::ResponseError for unexpected conditions. See
|
141
|
+
# Restfolia::HTTP::Behaviour methods for more details.
|
142
|
+
# Raises URI::InvalidURIError if url attribute is invalid.
|
143
|
+
def delete
|
144
|
+
http_resp = Restfolia::HTTP::Request.do_request(:delete, self.url, self.configuration)
|
145
|
+
Restfolia::HTTP.response_by_status_code(http_resp)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Returns url and rel for inspecting.
|
149
|
+
def inspect
|
150
|
+
"#<#{self.class} @url=\"#{@url}\" @rel=\"#{@rel}\">"
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
|
156
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Restfolia
|
2
|
+
|
3
|
+
# Public: Exception to represent an invalid HTTP response.
|
4
|
+
#
|
5
|
+
# Examples
|
6
|
+
#
|
7
|
+
# begin
|
8
|
+
# # assuming http_response is 404 response
|
9
|
+
# raise ResponseError.new("message", caller, http_response)
|
10
|
+
# rescue Restfolia::ResponseError => ex
|
11
|
+
# ex.http_code # => 404
|
12
|
+
# ex.http_message # => "Not Found"
|
13
|
+
# ex.http_object # => http_response object
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
class ResponseError < StandardError
|
17
|
+
|
18
|
+
# Returns nil or #code from http_response instance
|
19
|
+
attr_reader :http_code
|
20
|
+
|
21
|
+
# Returns nil or status code definition from #http_code
|
22
|
+
attr_reader :http_message
|
23
|
+
|
24
|
+
# List of HTTP Status code definitions.
|
25
|
+
# Source http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
26
|
+
HTTP_CODE_MSG = {
|
27
|
+
# 2xx
|
28
|
+
200 => 'OK',
|
29
|
+
201 => 'Created',
|
30
|
+
202 => 'Accepted',
|
31
|
+
203 => 'Non-Authoritative Information',
|
32
|
+
204 => 'No Content',
|
33
|
+
205 => 'Reset Content',
|
34
|
+
206 => 'Partial Content',
|
35
|
+
# 3xx
|
36
|
+
300 => 'Multiple Choices',
|
37
|
+
301 => 'MovedPermanently',
|
38
|
+
302 => 'Found',
|
39
|
+
303 => 'SeeOther',
|
40
|
+
304 => 'NotModified',
|
41
|
+
305 => 'UseProxy',
|
42
|
+
306 => '(Unused)',
|
43
|
+
307 => 'TemporaryRedirect',
|
44
|
+
# 4xx
|
45
|
+
400 => 'Bad Request',
|
46
|
+
401 => 'Unauthorized',
|
47
|
+
402 => 'Payment Required',
|
48
|
+
403 => 'Forbidden',
|
49
|
+
404 => 'Not Found',
|
50
|
+
405 => 'Method Not Allowed',
|
51
|
+
406 => 'Not Acceptable',
|
52
|
+
407 => 'Proxy Authentication Required',
|
53
|
+
408 => 'Request Timeout',
|
54
|
+
409 => 'Conflict',
|
55
|
+
410 => 'Gone',
|
56
|
+
411 => 'Length Required',
|
57
|
+
412 => 'Precondition Failed',
|
58
|
+
413 => 'Request Entity Too Large',
|
59
|
+
414 => 'Request-URI Too Long',
|
60
|
+
415 => 'Unsupported Media Type',
|
61
|
+
416 => 'Requested Range Not Satisfiable',
|
62
|
+
417 => 'Expectation Failed',
|
63
|
+
# 5xx
|
64
|
+
500 => 'Internal Server Error',
|
65
|
+
501 => 'Not Implemented',
|
66
|
+
502 => 'Bad Gateway',
|
67
|
+
503 => 'Service Unavailable',
|
68
|
+
504 => 'Gateway Timeout',
|
69
|
+
505 => 'HTTP Version Not Supported'
|
70
|
+
}
|
71
|
+
|
72
|
+
# Public: Creates a ResponseError.
|
73
|
+
#
|
74
|
+
# message - String to describe the error.
|
75
|
+
# backtrace - Array, usually mounted by Kernel#caller.
|
76
|
+
# http_response - Net::HTTPResponse with error.
|
77
|
+
#
|
78
|
+
# Examples
|
79
|
+
#
|
80
|
+
# begin
|
81
|
+
# # assuming http_response is 404 response
|
82
|
+
# raise ResponseError.new("message", caller, http_response)
|
83
|
+
# rescue Restfolia::ResponseError => ex
|
84
|
+
# ex.http_code # => 404
|
85
|
+
# ex.http_message # => "Not Found"
|
86
|
+
# ex.http_object # => http_response object
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
def initialize(message, backtrace, http_response)
|
90
|
+
super(message)
|
91
|
+
self.set_backtrace(backtrace)
|
92
|
+
|
93
|
+
@http_response = http_response
|
94
|
+
@http_code, @http_message = nil
|
95
|
+
if http_response.respond_to?(:code)
|
96
|
+
@http_code = http_response.code.to_i
|
97
|
+
@http_message = HTTP_CODE_MSG[@http_code] \
|
98
|
+
|| "Unknown HTTP code (#{@http_code})"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Returns nil or Net::HTTPResponse instance.
|
103
|
+
def http_object
|
104
|
+
@http_response
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
module Restfolia::HTTP
|
2
|
+
|
3
|
+
# Public: Separated methods to handle HTTP response by status code.
|
4
|
+
module Behaviour
|
5
|
+
|
6
|
+
# Returns Restfolia::HTTP::Behaviour::Store instance.
|
7
|
+
def self.store
|
8
|
+
@store ||= Store.new
|
9
|
+
end
|
10
|
+
|
11
|
+
# Internal: Helpers Available to behaviours blocks
|
12
|
+
#
|
13
|
+
# Examples
|
14
|
+
#
|
15
|
+
# Restfolia::HTTP.behaviours do
|
16
|
+
# on(200) do |http_response|
|
17
|
+
# helpers.parse_json(http_response.body)
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
class Helpers
|
21
|
+
|
22
|
+
# Internal: Parse response body, checking for errors.
|
23
|
+
#
|
24
|
+
# http_response - HTTP Response with body. Expected to be a JSON.
|
25
|
+
#
|
26
|
+
# Returns Hash who represents JSON parsed.
|
27
|
+
# Raises Restfolia::ResponseError if body seens invalid somehow.
|
28
|
+
def parse_json(http_response)
|
29
|
+
body = http_response.body
|
30
|
+
begin
|
31
|
+
MultiJson.load(body, :symbolize_keys => true)
|
32
|
+
rescue MultiJson::DecodeError => ex
|
33
|
+
msg = "Body should be a valid json. #{ex.message}"
|
34
|
+
raise Restfolia::ResponseError.new(msg, caller, http_response)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Public: Request url with GET and forwards to Restfolia::HTTP.
|
39
|
+
#
|
40
|
+
# url - String. Ex: http://service.com/resources
|
41
|
+
#
|
42
|
+
# Returns what Restfolia::HTTP.response_by_status_code returns.
|
43
|
+
def follow_url(url)
|
44
|
+
http_resp = Request.do_request(:get, url)
|
45
|
+
Restfolia::HTTP.response_by_status_code(http_resp)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
# Public: Responsible to store behaviours. See #behaviours for details.
|
51
|
+
class Store
|
52
|
+
|
53
|
+
# Returns Restfolia::HTTP::Behaviour::Helpers instance.
|
54
|
+
attr_reader :helpers
|
55
|
+
|
56
|
+
# Public: Creates a Store.
|
57
|
+
def initialize
|
58
|
+
self.clear
|
59
|
+
@helpers = Helpers.new
|
60
|
+
end
|
61
|
+
|
62
|
+
# Public: clear all defined behaviours.
|
63
|
+
# Returns nothing.
|
64
|
+
def clear
|
65
|
+
@behaviours = {}
|
66
|
+
@behaviours_range = {}
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
|
70
|
+
# Public: It's a nice way to define configurations for
|
71
|
+
# your behaves using a block.
|
72
|
+
#
|
73
|
+
# block - Required block to customize your behaves. Below are
|
74
|
+
# the methods available inside block:
|
75
|
+
# #on - See #on
|
76
|
+
#
|
77
|
+
# Examples
|
78
|
+
#
|
79
|
+
# store = Restfolia::HTTP::Behaviour::Store.new
|
80
|
+
# store.behaviours do
|
81
|
+
# on(200) { '200 behaviour' }
|
82
|
+
# on([201, 204]) { 'behaviour for 201 and 204 codes' }
|
83
|
+
# on(300...400) { '3xx range' }
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# Returns nothing.
|
87
|
+
def behaviours(&block)
|
88
|
+
self.instance_eval(&block)
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
# Public: Add behaviour on this store. See #behaviours for
|
93
|
+
# examples.
|
94
|
+
#
|
95
|
+
# code - Integer or any object that respond to #include?
|
96
|
+
# block - Required block with behaviour for this code.
|
97
|
+
#
|
98
|
+
# Returns nothing.
|
99
|
+
def on(code, &block)
|
100
|
+
if code.is_a?(Integer)
|
101
|
+
@behaviours[code] = block
|
102
|
+
elsif code.respond_to?(:include?)
|
103
|
+
@behaviours_range[code] = block
|
104
|
+
end
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
|
108
|
+
# Public: Method called by #execute in case of 'not found' http code.
|
109
|
+
#
|
110
|
+
# http_response - Net::HTTPResponse object.
|
111
|
+
#
|
112
|
+
# Examples
|
113
|
+
#
|
114
|
+
# store = Restfolia::HTTP::Behaviour::Store.new
|
115
|
+
# store.behaviours do
|
116
|
+
# on(200) { '200 ok' }
|
117
|
+
# end
|
118
|
+
# http_resp = OpenStruct.new(:code => 201) #simulate response 201
|
119
|
+
# store.execute(http_resp)
|
120
|
+
# # => #<Restfolia::ResponseError "Undefined behaviour for 201" ...>
|
121
|
+
#
|
122
|
+
# Returns nothing.
|
123
|
+
# Raises Restfolia::ResponseError
|
124
|
+
def default_behaviour(http_response)
|
125
|
+
msg = "Undefined behaviour for #{http_response.code}"
|
126
|
+
raise Restfolia::ResponseError.new(msg, caller, http_response)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Public: Look for defined behaviour, based on HTTP code.
|
130
|
+
# If behaviour not found, call #default_behaviour.
|
131
|
+
#
|
132
|
+
# http_response - Net::HTTPResponse.
|
133
|
+
# Returns Result from Proc behaviour or default_behaviour method.
|
134
|
+
def execute(http_response)
|
135
|
+
code = http_response.code.to_i
|
136
|
+
if (behaviour = find(code))
|
137
|
+
behaviour.call(http_response)
|
138
|
+
else
|
139
|
+
default_behaviour(http_response)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
protected
|
144
|
+
|
145
|
+
# Internal: Search for defined behaviour.
|
146
|
+
#
|
147
|
+
# code - Integer or object that respond to #include?
|
148
|
+
#
|
149
|
+
# Returns nil or Proc
|
150
|
+
def find(code)
|
151
|
+
behaviour = @behaviours[code]
|
152
|
+
if behaviour.nil?
|
153
|
+
_, behaviour = @behaviours_range.detect do |range, proc|
|
154
|
+
range.include?(code)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
behaviour
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Restfolia::HTTP
|
2
|
+
|
3
|
+
# Internal: Simply a bag of HTTP options like headers, cookies, auth ... etc.
|
4
|
+
module Configuration
|
5
|
+
|
6
|
+
# Public: Sets/Returns cookies values as String.
|
7
|
+
attr_accessor :cookies
|
8
|
+
|
9
|
+
# Public: Returns Hash to be used as Headers on request.
|
10
|
+
def headers
|
11
|
+
@headers ||= {}
|
12
|
+
end
|
13
|
+
|
14
|
+
# Public: A fluent way to add Cookies to Request.
|
15
|
+
#
|
16
|
+
# cookies - String in cookie format.
|
17
|
+
#
|
18
|
+
# Examples
|
19
|
+
#
|
20
|
+
# # setting cookie from Google Translate
|
21
|
+
# cookies = "PREF=ID=07eb...; expires=Sat, 26-Apr-2014 19:19:36 GMT; path=/; domain=.google.com, NID=59...; expires=Fri, 26-Oct-2012 19:19:36 GMT; path=/; domain=.google.com; HttpOnly"
|
22
|
+
# resource = Restfolia.at("http://fake.com").
|
23
|
+
# set_cookies(cookies).get
|
24
|
+
#
|
25
|
+
# Returns self, always!
|
26
|
+
def set_cookies(cookies)
|
27
|
+
self.cookies = cookies
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
# Public: A fluent way to add HTTP headers.
|
32
|
+
# Headers informed here are merged with headers attribute.
|
33
|
+
#
|
34
|
+
# new_headers - Hash with headers.
|
35
|
+
#
|
36
|
+
# Examples
|
37
|
+
#
|
38
|
+
# entry_point = Restfolia.at("http://fake.com")
|
39
|
+
# entry_point.with_headers("X-Custom1" => "value",
|
40
|
+
# "X-Custom2" => "value2").get
|
41
|
+
#
|
42
|
+
# Returns self, always!
|
43
|
+
# Raises ArgumentError unless new_headers is a Hash.
|
44
|
+
def with_headers(new_headers)
|
45
|
+
unless new_headers.is_a?(Hash)
|
46
|
+
raise ArgumentError, "New Headers should Hash object."
|
47
|
+
end
|
48
|
+
|
49
|
+
headers.merge!(new_headers)
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
# Public: A fluent way to add Content-Type and Accept headers.
|
54
|
+
#
|
55
|
+
# content_type - String value. Ex: "application/json"
|
56
|
+
#
|
57
|
+
# Returns self, always!
|
58
|
+
def as(content_type)
|
59
|
+
headers["Content-Type"] = content_type
|
60
|
+
headers["Accept"] = content_type
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
protected
|
65
|
+
|
66
|
+
# Returns Hash with headers, cookies, auth ... etc.
|
67
|
+
def configuration
|
68
|
+
{:headers => headers,
|
69
|
+
:cookies => cookies}
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|