barrister 0.1.0
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/barrister.rb +862 -0
- metadata +61 -0
data/lib/barrister.rb
ADDED
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
# **barrister.rb** contains Ruby bindings for Barrister RPC.
|
|
2
|
+
#
|
|
3
|
+
# The README on the github site has some basic usage examples.
|
|
4
|
+
# The Barrister web site has information how to write an IDL file.
|
|
5
|
+
#
|
|
6
|
+
# For more information, please visit:
|
|
7
|
+
#
|
|
8
|
+
# * [Barrister main site](http://barrister.bitmechanic.com/)
|
|
9
|
+
# * [barrister-ruby on github](https://github.com/coopernurse/barrister-ruby)
|
|
10
|
+
#
|
|
11
|
+
|
|
12
|
+
### Dependencies
|
|
13
|
+
|
|
14
|
+
# We use [flori's JSON library](http://flori.github.com/json/) which provides
|
|
15
|
+
# optional escaping for non-ascii characters.
|
|
16
|
+
require "json"
|
|
17
|
+
|
|
18
|
+
# We use the built in HTTP lib in the default HttpTransport class.
|
|
19
|
+
# You can write your own transport class if you want to use another lib
|
|
20
|
+
# such as typhoeus. Transports are designed to be pluggable.
|
|
21
|
+
require "net/http"
|
|
22
|
+
require "uri"
|
|
23
|
+
|
|
24
|
+
### Barrister Module
|
|
25
|
+
|
|
26
|
+
module Barrister
|
|
27
|
+
|
|
28
|
+
# Reads the given filename and returns a Barrister::Contract
|
|
29
|
+
# object. The filename should be a Barrister IDL JSON file created with
|
|
30
|
+
# the `barrister` tool.
|
|
31
|
+
def contract_from_file(fname)
|
|
32
|
+
file = File.open(fname, "r")
|
|
33
|
+
contents = file.read
|
|
34
|
+
file.close
|
|
35
|
+
idl = JSON::parse(contents)
|
|
36
|
+
return Contract.new(idl)
|
|
37
|
+
end
|
|
38
|
+
module_function :contract_from_file
|
|
39
|
+
|
|
40
|
+
# Helper function to generate IDs for requests. These IDs only need to
|
|
41
|
+
# be unique within a single request batch, although they may be used for
|
|
42
|
+
# other purposes in the future. This library will generate a 22 character
|
|
43
|
+
# alpha-numeric ID, which is about 130 bits of entropy.
|
|
44
|
+
def rand_str(len)
|
|
45
|
+
rchars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
46
|
+
s = ""
|
|
47
|
+
len.times do ||
|
|
48
|
+
pos = rand(rchars.length)
|
|
49
|
+
s += rchars[pos,1]
|
|
50
|
+
end
|
|
51
|
+
return s
|
|
52
|
+
end
|
|
53
|
+
module_function :rand_str
|
|
54
|
+
|
|
55
|
+
# Helper function that takes a JSON-RPC method string and tokenizes
|
|
56
|
+
# it at the period. Barrister encodes methods as "interface.function".
|
|
57
|
+
# Returns a two element tuple: interface name, and function name.
|
|
58
|
+
#
|
|
59
|
+
# If no period exists in the method, then we return a nil
|
|
60
|
+
# interface name, and the whole method as the function name.
|
|
61
|
+
def parse_method(method)
|
|
62
|
+
pos = method.index(".")
|
|
63
|
+
if pos == nil
|
|
64
|
+
return nil, method
|
|
65
|
+
else
|
|
66
|
+
iface_name = method.slice(0, pos)
|
|
67
|
+
func_name = method.slice(pos+1, method.length)
|
|
68
|
+
return iface_name, func_name
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
module_function :parse_method
|
|
72
|
+
|
|
73
|
+
# Helper function to create a JSON-RPC 2.0 response hash.
|
|
74
|
+
#
|
|
75
|
+
# * `req` - Request hash sent from the client
|
|
76
|
+
# * `result` - Result object from the handler function we called
|
|
77
|
+
#
|
|
78
|
+
# Returns a hash with the `result` slot set, but no `error` slot
|
|
79
|
+
def ok_resp(req, result)
|
|
80
|
+
resp = { "jsonrpc"=>"2.0", "result"=>result }
|
|
81
|
+
if req["id"]
|
|
82
|
+
resp["id"] = req["id"]
|
|
83
|
+
end
|
|
84
|
+
return resp
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Helper function to create a JSON-RPC 2.0 response error hash.
|
|
88
|
+
#
|
|
89
|
+
# * `req` - Request hash sent from the client
|
|
90
|
+
# * `code` - Integer error code
|
|
91
|
+
# * `message` - String description of the error
|
|
92
|
+
# * `data` - Optional. Additional info about the error. Must be JSON serializable.
|
|
93
|
+
#
|
|
94
|
+
# Returns a hash with the `error` slot set, but no `result`
|
|
95
|
+
def err_resp(req, code, message, data=nil)
|
|
96
|
+
resp = { "jsonrpc"=>"2.0", "error"=> { "code"=>code, "message"=>message } }
|
|
97
|
+
if req["id"]
|
|
98
|
+
resp["id"] = req["id"]
|
|
99
|
+
end
|
|
100
|
+
if data
|
|
101
|
+
resp["error"]["data"] = data
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
return resp
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Represents a JSON-RPC error response. The Client proxy classes raise this
|
|
108
|
+
# exception if a response is received with an `error` slot.
|
|
109
|
+
#
|
|
110
|
+
# See the [JSON-RPC 2.0 spec](http://jsonrpc.org/specification) for info on
|
|
111
|
+
# built in error codes. Your code can raise this exception with custom error
|
|
112
|
+
# codes. Use positive integers as codes to avoid collisions with the built in
|
|
113
|
+
# error codes.
|
|
114
|
+
class RpcException < StandardError
|
|
115
|
+
|
|
116
|
+
attr_accessor :code, :message, :data
|
|
117
|
+
|
|
118
|
+
def initialize(code, message, data=nil)
|
|
119
|
+
@code = code
|
|
120
|
+
@message = message
|
|
121
|
+
@data = data
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
### Server
|
|
127
|
+
|
|
128
|
+
# The Server class is responsible for taking an incoming request, validating
|
|
129
|
+
# the method and params, invoking the correct handler function (your code), and
|
|
130
|
+
# returning the result.
|
|
131
|
+
#
|
|
132
|
+
# Server has a Barrister::Contract that is initialized in the contructor.
|
|
133
|
+
# It uses the Contract for validation.
|
|
134
|
+
#
|
|
135
|
+
# The Server doesn't do any network communication. It contains a default
|
|
136
|
+
# `handle_json` convenience method that encapsulates JSON serialization, and a
|
|
137
|
+
# lower level `handle` method. This will make it easy to add other serialization
|
|
138
|
+
# formats (such as MessagePack) later.
|
|
139
|
+
#
|
|
140
|
+
class Server
|
|
141
|
+
include Barrister
|
|
142
|
+
|
|
143
|
+
# Create a server with the given Barrister::Contract instance
|
|
144
|
+
def initialize(contract)
|
|
145
|
+
@contract = contract
|
|
146
|
+
@handlers = { }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Register a handler class with the given interface name
|
|
150
|
+
#
|
|
151
|
+
# The `handler` is any Ruby class that contains methods for each
|
|
152
|
+
# function on the given IDL interface name.
|
|
153
|
+
#
|
|
154
|
+
# These methods will be called when a request is handled by the Server.
|
|
155
|
+
def add_handler(iface_name, handler)
|
|
156
|
+
iface = @contract.interface(iface_name)
|
|
157
|
+
if !iface
|
|
158
|
+
raise "No interface found with name: #{iface_name}"
|
|
159
|
+
end
|
|
160
|
+
@handlers[iface_name] = handler
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Handles a request encoded as JSON.
|
|
164
|
+
# Returns the result as a JSON encoded string.
|
|
165
|
+
def handle_json(json_str)
|
|
166
|
+
begin
|
|
167
|
+
req = JSON::parse(json_str)
|
|
168
|
+
resp = handle(req)
|
|
169
|
+
rescue JSON::ParserError => e
|
|
170
|
+
resp = err_resp({ }, -32700, "Unable to parse JSON: #{e.message}")
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Note the `:ascii_only` usage here. Important.
|
|
174
|
+
return JSON::generate(resp, { :ascii_only=>true })
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Handles a deserialized request and returns the result
|
|
178
|
+
#
|
|
179
|
+
# `req` must either be a Hash (single request), or an Array (batch request)
|
|
180
|
+
#
|
|
181
|
+
# `handle` returns an Array of results for batch requests, and a single
|
|
182
|
+
# Hash for single requests.
|
|
183
|
+
def handle(req)
|
|
184
|
+
if req.kind_of?(Array)
|
|
185
|
+
resp_list = [ ]
|
|
186
|
+
req.each do |r|
|
|
187
|
+
resp_list << handle_single(r)
|
|
188
|
+
end
|
|
189
|
+
return resp_list
|
|
190
|
+
else
|
|
191
|
+
return handle_single(req)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Internal method that validates and executes a single request.
|
|
196
|
+
def handle_single(req)
|
|
197
|
+
method = req["method"]
|
|
198
|
+
if !method
|
|
199
|
+
return err_resp(req, -32600, "No method provided on request")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Special case - client is requesting the IDL bound to this server, so
|
|
203
|
+
# we return it verbatim. No further validation is needed in this case.
|
|
204
|
+
if method == "barrister-idl"
|
|
205
|
+
return ok_resp(req, @contract.idl)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Make sure we can find an interface and function on the IDL for this
|
|
209
|
+
# request method string
|
|
210
|
+
err_resp, iface, func = @contract.resolve_method(req)
|
|
211
|
+
if err_resp != nil
|
|
212
|
+
return err_resp
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Make sure that the params on the request match the IDL types
|
|
216
|
+
err_resp = @contract.validate_params(req, func)
|
|
217
|
+
if err_resp != nil
|
|
218
|
+
return err_resp
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
params = [ ]
|
|
222
|
+
if req["params"]
|
|
223
|
+
params = req["params"]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Make sure we have a handler bound to this Server for the interface.
|
|
227
|
+
# If not, that means `server.add_handler` was not called for this interface
|
|
228
|
+
# name. That's likely a misconfiguration.
|
|
229
|
+
handler = @handlers[iface.name]
|
|
230
|
+
if !handler
|
|
231
|
+
return err_resp(req, -32000, "Server error. No handler is bound to interface #{iface.name}")
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Make sure that the handler has a method for the given function.
|
|
235
|
+
if !handler.respond_to?(func.name)
|
|
236
|
+
return err_resp(req, -32000, "Server error. Handler for #{iface.name} does not implement #{func.name}")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
begin
|
|
240
|
+
# Call the handler function. This is where your code gets invoked.
|
|
241
|
+
result = handler.send(func.name, *params)
|
|
242
|
+
|
|
243
|
+
# Verify that the handler function's return value matches the
|
|
244
|
+
# correct type as specified in the IDL
|
|
245
|
+
err_resp = @contract.validate_result(req, result, func)
|
|
246
|
+
if err_resp != nil
|
|
247
|
+
return err_resp
|
|
248
|
+
else
|
|
249
|
+
return ok_resp(req, result)
|
|
250
|
+
end
|
|
251
|
+
rescue RpcException => e
|
|
252
|
+
# If the handler raised a RpcException, that's ok - return it unmodified.
|
|
253
|
+
return err_resp(req, e.code, e.message, e.data)
|
|
254
|
+
rescue => e
|
|
255
|
+
# If any other error was raised, print it and return a generic error to the client
|
|
256
|
+
puts e.inspect
|
|
257
|
+
puts e.backtrace
|
|
258
|
+
return err_resp(req, -32000, "Unknown error: #{e}")
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
### Client
|
|
265
|
+
|
|
266
|
+
# This is the main class used when writing a client for a Barrister service.
|
|
267
|
+
#
|
|
268
|
+
# Clients accept a transport class on the constructor which encapsulates
|
|
269
|
+
# serialization and network communciation. Currently this module only provides a
|
|
270
|
+
# basic HTTP transport, but other transports can be easily written.
|
|
271
|
+
class Client
|
|
272
|
+
include Barrister
|
|
273
|
+
|
|
274
|
+
attr_accessor :trans
|
|
275
|
+
|
|
276
|
+
# Create a new Client. This immediately makes a `barrister-idl` request to fetch
|
|
277
|
+
# the IDL from the Server. A Barrister::Contract is created from this IDL and used
|
|
278
|
+
# to expose proxy objects for each interface on the IDL.
|
|
279
|
+
#
|
|
280
|
+
# * `trans` - Transport instance to use. Must have a `request(req)` method
|
|
281
|
+
# * `validate_req` - If true, request parameters will be validated against the IDL
|
|
282
|
+
# before sending the request to the transport.
|
|
283
|
+
# * `validate_result` - If true, the result from the server will be validated against the IDL
|
|
284
|
+
#
|
|
285
|
+
def initialize(trans, validate_req=true, validate_result=true)
|
|
286
|
+
@trans = trans
|
|
287
|
+
@validate_req = validate_req
|
|
288
|
+
@validate_result = validate_result
|
|
289
|
+
|
|
290
|
+
load_contract
|
|
291
|
+
init_proxies
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Returns a Barrister::BatchClient instance that is associated with this Client instance
|
|
295
|
+
#
|
|
296
|
+
# Batches let you send multiple requests in a single round trip
|
|
297
|
+
def start_batch
|
|
298
|
+
return BatchClient.new(self, @contract)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Internal method invoked by `initialize`. Sends a `barrister-idl` request to the
|
|
302
|
+
# server and creates a Barrister::Contract with the result.
|
|
303
|
+
def load_contract
|
|
304
|
+
req = { "jsonrpc" => "2.0", "id" => "1", "method" => "barrister-idl" }
|
|
305
|
+
resp = @trans.request(req)
|
|
306
|
+
if resp.key?("result")
|
|
307
|
+
@contract = Contract.new(resp["result"])
|
|
308
|
+
else
|
|
309
|
+
raise RpcException.new(-32000, "Invalid contract response: #{resp}")
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Internal method invoked by `initialize`. Iterates through the Contract and
|
|
314
|
+
# creates proxy classes for each interface.
|
|
315
|
+
def init_proxies
|
|
316
|
+
singleton = class << self; self end
|
|
317
|
+
@contract.interfaces.each do |iface|
|
|
318
|
+
proxy = InterfaceProxy.new(self, iface)
|
|
319
|
+
singleton.send :define_method, iface.name do
|
|
320
|
+
return proxy
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Sends a JSON-RPC request. This method is automatically called by the proxy classes,
|
|
326
|
+
# so in practice you don't usually call it directly. However, it is available if you
|
|
327
|
+
# wish to avoid the use of proxy classes.
|
|
328
|
+
#
|
|
329
|
+
# * `method` - string of the method to invoke. Format: "interface.function".
|
|
330
|
+
# For example: "ContactService.saveContact"
|
|
331
|
+
# * `params` - parameters to pass to the function. Must be an Array
|
|
332
|
+
def request(method, params)
|
|
333
|
+
req = { "jsonrpc" => "2.0", "id" => Barrister::rand_str(22), "method" => method }
|
|
334
|
+
if params
|
|
335
|
+
req["params"] = params
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# We always validate that the method is valid
|
|
339
|
+
err_resp, iface, func = @contract.resolve_method(req)
|
|
340
|
+
if err_resp != nil
|
|
341
|
+
return err_resp
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
if @validate_req
|
|
345
|
+
err_resp = @contract.validate_params(req, func)
|
|
346
|
+
if err_resp != nil
|
|
347
|
+
return err_resp
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# This makes the request to the server
|
|
352
|
+
resp = @trans.request(req)
|
|
353
|
+
|
|
354
|
+
if @validate_result && resp != nil && resp.key?("result")
|
|
355
|
+
err_resp = @contract.validate_result(req, resp["result"], func)
|
|
356
|
+
if err_resp != nil
|
|
357
|
+
resp = err_resp
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
return resp
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Default HTTP transport implementation. This is a simple implementation that
|
|
367
|
+
# doesn't support many options. We may extend this class in the future, but
|
|
368
|
+
# you can always write your own transport class based on this one.
|
|
369
|
+
class HttpTransport
|
|
370
|
+
|
|
371
|
+
# Takes the URL to the server endpoint and parses it
|
|
372
|
+
def initialize(url)
|
|
373
|
+
@url = url
|
|
374
|
+
@uri = URI.parse(url)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# `request` is the only required method on a transport class.
|
|
378
|
+
#
|
|
379
|
+
# `req` is a JSON-RPC request with `id`, `method`, and optionally `params` slots.
|
|
380
|
+
#
|
|
381
|
+
# The transport is very simple, and does the following:
|
|
382
|
+
#
|
|
383
|
+
# * Serialize `req` to JSON. Make sure to use `:ascii_only=true`
|
|
384
|
+
# * POST the JSON string to the endpoint, setting the MIME type correctly
|
|
385
|
+
# * Deserialize the JSON response string
|
|
386
|
+
# * Return the deserialized hash
|
|
387
|
+
#
|
|
388
|
+
def request(req)
|
|
389
|
+
json_str = JSON::generate(req, { :ascii_only=>true })
|
|
390
|
+
http = Net::HTTP.new(@uri.host, @uri.port)
|
|
391
|
+
request = Net::HTTP::Post.new(@uri.request_uri)
|
|
392
|
+
request.body = json_str
|
|
393
|
+
request["Content-Type"] = "application/json"
|
|
394
|
+
response = http.request(request)
|
|
395
|
+
if response.code != "200"
|
|
396
|
+
raise RpcException.new(-32000, "Non-200 response #{response.code} from #{@url}")
|
|
397
|
+
else
|
|
398
|
+
return JSON::parse(response.body)
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Represents as single JSON-RPC response. This is used by the Batch class
|
|
405
|
+
# so that responses are wrapped in a more friendly class container.
|
|
406
|
+
#
|
|
407
|
+
# Non-batch calls don't need this wrapper, as they receive the result directly,
|
|
408
|
+
# or have a RpcException raised.
|
|
409
|
+
class RpcResponse
|
|
410
|
+
|
|
411
|
+
# Properties exposed on the response
|
|
412
|
+
#
|
|
413
|
+
# You can raise `resp.error` when you iterate through
|
|
414
|
+
# results from a batch send.
|
|
415
|
+
attr_accessor :id, :method, :params, :result, :error
|
|
416
|
+
|
|
417
|
+
def initialize(req, resp)
|
|
418
|
+
@id = resp["id"]
|
|
419
|
+
@result = resp["result"]
|
|
420
|
+
@method = req["method"]
|
|
421
|
+
@params = req["params"]
|
|
422
|
+
|
|
423
|
+
if resp["error"]
|
|
424
|
+
e = resp["error"]
|
|
425
|
+
@error = RpcException.new(e["code"], e["message"], e["data"])
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Internal transport used by the BatchClient. You shouldn't create this
|
|
432
|
+
# directly.
|
|
433
|
+
class BatchTransport
|
|
434
|
+
|
|
435
|
+
attr_accessor :sent, :requests
|
|
436
|
+
|
|
437
|
+
def initialize(client)
|
|
438
|
+
@client = client
|
|
439
|
+
@requests = [ ]
|
|
440
|
+
@sent = false
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# Request simply stores the req object in an interal Array
|
|
444
|
+
# When send() is called on the BatchClient, these are sent to the server.
|
|
445
|
+
def request(req)
|
|
446
|
+
if @sent
|
|
447
|
+
raise "Batch has already been sent!"
|
|
448
|
+
end
|
|
449
|
+
@requests << req
|
|
450
|
+
return nil
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# BatchClient acts like a Client and exposes the same proxy classes
|
|
456
|
+
# as a normal Client instance. However, none of the proxy function calls
|
|
457
|
+
# return values. Instead, they are stored in an Array until `batch.send()`
|
|
458
|
+
# is called.
|
|
459
|
+
#
|
|
460
|
+
# Use a batch if you have many small requests that you'd like to send at once.
|
|
461
|
+
#
|
|
462
|
+
# **Note:** the JSON-RPC spec indicates that servers **may** execute batch
|
|
463
|
+
# requests in parallel. Do **not** batch requests that depend on being
|
|
464
|
+
# sequentially executed.
|
|
465
|
+
class BatchClient < Client
|
|
466
|
+
|
|
467
|
+
# * `parent` - the Client instance we were created from
|
|
468
|
+
# * `contract` - The contract associated with this Client. Used to init proxies.
|
|
469
|
+
def initialize(parent, contract)
|
|
470
|
+
@parent = parent
|
|
471
|
+
@trans = BatchTransport.new(self)
|
|
472
|
+
@contract = contract
|
|
473
|
+
init_proxies
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Overrides start_batch and blows up if called
|
|
477
|
+
def start_batch
|
|
478
|
+
raise "Cannot call start_batch on a batch!"
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Sends the batch of requests to the server.
|
|
482
|
+
#
|
|
483
|
+
# Returns an Array of RpcResponse instances. The Array is ordered
|
|
484
|
+
# in the order of the requests made to the batch. Your code needs
|
|
485
|
+
# to check each element in the Array for errors.
|
|
486
|
+
#
|
|
487
|
+
# * Cannot be called more than once
|
|
488
|
+
# * Will raise RpcException if the batch is empty
|
|
489
|
+
def send
|
|
490
|
+
if @trans.sent
|
|
491
|
+
raise "Batch has already been sent!"
|
|
492
|
+
end
|
|
493
|
+
@trans.sent = true
|
|
494
|
+
|
|
495
|
+
requests = @trans.requests
|
|
496
|
+
|
|
497
|
+
if requests.length < 1
|
|
498
|
+
raise RpcException.new(-32600, "Batch cannot be empty")
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# Send request batch to server
|
|
502
|
+
resp_list = @parent.trans.request(requests)
|
|
503
|
+
|
|
504
|
+
# Build a hash for the responses so we can re-order them
|
|
505
|
+
# in request order.
|
|
506
|
+
sorted = [ ]
|
|
507
|
+
by_req_id = { }
|
|
508
|
+
resp_list.each do |resp|
|
|
509
|
+
by_req_id[resp["id"]] = resp
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Iterate through the requests in the batch and assemble
|
|
513
|
+
# the sorted result array
|
|
514
|
+
requests.each do |req|
|
|
515
|
+
id = req["id"]
|
|
516
|
+
resp = by_req_id[id]
|
|
517
|
+
if !resp
|
|
518
|
+
msg = "No result for request id: #{id}"
|
|
519
|
+
resp = { "id" => id, "error" => { "code"=>-32603, "message" => msg } }
|
|
520
|
+
end
|
|
521
|
+
sorted << RpcResponse.new(req, resp)
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
return sorted
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# Internal class used by the Client and BatchClient classes
|
|
530
|
+
#
|
|
531
|
+
# Each instance represents a proxy for a single interface in the IDL,
|
|
532
|
+
# and will contain a method for each function in the interface.
|
|
533
|
+
#
|
|
534
|
+
# These proxy methods call `Client.request` when invoked
|
|
535
|
+
#
|
|
536
|
+
class InterfaceProxy
|
|
537
|
+
|
|
538
|
+
def initialize(client, iface)
|
|
539
|
+
singleton = class << self; self end
|
|
540
|
+
iface.functions.each do |f|
|
|
541
|
+
method = iface.name + "." + f.name
|
|
542
|
+
singleton.send :define_method, f.name do |*args|
|
|
543
|
+
resp = client.request(method, args)
|
|
544
|
+
if client.trans.instance_of? BatchTransport
|
|
545
|
+
return nil
|
|
546
|
+
else
|
|
547
|
+
if resp.key?("result")
|
|
548
|
+
return resp["result"]
|
|
549
|
+
else
|
|
550
|
+
err = resp["error"]
|
|
551
|
+
raise RpcException.new(err["code"], err["message"], err["data"])
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
### Contract / IDL
|
|
561
|
+
|
|
562
|
+
# Represents a single parsed IDL definition
|
|
563
|
+
class Contract
|
|
564
|
+
include Barrister
|
|
565
|
+
|
|
566
|
+
attr_accessor :idl
|
|
567
|
+
|
|
568
|
+
# `idl` must be an Array loaded from a Barrister IDL JSON file
|
|
569
|
+
#
|
|
570
|
+
# `initialize` iterates through the IDL and stores the
|
|
571
|
+
# interfaces, structs, and enums specified in the IDL
|
|
572
|
+
def initialize(idl)
|
|
573
|
+
@idl = idl
|
|
574
|
+
@interfaces = { }
|
|
575
|
+
@structs = { }
|
|
576
|
+
@enums = { }
|
|
577
|
+
|
|
578
|
+
idl.each do |item|
|
|
579
|
+
type = item["type"]
|
|
580
|
+
if type == "interface"
|
|
581
|
+
@interfaces[item["name"]] = Interface.new(item)
|
|
582
|
+
elsif type == "struct"
|
|
583
|
+
@structs[item["name"]] = item
|
|
584
|
+
elsif type == "enum"
|
|
585
|
+
@enums[item["name"]] = item
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
# Returns the Interface instance for the given name
|
|
591
|
+
def interface(name)
|
|
592
|
+
return @interfaces[name]
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
# Returns all Interfaces defined on this Contract
|
|
596
|
+
def interfaces
|
|
597
|
+
return @interfaces.values
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
# Takes a JSON-RPC request hash, and returns a 3 element tuple. This is called as
|
|
601
|
+
# part of the request validation sequence.
|
|
602
|
+
#
|
|
603
|
+
# `0` - JSON-RPC response hash representing an error. nil if valid.
|
|
604
|
+
# `1` - Interface instance on this Contract that matches `req["method"]`
|
|
605
|
+
# `2` - Function instance on the Interface that matches `req["method"]`
|
|
606
|
+
def resolve_method(req)
|
|
607
|
+
method = req["method"]
|
|
608
|
+
iface_name, func_name = Barrister::parse_method(method)
|
|
609
|
+
if iface_name == nil
|
|
610
|
+
return err_resp(req, -32601, "Method not found: #{method}")
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
iface = interface(iface_name)
|
|
614
|
+
if !iface
|
|
615
|
+
return err_resp(req, -32601, "Interface not found on IDL: #{iface_name}")
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
func = iface.function(func_name)
|
|
619
|
+
if !func
|
|
620
|
+
return err_resp(req, -32601, "Function #{func_name} does not exist on interface #{iface_name}")
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
return nil, iface, func
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# Validates that the parameters on the JSON-RPC request match the types specified for
|
|
627
|
+
# this function
|
|
628
|
+
#
|
|
629
|
+
# Returns a JSON-RPC response hash if invalid, or nil if valid.
|
|
630
|
+
#
|
|
631
|
+
# * `req` - JSON-RPC request hash
|
|
632
|
+
# * `func` - Barrister::Function instance
|
|
633
|
+
#
|
|
634
|
+
def validate_params(req, func)
|
|
635
|
+
params = req["params"]
|
|
636
|
+
if !params
|
|
637
|
+
params = []
|
|
638
|
+
end
|
|
639
|
+
e_params = func.params.length
|
|
640
|
+
r_params = params.length
|
|
641
|
+
if e_params != r_params
|
|
642
|
+
msg = "Function #{func.name}: Param length #{r_params} != expected length: #{e_params}"
|
|
643
|
+
return err_resp(req, -32602, msg)
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
for i in (0..(e_params-1))
|
|
647
|
+
expected = func.params[i]
|
|
648
|
+
invalid = validate("Param[#{i}]", expected, expected["is_array"], params[i])
|
|
649
|
+
if invalid != nil
|
|
650
|
+
return err_resp(req, -32602, invalid)
|
|
651
|
+
end
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
# valid
|
|
655
|
+
return nil
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
# Validates that the result from a handler method invocation match the return type
|
|
659
|
+
# for this function
|
|
660
|
+
#
|
|
661
|
+
# Returns a JSON-RPC response hash if invalid, or nil if valid.
|
|
662
|
+
#
|
|
663
|
+
# * `req` - JSON-RPC request hash
|
|
664
|
+
# * `result` - Result object from the handler method call
|
|
665
|
+
# * `func` - Barrister::Function instance
|
|
666
|
+
#
|
|
667
|
+
def validate_result(req, result, func)
|
|
668
|
+
invalid = validate("", func.returns, func.returns["is_array"], result)
|
|
669
|
+
if invalid == nil
|
|
670
|
+
return nil
|
|
671
|
+
else
|
|
672
|
+
return err_resp(req, -32001, invalid)
|
|
673
|
+
end
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
# Validates the type for a single value. This method is recursive when validating
|
|
677
|
+
# arrays or structs.
|
|
678
|
+
#
|
|
679
|
+
# Returns a string describing the validation error if invalid, or nil if valid
|
|
680
|
+
#
|
|
681
|
+
# * `name` - string to prefix onto the validation error
|
|
682
|
+
# * `expected` - expected type (hash)
|
|
683
|
+
# * `expect_array` - if true, we expect val to be an Array
|
|
684
|
+
# * `val` - value to validate
|
|
685
|
+
#
|
|
686
|
+
def validate(name, expected, expect_array, val)
|
|
687
|
+
# If val is nil, then check if the IDL allows this type to be optional
|
|
688
|
+
if val == nil
|
|
689
|
+
if expected["optional"]
|
|
690
|
+
return nil
|
|
691
|
+
else
|
|
692
|
+
return "#{name} cannot be null"
|
|
693
|
+
end
|
|
694
|
+
else
|
|
695
|
+
exp_type = expected["type"]
|
|
696
|
+
|
|
697
|
+
# If we expect an array, make sure that val is an Array, and then
|
|
698
|
+
# recursively validate the elements in the array
|
|
699
|
+
if expect_array
|
|
700
|
+
if val.kind_of?(Array)
|
|
701
|
+
stop = val.length - 1
|
|
702
|
+
for i in (0..stop)
|
|
703
|
+
invalid = validate("#{name}[#{i}]", expected, false, val[i])
|
|
704
|
+
if invalid != nil
|
|
705
|
+
return invalid
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
return nil
|
|
710
|
+
else
|
|
711
|
+
return type_err(name, "[]"+expected["type"], val)
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
# Check the built in Barrister primitive types
|
|
715
|
+
elsif exp_type == "string"
|
|
716
|
+
if val.class == String
|
|
717
|
+
return nil
|
|
718
|
+
else
|
|
719
|
+
return type_err(name, exp_type, val)
|
|
720
|
+
end
|
|
721
|
+
elsif exp_type == "bool"
|
|
722
|
+
if val.class == TrueClass || val.class == FalseClass
|
|
723
|
+
return nil
|
|
724
|
+
else
|
|
725
|
+
return type_err(name, exp_type, val)
|
|
726
|
+
end
|
|
727
|
+
elsif exp_type == "int" || exp_type == "float"
|
|
728
|
+
if val.class == Integer || val.class == Fixnum || val.class == Bignum
|
|
729
|
+
return nil
|
|
730
|
+
elsif val.class == Float && exp_type == "float"
|
|
731
|
+
return nil
|
|
732
|
+
else
|
|
733
|
+
return type_err(name, exp_type, val)
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
# Expected type is not an array or a Barrister primitive.
|
|
737
|
+
# It must be a struct or an enum.
|
|
738
|
+
else
|
|
739
|
+
|
|
740
|
+
# Try to find a struct
|
|
741
|
+
struct = @structs[exp_type]
|
|
742
|
+
if struct
|
|
743
|
+
if !val.kind_of?(Hash)
|
|
744
|
+
return "#{name} #{exp_type} value must be a map/hash. not: " + val.class.name
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
s_field_keys = { }
|
|
748
|
+
|
|
749
|
+
# Resolve all fields on the struct and its ancestors
|
|
750
|
+
s_fields = all_struct_fields([], struct)
|
|
751
|
+
|
|
752
|
+
# Validate that each field on the struct has a valid value
|
|
753
|
+
s_fields.each do |f|
|
|
754
|
+
fname = f["name"]
|
|
755
|
+
invalid = validate("#{name}.#{fname}", f, f["is_array"], val[fname])
|
|
756
|
+
if invalid != nil
|
|
757
|
+
return invalid
|
|
758
|
+
end
|
|
759
|
+
s_field_keys[fname] = 1
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
# Validate that there are no extraneous elements on the value
|
|
763
|
+
val.keys.each do |k|
|
|
764
|
+
if !s_field_keys.key?(k)
|
|
765
|
+
return "#{name}.#{k} is not a field in struct '#{exp_type}'"
|
|
766
|
+
end
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
# Struct is valid
|
|
770
|
+
return nil
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
# Try to find an enum
|
|
774
|
+
enum = @enums[exp_type]
|
|
775
|
+
if enum
|
|
776
|
+
if val.class != String
|
|
777
|
+
return "#{name} enum value must be a string. got: " + val.class.name
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
# Try to find an enum value that matches this val
|
|
781
|
+
enum["values"].each do |en|
|
|
782
|
+
if en["value"] == val
|
|
783
|
+
return nil
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
# Invalid
|
|
788
|
+
return "#{name} #{val} is not a value in enum '#{exp_type}'"
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
# Unlikely branch - suggests the IDL is internally inconsistent
|
|
792
|
+
return "#{name} unknown type: #{exp_type}"
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
# Panic if we have a branch unaccounted for. Indicates a Barrister bug.
|
|
796
|
+
raise "Barrister ERROR: validate did not return for: #{name} #{expected}"
|
|
797
|
+
end
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
# Recursively resolves all fields for the struct and its ancestors
|
|
801
|
+
#
|
|
802
|
+
# Returns an Array with all the fields
|
|
803
|
+
def all_struct_fields(arr, struct)
|
|
804
|
+
struct["fields"].each do |f|
|
|
805
|
+
arr << f
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
if struct["extends"]
|
|
809
|
+
parent = @structs[struct["extends"]]
|
|
810
|
+
if parent
|
|
811
|
+
return all_struct_fields(arr, parent)
|
|
812
|
+
end
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
return arr
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
# Helper function that returns a formatted string for a type mismatch error
|
|
819
|
+
def type_err(name, exp_type, val)
|
|
820
|
+
actual = val.class.name
|
|
821
|
+
return "#{name} expects type '#{exp_type}' but got type '#{actual}'"
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
# Represents a Barrister IDL "interface"
|
|
827
|
+
class Interface
|
|
828
|
+
|
|
829
|
+
attr_accessor :name
|
|
830
|
+
|
|
831
|
+
def initialize(iface)
|
|
832
|
+
@name = iface["name"]
|
|
833
|
+
@functions = { }
|
|
834
|
+
iface["functions"].each do |f|
|
|
835
|
+
@functions[f["name"]] = Function.new(f)
|
|
836
|
+
end
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
def functions
|
|
840
|
+
return @functions.values
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
def function(name)
|
|
844
|
+
return @functions[name]
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
end
|
|
848
|
+
|
|
849
|
+
# Represents a single function on a Barrister IDL "interface"
|
|
850
|
+
class Function
|
|
851
|
+
|
|
852
|
+
attr_accessor :name, :returns, :params
|
|
853
|
+
|
|
854
|
+
def initialize(f)
|
|
855
|
+
@name = f["name"]
|
|
856
|
+
@returns = f["returns"]
|
|
857
|
+
@params = f["params"]
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: barrister
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- James Cooper
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2012-04-11 00:00:00.000000000 Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: json
|
|
16
|
+
requirement: &2153042800 !ruby/object:Gem::Requirement
|
|
17
|
+
none: false
|
|
18
|
+
requirements:
|
|
19
|
+
- - ! '>='
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: 1.5.0
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: *2153042800
|
|
25
|
+
description: ! 'Barrister RPC makes it easy to expose type safe services. This module
|
|
26
|
+
|
|
27
|
+
provides Ruby bindings for Barrister.
|
|
28
|
+
|
|
29
|
+
'
|
|
30
|
+
email:
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- lib/barrister.rb
|
|
36
|
+
homepage: https://github.com/coopernurse/barrister-ruby
|
|
37
|
+
licenses:
|
|
38
|
+
- MIT
|
|
39
|
+
post_install_message:
|
|
40
|
+
rdoc_options: []
|
|
41
|
+
require_paths:
|
|
42
|
+
- lib
|
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
44
|
+
none: false
|
|
45
|
+
requirements:
|
|
46
|
+
- - ! '>='
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0'
|
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
|
+
none: false
|
|
51
|
+
requirements:
|
|
52
|
+
- - ! '>='
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
requirements: []
|
|
56
|
+
rubyforge_project:
|
|
57
|
+
rubygems_version: 1.8.11
|
|
58
|
+
signing_key:
|
|
59
|
+
specification_version: 3
|
|
60
|
+
summary: Ruby bindings for Barrister RPC
|
|
61
|
+
test_files: []
|