blest 0.1.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +10 -41
- data/lib/blest.rb +48 -387
- data/spec/blest_spec.rb +15 -14
- metadata +9 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dda8d494a672ecd29209b9971076decfaec476253157103dc75a6d74cfc2f327
|
4
|
+
data.tar.gz: dee130556eb32a00cdbb14ae3f5c160133241763061d6558592d1712963cbfc4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4407b468d713463014fa4449efb4e2be40ac59d93fbca2302d1ddc13aa036907b0ba6b4067cd92e1e90a64e37dcca3cd29b0aec605722fd7a5edf4458058245f
|
7
|
+
data.tar.gz: f1c050cdaa44297e90c968ba99be88f3fb440743a959e9ab32286d04ce42a4dc886059bb598c1c29ab0d38d41d7bccc384a86c9d114d5f6398f68da275afb8a3
|
data/LICENSE
CHANGED
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
|
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
|
-
-
|
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 |
|
72
|
-
if
|
40
|
+
router.before do |body, context|
|
41
|
+
if context.dig('headers', 'auth') == 'myToken'?
|
73
42
|
context['user'] = {
|
74
|
-
|
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 |
|
52
|
+
router.route('greet') do |body, context|
|
84
53
|
{
|
85
|
-
greeting: "Hi, #{
|
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,
|
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' },
|
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
|
-
|
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?('
|
79
|
-
raise ArgumentError, '
|
80
|
-
@routes[route]['
|
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,
|
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
|
-
@
|
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,
|
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,
|
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, @
|
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 =~
|
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
|
-
|
707
|
-
|
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
|
718
|
-
return handle_error(400, 'Request item
|
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
|
722
|
-
return handle_error(400, 'Request item
|
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
|
-
|
745
|
-
|
406
|
+
body: body || {},
|
407
|
+
headers: headers
|
746
408
|
}
|
747
409
|
|
748
|
-
|
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
|
-
|
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,
|
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[:
|
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[:
|
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
|
-
|
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 |
|
40
|
-
{ 'route'=> 'basicRoute', '
|
39
|
+
router.route('basicRoute') do |body, context|
|
40
|
+
{ 'route'=> 'basicRoute', 'body' => body, 'context' => context }
|
41
41
|
end
|
42
42
|
|
43
|
-
router.before do |
|
44
|
-
context['test'] = { 'value' =>
|
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 |
|
56
|
-
{ 'route' => 'mergedRoute', '
|
56
|
+
router2.route('mergedRoute') do |body, context|
|
57
|
+
{ 'route' => 'mergedRoute', 'body' => body, 'context' => context }
|
57
58
|
end
|
58
59
|
|
59
|
-
router2.route('timeoutRoute') do |
|
60
|
+
router2.route('timeoutRoute') do |body|
|
60
61
|
sleep(0.2)
|
61
|
-
{ 'testValue' =>
|
62
|
+
{ 'testValue' => body['testValue'] }
|
62
63
|
end
|
63
64
|
|
64
65
|
router.merge(router2)
|
65
66
|
|
66
|
-
router3.route('errorRoute') do |
|
67
|
-
error = BlestError.new(
|
68
|
-
error.code = "ERROR_#{(
|
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
|
144
|
-
expect(result1[0][2]['
|
145
|
-
expect(result2[0][2]['
|
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
|
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:
|
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
|
16
|
-
|
15
|
+
leverages JSON, supports request batching by default, and provides a modern alternative
|
16
|
+
to REST.
|
17
17
|
email:
|
18
|
-
-
|
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.
|
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: []
|