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
data/README.rdoc DELETED
@@ -1,146 +0,0 @@
1
- = restfully
2
-
3
- An attempt at dynamically providing "clever" wrappers on top of RESTful APIs that follow the principle of Hyperlinks As The Engine Of Application State (HATEOAS). It does not require to use specific (and often complex) server-side libraries, but a few constraints and conventions must be followed:
4
- 1. Return sensible HTTP status codes;
5
- 2. Make use of GET, POST, PUT, DELETE HTTP methods;
6
- 3. Return a Location HTTP header in 201 or 202 responses;
7
- 4. Return a <tt>links</tt> property in all responses to a GET request, that contains a list of link objects:
8
-
9
- {
10
- "property": "value",
11
- "links": [
12
- {"rel": "self", "href": "uri/to/resource", "type": "application/vnd.whatever+json;level=1,application/json"},
13
- {"rel": "parent", "href": "uri/to/parent/resource", "type": "application/json"}
14
- {"rel": "collection", "href": "uri/to/collection", "title": "my_collection", "type": "application/json"},
15
- {"rel": "member", "href": "uri/to/member", "title": "member_title", "type": "application/json"}
16
- ]
17
- }
18
-
19
- * Adding a <tt>parent</tt> link automatically creates a <tt>#parent</tt> method on the current resource.
20
- * Adding a <tt>collection</tt> link automatically creates a <tt>#my_collection</tt> method that will fetch the Collection when called.
21
- * Adding a <tt>member</tt> link automatically creates a <tt>#member_title</tt> method that will fetch the Resource when called.
22
- 5. Advertise allowed HTTP methods in the response to GET requests by returning a <tt>Allow</tt> HTTP header containing a comma-separated list of the HTTP methods that can be used on the resource. This will allow the automatic generation of methods to interact with the resource. e.g.: advertising a <tt>POST</tt> method (<tt>Allow: GET, POST</tt>) will result in the creation of a <tt>submit</tt> method on the resource.
23
-
24
- == Installation
25
- $ gem install restfully --source http://gemcutter.org
26
-
27
- == Usage
28
- === Command line
29
- $ export RUBYOPT="-rubygems"
30
- $ restfully base_uri [-u username] [-p password]
31
-
32
- e.g., for the Grid5000 API:
33
- $ restfully https://api.grid5000.fr/sid/grid5000 -u username -p password
34
-
35
- If the connection was successful, you should get a prompt. You may enter
36
- irb(main):001:0> pp root
37
-
38
- to get back a pretty-printed output of the root resource:
39
- #<Restfully::Resource:0x91f08c
40
- @uri=#<URI::HTTP:0x123e30c URL:http://api.local/sid/grid5000>
41
- LINKS
42
- @environments=#<Restfully::Collection:0x917666>,
43
- @sites=#<Restfully::Collection:0x9170d0>,
44
- @version=#<Restfully::Resource:0x91852a>,
45
- @versions=#<Restfully::Collection:0x917e68>
46
- PROPERTIES
47
- "uid"=>"grid5000",
48
- "type"=>"grid",
49
- "version"=>"4fe96b25d2cbfee16abe5a4fb999c82dbafc2ee8">
50
-
51
- You can see the LINKS and PROPERTIES headers that respectively indicate what links you can follow from there (by calling <tt>root.link_name</tt>) and what properties are available (by calling <tt>root[property_name]</tt>).
52
-
53
- Let's say you want to access the collection of +sites+, you would enter:
54
- irb(main):002:0> pp root.sites
55
-
56
- and get back:
57
- #<Restfully::Collection:0x9170d0
58
- @uri=#<URI::HTTP:0x122e128 URL:http://api.local/sid/grid5000/sites>
59
- LINKS
60
- @version=#<Restfully::Resource:0x8f553e>,
61
- @versions=#<Restfully::Collection:0x8f52be>
62
- PROPERTIES
63
- "total"=>9,
64
- "version"=>"4fe96b25d2cbfee16abe5a4fb999c82dbafc2ee8",
65
- "offset"=>0
66
- ITEMS (0..9)/9
67
- #<Restfully::Resource:0x9058bc uid="bordeaux">,
68
- #<Restfully::Resource:0x903d0a uid="grenoble">,
69
- #<Restfully::Resource:0x901cc6 uid="lille">,
70
- #<Restfully::Resource:0x8fff0c uid="lyon">,
71
- #<Restfully::Resource:0x8fe288 uid="nancy">,
72
- #<Restfully::Resource:0x8fc4a6 uid="orsay">,
73
- #<Restfully::Resource:0x8fa782 uid="rennes">,
74
- #<Restfully::Resource:0x8f8bb2 uid="sophia">,
75
- #<Restfully::Resource:0x8f6c9a uid="toulouse">>
76
-
77
- A Restfully::Collection is a special kind of Resource, which includes the Enumerable module, which means you can call all of its methods on the Restfully::Collection object. For example:
78
- irb(main):003:0> pp root.sites.find{|s| s['uid'] == 'rennes'}
79
- #<Restfully::Resource:0x8fa782
80
- @uri=#<URI::HTTP:0x11f4e64 URL:http://api.local/sid/grid5000/sites/rennes>
81
- LINKS
82
- @environments=#<Restfully::Collection:0x8f9ab2>,
83
- @parent=#<Restfully::Resource:0x8f981e>,
84
- @deployments=#<Restfully::Collection:0x8f935a>,
85
- @clusters=#<Restfully::Collection:0x8f9d46>,
86
- @version=#<Restfully::Resource:0x8fa354>,
87
- @versions=#<Restfully::Collection:0x8fa0b6>,
88
- @status=#<Restfully::Collection:0x8f95ee>
89
- PROPERTIES
90
- "name"=>"Rennes",
91
- "latitude"=>48.1,
92
- "location"=>"Rennes, France",
93
- "security_contact"=>"rennes-staff@lists.grid5000.fr",
94
- "uid"=>"rennes",
95
- "type"=>"site",
96
- "user_support_contact"=>"rennes-staff@lists.grid5000.fr",
97
- "version"=>"4fe96b25d2cbfee16abe5a4fb999c82dbafc2ee8",
98
- "description"=>"",
99
- "longitude"=>-1.6667,
100
- "compilation_server"=>false,
101
- "email_contact"=>"rennes-staff@lists.grid5000.fr",
102
- "web"=>"http://www.irisa.fr",
103
- "sys_admin_contact"=>"rennes-staff@lists.grid5000.fr">
104
-
105
- or:
106
- irb(main):006:0> root.sites.map{|s| s['uid']}.grep(/re/)
107
- => ["grenoble", "rennes"]
108
-
109
- A shortcut is available to find a specific entry in a collection, by entering the searched uid as a Symbol:
110
- irb(main):007:0> root.sites[:rennes]
111
- # will find the item whose uid is "rennes"
112
-
113
- For ease of use and better security, you may prefer to use a configuration file to avoid re-entering the options every time you use the client:
114
- $ echo '
115
- base_uri: https://api.grid5000.fr/sid/grid5000
116
- username: MYLOGIN
117
- password: MYPASSWORD
118
- ' > ~/.restfully/api.grid5000.fr.yml && chmod 600 ~/.restfully/api.grid5000.fr.yml
119
-
120
- And then:
121
- $ restfully -c ~/.restfully/api.grid5000.fr.yml
122
-
123
- === As a library
124
- See the +examples+ directory for examples.
125
-
126
- == Discovering the API capabilities
127
- A Restfully::Resource (and by extension its child Restfully::Collection) has the following methods available for introspection:
128
- * <tt>links</tt> will return a hash whose keys are the name of the methods that can be called to navigate between resources;
129
- * <tt>http_methods</tt> will return an array containing the list of the HTTP methods that are allowed on the resource;
130
-
131
- == Note on Patches/Pull Requests
132
-
133
- * Fork the project.
134
- * Make your feature addition or bug fix.
135
- * Add tests for it. This is important so I don't break it in a future version unintentionally.
136
- * Commit, do not mess with rakefile, version, or history (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull).
137
- * Send me a pull request. Bonus points for topic branches.
138
-
139
- == Testing
140
-
141
- * rake spec, or
142
- * run autotest in the project directory.
143
-
144
- == Copyright
145
-
146
- Copyright (c) 2009 Cyril Rohr, INRIA Rennes - Bretagne Atlantique. See LICENSE for details.
data/TODO.rdoc DELETED
@@ -1,3 +0,0 @@
1
- * Detects methods allowed on a resource using the Allow HTTP header, and generate corresponding wrappers.
2
- * On a 406, detects accepted content based on a (custom) HTTP header, and retry (if possible) with another Accept header for which a parser is available.
3
- * Investigate the possibility of using a :include => {} when loading resources or collection so that Restfully prefetches the included associations (using threads or events).
data/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.6.3
data/examples/grid5000.rb DELETED
@@ -1,33 +0,0 @@
1
- require 'rubygems'
2
- $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
- require 'pp'
4
-
5
- require File.dirname(__FILE__)+'/../lib/restfully'
6
-
7
- logger = Logger.new(STDOUT)
8
- logger.level = Logger::WARN
9
-
10
- RestClient.log = 'stdout'
11
-
12
- # This yaml file contains the following attributes:
13
- # username: my_username
14
- # password: my_password
15
- options = YAML.load_file(File.expand_path('~/.restfully/api.grid5000.fr.yml'))
16
- options[:base_uri] = 'https://api.grid5000.fr/sid/grid5000'
17
- options[:logger] = logger
18
- Restfully::Session.new(options) do |grid, session|
19
- grid_stats = {'hardware' => {}, 'system' => {}}
20
- grid.sites.each do |site|
21
- site_stats = site.status.inject({'hardware' => {}, 'system' => {}}) {|accu, node_status|
22
- accu['hardware'][node_status['hardware_state']] = (accu['hardware'][node_status['hardware_state']] || 0) + 1
23
- accu['system'][node_status['system_state']] = (accu['system'][node_status['system_state']] || 0) + 1
24
- accu
25
- } rescue {'hardware' => {}, 'system' => {}}
26
- grid_stats['hardware'].merge!(site_stats['hardware']) { |key,oldval,newval| oldval+newval }
27
- grid_stats['system'].merge!(site_stats['system']) { |key,oldval,newval| oldval+newval }
28
- p [site['uid'], site_stats]
29
- end
30
- p [:total, grid_stats]
31
- puts "Getting status of a few nodes in rennes:"
32
- pp grid.sites.find{|s| s['uid'] == 'rennes'}.status(:query => {:only => ['paradent-1', 'paradent-10', 'paramount-3']})
33
- end
data/examples/scratch.rb DELETED
@@ -1,37 +0,0 @@
1
- m = MediaType.new('application/vnd.grid5000+json')
2
- m.parser = JSON
3
-
4
- class Grid5000MediaType < Restfully::MediaType
5
-
6
- class << self
7
- def supports?(given_type)
8
- if given_type.kind_of?(Restfully::MediaType)
9
- given_type = given_type.type
10
- end
11
- case given_type
12
- when Regexp
13
- given_type =~ type
14
- else
15
- given_type == type.to_s || given_type =~ Regexp.new(type.to_s)
16
- end
17
- end
18
- end
19
-
20
-
21
- end
22
-
23
-
24
- get 'https://api.grid5000.fr/sid/grid5000'
25
- type = 'application/vnd.grid5000+json'
26
- m = MediaType.find(type)
27
- if m.nil?
28
- "does not know how to interpret #{type}"
29
- else
30
- grid5000 = Resource.new(uri, links, properties)
31
- grid5000.sites[:rennes]
32
-
33
- m.links
34
- m.properties
35
- end
36
-
37
-
@@ -1,34 +0,0 @@
1
- class BasicObject #:nodoc:
2
- instance_methods.each { |m| undef_method m unless m =~ /^__|instance_eval/ }
3
- end unless defined?(BasicObject)
4
-
5
- class Object
6
- # Taken from ActiveSupport
7
- def to_query(key)
8
- require 'cgi' unless defined?(CGI) && defined?(CGI::escape)
9
- "#{CGI.escape(key.to_s)}=#{CGI.escape(to_params.to_s)}"
10
- end
11
-
12
- def to_params
13
- to_s
14
- end
15
- end
16
-
17
- class Hash
18
- # Converts a hash into a string suitable for use as a URL query string.
19
- # An optional <tt>namespace</tt> can be passed to enclose the param names.
20
- # Taken from ActiveSupport
21
- def to_params(namespace = nil)
22
- collect do |key, value|
23
- value.to_query(namespace ? "#{namespace}[#{key}]" : key)
24
- end * '&'
25
- end
26
- end
27
-
28
- class Array
29
- # Taken from ActiveSupport
30
- def to_query(key)
31
- prefix = "#{key}[]"
32
- collect { |value| value.to_query(prefix) }.join '&'
33
- end
34
- end
@@ -1,29 +0,0 @@
1
- module Restfully
2
- module HTTP
3
- module Adapters
4
-
5
- class AbstractAdapter
6
- attr_reader :logger, :options
7
- def initialize(base_uri, options = {})
8
- @options = options.symbolize_keys
9
- @logger = @options.delete(:logger) || Restfully::NullLogger.new
10
- @base_uri = base_uri
11
- end
12
-
13
- def get(request)
14
- raise NotImplementedError, "GET is not supported by your adapter."
15
- end
16
- def post(request)
17
- raise NotImplementedError, "POST is not supported by your adapter."
18
- end
19
- def put(request)
20
- raise NotImplementedError, "PUT is not supported by your adapter."
21
- end
22
- def delete(request)
23
- raise NotImplementedError, "DELETE is not supported by your adapter."
24
- end
25
- end
26
-
27
- end
28
- end
29
- end
@@ -1,16 +0,0 @@
1
- require 'restfully/http/adapters/abstract_adapter'
2
- require 'patron'
3
-
4
- module Restfully
5
- module HTTP
6
- module Adapters
7
- class PatronAdapter < AbstractAdapter
8
-
9
- def initialize(base_url, options = {})
10
- super(base_url, options)
11
- end
12
-
13
- end
14
- end
15
- end
16
- end
@@ -1,75 +0,0 @@
1
- require 'restfully/http/adapters/abstract_adapter'
2
- require 'restclient'
3
-
4
- module Restfully
5
- module HTTP
6
- module Adapters
7
- class RestClientAdapter < AbstractAdapter
8
-
9
- def initialize(base_uri, options = {})
10
- super(base_uri, options)
11
- @options[:user] = @options.delete(:username)
12
- RestClient.log = logger
13
- end # def initialize
14
-
15
- def head(request)
16
- in_order_to_get_the_response_to(request) do |resource|
17
- resource.head(convert_header_keys_into_symbols(request.headers))
18
- end
19
- end # def head
20
-
21
- def get(request)
22
- in_order_to_get_the_response_to(request) do |resource|
23
- resource.get(convert_header_keys_into_symbols(request.headers))
24
- end
25
- end # def get
26
-
27
- def delete(request)
28
- in_order_to_get_the_response_to(request) do |resource|
29
- resource.delete(convert_header_keys_into_symbols(request.headers))
30
- end
31
- end # def delete
32
-
33
- def put(request)
34
- in_order_to_get_the_response_to(request) do |resource|
35
- resource.put(request.raw_body, convert_header_keys_into_symbols(request.headers))
36
- end
37
- end # def put
38
-
39
- def post(request)
40
- in_order_to_get_the_response_to(request) do |resource|
41
- resource.post(request.raw_body, convert_header_keys_into_symbols(request.headers))
42
- end
43
- end # def post
44
-
45
- protected
46
- def in_order_to_get_the_response_to(request, &block)
47
- begin
48
- resource = RestClient::Resource.new(request.uri.to_s, @options)
49
- response = block.call(resource)
50
- headers = response.headers
51
- body = response.to_s
52
- headers.delete(:status)
53
- status = response.code
54
- rescue RestClient::ExceptionWithResponse => e
55
- body = e.response.to_s
56
- headers = e.response.headers rescue {}
57
- status = e.http_code
58
- end
59
- Response.new(status, headers, body)
60
- end # def in_order_to_get_the_response_to
61
-
62
- # there is a bug in RestClient, when passing headers whose keys are string that are already defined as default headers, they get overwritten.
63
- def convert_header_keys_into_symbols(headers)
64
- headers.inject({}) do |final, (key,value)|
65
- key = key.to_s.gsub(/-/, "_").downcase.to_sym
66
- final[key] = value
67
- final
68
- end
69
- end # def convert_header_keys_into_symbols
70
-
71
- end
72
-
73
- end
74
- end
75
- end
@@ -1,20 +0,0 @@
1
- module Restfully
2
- module HTTP
3
- module Headers
4
- def sanitize_http_headers(headers = {})
5
- sanitized_headers = {}
6
- headers.each do |key, value|
7
- sanitized_key = key.to_s.downcase.gsub(/[_-]/, ' ').split(' ').map{|word| word.capitalize}.join("-")
8
- sanitized_value = case value
9
- when Array
10
- value.join(", ")
11
- else
12
- value
13
- end
14
- sanitized_headers[sanitized_key] = sanitized_value
15
- end
16
- sanitized_headers
17
- end
18
- end
19
- end
20
- end
@@ -1,66 +0,0 @@
1
-
2
-
3
- module Restfully
4
-
5
- module Parsing
6
-
7
- class ParserNotFound < Restfully::Error; end
8
-
9
- PARSERS = [
10
- {
11
- :supported_types => [/^application\/.*?json/i],
12
- :parse => lambda{|object, options|
13
- require 'json'
14
- JSON.parse(object)
15
- },
16
- :dump => lambda{|object, options|
17
- require 'json'
18
- JSON.dump(object)
19
- },
20
- :object => true
21
- },
22
- {
23
- :supported_types => [/^text\/.*?(plain|html)/i],
24
- :parse => lambda{|object, options| object},
25
- :dump => lambda{|object, options| object}
26
- },
27
- { # just store the binary data in a 'raw' property
28
- :supported_types => ["application/zip"],
29
- :parse => lambda{|object, options| {'raw' => object}},
30
- :dump => lambda{|object, options| object['raw']}
31
- }
32
- ]
33
-
34
- def unserialize(object, options = {})
35
- content_type = options[:content_type]
36
- content_type ||= object.headers['Content-Type'] if object.respond_to?(:headers)
37
- parser = select_parser_for(content_type)
38
- if parser
39
- parser[:parse].call(object, options)
40
- else
41
- raise ParserNotFound.new("Cannot find a parser to parse '#{content_type}' content.")
42
- end
43
- end
44
-
45
- def serialize(object, options = {})
46
- content_type = options[:content_type]
47
- content_type ||= object.headers['Content-Type'] if object.respond_to?(:headers)
48
- parser = select_parser_for(content_type)
49
- if parser
50
- parser[:dump].call(object, options)
51
- else
52
- raise ParserNotFound.new("Cannot find a parser to dump object into '#{content_type}' content.")
53
- end
54
- end
55
-
56
- def select_parser_for(content_type)
57
- raise ParserNotFound.new("The Content-Type HTTP header of the resource is empty. Cannot find a parser.") if content_type.nil? || content_type.empty?
58
- content_type.split(",").each do |type|
59
- parser = PARSERS.find{|parser| parser[:supported_types].find{|supported_type| type =~ (supported_type.kind_of?(String) ? Regexp.new(supported_type) : supported_type) }}
60
- return parser unless parser.nil?
61
- end
62
- nil
63
- end
64
-
65
- end
66
- end