dbalatero-relax 0.0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/relax.rb +13 -0
- data/lib/relax/parsers.rb +13 -0
- data/lib/relax/parsers/base.rb +30 -0
- data/lib/relax/parsers/factory.rb +29 -0
- data/lib/relax/parsers/hpricot.rb +133 -0
- data/lib/relax/parsers/rexml.rb +147 -0
- data/lib/relax/query.rb +46 -0
- data/lib/relax/request.rb +107 -0
- data/lib/relax/response.rb +82 -0
- data/lib/relax/service.rb +102 -0
- data/lib/relax/symbolic_hash.rb +79 -0
- data/spec/parser_helper.rb +49 -0
- data/spec/parsers/factory_spec.rb +29 -0
- data/spec/parsers/hpricot_spec.rb +31 -0
- data/spec/parsers/rexml_spec.rb +36 -0
- data/spec/query_spec.rb +60 -0
- data/spec/request_spec.rb +114 -0
- data/spec/response_spec.rb +98 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/symbolic_hash_spec.rb +67 -0
- metadata +72 -0
@@ -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
|
data/spec/query_spec.rb
ADDED
@@ -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
|