restfully 0.6.3 → 0.7.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/README.md +166 -0
  2. data/Rakefile +35 -35
  3. data/bin/restfully +68 -10
  4. data/lib/restfully.rb +8 -14
  5. data/lib/restfully/collection.rb +70 -90
  6. data/lib/restfully/error.rb +2 -0
  7. data/lib/restfully/http.rb +3 -3
  8. data/lib/restfully/http/error.rb +1 -20
  9. data/lib/restfully/http/helper.rb +49 -0
  10. data/lib/restfully/http/request.rb +60 -24
  11. data/lib/restfully/http/response.rb +55 -24
  12. data/lib/restfully/link.rb +32 -24
  13. data/lib/restfully/media_type.rb +70 -0
  14. data/lib/restfully/media_type/abstract_media_type.rb +162 -0
  15. data/lib/restfully/media_type/application_json.rb +21 -0
  16. data/lib/restfully/media_type/application_vnd_bonfire_xml.rb +177 -0
  17. data/lib/restfully/media_type/application_x_www_form_urlencoded.rb +33 -0
  18. data/lib/restfully/media_type/grid5000.rb +67 -0
  19. data/lib/restfully/media_type/wildcard.rb +27 -0
  20. data/lib/restfully/rack.rb +1 -0
  21. data/lib/restfully/rack/basic_auth.rb +26 -0
  22. data/lib/restfully/resource.rb +134 -197
  23. data/lib/restfully/session.rb +127 -70
  24. data/lib/restfully/version.rb +3 -0
  25. data/spec/fixtures/bonfire-collection-with-fragments.xml +6 -0
  26. data/spec/fixtures/bonfire-compute-existing.xml +43 -0
  27. data/spec/fixtures/bonfire-empty-collection.xml +4 -0
  28. data/spec/fixtures/bonfire-experiment-collection.xml +51 -0
  29. data/spec/fixtures/bonfire-network-collection.xml +35 -0
  30. data/spec/fixtures/bonfire-network-existing.xml +6 -0
  31. data/spec/fixtures/bonfire-root.xml +5 -0
  32. data/spec/fixtures/grid5000-rennes-jobs.json +988 -146
  33. data/spec/fixtures/grid5000-rennes.json +63 -0
  34. data/spec/restfully/collection_spec.rb +87 -0
  35. data/spec/restfully/http/helper_spec.rb +18 -0
  36. data/spec/restfully/http/request_spec.rb +97 -0
  37. data/spec/restfully/http/response_spec.rb +53 -0
  38. data/spec/restfully/link_spec.rb +80 -0
  39. data/spec/restfully/media_type/application_vnd_bonfire_xml_spec.rb +153 -0
  40. data/spec/restfully/media_type_spec.rb +117 -0
  41. data/spec/restfully/resource_spec.rb +109 -0
  42. data/spec/restfully/session_spec.rb +229 -0
  43. data/spec/spec_helper.rb +10 -9
  44. metadata +162 -83
  45. data/.document +0 -5
  46. data/CHANGELOG +0 -62
  47. data/README.rdoc +0 -146
  48. data/TODO.rdoc +0 -3
  49. data/VERSION +0 -1
  50. data/examples/grid5000.rb +0 -33
  51. data/examples/scratch.rb +0 -37
  52. data/lib/restfully/extensions.rb +0 -34
  53. data/lib/restfully/http/adapters/abstract_adapter.rb +0 -29
  54. data/lib/restfully/http/adapters/patron_adapter.rb +0 -16
  55. data/lib/restfully/http/adapters/rest_client_adapter.rb +0 -75
  56. data/lib/restfully/http/headers.rb +0 -20
  57. data/lib/restfully/parsing.rb +0 -66
  58. data/lib/restfully/special_array.rb +0 -5
  59. data/lib/restfully/special_hash.rb +0 -5
  60. data/restfully.gemspec +0 -114
  61. data/spec/collection_spec.rb +0 -120
  62. data/spec/fixtures/configuration_file.yml +0 -4
  63. data/spec/fixtures/grid5000-sites.json +0 -540
  64. data/spec/http/error_spec.rb +0 -18
  65. data/spec/http/headers_spec.rb +0 -17
  66. data/spec/http/request_spec.rb +0 -49
  67. data/spec/http/response_spec.rb +0 -19
  68. data/spec/http/rest_client_adapter_spec.rb +0 -35
  69. data/spec/link_spec.rb +0 -61
  70. data/spec/parsing_spec.rb +0 -40
  71. data/spec/resource_spec.rb +0 -320
  72. data/spec/restfully_spec.rb +0 -16
  73. data/spec/session_spec.rb +0 -171
