dbalatero-relax 0.0.7.1

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