HTTPRuby 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 +7 -0
- data/README.md +100 -0
- data/lib/TeleRuby/app.rb +82 -0
- data/lib/TeleRuby/http_response.rb +17 -0
- data/lib/TeleRuby/router.rb +166 -0
- data/lib/TeleRuby.rb +5 -0
- metadata +91 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fd11c3b0f3a918f0a03b5598ce7f4dd6c2e4dfe6c17c8a400bddd2730898248b
|
4
|
+
data.tar.gz: 93ecdbcd668d9c1122f53246654e04251bb3cc9bfe7258b4602b04510befa47c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b387fdf611455d8823831e82bbbb765b43f0e1d299e4a1f3b1cc1a7c4bb385d62a6c15eec4ca70d44bbf55c539371f09ebd93216e789a8faceaad2c4e5eeef05
|
7
|
+
data.tar.gz: 2daf5df235ad5ea20156f83a1e0f267107c724534e2010c93be16e2f11ab81486d95ed9cdb0e5bd59d3c7b3e0094c33180e855a6f05b51bd9c4a79a09058d4aa
|
data/README.md
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# Own HTTP Server Framework
|
2
|
+
|
3
|
+
This project is a lightweight and customizable HTTP server framework built in Ruby, designed to work seamlessly with Puma. It provides robust tools for creating and managing web APIs, offering features such as route handling, dynamic parameters, query validation, typed responses, and JSON body parsing, all while processing requests asynchronously.
|
4
|
+
|
5
|
+
## Purpose
|
6
|
+
|
7
|
+
The goal of this project is to build an HTTP server framework similar to FastAPI but for Ruby, enabling developers to create web applications quickly and effectively.
|
8
|
+
|
9
|
+
## Technologies and Libraries
|
10
|
+
|
11
|
+
- **Ruby**: Core programming language for the framework.
|
12
|
+
- **Puma**: High-performance web server used to handle requests.
|
13
|
+
- **Rack**: Interface between web servers and web applications.
|
14
|
+
- **JSON**: Parsing and generating JSON responses.
|
15
|
+
|
16
|
+
## Implementation Overview
|
17
|
+
|
18
|
+
This framework consists of the following main components:
|
19
|
+
|
20
|
+
### `App`
|
21
|
+
The main class responsible for managing routers and handling requests asynchronously.
|
22
|
+
|
23
|
+
#### Key Features:
|
24
|
+
- Adds routers to the application.
|
25
|
+
- Handles incoming requests and delegates them to appropriate routers.
|
26
|
+
- Parses JSON bodies and query parameters.
|
27
|
+
- Generates JSON responses.
|
28
|
+
- Asynchronous request handling.
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
class Router
|
32
|
+
def initialize
|
33
|
+
@routes = { GET: {}, POST: {}, PUT: {}, DELETE: {} }
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_route(method, path, accepted_queries: nil, accepted_body: nil, accepted_response: nil, &block)
|
37
|
+
@routes[method][path] = {
|
38
|
+
accepted_queries: accepted_queries,
|
39
|
+
accepted_body: accepted_body,
|
40
|
+
accepted_response: accepted_response,
|
41
|
+
handler: block
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def get(path, accepted_queries: nil, accepted_body: nil, accepted_response: nil, &block)
|
46
|
+
add_route(:GET, path, accepted_queries: accepted_queries, accepted_body: accepted_body, accepted_response: accepted_response, &block)
|
47
|
+
end
|
48
|
+
|
49
|
+
def post(path, accepted_queries: nil, accepted_body: nil, accepted_response: nil, &block)
|
50
|
+
add_route(:POST, path, accepted_queries: accepted_queries, accepted_body: accepted_body, accepted_response: accepted_response, &block)
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
### `Router`
|
55
|
+
Handles route definitions and request matching. Supports dynamic routes, query validation, and request body validation.
|
56
|
+
|
57
|
+
#### Key Features:
|
58
|
+
- Define routes for `GET`, `POST`, `PUT`, and `DELETE` methods.
|
59
|
+
- Validate query parameters and request bodies.
|
60
|
+
- Support for dynamic route parameters with type constraints.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
class Router
|
64
|
+
def initialize
|
65
|
+
@routes = { GET: {}, POST: {}, PUT: {}, DELETE: {} }
|
66
|
+
end
|
67
|
+
|
68
|
+
def get(path, accepted_queries: nil, accepted_body: nil, accepted_response: nil, &block)
|
69
|
+
add_route(:GET, path, accepted_queries: accepted_queries, accepted_body: accepted_body, accepted_response: accepted_response, &block)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
### `HTTPResponse`
|
75
|
+
Encapsulates HTTP responses with status codes, headers, and JSON bodies.
|
76
|
+
|
77
|
+
#### Key Features:
|
78
|
+
- Generates structured responses for the client.
|
79
|
+
- Based on Rack return standard
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
class HTTPResponse
|
83
|
+
def initialize(status, json)
|
84
|
+
@status = status
|
85
|
+
@headers = { 'Content-Type' => 'application/json' }
|
86
|
+
@body = [JSON.generate(json)]
|
87
|
+
end
|
88
|
+
|
89
|
+
def to_a
|
90
|
+
[@status, @headers, @body]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
## Usage
|
96
|
+
|
97
|
+
## Conclusion
|
98
|
+
|
99
|
+
This framework offers a clean, Ruby-based solution for building HTTP APIs, complete with dynamic routing, type validation, and asynchronous processing. By leveraging the power of Puma and Rack, it provides a high-performance and scalable foundation for your web applications.
|
100
|
+
|
data/lib/TeleRuby/app.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'puma'
|
3
|
+
require 'rack'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
require_relative 'router'
|
7
|
+
|
8
|
+
class App
|
9
|
+
def initialize
|
10
|
+
@routers = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_router(path, router)
|
14
|
+
@routers[path] = router
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
# Use a Thread to handle the request asynchronously
|
19
|
+
result = nil
|
20
|
+
thread = Thread.new do
|
21
|
+
result = handle_request(env)
|
22
|
+
end
|
23
|
+
thread.join # Ensure the thread completes before returning
|
24
|
+
result # Return the result from the thread
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
def handle_request(env)
|
29
|
+
begin
|
30
|
+
if env["CONTENT_TYPE"] == "application/json"
|
31
|
+
request_body = JSON.parse(env["rack.input"].read)
|
32
|
+
env["parsed_body"] = request_body
|
33
|
+
end
|
34
|
+
|
35
|
+
query_params = Rack::Utils.parse_query(env["QUERY_STRING"])
|
36
|
+
env["query_params"] = query_params
|
37
|
+
|
38
|
+
full_path = env["PATH_INFO"]
|
39
|
+
method = env["REQUEST_METHOD"].to_sym
|
40
|
+
|
41
|
+
@routers.each do |base_path, router|
|
42
|
+
if full_path.start_with?(base_path)
|
43
|
+
sub_path = full_path.sub(base_path, "")
|
44
|
+
return router.call(sub_path, method, env, full_path)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
json_response(404, { error: "Not Found" })
|
49
|
+
rescue => e
|
50
|
+
puts "[ERROR] #{e.message}"
|
51
|
+
puts e.backtrace.join("\n")
|
52
|
+
json_response(500, { error: "Internal Server Error" })
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
def run
|
58
|
+
puts "Starting Puma Server..."
|
59
|
+
server = Puma::Server.new(self)
|
60
|
+
|
61
|
+
server.add_tcp_listener('127.0.0.1', 3000) # Port setup
|
62
|
+
|
63
|
+
trap(:INT) do
|
64
|
+
puts "\nStopping the server..."
|
65
|
+
server.stop
|
66
|
+
end
|
67
|
+
|
68
|
+
puts "Server successfully started."
|
69
|
+
server.run
|
70
|
+
server.thread.join # Ensure server blocks the main thread
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def json_response(status, data)
|
76
|
+
[
|
77
|
+
status,
|
78
|
+
{ 'Content-Type' => 'application/json' },
|
79
|
+
[JSON.generate(data)]
|
80
|
+
]
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class HTTPResponse
|
4
|
+
|
5
|
+
attr_accessor :status, :headers, :body
|
6
|
+
|
7
|
+
def initialize(status, json)
|
8
|
+
@status = status
|
9
|
+
@headers = { 'Content-Type' => 'application/json' }
|
10
|
+
@body = [JSON.generate(json)]
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_a
|
14
|
+
[@status, @headers, @body]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
class Router
|
5
|
+
def initialize
|
6
|
+
@routes = { GET: {}, POST: {}, PUT: {}, DELETE: {} }
|
7
|
+
end
|
8
|
+
|
9
|
+
def add_route(method, path, accepted_queries: nil, accepted_body: nil, accepted_response: nil, &block)
|
10
|
+
@routes[method][path] = {
|
11
|
+
accepted_queries: accepted_queries,
|
12
|
+
accepted_body: accepted_body,
|
13
|
+
accepted_response: accepted_response,
|
14
|
+
handler: block
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def get(path, accepted_queries: nil, accepted_body: nil, accepted_response: nil, &block)
|
19
|
+
add_route(:GET, path, accepted_queries: accepted_queries, accepted_body: accepted_body, accepted_response: accepted_response, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def post(path, accepted_queries: nil, accepted_body: nil, accepted_response: nil, &block)
|
23
|
+
add_route(:POST, path, accepted_queries: accepted_queries, accepted_body: accepted_body, accepted_response: accepted_response, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def call(path, method, env, full_path)
|
27
|
+
log_request(method, full_path)
|
28
|
+
|
29
|
+
unless @routes.key?(method)
|
30
|
+
log_response(405, "Method Not Allowed")
|
31
|
+
return json_response(405, { error: "Method Not Allowed" })
|
32
|
+
end
|
33
|
+
|
34
|
+
@routes[method].each do |route_path, route|
|
35
|
+
match = match_path(route_path, path)
|
36
|
+
|
37
|
+
if match
|
38
|
+
# If there's a match, extract URL parameters (if any)
|
39
|
+
if match.is_a?(MatchData)
|
40
|
+
env["url_params"] = match.named_captures || {}
|
41
|
+
else
|
42
|
+
env["url_params"] = {}
|
43
|
+
end
|
44
|
+
|
45
|
+
# Validate query parameters
|
46
|
+
if route[:accepted_queries] && !validate_queries(route[:accepted_queries], env["query_params"])
|
47
|
+
log_response(400, "Invalid Query Parameters")
|
48
|
+
return json_response(400, { error: "Invalid Query Parameters" })
|
49
|
+
end
|
50
|
+
|
51
|
+
# Validate body parameters
|
52
|
+
if route[:accepted_body] && !validate_body(route[:accepted_body], env["parsed_body"])
|
53
|
+
log_response(400, "Invalid Body Parameters")
|
54
|
+
return json_response(400, { error: "Invalid Body Parameters" })
|
55
|
+
end
|
56
|
+
|
57
|
+
# Call the handler block
|
58
|
+
result = route[:handler].call(env)
|
59
|
+
|
60
|
+
# Ensure result is an HTTPResponse and validate it
|
61
|
+
if result.is_a?(HTTPResponse)
|
62
|
+
if route[:accepted_response] && !validate_accepted_response(route[:accepted_response], result.body)
|
63
|
+
log_response(500, "Invalid Response Structure")
|
64
|
+
return json_response(500, { error: "Invalid Response Structure" })
|
65
|
+
end
|
66
|
+
|
67
|
+
log_response(result.status, result.body[0] || result.body[0])
|
68
|
+
|
69
|
+
return result.to_a # Convert HTTPResponse to Rack-compatible response
|
70
|
+
else
|
71
|
+
log_response(500, "Handler did not return an HTTPResponse")
|
72
|
+
return json_response(500, { error: "Handler must return an HTTPResponse" })
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# If no match is found, return 404
|
78
|
+
log_response(404, "Not Found")
|
79
|
+
json_response(404, { error: "Not Found" })
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def json_response(status, data)
|
85
|
+
[
|
86
|
+
status,
|
87
|
+
{ 'Content-Type' => 'application/json' },
|
88
|
+
[JSON.generate(data)]
|
89
|
+
]
|
90
|
+
end
|
91
|
+
|
92
|
+
def log_request(method, path)
|
93
|
+
method_color = case method
|
94
|
+
when :GET then "\e[32m" # Green for GET
|
95
|
+
when :POST then "\e[34m" # Blue for POST
|
96
|
+
when :PUT then "\e[33m" # Yellow for PUT
|
97
|
+
when :DELETE then "\e[31m" # Red for DELETE
|
98
|
+
else "\e[37m" # White for others
|
99
|
+
end
|
100
|
+
reset = "\e[0m"
|
101
|
+
puts "#{method_color}[REQUEST] Method: #{method} | Path: #{path}#{reset}"
|
102
|
+
end
|
103
|
+
|
104
|
+
def log_response(status, reason)
|
105
|
+
status_color = case status
|
106
|
+
when 200 then "\e[32m" # Green for success
|
107
|
+
when 400 then "\e[33m" # Yellow for client errors
|
108
|
+
when 404 then "\e[31m" # Red for not found
|
109
|
+
when 405 then "\e[31m" # Red for method not allowed
|
110
|
+
when 500 then "\e[35m" # Magenta for server errors
|
111
|
+
else "\e[37m" # White for other statuses
|
112
|
+
end
|
113
|
+
reset = "\e[0m"
|
114
|
+
puts "#{status_color}[RESPONSE] Status: #{status} | Reason: #{reason}#{reset}"
|
115
|
+
end
|
116
|
+
|
117
|
+
def match_path(route_path, request_path)
|
118
|
+
regex_path = route_path.gsub(/\$\{(\w+)(?::(\w+))?\}/) do |_match|
|
119
|
+
param_name = Regexp.last_match(1)
|
120
|
+
param_type = Regexp.last_match(2)
|
121
|
+
|
122
|
+
# Handle different types
|
123
|
+
case param_type
|
124
|
+
when "int"
|
125
|
+
"(?<#{param_name}>\\d+)" # Only digits
|
126
|
+
when "string", nil
|
127
|
+
"(?<#{param_name}>[^/]+)" # Default to any non-slash characters
|
128
|
+
else
|
129
|
+
raise "Unsupported type: #{param_type}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
Regexp.new("^#{regex_path}$").match(request_path)
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
def validate_accepted_response(accepted_response_class, response_body)
|
138
|
+
return false unless response_body.is_a?(Array) && response_body.first.is_a?(String)
|
139
|
+
|
140
|
+
parsed_json = JSON.parse(response_body.first, symbolize_names: true)
|
141
|
+
|
142
|
+
instance = accepted_response_class.new
|
143
|
+
|
144
|
+
parsed_json.each do |key, value|
|
145
|
+
return false unless instance.respond_to?("#{key}=")
|
146
|
+
end
|
147
|
+
|
148
|
+
true
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
|
153
|
+
|
154
|
+
def validate_queries(accepted_queries, query_params)
|
155
|
+
# Ensure all required queries are present in the query_params
|
156
|
+
accepted_queries.all? { |query| query_params.key?(query) }
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
def validate_body(accepted_body, parsed_body)
|
161
|
+
|
162
|
+
accepted_body.instance_variables.all? do |var|
|
163
|
+
parsed_body.key?(var.to_s.sub('@', ''))
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
data/lib/TeleRuby.rb
ADDED
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: HTTPRuby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bryan Sigaran
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-12-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: puma
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rack
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: json
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.0'
|
55
|
+
description: An HTTP server framework similar to FastAPI, featuring async processing,
|
56
|
+
dynamic routing, and type validation.
|
57
|
+
email:
|
58
|
+
- bryanohss@gmail.com
|
59
|
+
executables: []
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- README.md
|
64
|
+
- lib/TeleRuby.rb
|
65
|
+
- lib/TeleRuby/app.rb
|
66
|
+
- lib/TeleRuby/http_response.rb
|
67
|
+
- lib/TeleRuby/router.rb
|
68
|
+
homepage: https://github.com/artisenpaiii/TeleRuby
|
69
|
+
licenses:
|
70
|
+
- MIT
|
71
|
+
metadata: {}
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
requirements: []
|
87
|
+
rubygems_version: 3.4.20
|
88
|
+
signing_key:
|
89
|
+
specification_version: 4
|
90
|
+
summary: A lightweight HTTP server framework for Ruby
|
91
|
+
test_files: []
|