relax 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -1,4 +1,153 @@
1
- Relax
2
- =====
1
+ = Relax
3
2
 
4
- A simple library for creating REST consumers.
3
+ Relax is a simple library for creating REST consumers.
4
+
5
+ When used as a basis for writing REST consumer APIs, it provides a set of
6
+ functionality common to all REST consumers, including:
7
+
8
+ - building HTTP queries (Relax::Request)
9
+ - issuing HTTP requests (Relax::Service)
10
+ - parsing XML responses (Relax::Response)
11
+
12
+
13
+ == Tutorial
14
+
15
+ This short tutorial will walk you through the basic steps of creating a simple Flickr API that supports a single call to search for photos by tags.
16
+
17
+ === Step 1
18
+
19
+ In the first step we're going to simply include Relax, and define the basis for
20
+ our Service class.
21
+
22
+ require 'rubygems'
23
+ require 'relax'
24
+
25
+ module Flickr
26
+ class Service < Relax::Service
27
+ ENDPOINT = 'http://api.flickr.com/services/rest/'
28
+
29
+ def initialize
30
+ super(ENDPOINT)
31
+ end
32
+ end
33
+ end
34
+
35
+
36
+ === Step 2
37
+
38
+ Next we're going to define common Request and Response classes for use
39
+ throughout our API. This gives us a single point to add any shared
40
+ functionality. For Flickr, this means that each request will have a "method"
41
+ parameter, and each response will have a "stat" attribute that will equal "ok"
42
+ when the response comes back without any errors.
43
+
44
+ module Flickr
45
+ class Request < Relax::Request
46
+ parameter :method
47
+ end
48
+
49
+ class Response < Relax::Response
50
+ def successful?
51
+ root[:stat] == 'ok'
52
+ end
53
+ end
54
+ end
55
+
56
+ While we're at it, we're also going to add a new line to the constructor from
57
+ our service to make sure that our Flickr API key gets passed along with each
58
+ request as well.
59
+
60
+ module Flickr
61
+ class Service < Relax::Service
62
+ ENDPOINT = 'http://api.flickr.com/services/rest/'
63
+
64
+ def initialize(api_key)
65
+ super(ENDPOINT)
66
+ Request[:api_key] = api_key
67
+ end
68
+ end
69
+ end
70
+
71
+ When we call our Request class as we have here, we're basically setting up a
72
+ value on our request that acts like a template. Each request we create now will
73
+ have the api_key property prepopulated for us.
74
+
75
+
76
+ === Step 3
77
+
78
+ Next, we're going to need a basic Photo class to represent photos that Flickr
79
+ returns to us.
80
+
81
+ module Flickr
82
+ class Photo < Response
83
+ parameter :id, :attribute => true, :type => :integer
84
+ parameter :title, :attribute => true
85
+ end
86
+ end
87
+
88
+ Here we're creating a Response class that extends our Flickr::Response, which
89
+ has two parameters: "id" and "title." By setting the attribute option to true,
90
+ we're telling Relax to look for an attribute by that name on the XML root
91
+ instead of checking for an element by that name. The type options can be used
92
+ to specify what type of data we're expecting the response to give us. The
93
+ default type is string.
94
+
95
+
96
+ === Step 4
97
+
98
+ Now we arrive at the final piece of the puzzle: a service call module. To keep
99
+ things contained, a Relax best practice is to create a module for each call
100
+ on your service. The one we're creating here is the PhotoSearch module for the
101
+ "flickr.photos.search" call on the Flickr API.
102
+
103
+ There are three main pieces to every service call module:
104
+
105
+ 1. a Relax::Request object
106
+ 2. a Relax::Response object
107
+ 3. a call method that calls Relax::Service#call
108
+
109
+ This module then gets included into the Service class, where the call method
110
+ can be easily utilized.
111
+
112
+ module Flickr
113
+ module PhotoSearch
114
+ class PhotoSearchRequest < Flickr::Request
115
+ parameter :per_page
116
+ parameter :tags
117
+
118
+ def initialize(options = {})
119
+ super
120
+ @method = 'flickr.photos.search'
121
+ end
122
+ end
123
+
124
+ class PhotoSearchResponse < Flickr::Response
125
+ parameter :photos, :element => 'photos/photo', :collection => Photo
126
+ end
127
+
128
+ def search(options = {})
129
+ call(PhotoSearchRequest.new(options), PhotoSearchResponse)
130
+ end
131
+
132
+ def find_by_tag(tags, options = {})
133
+ search(options.merge(:tags => tags))
134
+ end
135
+ end
136
+ end
137
+
138
+ As you can see, we have our request (PhotoSearchRequest), response
139
+ (PhotoSearchResponse), and call method (actually, two in this case: search and
140
+ find_by_tag). This will get included into our Flickr::Service class, and we
141
+ can use it by calling either of the call methods.
142
+
143
+ flickr = Flickr::Service.new(ENV['FLICKR_API_KEY'])
144
+ relax = flickr.find_by_tag('relax', :per_page => 10)
145
+
146
+ if relax.successful?
147
+ relax.photos.each do |photo|
148
+ puts "[#{photo.id}] #{photo.title}"
149
+ end
150
+ end
151
+
152
+ This will output the IDs and titles for the first 10 photos on Flickr that have
153
+ the tag "relax."
data/lib/relax/query.rb CHANGED
@@ -4,7 +4,10 @@ require 'uri'
4
4
  require 'relax/symbolic_hash'
