restfulie 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -5,7 +5,7 @@ require 'rake/gempackagetask'
5
5
  require 'spec/rake/spectask'
6
6
 
7
7
  GEM = "restfulie"
8
- GEM_VERSION = "0.5.0"
8
+ GEM_VERSION = "0.6.0"
9
9
  SUMMARY = "Hypermedia aware resource based library in ruby (client side) and ruby on rails (server side)."
10
10
  AUTHOR = "Guilherme Silveira, Caue Guerra"
11
11
  EMAIL = "guilherme.silveira@caelum.com.br"
@@ -18,7 +18,7 @@ spec = Gem::Specification.new do |s|
18
18
  s.summary = SUMMARY
19
19
  s.require_paths = ['lib']
20
20
  s.files = FileList['lib/**/*.rb', '[A-Z]*'].to_a
21
- # s.add_dependency("ratom", [">= 0.6.3"])
21
+ s.add_dependency("ratom", [">= 0.6.3"])
22
22
  # s.add_dependency("jeokkarak", [">= 1.0.3"])
23
23
 
24
24
  # s.add_dependency(%q<rubigen>, [">= 1.3.4"])
@@ -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
  # TODO break media type registering for DECODING and ENCODING appart, so we can have two files
@@ -23,7 +40,7 @@ module Restfulie
23
40
  # retrieves the nth element from an atom feed
24
41
  def [](position)
25
42
 
26
- hash = entry[position].content.hash
43
+ hash = entry[position].content.internal_hash
27
44
  hash = hash.dup
28
45
  hash.delete("type")
29
46
  result = Restfulie::MediaType::DefaultMediaTypeDecoder.from_hash(hash)
@@ -36,7 +53,7 @@ module Restfulie
36
53
  private
37
54
 
38
55
  def add_links_to(result, entry)
39
- links = entry.link.hash
56
+ links = entry.link.internal_hash
40
57
  links = [links] if links.kind_of? Hash
41
58
  self_definition = self_from(links)
42
59
  links << {:rel => "destroy", :method => "delete", :href => self_definition["href"]} unless self_definition.nil?
@@ -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
  class Hash
2
19
  def to_object(body)
3
20
  if keys.length>1
@@ -15,46 +32,34 @@ module Restfulie
15
32
 
16
33
  # will execute some action in a specific URI
17
34
  def self.at(uri)
18
- Client::RequestExecution.new(nil).at uri
35
+ Client::RequestExecution.new(nil, nil).at uri
19
36
  end
20
-
37
+
21
38
  module Client
22
- module Base
39
+ module Config
40
+ BASIC_MAPPING = { :delete => Net::HTTP::Delete, :put => Net::HTTP::Put, :get => Net::HTTP::Get, :post => Net::HTTP::Post}
41
+ DEFAULTS = { :destroy => Net::HTTP::Delete, :delete => Net::HTTP::Delete, :cancel => Net::HTTP::Delete,
42
+ :refresh => Net::HTTP::Get, :reload => Net::HTTP::Get, :show => Net::HTTP::Get, :latest => Net::HTTP::Get, :self => Net::HTTP::Get}
23
43
 
24
- SELF_RETRIEVAL = [:latest, :refresh, :reload]
25
-
26
- # translates a response to an object
27
- def from_response(res, invoking_object)
28
-
29
- return invoking_object if res.code=="304"
30
-
31
- raise UnsupportedContentType.new("unsupported content type '#{res.content_type}' '#{res.code}'") unless res.content_type=="application/xml"
32
-
33
- # TODO this method should use the RequestExecution process to parse the content type and body
34
- # TODO add default html parser: do nothin
35
-
36
- body = res.body
37
- return {} if body.empty?
38
-
39
- hash = Hash.from_xml body
40
- hash.to_object(body)
41
-
44
+ def self.self_retrieval
45
+ [:latest, :refresh, :reload, :self]
46
+ end
47
+
48
+ def self.requisition_method_for(overriden_option,name)
49
+ return BASIC_MAPPING[overriden_option.to_sym] if overriden_option
50
+ DEFAULTS[name.to_sym] || Net::HTTP::Post
42
51
  end