@@ -1,4 +1,6 @@
1
1
 
2
2
  module Restfully
3
3
  class Error < StandardError; end
4
+ class NotImplemented < Error; end
5
+ class MethodNotAllowed < Error; end
4
6
  end
@@ -1,9 +1,9 @@
1
- require 'restfully/http/headers'
1
+ require 'restfully/http/helper'
2
2
  require 'restfully/http/error'
3
3
  require 'restfully/http/request'
4
4
  require 'restfully/http/response'
5
- require 'restfully/http/adapters/abstract_adapter'
5
+
6
6
  module Restfully
7
7
  module HTTP
8
8
  end
9
- end
9
+ end
@@ -1,25 +1,6 @@
1
1
  module Restfully
2
2
  module HTTP
3
- class Error < Restfully::Error
4
- STATUS_CODES = {
5
- 400 => "Bad Request",
6
- 401 => "Authorization Required",
7
- 403 => "Forbiden",
8
- 406 => "Not Acceptable"
9
- }
10
-
11
- attr_reader :response
12
- def initialize(response)
13
- @response = response
14
- response_body = response.body rescue response.raw_body
15
- if response_body.kind_of?(Hash)
16
- message = "#{response.status} #{response_body['title']}. #{response_body['message']}"
17
- else
18
- message = "#{response.status} #{STATUS_CODES[response.status] || (response_body[0..100]+"...")}"
19
- end
20
- super(message)
21
- end
22
- end
3
+ class Error < Restfully::Error; end
23
4
  class ClientError < Restfully::HTTP::Error; end
24
5
  class ServerError < Restfully::HTTP::Error; end
25
6
  end
@@ -0,0 +1,49 @@
1
+ module Restfully
2
+ module HTTP
3
+ module Helper
4
+
5
+ def sanitize_head(h = {})
6
+ sanitized_headers = {}
7
+ h.each do |key, value|
8
+ sanitized_key = key.to_s.
9
+ downcase.
10
+ gsub(/[_-]/, ' ').
11
+ split(' ').
12
+ map{|word| word.capitalize}.
13
+ join("-")
14
+ sanitized_value = case value
15
+ when Array
16
+ value.join(", ")
17
+ else
18
+ value
19
+ end
20
+ sanitized_headers[sanitized_key] = sanitized_value
21
+ end
22
+ sanitized_headers
23
+ end
24
+
25
+ def sanitize_query(h = {})
26
+ sanitized_query = {}
27
+ h.each do |key,value|
28
+ sanitized_query[key] = stringify(value)
29
+ end
30
+ sanitized_query
31
+ end
32
+
33
+ protected
34
+ def stringify(value)
35
+ case value
36
+ when Hash
37
+ h = {}
38
+ value.each{|k,v| h[k] = stringify(v)}
39
+ h
40
+ when Array
41
+ value.map!{|v| stringify(v)}
42
+ else
43
+ value.to_s
44
+ end
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -1,42 +1,78 @@
1
- require 'uri'
1
+
2
+
2
3
  module Restfully
3
4
  module HTTP
4
5
 
5
6
  class Request
6
- include Headers, Restfully::Parsing
7
- attr_reader :headers, :uri
8
- attr_accessor :retries
7
+ include Helper
9
8
 
10
- def initialize(url, options = {})
11
- options = options.symbolize_keys
12
- @uri = url.kind_of?(URI) ? url : URI.parse(url)
13
- @headers = sanitize_http_headers(options.delete(:headers) || {})
14
- if query = options.delete(:query)
15
- @uri.query = [@uri.query, query.to_params].compact.join("&")
9
+ attr_reader :method, :uri, :head, :body
10
+
11
+ def initialize(session, method, path, options)
12
+ @session = session
13
+
14
+ request = options.symbolize_keys
15
+ request[:method] = method
16
+
17
+ request[:head] = sanitize_head(@session.default_headers).merge(
18
+ build_head(request)
19
+ )
20
+
21
+ request[:uri] = @session.uri_to(path)
22
+ if request[:query]
23
+ request[:uri].query_values = sanitize_query(request[:query])
24
+ end
25
+
26
+ request[:body] = if [:post, :put].include?(request[:method])
27
+ build_body(request)
16
28
  end
17
- @body = options.delete(:body)
18
- @retries = 0
29
+
30
+ @method, @uri, @head, @body = request.values_at(
31
+ :method, :uri, :head, :body
32
+ )
19
33
  end