5
5
 
6
6
  module Relax
7
+ # Query is used to represent the query portion of a URL. It's basically just
8
+ # a hash, where each key/value pair is a query parameter.
7
9
  class Query < SymbolicHash
10
+ # Converts the Query to a query string for use in a URL.
8
11
  def to_s
9
12
  keys.sort { |a, b| a.to_s <=> b.to_s }.collect do |key|
10
13
  "#{key.to_s}=#{self.class.escape_value(fetch(key))}"
@@ -12,6 +15,7 @@ module Relax
12
15
  end
13
16
 
14
17
  class << self
18
+ # Parses a URL and returns a Query with its query portion.
15
19
  def parse(uri)
16
20
  query = uri.query.split('&').inject({}) do |query, parameter|
17
21
  key, value = parameter.split('=')
@@ -21,10 +25,12 @@ module Relax
21
25
  self.new(query)
22
26
  end
23
27
 
28
+ # Escapes a query parameter value.
24
29
  def escape_value(value)
25
30
  ERB::Util.url_encode(value.to_s).gsub('%20', '+')
26
31
  end
27
32
 
33
+ # Unescapes a query parameter value.
28
34
  def unescape_value(value)
29
35
  URI.unescape(value)
30
36
  end
@@ -32,6 +38,7 @@ module Relax
32
38
 
33
39
  protected
34
40
 
41
+ # Converts each value of the Query to a string as it's added.
35
42
  def convert_value(value)
36
43
  value.to_s
37
44
  end
data/lib/relax/request.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require 'relax/query'
2
2
 
3
3
  module Relax
4
+ # Request is intended to be a parent class for requests passed to
5
+ # Service#call.
4
6
  class Request
5
7
  def initialize(options = {})
6
8
  self.class.class_variables.each do |variable|
@@ -12,6 +14,7 @@ module Relax
12
14
  end
13
15
  end
14
16
 
17
+ # Converts this request into a Query object.
15
18
  def to_query
16
19
  keys.inject(Query.new) do |parameters, key|
17
20
  parameters[convert_key(key)] = send(key)
@@ -19,18 +22,23 @@ module Relax
19
22
  end
20
23
  end
21
24
 
25
+ # Converts this request into a query string for use in a URL.
22
26
  def to_s