43
52
 
44
- def requisition_method_for(overriden_option,name)
45
- basic_mapping = { :delete => Net::HTTP::Delete, :put => Net::HTTP::Put, :get => Net::HTTP::Get, :post => Net::HTTP::Post}
46
- defaults = {:destroy => Net::HTTP::Delete, :delete => Net::HTTP::Delete, :cancel => Net::HTTP::Delete,
47
- :refresh => Net::HTTP::Get, :reload => Net::HTTP::Get, :show => Net::HTTP::Get, :latest => Net::HTTP::Get, :self => Net::HTTP::Get}
53
+ end
54
+
55
+ module Base
48
56
 
49
- return basic_mapping[overriden_option.to_sym] if overriden_option
50
- defaults[name.to_sym] || Net::HTTP::Post
51
- end
52
-
53
57
  def is_self_retrieval?(name)
54
58
  name = name.to_sym if name.kind_of? String
55
- SELF_RETRIEVAL.include? name
59
+ Restfulie::Client::Config.self_retrieval.include? name
56
60
  end
57
-
61
+
58
62
  end
63
+
59
64
  end
60
65
  end
@@ -0,0 +1,103 @@
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
+ # Basic cache implementation for restfulie.
19
+ #
20
+ # It uses the request headers and uri to store it in memory.
21
+ # This cache might not be optimal for long running clients, which should use a memcached based one.
22
+ # Use Restfulie.cache_provider to change the provider
23
+ class Restfulie::BasicCache
24
+
25
+ def put(url, req, response)
26
+ if Restfulie::Cache::Restrictions.may_cache?(req, response)
27
+ Restfulie.logger.debug "caching #{url} #{req} #{response}"
28
+ cache[key_for(url, req)] = response
29
+ end
30
+ response
31
+ end
32
+
33
+ def get(url, req)
34
+
35
+ response = cache[key_for(url, req)]
36
+ return nil if response.nil?
37
+
38
+ if response.has_expired_cache?
39
+ remove(key_for(url, req))
40
+ else
41
+ Restfulie.logger.debug "RETURNING cache #{url} #{req}"
42
+ response
43
+ end
44
+
45
+ end
46
+
47
+ # removes all elements from the cache
48
+ def clear
49
+ cache.clear
50
+ end
51
+
52
+ private
53
+
54
+ def remove(what)
55
+ @cache.delete(what)
56
+ nil
57
+ end
58
+
59
+ def cache
60
+ @cache ||= {}
61
+ end
62
+
63
+ def key_for(url, req)
64
+ [url, req.class]
65
+ end
66
+
67
+ end
68
+
69
+ # Fake cache that does not cache anything
70
+ # Use Restfulie.cache_provider = Restfulie::FakeCache.new
71
+ class Restfulie::FakeCache
72
+
73
+ def put(url, req, response)
74
+ response
75
+ end
76
+
77
+ def get(url, req)
78
+ end
79
+
80
+ def clear
81
+ end
82
+
83
+ end
84
+
85
+ module Restfulie::Cache
86
+ module Restrictions
87
+
88
+ class << self
89
+
90
+ # checks whether this request verb and its cache headers allow caching
91
+ def may_cache?(request,response)
92
+ may_cache_method?(request) && response.may_cache?
93
+ end
94
+
95
+ # only Post and Get requests are cacheable so far
96
+ def may_cache_method?(verb)
97
+ verb.kind_of?(Net::HTTP::Post) || verb.kind_of?(Net::HTTP::Get)
98
+ end
99
+
100
+ end
101
+
102
+ end
103
+ 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
  module Client
3
20
  module Base
@@ -22,12 +39,12 @@ module Restfulie
22
39
 
23
40
  # retrieves a resource form a specific uri
24
41
  def from_web(uri, options = {})
