blest 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +15 -16
- data/lib/blest.rb +48 -45
- 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: c4f44d941bb783457e67fa52ada69792f5f645a76bac399165d59c73f4ba7b4b
|
4
|
+
data.tar.gz: 8f7ee45d396ae4cab3c8178f1bef7e6605612efb94e259cbb35bcd3da842e347
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aecfa3005a9a29bcfd4b4f501710a0f8a49251ff7fc62f4043bfe8e5b0e7f6c8e2ddbb09df2ae14c08bc7617a09ab08d84ed52d9023272bd9bb05dabd1fb8e41
|
7
|
+
data.tar.gz: 2855b895975b23f418e0e084ecd3d15bd8df43764a896aca1f8fa5788d21e74ab7fb5ff5fdc4bbcd79a074dff7ce2f14bb3a10ee4e497d8ef1d63b1a645066c3
|
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,7 +24,7 @@ gem install blest
|
|
25
24
|
|
26
25
|
## Usage
|
27
26
|
|
28
|
-
|
27
|
+
The `Blest` class of this library has an interface 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
28
|
|
30
29
|
```ruby
|
31
30
|
require 'blest'
|
@@ -33,10 +32,10 @@ require 'blest'
|
|
33
32
|
app = Blest.new(timeout: 1000, port: 8080, host: 'localhost', cors: 'http://localhost:3000')
|
34
33
|
|
35
34
|
# Create some middleware (optional)
|
36
|
-
app.before do |
|
37
|
-
if
|
35
|
+
app.before do |body, context|
|
36
|
+
if context.dig('headers', 'auth') == 'myToken'?
|
38
37
|
context['user'] = {
|
39
|
-
|
38
|
+
# user info for example
|
40
39
|
}
|
41
40
|
nil
|
42
41
|
else
|
@@ -45,9 +44,9 @@ app.before do |params, context|
|
|
45
44
|
end
|
46
45
|
|
47
46
|
# Create a route controller
|
48
|
-
app.route('greet') do |
|
47
|
+
app.route('greet') do |body, context|
|
49
48
|
{
|
50
|
-
greeting: "Hi, #{
|
49
|
+
greeting: "Hi, #{body['name']}!"
|
51
50
|
}
|
52
51
|
end
|
53
52
|
|
@@ -68,10 +67,10 @@ require 'blest'
|
|
68
67
|
router = Router.new(timeout: 1000)
|
69
68
|
|
70
69
|
# Create some middleware (optional)
|
71
|
-
router.before do |
|
72
|
-
if
|
70
|
+
router.before do |body, context|
|
71
|
+
if context.dig('headers', 'auth') == 'myToken'?
|
73
72
|
context['user'] = {
|
74
|
-
|
73
|
+
# user info for example
|
75
74
|
}
|
76
75
|
nil
|
77
76
|
else
|
@@ -80,9 +79,9 @@ router.before do |params, context|
|
|
80
79
|
end
|
81
80
|
|
82
81
|
# Create a route controller
|
83
|
-
router.route('greet') do |
|
82
|
+
router.route('greet') do |body, context|
|
84
83
|
{
|
85
|
-
greeting: "Hi, #{
|
84
|
+
greeting: "Hi, #{body['name']}!"
|
86
85
|
}
|
87
86
|
end
|
88
87
|
|
@@ -106,7 +105,7 @@ end
|
|
106
105
|
require 'blest'
|
107
106
|
|
108
107
|
# Create a client
|
109
|
-
client = HttpClient.new('http://localhost:8080', max_batch_size = 25, buffer_delay = 10,
|
108
|
+
client = HttpClient.new('http://localhost:8080', max_batch_size = 25, buffer_delay = 10, http_headers = {
|
110
109
|
'Authorization': 'Bearer token'
|
111
110
|
})
|
112
111
|
|
data/lib/blest.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'socket'
|
2
2
|
require 'json'
|
3
|
-
require 'date'
|
4
3
|
require 'concurrent'
|
5
4
|
require 'securerandom'
|
6
5
|
require 'net/http'
|
@@ -50,7 +49,7 @@ class Router
|
|
50
49
|
end
|
51
50
|
|
52
51
|
def route(route, &handler)
|
53
|
-
route_error = validate_route(route)
|
52
|
+
route_error = validate_route(route, false)
|
54
53
|
raise ArgumentError, route_error if route_error
|
55
54
|
raise ArgumentError, 'Route already exists' if @routes.key?(route)
|
56
55
|
raise ArgumentError, 'Handler should be a function' unless handler.respond_to?(:call)
|
@@ -58,8 +57,7 @@ class Router
|
|
58
57
|
@routes[route] = {
|
59
58
|
handler: [*@middleware, handler, *@afterware],
|
60
59
|
description: nil,
|
61
|
-
|
62
|
-
result: nil,
|
60
|
+
schema: nil,
|
63
61
|
visible: @introspection,
|
64
62
|
validate: false,
|
65
63
|
timeout: @timeout
|
@@ -75,14 +73,9 @@ class Router
|
|
75
73
|
@routes[route]['description'] = config['description']
|
76
74
|
end
|
77
75
|
|
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']
|
76
|
+
if config.key?('schema')
|
77
|
+
raise ArgumentError, 'Schema should be a dict' if !config['schema'].nil? && !config['schema'].is_a?(Hash)
|
78
|
+
@routes[route]['schema'] = config['schema']
|
86
79
|
end
|
87
80
|
|
88
81
|
if config.key?('visible')
|
@@ -125,7 +118,7 @@ class Router
|
|
125
118
|
def namespace(prefix, router)
|
126
119
|
raise ArgumentError, 'Router is required' unless router.is_a?(Router)
|
127
120
|
|
128
|
-
prefix_error = validate_route(prefix)
|
121
|
+
prefix_error = validate_route(prefix, false)
|
129
122
|
raise ArgumentError, prefix_error if prefix_error
|
130
123
|
|
131
124
|
new_routes = router.routes.keys
|
@@ -182,24 +175,24 @@ class HttpClient
|
|
182
175
|
attr_reader :queue, :futures
|
183
176
|
attr_accessor :url, :max_batch_size, :buffer_delay, :headers
|
184
177
|
|
185
|
-
def initialize(url, max_batch_size = 25, buffer_delay = 10,
|
178
|
+
def initialize(url, max_batch_size = 25, buffer_delay = 10, http_headers = {})
|
186
179
|
@url = url
|
187
180
|
@max_batch_size = max_batch_size
|
188
181
|
@buffer_delay = buffer_delay
|
189
|
-
@
|
182
|
+
@http_headers = http_headers
|
190
183
|
@queue = Queue.new
|
191
184
|
@futures = {}
|
192
185
|
@lock = Mutex.new
|
193
186
|
end
|
194
187
|
|
195
|
-
def request(route,
|
196
|
-
uuid = SecureRandom.uuid
|
188
|
+
def request(route, body=nil, headers=nil)
|
189
|
+
uuid = SecureRandom.uuid()
|
197
190
|
future = Concurrent::Promises.resolvable_future
|
198
191
|
@lock.synchronize do
|
199
192
|
@futures[uuid] = future
|
200
193
|
end
|
201
194
|
|
202
|
-
@queue.push({ uuid: uuid, data: [uuid, route,
|
195
|
+
@queue.push({ uuid: uuid, data: [uuid, route, body, headers] })
|
203
196
|
process_timeout()
|
204
197
|
future
|
205
198
|
end
|
@@ -232,7 +225,7 @@ class HttpClient
|
|
232
225
|
http = Net::HTTP.new(uri.host, uri.port)
|
233
226
|
http.use_ssl = true if uri.scheme == 'https'
|
234
227
|
|
235
|
-
request = Net::HTTP::Post.new(path, @
|
228
|
+
request = Net::HTTP::Post.new(path, @http_headers.merge({ 'Accept' => 'application/json', 'Content-Type' => 'application/json' }))
|
236
229
|
request.body = JSON.generate(batch.map { |item| item[:data] })
|
237
230
|
|
238
231
|
http.request(request)
|
@@ -610,7 +603,7 @@ def create_request_handler(routes)
|
|
610
603
|
my_routes = {}
|
611
604
|
|
612
605
|
routes.each do |key, route|
|
613
|
-
route_error = validate_route(key)
|
606
|
+
route_error = validate_route(key, false)
|
614
607
|
raise ArgumentError, "#{route_error}: #{key}" if route_error
|
615
608
|
|
616
609
|
if route.is_a?(Array)
|
@@ -653,16 +646,26 @@ end
|
|
653
646
|
|
654
647
|
|
655
648
|
|
656
|
-
def validate_route(route)
|
649
|
+
def validate_route(route, system)
|
657
650
|
route_regex = /^[a-zA-Z][a-zA-Z0-9_\-\/]*[a-zA-Z0-9]$/
|
651
|
+
system_route_regex = /^_[a-zA-Z][a-zA-Z0-9_\-\/]*[a-zA-Z0-9]$/
|
658
652
|
if route.nil? || route.empty?
|
659
653
|
return 'Route is required'
|
660
|
-
elsif !(route =~
|
654
|
+
elsif system && !(route =~ system_route_regex)
|
655
|
+
route_length = route.length
|
656
|
+
if route_length < 3
|
657
|
+
return 'System route should be at least three characters long'
|
658
|
+
elsif route[0] != '_'
|
659
|
+
return 'System route should start with an underscore'
|
660
|
+
elsif !(route[-1] =~ /^[a-zA-Z0-9]/)
|
661
|
+
return 'System route should end with a letter or a number'
|
662
|
+
else
|
663
|
+
return 'System route should contain only letters, numbers, dashes, underscores, and forward slashes'
|
664
|
+
end
|
665
|
+
elsif !system && !(route =~ route_regex)
|
661
666
|
route_length = route.length
|
662
667
|
if route_length < 2
|
663
668
|
return 'Route should be at least two characters long'
|
664
|
-
elsif route[-1] == '/'
|
665
|
-
return 'Route should not end in a forward slash'
|
666
669
|
elsif !(route[0] =~ /^[a-zA-Z]/)
|
667
670
|
return 'Route should start with a letter'
|
668
671
|
elsif !(route[-1] =~ /^[a-zA-Z0-9]/)
|
@@ -692,6 +695,7 @@ def handle_request(routes, requests, context = {})
|
|
692
695
|
return handle_error(400, 'Request should be an array')
|
693
696
|
end
|
694
697
|
|
698
|
+
batch_id = SecureRandom.uuid()
|
695
699
|
unique_ids = []
|
696
700
|
promises = []
|
697
701
|
|
@@ -703,8 +707,8 @@ def handle_request(routes, requests, context = {})
|
|
703
707
|
|
704
708
|
id = request[0] || nil
|
705
709
|
route = request[1] || nil
|
706
|
-
|
707
|
-
|
710
|
+
body = request[2] || nil
|
711
|
+
headers = request[3] || nil
|
708
712
|
|
709
713
|
if id.nil? || !id.is_a?(String)
|
710
714
|
return handle_error(400, 'Request item should have an ID')
|
@@ -714,12 +718,12 @@ def handle_request(routes, requests, context = {})
|
|
714
718
|
return handle_error(400, 'Request items should have a route')
|
715
719
|
end
|
716
720
|
|
717
|
-
if
|
718
|
-
return handle_error(400, 'Request item
|
721
|
+
if body && !body.is_a?(Hash)
|
722
|
+
return handle_error(400, 'Request item body should be an object')
|
719
723
|
end
|
720
724
|
|
721
|
-
if
|
722
|
-
return handle_error(400, 'Request item
|
725
|
+
if headers && !headers.is_a?(Hash)
|
726
|
+
return handle_error(400, 'Request item headers should be an object')
|
723
727
|
end
|
724
728
|
|
725
729
|
if unique_ids.include?(id)
|
@@ -741,21 +745,20 @@ def handle_request(routes, requests, context = {})
|
|
741
745
|
request_object = {
|
742
746
|
id: id,
|
743
747
|
route: route,
|
744
|
-
|
745
|
-
|
748
|
+
body: body || {},
|
749
|
+
headers: headers
|
746
750
|
}
|
747
751
|
|
748
|
-
|
749
|
-
'requestId' => id,
|
750
|
-
'routeName' => route,
|
751
|
-
'selector' => selector,
|
752
|
-
'requestTime' => DateTime.now.to_time.to_i
|
753
|
-
}
|
752
|
+
request_context = {}
|
754
753
|
if context.is_a?(Hash)
|
755
|
-
|
754
|
+
request_context = request_context.merge(context)
|
756
755
|
end
|
756
|
+
request_context["batch_id"] = batch_id
|
757
|
+
request_context["request_id"] = id
|
758
|
+
request_context["route"] = route
|
759
|
+
request_context["headers"] = headers
|
757
760
|
|
758
|
-
promises << Thread.new { route_reducer(route_handler, request_object,
|
761
|
+
promises << Thread.new { route_reducer(route_handler, request_object, request_context, timeout) }
|
759
762
|
end
|
760
763
|
|
761
764
|
results = promises.map(&:value)
|
@@ -820,7 +823,7 @@ def route_reducer(handler, request, context, timeout = nil)
|
|
820
823
|
if h.respond_to?(:call)
|
821
824
|
temp_result = Concurrent::Promises.future do
|
822
825
|
begin
|
823
|
-
h.call(request[:
|
826
|
+
h.call(request[:body], safe_context)
|
824
827
|
rescue => e
|
825
828
|
error = e
|
826
829
|
end
|
@@ -845,7 +848,7 @@ def route_reducer(handler, request, context, timeout = nil)
|
|
845
848
|
if handler.respond_to?(:call)
|
846
849
|
my_result = Concurrent::Promises.future do
|
847
850
|
begin
|
848
|
-
handler.call(request[:
|
851
|
+
handler.call(request[:body], safe_context)
|
849
852
|
rescue => e
|
850
853
|
error = e
|
851
854
|
end
|
@@ -880,9 +883,9 @@ def route_reducer(handler, request, context, timeout = nil)
|
|
880
883
|
return [request[:id], request[:route], nil, { 'message' => 'Internal Server Error', 'status' => 500 }]
|
881
884
|
end
|
882
885
|
|
883
|
-
if request[:selector]
|
884
|
-
|
885
|
-
end
|
886
|
+
# if request[:selector]
|
887
|
+
# result = filter_object(result, request[:selector])
|
888
|
+
# end
|
886
889
|
|
887
890
|
[request[:id], request[:route], result, nil]
|
888
891
|
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:
|
4
|
+
version: 1.0.0
|
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-28 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: []
|