restfulie 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,12 +1,183 @@
1
+ #
2
+ # Copyright (c) 2009 Caelum - www.caelum.com.br/opensource
3
+ # All rights reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'restfulie/client/extensions/http'
19
+
1
20
  module Restfulie
2
21
  module Client
22
+
23
+ # extension to all answers that allows you to access the web response
24
+ module WebResponse
25
+ attr_accessor :web_response
26
+ end
27
+
28
+ # some error ocurred while processing the request
29
+ class ResponseError < Exception
30
+ end
31
+
32
+ # Handles response answered by servers by accessing the specific callback for a http response code.
33
+ module ResponseHandler
34
+
35
+ class << self
36
+
37
+ # registers a new callback for a specific range of response codes
38
+ #
39
+ # Restfulie::Client::ResponseHandler.register(400, 599) do |restfulie_response|
40
+ # # returns the original reponse object
41
+ # restfulie_response.response
42
+ # end
43
+ def register(min_code, max_code, &block)
44
+ (min_code..max_code).each do |code|
45
+ handlers[code] = block
46
+ end
47
+ end
48
+
49
+ # callback that returns the http response object
50
+ def pure_response_return(restfulie_response)
51
+ restfulie_response.response
52
+ end
53
+
54
+ # callback that raises a ResponseError
55
+ def raise_error(restfulie_response)
56
+ raise Restfulie::Client::ResponseError.new(restfulie_response.response)
57
+ end
58
+
59
+ # callback that parses the response media type and body,
60
+ # returning a deserialized representation of the resource.
61
+ # note that this will also set the _came_from instance variable with the content type
62
+ # this resource was represented, in order to allow further requests to prefer this content type.
63
+ def parse_entity(restfulie_response)
64
+ response = restfulie_response.response
65
+ content_type = response.content_type
66
+ type = Restfulie::MediaType.type_for(content_type)
67
+ if content_type[-3,3]=="xml"
68
+ result = type.from_xml response.body
69
+ elsif content_type[-4,4]=="json"
70
+ result = type.from_json response.body
71
+ else
72
+ result = generic_parse_entity restfulie_response
73
+ end
74
+ result.instance_variable_set :@_came_from, content_type
75
+ result
76
+ end
77
+
78
+ # callback taht executes a GET request to the response Location header.
79
+ # this is the typical callback for 200 response codes.
80
+ def retrieve_resource_from_location(restfulie_response)
81
+ restfulie_response.type.from_web restfulie_response.response["Location"]
82
+ end
83
+
84
+ # given a restfulie response, extracts the response code and invoke the registered callback.
85
+ def handle(restfulie_response)
86
+ handlers[restfulie_response.response.code.to_i].call(restfulie_response)
87
+ end
88
+
89
+ def generic_parse_entity(restfulie_response)
90
+ response = restfulie_response.response
91
+ content_type = response.content_type
92
+ type = Restfulie::MediaType.type_for(content_type)
93
+ method = "from_#{content_type}".to_sym
94
+ raise Restfulie::UnsupportedContentType.new("unsupported content type '#{content_type}' because '#{type}.#{method.to_s}' was not found") unless type.respond_to? method
95
+ type.send(method, response.body)
96
+ end
97
+
98
+ private
99
+ def handlers
100
+ @handlers ||= {}
101
+ end
102
+
103
+ def register_func(min, max, proc)
104
+ register(min, max) { |r| proc.call(r) }
105
+ end
106
+
107
+ end
108
+
109
+ register_func( 100, 599, Proc.new{ |r| pure_response_return r} )
110
+ register_func( 200, 200, Proc.new{ |r| parse_entity r} )
111
+ register_func( 301, 301, Proc.new{ |r| retrieve_resource_from_location r} )
112
+
113
+ end
114
+
115
+
116
+ # TODO there should be a response for each method type
117
+ class Response
118
+
119
+ attr_reader :type, :response
120
+
121
+ def initialize(type, response)
122
+ @type = type
123
+ @response = response
124
+ end
125
+
126
+ # TODO remote_post can probably be moved, does not need to be on the object's class itself
127
+ # the expected_content_type is used in case a redirection takes place
128
+ def parse_post(expected_content_type)
129
+ code = @response.code
130
+ if code=="301" && @type.follows.moved_permanently? == :all
131
+ result = @type.remote_post_to(@response["Location"], @response.body)
132
+ enhance(result)
133
+ elsif code=="201"
134
+ Restfulie.at(@response["Location"]).accepts(expected_content_type).get
135
+ else
136
+ final_parse
137
+ end
138
+ end
139
+
140
+ # gets a result object and enhances it with web response methods
141
+ # by extending WebResponse and defining the attribute web_response
142
+ def enhance(result)
143
+ @response.previous = result.web_response if result.respond_to? :web_response
144
+ result.extend Restfulie::Client::WebResponse
145
+ result.web_response = @response
146
+ result
147
+ end
148
+
149
+ # parses this response using the correct ResponseHandler and enhances it
150
+ def final_parse
151
+ enhance Restfulie::Client::ResponseHandler.handle(@response)
152
+ end
153
+
154
+ # detects which type of method invocation it was and act accordingly
155
+ # TODO this should be called by RequestExcution, not instance
156
+ def parse(method, invoking_object, content_type)
157
+
158
+ return enhance(invoking_object) if @response.code == "304"
159
+
160
+ # return block.call(@response) if block
161
+
162
+ return final_parse if method == Net::HTTP::Get
163
+ return parse_post(content_type) if method == Net::HTTP::Post
164
+ final_parse
165
+
166
+ end
167
+
168
+ end
3
169
 