25
- RequestExecution.new(self).at(uri).get(options)
42
+ RequestExecution.new(self, nil).at(uri).get(options)
26
43
  end
27
44
 
28
45
  private
29
46
  def remote_post(content)
30
- RequestExecution.new(self).at(entry_point_for.create.uri).post(content)
47
+ RequestExecution.new(self, nil).at(entry_point_for.create.uri).post(content)
31
48
  end
32
49
 
33
50
  end
@@ -0,0 +1,116 @@
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 'time'
19
+
20
+ # an extesion to http responses
21
+ module Restfulie::Client::HTTPResponse
22
+
23
+ attr_accessor :previous
24
+
25
+ # determines if this response code was successful (according to http specs: 200~299)
26
+ def is_successful?
27
+ code.to_i >= 200 && code.to_i <= 299
28
+ end
29
+
30
+ # determines if this response code was successful (according to http specs: 100~199)
31
+ def is_informational?
32
+ code.to_i >= 100 && code.to_i <= 199
33
+ end
34
+
35
+ # determines if this response code was successful (according to http specs: 300~399)
36
+ def is_redirection?
37
+ code.to_i >= 300 && code.to_i <= 399
38
+ end
39
+
40
+ # determines if this response code was successful (according to http specs: 400~499)
41
+ def is_client_error?
42
+ code.to_i >= 400 && code.to_i <= 499
43
+ end
44
+
45
+ # determines if this response code was successful (according to http specs: 500~599)
46
+ def is_server_error?
47
+ code.to_i >= 500 && code.to_i <= 599
48
+ end
49
+
50
+ def etag
51
+ self['Etag']
52
+ end
53
+
54
+ def last_modified
55
+ self['Last-Modified']
56
+ end
57
+
58
+ end
59
+
60
+ module Restfulie::Client::HTTPResponse
61
+
62
+ def cache_max_age
63
+ val = header_value_from('Cache-control', /^\s*max-age=(\d+)/)
64
+ if val
65
+ val.to_i
66
+ else
67
+ 0
68
+ end
69
+ end
70
+
71
+ def header_value_from(header, expression)
72
+ h = value_for(get_fields(header)[0], expression)
73
+ return nil if h.nil?
74
+ h.match(expression)[1]
75
+ end
76
+
77
+ def has_expired_cache?
78
+ return true if self['Date'].nil?
79
+ Time.now > Time.rfc2822(self['Date']) + cache_max_age.seconds
80
+ end
81
+
82
+ # checks if the header's max-age is available and no no-store if available.
83
+ def may_cache?
84
+ may_cache_field?(get_fields('Cache-control'))
85
+ end
86
+
87
+ def may_cache_field?(field)
88
+ return false if field.nil?
89
+
90
+ if field.kind_of? Array
91
+ field.each do |f|
92
+ return false if !may_cache_field?(f)
93
+ end
94
+ return true
95
+ end
96
+
97
+ max_age_header = value_for(field, /^max-age=(\d+)/)
98
+ return false if max_age_header.nil?
99
+ max_age = max_age_header[1]
100
+
101
+ return false if value_for(field, /^no-store/)
102
+
103
+ true
104
+ end
105
+
106
+ # extracts the header value for an specific expression, which can be located at the start or in the middle
107
+ # of the expression
108
+ def value_for(value, expression)
109
+ value.split(",").find { |obj| obj.strip =~ expression }
110
+ end
111
+
112
+ end
113
+
114
+ class Net::HTTPResponse
115
+ include Restfulie::Client::HTTPResponse
116
+ 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
  module Client
3
20
  module Helper
@@ -1,134 +1,158 @@
1
- module Restfulie
2
- module Client
3
- module Instance
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
+ module Restfulie::Client::Instance
4
19
 
