restfulie 0.8.1 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
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