relax 0.0.1 → 0.0.2

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