20
34
 
21
- def body
22
- if @body.kind_of?(String)
23
- @unserialized_body ||= unserialize(@body, :content_type => @headers['Content-Type'])
35
+ # Updates the request header and query parameters
36
+ # Returns nil if no changes were made, otherwise self.
37
+ def update!(options = {})
38
+ objects_that_may_be_updated = [@uri, @head]
39
+ old_hash = objects_that_may_be_updated.map(&:hash)
40
+ opts = options.symbolize_keys
41
+ @head.merge!(build_head(opts))
42
+ if opts[:query]
43
+ @uri.query_values = sanitize_query(opts[:query])
44
+ end
45
+ if old_hash == objects_that_may_be_updated.map(&:hash)
46
+ nil
24
47
  else
25
- @body
48
+ self
26
49
  end
27
50
  end
28
51
 
29
- def raw_body
30
- if @body.kind_of?(String)
31
- @body
52
+ def no_cache?
53
+ head['Cache-Control'] && head['Cache-Control'].include?('no-cache')
54
+ end
55
+
56
+ protected
57
+ def build_head(options = {})
58
+ sanitize_head(
59
+ options.delete(:headers) || options.delete(:head) || {}
60
+ )
61
+ end
62
+
63
+ def build_body(options = {})
64
+ if options[:body]
65
+ type = MediaType.find(options[:head]['Content-Type'])
66
+ if type.nil?
67
+ type = MediaType.find('application/x-www-form-urlencoded')
68
+ options[:head]['Content-Type'] = type.default_type
69
+ end
70
+ type.serialize(options[:body], :uri => options[:uri])
32
71
  else
33
- @serialized_body ||= serialize(@body, :content_type => @headers['Content-Type'])
72
+ nil
34
73
  end
35
74
  end
36
75
 
37
- def add_headers(headers = {})
38
- @headers.merge!(sanitize_http_headers(headers || {}))
39
- end
40
76
  end
41
77
  end
42
- end
78
+ end
@@ -1,37 +1,68 @@
1
1
  module Restfully
2
2
  module HTTP
3
- # Container for an HTTP Response. Has <tt>status</tt>, <tt>headers</tt> and <tt>body</tt> properties.
4
3
  class Response
5
- include Headers, Restfully::Parsing
6
- attr_reader :status, :headers
7
-
8
- # <tt>body</tt>:: may be a string (that will be parsed when calling the #body function), or an object (that will be returned as is when calling the #body function)
9
- def initialize(status, headers, body)
10
- @status = status.to_i
11
- @headers = sanitize_http_headers(headers)
12
- @body = body
13
- end
4
+ include Helper
14
5
 
15
- def body
16
- if @body.kind_of?(String)
17
- if @body.empty?
18
- nil
19
- else
20
- @unserialized_body ||= unserialize(@body, :content_type => @headers['Content-Type'])
21
- end
6
+ attr_reader :io, :code, :head
7
+
8
+ def initialize(session, code, head, body)
9
+ @session = session
10
+ @io = StringIO.new
11
+ case body
12
+ when String
13
+ @io << body
22
14
  else
23
- @body
15
+ body.each{|chunk| @io << chunk}
24
16
  end
17
+ @io.rewind
18
+
19
+ @code = code
20
+ @head = sanitize_head(head)
25
21
  end
26
22
 
27
- def raw_body
28
- if @body.kind_of?(String)
29
- @body
30
- else
31
- @serialized_body ||= serialize(@body, :content_type => @headers['Content-Type']) unless @body.nil?
23
+ def body
24
+ @io.rewind
25
+ @body = @io.read
26
+ @io.rewind
27
+ @body
28
+ end
29
+
30
+ def io
31
+ @io
32
+ end
33
+
34
+ def media_type
35
+ @media_type ||= begin
36
+ m = MediaType.find(head['Content-Type'])
37
+ raise Error, "Cannot find a media-type for content-type=#{head['Content-Type'].inspect}" if m.nil?
38
+ @session.logger.debug "Using media-type #{m.inspect}"
39
+ m.new(io, @session)
32
40
  end
33
41
  end
34
42
 
43
+ # TODO: we could also search for Link headers here.
44
+ def links
45
+ media_type.links
46
+ end
47
+
48
+ def property(key)
49
+ media_type.property(key)
50
+ end
51
+
52
+ def allow?(http_method)
53
+ http_method = http_method.to_sym
54
+ return true if http_method == :get
55
+ (
56
+ media_type.respond_to?(:allow?) &&
57
+ media_type.allow?(http_method)
58
+ ) || (
59
+ head['Allow'] &&
60
+ head['Allow'].split(/\s*,\s*/).map{|m|
61
+ m.downcase.to_sym
62
+ }.include?(http_method)
63
+ )
64
+ end
65
+
35
66
  end