23
- keys.sort { |a, b| a.to_s <=> b.to_s }.collect do |key|
24
- "#{key.to_s}=#{ERB::Util.url_encode(send('[]', key).to_s)}"
25
- end.join('&')
27
+ to_query.to_s
26
28
  end
27
29
 
28
30
  class << self
31
+ # Specifies a parameter to create on the request class.
32
+ #
33
+ # Options:
34
+ # - <tt>:value</tt>: The default value for this parameter.
29
35
  def parameter(name, options = {})
30
36
  attr_accessor name
31
37
  class_variable_set("@@#{name}", options.delete(:value)) if options[:value]
32
38
  end
33
39
 
40
+ # Adds a template value to a request class. Equivalent to creating a
41
+ # parameter with a default value.
34
42
  def []=(key, value)
35
43
  parameter(key, {:value => value})
36
44
  end
@@ -38,10 +46,15 @@ module Relax
38
46
 
39
47
  protected
40
48
 
49
+ # Returns an array of the parameter names for this request.
41
50
  def keys
42
51
  instance_variables.collect { |v| v.sub('@', '') }
43
52
  end
44
53
 
54
+ # Converts a key when the Request is converted to a query. By default, no
55
+ # conversion actually takes place, but this method can be overridden by
56
+ # child classes to perform standard manipulations, such as replacing
57
+ # underscores.
45
58
  def convert_key(key)
46
59
  key
47
60
  end
@@ -4,9 +4,18 @@ require 'hpricot'
4
4
  require 'date'
5
5
 
6
6
  module Relax
7
+ # Response is intended to be a parent class for responses passed to
8
+ # Service#call.
9
+ #
10
+ # A response is in essence an object used to facilitate XML parsing. It
11
+ # stores an XML document, and provides access to it through methods like
12
+ # #element and #attribute.
7
13
  class Response
8
14
  attr_accessor :xml
9
15
 
16
+ # New takes in the XML from the response. For the initial response, this
17
+ # will be the root element, but child elements may also be passed into
18
+ # Response objects.
10
19
  def initialize(xml)
11
20
  @xml = Hpricot.XML(xml.to_s)
12
21
 
@@ -29,7 +38,10 @@ module Relax
29
38
  options[:collection].new(element)
30
39
  end
31
40
  else
32
- case options[:type]
41
+ case type = options[:type]
42
+ when Response
43
+ value = type.new(node)
44
+
33
45
  when :float
34
46
  value = float_value(node)
35
47
 
@@ -47,62 +59,89 @@ module Relax
47
59
  end
48
60
  end
49
61
 
62
+ # Returns the root of the XML document.
50
63
  def root
51
64
  @xml.root
52
65
  end
53
66
 
67
+ # Checks the name of the root node.
54
68
  def is?(name)
55
69
  root.name.gsub(/.*:(.*)/, '\1') == node_name(name)
56
70
  end
57
71
 
72
+ # Returns an element of the specified name.
58
73
  def element(name)
59
74
  root.at(root_path(name))
60
75
  end
76
+ alias :has? :element
61
77
 
78
+ # Returns an attribute on an element.
62
79
  def attribute(element, name)
63
80
  element[name]
64
81
  end
65
82
 
83
+ # Returns a set of elements matching name.
66
84
  def elements(name)
67
85
  root.search(root_path(name))
68
86
  end
69
87
 
88
+ # Gets the value of an element or attribute.
70
89
  def value(value)
71
90
  value.is_a?(Hpricot::Elem) ? value.inner_text : value.to_s
72
91
  end
73
92
 
93
+ # Gets a text value.
74
94
  def text_value(value)
75
95
  value(value)
76
96
  end
77
97
 
98
+ # Gets an integer value.
78
99
  def integer_value(value)
79
100
  value(value).to_i
80
101
  end
81
102
 
103
+ # Gets a float value.
82
104
  def float_value(value)
