tylerhunt-relax 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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