4
170
  class RequestExecution
5
171
 
6
172
  def initialize(type)
173
+ initialize(type, nil)
174
+ end
175
+
176
+ def initialize(type, invoking_object)
7
177
  @type = type
8
178
  @content_type = "application/xml"
9
179
  @accepts = "application/xml"
180
+ @invoking_object = invoking_object
10
181
  end
11
182
 
12
183
  def at(uri)
@@ -26,12 +197,51 @@ module Restfulie
26
197
  @accepts = content_type
27
198
  self
28
199
  end
200
+
201
+ def with(headers)
202
+ @headers = headers
203
+ self
204
+ end
29
205
 
30
206
  # asks to create this content on the server (post it)
31
207
  def create(content)
32
208
  post(content)
33
209
  end
210
+
211
+ # executes an http request using the specified verb
212
+ #
213
+ # example:
214
+ # do(Net::HTTP::Get, 'self', nil)
215
+ # do(Net::HTTP::Post, 'payment', '<payment/>')
216
+ def do(verb, relation_name, body = nil)
217
+ url, http_request = prepare_request(verb, relation_name, body)
218
+ response = execute_request(url, http_request)
219
+ Restfulie::Client::Response.new(@type, response).parse(verb, @invoking_object, "application/xml")
220
+ end
221
+
222
+ private
223
+ def execute_request(url, http_request)
224
+ cached = Restfulie.cache_provider.get(url, http_request)
225
+ return cached if cached
34
226
 
227
+ response = Net::HTTP.new(url.host, url.port).request(http_request)
228
+ Restfulie.cache_provider.put(url, http_request, response)
229
+ end
230
+
231
+ public
232
+
233
+ def prepare_request(verb, relation_name, body = nil)
234
+ url = URI.parse(@uri)
235
+ req = verb.new(url.path)
236
+ add_basic_request_headers(req, relation_name)
237
+
238
+ if body
239
+ req.body = body
240
+ req.add_field("Content-type", "application/xml") if req.get_fields("Content-type").nil?
241
+ end
242
+ [url, req]
243
+ end
244
+
35
245
  # post this content to the server
36
246
  def post(content)
37
247
  remote_post_to(@uri, content)
@@ -41,6 +251,48 @@ module Restfulie
41
251
  def get(options = {})
