tylerhunt-relax 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,95 @@
1
+ require 'relax/query'
2
+
3
+ module Relax
4
+ # Request is intended to be a parent class for requests passed to
5
+ # Service#call.
6
+ class Request
7
+ @parameters = {}
8
+
9
+ # New takes an optional hash of default parameter values. When passed,
10
+ # the values will be set on the request if the key exists as a valid
11
+ # parameter name.
12
+ def initialize(defaults = {})
13
+ # initialize default parameter values
14
+ self.class.parameters.each do |parameter, options|
15
+ if defaults.has_key?(parameter)
16
+ value = defaults[parameter]
17
+ elsif options[:value]
18
+ value = options[:value]
19
+ end
20
+
21
+ instance_variable_set("@#{parameter}", value) if value
22
+ end
23
+ end
24
+
25
+ # Converts this request into a Query object.
26
+ def to_query
27
+ self.class.parameters.keys.inject(Query.new) do |query, key|
28
+ value = send(key)
29
+ options = self.class.parameters[key]
30
+ if value && !options[:type]
31
+ query[convert_key(key)] = value if value
32
+ elsif options[:type]
33
+ options[:type].parameters.each do |parameter, options|
34
+ query[convert_complex_key(key, parameter)] = value.send(parameter) if value
35
+ end
36
+ end
37
+ query
38
+ end
39
+ end
40
+
41
+ # Converts this request into a query string for use in a URL.
42
+ def to_s
43
+ to_query.to_s
44
+ end
45
+
46
+ # Converts a key when the Request is converted to a query. By default, no
47
+ # conversion actually takes place, but this method can be overridden by
48
+ # child classes to perform standard manipulations, such as replacing
49
+ # underscores.
50
+ def convert_key(key)
51
+ key
52
+ end
53
+ protected :convert_key
54
+
55
+ # Converts a complex key (i.e. a parameter with a custom type) when the
56
+ # Request is converted to a query. By default, this means the key name and
57
+ # the parameter name separated by two underscores. This method can be
58
+ # overridden by child classes.
59
+ def convert_complex_key(key, parameter)
60
+ "#{convert_key(key)}.#{convert_key(parameter)}"
61
+ end
62
+ protected :convert_complex_key
63
+
64
+ class << self
65
+ # Create the parameters hash for the subclass.
66
+ def inherited(subclass) #:nodoc:
67
+ subclass.instance_variable_set('@parameters', {})
68
+ end
69
+
70
+ # Specifies a parameter to create on the request class.
71
+ #
72
+ # Options:
73
+ # - <tt>:type</tt>: An optional custom data type for the parameter.
74
+ # This must be a class that is a descendent of Request.
75
+ # - <tt>:value</tt>: The default value for this parameter.
76
+ def parameter(name, options = {})
77
+ attr_accessor name
78
+ options = @parameters[name].merge(options) if @parameters.has_key?(name)
79
+ @parameters[name] = options
80
+ end
81
+
82
+ # Adds a template value to a request class. Equivalent to creating a
83
+ # parameter with a default value.
84
+ def []=(key, value)
85
+ parameter(key, {:value => value})
86
+ end
87
+
88
+ # Returns a hash of all of the parameters for this request, including
89
+ # those that are inherited.
90
+ def parameters #:nodoc:
91
+ (superclass.respond_to?(:parameters) ? superclass.parameters : {}).merge(@parameters)
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,78 @@
1
+ module Relax
2
+ # Response is intended to be a parent class for responses passed to
3
+ # Service#call.
4
+ #
5
+ # A response is in essence an object used to facilitate XML parsing. It
6
+ # stores an XML document, and provides access to it through methods like
7
+ # #element and #attribute.
8
+ class Response
9
+ attr_accessor :raw
10
+
11
+ # New takes in and parses the raw response.
12
+ #
13
+ # This will raise a MissingParameter error if a parameterd marked as
14
+ # required is not present in the parsed response.
15
+ def initialize(xml)
16
+ @raw = xml
17
+ @parser = Relax::Parsers::Factory.get(parser_name).new(xml.to_s, self)
18
+ end
19
+
20
+ def parser_name
21
+ self.class.instance_variable_get('@parser') || :default
22
+ end
23
+
24
+ def method_missing(method, *args) #:nodoc:
25
+ if @parser.respond_to?(method)
26
+ @parser.__send__(method, *args)
27
+ else
28
+ super
29
+ end
30
+ end
31
+
32
+ class << self
33
+ # When a Response is extended, the superclass's parameters are copied
34
+ # into the new class. This behavior has the following side-effect: if
35
+ # parameters are added to the superclass after it has been extended,
36
+ # those new paramters won't be passed on to its children. This shouldn't
37
+ # be a problem in most cases.
38
+ def inherited(subclass)
39
+ @parameters.each do |name, options|
40
+ subclass.parameter(name, options)
41
+ end if @parameters
42
+ subclass.parser(@parser) if @parser
43
+ end
44
+
45
+ # Specifes a parameter that will be automatically parsed when the
46
+ # Response is instantiated.
47
+ #
48
+ # Options:
49
+ # - <tt>:attribute</tt>: An attribute name to use, or <tt>true</tt> to
50
+ # use the <tt>:element</tt> value as the attribute name on the root.
51
+ # - <tt>:collection</tt>: A class used to instantiate each item when
52
+ # selecting a collection of elements.
53
+ # - <tt>:element</tt>: The XML element name.
54
+ # - <tt>:object</tt>: A class used to instantiate an element.
55
+ # - <tt>:type</tt>: The type of the parameter. Should be one of
56
+ # <tt>:text</tt>, <tt>:integer</tt>, <tt>:float</tt>, or <tt>:date</tt>.
57
+ def parameter(name, options = {})
58
+ attr_accessor name
59
+ @parameters ||= {}
60
+ @parameters[name] = options
61
+ end
62
+
63
+ # Specifies the parser to use when decoding the server response. If
64
+ # no parser is specified for the response, then the default parser will
65
+ # be used.
66
+ #
67
+ # See Relax::Parsers for a list of available parsers.
68
+ def parser(name)
69
+ @parser ||= name
70
+ end
71
+
72
+ def ===(response)
73
+ response.is_a?(Class) ? response.ancestors.include?(self) : super
74
+ end
75
+ end
76
+
77
+ end
78
+ 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
@@ -0,0 +1,79 @@
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.
18
+ class SymbolicHash < Hash
19
+ def initialize(constructor = {})
20
+ if constructor.is_a?(Hash)
21
+ super()
22
+ update(constructor)
23
+ else
24
+ super(constructor)
25
+ end
26
+ end
27
+
28
+ def [](key)
29
+ super(convert_key(key))
30
+ end
31
+
32
+ def []=(key, value)
33
+ super(convert_key(key), convert_value(value))
34
+ end
35
+
36
+ def update(other_hash)
37
+ other_hash.each_pair { |key, value| store(convert_key(key), convert_value(value)) }
38
+ self
39
+ end
40
+ alias :merge! :update
41
+
42
+ def fetch(key, *extras)
43
+ super(convert_key(key), *extras)
44
+ end
45
+
46
+ def values_at(*indices)
47
+ indices.collect { |key| self[convert_key(key)] }
48
+ end
49
+
50
+ def dup
51
+ SymbolicHash.new(self)
52
+ end
53
+
54
+ def merge(hash)
55
+ self.dup.update(hash)
56
+ end
57
+
58
+ def delete(key)
59
+ super(convert_key(key))
60
+ end
61
+
62
+ def key?(key)
63
+ super(convert_key(key))
64
+ end
65
+ alias :include? :key?
66
+ alias :has_key? :key?
67
+ alias :member? :key?
68
+
69
+ protected
70
+
71
+ def convert_key(key)
72
+ !key.kind_of?(Symbol) ? key.to_sym : key
73
+ end
74
+
75
+ def convert_value(value)
76
+ value
77
+ end
78
+ end
79
+ end
data/lib/relax.rb ADDED
@@ -0,0 +1,13 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
2
+
3
+ require 'relax/query'
4
+ require 'relax/parsers'
5
+ require 'relax/request'
6
+ require 'relax/response'
7
+ require 'relax/service'
8
+ require 'relax/symbolic_hash'
9
+
10
+ module Relax
11
+ class MissingParameter < ArgumentError ; end
12
+ class UnrecognizedParser < ArgumentError ; end
13
+ end
@@ -0,0 +1,29 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ require 'relax'
4
+ require 'relax/parsers/factory'
5
+
6
+ class TestParser ; end
7
+
8
+ describe 'a parser factory' do
9
+
10
+ before(:each) do
11
+ @factory = Relax::Parsers::Factory
12
+ Relax::Parsers::Factory.register(:test, TestParser)
13
+ end
14
+
15
+ it 'should raise UnrecognizedParser for un-registered names' do
16
+ lambda {
17
+ @factory.get(:bad_name)
18
+ }.should raise_error(Relax::UnrecognizedParser)
19
+ end
20
+
21
+ it 'should return a registered parser class' do
22
+ @factory.get(:test).should ==TestParser
23
+ end
24
+
25
+ it 'should register the first registered parser as the default' do
26
+ @factory.get(:default).should ==Relax::Parsers::Hpricot
27
+ end
28
+
29
+ end
@@ -0,0 +1,35 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+ require File.dirname(__FILE__) + '/../parser_helper'
3
+
4
+
5
+ class HpricotTestResponse < Relax::Response
6
+ class Token < Relax::Response
7
+ parser :hpricot
8
+ parameter :token_id, :element => :tokenid
9
+ parameter :status
10
+ end
11
+
12
+ class Error < Relax::Response
13
+ parser :hpricot
14
+ parameter :code, :type => :integer
15
+ parameter :message
16
+ end
17
+
18
+ parser :hpricot
19
+ parameter :status, :required => true
20
+ parameter :request_id, :element => :requestid, :type => :integer
21
+ parameter :valid_request, :element => :requestid, :attribute => :valid
22
+ parameter :tokens, :collection => Token
23
+ parameter :error, :type => Error
24
+ end
25
+
26
+
27
+ describe 'an Hpricot parser' do
28
+
29
+ before(:each) do
30
+ @response = HpricotTestResponse.new(XML)
31
+ end
32
+
33
+ it_should_behave_like 'a successfully parsed response'
34
+
35
+ end
@@ -0,0 +1,40 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+ require File.dirname(__FILE__) + '/../parser_helper'
3
+
4
+
5
+ class RexmlTestResponse < Relax::Response
6
+ class Token < Relax::Response
7
+ parser :rexml
8
+ parameter :token_id, :element => 'TokenId'
9
+ parameter :status, :element => 'Status'
10
+ end
11
+
12
+ class Error < Relax::Response
13
+ parser :rexml
14
+ parameter :code, :element => 'Code', :type => :integer
15
+ parameter :message, :element => 'Message'
16
+ end
17
+
18
+ parser :rexml
19
+ parameter :status, :element => 'Status', :required => true
20
+ parameter :request_id, :element => 'RequestId', :type => :integer
21
+ parameter :valid_request, :element => 'RequestId', :attribute => :valid
22
+ parameter :namespace, :element => 'Namespace', :namespace => 'ns1'
23
+ parameter :tokens, :element => 'Tokens', :collection => Token
24
+ parameter :error, :element => 'Error', :type => Error
25
+ end
26
+
27
+
28
+ describe 'a REXML parser' do
29
+
30
+ before(:each) do
31
+ @response = RexmlTestResponse.new(XML)
32
+ end
33
+
34
+ it_should_behave_like 'a successfully parsed response'
35
+
36
+ it 'should parse namespaced parameters' do
37
+ @response.namespace.should eql('Passed')
38
+ end
39
+
40
+ end
@@ -0,0 +1,60 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ require 'relax/query'
4
+
5
+ describe 'a query' do
6
+ before(:each) do
7
+ @uri = URI::parse('http://example.com/?action=search&query=keyword')
8
+ @query = Relax::Query.new
9
+ end
10
+
11
+ it 'should convert to a query string' do
12
+ @query[:action] = 'Search'
13
+ @query[:query] = 'strings'
14
+ @query.to_s.should eql('action=Search&query=strings')
15
+ end
16
+
17
+ it 'should convert its values to strings' do
18
+ date = Date.today
19
+ @query[:date] = date
20
+ @query.to_s.should eql("date=#{date.to_s}")
21
+ end
22
+
23
+ it 'should escape its values using "+" instead of "%20"' do
24
+ Relax::Query.send(:escape_value, 'two words').should == 'two+words'
25
+ end
26
+
27
+ it 'should sort its parameters' do
28
+ @query[:charlie] = 3
29
+ @query[:alpha] = 1
30
+ @query[:bravo] = 2
31
+ @query.to_s.should eql('alpha=1&bravo=2&charlie=3')
32
+ end
33
+
34
+ it 'should encode its parameter values' do
35
+ @query[:spaces] = 'two words'
36
+ @query[:url] = 'http://example.com/'
37
+ @query.to_s.should eql('spaces=two+words&url=http%3A%2F%2Fexample.com%2F')
38
+ end
39
+
40
+ it 'should be able to parse query strings' do
41
+ parsed_query = Relax::Query.parse(@uri)
42
+ parsed_query[:action].should eql('search')
43
+ parsed_query[:query].should eql('keyword')
44
+ end
45
+
46
+ it 'should parse key value pairs into only two parts' do
47
+ parsed_query = Relax::Query.parse(URI.parse("http://example.com/?action=test=&foo=bar"))
48
+ parsed_query[:action].should eql('test=')
49
+ end
50
+
51
+ it 'should unescape query string key-value pair keys' do
52
+ parsed_query = Relax::Query.parse(URI.parse("http://example.com/?action%20helper=test"))
53
+ parsed_query[:"action helper"].should eql('test')
54
+ end
55
+
56
+ it 'should unescape query string key-value pair values' do
57
+ parsed_query = Relax::Query.parse(URI.parse("http://example.com/?action=test%20action"))
58
+ parsed_query[:action].should eql('test action')
59
+ end
60
+ end
@@ -0,0 +1,108 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ require 'relax/request'
4
+
5
+ class Amount < Relax::Request
6
+ parameter :amount
7
+ parameter :currency
8
+ end
9
+
10
+ class TestRequest < Relax::Request
11
+ parameter :action
12
+ parameter :token_id
13
+ parameter :user_id
14
+ parameter :amount, :type => Amount
15
+ end
16
+
17
+ class ChildRequest < TestRequest
18
+ parameter :child_id
19
+ end
20
+
21
+ describe 'an option initialized request', :shared => true do
22
+ it 'should have its values set by the options hash' do
23
+ request = TestRequest.new(:action => 'FetchAll', :token_id => 123)
24
+ request.action.should eql('FetchAll')
25
+ request.token_id.should eql(123)
26
+ request.user_id.should be_nil
27
+ end
28
+ end
29
+
30
+ describe 'a request that converts to a query', :shared => true do
31
+ before(:each) do
32
+ @query = TestRequest.new(:action => 'Search', :token_id => 123).to_query
33
+ end
34
+
35
+ it 'should include its parameters in the query' do
36
+ @query[:action].should eql('Search')
37
+ @query[:token_id].should eql('123')
38
+ @query[:user_id].should be_nil
39
+ @query[:amount].should be_nil
40
+ end
41
+
42
+ it 'should only include parameters in the query if they are set' do
43
+ @query.key?(:action).should be_true
44
+ @query.key?(:token_id).should be_true
45
+ @query.key?(:user_id).should be_false
46
+ @query.key?(:amount).should be_false
47
+ end
48
+ end
49
+
50
+ describe 'a normal request' do
51
+ it_should_behave_like 'a request that converts to a query'
52
+ it_should_behave_like 'an option initialized request'
53
+ end
54
+
55
+ describe 'a template request' do
56
+ it_should_behave_like 'a request that converts to a query'
57
+ it_should_behave_like 'an option initialized request'
58
+
59
+ before(:each) do
60
+ # this syntax may need to go away unless we can find a way to make it work
61
+ TestRequest[:api_key] = '123456'
62
+ TestRequest[:secret] = 'shhh!'
63
+ end
64
+
65
+ it 'should always have the template values in its query' do
66
+ request = TestRequest.new
67
+ request.api_key.should eql('123456')
68
+ request.secret.should eql('shhh!')
69
+ end
70
+
71
+ it 'should allow its template variables to be overridden' do
72
+ request = TestRequest.new(:secret => 'abracadabra')
73
+ request.api_key.should eql('123456')
74
+ request.secret.should eql('abracadabra')
75
+ end
76
+
77
+ it 'should pass its template on to its children' do
78
+ request = ChildRequest.new
79
+ request.api_key.should eql('123456')
80
+ request.secret.should eql('shhh!')
81
+ end
82
+
83
+ it 'should allow template parameters on its children that are additive' do
84
+ ChildRequest[:query] = '1a2b3c'
85
+ child = ChildRequest.new
86
+ child.api_key.should eql('123456')
87
+ child.secret.should eql('shhh!')
88
+ child.query.should eql('1a2b3c')
89
+
90
+ parent = TestRequest.new
91
+ parent.api_key.should eql('123456')
92
+ parent.secret.should eql('shhh!')
93
+ parent.respond_to?(:query).should be_false
94
+ end
95
+ end
96
+
97
+ describe 'a request with a custom type' do
98
+ before(:each) do
99
+ request = TestRequest.new(:action => 'Pay', :token_id => 123)
100
+ request.amount = Amount.new(:amount => 3.50, :currency => 'USD')
101
+ @query = request.to_query
102
+ end
103
+
104
+ it 'should add the type parameters to the query' do
105
+ @query.key?(:"amount.amount").should be_true
106
+ @query.key?(:"amount.currency").should be_true
107
+ end
108
+ end