restfully 0.6.3 → 0.7.0.pre

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