42
252
  from_web(@uri, options)
43
253
  end
254
+
255
+ def add_headers_to(hash)
256
+ hash[:headers] = {} unless hash[:headers]
257
+ hash[:headers]["Content-type"] = @content_type
258
+ hash[:headers]["Accept"] = @accepts
259
+ hash
260
+ end
261
+
262
+ def change_to_state(name, args)
263
+ if !args.empty? && args[args.size-1].kind_of?(Hash)
264
+ add_headers_to(args[args.size-1])
265
+ else
266
+ args << add_headers_to({})
267
+ end
268
+ @invoking_object.invoke_remote_transition name, args
269
+ end
270
+
271
+ # invokes an existing relation or delegates to the existing definition of method_missing
272
+ def method_missing(name, *args)
273
+ if @invoking_object && @invoking_object.existing_relations[name.to_s]
274
+ change_to_state(name.to_s, args)
275
+ else
276
+ super(name, args)
277
+ end
278
+ end
279
+
280
+ def add_basic_request_headers(req, name = nil)
281
+
282
+ req.add_field("Accept", @accepts) unless @accepts.nil?
283
+
284
+ @headers.each do |key, value|
285
+ req.add_field(key, value)
286
+ end if @headers
287
+
288
+ req.add_field("Accept", @invoking_object._came_from) if req.get_fields("Accept")==["*/*"]
289
+
290
+ if @type && name && @type.is_self_retrieval?(name) && @invoking_object.respond_to?(:web_response)
291
+ req.add_field("If-None-Match", @invoking_object.web_response.etag) if !@invoking_object.web_response.etag.nil?
292
+ req.add_field("If-Modified-Since", @invoking_object.web_response.last_modified) if !@invoking_object.web_response.last_modified.nil?
293
+ end
294
+
295
+ end
44
296
 
45
297
  private
46
298
  def remote_post_to(uri, content)
@@ -52,78 +304,16 @@ module Restfulie
52
304
  req.add_field("Content-type", @content_type)
53
305
 
54
306
  response = Net::HTTP.new(url.host, url.port).request(req)
55
- parse_post_response(response, content)
56
- end
57
-
58
- def parse_post_response(response, content)
59
- code = response.code
60
- if code=="301" && @type.follows.moved_permanently? == :all
61
- remote_post_to(response["Location"], content)
62
- elsif code=="201"
63
- from_web(response["Location"], "Accept" => "application/xml")
64
- else
65
- response
66
- end
307
+ Restfulie::Client::Response.new(@type, response).parse_post(@content_type)
67
308
  end
68
309
 
69
310
  def from_web(uri, options = {})
70
311
  uri = URI.parse(uri)
71
312
  req = Net::HTTP::Get.new(uri.path)
72
- options.each do |key,value| req[key] = value end
313
+ options.each { |key,value| req[key] = value }
73
314
  add_basic_request_headers(req)
74
-
75
315
  res = Net::HTTP.new(uri.host, uri.port).request(req)
76
- parse_get_response(res)
77
- end
78
-
79
- private
80
-
81
- def add_basic_request_headers(req)
82
- req.add_field("Accept", @accepts) unless @accepts.nil?
83
- end
84
-
85
- # parses a get response.
86
- # if the result code is 301, redirect
87
- # otherwise, parses an ok response
88
- def parse_get_response(res)
89
-
90
- code = res.code
91
- return from_web(res["Location"]) if code=="301"
92
- parse_get_ok_response(res, code)
93
-
94
- end
95
-
96
- # parses a successful get response.
97
- # parses the entity and add extra (response related) fields.
98
- def parse_get_ok_response(res, code)
99
- result = parse_get_entity(res, code)
100
- add_extra_fields(result, res)
101
- result
102
- end
103
-
104
- # add etag, last_modified and web_response fields to the resulting object
105
- def add_extra_fields(result,res)
106
- result.etag = res['Etag'] unless res['Etag'].nil?
107
- result.last_modified = res['Last-Modified'] unless res['Last-Modified'].nil?
108
- result.web_response = res
109
- end
110
-
111
- # returns an entity for a specific response
112
- def parse_get_entity(res, code)
113
- if code=="200"
114
- content_type = res.content_type
115
- type = Restfulie::MediaType.type_for(content_type)
116
- if content_type[-3,3]=="xml"
117
- result = type.from_xml res.body
118
- elsif content_type[-4,4]=="json"
119
- result = type.from_json res.body
120
- else
121
- raise Restfulie::UnsupportedContentType.new("unsupported content type '#{content_type}'")
122
- end
123
- result
124
- else
125
- res
126
- end
316
+ Restfulie::Client::Response.new(@type, res).final_parse
127
317
  end
