barrister 0.1.0

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