rapuncel 0.0.5.RC1 → 0.0.5.RC2
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +30 -0
- data/README.md +44 -22
- data/lib/rapuncel/base_64_string.rb +17 -12
- data/lib/rapuncel/client.rb +29 -4
- data/lib/rapuncel/connection.rb +2 -1
- data/lib/rapuncel/proxy.rb +10 -3
- data/lib/rapuncel/request.rb +0 -2
- data/lib/rapuncel/response.rb +5 -6
- data/lib/rapuncel/xml_rpc/deserializer.rb +113 -0
- data/lib/rapuncel/xml_rpc/serializer.rb +153 -0
- data/rapuncel-0.0.5.RC1.gem +0 -0
- data/rapuncel.gemspec +1 -1
- data/spec/unit/array_spec.rb +2 -2
- data/spec/unit/base64_spec.rb +2 -2
- data/spec/unit/boolean_spec.rb +5 -5
- data/spec/unit/custom_serialization_spec.rb +23 -0
- data/spec/unit/float_spec.rb +5 -5
- data/spec/unit/hash_spec.rb +2 -2
- data/spec/unit/int_spec.rb +3 -3
- data/spec/unit/nil_spec.rb +1 -1
- data/spec/unit/object_spec.rb +23 -2
- data/spec/unit/request_spec.rb +1 -1
- data/spec/unit/response_spec.rb +3 -3
- data/spec/unit/string_spec.rb +5 -5
- data/spec/unit/time_spec.rb +2 -2
- metadata +92 -51
- data/lib/rapuncel/xml_rpc_deserializer.rb +0 -110
- data/lib/rapuncel/xml_rpc_serializer.rb +0 -148
data/Gemfile.lock
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
rapuncel (0.0.5.RC1)
|
5
|
+
activesupport (>= 3.0.0)
|
6
|
+
nokogiri
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: http://www.rubygems.org/
|
10
|
+
specs:
|
11
|
+
activesupport (3.0.9)
|
12
|
+
diff-lcs (1.1.2)
|
13
|
+
nokogiri (1.5.0)
|
14
|
+
rake (0.9.2)
|
15
|
+
rspec (2.6.0)
|
16
|
+
rspec-core (~> 2.6.0)
|
17
|
+
rspec-expectations (~> 2.6.0)
|
18
|
+
rspec-mocks (~> 2.6.0)
|
19
|
+
rspec-core (2.6.4)
|
20
|
+
rspec-expectations (2.6.0)
|
21
|
+
diff-lcs (~> 1.1.2)
|
22
|
+
rspec-mocks (2.6.0)
|
23
|
+
|
24
|
+
PLATFORMS
|
25
|
+
ruby
|
26
|
+
|
27
|
+
DEPENDENCIES
|
28
|
+
rake
|
29
|
+
rapuncel!
|
30
|
+
rspec (>= 2.6.0)
|
data/README.md
CHANGED
@@ -6,10 +6,8 @@ It's based on Nokogiri for XML parsing and thus provides a major performance imp
|
|
6
6
|
## I need your help
|
7
7
|
I currently have exactly 1 application for Rapuncel, and that's [Kangaroo](https://github.com/cice/kangARoo), i.e.
|
8
8
|
the OpenERP XMLRPC service, where it works absolutely fine. To improve Rapuncel i need your experience with
|
9
|
-
other services and their quirks.
|
9
|
+
other services and their quirks. Just open a feature request or file a bug report.
|
10
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)
|
12
|
-
|
13
11
|
## Installation
|
14
12
|
|
15
13
|
### Rails
|
@@ -45,29 +43,32 @@ First you have to create a client with the connection details, e.g.
|
|
45
43
|
Available options are:
|
46
44
|
|
47
45
|
* **host**
|
48
|
-
hostname or ip-address,
|
49
|
-
_default_: localhost
|
46
|
+
hostname or ip-address,
|
47
|
+
_default_: `localhost`
|
50
48
|
* **port**
|
51
|
-
port where your XMLRPC service is listening,
|
52
|
-
_default_: 8080
|
49
|
+
port where your XMLRPC service is listening,
|
50
|
+
_default_: `8080`
|
53
51
|
* **path**
|
54
|
-
path to the service,
|
55
|
-
_default_:
|
52
|
+
path to the service,
|
53
|
+
_default_: `/`
|
56
54
|
* **user**
|
57
|
-
Username for HTTP Authentication
|
55
|
+
Username for HTTP Authentication
|
58
56
|
_default_: _empty_
|
59
57
|
* **password**
|
60
|
-
Password for HTTP Authentication
|
58
|
+
Password for HTTP Authentication
|
61
59
|
_default_: _empty_
|
62
60
|
* **headers**
|
63
|
-
Hash to set additional HTTP headers for the request, e.g. to send an X-ApiKey header for authentication
|
64
|
-
_default_: {}
|
61
|
+
Hash to set additional HTTP headers for the request, e.g. to send an X-ApiKey header for authentication
|
62
|
+
_default_: `{}`
|
65
63
|
* **ssl**
|
66
|
-
Flag wether to use SSL
|
67
|
-
_default_: false
|
64
|
+
Flag wether to use SSL
|
65
|
+
_default_: `false`
|
68
66
|
* **raise_on**
|
69
|
-
Lets you define the behavior on errors or faults, if set to
|
67
|
+
Lets you define the behavior on errors or faults, if set to `:fault`, `:error` or `:both`,
|
70
68
|
an Exception will be raised if something goes wrong
|
69
|
+
* **serialization**
|
70
|
+
Use your own (extended) (De)Serializers. See Custom Serialization
|
71
|
+
_default_: `Rapuncel::XmlRpc`
|
71
72
|
|
72
73
|
### Get a proxy object and ...
|
73
74
|
A proxy object receives ruby method calls, redirects them to your XMLRPC service and returns the response as ruby objects!
|
@@ -91,8 +92,8 @@ Rapuncel supports natively following object-types (and all their subclasses):
|
|
91
92
|
* Array
|
92
93
|
* Hash
|
93
94
|
* TrueClass, FalseClass
|
94
|
-
* Float
|
95
|
-
* BigDecimal (treated like Float)
|
95
|
+
* Float / Double
|
96
|
+
* BigDecimal (treated like Float, unless you set `double_as_bigdecimal` to true)
|
96
97
|
* Time, Time-like objects
|
97
98
|
* Base64
|
98
99
|
|
@@ -112,7 +113,7 @@ Base64 return values from normal String return values.
|
|
112
113
|
## Supported methods
|
113
114
|
You can use most methods via
|
114
115
|
|
115
|
-
proxy.methodname
|
116
|
+
proxy.methodname 'a', 1, [:a, :b], :a => :d
|
116
117
|
|
117
118
|
However methods starting with \_\_, or ending with a bang \! or a question mark ? are not supported. To call those methods you can always
|
118
119
|
use
|
@@ -127,11 +128,11 @@ note
|
|
127
128
|
|
128
129
|
client.call 'methodname', *args
|
129
130
|
|
130
|
-
will return a Rapuncel::Response object, use _call\_to\_ruby_ to get
|
131
|
+
will return a Rapuncel::Response object, use _call\_to\_ruby_ to get a parsed result
|
131
132
|
|
132
133
|
## Deserialization options
|
133
134
|
|
134
|
-
At the moment there are 2 options, to be set quite ugly as class attributes on Rapuncel::
|
135
|
+
At the moment there are 2 options, to be set quite ugly as class attributes on Rapuncel::XmlRpc::Deserializer,
|
135
136
|
which will definitely change.
|
136
137
|
|
137
138
|
1. **double\_as\_bigdecimal**
|
@@ -139,6 +140,17 @@ Deserialize all <double> tags as BigDecimal.
|
|
139
140
|
2. **hash\_keys\_as\_string**
|
140
141
|
Don't use Symbols as keys for deserialized <struct>, but Strings.
|
141
142
|
|
143
|
+
## Custom Serialization
|
144
|
+
|
145
|
+
module MySpecialSerialization
|
146
|
+
class Serializer < Rapuncel::XmlRpc::Serializer
|
147
|
+
end
|
148
|
+
class Deserializer < Rapuncel::XmlRpc::Deserializer
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
client = Rapuncel::Client.new :serialization => MySpecialSerialization
|
153
|
+
# or :serialization => 'MySpecialSerialization'
|
142
154
|
|
143
155
|
## Todo ?
|
144
156
|
|
@@ -155,11 +167,21 @@ See Usage -> configuration -> raise\_on switch
|
|
155
167
|
### Malformed XML/XMLRPC
|
156
168
|
Rapuncel will most likely fail hard.
|
157
169
|
|
170
|
+
## Changelog
|
171
|
+
|
172
|
+
* **0.0.5**
|
173
|
+
* Refactored serialization, preparation for pluggable extensions
|
174
|
+
* Deserialization option "double\_as\_bigdecimal"
|
175
|
+
* Deserialization option "hash\_keys\_as\_string"
|
176
|
+
* base64 support
|
177
|
+
* Object#to\_xmlrpc now should expect a XmlRpc::Serializer instance,
|
178
|
+
not a Builder (you can access the Builder directly via XmlRpc::Serializer#builder)
|
179
|
+
|
158
180
|
## Open Source
|
159
181
|
|
160
182
|
### License
|
161
183
|
|
162
|
-
Copyright (c)
|
184
|
+
Copyright (c) 2011 ['Marian Theisen', 'Michael Eickenberg']
|
163
185
|
|
164
186
|
Permission is hereby granted, free of charge, to any person obtaining
|
165
187
|
a copy of this software and associated documentation files (the
|
@@ -10,22 +10,27 @@ end
|
|
10
10
|
|
11
11
|
module Rapuncel
|
12
12
|
class Base64String < String
|
13
|
-
|
14
|
-
|
13
|
+
|
14
|
+
if RUBY_VERSION =~ /^1\.9/
|
15
|
+
|
16
|
+
def base64_encoded
|
15
17
|
[self].pack 'm'
|
16
|
-
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.decode_base64 string
|
21
|
+
new string.unpack('m')[0]
|
22
|
+
end
|
23
|
+
|
24
|
+
else
|
25
|
+
|
26
|
+
def base64_encoded
|
17
27
|
Base64.encode64 self
|
18
28
|
end
|
19
|
-
end
|
20
29
|
|
21
|
-
|
22
|
-
|
23
|
-
if RUBY_VERSION =~ /^1\.9/
|
24
|
-
new string.unpack('m')[0]
|
25
|
-
else
|
26
|
-
new Base64.decode64 string
|
27
|
-
end
|
30
|
+
def self.decode_base64 string
|
31
|
+
new Base64.decode64 string
|
28
32
|
end
|
33
|
+
|
29
34
|
end
|
30
35
|
end
|
31
|
-
end
|
36
|
+
end
|
data/lib/rapuncel/client.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
require 'rapuncel/adapters/net_http_adapter'
|
2
2
|
require 'rapuncel/connection'
|
3
|
+
require 'rapuncel/xml_rpc/serializer'
|
4
|
+
require 'rapuncel/xml_rpc/deserializer'
|
3
5
|
require 'active_support/core_ext/hash/except'
|
6
|
+
require 'active_support/inflector/methods'
|
4
7
|
|
5
8
|
module Rapuncel
|
6
9
|
class Client
|
@@ -17,8 +20,9 @@ module Rapuncel
|
|
17
20
|
end
|
18
21
|
|
19
22
|
def initialize configuration = {}
|
20
|
-
@connection = Connection.new configuration.except(:raise_on)
|
21
|
-
|
23
|
+
@connection = Connection.new configuration.except(:raise_on, :serialization)
|
24
|
+
@serialization = configuration[:serialization]
|
25
|
+
|
22
26
|
@raise_on_fault, @raise_on_error = case configuration[:raise_on]
|
23
27
|
when :fault
|
24
28
|
[true, false]
|
@@ -31,10 +35,12 @@ module Rapuncel
|
|
31
35
|
end
|
32
36
|
end
|
33
37
|
|
38
|
+
# Dispatch a method call and return the response as Rapuncel::Response object.
|
34
39
|
def call name, *args
|
35
40
|
execute Request.new(name, *args)
|
36
41
|
end
|
37
42
|
|
43
|
+
# Dispatch a method call and return the response as parsed ruby.
|
38
44
|
def call_to_ruby name, *args
|
39
45
|
response = call name, *args
|
40
46
|
|
@@ -46,9 +52,28 @@ module Rapuncel
|
|
46
52
|
|
47
53
|
protected
|
48
54
|
def execute request
|
49
|
-
xml =
|
55
|
+
xml = serializer[request]
|
50
56
|
|
51
|
-
Response.new send_method_call(xml)
|
57
|
+
Response.new send_method_call(xml), deserializer
|
58
|
+
end
|
59
|
+
|
60
|
+
def serialization
|
61
|
+
case @serialization
|
62
|
+
when Module
|
63
|
+
@serialization
|
64
|
+
when String, Symbol
|
65
|
+
ActiveSupport::Inflector.constantize(@serialization.to_s)
|
66
|
+
else
|
67
|
+
XmlRpc
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def serializer
|
72
|
+
@serializer ||= serialization.const_get 'Serializer'
|
73
|
+
end
|
74
|
+
|
75
|
+
def deserializer
|
76
|
+
@deserializer ||= serialization.const_get 'Deserializer'
|
52
77
|
end
|
53
78
|
|
54
79
|
private
|
data/lib/rapuncel/connection.rb
CHANGED
@@ -42,6 +42,7 @@ module Rapuncel
|
|
42
42
|
def protocol
|
43
43
|
ssl? ? 'https' : 'http'
|
44
44
|
end
|
45
|
+
alias_method :scheme, :protocol
|
45
46
|
|
46
47
|
def auth?
|
47
48
|
!!user && !!password
|
@@ -57,7 +58,7 @@ module Rapuncel
|
|
57
58
|
self.path = configuration[:path] || '/'
|
58
59
|
self.headers = configuration[:headers] || {}
|
59
60
|
self.user = configuration[:user]
|
60
|
-
self.password
|
61
|
+
self.password = configuration[:password]
|
61
62
|
end
|
62
63
|
end
|
63
64
|
end
|
data/lib/rapuncel/proxy.rb
CHANGED
@@ -3,13 +3,13 @@ require 'rapuncel/request'
|
|
3
3
|
module Rapuncel
|
4
4
|
class Proxy
|
5
5
|
PROXY_METHODS = %w(tap inspect clone freeze dup class initialize to_s).freeze
|
6
|
-
LOCKED_METHODS = %w(method_missing).freeze
|
6
|
+
LOCKED_METHODS = %w(method_missing object_id).freeze
|
7
7
|
LOCKED_PATTERN = /(\A__|\?\Z|!\Z)/.freeze
|
8
8
|
|
9
9
|
class << self
|
10
10
|
# Initialize a new Proxy object for a specific Client. Alternatively
|
11
11
|
# you can pass a Hash containing configuration for a new Client, which
|
12
|
-
# will be created on-the-fly, but not accessible. The second parameter
|
12
|
+
# will be created on-the-fly, but is directly not accessible. The second parameter
|
13
13
|
# specifies a specific interface/namespace for the remote calls,
|
14
14
|
# i.e. if your RPC method is
|
15
15
|
#
|
@@ -35,6 +35,13 @@ module Rapuncel
|
|
35
35
|
end
|
36
36
|
RUBY
|
37
37
|
end
|
38
|
+
|
39
|
+
# Predefine some proxy methods.
|
40
|
+
def define_proxy_methods *names
|
41
|
+
names.each do |name|
|
42
|
+
define_proxy_method name
|
43
|
+
end
|
44
|
+
end
|
38
45
|
end
|
39
46
|
|
40
47
|
PROXY_METHODS.each do |name|
|
@@ -44,7 +51,7 @@ module Rapuncel
|
|
44
51
|
alias_method "__inspect__", "__to_s__"
|
45
52
|
|
46
53
|
instance_methods.each do |name|
|
47
|
-
unless LOCKED_METHODS.include?(name) || LOCKED_PATTERN.match(name)
|
54
|
+
unless LOCKED_METHODS.include?(name.to_s) || LOCKED_PATTERN.match(name.to_s)
|
48
55
|
define_proxy_method name
|
49
56
|
end
|
50
57
|
end
|
data/lib/rapuncel/request.rb
CHANGED
data/lib/rapuncel/response.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'active_support/core_ext/module/delegation'
|
2
|
-
require 'rapuncel/xml_rpc_deserializer'
|
3
2
|
|
4
3
|
module Rapuncel
|
5
4
|
class Response
|
@@ -7,14 +6,14 @@ module Rapuncel
|
|
7
6
|
class Fault < Exception ; end
|
8
7
|
class Error < Exception ; end
|
9
8
|
|
10
|
-
attr_accessor :http_response, :status, :status_code, :response
|
9
|
+
attr_accessor :http_response, :status, :status_code, :response, :deserializer
|
11
10
|
|
12
11
|
delegate :body,
|
13
12
|
:to => :http_response
|
14
13
|
|
15
|
-
def initialize http_response
|
16
|
-
@http_response = http_response
|
17
|
-
|
14
|
+
def initialize http_response, deserializer
|
15
|
+
@http_response, @deserializer = http_response, deserializer
|
16
|
+
|
18
17
|
evaluate_status
|
19
18
|
end
|
20
19
|
|
@@ -64,7 +63,7 @@ module Rapuncel
|
|
64
63
|
|
65
64
|
protected
|
66
65
|
def deserialize_response
|
67
|
-
@response =
|
66
|
+
@response = deserializer[body]
|
68
67
|
end
|
69
68
|
end
|
70
69
|
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module Rapuncel
|
2
|
+
module XmlRpc
|
3
|
+
class Deserializer
|
4
|
+
XML_ENCODING = 'UTF-8'
|
5
|
+
|
6
|
+
def initialize xml
|
7
|
+
@xml_doc = Nokogiri::XML.parse xml
|
8
|
+
end
|
9
|
+
|
10
|
+
def deserialize xml_node = root_node
|
11
|
+
send "deserialize_#{xml_node.name}", xml_node
|
12
|
+
end
|
13
|
+
|
14
|
+
def deserialize_base64 xml_node
|
15
|
+
Base64String.decode_base64 xml_node.text.strip
|
16
|
+
end
|
17
|
+
|
18
|
+
def deserialize_string xml_node
|
19
|
+
xml_node.text.gsub(/(\r\n|\r)/, "\n")
|
20
|
+
end
|
21
|
+
|
22
|
+
def deserialize_array xml_node
|
23
|
+
values = xml_node.first_element_child.element_children
|
24
|
+
|
25
|
+
values.map do |value|
|
26
|
+
deserialize value.first_element_child
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def deserialize_boolean xml_node
|
31
|
+
xml_node.text == "1"
|
32
|
+
end
|
33
|
+
|
34
|
+
def deserialize_double xml_node
|
35
|
+
text = xml_node.text.strip
|
36
|
+
|
37
|
+
if double_as_bigdecimal?
|
38
|
+
BigDecimal.new text
|
39
|
+
else
|
40
|
+
text.to_f
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def deserialize_struct xml_node
|
45
|
+
keys_and_values = xml_node.element_children
|
46
|
+
|
47
|
+
{}.tap do |hash|
|
48
|
+
keys_and_values.each do |key_value|
|
49
|
+
key = key_value.first_element_child.text.strip
|
50
|
+
key = key.to_sym unless hash_keys_as_string?
|
51
|
+
|
52
|
+
value = deserialize key_value.last_element_child.first_element_child
|
53
|
+
|
54
|
+
hash[key] = value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def deserialize_int xml_node
|
60
|
+
xml_node.text.strip.to_i
|
61
|
+
end
|
62
|
+
alias_method :deserialize_i4, :deserialize_int
|
63
|
+
|
64
|
+
define_method "deserialize_dateTime.iso8601" do |xml_node|
|
65
|
+
Time.parse xml_node.text.strip
|
66
|
+
end
|
67
|
+
|
68
|
+
def deserialize_methodResponse xml_node
|
69
|
+
response = xml_node.first_element_child
|
70
|
+
|
71
|
+
if response.name == 'fault'
|
72
|
+
deserialize_methodResponse_fault response
|
73
|
+
else
|
74
|
+
deserialize_methodResponse_params response
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def deserialize_methodResponse_params xml_node
|
79
|
+
deserialize xml_node.first_element_child.first_element_child.first_element_child
|
80
|
+
end
|
81
|
+
|
82
|
+
def deserialize_methodResponse_fault xml_node
|
83
|
+
deserialize xml_node.first_element_child.first_element_child
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_ruby
|
87
|
+
deserialize
|
88
|
+
end
|
89
|
+
|
90
|
+
protected
|
91
|
+
def root_node
|
92
|
+
@xml_doc.root
|
93
|
+
end
|
94
|
+
|
95
|
+
def double_as_bigdecimal?
|
96
|
+
!!self.class.double_as_bigdecimal
|
97
|
+
end
|
98
|
+
|
99
|
+
def hash_keys_as_string?
|
100
|
+
!!self.class.hash_keys_as_string
|
101
|
+
end
|
102
|
+
|
103
|
+
class << self
|
104
|
+
attr_accessor :double_as_bigdecimal, :hash_keys_as_string
|
105
|
+
|
106
|
+
def deserialize xml
|
107
|
+
new(xml).to_ruby
|
108
|
+
end
|
109
|
+
alias_method :[], :deserialize
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|