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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -16
  3. data/lib/blest.rb +48 -45
  4. data/spec/blest_spec.rb +15 -14
  5. 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: c4f44d941bb783457e67fa52ada69792f5f645a76bac399165d59c73f4ba7b4b
4
+ data.tar.gz: 8f7ee45d396ae4cab3c8178f1bef7e6605612efb94e259cbb35bcd3da842e347
5
5
  SHA512:
6
- metadata.gz: 1f0b5c80b2ae39259d5386c60a7a27deb6b04dc35a5162c30be64056d10581de3b6a6ee47cd800e27f4e4cac1f52a6cb692b47abbda7533ed48dc85c3eafe403
7
- data.tar.gz: 964ab1acc3ca2db12a2918bb141efa3f254438c095b47c7e81313c4b5140d7a10b13712b3a4773e6fa1b76158ccc75dffa2af591df45fdb55a767278ea72e470
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 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,7 +24,7 @@ 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.
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 |params, context|
37
- if params['name'].present?
35
+ app.before do |body, context|
36
+ if context.dig('headers', 'auth') == 'myToken'?
38
37
  context['user'] = {
39
- name: params['name']
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 |params, context|
47
+ app.route('greet') do |body, context|
49
48
  {
50
- greeting: "Hi, #{context['user']['name']}!"
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 |params, context|
72
- if params['name'].present?
70
+ router.before do |body, context|
71
+ if context.dig('headers', 'auth') == 'myToken'?
73
72
  context['user'] = {
74
- name: params['name']
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 |params, context|
82
+ router.route('greet') do |body, context|
84
83
  {
85
- greeting: "Hi, #{context['user']['name']}!"
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, headers = {
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
- parameters: nil,
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?('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']
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, headers = {})
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
- @headers = headers
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, parameters=nil, selector=nil)
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, parameters, selector] })
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, @headers.merge({ 'Accept' => 'application/json', 'Content-Type' => 'application/json' }))
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 =~ route_regex)
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
- parameters = request[2] || nil
707
- selector = request[3] || nil
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 parameters && !parameters.is_a?(Hash)
718
- return handle_error(400, 'Request item parameters should be an object')
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 selector && !selector.is_a?(Array)
722
- return handle_error(400, 'Request item selector should be an array')
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
- parameters: parameters || {},
745
- selector: selector
748
+ body: body || {},
749
+ headers: headers
746
750
  }
747
751
 
748
- my_context = {
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
- my_context = my_context.merge(context)
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, my_context, timeout) }
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[:parameters], safe_context)
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[:parameters], safe_context)
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
- result = filter_object(result, request[:selector])
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 |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.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: 2023-08-01 00:00:00.000000000 Z
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 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: []