tylerhunt-relax 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README +171 -0
- data/lib/relax/parsers/base.rb +34 -0
- data/lib/relax/parsers/factory.rb +43 -0
- data/lib/relax/parsers/hpricot.rb +145 -0
- data/lib/relax/parsers/rexml.rb +158 -0
- data/lib/relax/parsers.rb +13 -0
- data/lib/relax/query.rb +46 -0
- data/lib/relax/request.rb +95 -0
- data/lib/relax/response.rb +78 -0
- data/lib/relax/service.rb +101 -0
- data/lib/relax/symbolic_hash.rb +79 -0
- data/lib/relax.rb +13 -0
- data/spec/parsers/factory_spec.rb +29 -0
- data/spec/parsers/hpricot_spec.rb +35 -0
- data/spec/parsers/rexml_spec.rb +40 -0
- data/spec/query_spec.rb +60 -0
- data/spec/request_spec.rb +108 -0
- data/spec/response_spec.rb +98 -0
- data/spec/symbolic_hash_spec.rb +67 -0
- metadata +89 -0
@@ -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
|
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
|
@@ -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
|