36
67
  end
37
- end
68
+ end
@@ -1,36 +1,44 @@
1
1
  require 'uri'
2
2
  module Restfully
3
3
  class Link
4
-
5
- VALID_RELATIONSHIPS = %w{member parent collection self alternate next}
6
- RELATIONSHIPS_REQUIRING_TITLE = %w{collection member}
7
-
8
- attr_reader :rel, :title, :href, :errors
9
-
4
+
5
+ attr_reader :rel, :title, :href, :errors, :type
6
+
10
7
  def initialize(attributes = {})
11
- @rel = attributes['rel']
12
- @title = attributes['title']
13
- @href = URI.parse(attributes['href'].to_s)
14
- @resolvable = attributes['resolvable'] || false
15
- @resolved = attributes['resolved'] || false
8
+ attributes = attributes.symbolize_keys
9
+ @rel = attributes[:rel]
10
+ @title = attributes[:title] || @rel
11
+ @href = URI.parse(attributes[:href].to_s)
12
+ @type = attributes[:type]
13
+ @id = attributes[:id]
16
14
  end
17
-
18
- def resolvable?; @resolvable == true; end
19
- def resolved?; @resolved == true; end
15
+
20
16
  def self?; @rel == 'self'; end
21
-
17
+
18
+ def types
19
+ type.split(";")
20
+ end
21
+
22
22
  def valid?
23
23
  @errors = []
24
- if href.nil?
25
- errors << "href cannot be nil."
26
- end
27
- unless VALID_RELATIONSHIPS.include?(rel)
28
- errors << "#{rel} is not a valid link relationship."
24
+ if type.nil? || type.empty?
25
+ errors << "type cannot be blank"
26
+ elsif media_type.nil?
27
+ errors << "cannot find a MediaType for type #{type.inspect}"
29
28
  end
30
- if (!title || title.empty?) && RELATIONSHIPS_REQUIRING_TITLE.include?(rel)
31
- errors << "#{rel} #{href} has no title."
29
+ if href.nil?
30
+ errors << "href cannot be nil"
32
31
  end
33
32
  errors.empty?
34
33
  end