83
105
  value(value).to_f
84
106
  end
85
107
 
108
+ # Gets a date value.
86
109
  def date_value(value)
87
110
  Date.parse(value(value))
88
111
  end
89
112
 
90
- alias :has? :element
91
-
92
113
  class << self
114
+ # Specifes a parameter that will be automatically parsed when the
115
+ # Response is instantiated.
116
+ #
117
+ # Options:
118
+ # - <tt>:attribute</tt>: An attribute name to use, or <tt>true</tt> to
119
+ # use the <tt>:element</tt> value as the attribute name on the root.
120
+ # - <tt>:collection</tt>: A class used to instantiate each item when
121
+ # selecting a collection of elements.
122
+ # - <tt>:element</tt>: The XML element name.
123
+ # - <tt>:object</tt>: A class used to instantiate an element.
124
+ # - <tt>:type</tt>: The type of the parameter. Should be one of
125
+ # <tt>:text</tt>, <tt>:integer</tt>, <tt>:float</tt>, or <tt>:date</tt>.
93
126
  def parameter(name, options = {})
94
127
  attr_accessor name
95
128
  @parameters ||= {}
96
129
  @parameters[name] = options
97
130
  end
131
+
132
+ def ===(response)
133
+ response.is_a?(Class) ? response.ancestors.include?(self) : super
134
+ end
98
135
  end
99
136
 
100
137
  private
101
138
 
139
+ # Converts a name to a node name.
102
140
  def node_name(name)
103
- name.to_s.downcase
141
+ name.to_s
104
142
  end
105
143
 
144
+ # Gets the XPath expression representing the root node.
106
145
  def root_path(name)
107
146
  "/#{node_name(name)}"
108
147
  end
@@ -0,0 +1,101 @@
1
+ require 'openssl'
2
+ require 'net/https'
3
+ require 'uri'
4
+ require 'date'
5
+ require 'base64'
6
+ require 'erb'
7
+
8
+ module Relax
9
+ # Service is the starting point for any REST consumer written with Relax. It
10
+ # is responsible for setting up the endpoint for the service, and issuing the
11
+ # HTTP requests for each call made.
12
+ #
13
+ # == Extending Service
14
+ #
15
+ # When writing consumers, you should start by extending Service by inheriting
16
+ # from it and calling its constructor with the endpoint for the service.
17
+ #
18
+ # === Example
19
+ #
20
+ # class Service < Relax::Service
21
+ # ENDPOINT = 'http://example.com/services/rest/'
22
+ #
23
+ # def initialize
24
+ # super(ENDPOINT)
25
+ # end
26
+ # end
27
+ #
28
+ # == Calling a Service
29
+ #
30
+ # Each call made to the service goes through the #call method of Service,
31
+ # which takes in a Request object and a Response class. The Request object is
32
+ # used to generate the query string that will be passed to the endpoint. The
33
+ # Reponse class is instantiated with the body of the response from the HTTP
34
+ # request.
35
+ #
36
+ # === Example
37
+ #
38
+ # This example show how to create a barebones call. This module can be then
39
+ # included into your Service class.
40
+ #
41
+ # module Search
42
+ # class SearchRequest < Relax::Request
43
+ # end
44
+ #
45
+ # class SearchResponse < Relax::Response
46
+ # end
47
+ #
48
+ # def search(options = {})
49
+ # call(SearchRequest.new(options), SearchResponse)
50
+ # end
51
+ # end
52
+ #
53
+ class Service
54
+ attr_reader :endpoint
55
+
56
+ # This constructor should be called from your Service with the endpoint URL
57
+ # for the REST service.
58
+ def initialize(endpoint)
59
+ @endpoint = URI::parse(endpoint)
60
+ end
61
+
62
+ protected
63
+
64
+ # Calls the service using a query built from the Request object passed in
65
+ # as its first parameter. Once the response comes back from the service,
66
+ # the body of the response is used to instantiate the response class, and
67
+ # this response object is returned.
68
+ def call(request, response_class)
69
+ uri = @endpoint.clone
70
+ uri.query = query(request).to_s
71
+ response = request(uri)
72
+ puts "Response:\n#{response.body}\n\n" if $DEBUG
73
+ response_class.new(response.body)
74
+ end
75
+
76
+ private
77
+
78
+ def default_query
79
+ Query.new
80
+ end
81
+
82
+ def query(request)
83
+ Query.new(default_query.merge(request.to_query))
84
+ end
85
+
86
+ def request(uri)
87
+ puts "Request:\n#{uri.to_s}\n\n" if $DEBUG
88
+ http = Net::HTTP.new(uri.host, uri.port)
89
+
90
+ if uri.scheme == 'https'
91
+ http.use_ssl = true
92
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
93
+ end
94
+
95
+ http.start do |http|
96
+ request = Net::HTTP::Get.new("#{uri.path}?#{uri.query}")
97
+ http.request(request)
98
+ end
99
+ end
100
+ end
101
+ end
@@ -1,4 +1,20 @@
1
1
  module Relax
