rapuncel 0.0.4 → 0.0.5.RC1

Sign up to get free protection for your applications and to get access to all the features.
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