rapuncel 0.0.4 → 0.0.5.RC1
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/Gemfile +3 -0
- data/README.md +35 -24
- data/Rakefile +11 -11
- data/lib/rapuncel.rb +9 -17
- data/lib/rapuncel/adapters/net_http_adapter.rb +7 -5
- data/lib/rapuncel/base_64_string.rb +31 -0
- data/lib/rapuncel/client.rb +2 -2
- data/lib/rapuncel/connection.rb +35 -58
- data/lib/rapuncel/request.rb +1 -38
- data/lib/rapuncel/response.rb +17 -30
- data/lib/rapuncel/xml_rpc_deserializer.rb +110 -0
- data/lib/rapuncel/xml_rpc_serializer.rb +148 -0
- data/rapuncel.gemspec +3 -2
- data/spec/functional/client_spec.rb +53 -0
- data/spec/spec_helper.rb +48 -0
- data/{test → spec}/test_server.rb +0 -1
- data/spec/unit/array_spec.rb +41 -0
- data/spec/unit/base64_spec.rb +30 -0
- data/spec/unit/boolean_spec.rb +39 -0
- data/spec/unit/connection_spec.rb +22 -0
- data/spec/unit/float_spec.rb +38 -0
- data/spec/unit/hash_spec.rb +54 -0
- data/spec/unit/int_spec.rb +23 -0
- data/spec/unit/nil_spec.rb +7 -0
- data/spec/unit/object_spec.rb +24 -0
- data/spec/unit/proxy_spec.rb +29 -0
- data/spec/unit/request_spec.rb +28 -0
- data/{test/unit/response_test.rb → spec/unit/response_spec.rb} +29 -44
- data/spec/unit/string_spec.rb +35 -0
- data/spec/unit/time_spec.rb +20 -0
- metadata +77 -104
- data/lib/rapuncel/base.rb +0 -7
- data/lib/rapuncel/core_ext/array.rb +0 -23
- data/lib/rapuncel/core_ext/big_decimal.rb +0 -7
- data/lib/rapuncel/core_ext/boolean.rb +0 -29
- data/lib/rapuncel/core_ext/float.rb +0 -11
- data/lib/rapuncel/core_ext/hash.rb +0 -32
- data/lib/rapuncel/core_ext/integer.rb +0 -11
- data/lib/rapuncel/core_ext/nil.rb +0 -7
- data/lib/rapuncel/core_ext/object.rb +0 -49
- data/lib/rapuncel/core_ext/string.rb +0 -12
- data/lib/rapuncel/core_ext/symbol.rb +0 -7
- data/lib/rapuncel/core_ext/time.rb +0 -14
- data/rapuncel-0.0.3.gem +0 -0
- data/test/functional/client_test.rb +0 -54
- data/test/functional_test_helper.rb +0 -13
- data/test/test_helper.rb +0 -38
- data/test/unit/array_test.rb +0 -97
- data/test/unit/boolean_test.rb +0 -34
- data/test/unit/connection_test.rb +0 -29
- data/test/unit/float_test.rb +0 -23
- data/test/unit/hash_test.rb +0 -54
- data/test/unit/int_test.rb +0 -27
- data/test/unit/nil_test.rb +0 -16
- data/test/unit/object_test.rb +0 -83
- data/test/unit/proxy_test.rb +0 -52
- data/test/unit/request_test.rb +0 -34
- data/test/unit/string_test.rb +0 -40
- data/test/unit/time_test.rb +0 -23
data/Gemfile
ADDED
data/README.md
CHANGED
@@ -3,8 +3,12 @@
|
|
3
3
|
Rapuncel ([wikipedia](http://en.wikipedia.org/wiki/Rapunzel)) is a simple and lightweight, but fast XML-RPC client library for ruby.
|
4
4
|
It's based on Nokogiri for XML parsing and thus provides a major performance improvement for large XML responses.
|
5
5
|
|
6
|
-
##
|
7
|
-
|
6
|
+
## I need your help
|
7
|
+
I currently have exactly 1 application for Rapuncel, and that's [Kangaroo](https://github.com/cice/kangARoo), i.e.
|
8
|
+
the OpenERP XMLRPC service, where it works absolutely fine. To improve Rapuncel i need your experience with
|
9
|
+
other services and their quirks.
|
10
|
+
|
11
|
+
This Readme is for the upcoming 0.1 release, 0.0.x Readme [here](https://github.com/cice/rapuncel/blob/be19d4427dba14dbc656de1d90501f9d42aa4ef8/README.md)
|
8
12
|
|
9
13
|
## Installation
|
10
14
|
|
@@ -53,16 +57,14 @@ _default_: /
|
|
53
57
|
Username for HTTP Authentication
|
54
58
|
_default_: _empty_
|
55
59
|
* **password**
|
56
|
-
|
60
|
+
Password for HTTP Authentication
|
57
61
|
_default_: _empty_
|
58
|
-
* **
|
59
|
-
HTTP
|
60
|
-
_default_:
|
61
|
-
* **
|
62
|
-
|
63
|
-
|
64
|
-
Allows you to modify the header key for API-Key auth
|
65
|
-
_default_: X-ApiKey
|
62
|
+
* **headers**
|
63
|
+
Hash to set additional HTTP headers for the request, e.g. to send an X-ApiKey header for authentication
|
64
|
+
_default_: {}
|
65
|
+
* **ssl**
|
66
|
+
Flag wether to use SSL
|
67
|
+
_default_: false
|
66
68
|
* **raise_on**
|
67
69
|
Lets you define the behavior on errors or faults, if set to _:fault_, _:error_ or _:both_,
|
68
70
|
an Exception will be raised if something goes wrong
|
@@ -92,23 +94,20 @@ Rapuncel supports natively following object-types (and all their subclasses):
|
|
92
94
|
* Float
|
93
95
|
* BigDecimal (treated like Float)
|
94
96
|
* Time, Time-like objects
|
97
|
+
* Base64
|
95
98
|
|
96
99
|
* Symbols are converted to Strings
|
97
|
-
|
100
|
+
* All Hashs have symbol keys
|
98
101
|
* All other objects are transformed into a Hash ('struct' in XMLRPC-speak) containing their instance variables as key-value-pairs.
|
99
102
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
def to_xml_rpc(builder = Rapuncel.get_builder)
|
104
|
-
self.to_s.to_xml_rpc(builder)
|
105
|
-
end
|
106
|
-
|
107
|
-
Of course you don't have to delegate to #to\_s, you just can use the Builder object directly
|
103
|
+
## Base64
|
104
|
+
If you want a string to be encoded as Base64 in your RPC call, just mark it:
|
108
105
|
|
109
|
-
|
110
|
-
|
111
|
-
|
106
|
+
proxy.some_method "my base64 encoded string".as_base64
|
107
|
+
|
108
|
+
Return values that arrive Base64 encoded, are instances of Rapuncel::Base64String,
|
109
|
+
which is a subclass of String, and therefore can be used as such, but allows you to differentiate
|
110
|
+
Base64 return values from normal String return values.
|
112
111
|
|
113
112
|
## Supported methods
|
114
113
|
You can use most methods via
|
@@ -130,13 +129,25 @@ note
|
|
130
129
|
|
131
130
|
will return a Rapuncel::Response object, use _call\_to\_ruby_ to get standard ruby objects
|
132
131
|
|
132
|
+
## Deserialization options
|
133
|
+
|
134
|
+
At the moment there are 2 options, to be set quite ugly as class attributes on Rapuncel::XmlRpcDeserializer,
|
135
|
+
which will definitely change.
|
136
|
+
|
137
|
+
1. **double\_as\_bigdecimal**
|
138
|
+
Deserialize all <double> tags as BigDecimal.
|
139
|
+
2. **hash\_keys\_as\_string**
|
140
|
+
Don't use Symbols as keys for deserialized <struct>, but Strings.
|
141
|
+
|
142
|
+
|
133
143
|
## Todo ?
|
134
144
|
|
135
145
|
* RDoc
|
136
146
|
* Extensive functional tests
|
147
|
+
* HTTP-Proxy support
|
137
148
|
* Async requests
|
138
|
-
* Base64 support (or rather a consistent concept for Base64)
|
139
149
|
* XMLRPC Extensions (pluggable support)
|
150
|
+
* How do i test SSL?
|
140
151
|
|
141
152
|
## What happens if something goes wrong?
|
142
153
|
### HTTP Errors / XMLRPC Faults
|
data/Rakefile
CHANGED
@@ -2,18 +2,7 @@ require 'rake'
|
|
2
2
|
require 'rake/testtask'
|
3
3
|
require 'rake/rdoctask'
|
4
4
|
|
5
|
-
desc 'Default: run unit tests.'
|
6
|
-
task :default => :test
|
7
|
-
|
8
|
-
desc 'Run Rapuncel Testsuite.'
|
9
|
-
Rake::TestTask.new(:test) do |t|
|
10
|
-
t.libs << 'lib'
|
11
|
-
t.libs << 'test'
|
12
|
-
t.pattern = 'test/**/*_test.rb'
|
13
|
-
t.verbose = true
|
14
|
-
end
|
15
5
|
|
16
|
-
#TODO: sdoc
|
17
6
|
desc 'Generate Rapuncel rdoc.'
|
18
7
|
Rake::RDocTask.new(:rdoc) do |rdoc|
|
19
8
|
rdoc.rdoc_dir = 'rdoc'
|
@@ -22,3 +11,14 @@ Rake::RDocTask.new(:rdoc) do |rdoc|
|
|
22
11
|
rdoc.rdoc_files.include('README.md')
|
23
12
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
24
13
|
end
|
14
|
+
|
15
|
+
begin
|
16
|
+
require 'rspec/core/rake_task'
|
17
|
+
desc 'Run RSpec suite.'
|
18
|
+
RSpec::Core::RakeTask.new('spec')
|
19
|
+
rescue LoadError
|
20
|
+
puts "RSpec is not available. In order to run specs, you must: gem install rspec"
|
21
|
+
end
|
22
|
+
|
23
|
+
desc 'Default: run unit tests.'
|
24
|
+
task :default => :spec
|
data/lib/rapuncel.rb
CHANGED
@@ -1,25 +1,17 @@
|
|
1
|
-
|
2
|
-
|
3
1
|
require 'active_support/core_ext/hash/keys'
|
4
|
-
|
5
2
|
require 'nokogiri'
|
6
3
|
|
7
|
-
|
4
|
+
module Rapuncel
|
5
|
+
BUILDER_OPTIONS = {:encoding => 'UTF-8'}
|
6
|
+
|
7
|
+
def self.get_builder options = {}
|
8
|
+
Nokogiri::XML::Builder.new options
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
8
12
|
require 'rapuncel/request'
|
9
13
|
require 'rapuncel/response'
|
10
14
|
require 'rapuncel/client'
|
11
15
|
require 'rapuncel/proxy'
|
12
16
|
|
13
|
-
|
14
|
-
require 'rapuncel/core_ext/object'
|
15
|
-
require 'rapuncel/core_ext/string'
|
16
|
-
require 'rapuncel/core_ext/symbol'
|
17
|
-
require 'rapuncel/core_ext/integer'
|
18
|
-
require 'rapuncel/core_ext/big_decimal'
|
19
|
-
require 'rapuncel/core_ext/float'
|
20
|
-
require 'rapuncel/core_ext/hash'
|
21
|
-
require 'rapuncel/core_ext/array'
|
22
|
-
require 'rapuncel/core_ext/boolean'
|
23
|
-
require 'rapuncel/core_ext/nil'
|
24
|
-
|
25
|
-
require 'rapuncel/core_ext/time'
|
17
|
+
require 'rapuncel/base_64_string'
|
@@ -1,11 +1,10 @@
|
|
1
1
|
require 'net/http'
|
2
|
-
require '
|
2
|
+
require 'net/https'
|
3
3
|
|
4
4
|
module Rapuncel
|
5
5
|
module Adapters
|
6
6
|
module NetHttpAdapter
|
7
|
-
|
8
|
-
|
7
|
+
# Small response wrapper
|
9
8
|
class HttpResponse
|
10
9
|
def initialize response
|
11
10
|
@response = response
|
@@ -24,10 +23,13 @@ module Rapuncel
|
|
24
23
|
end
|
25
24
|
end
|
26
25
|
|
26
|
+
# Dispatch a XMLRPC via HTTP and return a response object.
|
27
27
|
def send_method_call str
|
28
|
-
|
28
|
+
request = Net::HTTP.new connection.host, connection.port
|
29
|
+
request.use_ssl = connection.ssl?
|
30
|
+
request.basic_auth connection.user, connection.password if connection.auth?
|
29
31
|
|
30
|
-
HttpResponse.new
|
32
|
+
HttpResponse.new request.post(connection.path, str, connection.headers)
|
31
33
|
end
|
32
34
|
end
|
33
35
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
if RUBY_VERSION =~ /^1\.8/
|
2
|
+
require 'base64'
|
3
|
+
end
|
4
|
+
|
5
|
+
class String
|
6
|
+
def as_base64
|
7
|
+
Rapuncel::Base64String.new self
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module Rapuncel
|
12
|
+
class Base64String < String
|
13
|
+
def base64_encoded
|
14
|
+
if RUBY_VERSION =~ /^1\.9/
|
15
|
+
[self].pack 'm'
|
16
|
+
else
|
17
|
+
Base64.encode64 self
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def decode_base64 string
|
23
|
+
if RUBY_VERSION =~ /^1\.9/
|
24
|
+
new string.unpack('m')[0]
|
25
|
+
else
|
26
|
+
new Base64.decode64 string
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/rapuncel/client.rb
CHANGED
@@ -17,7 +17,7 @@ module Rapuncel
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def initialize configuration = {}
|
20
|
-
@connection = Connection.
|
20
|
+
@connection = Connection.new configuration.except(:raise_on)
|
21
21
|
|
22
22
|
@raise_on_fault, @raise_on_error = case configuration[:raise_on]
|
23
23
|
when :fault
|
@@ -46,7 +46,7 @@ module Rapuncel
|
|
46
46
|
|
47
47
|
protected
|
48
48
|
def execute request
|
49
|
-
xml = request.
|
49
|
+
xml = XmlRpcSerializer.new(request).to_xml
|
50
50
|
|
51
51
|
Response.new send_method_call(xml)
|
52
52
|
end
|
data/lib/rapuncel/connection.rb
CHANGED
@@ -1,40 +1,24 @@
|
|
1
1
|
module Rapuncel
|
2
2
|
class Connection
|
3
|
-
|
4
|
-
configuration = configuration.symbolize_keys
|
5
|
-
|
6
|
-
case
|
7
|
-
when configuration[:user], configuration[:auth_method], configuration[:password]
|
8
|
-
AuthConnection.new configuration
|
9
|
-
when configuration[:api_key_header], configuration[:api_key]
|
10
|
-
ApiKeyAuthConnection.new configuration
|
11
|
-
else
|
12
|
-
new configuration
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
attr_accessor :host, :port, :path, :ssl, :headers
|
3
|
+
attr_accessor :host, :port, :path, :ssl, :user, :password
|
17
4
|
alias_method :ssl?, :ssl
|
18
5
|
|
19
6
|
def initialize configuration = {}
|
20
|
-
|
21
|
-
self.host = configuration[:host] || 'localhost'
|
22
|
-
self.port = configuration[:port] || '8080'
|
23
|
-
self.path = configuration[:path] || '/'
|
24
|
-
self.headers = configuration[:headers] || {}
|
25
|
-
|
26
|
-
if ssl = configuration[:ssl]
|
27
|
-
@ssl = true
|
28
|
-
#TODO
|
29
|
-
end
|
7
|
+
load_configuration configuration
|
30
8
|
end
|
31
9
|
|
32
10
|
def url
|
33
|
-
"
|
11
|
+
"#{protocol}://#{host}:#{port}#{path}"
|
34
12
|
end
|
35
13
|
|
36
14
|
def host= value
|
37
|
-
@host = value.to_s.sub /^http
|
15
|
+
@host = value.to_s.sub /^http(s)?\:\/\//, ''
|
16
|
+
|
17
|
+
if $1 == 's'
|
18
|
+
@ssl = true
|
19
|
+
end
|
20
|
+
|
21
|
+
@host
|
38
22
|
end
|
39
23
|
|
40
24
|
def path= value
|
@@ -44,43 +28,36 @@ module Rapuncel
|
|
44
28
|
|
45
29
|
@path = value
|
46
30
|
end
|
31
|
+
|
32
|
+
def headers= headers
|
33
|
+
@headers = {
|
34
|
+
'User-Agent' => 'Rapuncel, Ruby XMLRPC Client'
|
35
|
+
}.merge headers.stringify_keys
|
36
|
+
end
|
47
37
|
|
48
38
|
def headers
|
49
|
-
@headers.merge
|
39
|
+
@headers.merge 'Accept' => 'text/xml', 'content-type' => 'text/xml'
|
50
40
|
end
|
51
|
-
|
52
|
-
def
|
53
|
-
|
54
|
-
end
|
55
|
-
|
56
|
-
class AuthConnection < Connection
|
57
|
-
attr_accessor :auth_method, :user, :password
|
58
|
-
|
59
|
-
def initialize configuration = {}
|
60
|
-
super
|
61
|
-
|
62
|
-
@auth_method = auth_method || 'basic'
|
63
|
-
@user = user || ''
|
64
|
-
@password = configuration[:password] || ''
|
41
|
+
|
42
|
+
def protocol
|
43
|
+
ssl? ? 'https' : 'http'
|
65
44
|
end
|
66
|
-
|
67
|
-
def
|
68
|
-
|
69
|
-
|
70
|
-
class ApiKeyAuthConnection < Connection
|
71
|
-
attr_accessor :api_key_header, :api_key
|
72
|
-
|
73
|
-
def initialize configuration = {}
|
74
|
-
super
|
75
|
-
|
76
|
-
@api_key_header = configuration[:api_key_header] || "X-ApiKey"
|
77
|
-
@api_key = configuration[:api_key] || '' #DISCUSS: raise error ?
|
45
|
+
|
46
|
+
def auth?
|
47
|
+
!!user && !!password
|
78
48
|
end
|
79
|
-
|
80
|
-
|
81
|
-
|
49
|
+
|
50
|
+
protected
|
51
|
+
def load_configuration configuration
|
52
|
+
configuration = configuration.symbolize_keys
|
53
|
+
|
54
|
+
self.ssl = !!configuration[:ssl]
|
55
|
+
self.host = configuration[:host] || 'localhost'
|
56
|
+
self.port = configuration[:port] || '8080'
|
57
|
+
self.path = configuration[:path] || '/'
|
58
|
+
self.headers = configuration[:headers] || {}
|
59
|
+
self.user = configuration[:user]
|
60
|
+
self.password = configuration[:password]
|
82
61
|
end
|
83
|
-
|
84
|
-
def api_auth? ; true ; end
|
85
62
|
end
|
86
63
|
end
|
data/lib/rapuncel/request.rb
CHANGED
@@ -1,49 +1,12 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require 'rapuncel/xml_rpc_serializer'
|
3
2
|
|
4
3
|
module Rapuncel
|
5
4
|
class Request
|
6
5
|
attr_accessor :method_name, :arguments
|
7
6
|
|
8
|
-
|
9
7
|
# Create a new XML-RPC request
|
10
8
|
def initialize method_name, *args
|
11
9
|
@method_name, @arguments = method_name, args
|
12
10
|
end
|
13
|
-
|
14
|
-
def to_xml_rpc builder = Rapuncel.get_builder
|
15
|
-
method_call! builder
|
16
|
-
|
17
|
-
builder.to_xml :encoding => 'UTF-8'
|
18
|
-
end
|
19
|
-
|
20
|
-
protected
|
21
|
-
def method_call! builder
|
22
|
-
|
23
|
-
builder.methodCall do |builder|
|
24
|
-
method_name! builder
|
25
|
-
params! builder
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def method_name! builder
|
30
|
-
builder.methodName method_name
|
31
|
-
end
|
32
|
-
|
33
|
-
def params! builder
|
34
|
-
builder.params do |builder|
|
35
|
-
arguments.each do |value|
|
36
|
-
param! builder, value
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def param! builder, value
|
42
|
-
builder.param do |builder|
|
43
|
-
builder.value do |builder|
|
44
|
-
value.to_xml_rpc builder
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
11
|
end
|
49
12
|
end
|
data/lib/rapuncel/response.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'active_support/core_ext/module/delegation'
|
2
|
+
require 'rapuncel/xml_rpc_deserializer'
|
2
3
|
|
3
4
|
module Rapuncel
|
4
5
|
class Response
|
@@ -6,7 +7,7 @@ module Rapuncel
|
|
6
7
|
class Fault < Exception ; end
|
7
8
|
class Error < Exception ; end
|
8
9
|
|
9
|
-
attr_accessor :http_response, :status, :status_code
|
10
|
+
attr_accessor :http_response, :status, :status_code, :response
|
10
11
|
|
11
12
|
delegate :body,
|
12
13
|
:to => :http_response
|
@@ -20,10 +21,16 @@ module Rapuncel
|
|
20
21
|
def evaluate_status
|
21
22
|
@status_code = http_response.code.to_i
|
22
23
|
|
23
|
-
@status =
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
@status = unless http_response.success?
|
25
|
+
'error'
|
26
|
+
else
|
27
|
+
deserialize_response
|
28
|
+
|
29
|
+
if Hash === response && response[:faultCode]
|
30
|
+
'fault'
|
31
|
+
else
|
32
|
+
'success'
|
33
|
+
end
|
27
34
|
end
|
28
35
|
end
|
29
36
|
|
@@ -40,31 +47,15 @@ module Rapuncel
|
|
40
47
|
end
|
41
48
|
|
42
49
|
def fault
|
43
|
-
|
44
|
-
@fault ||= begin
|
45
|
-
fault_node = parsed_body.xpath('/methodResponse/fault/value/struct').first
|
46
|
-
|
47
|
-
Hash.from_xml_rpc(fault_node).tap do |h|
|
48
|
-
h[:faultString] = h[:faultString].strip
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
50
|
+
fault? && @response
|
52
51
|
end
|
53
52
|
|
54
53
|
def error
|
55
|
-
|
56
|
-
@error ||= { :http_code => @status_code, :http_body => body }
|
57
|
-
end
|
54
|
+
error? && { :http_code => @status_code, :http_body => body }
|
58
55
|
end
|
59
56
|
|
60
57
|
def result
|
61
|
-
|
62
|
-
@result ||= begin
|
63
|
-
return_values = parsed_body.xpath('/methodResponse/params/param/value/*')
|
64
|
-
|
65
|
-
Object.from_xml_rpc return_values.first
|
66
|
-
end
|
67
|
-
end
|
58
|
+
success? && @response
|
68
59
|
end
|
69
60
|
|
70
61
|
def to_ruby
|
@@ -72,12 +63,8 @@ module Rapuncel
|
|
72
63
|
end
|
73
64
|
|
74
65
|
protected
|
75
|
-
def
|
76
|
-
@
|
77
|
-
end
|
78
|
-
|
79
|
-
def method_response_success?
|
80
|
-
parsed_body.xpath('/methodResponse/fault').empty?
|
66
|
+
def deserialize_response
|
67
|
+
@response = XmlRpcDeserializer.new(body).to_ruby
|
81
68
|
end
|
82
69
|
end
|
83
70
|
end
|