2
+ # SymbolicHash provides an extension of Hash, but one that only supports keys
3
+ # that are symbols. This has been done in an effort to prevent the case where
4
+ # both a string key and a symbol key are set on the same hash, and espcially
5
+ # for dealing with this particular case when convert the hash to a string.
6
+ #
7
+ # === Example
8
+ #
9
+ # hash = Relax::SymbolicHash.new
10
+ # hash[:one] = 1
11
+ # hash['one'] = 2
12
+ # puts hash[:one] # => 2
13
+ #
14
+ # === Credits
15
+ #
16
+ # Some of the inspiration (and code) for this class comes from the
17
+ # HashWithIndifferentAccess that ships with Rails.
2
18
  class SymbolicHash < Hash
3
19
  def initialize(constructor = {})
4
20
  if constructor.is_a?(Hash)
@@ -21,8 +37,7 @@ module Relax
21
37
  other_hash.each_pair { |key, value| store(convert_key(key), convert_value(value)) }
22
38
  self
23
39
  end
24
-
25
- alias_method :merge!, :update
40
+ alias :merge! :update
26
41
 
27
42
  def fetch(key, *extras)
28
43
  super(convert_key(key), *extras)
@@ -47,10 +62,9 @@ module Relax
47
62
  def key?(key)
48
63
  super(convert_key(key))
49
64
  end
50
-
51
- alias_method :include?, :key?
52
- alias_method :has_key?, :key?
53
- alias_method :member?, :key?
65
+ alias :include? :key?
66
+ alias :has_key? :key?
67
+ alias :member? :key?
54
68
 
55
69
  protected
56
70
 
data/lib/relax.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
2
2
 
3
- require 'relax/api'
4
- require 'relax/symbolic_hash'
5
3
  require 'relax/query'
6
4
  require 'relax/request'
7
5
  require 'relax/response'
6
+ require 'relax/service'
7
+ require 'relax/symbolic_hash'
@@ -15,6 +15,10 @@ XML = <<EOF
15
15
  </Tokens>
16
16
  <Status>Success</Status>
17
17
  <RequestId valid="true">44287</RequestId>
18
+ <Error>
19
+ <Code>1</Code>
20
+ <Message>Failed</Message>
21
+ </Error>
18
22
  </RESTResponse>
19
23
  EOF
20
24
 
@@ -24,10 +28,16 @@ class TestResponse < Relax::Response
24
28
  parameter :status
25
29
  end
26
30
 
31
+ class Error < Relax::Response
32
+ parameter :code, :type => :integer
33
+ parameter :message
34
+ end
35
+
27
36
  parameter :status
28
37
  parameter :request_id, :element => :requestid, :type => :integer
29
38
  parameter :valid_request, :element => :requestid, :attribute => :valid