35
- end
36
- end
34
+
35
+ def media_type
36
+ @media_type ||= MediaType.find(type)
37
+ end # def catalog
38
+
39
+ def id
40
+ title.to_s.downcase.gsub(/[^a-z]/,'_').squeeze('_').to_sym
41
+ end
42
+
43
+ end # class Link
44
+ end
@@ -0,0 +1,70 @@
1
+ require 'restfully/media_type/abstract_media_type'
2
+
3
+ module Restfully
4
+ module MediaType
5
+ class << self
6
+ def catalog
7
+ @catalog ||= Set.new
8
+ end
9
+
10
+ def find(*types)
11
+ return nil if types.compact.empty?
12
+
13
+ found = {}
14
+
15
+ catalog.each{ |media_type|
16
+ match = media_type.supports?(*types)
17
+ found[match] = media_type unless match.nil?
18
+ }
19
+
20
+ if found.empty?
21
+ nil
22
+ else
23
+ found.sort{|a, b| a[0].length <=> b[0].length }.last[1]
24
+ end
25
+ end
26
+
27
+
28
+ def unregister(media_type)
29
+ catalog.delete(media_type)
30
+ build_index
31
+ self
32
+ end
33
+
34
+ def register(media_type)
35
+ if media_type.signature.empty?
36
+ raise ArgumentError, "The given MediaType (#{media_type}) has no signature"
37
+ end
38
+ if media_type.parser.nil?
39
+ raise ArgumentError, "The given MediaType (#{media_type}) has no parser"
40
+ end
41
+ unregister(media_type).catalog.add(media_type)
42
+ end
43
+
44
+ # Reset the catalog to the default list of media types
45
+ def reset
46
+ %w{
47
+ wildcard
48
+ application_json
49
+ application_x_www_form_urlencoded
50
+ grid5000
51
+ }.each do |m|
52
+ require "restfully/media_type/#{m}"
53
+ register MediaType.const_get(m.camelize)
54
+ end
55
+ end
56
+
57
+ def build_index
58
+ # @index = []
59
+ # catalog.each do |media_type|
60
+ #
61
+ # end
62
+ end
63
+
64
+
65
+ end
66
+
67
+ reset
68
+
69
+ end
70
+ end
@@ -0,0 +1,162 @@
1
+ require 'set'
2
+
3
+ module Restfully
4
+ module MediaType
5
+
6
+ class AbstractMediaType
7
+
8
+ #
9
+ # These class functions should NOT be overwritten by descendants.
10
+ #
11
+ class << self
12
+
13
+ def parent(method)
14
+ if superclass.respond_to?(method)
15
+ superclass.send(method)
16
+ else
17
+ nil
18
+ end
19
+ end
20
+
21
+ # @return [Hash] the hash of default options set.
22
+ def defaults
23
+ @defaults ||= (parent(:defaults) || {}).dup
24
+ end
25
+
26
+ # Sets a new <tt>value</tt> for a default <tt>attribute</tt>.
27
+ def set(attribute, value)
28
+ defaults[attribute.to_sym] = value
29
+ end
30
+
31
+ # Returns the media-type signature, i.e. the list of metia-type it
32
+ # supports.
33
+ def signature
34
+ [(defaults[:signature] || [])].flatten
35
+ end
36
+
37
+ # Returns the media-type parser.
38
+ def parser
39
+ defaults[:parser]
40
+ end
41
+
42
+ # Returns the media-type default signature.
43
+ def default_type
44
+ signature.first
45
+ end
46
+
47
+ # Returns the first supported media-type of the signature that matches
48
+ # one of the given <tt>types</tt>.
49
+ def supports?(*types)
50
+ types.each do |type|
51
+ type = type.to_s.downcase.split(";")[0]
52
+ found = signature.find{ |s|
53
+ type =~ Regexp.new(Regexp.escape(s.downcase).gsub('\*', ".*"))
54
+ }
55
+ return found if found
56
+ end
57
+ nil
58
+ end
59
+
60
+ # Serialize an object into a String.
61
+ # Calls parser#dump.
62
+ def serialize(object, *args)
63
+ case object
64
+ when String
65
+ object
66
+ else
67
+ parser.dump(object, *args)
68
+ end
69
+ end
70
+
71
+ # Unserialize an io object into a Hash.
72
+ # Calls parser#load.
73
+ def unserialize(io, *args)
74
+ parser.load(io, *args)
75
+ end
76
+
77
+ end
78
+
79
+
80
+ attr_reader :io, :session
81
+
82
+ # A MediaType instance takes the original io object and the current
83
+ # session object as input.
84
+ def initialize(io, session)
85
+ @io = io
86
+ @session = session
87
+ end
88
+
89
+ # Returns an array of Link objects.
90
+ # Do not overwrite directly. Overwrite #extract_links instead.
91
+ def links
92
+ @links ||= extract_links.select{|l| l.valid?}
93
+ end
94
+
95
+ # Returns the unserialized version of the io object.
96
+ def unserialized
97
+ @unserialized ||= self.class.unserialize(@io)
98
+ end
99
+
100
+ # Without argument, returns the properties Hash obtained from calling
101
+ # #unserialized.
102
+ # With an argument, returns the value corresponding to that key.
103
+ #
104
+ # Should be overwritten if required.
105
+ def property(key = nil)
106
+ @properties ||= unserialized
107
+ if key
108
+ @properties[key]
109
+ else
110
+ @properties
111
+ end
112
+ end
113
+
114
+ # Should return true if the current resource represented by this
115
+ # media-type can be designated with <tt>id</tt>.
116
+ #
117
+ # Should be overwritten.
118
+ def represents?(id)
119
+ false
120
+ end
121
+
122
+ # Returns true if the current io object must be handled as a Collection.
123
+ #
124
+ # Should be overwritten.
125
+ def collection?
126
+ false
127
+ end
128
+
129
+ # Returns true if the current io object is completely loaded.
130
+ # Overwrite and return false to force a reloading if your object is just
131
+ # a URI reference.
132
+ def complete?
133
+ true
134
+ end
135
+
136
+ # An object to display on Resource#inspect.
137
+ #
138
+ # Should be overwritten.
139
+ def meta
140
+ property
141
+ end
142
+
143
+ # How to iterate over a collection
144
+ #
145
+ # Should be overwritten.
146
+ def each(*args, &block)
147
+ raise NotImplemented
148
+ end
149
+
150
+
151
+ protected
152
+ # The function that returns the array of Link object.
153
+ #
154
+ # Should be overwritten.
155
+ def extract_links
156
+ []
157
+ end
158
+ end
159
+
160
+ end
161
+
162
+ end