blest 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +172 -0
  4. data/lib/blest.rb +281 -0
  5. metadata +49 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e0a8f112cb397ac2725175231766a0ca838e9ce898180cfc8be6e59e7fa498b8
4
+ data.tar.gz: b9f91cdfb82e5058d889fd6abddc8f14248c567567a3cb0acbd673d04b99f84c
5
+ SHA512:
6
+ metadata.gz: 7fc3d8ae042c5b9dc96cb9fda9edeada1e34c26d1d08e890e72359c74f675e7c874b3c71ec78e4c056a8aaa886081be3374d87a46e22f86fd8689019149a9c8f
7
+ data.tar.gz: a862d3210e033fdf7b9d71b6b65b710a169955b49804df64aacfe0aacc73486f28e42a5dbf37994e66fa7a66ade5a39bbb2ecb5984941f5cf2c7284cc87308a8
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 JHunt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # BLEST Ruby
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.
4
+
5
+ To learn more about BLEST, please refer to the white paper: https://jhunt.dev/BLEST%20White%20Paper.pdf
6
+
7
+ For a front-end implementation in React, please visit https://github.com/jhuntdev/blest-react
8
+
9
+ ## Features
10
+
11
+ - Built on JSON - Reduce parsing time and overhead
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
16
+ - Fully Encrypted - Improve data privacy
17
+
18
+ <!-- ## Installation
19
+
20
+ Install BLEST Python from PyPI.
21
+
22
+ ```bash
23
+ python3 -m pip install blest
24
+ ``` -->
25
+
26
+ ## Usage
27
+
28
+ Use the `create_request_handler` function to create a request handler suitable for use in an existing Python application. Use the `create_http_server` function to create a standalone HTTP server for your request handler.
29
+
30
+ <!-- Use the `create_http_client` function to create a BLEST HTTP client. -->
31
+
32
+ ### create_request_handler
33
+
34
+ ```ruby
35
+ require 'socket'
36
+ require 'json'
37
+ require './blest.rb'
38
+
39
+ server = TCPServer.new('localhost', 8080)
40
+
41
+ # Create some middleware (optional)
42
+ auth_middleware = ->(params, context) {
43
+ if params[:name].present?
44
+ context[:user] = {
45
+ name: params[:name]
46
+ }
47
+ nil
48
+ else
49
+ raise RuntimeError, "Unauthorized"
50
+ end
51
+ }
52
+
53
+ # Create a route controller
54
+ greet_controller = ->(params, context) {
55
+ {
56
+ greeting: "Hi, #{context[:user][:name]}!"
57
+ }
58
+ }
59
+
60
+ # Create a router
61
+ router = {
62
+ greet: [auth_middleware, greet_controller]
63
+ }
64
+
65
+ # Create a request handler
66
+ handler = create_request_handler(router)
67
+
68
+ puts "Server listening on port 8080"
69
+
70
+ loop do
71
+
72
+ client = server.accept
73
+
74
+ request = client.gets
75
+ if request.nil?
76
+ client.close
77
+ else
78
+
79
+ method, path, _ = request.split(' ')
80
+
81
+ if method != 'POST'
82
+ client.puts "HTTP/1.1 405 Method Not Allowed"
83
+ client.puts "\r\n"
84
+ elsif path != '/'
85
+ client.puts "HTTP/1.1 404 Not Found"
86
+ client.puts "\r\n"
87
+ else
88
+ content_length = 0
89
+ while line = client.gets
90
+ break if line == "\r\n"
91
+ content_length = line.split(': ')[1].to_i if line.start_with?('Content-Length')
92
+ end
93
+
94
+ body = client.read(content_length)
95
+ data = JSON.parse(body)
96
+
97
+ context = {
98
+ headers: request.headers
99
+ }
100
+
101
+ # Use the request handler]
102
+ result, error = handler.(data, context)
103
+
104
+ if error
105
+ response = error.to_json
106
+ client.puts "HTTP/1.1 500 Internal Server Error"
107
+ client.puts "Content-Type: application/json"
108
+ client.puts "Content-Length: #{response.bytesize}"
109
+ client.puts "\r\n"
110
+ client.puts response
111
+ elsif result
112
+ response = result.to_json
113
+ client.puts "HTTP/1.1 200 OK"
114
+ client.puts "Content-Type: application/json"
115
+ client.puts "Content-Length: #{response.bytesize}"
116
+ client.puts "\r\n"
117
+ client.puts response
118
+ else
119
+ client.puts "HTTP/1.1 204 No Content"
120
+ end
121
+ end
122
+
123
+ client.close
124
+
125
+ end
126
+ end
127
+ ```
128
+
129
+ ### create_http_server
130
+
131
+ ```ruby
132
+ require 'socket'
133
+ require 'json'
134
+ require './blest.rb'
135
+
136
+ # Create some middleware (optional)
137
+ auth_middleware = ->(params, context) {
138
+ if params[:name].present?
139
+ context[:user] = {
140
+ name: params[:name]
141
+ }
142
+ nil
143
+ else
144
+ raise RuntimeError, "Unauthorized"
145
+ end
146
+ }
147
+
148
+ # Create a route controller
149
+ greet_controller = ->(params, context) {
150
+ {
151
+ greeting: "Hi, #{context[:user][:name]}!"
152
+ }
153
+ }
154
+
155
+ # Create a router
156
+ router = {
157
+ greet: [auth_middleware, greet_controller]
158
+ }
159
+
160
+ # Create a request handler
161
+ handler = create_request_handler(router)
162
+
163
+ # Create the server
164
+ server = create_http_server(handler, { port: 8080 })
165
+
166
+ # Run the server
167
+ server.()
168
+ ```
169
+
170
+ ## License
171
+
172
+ This project is licensed under the [MIT License](LICENSE).
data/lib/blest.rb ADDED
@@ -0,0 +1,281 @@
1
+ require 'socket'
2
+ require 'json'
3
+
4
+ def create_http_server(request_handler, options = nil)
5
+
6
+ def parse_request(request_line)
7
+ method, path, _ = request_line.split(' ')
8
+ { method: method, path: path }
9
+ end
10
+
11
+ def parse_headers(client)
12
+ headers = {}
13
+
14
+ while (line = client.gets.chomp)
15
+ break if line.empty?
16
+
17
+ key, value = line.split(':', 2)
18
+ headers[key] = value.strip
19
+ end
20
+
21
+ headers
22
+ end
23
+
24
+ def parse_body(client, content_length)
25
+ body = ''
26
+
27
+ while content_length > 0
28
+ chunk = client.readpartial([content_length, 4096].min)
29
+ body += chunk
30
+ content_length -= chunk.length
31
+ end
32
+
33
+ body
34
+ end
35
+
36
+ def build_response(status, headers, body)
37
+ response = "HTTP/1.1 #{status}\r\n"
38
+ headers.each { |key, value| response += "#{key}: #{value}\r\n" }
39
+ response += "\r\n"
40
+ response += body
41
+ response
42
+ end
43
+
44
+ def handle_request(client, request_handler)
45
+ request_line = client.gets
46
+ return unless request_line
47
+
48
+ request = parse_request(request_line)
49
+
50
+ headers = parse_headers(client)
51
+
52
+ cors_headers = {
53
+ 'Access-Control-Allow-Origin' => '*',
54
+ 'Access-Control-Allow-Methods' => 'POST, OPTIONS',
55
+ 'Access-Control-Allow-Headers' => 'Content-Type'
56
+ }
57
+
58
+ if request[:path] != '/'
59
+ response = build_response('404 Not Found', cors_headers, '')
60
+ client.print(response)
61
+ elsif request[:method] == 'OPTIONS'
62
+ response = build_response('204 No Content', cors_headers, '')
63
+ client.print(response)
64
+ elsif request[:method] == 'POST'
65
+
66
+ content_length = headers['Content-Length'].to_i
67
+ body = parse_body(client, content_length)
68
+
69
+ begin
70
+ json_data = JSON.parse(body)
71
+ context = {
72
+ 'headers' => headers
73
+ }
74
+
75
+ response_headers = cors_headers.merge({
76
+ 'Content-Type' => 'application/json'
77
+ })
78
+
79
+ result, error = request_handler.(json_data, context)
80
+
81
+ if error
82
+ response_json = error.to_json
83
+ response = build_response('500 Internal Server Error', response_headers, response_json)
84
+ client.print response
85
+ elsif result
86
+ response_json = result.to_json
87
+ response = build_response('200 OK', response_headers, response_json)
88
+ client.print response
89
+ else
90
+ response = build_response('500 Internal Server Error', response_headers, { 'message' => 'Request handler failed to return a result' }.to_json)
91
+ client.print response
92
+ end
93
+
94
+ rescue JSON::ParserError
95
+ response = build_response('400 Bad Request', cors_headers, '')
96
+ end
97
+
98
+ else
99
+ response = build_response('405 Method Not Allowed', cors_headers, '')
100
+ client.print(response)
101
+ end
102
+ client.close()
103
+ end
104
+
105
+ run = ->() do
106
+
107
+ port = options&.fetch(:port, 8080) if options.is_a?(Hash)
108
+ port ||= 8080
109
+
110
+ server = TCPServer.new('localhost', 8080)
111
+ puts "Server listening on port #{port}"
112
+
113
+ loop do
114
+ client = server.accept
115
+ Thread.new { handle_request(client, request_handler) }
116
+ end
117
+
118
+ end
119
+
120
+ return run
121
+ end
122
+
123
+ def create_request_handler(routes, options = nil)
124
+ if options
125
+ puts 'The "options" argument is not yet used, but may be used in the future'
126
+ end
127
+
128
+ def handle_result(result)
129
+ return [result, nil]
130
+ end
131
+
132
+ def handle_error(code, message)
133
+ return [nil, { 'code' => code, 'message' => message }]
134
+ end
135
+
136
+ def route_not_found(_, _)
137
+ raise 'Route not found'
138
+ end
139
+
140
+ def route_reducer(handler, request, context = nil)
141
+ safe_context = context ? context.clone : {}
142
+ result = nil
143
+
144
+ if handler.is_a?(Array)
145
+ handler.each_with_index do |func, i|
146
+ temp_result = func.call(request[:parameters], safe_context)
147
+
148
+ if i == handler.length - 1
149
+ result = temp_result
150
+ else
151
+ raise 'Middleware should not return anything but may mutate context' if temp_result
152
+ end
153
+ end
154
+ else
155
+ result = handler.call(request[:parameters], safe_context)
156
+ end
157
+
158
+ raise 'The result, if any, should be a JSON object' if result && !(result.is_a?(Hash))
159
+
160
+ result = filter_object(result, request[:selector]) if result && request[:selector]
161
+ return [request[:id], request[:route], result, nil]
162
+ rescue StandardError => error
163
+ return [request[:id], request[:route], nil, { message: error.message }]
164
+ end
165
+
166
+ def filter_object(obj, arr)
167
+ if arr.is_a?(Array)
168
+ filtered_obj = {}
169
+ arr.each do |key|
170
+ if key.is_a?(String)
171
+ if obj.key?(key.to_sym)
172
+ filtered_obj[key.to_sym] = obj[key.to_sym]
173
+ end
174
+ elsif key.is_a?(Array)
175
+ nested_obj = obj[key[0].to_sym]
176
+ nested_arr = key[1]
177
+ if nested_obj.is_a?(Array)
178
+ filtered_arr = []
179
+ nested_obj.each do |nested_item|
180
+ filtered_nested_obj = filter_object(nested_item, nested_arr)
181
+ if filtered_nested_obj.keys.length > 0
182
+ filtered_arr << filtered_nested_obj
183
+ end
184
+ end
185
+ if filtered_arr.length > 0
186
+ filtered_obj[key[0].to_sym] = filtered_arr
187
+ end
188
+ elsif nested_obj.is_a?(Hash)
189
+ filtered_nested_obj = filter_object(nested_obj, nested_arr)
190
+ if filtered_nested_obj.keys.length > 0
191
+ filtered_obj[key[0].to_sym] = filtered_nested_obj
192
+ end
193
+ end
194
+ end
195
+ end
196
+ return filtered_obj
197
+ end
198
+ return obj
199
+ end
200
+
201
+ route_regex = /^[a-zA-Z][a-zA-Z0-9_\-\/]*[a-zA-Z0-9_\-]$/
202
+
203
+ handler = ->(requests, context = {}) do
204
+ if !requests || !requests.is_a?(Array)
205
+ return handle_error(400, 'Request body should be a JSON array')
206
+ end
207
+
208
+ unique_ids = []
209
+ promises = []
210
+
211
+ requests.each do |request|
212
+ if !request.is_a?(Array)
213
+ return handle_error(400, 'Request item should be an array')
214
+ end
215
+
216
+ id = request[0]
217
+ route = request[1]
218
+ parameters = request[2] || nil
219
+ selector = request[3] || nil
220
+
221
+ if !id || !id.is_a?(String)
222
+ return handle_error(400, 'Request item should have an ID')
223
+ end
224
+
225
+ if !route || !route.is_a?(String)
226
+ return handle_error(400, 'Request item should have a route')
227
+ end
228
+
229
+ if !route_regex.match?(route)
230
+ route_length = route.length
231
+ if route_length < 2
232
+ return handle_error(400, 'Request item route should be at least two characters long')
233
+ elsif route[-1] == '/'
234
+ return handle_error(400, 'Request item route should not end in a forward slash')
235
+ elsif !/[a-zA-Z]/.match?(route[0])
236
+ return handle_error(400, 'Request item route should start with a letter')
237
+ else
238
+ return handle_error(
239
+ 400,
240
+ 'Request item route should contain only letters, numbers, dashes, underscores, and forward slashes'
241
+ )
242
+ end
243
+ end
244
+
245
+ if parameters && !parameters.is_a?(Hash)
246
+ return handle_error(400, 'Request item parameters should be a JSON object')
247
+ end
248
+
249
+ if selector && !selector.is_a?(Array)
250
+ return handle_error(400, 'Request item selector should be a JSON array')
251
+ end
252
+
253
+ if unique_ids.include?(id)
254
+ return handle_error(400, 'Request items should have unique IDs')
255
+ end
256
+
257
+ unique_ids << id
258
+
259
+ route_handler = routes[route] || routes[route.to_sym] || method(:route_not_found)
260
+
261
+ request_object = {
262
+ id: id,
263
+ route: route,
264
+ parameters: parameters,
265
+ selector: selector,
266
+ }
267
+
268
+ promises << route_reducer(route_handler, request_object, context)
269
+ end
270
+
271
+ results = []
272
+
273
+ promises.each do |result|
274
+ results << result
275
+ end
276
+
277
+ return handle_result(results)
278
+ end
279
+
280
+ return handler
281
+ end
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: blest
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - JHunt
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-07-10 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: The Ruby reference implementation of BLEST (Batch-able, Lightweight,
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.
17
+ email:
18
+ - blest@jhunt.dev
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - LICENSE
24
+ - README.md
25
+ - lib/blest.rb
26
+ homepage: https://blest.jhunt.dev
27
+ licenses:
28
+ - MIT
29
+ metadata: {}
30
+ post_install_message:
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubygems_version: 3.0.3
46
+ signing_key:
47
+ specification_version: 4
48
+ summary: The Ruby reference implementation of BLEST
49
+ test_files: []