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.
Files changed (59) hide show
  1. data/Gemfile +3 -0
  2. data/README.md +35 -24
  3. data/Rakefile +11 -11
  4. data/lib/rapuncel.rb +9 -17
  5. data/lib/rapuncel/adapters/net_http_adapter.rb +7 -5
  6. data/lib/rapuncel/base_64_string.rb +31 -0
  7. data/lib/rapuncel/client.rb +2 -2
  8. data/lib/rapuncel/connection.rb +35 -58
  9. data/lib/rapuncel/request.rb +1 -38
  10. data/lib/rapuncel/response.rb +17 -30
  11. data/lib/rapuncel/xml_rpc_deserializer.rb +110 -0
  12. data/lib/rapuncel/xml_rpc_serializer.rb +148 -0
  13. data/rapuncel.gemspec +3 -2
  14. data/spec/functional/client_spec.rb +53 -0
  15. data/spec/spec_helper.rb +48 -0
  16. data/{test → spec}/test_server.rb +0 -1
  17. data/spec/unit/array_spec.rb +41 -0
  18. data/spec/unit/base64_spec.rb +30 -0
  19. data/spec/unit/boolean_spec.rb +39 -0
  20. data/spec/unit/connection_spec.rb +22 -0
  21. data/spec/unit/float_spec.rb +38 -0
  22. data/spec/unit/hash_spec.rb +54 -0
  23. data/spec/unit/int_spec.rb +23 -0
  24. data/spec/unit/nil_spec.rb +7 -0
  25. data/spec/unit/object_spec.rb +24 -0
  26. data/spec/unit/proxy_spec.rb +29 -0
  27. data/spec/unit/request_spec.rb +28 -0
  28. data/{test/unit/response_test.rb → spec/unit/response_spec.rb} +29 -44
  29. data/spec/unit/string_spec.rb +35 -0
  30. data/spec/unit/time_spec.rb +20 -0
  31. metadata +77 -104
  32. data/lib/rapuncel/base.rb +0 -7
  33. data/lib/rapuncel/core_ext/array.rb +0 -23
  34. data/lib/rapuncel/core_ext/big_decimal.rb +0 -7
  35. data/lib/rapuncel/core_ext/boolean.rb +0 -29
  36. data/lib/rapuncel/core_ext/float.rb +0 -11
  37. data/lib/rapuncel/core_ext/hash.rb +0 -32
  38. data/lib/rapuncel/core_ext/integer.rb +0 -11
  39. data/lib/rapuncel/core_ext/nil.rb +0 -7
  40. data/lib/rapuncel/core_ext/object.rb +0 -49
  41. data/lib/rapuncel/core_ext/string.rb +0 -12
  42. data/lib/rapuncel/core_ext/symbol.rb +0 -7
  43. data/lib/rapuncel/core_ext/time.rb +0 -14
  44. data/rapuncel-0.0.3.gem +0 -0
  45. data/test/functional/client_test.rb +0 -54
  46. data/test/functional_test_helper.rb +0 -13
  47. data/test/test_helper.rb +0 -38
  48. data/test/unit/array_test.rb +0 -97
  49. data/test/unit/boolean_test.rb +0 -34
  50. data/test/unit/connection_test.rb +0 -29
  51. data/test/unit/float_test.rb +0 -23
  52. data/test/unit/hash_test.rb +0 -54
  53. data/test/unit/int_test.rb +0 -27
  54. data/test/unit/nil_test.rb +0 -16
  55. data/test/unit/object_test.rb +0 -83
  56. data/test/unit/proxy_test.rb +0 -52
  57. data/test/unit/request_test.rb +0 -34
  58. data/test/unit/string_test.rb +0 -40
  59. data/test/unit/time_test.rb +0 -23
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'http://www.rubygems.org'
2
+ gemspec
3
+
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
- ## Alpha release!
7
- Not everything is working yet, especially: SSL, HTTP Basic Auth, ApiKey-Auth. If in doubt, have a look at the functionality being auto-tested.
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
- Username for HTTP Authentication
60
+ Password for HTTP Authentication
57
61
  _default_: _empty_
58
- * **auth\_method**
59
- HTTP Auth method
60
- _default_: basic **IF** user or password is set
61
- * **api\_key**
62
- If set, sends all request with a X-ApiKey: _api\_key_ header
63
- * **api\_key\_header**
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
- ### Can i customize this behavior for my objects?
101
- Yes you can, and it's dead simple, just override _to\_xml\_rpc_ with following signature (this is taken from Symbol#to\_xml\_rpc):
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
- def to_xml_rpc(builder = Rapuncel.get_builder)
110
- builder.string(self.to_s)
111
- end
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
@@ -1,25 +1,17 @@
1
-
2
-
3
1
  require 'active_support/core_ext/hash/keys'
4
-
5
2
  require 'nokogiri'
6
3
 
7
- require 'rapuncel/base'
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
- # Ruby Core extensions:
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 'active_support/memoizable'
2
+ require 'net/https'
3
3
 
4
4
  module Rapuncel
5
5
  module Adapters
6
6
  module NetHttpAdapter
7
- extend ActiveSupport::Memoizable
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
- req = Net::HTTP.new connection.host, connection.port
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 req.post(connection.path, str, connection.headers.stringify_keys)
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
@@ -17,7 +17,7 @@ module Rapuncel
17
17
  end
18
18
 
19
19
  def initialize configuration = {}
20
- @connection = Connection.build configuration.except(:raise_on)
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.to_xml_rpc
49
+ xml = XmlRpcSerializer.new(request).to_xml
50
50
 
51
51
  Response.new send_method_call(xml)
52
52
  end
@@ -1,40 +1,24 @@
1
1
  module Rapuncel
2
2
  class Connection
3
- def self.build configuration = {}
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
- "http://#{host}:#{port}#{path}"
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 :Accept => 'text/xml', :'content-type' => 'text/xml'
39
+ @headers.merge 'Accept' => 'text/xml', 'content-type' => 'text/xml'
50
40
  end
51
-
52
- def http_auth? ; false ; end
53
- def api_auth? ; false ; end
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 http_auth? ; true ; end
68
- end
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
- def headers
81
- super.merge api_key_header => api_key
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
@@ -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
@@ -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 = case
24
- when !http_response.success? then 'error'
25
- when parsed_body && method_response_success? then 'success'
26
- else 'fault'
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
- if fault?
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
- if error?
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
- if success?
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 parsed_body
76
- @xml_doc ||= Nokogiri::XML.parse body
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