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.
Files changed (2) hide show
  1. data/lib/barrister.rb +862 -0
  2. 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: []