jsonrpc2 0.0.8 → 0.0.9
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/jsonrpc2/client.rb +56 -21
- data/lib/jsonrpc2/types.rb +116 -118
- data/lib/jsonrpc2/version.rb +1 -1
- metadata +1 -1
data/lib/jsonrpc2/client.rb
CHANGED
@@ -10,6 +10,14 @@ end
|
|
10
10
|
class RemoteAuthError < RemoteError
|
11
11
|
end
|
12
12
|
|
13
|
+
# JSON RPC invalid JSON data
|
14
|
+
class RemoteDataError < RemoteError
|
15
|
+
end
|
16
|
+
|
17
|
+
# JSON RPC wrong content type
|
18
|
+
class WrongContentTypeError < RemoteError
|
19
|
+
end
|
20
|
+
|
13
21
|
# Simple JSONRPC client
|
14
22
|
class Client
|
15
23
|
# Create client object
|
@@ -17,11 +25,11 @@ class Client
|
|
17
25
|
# @param [String] uri Create client object
|
18
26
|
# @param [Hash] options Global options
|
19
27
|
def initialize(uri, options = {})
|
20
|
-
|
21
|
-
|
28
|
+
@uri = uri
|
29
|
+
@client = HTTPClient.new
|
22
30
|
@options = options
|
23
|
-
|
24
|
-
|
31
|
+
@id = 0
|
32
|
+
end
|
25
33
|
|
26
34
|
# Call method with named arguments
|
27
35
|
# @param [String] method Remote method name
|
@@ -30,8 +38,8 @@ class Client
|
|
30
38
|
# @return Method call result
|
31
39
|
# @raise [RemoteError] Error thrown by API
|
32
40
|
# @raise Transport/Network/HTTP errors
|
33
|
-
|
34
|
-
|
41
|
+
def call(method, args = {}, options = {}, &block)
|
42
|
+
headers = { 'Content-Type' => 'application/json-rpc' }
|
35
43
|
|
36
44
|
# Merge one level of hashes - ie. merge :headers
|
37
45
|
options = @options.merge(options) { |key,v1,v2| v2 = v1.merge(v2) if v1.class == v2.class && v1.is_a?(Hash); v2 }
|
@@ -43,30 +51,57 @@ class Client
|
|
43
51
|
if options[:user] && options[:pass]
|
44
52
|
@client.set_auth(@uri, options[:user], options[:pass])
|
45
53
|
end
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
54
|
+
result = @client.post(@uri,
|
55
|
+
{ 'method' => method, 'params' => args, 'jsonrpc' => '2.0', 'id' => (@id+=1) }.to_json,
|
56
|
+
headers)
|
57
|
+
|
58
|
+
body = result.body
|
59
|
+
body = body.content if body.respond_to?(:content) # Only on old versions of HTTPAccess2 ?
|
60
|
+
|
61
|
+
if result.status_code == 200
|
62
|
+
begin
|
63
|
+
data = JSON.parse body
|
64
|
+
rescue Exception
|
65
|
+
body = result.body.to_s
|
66
|
+
body = "#{body[0..256]}...[#{body.size-256} bytes trimmed]" if body.size > 256
|
67
|
+
|
68
|
+
if result.contenttype =~ /^application\/json/
|
69
|
+
raise RemoteDataError, "Content-Type is '#{result.contenttype}', but body of '#{body}' failed to parse."
|
70
|
+
else
|
71
|
+
raise WrongContentTypeError, "Content-Type is '#{result.contenttype}', but should be application/json-rpc or application/json (body='#{body}')"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
unless data.is_a?(Hash) && data["jsonrpc"] == "2.0"
|
76
|
+
raise RemoteDataError, "No jsonrpc parameter in response. This must be \"2.0\""
|
77
|
+
end
|
78
|
+
|
79
|
+
unless result.contenttype =~ /^application\/json/
|
80
|
+
STDERR.puts "WARNING: Content-Type is '#{result.contenttype}', but should be application/json-rpc or application/json."
|
81
|
+
end
|
82
|
+
|
83
|
+
if data.has_key?('result')
|
84
|
+
return data['result']
|
85
|
+
elsif data.has_key?('error')
|
56
86
|
if data['error']['code'] == -32000 && data['error']['message'] =~ /^AuthFail/
|
57
87
|
raise RemoteAuthError, data['error']['message']
|
58
88
|
else
|
59
89
|
raise RemoteError, data['error']['message']
|
60
90
|
end
|
61
|
-
|
91
|
+
else
|
92
|
+
body = result.body.to_s
|
93
|
+
body = "#{body[0..256]}...[#{body.size-256} bytes trimmed]" if body.size > 256
|
94
|
+
|
95
|
+
raise RemoteDataError, "A JSON-RPC 2.0 response must either have a 'result' or an 'error' value - in '#{body}'."
|
96
|
+
end
|
62
97
|
elsif result.status_code == 401
|
63
98
|
if result.headers['WWW-Authenticate'].to_s =~ /realm="([^"]*?)"/
|
64
99
|
suffix = " for #{$1}"
|
65
100
|
end
|
66
101
|
raise RemoteAuthError, "Authentication failed#{suffix}"
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
102
|
+
else
|
103
|
+
raise RemoteAuthError, "Call failed - HTTP status #{result.status_code}"
|
104
|
+
end
|
105
|
+
end
|
71
106
|
end
|
72
107
|
end
|
data/lib/jsonrpc2/types.rb
CHANGED
@@ -15,8 +15,8 @@ module JSONRPC2
|
|
15
15
|
# * Array [Type] - An array of type Type
|
16
16
|
# * Value (or Any or void) - Any value of any type
|
17
17
|
# * CustomType - A defined custom object type
|
18
|
-
|
19
|
-
|
18
|
+
module Types
|
19
|
+
module_function
|
20
20
|
DateTimeRegex = %r"([0-9]{4})(-([0-9]{2})(-([0-9]{2})(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?"
|
21
21
|
DateRegex = %r'\A\d{4}-\d{2}-\d{2}\z'
|
22
22
|
TimeRegex = %r'\A\d{2}:\d{2}(?:\.\d{1,4})?\z'
|
@@ -29,53 +29,51 @@ module JSONRPC2
|
|
29
29
|
# separated if more than one
|
30
30
|
# @param object Value to check check type
|
31
31
|
# @return [Boolean] true if ok
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
32
|
+
def valid?(interface, type_string, object)
|
33
|
+
res = type_string.split(/,/).any? do |type|
|
34
|
+
case type
|
35
|
+
when 'String'
|
36
|
+
object.is_a?(String)
|
37
|
+
when 'Number'
|
38
|
+
object.kind_of?(Numeric)
|
39
|
+
when 'true'
|
40
|
+
object == true
|
41
|
+
when 'false'
|
42
|
+
object == false
|
43
|
+
when 'Boolean'
|
44
|
+
object == true || object == false
|
45
|
+
when 'null'
|
46
|
+
object.nil?
|
47
|
+
when 'Integer'
|
48
|
+
object.kind_of?(Numeric) && (object.to_i.to_f == object.to_f)
|
49
|
+
when 'Object'
|
50
|
+
object.is_a?(Hash)
|
51
|
+
when 'Array'
|
52
|
+
object.is_a?(Array)
|
53
|
+
when 'Date'
|
54
|
+
object.is_a?(String) && DateRegex.match(object) || object.is_a?(Date)
|
55
|
+
when 'Time'
|
56
|
+
object.is_a?(String) && TimeRegex.match(object) || object.is_a?(Time)
|
57
|
+
when 'DateTime'
|
58
|
+
object.is_a?(String) && DateTimeRegex.match(object) || object.is_a?(Time)
|
59
|
+
when /\AArray\[(.*)\]\z/
|
60
|
+
object.is_a?(Array) && object.all? { |value| valid?(interface, $1, value) }
|
61
|
+
when 'Value', 'Any', 'void'
|
62
|
+
true
|
63
|
+
else # Custom type
|
64
|
+
subset = (type[-1] == ?*)
|
65
|
+
type = type[0...-1] if subset
|
66
66
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
raise "Invalid/unknown type: #{type} for #{interface.name}"
|
73
|
-
end
|
67
|
+
custom = interface.types[type]
|
68
|
+
if custom
|
69
|
+
custom.valid_object?(interface, object, subset)
|
70
|
+
else
|
71
|
+
raise "Invalid/unknown type: #{type} for #{interface.name}"
|
74
72
|
end
|
75
73
|
end
|
76
|
-
|
77
|
-
|
78
|
-
|
74
|
+
end
|
75
|
+
res
|
76
|
+
end
|
79
77
|
|
80
78
|
# Checks that param hash is valid for API call
|
81
79
|
#
|
@@ -83,103 +81,103 @@ module JSONRPC2
|
|
83
81
|
# @param [String] method Method name
|
84
82
|
# @param [Hash] data params hash to check
|
85
83
|
# @return [Boolean] true if ok
|
86
|
-
|
87
|
-
|
88
|
-
|
84
|
+
def valid_params?(interface, method, data)
|
85
|
+
about = interface.about[method.to_s]
|
86
|
+
return true if about.nil? # No defined params
|
89
87
|
|
90
|
-
|
91
|
-
|
88
|
+
params = (about[:params] || [])
|
89
|
+
param_names = params.map { |param| param[:name] }
|
92
90
|
|
93
|
-
|
94
|
-
|
95
|
-
|
91
|
+
if params.empty? && (data.nil? or data.empty?)
|
92
|
+
return true
|
93
|
+
end
|
96
94
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
95
|
+
raise "Params should not be nil" if data.nil?
|
96
|
+
|
97
|
+
extra_keys = data.keys - param_names
|
98
|
+
unless extra_keys.empty?
|
99
|
+
raise "Extra parameters #{extra_keys.inspect} for #{method}."
|
100
|
+
end
|
101
|
+
|
102
|
+
params.each do |param|
|
103
|
+
if data.has_key?(param[:name])
|
104
104
|
value = data[param[:name].to_s]
|
105
|
-
|
105
|
+
unless valid?(interface, param[:type], value)
|
106
106
|
raise "'#{param[:name]}' should be of type #{param[:type]}, was #{value.class.name}"
|
107
107
|
end
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
108
|
+
elsif ! param[:required]
|
109
|
+
next true
|
110
|
+
else
|
111
|
+
raise "Missing parameter: '#{param[:name]}' of type #{param[:type]} for #{method}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
115
|
|
116
116
|
# Checks that result is valid for API call
|
117
|
-
#
|
117
|
+
#
|
118
118
|
# @param [Interface] interface API class
|
119
119
|
# @param [String] method Method name
|
120
120
|
# @param [Hash] value Value to check
|
121
121
|
# @return [Boolean] true if ok
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
122
|
+
def valid_result?(interface, method, value)
|
123
|
+
about = interface.about[method.to_s]
|
124
|
+
return true if about.nil? # Undefined
|
125
|
+
if about[:returns].nil?
|
126
|
+
return value.nil?
|
127
|
+
end
|
128
|
+
valid?(interface, about[:returns][:type], value) or
|
129
129
|
raise "Invalid return type: should have been #{about[:returns][:type]}, was #{value.class.name}"
|
130
|
-
|
131
|
-
|
130
|
+
end
|
131
|
+
end
|
132
132
|
|
133
133
|
# Description of JSON object
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
134
|
+
class JsonObjectType
|
135
|
+
attr_accessor :name, :fields
|
136
|
+
def initialize(name, fields)
|
137
|
+
@name, @fields = name, fields
|
138
|
+
@required = true
|
139
|
+
end
|
140
140
|
|
141
141
|
# Check that #object Hash is valid version of this type
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
142
|
+
def valid_object?(interface, object, subset = false)
|
143
|
+
object.keys.all? { |key| fields.any? { |field| field[:name] == key } } &&
|
144
|
+
fields.all? { |field| (object.keys.include?(field[:name]) &&
|
145
|
+
Types.valid?(interface, field[:type], object[field[:name]])) || subset || (! field[:required]) }
|
146
|
+
end
|
147
147
|
|
148
148
|
# Add field of #name and #type to type description
|
149
|
-
|
150
|
-
|
151
|
-
|
149
|
+
def field(name, type, desc, options={})
|
150
|
+
@fields << { :name => name, :type => type, :desc => desc, :required => @required }.merge(options)
|
151
|
+
end
|
152
152
|
# Shortcut to define string field
|
153
|
-
|
153
|
+
def string name, desc, options={}; field(name, 'String', desc, options); end
|
154
154
|
# Shortcut to define number field
|
155
|
-
|
155
|
+
def number name, desc, options={}; field(name, 'Number', desc, options); end
|
156
156
|
# Shortcut to define integer field
|
157
|
-
|
157
|
+
def integer name, desc, options={}; field(name, 'Integer', desc, options); end
|
158
158
|
# Shortcut to define boolean field
|
159
|
-
|
159
|
+
def boolean name, desc, options={}; field(name, 'Boolean', desc, options); end
|
160
160
|
|
161
161
|
# Make fields defined in block optional by default
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
162
|
+
def optional(&block)
|
163
|
+
old_required = @required
|
164
|
+
begin
|
165
|
+
@required = false
|
166
|
+
yield(self)
|
167
|
+
ensure
|
168
|
+
@required = old_required
|
169
|
+
end
|
170
|
+
end
|
171
171
|
|
172
172
|
# Make fields defined in block required by default
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
end
|
184
|
-
|
173
|
+
def required(&block)
|
174
|
+
old_required = @required
|
175
|
+
begin
|
176
|
+
@required = true
|
177
|
+
yield(self)
|
178
|
+
ensure
|
179
|
+
@required = old_required
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
185
183
|
end
|
data/lib/jsonrpc2/version.rb
CHANGED