5
- # list of possible states to access
6
- def _possible_states
7
- @_possible_states ||= {}
8
- end
9
-
10
- # which content-type generated this data
11
- attr_accessor :_came_from
12
-
13
- def invoke_remote_transition(name, options, block)
14
-
15
- method = self.class.requisition_method_for options[:method], name
16
-
17
- state = self._possible_states[name]
18
- url = URI.parse(state["href"] || state[:href])
19
- req = method.new(url.path)
20
- req.body = options[:data] if options[:data]
21
- add_request_headers(req, name)
22
-
23
- response = Net::HTTP.new(url.host, url.port).request(req)
20
+ # list of possible states to access
21
+ def existing_relations
22
+ @existing_relations ||= {}
23
+ end
24
24
 
25
- return block.call(response) if block
26
- return response unless method == Net::HTTP::Get
27
- self.class.from_response response, self
28
- end
29
-
30
- private
31
- def add_request_headers(req, name)
32
- req.add_field("Accept", "application/xml") if self._came_from == :xml
33
- req.add_field("If-None-Match", self.etag) if self.class.is_self_retrieval?(name) && self.respond_to?(:etag)
34
- req.add_field("If-Modified-Since", self.last_modified) if self.class.is_self_retrieval?(name) && self.respond_to?(:last_modified)
25
+ # which content-type generated this data
26
+ attr_reader :_came_from
27
+
28
+ # prepares a new request
29
+ def request
30
+ Restfulie::Client::RequestExecution.new(self.class, self)
31
+ end
32
+
33
+ # parse arguments from a transition invocation (or relation)
34
+ # it will receive either zero, one or two args, if there are two args, return them
35
+ # if there is one hash arg, its the options, add a data = nil
36
+ # if there is one arg (not a hash), its the data, add a options = {}
37
+ # if there are no args, data is nil and options = {}
38
+ def parse_args_from_transition(args)
39
+ data = nil
40
+ if args.nil? || args.size==0
41
+ options = {}
42
+ elsif args.size==1
43
+ if args[0].kind_of?(Hash)
44
+ options = args[0]
45
+ else
46
+ data = args[0]
47
+ options = {}
35
48
  end
49
+ elsif args.size==2
50
+ data = args[0]
51
+ options = args[1] || {}
52
+ end
53
+ [data, options]
54
+ end
55
+
56
+ def invoke_remote_transition(name, args, block = nil)
57
+
58
+ data, options = parse_args_from_transition(args)
59
+
60
+ method = Restfulie::Client::Config.requisition_method_for options[:method], name
61
+ state = self.existing_relations[name]
62
+
63
+ request = Restfulie::Client::RequestExecution.new(self.class, self).at(state["href"] || state[:href]).with(options[:headers])
64
+ request.do method, name, data
36
65
 
37
- public
66
+ end
38
67
 
39
-
40
- # inserts all links from this object as can_xxx and xxx methods
41
- def add_transitions(links)
42
-
43
- links.each do |t|
44
- self._possible_states[t["rel"] || t[:rel]] = t
45
- self.add_state(t)
46
- end
47
- self.extend Restfulie::Client::State
48
- end
68
+ # inserts all links from this object as can_xxx and xxx methods
69
+ def add_transitions(links)
70
+ links.each do |t|
71
+ self.existing_relations[t["rel"] || t[:rel]] = t
72
+ self.add_state(t)
73
+ end
74
+ self.extend Restfulie::Client::State
75
+ end
49
76
 
77
+ # adds the specific information for one state change or related resource
78
+ def add_state(transition)
79
+ name = transition["rel"] || transition[:rel]
80
+
81
+ # TODO: wrong, should be instance_eval
82
+ self.class.module_eval do
50
83
 
51
- def add_state(transition)
52
- name = transition["rel"] || transition[:rel]
53
-
54
- # TODO: wrong, should be instance_eval
55
- self.class.module_eval do
56
-
57
- def temp_method(options = {}, &block)
58
- self.invoke_remote_transition(Restfulie::Client::Helper.current_method, options, block)
59
- end
60
-
61
- alias_method name, :temp_method
62
- undef :temp_method
63
- end
64
- end
65
-
66
- # returns a list of extended fields for this instance.
67
- # extended fields are those unknown to this model but kept in a hash
68
- # to allow forward-compatibility.
69
- def extended_fields
70
- @hash ||= {}
71
- @hash
84
+ def temp_method(*args, &block)
85
+ self.invoke_remote_transition(Restfulie::Client::Helper.current_method, args, block)
72
86
  end
