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 CHANGED
@@ -23,4 +23,6 @@ group :test do
23
23
  gem "sinatra"
24
24
  gem "state_machine"
25
25
  gem "test-unit", "= 1.2.3"
26
+ gem "rails", "= 2.3.5"
27
+ gem "fakeweb"
26
28
  end
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.caelum.com.br":http://restfulie.caelum.com.br
4
- Mikyung's, a REST agent framework built on top of Restfulie can be seen at "http://github.com/caelum/mikyung":http://github.com/caelum/mikyung.
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. Download source example
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/mikyung using Restfulie and Mikyung, according to the Rest Architecture Maturity Model.
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
- namespace :test do
38
- def execute_process(name)
39
- sh "ruby ./spec/units/client/#{name}.rb &"
40
- sleep 15
41
- %x(ps -ef | grep #{name}).split[1]
42
- end
43
- def process(name)
44
- %x(ps -ef | grep #{name} | grep -v grep).split[1] || execute_process(name)
45
- end
46
- def start_server_and_invoke_test(task_name)
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
- sh "kill -9 #{pid}"
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
- begin
160
+ optionally do
128
161
  require 'yard'
129
162
  YARD::Rake::YardocTask.new do |t|
130
- t.files = ['lib/restfulie/**/*.rb', 'README.textile'] # optional
131
- # t.options = ['--any', '--extra', '--opts'] # optional
163
+ t.files = ['lib/restfulie/**/*.rb', 'README.textile']
132
164
  end
133
- rescue LoadError; end
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
@@ -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
@@ -3,6 +3,7 @@ module Restfulie
3
3
  module HTTP
4
4
  module LinkRequestBuilder
5
5
  include RequestMarshaller
6
+ include RequestFollow
6
7
 
7
8
  def path#:nodoc:
8
9
  at(href)
@@ -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
- response = ResponseHandler.handle(method, path, get_connection_provider.send(method, path, *args))
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 :XmlD, 'restfulie/common/representation/xml'
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")
@@ -0,0 +1,11 @@
1
+ module Restfulie
2
+ module Common
3
+ module Representation
4
+ module Links
5
+ def self.extract_link_header(links)
6
+ links.collect {|link| "<#{link.href}>; rel=#{link.rel}"}.join(', ')
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -12,3 +12,9 @@ end
12
12
 
13
13
  require 'restfulie/server/core_ext'
14
14
  Restfulie::Server::ActionView::TemplateHandlers.activate!
15
+
16
+ class ActionController::Base
17
+ def self.restfulie
18
+ include Restfulie::Server::ActionController::Base
19
+ end
20
+ end
@@ -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) && controller_class_name.constantize.include?(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
- def to_format
6
- return if do_http_cache? && do_http_cache!
7
- super
8
- end
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 = "Collection of #{map {|i| i.class.name }.uniq.to_sentence}"
16
- feed.updated = updated_at
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
@@ -1,7 +1,7 @@
1
1
  module Restfulie
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 8
4
+ MINOR = 9
5
5
  TINY = 1
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
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
8
+ - 9
8
9
  - 1
9
- version: 0.8.1
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-06-03 00:00:00 -03:00
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/xml.rb
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.6
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