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.
- 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
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,30 @@
|
|
1
|
+
module Relax
|
2
|
+
module Parsers
|
3
|
+
class Base
|
4
|
+
attr_reader :parent
|
5
|
+
attr_reader :parameters
|
6
|
+
|
7
|
+
def initialize(raw, parent)
|
8
|
+
@parent = parent
|
9
|
+
@parameters = parent.class.instance_variable_get('@parameters')
|
10
|
+
parse!
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse!; end
|
14
|
+
|
15
|
+
def root; end
|
16
|
+
def is?(name); end
|
17
|
+
def has?(name); end
|
18
|
+
def element(name); end
|
19
|
+
def elements(name); end
|
20
|
+
|
21
|
+
def attribute(element, name); end
|
22
|
+
def value(value); end
|
23
|
+
def text_value(value); end
|
24
|
+
def integer_value(value); end
|
25
|
+
def float_value(value); end
|
26
|
+
def date_value(value); end
|
27
|
+
def time_value(value); end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Relax
|
2
|
+
module Parsers
|
3
|
+
# Manages the Relax::Parsers in the library.
|
4
|
+
module Factory
|
5
|
+
class << self
|
6
|
+
# Returns the parser class which has been registered for the given
|
7
|
+
# +name+.
|
8
|
+
def get(name)
|
9
|
+
@@parsers ||= {}
|
10
|
+
@@parsers[name] || raise(UnrecognizedParser, "Given parser name not recognized: #{name.inspect}. Expected one of: #{@@parsers.keys.inspect}")
|
11
|
+
end
|
12
|
+
|
13
|
+
# Registers a new parser with the factory. The +name+ should be unique,
|
14
|
+
# but if not, it will override the previously defined parser for the
|
15
|
+
# given +name+.
|
16
|
+
def register(name, klass)
|
17
|
+
@@parsers ||= {}
|
18
|
+
@@parsers[:default] = klass if @@parsers.empty?
|
19
|
+
@@parsers[name] = klass
|
20
|
+
end
|
21
|
+
|
22
|
+
# Removes all registered parsers from the factory.
|
23
|
+
def clear!
|
24
|
+
@@parsers = {}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'hpricot'
|
3
|
+
|
4
|
+
module Relax
|
5
|
+
module Parsers
|
6
|
+
# Parses the server's raw response using the Hpricot library.
|
7
|
+
class Hpricot < Base
|
8
|
+
FACTORY_NAME = :hpricot
|
9
|
+
|
10
|
+
def initialize(raw, parent)
|
11
|
+
@xml = ::Hpricot.XML(raw)
|
12
|
+
super(raw, parent)
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse!
|
16
|
+
if parameters
|
17
|
+
parameters.each do |parameter, options|
|
18
|
+
begin
|
19
|
+
element = options[:element] || parameter
|
20
|
+
|
21
|
+
node = case
|
22
|
+
when options[:attribute] && options[:attribute] == true
|
23
|
+
attribute(root, element)
|
24
|
+
when options[:attribute]
|
25
|
+
attribute(element(element), options[:attribute])
|
26
|
+
when options[:collection]
|
27
|
+
elements(element)
|
28
|
+
else
|
29
|
+
element(element)
|
30
|
+
end
|
31
|
+
|
32
|
+
if options[:collection]
|
33
|
+
value = node.collect do |element|
|
34
|
+
options[:collection].new(element)
|
35
|
+
end
|
36
|
+
else
|
37
|
+
value = case type = options[:type]
|
38
|
+
when Response
|
39
|
+
type.new(node)
|
40
|
+
when :date
|
41
|
+
date_value(node)
|
42
|
+
when :time
|
43
|
+
time_value(node)
|
44
|
+
when :float
|
45
|
+
float_value(node)
|
46
|
+
when :integer
|
47
|
+
integer_value(node)
|
48
|
+
when :text
|
49
|
+
else
|
50
|
+
text_value(node)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
parent.instance_variable_set("@#{parameter}", value)
|
55
|
+
rescue ::Hpricot::Error
|
56
|
+
raise Relax::MissingParameter if options[:required]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns the root of the XML document.
|
63
|
+
def root
|
64
|
+
@xml.root
|
65
|
+
end
|
66
|
+
|
67
|
+
# Checks the name of the root node.
|
68
|
+
def is?(name)
|
69
|
+
root.name.gsub(/.*:(.*)/, '\1') == node_name(name)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns a set of elements matching name.
|
73
|
+
def elements(name)
|
74
|
+
root.search(root_path(name))
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns an element of the specified name.
|
78
|
+
def element(name)
|
79
|
+
root.at(root_path(name))
|
80
|
+
end
|
81
|
+
alias :has? :element
|
82
|
+
|
83
|
+
# Returns an attribute on an element.
|
84
|
+
def attribute(element, name)
|
85
|
+
element[name]
|
86
|
+
end
|
87
|
+
|
88
|
+
# Gets the value of an element or attribute.
|
89
|
+
def value(value)
|
90
|
+
value.is_a?(::Hpricot::Elem) ? value.inner_text : value.to_s
|
91
|
+
end
|
92
|
+
|
93
|
+
# Gets a text value.
|
94
|
+
def text_value(value)
|
95
|
+
value(value)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Gets an integer value.
|
99
|
+
def integer_value(value)
|
100
|
+
value(value).to_i
|
101
|
+
end
|
102
|
+
|
103
|
+
# Gets a float value.
|
104
|
+
def float_value(value)
|
105
|
+
value(value).to_f
|
106
|
+
end
|
107
|
+
|
108
|
+
# Gets a date value.
|
109
|
+
def date_value(value)
|
110
|
+
Date.parse(value(value))
|
111
|
+
end
|
112
|
+
|
113
|
+
# Gets a time value.
|
114
|
+
def time_value(value)
|
115
|
+
Time.parse(value(value))
|
116
|
+
end
|
117
|
+
|
118
|
+
# Converts a name to a node name.
|
119
|
+
def node_name(name)
|
120
|
+
@parent.node_name(name)
|
121
|
+
end
|
122
|
+
private :node_name
|
123
|
+
|
124
|
+
# Gets the XPath expression representing the root node.
|
125
|
+
def root_path(name)
|
126
|
+
"/#{node_name(name)}"
|
127
|
+
end
|
128
|
+
private :root_path
|
129
|
+
end
|
130
|
+
|
131
|
+
Factory.register(Hpricot::FACTORY_NAME, Hpricot)
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rexml/document'
|
3
|
+
|
4
|
+
module Relax
|
5
|
+
module Parsers
|
6
|
+
# Parsers the server's response using the REXML library.
|
7
|
+
#
|
8
|
+
# Benefits:
|
9
|
+
#
|
10
|
+
# * XML Namespace support (parameter :foo, :namespace => 'bar')
|
11
|
+
#
|
12
|
+
# Drawbacks:
|
13
|
+
#
|
14
|
+
# * Case sensitive field names (<Status>..</> != parameter :status)
|
15
|
+
class REXML < Base
|
16
|
+
FACTORY_NAME = :rexml
|
17
|
+
|
18
|
+
def initialize(raw, parent)
|
19
|
+
@xml = ::REXML::Document.new(raw)
|
20
|
+
super(raw, parent)
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse!
|
24
|
+
if parameters
|
25
|
+
parameters.each do |parameter, options|
|
26
|
+
begin
|
27
|
+
element = options[:element] || parameter
|
28
|
+
namespace = options[:namespace]
|
29
|
+
|
30
|
+
node = case
|
31
|
+
when options[:attribute] && options[:attribute] == true
|
32
|
+
attribute(root, element, namespace)
|
33
|
+
when options[:attribute]
|
34
|
+
attribute(element(element), options[:attribute], namespace)
|
35
|
+
when options[:collection]
|
36
|
+
elements(element, namespace)
|
37
|
+
else
|
38
|
+
element(element, namespace)
|
39
|
+
end
|
40
|
+
|
41
|
+
if options[:collection]
|
42
|
+
value = node.collect do |element|
|
43
|
+
options[:collection].new(element.to_s)
|
44
|
+
end
|
45
|
+
else
|
46
|
+
value = case type = options[:type]
|
47
|
+
when Response
|
48
|
+
type.new(node)
|
49
|
+
when :date
|
50
|
+
date_value(node)
|
51
|
+
when :time
|
52
|
+
time_value(node)
|
53
|
+
when :float
|
54
|
+
float_value(node)
|
55
|
+
when :integer
|
56
|
+
integer_value(node)
|
57
|
+
when :text
|
58
|
+
else
|
59
|
+
text_value(node)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
parent.instance_variable_set("@#{parameter}", value)
|
64
|
+
rescue
|
65
|
+
raise(Relax::MissingParameter) if node.nil? && options[:required]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns the root of the XML document.
|
72
|
+
def root
|
73
|
+
@xml.root
|
74
|
+
end
|
75
|
+
|
76
|
+
# Checks the name of the root node.
|
77
|
+
def is?(name, namespace=nil)
|
78
|
+
root.name == node_name(name, nil)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns a set of elements matching name.
|
82
|
+
def elements(name, namespace=nil)
|
83
|
+
root.get_elements(node_path(name, namespace))
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns an element of the specified name.
|
87
|
+
def element(name, namespace=nil)
|
88
|
+
root.elements[node_path(name, namespace)]
|
89
|
+
end
|
90
|
+
alias :has? :element
|
91
|
+
|
92
|
+
# Returns an attribute on an element.
|
93
|
+
def attribute(element, name, namespace=nil)
|
94
|
+
element.attribute(name)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Gets the value of an element or attribute.
|
98
|
+
def value(value)
|
99
|
+
value.is_a?(::REXML::Element) ? value.text : value.to_s
|
100
|
+
end
|
101
|
+
|
102
|
+
# Gets a text value.
|
103
|
+
def text_value(value)
|
104
|
+
value(value)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Gets an integer value.
|
108
|
+
def integer_value(value)
|
109
|
+
value(value).to_i
|
110
|
+
end
|
111
|
+
|
112
|
+
# Gets a float value.
|
113
|
+
def float_value(value)
|
114
|
+
value(value).to_f
|
115
|
+
end
|
116
|
+
|
117
|
+
# Gets a date value.
|
118
|
+
def date_value(value)
|
119
|
+
Date.parse(value(value))
|
120
|
+
end
|
121
|
+
|
122
|
+
# Gets a time value.
|
123
|
+
def time_value(value)
|
124
|
+
Time.parse(value(value))
|
125
|
+
end
|
126
|
+
|
127
|
+
# Converts a name to a node name.
|
128
|
+
def node_name(name, namespace=nil)
|
129
|
+
@parent.node_name(name, namespace)
|
130
|
+
end
|
131
|
+
private :node_name
|
132
|
+
|
133
|
+
# Gets the XPath expression representing the root node.
|
134
|
+
def root_path(name)
|
135
|
+
"/#{node_name(name)}"
|
136
|
+
end
|
137
|
+
private :root_path
|
138
|
+
|
139
|
+
def node_path(name, namespace=nil)
|
140
|
+
"#{node_name(name, namespace)}"
|
141
|
+
end
|
142
|
+
private :node_path
|
143
|
+
end
|
144
|
+
|
145
|
+
Factory.register(REXML::FACTORY_NAME, REXML)
|
146
|
+
end
|
147
|
+
end
|
data/lib/relax/query.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
require 'relax/symbolic_hash'
|
5
|
+
|
6
|
+
module Relax
|
7
|
+
# Query is used to represent the query portion of a URL. It's basically just
|
8
|
+
# a hash, where each key/value pair is a query parameter.
|
9
|
+
class Query < SymbolicHash
|
10
|
+
# Converts the Query to a query string for use in a URL.
|
11
|
+
def to_s
|
12
|
+
keys.sort { |a, b| a.to_s <=> b.to_s }.collect do |key|
|
13
|
+
"#{key.to_s}=#{self.class.escape_value(fetch(key))}"
|
14
|
+
end.join('&')
|
15
|
+
end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
# Parses a URL and returns a Query with its query portion.
|
19
|
+
def parse(uri)
|
20
|
+
query = uri.query.split('&').inject({}) do |query, parameter|
|
21
|
+
key, value = parameter.split('=', 2)
|
22
|
+
query[unescape_value(key)] = unescape_value(value)
|
23
|
+
query
|
24
|
+
end
|
25
|
+
self.new(query)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Escapes a query parameter value.
|
29
|
+
def escape_value(value)
|
30
|
+
CGI.escape(value.to_s).gsub('%20', '+')
|
31
|
+
end
|
32
|
+
|
33
|
+
# Unescapes a query parameter value.
|
34
|
+
def unescape_value(value)
|
35
|
+
CGI.unescape(value)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
# Converts each value of the Query to a string as it's added.
|
42
|
+
def convert_value(value)
|
43
|
+
value.to_s
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,107 @@
|
|
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
|
+
|
31
|
+
if value && !options[:type]
|
32
|
+
query[convert_key(key)] = value if value
|
33
|
+
elsif options[:type]
|
34
|
+
options[:type].parameters.each do |parameter, options|
|
35
|
+
query[convert_complex_key(key, parameter)] = value.send(parameter) if value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
query
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Converts this request into a query string for use in a URL.
|
44
|
+
def to_s
|
45
|
+
to_query.to_s
|
46
|
+
end
|
47
|
+
|
48
|
+
# Checks the validity of the property values in this request.
|
49
|
+
def valid?
|
50
|
+
self.class.parameters.each do |key, options|
|
51
|
+
if options[:required]
|
52
|
+
value = send(key)
|
53
|
+
raise Relax::MissingParameter if value.nil?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Converts a key when the Request is converted to a query. By default, no
|
59
|
+
# conversion actually takes place, but this method can be overridden by
|
60
|
+
# child classes to perform standard manipulations, such as replacing
|
61
|
+
# underscores.
|
62
|
+
def convert_key(key)
|
63
|
+
key
|
64
|
+
end
|
65
|
+
protected :convert_key
|
66
|
+
|
67
|
+
# Converts a complex key (i.e. a parameter with a custom type) when the
|
68
|
+
# Request is converted to a query. By default, this means the key name and
|
69
|
+
# the parameter name separated by two underscores. This method can be
|
70
|
+
# overridden by child classes.
|
71
|
+
def convert_complex_key(key, parameter)
|
72
|
+
"#{convert_key(key)}.#{convert_key(parameter)}"
|
73
|
+
end
|
74
|
+
protected :convert_complex_key
|
75
|
+
|
76
|
+
class << self
|
77
|
+
# Create the parameters hash for the subclass.
|
78
|
+
def inherited(subclass) #:nodoc:
|
79
|
+
subclass.instance_variable_set('@parameters', {})
|
80
|
+
end
|
81
|
+
|
82
|
+
# Specifies a parameter to create on the request class.
|
83
|
+
#
|
84
|
+
# Options:
|
85
|
+
# - <tt>:type</tt>: An optional custom data type for the parameter.
|
86
|
+
# This must be a class that is a descendent of Request.
|
87
|
+
# - <tt>:value</tt>: The default value for this parameter.
|
88
|
+
def parameter(name, options = {})
|
89
|
+
attr_accessor name
|
90
|
+
options = @parameters[name].merge(options) if @parameters.has_key?(name)
|
91
|
+
@parameters[name] = options
|
92
|
+
end
|
93
|
+
|
94
|
+
# Adds a template value to a request class. Equivalent to creating a
|
95
|
+
# parameter with a default value.
|
96
|
+
def []=(key, value)
|
97
|
+
parameter(key, :value => value)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns a hash of all of the parameters for this request, including
|
101
|
+
# those that are inherited.
|
102
|
+
def parameters #:nodoc:
|
103
|
+
(superclass.respond_to?(:parameters) ? superclass.parameters : {}).merge(@parameters)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|