blest 0.1.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +10 -41
  4. data/lib/blest.rb +48 -387
  5. data/spec/blest_spec.rb +15 -14
  6. metadata +9 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6307985d2fcaa8aa558a9d6fa55bd61787de23accd0d762d12843cf1202784c8
4
- data.tar.gz: b58fc887beb5b6312f14f7332daab521b37baa037750eb3b1b406191160ae0d1
3
+ metadata.gz: dda8d494a672ecd29209b9971076decfaec476253157103dc75a6d74cfc2f327
4
+ data.tar.gz: dee130556eb32a00cdbb14ae3f5c160133241763061d6558592d1712963cbfc4
5
5
  SHA512:
6
- metadata.gz: 1f0b5c80b2ae39259d5386c60a7a27deb6b04dc35a5162c30be64056d10581de3b6a6ee47cd800e27f4e4cac1f52a6cb692b47abbda7533ed48dc85c3eafe403
7
- data.tar.gz: 964ab1acc3ca2db12a2918bb141efa3f254438c095b47c7e81313c4b5140d7a10b13712b3a4773e6fa1b76158ccc75dffa2af591df45fdb55a767278ea72e470
6
+ metadata.gz: 4407b468d713463014fa4449efb4e2be40ac59d93fbca2302d1ddc13aa036907b0ba6b4067cd92e1e90a64e37dcca3cd29b0aec605722fd7a5edf4458058245f
7
+ data.tar.gz: f1c050cdaa44297e90c968ba99be88f3fb440743a959e9ab32286d04ce42a4dc886059bb598c1c29ab0d38d41d7bccc384a86c9d114d5f6398f68da275afb8a3
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2023 JHunt
3
+ Copyright (c) 2023-2024 JHunt
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # BLEST Ruby
2
2
 
3
- The Ruby reference implementation of BLEST (Batch-able, Lightweight, Encrypted State Transfer), an improved communication protocol for web APIs which leverages JSON, supports request batching and selective returns, and provides a modern alternative to REST.
3
+ The Ruby reference implementation of BLEST (Batch-able, Lightweight, Encrypted State Transfer), an improved communication protocol for web APIs which leverages JSON, supports request batching by default, and provides a modern alternative to REST.
4
4
 
5
5
  To learn more about BLEST, please visit the website: https://blest.jhunt.dev
6
6
 
@@ -10,9 +10,8 @@ For a front-end implementation in React, please visit https://github.com/jhuntde
10
10
 
11
11
  - Built on JSON - Reduce parsing time and overhead
12
12
  - Request Batching - Save bandwidth and reduce load times
13
- - Compact Payloads - Save more bandwidth
14
- - Selective Returns - Save even more bandwidth
15
- - Single Endpoint - Reduce complexity and improve data privacy
13
+ - Compact Payloads - Save even more bandwidth
14
+ - Single Endpoint - Reduce complexity and facilitate introspection
16
15
  - Fully Encrypted - Improve data privacy
17
16
 
18
17
  ## Installation
@@ -25,36 +24,6 @@ gem install blest
25
24
 
26
25
  ## Usage
27
26
 
28
- This core class of this library has an interface somewhat similar to Sinatra. It also provides a `Router` class with a `handle` method for use in an existing Ruby API and an `HttpClient` class with a `request` method for making BLEST HTTP requests.
29
-
30
- ```ruby
31
- require 'blest'
32
-
33
- app = Blest.new(timeout: 1000, port: 8080, host: 'localhost', cors: 'http://localhost:3000')
34
-
35
- # Create some middleware (optional)
36
- app.before do |params, context|
37
- if params['name'].present?
38
- context['user'] = {
39
- name: params['name']
40
- }
41
- nil
42
- else
43
- raise RuntimeError, "Unauthorized"
44
- end
45
- end
46
-
47
- # Create a route controller
48
- app.route('greet') do |params, context|
49
- {
50
- greeting: "Hi, #{context['user']['name']}!"
51
- }
52
- end
53
-
54
- # Start the server
55
- app.listen
56
- ```
57
-
58
27
  ### Router