128
318
 
129
319
  end
@@ -1,3 +1,20 @@
1
+ #
2
+ # Copyright (c) 2009 Caelum - www.caelum.com.br/opensource
3
+ # All rights reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
1
18
  module Restfulie
2
19
 
3
20
  module Client
@@ -12,7 +29,7 @@ module Restfulie
12
29
 
13
30
  # returns true if this resource has a state named name
14
31
  def has_state(name)
15
- !@_possible_states[name].nil?
32
+ !@existing_relations[name].nil?
16
33
  end
17
34
  end
18
35
  end
@@ -1,3 +1,22 @@
1
+ #
2
+ # Copyright (c) 2009 Caelum - www.caelum.com.br/opensource
3
+ # All rights reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'restfulie/logger'
19
+
1
20
  require 'net/http'
2
21
  require 'uri'
3
22
  require 'vendor/jeokkarak/jeokkarak'
@@ -10,9 +29,15 @@ require 'restfulie/client/helper'
10
29
  require 'restfulie/client/instance'
11
30
  require 'restfulie/client/request_execution'
12
31
  require 'restfulie/client/state'
32
+ require 'restfulie/client/cache'
33
+ require 'restfulie/unmarshalling'
13
34
 
14
35
  module Restfulie
15
36
 
37
+ class << self
38
+ attr_accessor :cache_provider
39
+ end
40
+
16
41
  # Extends your class to support restfulie-client side's code.
17
42
  # This will extends Restfulie::Client::Base methods as class methods,
18
43
  # Restfulie::Client::Instance as instance methods and Restfulie::Unmarshalling as class methods.
@@ -26,10 +51,10 @@ end
26
51
 
27
52
  Object.extend Restfulie
28
53
 
29
- require 'restfulie/unmarshalling'
54
+ include ActiveSupport::CoreExtensions::Hash
30
55
 
31
- module Hashi
32
- class CustomHash
56
+ class Hashi::CustomHash
33
57
  uses_restfulie
34
- end
35
58
  end
59
+
60
+ Restfulie.cache_provider = Restfulie::BasicCache.new
@@ -0,0 +1,13 @@
1
+ module Restfulie
2
+
3
+ # Configure the logger used by Restfulie
4
+ #
5
+ # The logger defaults to ActiveSupport::BufferedLogger.new(STDOUT)
6
+ class << self
7
+ attr_accessor :logger
8
+ end
9
+
10
+ end
11
+
12
+ Restfulie.logger = ActiveSupport::BufferedLogger.new(STDOUT)
13
+ Restfulie.logger.level = Logger::DEBUG
@@ -1,3 +1,20 @@
1
+ #
2
+ # Copyright (c) 2009 Caelum - www.caelum.com.br/opensource
3
+ # All rights reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
1
18
  require 'restfulie/media_type_control'
2
19
 
3
20
  module Restfulie
@@ -89,16 +106,10 @@ module Restfulie
89
106
 
90
107
  def self.from_hash(hash)
91
108
 
92
- # TODO atom media type from_xml on entry is not working correctly
93
- # raise "there should be only one root element but got #{hash.keys}" unless hash.keys.size==1
94
-
95
109
  type = Restfulie::MediaType.constantize(hash.keys.first.camelize) rescue Hashi