30
39
  parameter :tokens, :collection => Token
40
+ parameter :error, :type => Error
31
41
  end
32
42
 
33
43
  describe 'a response' do
@@ -38,7 +48,7 @@ describe 'a response' do
38
48
  it 'should allow access to the root' do
39
49
  root = @response.root
40
50
  root.should be_an_instance_of(Hpricot::Elem)
41
- root.name.should eql('restresponse')
51
+ root.name.should eql('RESTResponse')
42
52
  end
43
53
 
44
54
  it 'should be checkable by the name of its root' do
@@ -66,7 +76,7 @@ describe 'a response' do
66
76
  @response.has?(:Errors).should be_nil
67
77
  end
68
78
 
69
- it 'should be able to define children of Request without modifying parent' do
79
+ it 'should be able to define children of Response without modifying parent' do
70
80
  Relax::Response.new(XML).respond_to?(:status).should be_false
71
81
  TestResponse.new(XML).respond_to?(:status).should be_true
72
82
  end
@@ -78,5 +88,11 @@ describe 'a response' do
78
88
  response.valid_request.should eql('true')
79
89
  response.tokens.length.should eql(2)
80
90
  response.tokens.first.status.should eql('Active')
91
+ response.error.code.should eql(1)
92
+ response.error.message.should eql('Failed')
93
+ end
94
+
95
+ it 'should be relationally equivalent to its children' do
96
+ (Relax::Response === TestResponse).should be_true
81
97
  end
82
98
  end
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.2
3
3
  specification_version: 1
4
4
  name: relax
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.0.1
7
- date: 2007-09-24 00:00:00 -04:00
6
+ version: 0.0.2
7
+ date: 2007-10-10 00:00:00 -04:00
8
8
  summary: A simple library for creating REST consumers.
9
9
  require_paths:
10
10
  - lib
@@ -32,8 +32,8 @@ files:
32
32
  - lib/relax.rb
33
33
  - lib/relax
34
34
  - lib/relax/response.rb
35
- - lib/relax/api.rb
36
35
  - lib/relax/request.rb
36
+ - lib/relax/service.rb
37
37
  - lib/relax/query.rb
38
38
  - lib/relax/symbolic_hash.rb
39
39
  - README
@@ -62,5 +62,5 @@ dependencies:
62
62
  requirements:
63
63
  - - ">="
64
64
  - !ruby/object:Gem::Version
65
- version: "0.5"
65
+ version: "0.6"
66
66
  version:
data/lib/relax/api.rb DELETED
@@ -1,49 +0,0 @@
1
- require 'openssl'
2
- require 'net/https'
3
- require 'uri'
4
- require 'date'
5
- require 'base64'
6
- require 'erb'
7
-
8
- module Relax
9
- class API
10
- attr_reader :endpoint
11
-
12
- def initialize(endpoint)
13
- @endpoint = URI::parse(endpoint)
14
- end
15
-
16
- private
17
-
18
- def default_query
19
- Query.new
20
- end
21
-
22
- def query(request)
23
- Query.new(default_query.merge(request.to_query))
24
- end
25
-
26
- def call(request, response_class)
27
- uri = @endpoint.clone
28
- uri.query = query(request).to_s
29
- response = request(uri)
30
- puts "Response:\n#{response.body}\n\n" if $DEBUG
31
- response_class.new(response.body)
32
- end
33
-
34
- def request(uri)
35
- puts "Request:\n#{uri.to_s}\n\n" if $DEBUG
36
- http = Net::HTTP.new(uri.host, uri.port)
37
-
38
- if uri.scheme == 'https'
39
- http.use_ssl = true
40
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
41
- end
42
-
43
- http.start do |http|
44
- request = Net::HTTP::Get.new("#{uri.path}?#{uri.query}")
45
- http.request(request)
46
- end
47
- end
48
- end
49
- end