87
+
88
+ alias_method name, :temp_method
89
+ undef :temp_method
90
+ end
91
+ end
92
+
93
+ # returns a list of extended fields for this instance.
94
+ # extended fields are those unknown to this model but kept in a hash
95
+ # to allow forward-compatibility.
96
+ def extended_fields
97
+ @extended_fields ||= {}
98
+ @extended_fields
99
+ end
73
100
 
74
- def method_missing(name, *args)
75
- name = name.to_s if name.kind_of? Symbol
76
-
77
- if name[-1,1] == "="
78
- extended_fields[name.chop] = args[0]
79
- elsif name[-1,1] == "?"
80
- found = extended_fields[name.chop]
81
- return super(name,args) if found.nil?
82
- parse(found)
83
- else
84
- found = extended_fields[name]
85
- return super(name,args) if found.nil?
86
- parse(transform(found))
87
- end
101
+ def method_missing(name, *args)
102
+ name = name.to_s if name.kind_of? Symbol
103
+
104
+ if name[-1,1] == "="
105
+ extended_fields[name.chop] = args[0]
106
+ elsif name[-1,1] == "?"
107
+ found = extended_fields[name.chop]
108
+ return super(name,args) if found.nil?
109
+ parse(found)
110
+ else
111
+ found = extended_fields[name]
112
+ return super(name,args) if found.nil?
113
+ parse(transform(found))
114
+ end
88
115
 
89
- end
116
+ end
90
117
 
91
- # TODO test this guy
92
- def respond_to?(sym)
93
- extended_fields[sym.to_s].nil? ? super(sym) : true
94
- end
118
+ def respond_to?(sym)
119
+ extended_fields[sym.to_s].nil? ? super(sym) : true
120
+ end
95
121
 
96
- # redefines attribute definition allowing the invocation of method_missing
97
- # when an attribute does not exist
98
- def attributes=(values)
99
- values.each do |key, value|
100
- unless attributes.include? key
101
- method_missing("#{key}=", value)
102
- values.delete key
103
- end
104
- end
105
- super(values)
122
+ # redefines attribute definition allowing the invocation of method_missing
123
+ # when an attribute does not exist
124
+ def attributes=(values)
125
+ values.each do |key, value|
126
+ unless attributes.include? key
127
+ method_missing("#{key}=", value)
128
+ values.delete key
106
129
  end
130
+ end
131
+ super(values)
132
+ end
107
133
 
108
134
 
109
- # serializes the extended fields with the existing fields
110
- def to_xml(options={})
111
- super(options) do |xml|
112
- extended_fields.each do |key,value|
113
- xml.tag! key, value
114
- end
115
- end
135
+ # serializes the extended fields with the existing fields
136
+ def to_xml(options={})
137
+ super(options) do |xml|
138
+ extended_fields.each do |key,value|
139
+ xml.tag! key, value
116
140
  end
141
+ end
142
+ end
117
143
 
118
- private
119
-
120
- # transforms a value in a custom hash
121
- def transform(value)
122
- return CustomHash.new(value) if value.kind_of?(Hash) || value.kind_of?(Array)
123
- value
124
- end
125
-
126
- def parse(val)
127
- raise "undefined method: '#{val}'" if val.nil?
128
- val
129
- end
144
+ private
145
+
146
+ # transforms a value in a custom hash
147
+ def transform(value)
148
+ return CustomHash.new(value) if value.kind_of?(Hash) || value.kind_of?(Array)
149
+ value
150
+ end
151
+
152
+ def parse(val)
153
+ raise "undefined method: '#{val}'" if val.nil?
154
+ val
155
+ end
130
156
 
131
157
 
132
- end
133
- end
134
158
  end