restfulie 0.8.1 → 0.9.1
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.
- data/Gemfile +2 -0
- data/README.textile +35 -13
- data/Rakefile +51 -19
- data/lib/restfulie.rb +2 -1
- data/lib/restfulie/client.rb +7 -0
- data/lib/restfulie/client/cache.rb +11 -0
- data/lib/restfulie/client/cache/basic.rb +75 -0
- data/lib/restfulie/client/cache/fake.rb +15 -0
- data/lib/restfulie/client/cache/http_ext.rb +121 -0
- data/lib/restfulie/client/cache/restrictions.rb +18 -0
- data/lib/restfulie/client/http/link_request_builder.rb +1 -0
- data/lib/restfulie/client/http/request_adapter.rb +7 -3
- data/lib/restfulie/client/http/request_builder.rb +12 -5
- data/lib/restfulie/client/http/request_history.rb +5 -3
- data/lib/restfulie/client/http/request_marshaller.rb +3 -1
- data/lib/restfulie/common/representation.rb +1 -1
- data/lib/restfulie/common/representation/atom/base.rb +10 -1
- data/lib/restfulie/common/representation/links.rb +11 -0
- data/lib/restfulie/server.rb +6 -0
- data/lib/restfulie/server/action_controller.rb +2 -0
- data/lib/restfulie/server/action_controller/base.rb +1 -7
- data/lib/restfulie/server/action_controller/cacheable_responder.rb +71 -0
- data/lib/restfulie/server/action_controller/created_responder.rb +19 -0
- data/lib/restfulie/server/action_controller/restful_responder.rb +4 -39
- data/lib/restfulie/server/action_view/template_handlers/tokamak.rb +2 -1
- data/lib/restfulie/server/core_ext/array.rb +7 -2
- data/lib/restfulie/version.rb +1 -1
- metadata +27 -5
- data/lib/restfulie/common/representation/xml.rb +0 -26
data/Gemfile
CHANGED
data/README.textile
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
h1. Web site
|
2
2
|
|
3
|
-
Restfulie's website can be found at "http://restfulie.
|
4
|
-
|
3
|
+
Restfulie's website can be found at "http://restfulie.caelumobjects.com":http://restfulie.caelumobjects.com
|
4
|
+
From there you can see some videos and read the "Restfulie from Scratch guide":http://restfulie.caelumobjects.com/caelumobjects-restful-rails.pdf.
|
5
5
|
|
6
6
|
h1. Quit pretending
|
7
7
|
|
@@ -17,15 +17,6 @@ h2. Why would I use restfulie?
|
|
17
17
|
# HATEOAS --> consumed resources will not affect your software whenever they change their flow
|
18
18
|
# Adaptability --> clients are able to adapt to your changes
|
19
19
|
|
20
|
-
h2. Documentation
|
21
|
-
|
22
|
-
Appart from the simple server and client examples provided here, you can find the following links useful:
|
23
|
-
|
24
|
-
* "RDocs":http://rdoc.info/projects/caelum/restfulie
|
25
|
-
* "Official website":http://restfulie.caelumobjects.com
|
26
|
-
* "How-tos":http://restfulie.caelumobjects.com/rails
|
27
|
-
* "Buying through Rest: Rest to the enterprise (video)":guilhermesilveira.wordpress.com/2010/04/13/buying-through-rest-applying-rest-to-the-enterprise/
|
28
|
-
|
29
20
|
h2. Simple server example
|
30
21
|
|
31
22
|
In the server side, all you need to do is notify inherited_resources which media types you are able to represent your resource:
|
@@ -75,11 +66,20 @@ puts receipt.id
|
|
75
66
|
|
76
67
|
In order to create a full REST client, "watch this video":http://guilhermesilveira.wordpress.com.
|
77
68
|
|
78
|
-
h2.
|
69
|
+
h2. Full examples
|
79
70
|
|
80
71
|
You can view an entire application running Restfulie under *spec/integration/order/server* and *spec/integration/order/client* in this git repository.
|
81
72
|
|
82
|
-
"You can also download a full example of a REST based agent and server":http://github.com/caelum/
|
73
|
+
"You can also download a full example of a REST based agent and server":http://github.com/caelum/full-restfulie-examples using Restfulie, according to the Rest Architecture Maturity Model.
|
74
|
+
|
75
|
+
h2. Documentation
|
76
|
+
|
77
|
+
Appart from the simple server and client examples provided here, you can find the following links useful:
|
78
|
+
|
79
|
+
* "RDocs":http://rdoc.info/projects/caelum/restfulie
|
80
|
+
* "Official website":http://restfulie.caelumobjects.com
|
81
|
+
* "How-tos":http://restfulie.caelumobjects.com/caelumobjects-restful-rails.pdf
|
82
|
+
* "Buying through Rest: Rest to the enterprise (video)":http://guilhermesilveira.wordpress.com/2010/04/13/buying-through-rest-applying-rest-to-the-enterprise/
|
83
83
|
|
84
84
|
h2. Installing
|
85
85
|
|
@@ -114,3 +114,25 @@ try {
|
|
114
114
|
var pageTracker = _gat._getTracker("UA-11770776-1");
|
115
115
|
pageTracker._trackPageview();
|
116
116
|
} catch(err) {}</script>
|
117
|
+
|
118
|
+
h2. Contributions
|
119
|
+
|
120
|
+
Restfulie was created and is maintained by "http://caelumobjects.com":Caelum, and has received enormous contributions from all those developers:
|
121
|
+
|
122
|
+
Project Leader
|
123
|
+
Guilherme Silveira, "http://caelum.com.br":Caelum
|
124
|
+
|
125
|
+
Caue Guerra, caelum
|
126
|
+
George Guimaraes, abril and plataforma
|
127
|
+
Fabio Akita
|
128
|
+
Diego Carrion
|
129
|
+
Leandro Silva
|
130
|
+
Gavin-John Noonan
|
131
|
+
Antonio Marques
|
132
|
+
Luis Cipriani, abril
|
133
|
+
Everton Ribeiro, abril
|
134
|
+
Paulo Ahagon, abril
|
135
|
+
Elomar França
|
136
|
+
Thomas Stefano
|
137
|
+
"http://www.caelum.com.br":"David Paniz"
|
138
|
+
"http://www.caikesouza.com/blog":Caike Souza
|
data/Rakefile
CHANGED
@@ -15,7 +15,6 @@ AUTHOR = "Guilherme Silveira, Caue Guerra, Luis Cipriani, Everton Ribeiro, Geo
|
|
15
15
|
EMAIL = "guilherme.silveira@caelum.com.br"
|
16
16
|
HOMEPAGE = "http://restfulie.caelumobjects.com"
|
17
17
|
|
18
|
-
|
19
18
|
spec = Gem::Specification.new do |s|
|
20
19
|
s.name = GEM
|
21
20
|
s.version = GEM_VERSION
|
@@ -34,22 +33,40 @@ spec = Gem::Specification.new do |s|
|
|
34
33
|
s.homepage = HOMEPAGE
|
35
34
|
end
|
36
35
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
pid = process "fake_server"
|
48
|
-
puts "fake_server pid >>>> #{pid}"
|
36
|
+
# optionally loads a task if the required gems exist
|
37
|
+
def optionally
|
38
|
+
begin
|
39
|
+
yield
|
40
|
+
rescue LoadError; end
|
41
|
+
end
|
42
|
+
|
43
|
+
def start_server_and_invoke_test(task_name)
|
44
|
+
IO.popen("ruby ./spec/units/client/fake_server.rb") do |pipe|
|
45
|
+
wait_server(4567)
|
49
46
|
Rake::Task[task_name].invoke
|
50
|
-
|
47
|
+
Process.kill 'INT', pipe.pid
|
51
48
|
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def wait_server(port=3000)
|
52
|
+
(1..15).each do
|
53
|
+
begin
|
54
|
+
Net::HTTP.get(URI.parse("http://localhost:#{port}/"))
|
55
|
+
return
|
56
|
+
rescue
|
57
|
+
sleep 1
|
58
|
+
end
|
59
|
+
end
|
60
|
+
raise "Waited for the server but it did not finish"
|
61
|
+
end
|
52
62
|
|
63
|
+
desc 'Start server'
|
64
|
+
task :server do
|
65
|
+
process 'fake_server'
|
66
|
+
end
|
67
|
+
|
68
|
+
namespace :test do
|
69
|
+
|
53
70
|
desc "Execute integration Order tests"
|
54
71
|
task :integration do
|
55
72
|
integration_path = "spec/integration/order/server"
|
@@ -99,6 +116,7 @@ namespace :test do
|
|
99
116
|
start_server_and_invoke_test('test:spec:all')
|
100
117
|
puts "Execution integration tests... (task test:integration)"
|
101
118
|
Rake::Task["test:integration"].invoke()
|
119
|
+
Rake::Task["test:examples"].invoke()
|
102
120
|
end
|
103
121
|
task :common do
|
104
122
|
start_server_and_invoke_test('test:spec:common')
|
@@ -113,6 +131,22 @@ namespace :test do
|
|
113
131
|
start_server_and_invoke_test('test:rcov:rcov')
|
114
132
|
end
|
115
133
|
end
|
134
|
+
|
135
|
+
desc "runs all example tests"
|
136
|
+
task :examples do
|
137
|
+
Rake::Task["install"].invoke()
|
138
|
+
|
139
|
+
target_dir = "full-examples/rest_from_scratch/part_3"
|
140
|
+
system "cd #{target_dir} && rake db:reset db:seed"
|
141
|
+
|
142
|
+
IO.popen("ruby #{target_dir}/script/server") do |pipe|
|
143
|
+
wait_server
|
144
|
+
system "cd #{target_dir} && rake spec"
|
145
|
+
Process.kill 'INT', pipe.pid
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
116
150
|
end
|
117
151
|
|
118
152
|
Rake::GemPackageTask.new(spec) do |pkg|
|
@@ -121,16 +155,14 @@ end
|
|
121
155
|
|
122
156
|
Rake::RDocTask.new("rdoc") do |rdoc|
|
123
157
|
rdoc.options << '--line-numbers' << '--inline-source'
|
124
|
-
# rdoc.rdoc_files.include('lib/**/**/*.rb')
|
125
158
|
end
|
126
159
|
|
127
|
-
|
160
|
+
optionally do
|
128
161
|
require 'yard'
|
129
162
|
YARD::Rake::YardocTask.new do |t|
|
130
|
-
t.files = ['lib/restfulie/**/*.rb', 'README.textile']
|
131
|
-
# t.options = ['--any', '--extra', '--opts'] # optional
|
163
|
+
t.files = ['lib/restfulie/**/*.rb', 'README.textile']
|
132
164
|
end
|
133
|
-
|
165
|
+
end
|
134
166
|
|
135
167
|
desc "Install the gem locally"
|
136
168
|
task :install => [:package] do
|
data/lib/restfulie.rb
CHANGED
@@ -7,7 +7,8 @@ require 'restfulie/server'
|
|
7
7
|
# Shortcut to Restfulie::Client::EntryPoint
|
8
8
|
module Restfulie
|
9
9
|
extend Restfulie::Client::EntryPoint
|
10
|
-
|
10
|
+
|
11
|
+
# creates a new entry point for executing requests
|
11
12
|
def self.at(uri)
|
12
13
|
Object.new.send(:extend, Restfulie::Client::EntryPoint).at(uri)
|
13
14
|
end
|
data/lib/restfulie/client.rb
CHANGED
@@ -7,6 +7,13 @@ module Restfulie
|
|
7
7
|
autoload :EntryPoint, 'restfulie/client/entry_point'
|
8
8
|
autoload :Base, 'restfulie/client/base'
|
9
9
|
autoload :Mikyung, 'restfulie/client/mikyung'
|
10
|
+
autoload :Cache, 'restfulie/client/cache'
|
11
|
+
|
12
|
+
mattr_accessor :cache_provider, :cache_store
|
13
|
+
|
14
|
+
Restfulie::Client.cache_store = ActiveSupport::Cache::MemoryStore.new
|
15
|
+
Restfulie::Client.cache_provider = Restfulie::Client::Cache::Basic.new
|
16
|
+
|
10
17
|
end
|
11
18
|
end
|
12
19
|
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Restfulie
|
2
|
+
module Client
|
3
|
+
module Cache
|
4
|
+
autoload :Basic, 'restfulie/client/cache/basic'
|
5
|
+
autoload :Fake, 'restfulie/client/cache/fake'
|
6
|
+
autoload :Restrictions, 'restfulie/client/cache/restrictions'
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
require 'restfulie/client/cache/http_ext'
|
11
|
+
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# Basic cache implementation for restfulie.
|
2
|
+
#
|
3
|
+
# It uses the request headers and uri to store it in memory.
|
4
|
+
# This cache might not be optimal for long running clients, which should use a memcached based one.
|
5
|
+
# Use Restfulie::Client.cache_provider to change the provider
|
6
|
+
module Restfulie::Client::Cache
|
7
|
+
class Basic
|
8
|
+
|
9
|
+
def put(key, req, response)
|
10
|
+
if Restfulie::Client::Cache::Restrictions.may_cache?(response)
|
11
|
+
Restfulie::Common::Logger.logger.debug "caching #{key} #{response}"
|
12
|
+
cache_add(key, req, response)
|
13
|
+
end
|
14
|
+
response
|
15
|
+
end
|
16
|
+
|
17
|
+
def get(key, request, verb)
|
18
|
+
|
19
|
+
# debugger
|
20
|
+
response = cache_get(key, request, verb)
|
21
|
+
return nil if response.nil?
|
22
|
+
|
23
|
+
if response.has_expired_cache?
|
24
|
+
remove(key)
|
25
|
+
else
|
26
|
+
Restfulie::Common::Logger.logger.debug "RETURNING cache #{key}"
|
27
|
+
cache_hit response
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
# removes all elements from the cache
|
33
|
+
def clear
|
34
|
+
cache.clear
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# allows response enhancement when the cache was hit with it
|
40
|
+
def cache_hit(response)
|
41
|
+
response
|
42
|
+
end
|
43
|
+
|
44
|
+
def cache_add(key, req, response)
|
45
|
+
values = (cache.read(key) || []).dup
|
46
|
+
values << [req, response]
|
47
|
+
cache.write(key, values)
|
48
|
+
end
|
49
|
+
|
50
|
+
def cache_get(key, req, verb)
|
51
|
+
return nil unless cache.exist?(key)
|
52
|
+
found = cache.read(key).find do |cached|
|
53
|
+
old_req = cached.first
|
54
|
+
old_response = cached.last
|
55
|
+
if old_response.vary_headers_for(old_req) == old_response.vary_headers_for(req) &&
|
56
|
+
old_response.method == verb
|
57
|
+
old_response
|
58
|
+
else
|
59
|
+
false
|
60
|
+
end
|
61
|
+
end
|
62
|
+
found ? found.last : nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def remove(key)
|
66
|
+
cache.delete(key)
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
|
70
|
+
def cache
|
71
|
+
Restfulie::Client.cache_store
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Fake cache that does not cache anything
|
2
|
+
# Use Restfulie::Client.cache_provider = Restfulie::Client::Cache::Fake.new
|
3
|
+
module Restfulie::Client::Cache
|
4
|
+
class Fake
|
5
|
+
def put(url, req, response)
|
6
|
+
response
|
7
|
+
end
|
8
|
+
|
9
|
+
def get(url, req)
|
10
|
+
end
|
11
|
+
|
12
|
+
def clear
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
# an extesion to http responses
|
4
|
+
module Restfulie::Client::HTTP::ResponseStatus
|
5
|
+
|
6
|
+
attr_accessor :previous
|
7
|
+
|
8
|
+
# determines if this response code was successful (according to http specs: 200~299)
|
9
|
+
def is_successful?
|
10
|
+
code.to_i >= 200 && code.to_i <= 299
|
11
|
+
end
|
12
|
+
|
13
|
+
# determines if this response code was successful (according to http specs: 100~199)
|
14
|
+
def is_informational?
|
15
|
+
code.to_i >= 100 && code.to_i <= 199
|
16
|
+
end
|
17
|
+
|
18
|
+
# determines if this response code was successful (according to http specs: 300~399)
|
19
|
+
def is_redirection?
|
20
|
+
code.to_i >= 300 && code.to_i <= 399
|
21
|
+
end
|
22
|
+
|
23
|
+
# determines if this response code was successful (according to http specs: 400~499)
|
24
|
+
def is_client_error?
|
25
|
+
code.to_i >= 400 && code.to_i <= 499
|
26
|
+
end
|
27
|
+
|
28
|
+
# determines if this response code was successful (according to http specs: 500~599)
|
29
|
+
def is_server_error?
|
30
|
+
code.to_i >= 500 && code.to_i <= 599
|
31
|
+
end
|
32
|
+
|
33
|
+
def etag
|
34
|
+
self['Etag']
|
35
|
+
end
|
36
|
+
|
37
|
+
def last_modified
|
38
|
+
self['Last-Modified']
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
module Restfulie::Client::HTTP::ResponseCacheCheck
|
44
|
+
|
45
|
+
def cache_max_age
|
46
|
+
val = header_value_from('cache-control', /^\s*max-age=(\d+)/)
|
47
|
+
if val
|
48
|
+
val.to_i
|
49
|
+
else
|
50
|
+
0
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def header_value_from(header, expression)
|
55
|
+
h = value_for(headers[header], expression)
|
56
|
+
return nil if h.nil?
|
57
|
+
h.match(expression)[1]
|
58
|
+
end
|
59
|
+
|
60
|
+
def has_expired_cache?
|
61
|
+
return true if headers['date'].nil?
|
62
|
+
max_time = Time.rfc2822(headers['date']) + cache_max_age.seconds
|
63
|
+
Time.now > max_time
|
64
|
+
end
|
65
|
+
|
66
|
+
# checks if the header's max-age is available and no no-store if available.
|
67
|
+
def may_cache?
|
68
|
+
may_cache_field?(headers['cache-control'])
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns whether this cache control field allows caching
|
72
|
+
#
|
73
|
+
# may_cache_field(['max-age=2000', 'no-store']) == false
|
74
|
+
# may_cache_field('max-age=2000,no-store') == false
|
75
|
+
# may_cache_field('max-age=2000') == true
|
76
|
+
def may_cache_field?(field)
|
77
|
+
return false if field.nil?
|
78
|
+
|
79
|
+
if field.kind_of? Array
|
80
|
+
field.each do |f|
|
81
|
+
return false if !may_cache_field?(f)
|
82
|
+
end
|
83
|
+
return true
|
84
|
+
end
|
85
|
+
|
86
|
+
max_age_header = value_for(field, /^max-age=(\d+)/)
|
87
|
+
return false if max_age_header.nil?
|
88
|
+
|
89
|
+
if value_for(field, /^no-store/)
|
90
|
+
false
|
91
|
+
else
|
92
|
+
true
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# extracts the header value for an specific expression, which can be located at the start or in the middle
|
97
|
+
# of the expression
|
98
|
+
def value_for(value, expression)
|
99
|
+
value.split(",").find { |obj| obj.strip =~ expression }
|
100
|
+
end
|
101
|
+
|
102
|
+
# extracts all header values related to the Vary header from this response, in order
|
103
|
+
# to implement Vary support from the HTTP Specification
|
104
|
+
#
|
105
|
+
# example
|
106
|
+
# if the response Vary header is 'Accept','Accept-Language', we have
|
107
|
+
# vary_headers_for({'Accept'=>'application/xml', 'Date' =>'...', 'Accept-Language'=>'de'}) == ['application/xml', 'de']
|
108
|
+
# vary_headers_for({'Date' => '...', 'Accept-Language'=>'de'}) == [nil, 'de']
|
109
|
+
def vary_headers_for(request)
|
110
|
+
return nil if headers['vary'].nil?
|
111
|
+
headers['vary'].split(',').map do |key|
|
112
|
+
request[key.strip]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
class Restfulie::Client::HTTP::Response
|
119
|
+
include Restfulie::Client::HTTP::ResponseStatus
|
120
|
+
include Restfulie::Client::HTTP::ResponseCacheCheck
|
121
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Restfulie::Client::Cache
|
2
|
+
module Restrictions
|
3
|
+
class << self
|
4
|
+
|
5
|
+
# checks whether this request verb and its cache headers allow caching
|
6
|
+
def may_cache?(response)
|
7
|
+
may_cache_method?(response.method) && response.may_cache?
|
8
|
+
end
|
9
|
+
|
10
|
+
# only Post and Get requests are cacheable so far
|
11
|
+
def may_cache_method?(verb)
|
12
|
+
verb == :get || verb == :post
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -125,7 +125,7 @@ module Restfulie
|
|
125
125
|
def request(method, path, *args)
|
126
126
|
request!(method, path, *args)
|
127
127
|
rescue Error::RESTError => se
|
128
|
-
se.response
|
128
|
+
[[@host, path], nil, se.response]
|
129
129
|
end
|
130
130
|
|
131
131
|
# Executes a request against your server and return a response instance.
|
@@ -142,14 +142,18 @@ module Restfulie
|
|
142
142
|
|
143
143
|
::Restfulie::Common::Logger.logger.info(request_to_s(method, path, *args)) if ::Restfulie::Common::Logger.logger
|
144
144
|
begin
|
145
|
-
|
145
|
+
http_request = get_connection_provider
|
146
|
+
response = Restfulie::Client.cache_provider.get([@host, path], http_request, method)
|
147
|
+
return [[@host, path], http_request, response] if response
|
148
|
+
response = ResponseHandler.handle(method, path, http_request.send(method, path, *args))
|
146
149
|
rescue Exception => e
|
150
|
+
Restfulie::Common::Logger.logger.error(e)
|
147
151
|
raise Error::ServerNotAvailableError.new(self, Response.new(method, path, 503, nil, {}), e )
|
148
152
|
end
|
149
153
|
|
150
154
|
case response.code
|
151
155
|
when 100..299
|
152
|
-
response
|
156
|
+
[[@host, path], http_request, response]
|
153
157
|
when 300..399
|
154
158
|
raise Error::Redirection.new(self, response)
|
155
159
|
when 400
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
1
3
|
module Restfulie
|
2
4
|
module Client
|
3
5
|
module HTTP #:nodoc:
|
@@ -48,8 +50,8 @@ module Restfulie
|
|
48
50
|
host.path
|
49
51
|
end
|
50
52
|
|
51
|
-
def get
|
52
|
-
request(:get, path, headers)
|
53
|
+
def get(params = {})
|
54
|
+
request(:get, add_querystring(path, params), headers)
|
53
55
|
end
|
54
56
|
|
55
57
|
def head
|
@@ -71,9 +73,9 @@ module Restfulie
|
|
71
73
|
def delete
|
72
74
|
request(:delete, path, headers)
|
73
75
|
end
|
74
|
-
|
75
|
-
def get!
|
76
|
-
request!(:get, path, headers)
|
76
|
+
|
77
|
+
def get!(params = {})
|
78
|
+
request!(:get, add_querystring(path, params), headers)
|
77
79
|
end
|
78
80
|
|
79
81
|
def head!
|
@@ -97,6 +99,11 @@ module Restfulie
|
|
97
99
|
end
|
98
100
|
|
99
101
|
protected
|
102
|
+
|
103
|
+
def add_querystring(path, params)
|
104
|
+
params = params.map { |param, value| "#{param}=#{value}"}.join("&")
|
105
|
+
params.blank? ? path : URI.escape("#{path}?#{params}")
|
106
|
+
end
|
100
107
|
|
101
108
|
def headers=(h)
|
102
109
|
@headers = h
|
@@ -14,12 +14,14 @@ module Restfulie
|
|
14
14
|
module RequestHistory
|
15
15
|
include RequestBuilder
|
16
16
|
|
17
|
-
attr_accessor_with_default :max_to_remind, 10
|
18
|
-
|
19
17
|
def snapshots
|
20
18
|
@snapshots ||= []
|
21
19
|
end
|
22
20
|
|
21
|
+
def max_to_remind
|
22
|
+
10
|
23
|
+
end
|
24
|
+
|
23
25
|
def request!(method=nil, path=nil, *args)#:nodoc:
|
24
26
|
if method == nil || path == nil
|
25
27
|
raise 'History not selected' unless @snapshot
|
@@ -37,7 +39,7 @@ module Restfulie
|
|
37
39
|
def request(method=nil, path=nil, *args)#:nodoc:
|
38
40
|
request!(method, path, *args)
|
39
41
|
rescue Error::RESTError => se
|
40
|
-
se.response
|
42
|
+
[[@host, path], nil, se.response]
|
41
43
|
end
|
42
44
|
|
43
45
|
def history(number)
|
@@ -60,7 +60,9 @@ module Restfulie
|
|
60
60
|
args = add_representation_headers(method, path, unmarshaller, *args)
|
61
61
|
end
|
62
62
|
|
63
|
-
response = super(method, path, *args)
|
63
|
+
key, req, response = super(method, path, *args)
|
64
|
+
Restfulie::Client.cache_provider.put(key, req, response)
|
65
|
+
|
64
66
|
parse_response(response)
|
65
67
|
end
|
66
68
|
|
@@ -4,7 +4,7 @@ module Restfulie
|
|
4
4
|
autoload :Atom, 'restfulie/common/representation/atom'
|
5
5
|
autoload :Generic, 'restfulie/common/representation/generic'
|
6
6
|
autoload :Json, 'restfulie/common/representation/json'
|
7
|
-
autoload :
|
7
|
+
autoload :Links, 'restfulie/common/representation/links'
|
8
8
|
end
|
9
9
|
end
|
10
10
|
end
|
@@ -63,7 +63,16 @@ module Restfulie
|
|
63
63
|
def updated=(value)
|
64
64
|
set_text("updated", value)
|
65
65
|
end
|
66
|
-
|
66
|
+
|
67
|
+
def published
|
68
|
+
value = text("published")
|
69
|
+
Time.parse(value) unless value.nil?
|
70
|
+
end
|
71
|
+
|
72
|
+
def published=(value)
|
73
|
+
set_text("published", value)
|
74
|
+
end
|
75
|
+
|
67
76
|
# text
|
68
77
|
def rights
|
69
78
|
text("rights")
|
data/lib/restfulie/server.rb
CHANGED
@@ -3,6 +3,8 @@ module Restfulie
|
|
3
3
|
module ActionController #:nodoc:
|
4
4
|
if defined?(::ActionController) || defined?(::ApplicationController)
|
5
5
|
autoload :ParamsParser, 'restfulie/server/action_controller/params_parser'
|
6
|
+
autoload :CacheableResponder, 'restfulie/server/action_controller/cacheable_responder'
|
7
|
+
autoload :CreatedResponder, 'restfulie/server/action_controller/created_responder'
|
6
8
|
autoload :RestfulResponder, 'restfulie/server/action_controller/restful_responder'
|
7
9
|
autoload :Base, 'restfulie/server/action_controller/base'
|
8
10
|
end
|
@@ -25,7 +25,7 @@ module Restfulie
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def include_restfulie?
|
28
|
-
defined?(Restfulie::Server::ActionController::Base) &&
|
28
|
+
defined?(Restfulie::Server::ActionController::Base) && self.include?(Restfulie::Server::ActionController::Base)
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
@@ -45,9 +45,3 @@ module Restfulie
|
|
45
45
|
end
|
46
46
|
end
|
47
47
|
end
|
48
|
-
|
49
|
-
class ActionController::Base
|
50
|
-
def self.acts_as_restfulie
|
51
|
-
include Restfulie::Server::ActionController::Base
|
52
|
-
end
|
53
|
-
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
class Expires
|
2
|
+
def do_http_cache(responder)
|
3
|
+
if responder.options[:expires_in]
|
4
|
+
responder.controller.send :expires_in,responder.options[:expires_in]
|
5
|
+
true
|
6
|
+
else
|
7
|
+
false
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class LastModifieds
|
13
|
+
# default implementation that will check whether caching can be applied
|
14
|
+
def do_http_cache?(responder)
|
15
|
+
responder.resources.flatten.select do |resource|
|
16
|
+
resource.respond_to?(:updated_at)
|
17
|
+
end &&
|
18
|
+
responder.controller.response.last_modified.nil? &&
|
19
|
+
!new_record?(responder)
|
20
|
+
end
|
21
|
+
|
22
|
+
def do_http_cache(responder)
|
23
|
+
return false unless do_http_cache?(responder)
|
24
|
+
|
25
|
+
timestamp = responder.resources.flatten.select do |resource|
|
26
|
+
resource.respond_to?(:updated_at)
|
27
|
+
end.map do |resource|
|
28
|
+
(resource.updated_at || Time.now).utc
|
29
|
+
end.max
|
30
|
+
|
31
|
+
responder.controller.response.last_modified = timestamp if timestamp
|
32
|
+
end
|
33
|
+
|
34
|
+
def new_record?(responder)
|
35
|
+
responder.resource.respond_to?(:new_record?) && responder.resource.new_record?
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
module Restfulie
|
41
|
+
module Server
|
42
|
+
module ActionController
|
43
|
+
module CacheableResponder
|
44
|
+
|
45
|
+
CACHES = [::Expires.new, ::LastModifieds.new]
|
46
|
+
|
47
|
+
def to_format
|
48
|
+
cached = CACHES.inject(false) do |cached, cache|
|
49
|
+
cached || cache.do_http_cache(self)
|
50
|
+
end
|
51
|
+
if ::ActionController::Base.perform_caching && !cached.nil?
|
52
|
+
set_public_cache_control!
|
53
|
+
head :not_modified if fresh = request.fresh?(controller.response)
|
54
|
+
fresh
|
55
|
+
else
|
56
|
+
super
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def set_public_cache_control!
|
61
|
+
cache_control = controller.response.headers["Cache-Control"].split(",").map {|k| k.strip }
|
62
|
+
cache_control.delete("private")
|
63
|
+
cache_control.delete("no-cache")
|
64
|
+
cache_control << "public"
|
65
|
+
controller.response.headers["Cache-Control"] = cache_control.join(', ')
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Restfulie
|
2
|
+
module Server
|
3
|
+
module ActionController
|
4
|
+
|
5
|
+
# Adds support to answering as a 201 when the resource has been just created
|
6
|
+
module CreatedResponder
|
7
|
+
|
8
|
+
def to_format
|
9
|
+
if [201, :created].include? options[:status]
|
10
|
+
head :status => 201, :location => controller.url_for(resource)
|
11
|
+
else
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -2,45 +2,10 @@ module Restfulie
|
|
2
2
|
module Server
|
3
3
|
module ActionController
|
4
4
|
class RestfulResponder < ::ActionController::Responder
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
protected
|
11
|
-
def do_http_cache!
|
12
|
-
timestamp = resources.flatten.select do |resource|
|
13
|
-
resource.respond_to?(:updated_at)
|
14
|
-
end.map do |resource|
|
15
|
-
(resource.updated_at || Time.now).utc
|
16
|
-
end.max
|
17
|
-
|
18
|
-
controller.response.last_modified = timestamp if timestamp
|
19
|
-
set_public_cache_control!
|
20
|
-
|
21
|
-
head :not_modified if fresh = request.fresh?(controller.response)
|
22
|
-
fresh
|
23
|
-
end
|
24
|
-
|
25
|
-
def do_http_cache?
|
26
|
-
get? &&
|
27
|
-
@http_cache != false &&
|
28
|
-
::ActionController::Base.perform_caching &&
|
29
|
-
!new_record? &&
|
30
|
-
controller.response.last_modified.nil?
|
31
|
-
end
|
32
|
-
|
33
|
-
def new_record?
|
34
|
-
resource.respond_to?(:new_record?) && resource.new_record?
|
35
|
-
end
|
36
|
-
|
37
|
-
def set_public_cache_control!
|
38
|
-
cache_control = controller.response.headers["Cache-Control"].split(",").map {|k| k.strip }
|
39
|
-
cache_control.delete("private")
|
40
|
-
cache_control.delete("no-cache")
|
41
|
-
cache_control << "public"
|
42
|
-
controller.response.headers["Cache-Control"] = cache_control.join(', ')
|
43
|
-
end
|
5
|
+
|
6
|
+
include CacheableResponder
|
7
|
+
include CreatedResponder
|
8
|
+
|
44
9
|
end
|
45
10
|
end
|
46
11
|
end
|
@@ -11,10 +11,11 @@ module Restfulie
|
|
11
11
|
"extend Restfulie::Server::ActionView::Helpers; " +
|
12
12
|
"code_block = lambda { #{template.source} };" +
|
13
13
|
"builder = code_block.call; " +
|
14
|
+
"self.response.headers['Link'] = Restfulie::Common::Representation::Links.extract_link_header(builder.links) if builder.respond_to?(:links); " +
|
14
15
|
"builder.to_s"
|
15
16
|
end
|
16
17
|
end
|
17
18
|
end
|
18
19
|
end
|
19
20
|
end
|
20
|
-
end
|
21
|
+
end
|
@@ -12,8 +12,9 @@ class Array
|
|
12
12
|
feed = Restfulie::Common::Representation::Atom::Feed.new
|
13
13
|
# TODO: Define better feed attributes
|
14
14
|
# Array#to_s can return a very long string
|
15
|
-
feed.title
|
16
|
-
feed.updated
|
15
|
+
feed.title = "Collection of #{map {|i| i.class.name }.uniq.to_sentence}"
|
16
|
+
feed.updated = updated_at
|
17
|
+
feed.published = published_at
|
17
18
|
# TODO: this id does not comply with Rest standards yet
|
18
19
|
feed.id = hash
|
19
20
|
|
@@ -43,4 +44,8 @@ class Array
|
|
43
44
|
def updated_at(field = :updated_at)
|
44
45
|
map { |item| item.send(field) if item.respond_to?(field) }.compact.max || Time.now
|
45
46
|
end
|
47
|
+
|
48
|
+
def published_at(field = :published_at)
|
49
|
+
map { |item| item.send(field) if item.respond_to?(field) }.compact.min || Time.now
|
50
|
+
end
|
46
51
|
end
|
data/lib/restfulie/version.rb
CHANGED
metadata
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: restfulie
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
+
hash: 57
|
4
5
|
prerelease: false
|
5
6
|
segments:
|
6
7
|
- 0
|
7
|
-
-
|
8
|
+
- 9
|
8
9
|
- 1
|
9
|
-
version: 0.
|
10
|
+
version: 0.9.1
|
10
11
|
platform: ruby
|
11
12
|
authors:
|
12
13
|
- Guilherme Silveira, Caue Guerra, Luis Cipriani, Everton Ribeiro, George Guimaraes, Paulo Ahagon
|
@@ -14,16 +15,18 @@ autorequire:
|
|
14
15
|
bindir: bin
|
15
16
|
cert_chain: []
|
16
17
|
|
17
|
-
date: 2010-
|
18
|
+
date: 2010-08-22 00:00:00 -03:00
|
18
19
|
default_executable:
|
19
20
|
dependencies:
|
20
21
|
- !ruby/object:Gem::Dependency
|
21
22
|
name: nokogiri
|
22
23
|
prerelease: false
|
23
24
|
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
24
26
|
requirements:
|
25
27
|
- - ">="
|
26
28
|
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
27
30
|
segments:
|
28
31
|
- 1
|
29
32
|
- 4
|
@@ -35,9 +38,11 @@ dependencies:
|
|
35
38
|
name: actionpack
|
36
39
|
prerelease: false
|
37
40
|
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
38
42
|
requirements:
|
39
43
|
- - ">="
|
40
44
|
- !ruby/object:Gem::Version
|
45
|
+
hash: 7
|
41
46
|
segments:
|
42
47
|
- 2
|
43
48
|
- 3
|
@@ -49,9 +54,11 @@ dependencies:
|
|
49
54
|
name: activesupport
|
50
55
|
prerelease: false
|
51
56
|
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
52
58
|
requirements:
|
53
59
|
- - ">="
|
54
60
|
- !ruby/object:Gem::Version
|
61
|
+
hash: 7
|
55
62
|
segments:
|
56
63
|
- 2
|
57
64
|
- 3
|
@@ -63,9 +70,11 @@ dependencies:
|
|
63
70
|
name: responders_backport
|
64
71
|
prerelease: false
|
65
72
|
requirement: &id004 !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
66
74
|
requirements:
|
67
75
|
- - ~>
|
68
76
|
- !ruby/object:Gem::Version
|
77
|
+
hash: 27
|
69
78
|
segments:
|
70
79
|
- 0
|
71
80
|
- 1
|
@@ -77,9 +86,11 @@ dependencies:
|
|
77
86
|
name: json_pure
|
78
87
|
prerelease: false
|
79
88
|
requirement: &id005 !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
80
90
|
requirements:
|
81
91
|
- - ">="
|
82
92
|
- !ruby/object:Gem::Version
|
93
|
+
hash: 23
|
83
94
|
segments:
|
84
95
|
- 1
|
85
96
|
- 2
|
@@ -97,6 +108,11 @@ extra_rdoc_files: []
|
|
97
108
|
|
98
109
|
files:
|
99
110
|
- lib/restfulie/client/base.rb
|
111
|
+
- lib/restfulie/client/cache/basic.rb
|
112
|
+
- lib/restfulie/client/cache/fake.rb
|
113
|
+
- lib/restfulie/client/cache/http_ext.rb
|
114
|
+
- lib/restfulie/client/cache/restrictions.rb
|
115
|
+
- lib/restfulie/client/cache.rb
|
100
116
|
- lib/restfulie/client/configuration.rb
|
101
117
|
- lib/restfulie/client/entry_point.rb
|
102
118
|
- lib/restfulie/client/ext/atom_ext.rb
|
@@ -168,10 +184,12 @@ files:
|
|
168
184
|
- lib/restfulie/common/representation/json/link.rb
|
169
185
|
- lib/restfulie/common/representation/json/link_collection.rb
|
170
186
|
- lib/restfulie/common/representation/json.rb
|
171
|
-
- lib/restfulie/common/representation/
|
187
|
+
- lib/restfulie/common/representation/links.rb
|
172
188
|
- lib/restfulie/common/representation.rb
|
173
189
|
- lib/restfulie/common.rb
|
174
190
|
- lib/restfulie/server/action_controller/base.rb
|
191
|
+
- lib/restfulie/server/action_controller/cacheable_responder.rb
|
192
|
+
- lib/restfulie/server/action_controller/created_responder.rb
|
175
193
|
- lib/restfulie/server/action_controller/params_parser.rb
|
176
194
|
- lib/restfulie/server/action_controller/patch.rb
|
177
195
|
- lib/restfulie/server/action_controller/restful_responder.rb
|
@@ -202,23 +220,27 @@ rdoc_options: []
|
|
202
220
|
require_paths:
|
203
221
|
- lib
|
204
222
|
required_ruby_version: !ruby/object:Gem::Requirement
|
223
|
+
none: false
|
205
224
|
requirements:
|
206
225
|
- - ">="
|
207
226
|
- !ruby/object:Gem::Version
|
227
|
+
hash: 3
|
208
228
|
segments:
|
209
229
|
- 0
|
210
230
|
version: "0"
|
211
231
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
232
|
+
none: false
|
212
233
|
requirements:
|
213
234
|
- - ">="
|
214
235
|
- !ruby/object:Gem::Version
|
236
|
+
hash: 3
|
215
237
|
segments:
|
216
238
|
- 0
|
217
239
|
version: "0"
|
218
240
|
requirements: []
|
219
241
|
|
220
242
|
rubyforge_project:
|
221
|
-
rubygems_version: 1.3.
|
243
|
+
rubygems_version: 1.3.7
|
222
244
|
signing_key:
|
223
245
|
specification_version: 3
|
224
246
|
summary: Hypermedia aware resource based library in ruby (client side) and ruby on rails (server side).
|
@@ -1,26 +0,0 @@
|
|
1
|
-
module Restfulie
|
2
|
-
module Common
|
3
|
-
module Representation
|
4
|
-
# Implements the interface for marshal Xml media type requests (application/xml)
|
5
|
-
class XmlD
|
6
|
-
cattr_reader :media_type_name
|
7
|
-
@@media_type_name = 'application/xml'
|
8
|
-
|
9
|
-
cattr_reader :headers
|
10
|
-
@@headers = {
|
11
|
-
:post => { 'Content-Type' => media_type_name }
|
12
|
-
}
|
13
|
-
|
14
|
-
def unmarshal(string)
|
15
|
-
Hash.from_xml(string)
|
16
|
-
end
|
17
|
-
|
18
|
-
def marshal(entity, rel)
|
19
|
-
return entity if entity.kind_of? String
|
20
|
-
return entity.values.first.to_xml(:root => entity.keys.first) if entity.kind_of?(Hash) && entity.size==1
|
21
|
-
entity.to_xml
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|