96
110
 
97
111
  result = type.from_hash hash.values.first
98
112
  return nil if result.nil?
99
- if result.respond_to? :_came_from=
100
- result._came_from = :xml
101
- end
102
113
  result
103
114
 
104
115
  end
@@ -108,6 +119,14 @@ module Restfulie
108
119
  type = hash.keys.first.camelize.constantize
109
120
  type.from_hash(hash.values.first)
110
121
  end
122
+
123
+ def self.from_html(body)
124
+ Hashi::CustomHash.new :body => body
125
+ end
126
+
127
+ def self.from_xhtml(body)
128
+ Hashi::CustomHash.new :body => body
129
+ end
111
130
 
112
131
  end
113
132
  end
@@ -1,3 +1,32 @@
1
+ #
2
+ # Copyright (c) 2009 Caelum - www.caelum.com.br/opensource
3
+ # All rights reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ # extends Rails mime type
18
+
19
+ module Mime
20
+ class << self
21
+ # alias_method :old_const_set, :const_set
22
+
23
+ # ignores setting the contest again
24
+ # def const_set(a,b)
25
+ # super(a,b) unless Mime.const_defined?(a)
26
+ # end
27
+ end
28
+ end
29
+
1
30
  module Restfulie
2
31
 
3
32
  module MediaTypeControl
@@ -31,12 +60,22 @@ module Restfulie
31
60
  end
32
61
 
33
62
 
63
+ def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false)
64
+ SET << Mime.const_get(symbol.to_s.upcase)
65
+
66
+ ([string] + mime_type_synonyms).each { |string| LOOKUP[string] = SET.last } unless skip_lookup
67
+ ([symbol.to_s] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext] = SET.last }
68
+ end
69
+
34
70
  module MediaType
35
71
 
36
72
  class << self
37
73
 
38
74
  def register(type)
39
- Mime::Type.register(type.name, type.short_name.to_sym)
75
+ silence_warnings do
76
+ Mime::Type.register(type.name, type.short_name.to_sym)
77
+ end
78
+
40
79
  media_types[type.name] = type
41
80
  end
42
81
 
@@ -1,11 +1,28 @@
1
+ #
2
+ # Copyright (c) 2009 Caelum - www.caelum.com.br/opensource
3
+ # All rights reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
1
18
  module Restfulie
2
19
 
3
20
  module MediaType
4
21
  def self.HtmlType
5
- custom_type('html', self, lambda {})
22
+ custom_type('html', DefaultMediaTypeDecoder, lambda {})
6
23
  end
7
24
  def self.TextHtmlType
8
- custom_type('text/html', self, lambda {})
25
+ custom_type('text/html', DefaultMediaTypeDecoder, lambda {})
9
26
  end
10
27
 
11
28
  # TODO rename it and move it
@@ -1,3 +1,20 @@
1
+ #
2
+ # Copyright (c) 2009 Caelum - www.caelum.com.br/opensource
3
+ # All rights reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
1
18
  module Restfulie
2
19
 
3
20
  module Server
@@ -69,11 +86,7 @@ class AtomFeed
69
86
  end
70
87
 
71
88
  def updated_at
72
- last = nil
73
- @feed.each do |item|
74
- last = item.updated_at if item.respond_to?(:updated_at) && (last.nil? || item.updated_at > last)
75
- end
76
- last || Time.now
89
+ @feed.updated_at
77
90
  end
78
91
 
79
92
  def items_to_atom_xml(controller, serializer = nil)
@@ -1,7 +1,24 @@
1
+ #
2
+ # Copyright (c) 2009 Caelum - www.caelum.com.br/opensource
3
+ # All rights reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
1
18
  module Restfulie
2
19
  module Server
3
20
  module Base
4
-
21
+
5
22
  # returns the definition for the transaction
6
23
  def existing_transition(name)
7
24
  transitions[name]