blest 0.1.0 → 1.0.1

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 (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: []