restfulie 0.5.0 → 0.6.0

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.
@@ -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]