dbalatero-relax 0.0.7.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,82 @@
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 #:nodoc:
21
+ self.class.instance_variable_get('@parser') || :default
22
+ end
23
+
24
+ def node_name(name, namespace=nil) #:nodoc:
25
+ "#{namespace.to_s + ':' if namespace}#{name}"
26
+ end
27
+
28
+ def method_missing(method, *args) #:nodoc:
29
+ if @parser.respond_to?(method)
30
+ @parser.__send__(method, *args)
31
+ else
32
+ super
33
+ end
34
+ end
35
+
36
+ class << self
37
+ # When a Response is extended, the superclass's parameters are copied
38
+ # into the new class. This behavior has the following side-effect: if
39
+ # parameters are added to the superclass after it has been extended,
40
+ # those new paramters won't be passed on to its children. This shouldn't
41
+ # be a problem in most cases.
42
+ def inherited(subclass)
43
+ @parameters.each do |name, options|
44
+ subclass.parameter(name, options)
45
+ end if @parameters
46
+
47
+ subclass.parser(@parser) if @parser
48
+ end
49
+
50
+ # Specifes a parameter that will be automatically parsed when the
51
+ # Response is instantiated.
52
+ #
53
+ # Options:
54
+ # - <tt>:attribute</tt>: An attribute name to use, or <tt>true</tt> to
55
+ # use the <tt>:element</tt> value as the attribute name on the root.
56
+ # - <tt>:collection</tt>: A class used to instantiate each item when
57
+ # selecting a collection of elements.
58
+ # - <tt>:element</tt>: The XML element name.
59
+ # - <tt>:object</tt>: A class used to instantiate an element.
60
+ # - <tt>:type</tt>: The type of the parameter. Should be one of
61
+ # <tt>:text</tt>, <tt>:integer</tt>, <tt>:float</tt>, or <tt>:date</tt>.
62
+ def parameter(name, options={})
63
+ attr_accessor name
64
+ @parameters ||= {}
65
+ @parameters[name] = options
66
+ end
67
+
68
+ # Specifies the parser to use when decoding the server response. If no
69
+ # parser is specified for the response, then the default parser will be
70
+ # used.
71
+ #
72
+ # See Relax::Parsers for a list of available parsers.
73
+ def parser(name)
74
+ @parser ||= name
75
+ end
76
+
77
+ def ===(response)
78
+ response.is_a?(Class) ? response.ancestors.include?(self) : super
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,102 @@
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
+ request.valid?
70
+ uri = @endpoint.clone
71
+ uri.query = query(request).to_s
72
+ response = request(uri)
73
+ puts "Response:\n#{response.body}\n\n" if $DEBUG
74
+ response_class.new(response.body)
75
+ end
76
+
77
+ private
78
+
79
+ def default_query
80
+ Query.new
81
+ end
82
+
83
+ def query(request)
84
+ Query.new(default_query.merge(request.to_query))
85
+ end
86
+
87
+ def request(uri)
88
+ puts "Request:\n#{uri.to_s}\n\n" if $DEBUG
89
+ http = Net::HTTP.new(uri.host, uri.port)
90
+
91
+ if uri.scheme == 'https'
92
+ http.use_ssl = true
93
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
94
+ end
95
+
96
+ http.start do |http|
97
+ request = Net::HTTP::Get.new("#{uri.path}?#{uri.query}")
98
+ http.request(request)
99
+ end
100
+ end
101
+ end
102
+ 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
+ def convert_key(key)
70
+ !key.kind_of?(Symbol) ? key.to_sym : key
71
+ end
72
+ protected :convert_key
73
+
74
+ def convert_value(value)
75
+ value
76
+ end
77
+ protected :convert_value
78
+ end
79
+ end
@@ -0,0 +1,49 @@
1
+ describe 'a successfully parsed response', :shared => true do
2
+ it 'should allow access to the root' do
3
+ root = @response.root
4
+ root.should_not be_nil
5
+ root.name.should eql('RESTResponse')
6
+ end
7
+
8
+ it 'should be checkable by the name of its root' do
9
+ @response.is?(:RESTResponse).should be_true
10
+ end
11
+
12
+ it 'should allow access to an element by its name' do
13
+ @response.element(:RequestId).should_not be_nil
14
+ end
15
+
16
+ it 'should allow access to an element\'s elements by its name' do
17
+ tokens = @response.elements(:Tokens)
18
+ tokens.should respond_to(:each)
19
+ tokens.should_not be_empty
20
+ end
21
+
22
+ it 'should allow access to an element\'s value by its name' do
23
+ token = Relax::Response.new(@response.elements(:Tokens).first)
24
+ token.element(:TokenId).inner_text.should eql('JPMQARDVJK')
25
+ token.element(:Status).inner_text.should eql('Active')
26
+ end
27
+
28
+ it 'should have a means of checking for the existence of a node' do
29
+ @response.has?(:Status).should_not be_nil
30
+ @response.has?(:Errors).should be_nil
31
+ end
32
+
33
+ it 'should set known parameters' do
34
+ @response.status.should eql('Success')
35
+ @response.request_id.should eql(44287)
36
+ @response.valid_request.should eql("true")
37
+ end
38
+
39
+ it 'should automatically pull parameters from the XML' do
40
+ @response.tokens.length.should eql(2)
41
+ @response.tokens.first.status.should eql('Active')
42
+ @response.error.code.should eql(1)
43
+ @response.error.message.should eql('Failed')
44
+ end
45
+
46
+ it 'should raise MissingParameter if required parameters are missing' do
47
+ proc { @response.class.new('') }.should raise_error(Relax::MissingParameter)
48
+ end
49
+ 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,31 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+ require File.dirname(__FILE__) + '/../parser_helper'
3
+
4
+ class HpricotTestResponse < Relax::Response
5
+ class Token < Relax::Response
6
+ parser :hpricot
7
+ parameter :token_id, :element => :tokenid
8
+ parameter :status
9
+ end
10
+
11
+ class Error < Relax::Response
12
+ parser :hpricot
13
+ parameter :code, :type => :integer
14
+ parameter :message
15
+ end
16
+
17
+ parser :hpricot
18
+ parameter :status, :required => true
19
+ parameter :request_id, :element => :requestid, :type => :integer
20
+ parameter :valid_request, :element => :requestid, :attribute => :valid
21
+ parameter :tokens, :collection => Token
22
+ parameter :error, :type => Error
23
+ end
24
+
25
+ describe 'an Hpricot parser' do
26
+ before(:each) do
27
+ @response = HpricotTestResponse.new(XML)
28
+ end
29
+
30
+ it_should_behave_like 'a successfully parsed response'
31
+ end
@@ -0,0 +1,36 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+ require File.dirname(__FILE__) + '/../parser_helper'
3
+
4
+ class RexmlTestResponse < Relax::Response
5
+ class Token < Relax::Response
6
+ parser :rexml
7
+ parameter :token_id, :element => 'TokenId'
8
+ parameter :status, :element => 'Status'
9
+ end
10
+
11
+ class Error < Relax::Response
12
+ parser :rexml
13
+ parameter :code, :element => 'Code', :type => :integer
14
+ parameter :message, :element => 'Message'
15
+ end
16
+
17
+ parser :rexml
18
+ parameter :status, :element => 'Status', :required => true
19
+ parameter :request_id, :element => 'RequestId', :type => :integer
20
+ parameter :valid_request, :element => 'RequestId', :attribute => :valid
21
+ parameter :namespace, :element => 'Namespace', :namespace => 'ns1'
22
+ parameter :tokens, :element => 'Tokens', :collection => Token
23
+ parameter :error, :element => 'Error', :type => Error
24
+ end
25
+
26
+ describe 'a REXML parser' do
27
+ before(:each) do
28
+ @response = RexmlTestResponse.new(XML)
29
+ end
30
+
31
+ it_should_behave_like 'a successfully parsed response'
32
+
33
+ it 'should parse namespaced parameters' do
34
+ @response.namespace.should eql('Passed')
35
+ end
36
+ 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