59
28
 
60
29
  The following example uses Sinatra.
@@ -68,10 +37,10 @@ require 'blest'
68
37
  router = Router.new(timeout: 1000)
69
38
 
70
39
  # Create some middleware (optional)
71
- router.before do |params, context|
72
- if params['name'].present?
40
+ router.before do |body, context|
41
+ if context.dig('headers', 'auth') == 'myToken'?
73
42
  context['user'] = {
74
- name: params['name']
43
+ # user info for example
75
44
  }
76
45
  nil
77
46
  else
@@ -80,9 +49,9 @@ router.before do |params, context|
80
49
  end
81
50
 
82
51
  # Create a route controller
83
- router.route('greet') do |params, context|
52
+ router.route('greet') do |body, context|
84
53
  {
85
- greeting: "Hi, #{context['user']['name']}!"
54
+ greeting: "Hi, #{body['name']}!"
86
55
  }
87
56
  end
88
57
 
@@ -106,13 +75,13 @@ end
106
75
  require 'blest'
107
76
 
108
77
  # Create a client
109
- client = HttpClient.new('http://localhost:8080', max_batch_size = 25, buffer_delay = 10, headers = {
78
+ client = HttpClient.new('http://localhost:8080', max_batch_size = 25, buffer_delay = 10, http_headers = {
110
79
  'Authorization': 'Bearer token'
111
80
  })
112
81
 
113
82
  # Send a request
114
83
  begin
115
- result = client.request('greet', { 'name': 'Steve' }, ['greeting']).value
84
+ result = client.request('greet', { 'name': 'Steve' }, { 'auth': 'myToken' }).value
116
85
  # Do something with the result
117
86
  rescue => error
118
87
  # Do something in case of error
data/lib/blest.rb CHANGED
@@ -1,6 +1,4 @@
1
- require 'socket'
2
1
  require 'json'
3
- require 'date'
4
2
  require 'concurrent'
5
3
  require 'securerandom'
6
4
  require 'net/http'
@@ -50,7 +48,7 @@ class Router
50
48
  end
51
49
 
52
50
  def route(route, &handler)
53
- route_error = validate_route(route)
51
+ route_error = validate_route(route, false)
54
52
  raise ArgumentError, route_error if route_error
55
53
  raise ArgumentError, 'Route already exists' if @routes.key?(route)
56
54
  raise ArgumentError, 'Handler should be a function' unless handler.respond_to?(:call)
@@ -58,8 +56,7 @@ class Router
58
56
  @routes[route] = {
59
57
  handler: [*@middleware, handler, *@afterware],
60
58
  description: nil,
61
- parameters: nil,
62
- result: nil,
59
+ schema: nil,
63
60
  visible: @introspection,
64
61
  validate: false,
65
62
  timeout: @timeout
@@ -75,14 +72,9 @@ class Router
75
72
  @routes[route]['description'] = config['description']
76
73
  end
77
74
 
78
- if config.key?('parameters')
79
- raise ArgumentError, 'Parameters should be a dict' if !config['parameters'].nil? && !config['parameters'].is_a?(Hash)
80
- @routes[route]['parameters'] = config['parameters']
81
- end
82
-
83
- if config.key?('result')
84
- raise ArgumentError, 'Result should be a dict' if !config['result'].nil? && !config['result'].is_a?(Hash)
85
- @routes[route]['result'] = config['result']
75
+ if config.key?('schema')
76
+ raise ArgumentError, 'Schema should be a dict' if !config['schema'].nil? && !config['schema'].is_a?(Hash)
77
+ @routes[route]['schema'] = config['schema']
86
78
  end
87
79
 
88
80
  if config.key?('visible')
@@ -125,7 +117,7 @@ class Router
125
117
  def namespace(prefix, router)
126
118
  raise ArgumentError, 'Router is required' unless router.is_a?(Router)
127
119
 
128
- prefix_error = validate_route(prefix)
120
+ prefix_error = validate_route(prefix, false)
129
121
  raise ArgumentError, prefix_error if prefix_error
130
122
 
131
123
  new_routes = router.routes.keys
@@ -155,51 +147,28 @@ end
155
147
 
156
148
 
157
149
 
158
- class Blest < Router
159
- @options = nil
160
- @errorhandler = nil
161
-
162
- def initialize(options = nil)
163
- @options = options
164
- super(options)
165
- end
166
-
167
- def errorhandler(&errorhandler)
168
- @errorhandler = errorhandler
169
- puts 'The errorhandler method is not currently used'
170
- end
171
-
172
- def listen(*args)
173
- request_handler = create_request_handler(@routes)
174
- server = create_http_server(request_handler, @options)
175
- server.call(*args)
176
- end
177
- end
178
-
179
-
180
-
181
150
  class HttpClient
182
151
  attr_reader :queue, :futures
183
152
  attr_accessor :url, :max_batch_size, :buffer_delay, :headers
184
153
 
185
- def initialize(url, max_batch_size = 25, buffer_delay = 10, headers = {})
154
+ def initialize(url, max_batch_size = 25, buffer_delay = 10, http_headers = {})
186
155
  @url = url
187
156
  @max_batch_size = max_batch_size
188
157
  @buffer_delay = buffer_delay
189
- @headers = headers
158
+ @http_headers = http_headers
190
159
  @queue = Queue.new
191
160
  @futures = {}
192
161
  @lock = Mutex.new
193
162
  end
194
163
 
195
- def request(route, parameters=nil, selector=nil)
196
- uuid = SecureRandom.uuid
164
+ def request(route, body=nil, headers=nil)
165
+ uuid = SecureRandom.uuid()
197
166
  future = Concurrent::Promises.resolvable_future
198
167
  @lock.synchronize do
199
168
  @futures[uuid] = future
200
169
  end
201
170
 
202
- @queue.push({ uuid: uuid, data: [uuid, route, parameters, selector] })
171
+ @queue.push({ uuid: uuid, data: [uuid, route, body, headers] })
203
172
  process_timeout()
204
173
  future
205
174
  end
@@ -232,7 +201,7 @@ class HttpClient
232
201
  http = Net::HTTP.new(uri.host, uri.port)
233
202
  http.use_ssl = true if uri.scheme == 'https'
234
203
 
235
- request = Net::HTTP::Post.new(path, @headers.merge({ 'Accept' => 'application/json', 'Content-Type' => 'application/json' }))
204
+ request = Net::HTTP::Post.new(path, @http_headers.merge({ 'Accept' => 'application/json', 'Content-Type' => 'application/json' }))
236
205
  request.body = JSON.generate(batch.map { |item| item[:data] })
237
206
 
238
207
  http.request(request)
@@ -286,331 +255,13 @@ end
286
255
 
287
256
 
288
257
 
289
- class HttpServer
290
- attr_reader :url, :host, :port, :cors, :headers
291
-
292
- def initialize(request_handler, options = {})
293
- unless request_handler.is_a?(Router)
294
- raise ArgumentError, "request_handler must be an instance of Router class"
295
- end
296
- @request_handler = request_handler
297
- if options
298
- options_error = validate_server_options(options)
299
- raise ArgumentError, options_error if options_error
300
- else
301
- @options = {}
302
- end
303
-
304
- @url = options && get_value(options, :url) || '/'
305
- @host = options && get_value(options, :host) || 'localhost'
306
- @port = options && get_value(options, :port) || 8080
307
- @cors = options && get_value(options, :cors) || false
308
- cors_default = cors == true ? '*' : cors || ''
309
-
310
- @headers = {
311
- 'access-control-allow-origin' => options && get_value(options, :accessControlAllowOrigin) || cors_default,
312
- 'content-security-policy' => options && get_value(options, :contentSecurityPolicy) || "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests",
313
- 'cross-origin-opener-policy' => options && get_value(options, :crossOriginOpenerPolicy) || 'same-origin',
314
- 'cross-origin-resource-policy' => options && get_value(options, :crossOriginResourcePolicy) || 'same-origin',
315
- 'origin-agent-cluster' => options && get_value(options, :originAgentCluster) || '?1',
316
- 'referrer-policy' => options && get_value(options, :referrerPolicy) || 'no-referrer',
317
- 'strict-transport-security' => options && get_value(options, :strictTransportSecurity) || 'max-age=1555200 includeSubDomains',
318
- 'x-content-type-options' => options && get_value(options, :xContentTypeOptions) || 'nosniff',
319
- 'x-dns-prefetch-control' => options && get_value(options, :xDnsPrefetchOptions) || 'off',
320
- 'x-download-options' => options && get_value(options, :xDownloadOptions) || 'noopen',
321
- 'x-frame-options' => options && get_value(options, :xFrameOptions) || 'SAMEORIGIN',
322
- 'x-permitted-cross-domain-policies' => options && get_value(options, :xPermittedCrossDomainPolicies) || 'none',
323
- 'x-xss-protection' => options && get_value(options, :xXssProtection) || '0'
324
- }
325
- end
326
-
327
- def listen()
328
- server = TCPServer.new(@host, @port)
329
- puts "Server listening on port #{@port}"
330
-
331
- loop do
332
- client = server.accept
333
- Thread.new { handle_http_request(client, @request_handler, @http_headers) }
334
- end
335
- end
336
-
337
- private
338
-
339
- def parse_http_request(request_line)
340
- method, path, _ = request_line.split(' ')
341
- { method: method, path: path }
342
- end
343
-
344
- def parse_http_headers(client)
345
- headers = {}
346
-
347
- while (line = client.gets.chomp)
348
- break if line.empty?
349
-
350
- key, value = line.split(':', 2)
351
- headers[key] = value.strip
352
- end
353
-
354
- headers
355
- end
356
-
357
- def parse_post_body(client, content_length)
358
- body = ''
359
-
360
- while content_length > 0
361
- chunk = client.readpartial([content_length, 4096].min)
362
- body += chunk
363
- content_length -= chunk.length
364
- end
365
-
366
- body
367
- end
368
-
369
- def build_http_response(status, headers, body)
370
- response = "HTTP/1.1 #{status}\r\n"
371
- headers.each { |key, value| response += "#{key}: #{value}\r\n" }
372
- response += "\r\n"
373
- response += body
374
- response
375
- end
376
-
377
- def handle_http_request(client, request_handler, http_headers)
378
- request_line = client.gets
379
- return unless request_line
380
-
381
- request = parse_http_request(request_line)
382
-
383
- headers = parse_http_headers(client)
384
-
385
- if request[:path] != '/'
386
- response = build_http_response('404 Not Found', http_headers, '')
387
- client.print(response)
388
- elsif request[:method] == 'OPTIONS'
389
- response = build_http_response('204 No Content', http_headers, '')
390
- client.print(response)
391
- elsif request[:method] == 'POST'
392
-
393
- content_length = headers['Content-Length'].to_i
394
- body = parse_post_body(client, content_length)
395
-
396
- begin
397
- json_data = JSON.parse(body)
398
- context = {
399
- 'headers' => headers
400
- }
401
-
402
- response_headers = http_headers.merge({
403
- 'Content-Type' => 'application/json'
404
- })
405
-
406
- result, error = request_handler.(json_data, context)
407
-
408
- if error
409
- response_json = error.to_json
410
- response = build_http_response('500 Internal Server Error', response_headers, response_json)
411
- client.print response
412
- elsif result
413
- response_json = result.to_json
414
- response = build_http_response('200 OK', response_headers, response_json)
415
- client.print response
416
- else
417
- response = build_http_response('500 Internal Server Error', response_headers, { 'message' => 'Request handler failed to return a result' }.to_json)
418
- client.print response
419
- end
420
-
421
- rescue JSON::ParserError
422
- response = build_http_response('400 Bad Request', http_headers, '')
423
- end
424
-
425
- else
426
- response = build_http_response('405 Method Not Allowed', http_headers, '')
427
- client.print(response)
428
- end
429
- client.close()
430
- end
431
-
432
- end
433
-
434
-
435
-
436
-
437
-
438
-
439
-
440
- def create_http_server(request_handler, options = nil)
441
- if options
442
- options_error = validate_server_options(options)
443
- raise ArgumentError, options_error if options_error
444
- end
445
-
446
- url = options && get_value(options, :url) || '/'
447
- port = options && get_value(options, :port) || 8080
448
- cors = options && get_value(options, :cors) || false
449
- cors_default = cors == true ? '*' : cors || ''
450
-
451
- http_headers = {
452
- 'access-control-allow-origin' => options && get_value(options, :accessControlAllowOrigin) || cors_default,
453
- 'content-security-policy' => options && get_value(options, :contentSecurityPolicy) || "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests",
454
- 'cross-origin-opener-policy' => options && get_value(options, :crossOriginOpenerPolicy) || 'same-origin',
455
- 'cross-origin-resource-policy' => options && get_value(options, :crossOriginResourcePolicy) || 'same-origin',
456
- 'origin-agent-cluster' => options && get_value(options, :originAgentCluster) || '?1',
457
- 'referrer-policy' => options && get_value(options, :referrerPolicy) || 'no-referrer',
458
- 'strict-transport-security' => options && get_value(options, :strictTransportSecurity) || 'max-age=1555200 includeSubDomains',
459
- 'x-content-type-options' => options && get_value(options, :xContentTypeOptions) || 'nosniff',
460
- 'x-dns-prefetch-control' => options && get_value(options, :xDnsPrefetchOptions) || 'off',
461
- 'x-download-options' => options && get_value(options, :xDownloadOptions) || 'noopen',
462
- 'x-frame-options' => options && get_value(options, :xFrameOptions) || 'SAMEORIGIN',
463
- 'x-permitted-cross-domain-policies' => options && get_value(options, :xPermittedCrossDomainPolicies) || 'none',
464
- 'x-xss-protection' => options && get_value(options, :xXssProtection) || '0'
465
- }
466
-
467
- def parse_http_request(request_line)
468
- method, path, _ = request_line.split(' ')
469
- { method: method, path: path }
470
- end
471
-
472
- def parse_http_headers(client)
473
- headers = {}
474
-
475
- while (line = client.gets.chomp)
476
- break if line.empty?
477
-
478
- key, value = line.split(':', 2)
479
- headers[key] = value.strip
480
- end
481
-
482
- headers
483
- end
484
-
485
- def parse_post_body(client, content_length)
486
- body = ''
487
-
488
- while content_length > 0
489
- chunk = client.readpartial([content_length, 4096].min)
490
- body += chunk
491
- content_length -= chunk.length
492
- end
493
-
494
- body
495
- end
496
-
497
- def build_http_response(status, headers, body)
498
- response = "HTTP/1.1 #{status}\r\n"
499
- headers.each { |key, value| response += "#{key}: #{value}\r\n" }
500
- response += "\r\n"
501
- response += body
502
- response
503
- end
504
-
505
- def handle_http_request(client, request_handler, http_headers)
506
- request_line = client.gets
507
- return unless request_line
508
-
509
- request = parse_http_request(request_line)
510
-
511
- headers = parse_http_headers(client)
512
-
513
- if request[:path] != '/'
514
- response = build_http_response('404 Not Found', http_headers, '')
515
- client.print(response)
516
- elsif request[:method] == 'OPTIONS'
517
- response = build_http_response('204 No Content', http_headers, '')
518
- client.print(response)
519
- elsif request[:method] == 'POST'
520
-
521
- content_length = headers['Content-Length'].to_i
522
- body = parse_post_body(client, content_length)
523
-
524
- begin
525
- json_data = JSON.parse(body)
526
- context = {
527
- 'headers' => headers
528
- }
529
-
530
- response_headers = http_headers.merge({
531
- 'Content-Type' => 'application/json'
532
- })
533
-
534
- result, error = request_handler.(json_data, context)
535
-
536
- if error
537
- response_json = error.to_json
538
- response = build_http_response('500 Internal Server Error', response_headers, response_json)
539
- client.print response
540
- elsif result
541
- response_json = result.to_json
542
- response = build_http_response('200 OK', response_headers, response_json)
543
- client.print response
544
- else
545
- response = build_http_response('500 Internal Server Error', response_headers, { 'message' => 'Request handler failed to return a result' }.to_json)
546
- client.print response
547
- end
548
-
549
- rescue JSON::ParserError
550
- response = build_http_response('400 Bad Request', http_headers, '')
551
- end
552
-
553
- else
554
- response = build_http_response('405 Method Not Allowed', http_headers, '')
555
- client.print(response)
556
- end
557
- client.close()
558
- end
559
-
560
- run = ->() do
561
-
562
- server = TCPServer.new('localhost', port)
563
- puts "Server listening on port #{port}"
564
-
565
- begin
566
- loop do
567
- client = server.accept
568
- Thread.new { handle_http_request(client, request_handler, http_headers) }
569
- end
570
- rescue Interrupt
571
- exit 1
572
- end
573
-
574
- end
575
-
576
- return run
577
- end
578
-
579
-
580
-
581
- def validate_server_options(options)
582
- if options.nil? || options.empty?
583
- return nil
584
- elsif !options.is_a?(Hash)
585
- return 'Options should be a hash'
586
- else
587
- if options.key?(:url)
588
- if !options[:url].is_a?(String)
589
- return '"url" option should be a string'
590
- elsif !options[:url].start_with?('/')
591
- return '"url" option should begin with a forward slash'
592
- end
593
- end
594
-
595
- if options.key?(:cors)
596
- if !options[:cors].is_a?(String) && !options[:cors].is_a?(TrueClass) && !options[:cors].is_a?(FalseClass)
597
- return '"cors" option should be a string or boolean'
598
- end
599
- end
600
- end
601
-
602
- return nil
603
- end
604
-
605
-
606
-
607
258
  def create_request_handler(routes)
608
259
  raise ArgumentError, 'A routes object is required' unless routes.is_a?(Hash)
609
260
 
610
261
  my_routes = {}
611
262
 
612
263
  routes.each do |key, route|
613
- route_error = validate_route(key)
264
+ route_error = validate_route(key, false)
614
265
  raise ArgumentError, "#{route_error}: #{key}" if route_error
615
266
 
616
267
  if route.is_a?(Array)
@@ -653,16 +304,26 @@ end
653
304
 
654
305
 
655
306
 
656
- def validate_route(route)
307
+ def validate_route(route, system)
657
308
  route_regex = /^[a-zA-Z][a-zA-Z0-9_\-\/]*[a-zA-Z0-9]$/
309
+ system_route_regex = /^_[a-zA-Z][a-zA-Z0-9_\-\/]*[a-zA-Z0-9]$/
658
310
  if route.nil? || route.empty?
659
311
  return 'Route is required'
660
- elsif !(route =~ route_regex)
312
+ elsif system && !(route =~ system_route_regex)
313
+ route_length = route.length
314
+ if route_length < 3
315
+ return 'System route should be at least three characters long'
316
+ elsif route[0] != '_'
317
+ return 'System route should start with an underscore'
318
+ elsif !(route[-1] =~ /^[a-zA-Z0-9]/)
319
+ return 'System route should end with a letter or a number'
320
+ else
321
+ return 'System route should contain only letters, numbers, dashes, underscores, and forward slashes'
322
+ end
323
+ elsif !system && !(route =~ route_regex)
661
324
  route_length = route.length
662
325
  if route_length < 2
663
326
  return 'Route should be at least two characters long'
664
- elsif route[-1] == '/'
665
- return 'Route should not end in a forward slash'
666
327
  elsif !(route[0] =~ /^[a-zA-Z]/)
667
328
  return 'Route should start with a letter'
668
329
  elsif !(route[-1] =~ /^[a-zA-Z0-9]/)
@@ -692,6 +353,7 @@ def handle_request(routes, requests, context = {})
692
353
  return handle_error(400, 'Request should be an array')
693
354
  end
694
355
 
356
+ batch_id = SecureRandom.uuid()
695
357
  unique_ids = []
696
358
  promises = []
697
359
 
@@ -703,8 +365,8 @@ def handle_request(routes, requests, context = {})
703
365
 
704
366
  id = request[0] || nil
705
367
  route = request[1] || nil
706
- parameters = request[2] || nil
707
- selector = request[3] || nil
368
+ body = request[2] || nil
369
+ headers = request[3] || nil
708
370
 
709
371
  if id.nil? || !id.is_a?(String)
710
372
  return handle_error(400, 'Request item should have an ID')
@@ -714,12 +376,12 @@ def handle_request(routes, requests, context = {})
714
376
  return handle_error(400, 'Request items should have a route')
715
377
  end
716
378
 
717
- if parameters && !parameters.is_a?(Hash)
718
- return handle_error(400, 'Request item parameters should be an object')
379
+ if body && !body.is_a?(Hash)
380
+ return handle_error(400, 'Request item body should be an object')
719
381
  end
720
382
 
721
- if selector && !selector.is_a?(Array)
722
- return handle_error(400, 'Request item selector should be an array')
383
+ if headers && !headers.is_a?(Hash)
384
+ return handle_error(400, 'Request item headers should be an object')
723
385
  end
724
386
 
725
387
  if unique_ids.include?(id)
@@ -741,21 +403,20 @@ def handle_request(routes, requests, context = {})
741
403
  request_object = {
742
404
  id: id,
743
405
  route: route,
744
- parameters: parameters || {},
745
- selector: selector
406
+ body: body || {},
407
+ headers: headers
746
408
  }
747
409
 
748
- my_context = {
749
- 'requestId' => id,
750
- 'routeName' => route,
751
- 'selector' => selector,
752
- 'requestTime' => DateTime.now.to_time.to_i
753
- }
410
+ request_context = {}
754
411
  if context.is_a?(Hash)
755
- my_context = my_context.merge(context)
412
+ request_context = request_context.merge(context)
756
413
  end
414
+ request_context["batch_id"] = batch_id
415
+ request_context["request_id"] = id
416
+ request_context["route"] = route
417
+ request_context["headers"] = headers
757
418
 
758
- promises << Thread.new { route_reducer(route_handler, request_object, my_context, timeout) }
419
+ promises << Thread.new { route_reducer(route_handler, request_object, request_context, timeout) }
759
420
  end
760
421
 
761
422
  results = promises.map(&:value)
@@ -820,7 +481,7 @@ def route_reducer(handler, request, context, timeout = nil)
820
481
  if h.respond_to?(:call)
821
482
  temp_result = Concurrent::Promises.future do
822
483
  begin
823
- h.call(request[:parameters], safe_context)
484
+ h.call(request[:body], safe_context)
824
485
  rescue => e
825
486
  error = e
826
487
  end
@@ -845,7 +506,7 @@ def route_reducer(handler, request, context, timeout = nil)
845
506
  if handler.respond_to?(:call)
846
507
  my_result = Concurrent::Promises.future do
847
508
  begin
848
- handler.call(request[:parameters], safe_context)
509
+ handler.call(request[:body], safe_context)
849
510
  rescue => e
850
511
  error = e
851
512
  end
@@ -880,9 +541,9 @@ def route_reducer(handler, request, context, timeout = nil)
880
541
  return [request[:id], request[:route], nil, { 'message' => 'Internal Server Error', 'status' => 500 }]
881
542
  end
882
543
 
883
- if request[:selector]
884
- result = filter_object(result, request[:selector])
885
- end
544
+ # if request[:selector]
545
+ # result = filter_object(result, request[:selector])
546
+ # end
886
547
 
887
548
  [request[:id], request[:route], result, nil]
888
549
  rescue => error
data/spec/blest_spec.rb CHANGED
@@ -36,12 +36,13 @@ RSpec.describe Router do
36
36
  error6 = nil
37
37
 
38
38
  before(:all) do
39
- router.route('basicRoute') do |parameters, context|
40
- { 'route'=> 'basicRoute', 'parameters' => parameters, 'context' => context }
39
+ router.route('basicRoute') do |body, context|
40
+ { 'route'=> 'basicRoute', 'body' => body, 'context' => context }
41
41
  end
42
42
 
43
- router.before do |parameters, context|
44
- context['test'] = { 'value' => parameters['testValue'] }
43
+ router.before do |body, context|
44
+ context['test'] = { 'value' => body['testValue'] }
45
+ context['requestTime'] = Time.now
45
46
  nil
46
47
  end
47
48
 
@@ -52,20 +53,20 @@ RSpec.describe Router do
52
53
  nil
53
54
  end
54
55
 
55
- router2.route('mergedRoute') do |parameters, context|
56
- { 'route' => 'mergedRoute', 'parameters' => parameters, 'context' => context }
56
+ router2.route('mergedRoute') do |body, context|
57
+ { 'route' => 'mergedRoute', 'body' => body, 'context' => context }
57
58
  end
58
59
 
59
- router2.route('timeoutRoute') do |parameters|
60
+ router2.route('timeoutRoute') do |body|
60
61
  sleep(0.2)
61
- { 'testValue' => parameters['testValue'] }
62
+ { 'testValue' => body['testValue'] }
62
63
  end
63
64
 
64
65
  router.merge(router2)
65
66
 
66
- router3.route('errorRoute') do |parameters|
67
- error = BlestError.new(parameters['testValue'])
68
- error.code = "ERROR_#{(parameters['testValue'].to_f * 10).round}"
67
+ router3.route('errorRoute') do |body|
68
+ error = BlestError.new(body['testValue'])
69
+ error.code = "ERROR_#{(body['testValue'].to_f * 10).round}"
69
70
  raise error
70
71
  end
71
72
 
@@ -140,9 +141,9 @@ RSpec.describe Router do
140
141
  expect(result5[0][1]).to eq('timeoutRoute')
141
142
  end
142
143
 
143
- it 'should accept parameters' do
144
- expect(result1[0][2]['parameters']['testValue']).to eq(testValue1)
145
- expect(result2[0][2]['parameters']['testValue']).to eq(testValue2)
144
+ it 'should accept body' do
145
+ expect(result1[0][2]['body']['testValue']).to eq(testValue1)
146
+ expect(result2[0][2]['body']['testValue']).to eq(testValue2)
146
147
  end
147
148
 
148
149
  it 'should respect context' do
metadata CHANGED
@@ -1,21 +1,21 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - JHunt
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-01 00:00:00.000000000 Z
11
+ date: 2024-10-31 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: The Ruby reference implementation of BLEST (Batch-able, Lightweight,
14
14
  Encrypted State Transfer), an improved communication protocol for web APIs which
15
- leverages JSON, supports request batching and selective returns, and provides a
16
- modern alternative to REST.
15
+ leverages JSON, supports request batching by default, and provides a modern alternative
16
+ to REST.
17
17
  email:
18
- - blest@jhunt.dev
18
+ - hello@jhunt.dev
19
19
  executables: []
20
20
  extensions: []
21
21
  extra_rdoc_files: []
@@ -28,7 +28,7 @@ homepage: https://blest.jhunt.dev
28
28
  licenses:
29
29
  - MIT
30
30
  metadata: {}
31
- post_install_message:
31
+ post_install_message:
32
32
  rdoc_options: []
33
33
  require_paths:
34
34
  - lib
@@ -43,8 +43,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
43
43
  - !ruby/object:Gem::Version
44
44
  version: '0'
45
45
  requirements: []
46
- rubygems_version: 3.0.3
47
- signing_key:
46
+ rubygems_version: 3.5.16
47
+ signing_key:
48
48
  specification_version: 4
49
49
  summary: The Ruby reference implementation of BLEST
50
50
  test_files: []