tylerhunt-relax 0.0.5

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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007-2008 Tyler Hunt
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,171 @@
1
+ = Relax
2
+
3
+ Relax is a simple library that provides a foundation for writing REST consumer
4
+ APIs, including the logic to handle the HTTP requests, build URLs with query
5
+ parameters, and parse XML responses.
6
+
7
+ It provides a basic set of functionality common to most REST consumers:
8
+
9
+ - building HTTP queries (Relax::Request)
10
+ - issuing HTTP requests (Relax::Service)
11
+ - parsing XML responses (Relax::Response)
12
+
13
+
14
+ == Tutorial
15
+
16
+ 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.
17
+
18
+ === Step 1
19
+
20
+ In the first step we're going to simply include Relax, and define the basis for
21
+ our Service class.
22
+
23
+ require 'rubygems'
24
+ require 'relax'
25
+
26
+ module Flickr
27
+ class Service < Relax::Service
28
+ ENDPOINT = 'http://api.flickr.com/services/rest/'
29
+
30
+ def initialize
31
+ super(ENDPOINT)
32
+ end
33
+ end
34
+ end
35
+
36
+
37
+ === Step 2
38
+
39
+ Next we're going to define common Request and Response classes for use
40
+ throughout our API. This gives us a single point to add any shared
41
+ functionality. For Flickr, this means that each request will have a "method"
42
+ parameter, and each response will have a "stat" attribute that will equal "ok"
43
+ when the response comes back without any errors.
44
+
45
+ module Flickr
46
+ class Request < Relax::Request
47
+ parameter :method
48
+ end
49
+
50
+ class Response < Relax::Response
51
+ def successful?
52
+ root[:stat] == 'ok'
53
+ end
54
+ end
55
+ end
56
+
57
+ While we're at it, we're also going to add a new line to the constructor from
58
+ our service to make sure that our Flickr API key gets passed along with each
59
+ request as well.
60
+
61
+ module Flickr
62
+ class Service < Relax::Service
63
+ ENDPOINT = 'http://api.flickr.com/services/rest/'
64
+
65
+ def initialize(api_key)
66
+ super(ENDPOINT)
67
+ Request[:api_key] = api_key
68
+ end
69
+ end
70
+ end
71
+
72
+ When we call our Request class as we have here, we're basically setting up a
73
+ value on our request that acts like a template. Each request we create now will
74
+ have the api_key property prepopulated for us.
75
+
76
+
77
+ === Step 3
78
+
79
+ Next, we're going to need a basic Photo class to represent photos that Flickr
80
+ returns to us.
81
+
82
+ module Flickr
83
+ class Photo < Response
84
+ parameter :id, :attribute => true, :type => :integer
85
+ parameter :title, :attribute => true
86
+ end
87
+ end
88
+
89
+ Here we're creating a Response class that extends our Flickr::Response, which
90
+ has two parameters: "id" and "title." By setting the attribute option to true,
91
+ we're telling Relax to look for an attribute by that name on the XML root
92
+ instead of checking for an element by that name. The type options can be used
93
+ to specify what type of data we're expecting the response to give us. The
94
+ default type is string.
95
+
96
+
97
+ === Step 4
98
+
99
+ Now we arrive at the final piece of the puzzle: a service call module. To keep
100
+ things contained, a Relax best practice is to create a module for each call
101
+ on your service. The one we're creating here is the PhotoSearch module for the
102
+ "flickr.photos.search" call on the Flickr API.
103
+
104
+ There are three main pieces to every service call module:
105
+
106
+ 1. a Relax::Request object
107
+ 2. a Relax::Response object
108
+ 3. a call method that calls Relax::Service#call
109
+
110
+ Here's what the PhotoSearch module looks like:
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 now needs to be included into our Flickr::Service class,
141
+ and then we'll be able to use it by calling either of the call methods.
142
+
143
+ module Flickr
144
+ class Service < Relax::Service
145
+ include Flickr::PhotoSearch
146
+
147
+ ENDPOINT = 'http://api.flickr.com/services/rest/'
148
+
149
+ def initialize(api_key)
150
+ super(ENDPOINT)
151
+ Request[:api_key] = api_key
152
+ end
153
+ end
154
+ end
155
+
156
+ Now we're ready to make a call against the API:
157
+
158
+ flickr = Flickr::Service.new(ENV['FLICKR_API_KEY'])
159
+ relax = flickr.find_by_tag('relax', :per_page => 10)
160
+
161
+ if relax.successful?
162
+ relax.photos.each do |photo|
163
+ puts "[#{photo.id}] #{photo.title}"
164
+ end
165
+ end
166
+
167
+ This will output the IDs and titles for the first 10 photos on Flickr that have
168
+ the tag "relax."
169
+
170
+
171
+ Copyright (c) 2007-2008 Tyler Hunt, released under the MIT license
@@ -0,0 +1,34 @@
1
+ module Relax
2
+ module Parsers
3
+
4
+ class Base
5
+
6
+ attr_reader :parent
7
+ attr_reader :parameters
8
+
9
+ def initialize(raw, parent)
10
+ @parent = parent
11
+ @parameters = parent.class.instance_variable_get('@parameters')
12
+ parse!
13
+ end
14
+
15
+ def parse!; end
16
+
17
+ def root; end
18
+ def is?(name); end
19
+ def has?(name); end
20
+ def element(name); end
21
+ def elements(name); end
22
+
23
+ def attribute(element, name); end
24
+ def value(value); end
25
+ def text_value(value); end
26
+ def integer_value(value); end
27
+ def float_value(value); end
28
+ def date_value(value); end
29
+ def time_value(value); end
30
+
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ module Relax
2
+ module Parsers
3
+
4
+ ##
5
+ # Manages the Relax::Parsers in the library.
6
+ #
7
+ module Factory
8
+
9
+ class << self
10
+
11
+ ##
12
+ # Returns the parser class which has been registered for the given
13
+ # +name+.
14
+ #
15
+ def get(name)
16
+ @@parsers ||= {}
17
+ @@parsers[name] || raise(UnrecognizedParser, "Given parser name not recognized: #{name.inspect}. Expected one of: #{@@parsers.keys.inspect}")
18
+ end
19
+
20
+ ##
21
+ # Registers a new parser with the factory. The +name+ should be unique,
22
+ # but if not, it will override the previously defined parser for the
23
+ # given +name+.
24
+ #
25
+ def register(name, klass)
26
+ @@parsers ||= {}
27
+ @@parsers[:default] = klass if @@parsers.empty?
28
+ @@parsers[name] = klass
29
+ end
30
+
31
+ ##
32
+ # Removes all registered parsers from the factory.
33
+ #
34
+ def clear!
35
+ @@parsers = {}
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,145 @@
1
+ require 'rubygems'
2
+ require 'hpricot'
3
+
4
+ module Relax
5
+ module Parsers
6
+
7
+ ##
8
+ # Parses the server's raw response using the Hpricot library.
9
+ #
10
+ class Hpricot < Base
11
+
12
+ FACTORY_NAME = :hpricot
13
+
14
+ def initialize(raw, parent)
15
+ @xml = ::Hpricot.XML(raw)
16
+ super(raw, parent)
17
+ end
18
+
19
+ def parse!
20
+ if parameters
21
+ parameters.each do |parameter, options|
22
+ begin
23
+ element = options[:element] || parameter
24
+
25
+ if attribute = options[:attribute] and attribute == true
26
+ node = attribute(root, element)
27
+ elsif attribute
28
+ node = attribute(element(element), attribute)
29
+ elsif options[:collection]
30
+ node = elements(element)
31
+ else
32
+ node = element(element)
33
+ end
34
+
35
+ if options[:collection]
36
+ value = node.collect do |element|
37
+ options[:collection].new(element)
38
+ end
39
+ else
40
+ case type = options[:type]
41
+ when Response
42
+ value = type.new(node)
43
+
44
+ when :date
45
+ value = date_value(node)
46
+
47
+ when :time
48
+ value = time_value(node)
49
+
50
+ when :float
51
+ value = float_value(node)
52
+
53
+ when :integer
54
+ value = integer_value(node)
55
+
56
+ when :text
57
+ else
58
+ value = text_value(node)
59
+ end
60
+ end
61
+
62
+ parent.instance_variable_set("@#{parameter}", value)
63
+ rescue ::Hpricot::Error
64
+ raise Relax::MissingParameter if options[:required]
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ # Returns the root of the XML document.
71
+ def root
72
+ @xml.root
73
+ end
74
+
75
+ # Checks the name of the root node.
76
+ def is?(name)
77
+ root.name.gsub(/.*:(.*)/, '\1') == node_name(name)
78
+ end
79
+
80
+ # Returns a set of elements matching name.
81
+ def elements(name)
82
+ root.search(root_path(name))
83
+ end
84
+
85
+ # Returns an element of the specified name.
86
+ def element(name)
87
+ root.at(root_path(name))
88
+ end
89
+ alias :has? :element
90
+
91
+ # Returns an attribute on an element.
92
+ def attribute(element, name)
93
+ element[name]
94
+ end
95
+
96
+ # Gets the value of an element or attribute.
97
+ def value(value)
98
+ value.is_a?(::Hpricot::Elem) ? value.inner_text : value.to_s
99
+ end
100
+
101
+ # Gets a text value.
102
+ def text_value(value)
103
+ value(value)
104
+ end
105
+
106
+ # Gets an integer value.
107
+ def integer_value(value)
108
+ value(value).to_i
109
+ end
110
+
111
+ # Gets a float value.
112
+ def float_value(value)
113
+ value(value).to_f
114
+ end
115
+
116
+ # Gets a date value.
117
+ def date_value(value)
118
+ Date.parse(value(value))
119
+ end
120
+
121
+ # Gets a time value.
122
+ def time_value(value)
123
+ Time.parse(value(value))
124
+ end
125
+
126
+
127
+ private
128
+
129
+
130
+ # Converts a name to a node name.
131
+ def node_name(name)
132
+ name.to_s
133
+ end
134
+
135
+ # Gets the XPath expression representing the root node.
136
+ def root_path(name)
137
+ "/#{node_name(name)}"
138
+ end
139
+
140
+ end
141
+
142
+ Factory.register(Hpricot::FACTORY_NAME, Hpricot)
143
+
144
+ end
145
+ end
@@ -0,0 +1,158 @@
1
+ require 'rubygems'
2
+ require 'rexml/document'
3
+
4
+ module Relax
5
+ module Parsers
6
+
7
+ ##
8
+ # Parsers the server's response using the REXML library.
9
+ #
10
+ # Benefits:
11
+ #
12
+ # * XML Namespace support (parameter :foo, :namespace => 'bar')
13
+ #
14
+ # Drawbacks:
15
+ #
16
+ # * Case sensitive field names (<Status>..</> != parameter :status)
17
+ #
18
+ class REXML < Base
19
+
20
+ FACTORY_NAME = :rexml
21
+
22
+ def initialize(raw, parent)
23
+ @xml = ::REXML::Document.new(raw)
24
+ super(raw, parent)
25
+ end
26
+
27
+ def parse!
28
+ if parameters
29
+ parameters.each do |parameter, options|
30
+ begin
31
+ element = options[:element] || parameter
32
+ namespace = options[:namespace]
33
+
34
+ if attribute = options[:attribute] and attribute == true
35
+ node = attribute(root, element, namespace)
36
+ elsif attribute
37
+ node = attribute(element(element), attribute, namespace)
38
+ elsif options[:collection]
39
+ node = elements(element, namespace)
40
+ else
41
+ node = element(element, namespace)
42
+ end
43
+
44
+ if options[:collection]
45
+ value = node.collect do |element|
46
+ options[:collection].new(element.deep_clone)
47
+ end
48
+ else
49
+ case type = options[:type]
50
+ when Response
51
+ value = type.new(node)
52
+
53
+ when :date
54
+ value = date_value(node)
55
+
56
+ when :time
57
+ value = time_value(node)
58
+
59
+ when :float
60
+ value = float_value(node)
61
+
62
+ when :integer
63
+ value = integer_value(node)
64
+
65
+ when :text
66
+ else
67
+ value = text_value(node)
68
+ end
69
+ end
70
+
71
+ parent.instance_variable_set("@#{parameter}", value)
72
+ rescue
73
+ raise(Relax::MissingParameter) if node.nil? && options[:required]
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ # Returns the root of the XML document.
80
+ def root
81
+ @xml.root
82
+ end
83
+
84
+ # Checks the name of the root node.
85
+ def is?(name, namespace = nil)
86
+ root.name == node_name(name, nil)
87
+ end
88
+
89
+ # Returns a set of elements matching name.
90
+ def elements(name, namespace = nil)
91
+ root.get_elements(node_path(name, namespace))
92
+ end
93
+
94
+ # Returns an element of the specified name.
95
+ def element(name, namespace = nil)
96
+ root.elements[node_path(name, namespace)]
97
+ end
98
+ alias :has? :element
99
+
100
+ # Returns an attribute on an element.
101
+ def attribute(element, name, namespace = nil)
102
+ element.attribute(name)
103
+ end
104
+
105
+ # Gets the value of an element or attribute.
106
+ def value(value)
107
+ value.is_a?(::REXML::Element) ? value.text : value.to_s
108
+ end
109
+
110
+ # Gets a text value.
111
+ def text_value(value)
112
+ value(value)
113
+ end
114
+
115
+ # Gets an integer value.
116
+ def integer_value(value)
117
+ value(value).to_i
118
+ end
119
+
120
+ # Gets a float value.
121
+ def float_value(value)
122
+ value(value).to_f
123
+ end
124
+
125
+ # Gets a date value.
126
+ def date_value(value)
127
+ Date.parse(value(value))
128
+ end
129
+
130
+ # Gets a time value.
131
+ def time_value(value)
132
+ Time.parse(value(value))
133
+ end
134
+
135
+
136
+ private
137
+
138
+
139
+ # Converts a name to a node name.
140
+ def node_name(name, namespace = nil)
141
+ "#{namespace.to_s + ':' if namespace}#{name}"
142
+ end
143
+
144
+ # Gets the XPath expression representing the root node.
145
+ def root_path(name)
146
+ "/#{node_name(name)}"
147
+ end
148
+
149
+ def node_path(name, namespace = nil)
150
+ "#{node_name(name, namespace)}"
151
+ end
152
+
153
+ end
154
+
155
+ Factory.register(REXML::FACTORY_NAME, REXML)
156
+
157
+ end
158
+ end
@@ -0,0 +1,13 @@
1
+ require 'date'
2
+ require 'time'
3
+
4
+ require 'relax/parsers/factory'
5
+ require 'relax/parsers/base'
6
+
7
+ require 'relax/parsers/hpricot'
8
+ require 'relax/parsers/rexml'
9
+
10
+ module Relax
11
+ module Parsers
12
+ end
13
+ end
@@ -0,0 +1,46 @@
1
+ require 'cgi'
2
+ require 'uri'
3
+
4
+ require 'relax/symbolic_hash'
5
+
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.
9
+ class Query < SymbolicHash
10
+ # Converts the Query to a query string for use in a URL.
11
+ def to_s
12
+ keys.sort { |a, b| a.to_s <=> b.to_s }.collect do |key|
13
+ "#{key.to_s}=#{self.class.escape_value(fetch(key))}"
14
+ end.join('&')
15
+ end
16
+
17
+ class << self
18
+ # Parses a URL and returns a Query with its query portion.
19
+ def parse(uri)
20
+ query = uri.query.split('&').inject({}) do |query, parameter|
21
+ key, value = parameter.split('=', 2)
22
+ query[unescape_value(key)] = unescape_value(value)
23
+ query
24
+ end
25
+ self.new(query)
26
+ end
27
+
28
+ # Escapes a query parameter value.
29
+ def escape_value(value)
30
+ CGI.escape(value.to_s).gsub('%20', '+')
31
+ end
32
+
33
+ # Unescapes a query parameter value.
34
+ def unescape_value(value)
35
+ CGI.unescape(value)
36
+ end
37
+ end
38
+
39
+ protected
40
+
41
+ # Converts each value of the Query to a string as it's added.
42
+ def convert_value(value)
43
+ value.to_s
44
+ end
45
+ end
46
+ end