guaraci 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 +145 -16
- data/lib/guaraci/request.rb +153 -8
- data/lib/guaraci/response.rb +206 -10
- data/lib/guaraci/server.rb +68 -3
- data/lib/guaraci/version.rb +1 -1
- data/lib/guaraci.rb +24 -0
- metadata +15 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 68c9e80fa2a0485fef35d157487369ecb47a9768d70e09d68245b99866a20e84
|
4
|
+
data.tar.gz: e63b52cdab47b82f80f741cbef4bc0b75615ca4c2054c6f47dc2e935df5ae783
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3eba3b02f82c5391b76644cde67d36790f3865e2643356cd0f6bcd2132e143fec02908319c98ed5c469673f207dd9bcdd923bc3795eefc0af702ac9e1c2e2188
|
7
|
+
data.tar.gz: 48521a8e90e75fade5e384d879c9ecf5d7f6c1242c8ef176fead81eee7e583bda0cf020ff833fa5b8bf2c804c5857debb96fe833afe7fbe78b0794d912d43f77
|
data/README.md
CHANGED
@@ -1,39 +1,168 @@
|
|
1
|
-
# Guaraci
|
1
|
+
# ☀️ Guaraci
|
2
2
|
|
3
|
-
|
3
|
+
[](https://rubygems.org/gems/guaraci)
|
4
|
+
[](https://badge.fury.io/rb/guaraci)
|
4
5
|
|
5
|
-
|
6
|
+
[](https://ruby-lang.org)
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
8
|
+
|
9
|
+
Guaraci is very a simple Ruby web microframework built on top of the powerful [Async::HTTP](https://github.com/socketry/async-http).
|
10
|
+
It was designed to be minimalist, providing a clean and intuitive API with as little overhead as possible.
|
11
|
+
|
12
|
+
Its goal is to be minimalist, with a small codebase focused on simplicity, without the need to learn extensive DSLs like other frameworks; it only requires plain Ruby.
|
13
|
+
|
14
|
+
## Features
|
15
|
+
|
16
|
+
- **High performance** - Powered by Async::HTTP for non-blocking concurrency
|
17
|
+
- **Flexible** - Easy to extend and customize
|
18
|
+
- **Lightweight** - Few dependencies
|
6
19
|
|
7
20
|
## Installation
|
8
21
|
|
9
|
-
|
22
|
+
In your gemfiile:
|
10
23
|
|
11
|
-
|
24
|
+
```ruby
|
25
|
+
gem 'guaraci'
|
26
|
+
```
|
27
|
+
|
28
|
+
And run:
|
12
29
|
|
13
30
|
```bash
|
14
|
-
bundle
|
31
|
+
bundle install
|
15
32
|
```
|
16
33
|
|
17
|
-
|
34
|
+
Or:
|
18
35
|
|
19
36
|
```bash
|
20
|
-
gem install
|
37
|
+
gem install guaraci
|
38
|
+
```
|
39
|
+
|
40
|
+
## How to use
|
41
|
+
|
42
|
+
### Hello World
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
require 'guaraci'
|
46
|
+
|
47
|
+
Guaraci::Server.run(port: 3000) do |request|
|
48
|
+
response = Guaraci::Response.ok
|
49
|
+
response.json({ message: "Hello, World!" })
|
50
|
+
response.render
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
### Routing
|
55
|
+
A little about routing first: I decided to keep the code simple, so no routing dsl and constructors were built. Why? The ideia is to use plain ruby, without the need to learn another DSL apart ruby itself, like rails, sinatra, roda, etc.
|
56
|
+
|
57
|
+
I REALLY recommend you to use the new pattern matching feature that comes with Ruby 3.x by default.
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
require 'guaraci'
|
61
|
+
|
62
|
+
Guaraci::Server.run(host: 'localhost', port: 8000) do |request|
|
63
|
+
case [request.method, request.path_segments]
|
64
|
+
in ['GET', []]
|
65
|
+
handle_api_request(request)
|
66
|
+
in ['GET', ['health']]
|
67
|
+
health_check
|
68
|
+
else
|
69
|
+
not_found
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def handle_api_request(request)
|
74
|
+
response = Guaraci::Response.ok
|
75
|
+
response.json({
|
76
|
+
method: request.method,
|
77
|
+
path: request.path_segments,
|
78
|
+
params: request.params,
|
79
|
+
timestamp: Time.now.iso8601
|
80
|
+
})
|
81
|
+
response.render
|
82
|
+
|
83
|
+
## Or you can pass a block like this
|
84
|
+
#
|
85
|
+
# Guaraci::Response.ok do |res|
|
86
|
+
# res.json({
|
87
|
+
# method: request.method,
|
88
|
+
# path: request.path_segments,
|
89
|
+
# params: request.params,
|
90
|
+
# timestamp: Time.now.iso8601
|
91
|
+
# })
|
92
|
+
# end.to_a
|
93
|
+
end
|
94
|
+
|
95
|
+
def health_check
|
96
|
+
response = Guaraci::Response.ok
|
97
|
+
response.json({ status: 'healthy', uptime: Process.clock_gettime(Process::CLOCK_MONOTONIC) })
|
98
|
+
response.render
|
99
|
+
|
100
|
+
## Or you can pass a block like this
|
101
|
+
#
|
102
|
+
# Guaraci::Response.ok do |res|
|
103
|
+
# res.json({
|
104
|
+
# status: 'healthy',
|
105
|
+
# uptime: Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
106
|
+
# })
|
107
|
+
# end
|
108
|
+
end
|
109
|
+
|
110
|
+
def not_found
|
111
|
+
response = Guaraci::Response.new(404)
|
112
|
+
response.json({ error: 'Not Found' })
|
113
|
+
response.render
|
114
|
+
end
|
115
|
+
|
21
116
|
```
|
22
117
|
|
23
|
-
##
|
118
|
+
## Examples
|
24
119
|
|
25
|
-
|
120
|
+
You can see more examples on how to build guaraci apps inside [Examples](https://github.com/glmsilva/guaraci/tree/main/examples) folder
|
26
121
|
|
27
122
|
## Development
|
28
123
|
|
29
|
-
|
124
|
+
Clone the repository:
|
125
|
+
|
126
|
+
1. Execute `bin/setup` to install dependencies
|
127
|
+
2. Execute `rake test` to run tests
|
128
|
+
3. Execute `bin/console` for an interactive prompt
|
129
|
+
|
130
|
+
### Tests
|
131
|
+
Its the plain and old minitest :)
|
132
|
+
|
133
|
+
```bash
|
134
|
+
rake test
|
135
|
+
|
136
|
+
ruby test/guaraci/test_request.rb
|
137
|
+
|
138
|
+
bundle exec rubocop
|
139
|
+
```
|
140
|
+
|
141
|
+
## Inspiration
|
142
|
+
### Why this name?
|
143
|
+
|
144
|
+
In **Tupi-Guarani** indigenous culture, Guaraci (_Guaracy_ or _Kûarasy_) is the solar deity associated with the origin of life. As a central figure in indigenous mythology, and son of **Tupã**, Guaraci embodies the power of the sun, is credited with giving rise to all living beings, and protector of the hunters.
|
145
|
+
|
146
|
+
Its also a word that can be translated to "Sun" in Tupi-Guarani language.
|
147
|
+
|
148
|
+
## Roadmap
|
149
|
+
|
150
|
+
- [ ] Integrated pipeline Middleware
|
151
|
+
- [ ] Templates support
|
152
|
+
- [ ] WebSocket support
|
153
|
+
- [ ] Streaming responses
|
154
|
+
- [ ] A better documentation
|
155
|
+
- [ ] More examples on how to use it
|
156
|
+
- [ ] Performance benchmark
|
30
157
|
|
31
|
-
|
158
|
+
## Acknowledgements
|
32
159
|
|
33
|
-
|
160
|
+
- [Async::HTTP](https://github.com/socketry/async-http) - The powerful asynchronous base of this project
|
161
|
+
- [Wisp](https://github.com/gleam-wisp/wisp) - The great framework that I enjoyed using so much and that inspired the philosophy behind building this one
|
34
162
|
|
35
|
-
|
163
|
+
## 📄 License
|
164
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
36
165
|
|
37
|
-
|
166
|
+
---
|
38
167
|
|
39
|
-
|
168
|
+
Thank you so much, feel free to contact me ☀️ [Guilherme Silva](https://github.com/glmsilva)
|
data/lib/guaraci/request.rb
CHANGED
@@ -1,41 +1,186 @@
|
|
1
|
-
# frozen_string_literal
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "json"
|
4
4
|
|
5
5
|
module Guaraci
|
6
|
+
# Represents the requests wrapper that provides convenient access to request data.
|
7
|
+
# This class wraps the HTTP request object from Async::HTTP.
|
8
|
+
# @example Basic request information
|
9
|
+
# request.method #=> "GET"
|
10
|
+
# request.path_segments #=> ["api", "users", "123"]
|
11
|
+
# request.headers #=> Protocol::HTTP::Headers instance
|
12
|
+
#
|
13
|
+
# @example Accessing request data
|
14
|
+
# request.params #=> {"name" => "John", "email" => "john@example.com"}
|
15
|
+
# request.query_params #=> [["sort", "name"], ["order", "asc"]]
|
16
|
+
#
|
17
|
+
# @see https://github.com/socketry/async-http Async::HTTP documentation
|
18
|
+
# @author Guilherme Silva
|
19
|
+
# @since 1.0.0
|
6
20
|
class Request
|
7
|
-
|
21
|
+
# The HTTP method for this request.
|
22
|
+
#
|
23
|
+
# Common methods include GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS.
|
24
|
+
# The method is automatically extracted from the underlying request object
|
25
|
+
# and normalized to uppercase string format.
|
26
|
+
#
|
27
|
+
# @return [String] HTTP method in uppercase (e.g., "GET", "POST")
|
28
|
+
# @example
|
29
|
+
# request.method #=> "GET"
|
30
|
+
attr_reader :method
|
8
31
|
|
32
|
+
# The path segments extracted from the request URL.
|
33
|
+
#
|
34
|
+
# The path is automatically split by "/" and empty segments are removed.
|
35
|
+
# This creates an array that's perfect for pattern matching and routing logic.
|
36
|
+
# Leading and trailing slashes are ignored.
|
37
|
+
#
|
38
|
+
# @return [Array<String>] Path segments without empty strings
|
39
|
+
# @example Path segments
|
40
|
+
# # For URL: /api/users/123/profile
|
41
|
+
# request.path_segments #=> ["api", "users", "123", "profile"]
|
42
|
+
# user_id = request.path_segments[2] #=> "123"
|
43
|
+
#
|
44
|
+
# @example Pattern Matching for Routing (Recommended)
|
45
|
+
# # Using Ruby's pattern matching for elegant routing
|
46
|
+
# case [request.method, request.path_segments]
|
47
|
+
# in ['GET', ['api', 'users', user_id]]
|
48
|
+
# # user_id automatically captured from URL
|
49
|
+
# in ['GET', ['api', 'posts', post_id, 'comments']]
|
50
|
+
# # post_id automatically captured from URL
|
51
|
+
# end
|
52
|
+
attr_reader :path_segments
|
53
|
+
|
54
|
+
# The original request object from the HTTP server.
|
55
|
+
#
|
56
|
+
# This provides access to the underlying request implementation when you need
|
57
|
+
# low-level access to request data that isn't exposed through the wrapper methods.
|
58
|
+
# Use this when you need to access protocol-specific features.
|
59
|
+
#
|
60
|
+
# @return [Object] The original request object from Async::HTTP
|
61
|
+
# @see https://github.com/socketry/async-http/blob/main/lib/async/http/protocol/request.rb
|
62
|
+
# @example
|
63
|
+
# request.raw_request.version #=> "HTTP/1.1"
|
64
|
+
# request.raw_request.scheme #=> "http"
|
65
|
+
attr_reader :raw_request
|
66
|
+
|
67
|
+
# Initialize a new request wrapper.
|
68
|
+
#
|
69
|
+
# Creates a new Request instance that wraps the underlying HTTP request object.
|
70
|
+
# Automatically extracts commonly needed information like HTTP method and path segments
|
71
|
+
# for easy access and pattern matching.
|
72
|
+
#
|
73
|
+
# @param raw_request [Object] The original request object from Async::HTTP
|
74
|
+
# @example
|
75
|
+
# # Usually called internally by the server
|
76
|
+
# request = Guaraci::Request.new(async_http_request)
|
9
77
|
def initialize(raw_request)
|
10
78
|
@method = raw_request.method
|
11
|
-
@path_segments = raw_request.path
|
79
|
+
@path_segments = raw_request.path&.split("/")&.reject(&:empty?)
|
12
80
|
@raw_request = raw_request
|
13
81
|
end
|
14
82
|
|
83
|
+
# Read and return the request body content.
|
84
|
+
# Returns nil if the request has no body content.
|
85
|
+
#
|
86
|
+
# @return [String, nil] The complete request body as a string
|
87
|
+
# @example
|
88
|
+
# request.body #=> '{"name":"John","email":"john@example.com"}'
|
89
|
+
# @example For requests without body
|
90
|
+
# request.body #=> nil
|
15
91
|
def body
|
16
|
-
@body ||= raw_request
|
92
|
+
@body ||= raw_request&.read
|
17
93
|
end
|
18
94
|
|
95
|
+
# Parse the request body as JSON and return the resulting object.
|
96
|
+
#
|
97
|
+
# Attempts to parse the request body as JSON using {JSON.parse}.
|
98
|
+
# If the body is not valid JSON or is empty, returns an empty hash instead
|
99
|
+
# of raising an exception. This makes it safe to call even when you're not
|
100
|
+
# sure if the request contains JSON data.
|
101
|
+
#
|
102
|
+
# @return [Hash] Parsed JSON data, or empty hash if parsing fails
|
103
|
+
# @see JSON.parse
|
104
|
+
# @example Successful parsing
|
105
|
+
# # Request body: '{"name":"John","age":30}'
|
106
|
+
# request.params #=> {"name" => "John", "age" => 30}
|
107
|
+
#
|
108
|
+
# @example Invalid JSON handling
|
109
|
+
# # Request body: 'invalid json'
|
110
|
+
# request.params #=> {}
|
111
|
+
#
|
112
|
+
# @example Empty body handling
|
113
|
+
# # Request body: nil
|
114
|
+
# request.params #=> {}
|
19
115
|
def params
|
20
116
|
JSON.parse(body)
|
21
|
-
rescue JSON::
|
117
|
+
rescue JSON::ParserError
|
22
118
|
{}
|
23
119
|
end
|
24
120
|
|
121
|
+
# Access the request headers.
|
122
|
+
#
|
123
|
+
# @return [Protocol::HTTP::Headers] The request headers collection
|
124
|
+
# @see https://github.com/socketry/protocol-http/blob/main/lib/protocol/http/headers.rb Protocol::HTTP::Headers
|
125
|
+
# All headers are accessed in lowercase strings according to Protocol::HTTP::Headers
|
126
|
+
# So that way, "User-Agent" becomes "user-agent"
|
127
|
+
# @example
|
128
|
+
# request.headers['content-type'] #=> 'application/json'
|
129
|
+
# request.headers['authorization'] #=> 'Bearer token123'
|
130
|
+
# request.headers['user-agent'] #=> 'Mozilla/5.0...'
|
25
131
|
def headers = raw_request.headers
|
26
132
|
|
133
|
+
# Extract the query string from the request URL.
|
134
|
+
#
|
135
|
+
# Returns the complete query string portion of the URL (everything after the "?").
|
136
|
+
# If no query string is present, returns an empty string. The query string is
|
137
|
+
# returned as-is without any URL decoding.
|
138
|
+
#
|
139
|
+
# @return [String] The query string or empty string if none present
|
140
|
+
# @example
|
141
|
+
# # For URL: /users?name=john&age=30&active=true
|
142
|
+
# request.query #=> "name=john&age=30&active=true"
|
143
|
+
#
|
144
|
+
# @example No query string
|
145
|
+
# # For URL: /users
|
146
|
+
# request.query #=> ""
|
27
147
|
def query
|
28
|
-
raw_request
|
148
|
+
raw_request&.query || ""
|
29
149
|
end
|
30
150
|
|
151
|
+
# Parse the query string into key-value pairs.
|
152
|
+
#
|
153
|
+
# Splits the query string into an array of [key, value] pairs for easy processing.
|
154
|
+
# Each parameter is split on the "=" character. Parameters without values will
|
155
|
+
# have the value portion as an empty string or nil. The result is cached for
|
156
|
+
# subsequent calls.
|
157
|
+
#
|
158
|
+
# @return [Array<Array<String>>] Array of [key, value] pairs
|
159
|
+
# @example
|
160
|
+
# # For query: "name=john&age=30&active=true"
|
161
|
+
# request.query_params #=> [["name", "john"], ["age", "30"], ["active", "true"]]
|
162
|
+
#
|
163
|
+
# @example Parameters without values
|
164
|
+
# # For query: "debug&verbose=1&flag"
|
165
|
+
# request.query_params #=> [["debug"], ["verbose", "1"], ["flag"]]
|
166
|
+
#
|
167
|
+
# @example No query string
|
168
|
+
# request.query_params #=> []
|
31
169
|
def query_params
|
32
170
|
@query_params ||= parse_query
|
33
171
|
end
|
34
172
|
|
35
173
|
private
|
36
174
|
|
175
|
+
# Split the query string into key-value pairs.
|
176
|
+
#
|
177
|
+
# Internal method that handles the actual parsing of the query string.
|
178
|
+
# Splits on "&" to separate parameters, then splits each parameter on "="
|
179
|
+
# to separate keys from values.
|
180
|
+
#
|
181
|
+
# @return [Array<Array<String>>] Parsed query parameters
|
37
182
|
def parse_query
|
38
|
-
query
|
183
|
+
query&.split("&")&.map { |q| q.split("=") }.to_a
|
39
184
|
end
|
40
185
|
end
|
41
186
|
end
|
data/lib/guaraci/response.rb
CHANGED
@@ -1,30 +1,226 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "json"
|
4
|
+
require "protocol/http"
|
4
5
|
|
5
6
|
module Guaraci
|
7
|
+
# HTTP response builder for the Guaraci web framework.
|
8
|
+
#
|
9
|
+
# It handles the conversion between high-level Ruby objects and the low-level Protocol::HTTP
|
10
|
+
# objects required by the HTTP server.
|
11
|
+
#
|
12
|
+
# @example Basic JSON response
|
13
|
+
# response = Guaraci::Response.ok do |res|
|
14
|
+
# res.json({ message: "Hello World", status: "success" })
|
15
|
+
# end
|
16
|
+
# response.render #=> Protocol::HTTP::Response
|
17
|
+
#
|
18
|
+
# @example HTML response with custom status
|
19
|
+
# response = Guaraci::Response.new(201) do |res|
|
20
|
+
# res.html("<h1>Resource Created</h1>")
|
21
|
+
# end
|
22
|
+
# response.render
|
23
|
+
#
|
24
|
+
# @example Plain text response
|
25
|
+
# response = Guaraci::Response.ok do |res|
|
26
|
+
# res.text("Simple message")
|
27
|
+
# end
|
28
|
+
# response.render
|
29
|
+
#
|
30
|
+
# @see https://github.com/socketry/protocol-http Protocol::HTTP documentation
|
31
|
+
# @see https://github.com/socketry/async-http Async::HTTP documentation
|
32
|
+
# @author Guilherme SIlva
|
33
|
+
# @since 1.0.0
|
6
34
|
class Response
|
7
|
-
|
35
|
+
# The HTTP status code for this response.
|
36
|
+
#
|
37
|
+
# Common status codes:
|
38
|
+
# - 200: OK (successful request)
|
39
|
+
# - 201: Created (resource created successfully)
|
40
|
+
# - 400: Bad Request (client error)
|
41
|
+
# - 404: Not Found (resource not found)
|
42
|
+
# - 500: Internal Server Error (server error)
|
43
|
+
#
|
44
|
+
# @return [Integer] HTTP status code (e.g., 200, 404, 500)
|
45
|
+
# @see https://tools.ietf.org/html/rfc7231#section-6 HTTP status code definitions
|
46
|
+
# @example
|
47
|
+
# response.status #=> 200
|
48
|
+
attr_reader :status
|
8
49
|
|
50
|
+
# The HTTP response body containing the actual content.
|
51
|
+
#
|
52
|
+
# The body is automatically converted to {Protocol::HTTP::Body::Buffered}
|
53
|
+
# format when content is written using {#write}, {#json}, {#html}, or {#text}.
|
54
|
+
# This ensures compatibility with Async::HTTP's streaming requirements.
|
55
|
+
#
|
56
|
+
# @return [Protocol::HTTP::Body::Buffered] HTTP response body
|
57
|
+
# @see https://github.com/socketry/protocol-http/blob/main/lib/protocol/http/body/buffered.rb Protocol::HTTP::Body::Buffered
|
58
|
+
# @example
|
59
|
+
# response.body.read #=> '{"message":"Hello World"}'
|
60
|
+
attr_reader :body
|
61
|
+
|
62
|
+
# The HTTP response headers collection.
|
63
|
+
#
|
64
|
+
# Headers are stored as {Protocol::HTTP::Headers} objects
|
65
|
+
#
|
66
|
+
# @return [Protocol::HTTP::Headers] HTTP response headers
|
67
|
+
# @see https://github.com/socketry/protocol-http/blob/main/lib/protocol/http/headers.rb Protocol::HTTP::Headers
|
68
|
+
# @example
|
69
|
+
# response.headers['content-type'] #=> 'application/json'
|
70
|
+
# response.headers['content-length'] #=> '25'
|
71
|
+
attr_reader :headers
|
72
|
+
|
73
|
+
# Initialize a new HTTP response with the specified status code.
|
74
|
+
#
|
75
|
+
# Creates a new response instance with empty headers and body.
|
76
|
+
# The headers are initialized as {Protocol::HTTP::Headers} and the
|
77
|
+
# body starts as an empty buffered body until content is written.
|
78
|
+
#
|
79
|
+
# @param status [Integer] HTTP status code
|
80
|
+
# @raise [ArgumentError] if status is not a valid HTTP status code
|
81
|
+
#
|
82
|
+
# @example Creating different response types
|
83
|
+
# success = Guaraci::Response.new(200) # OK
|
84
|
+
# not_found = Guaraci::Response.new(404) # Not Found
|
85
|
+
# error = Guaraci::Response.new(500) # Internal Server Error
|
86
|
+
#
|
87
|
+
# @example With content
|
88
|
+
# response = Guaraci::Response.new(201)
|
89
|
+
# response.json({ id: 123, created: true })
|
9
90
|
def initialize(status)
|
10
91
|
@status = status
|
11
|
-
@headers =
|
12
|
-
@body =
|
92
|
+
@headers = Protocol::HTTP::Headers.new
|
93
|
+
@body = default_body
|
13
94
|
end
|
14
95
|
|
96
|
+
# Create a successful HTTP response (200 OK).
|
97
|
+
#
|
98
|
+
# This is a convenient factory method for creating successful responses.
|
99
|
+
# It automatically sets the status to 200 and yields the response instance
|
100
|
+
# to the provided block for content configuration.
|
101
|
+
#
|
102
|
+
# @yield [response] Block to configure the response content and headers
|
103
|
+
# @yieldparam response [Response] The response instance to configure
|
104
|
+
# @return [Response] The configured response instance with status 200
|
105
|
+
#
|
106
|
+
# @example Without block (empty 200 response)
|
107
|
+
# response = Guaraci::Response.ok
|
108
|
+
# response.status #=> 200
|
109
|
+
#
|
110
|
+
# @example With configuration block
|
111
|
+
# response = Guaraci::Response.ok do |res|
|
112
|
+
# res.json({ message: "Operation successful!" })
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# @example Method chaining after creation
|
116
|
+
# response = Guaraci::Response.ok
|
117
|
+
# response.json({ data: [1, 2, 3] })
|
15
118
|
def self.ok
|
16
119
|
res = new(200)
|
17
120
|
yield(res) if block_given?
|
18
121
|
res
|
19
122
|
end
|
20
123
|
|
21
|
-
|
22
|
-
|
23
|
-
|
124
|
+
# Write content to the response body with specified content type.
|
125
|
+
#
|
126
|
+
# This is the base method used by all other content methods (json, html, text).
|
127
|
+
# It automatically converts the content to {Protocol::HTTP::Body::Buffered} format
|
128
|
+
# required by Async::HTTP and sets the appropriate Content-Type header.
|
129
|
+
#
|
130
|
+
# @param content [String, Array] Content to write to response body
|
131
|
+
# @param content_type [String] MIME type for the response
|
132
|
+
# @return [Response] Self for method chaining
|
133
|
+
#
|
134
|
+
# @see Protocol::HTTP::Body::Buffered
|
135
|
+
def write(content, content_type: "application/json")
|
136
|
+
@headers["content-type"] = content_type
|
137
|
+
@body = Protocol::HTTP::Body::Buffered.wrap(content)
|
138
|
+
self
|
139
|
+
end
|
140
|
+
|
141
|
+
# Write JSON content to the response body.
|
142
|
+
#
|
143
|
+
# Automatically serializes the provided object to JSON using {JSON.dump}
|
144
|
+
#
|
145
|
+
# @param content [Object] Any object that can be serialized to JSON
|
146
|
+
# @return [Response] Self for method chaining
|
147
|
+
#
|
148
|
+
# @example Hash object
|
149
|
+
# response.json({ message: "Hello", data: [1, 2, 3] })
|
150
|
+
def json(content)
|
151
|
+
write(JSON.dump(content))
|
152
|
+
end
|
153
|
+
|
154
|
+
# Write HTML content to the response body.
|
155
|
+
#
|
156
|
+
# Sets the Content-Type to "text/html" and writes the provided HTML string.
|
157
|
+
# No HTML validation or processing is performed - the content is sent as-is.
|
158
|
+
#
|
159
|
+
# @param content [String] HTML content
|
160
|
+
# @return [Response] Self for method chaining
|
161
|
+
#
|
162
|
+
# @example Simple HTML
|
163
|
+
# response.html("<h1>Welcome</h1>")
|
164
|
+
#
|
165
|
+
# @example Complete HTML document
|
166
|
+
# response.html(<<~HTML)
|
167
|
+
# <!DOCTYPE html>
|
168
|
+
# <html>
|
169
|
+
# <head><title>My Page</title></head>
|
170
|
+
# <body><h1>Hello World!</h1></body>
|
171
|
+
# </html>
|
172
|
+
# HTML
|
173
|
+
def html(content)
|
174
|
+
write(content, content_type: "text/html")
|
24
175
|
end
|
25
176
|
|
26
|
-
|
27
|
-
|
177
|
+
# Write plain text content to the response body.
|
178
|
+
#
|
179
|
+
# Sets the Content-Type to "text/plain" and writes the provided text.
|
180
|
+
#
|
181
|
+
# @param content [String] Plain text content
|
182
|
+
# @return [Response] Self for method chaining
|
183
|
+
#
|
184
|
+
# @example Simple text
|
185
|
+
# response.text("Hello, World!")
|
186
|
+
#
|
187
|
+
# @example Multi-line text
|
188
|
+
# response.text("Line 1\nLine 2\nLine 3")
|
189
|
+
def text(content)
|
190
|
+
write(content, content_type: "text/plain")
|
191
|
+
end
|
192
|
+
|
193
|
+
# Convert the response to a Protocol::HTTP::Response object.
|
194
|
+
#
|
195
|
+
# This method transforms the Guaraci::Response into the low-level response format
|
196
|
+
# required by the Async::HTTP server. It ensures that all components (status, headers, body)
|
197
|
+
# are properly formatted for HTTP transmission.
|
198
|
+
#
|
199
|
+
# The returned object contains:
|
200
|
+
# - version: HTTP version (automatically determined by Protocol::HTTP)
|
201
|
+
# - status: Integer HTTP status code
|
202
|
+
# - headers: Protocol::HTTP::Headers instance with all response headers
|
203
|
+
# - body: Protocol::HTTP::Body::Buffered instance containing the response content
|
204
|
+
#
|
205
|
+
# @return [Protocol::HTTP::Response] A complete HTTP response object ready for transmission
|
206
|
+
# @see https://github.com/socketry/protocol-http/blob/main/lib/protocol/http/response.rb Protocol::HTTP::Response
|
207
|
+
# @see https://github.com/socketry/async-http Async::HTTP server documentation
|
208
|
+
#
|
209
|
+
# @example Basic usage
|
210
|
+
# response = Guaraci::Response.ok { |r| r.json({message: "Hello"}) }
|
211
|
+
# http_response = response.render
|
212
|
+
# http_response.status #=> 200
|
213
|
+
# http_response.headers['content-type'] #=> 'application/json'
|
214
|
+
def render
|
215
|
+
Protocol::HTTP::Response.new(nil, @status, @headers, @body)
|
216
|
+
end
|
217
|
+
|
218
|
+
private
|
219
|
+
|
220
|
+
# @return [Protocol::HTTP::Body::Buffered] An empty body
|
221
|
+
# @see Protocol::HTTP::Body::Buffered.wrap
|
222
|
+
def default_body
|
223
|
+
Protocol::HTTP::Body::Buffered.wrap("")
|
28
224
|
end
|
29
225
|
end
|
30
|
-
end
|
226
|
+
end
|
data/lib/guaraci/server.rb
CHANGED
@@ -1,25 +1,90 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
3
|
+
require_relative "./request"
|
4
4
|
require "async"
|
5
5
|
require "async/http/server"
|
6
6
|
require "async/http/endpoint"
|
7
7
|
require "async/http/protocol/http1"
|
8
|
-
require "json"
|
9
8
|
|
10
9
|
module Guaraci
|
10
|
+
# HTTP server that handles incoming requests with a simple block-based API.
|
11
|
+
# Built on top of Async::HTTP for high performance and non-blocking I/O.
|
12
|
+
#
|
13
|
+
# @example Basic server
|
14
|
+
# Guaraci::Server.run do |request|
|
15
|
+
# Guaraci::Response.ok { |r| r.json({message: "Hello World"}) }.render
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# @example Pattern Matching Routing (What I recommend)
|
19
|
+
# Guaraci::Server.run do |request|
|
20
|
+
# case [request.method, request.path_segments]
|
21
|
+
# in ['GET', []]
|
22
|
+
# Guaraci::Response.ok { |r| r.html("<h1>Welcome!</h1>") }.render
|
23
|
+
# in ['GET', ['health']]
|
24
|
+
# Guaraci::Response.ok { |r| r.json({ status: "healthy" }) }.render
|
25
|
+
# in ['GET', ['api', 'users']]
|
26
|
+
# Guaraci::Response.ok { |r| r.json({ users: [] }) }.render
|
27
|
+
# in ['GET', ['api', 'users', user_id]]
|
28
|
+
# Guaraci::Response.ok { |r| r.json({ user: { id: user_id } }) }.render
|
29
|
+
# in ['POST', ['api', 'users']]
|
30
|
+
# user_data = request.params
|
31
|
+
# Guaraci::Response.new(201) { |r| r.json({ created: user_data }) }.render
|
32
|
+
# else
|
33
|
+
# Guaraci::Response.new(404) { |r| r.json({ error: "Not Found" }) }.render
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# @author Guilherme Silva
|
38
|
+
# @since 1.0.0
|
39
|
+
# @see Guaraci::Request
|
40
|
+
# @see Guaraci::Response
|
41
|
+
# @see https://github.com/socketry/async-http Async::HTTP documentation
|
11
42
|
class Server
|
43
|
+
# Creates a new server instance with the given handler block.
|
44
|
+
#
|
45
|
+
# @param block [Proc] The request handler block that will be called for each HTTP request
|
46
|
+
# @yield [request] Block to handle incoming HTTP requests
|
47
|
+
# @yieldparam request [Guaraci::Request] The wrapped HTTP request object
|
48
|
+
# @yieldreturn [Protocol::HTTP::Response] Response object from calling .render on a Guaraci::Response
|
49
|
+
#
|
50
|
+
# @example
|
51
|
+
# server = Guaraci::Server.new do |request|
|
52
|
+
# case request.method
|
53
|
+
# when "GET"
|
54
|
+
# Guaraci::Response.ok { |r| r.text("Hello World") }.render
|
55
|
+
# else
|
56
|
+
# Guaraci::Response.new(405) { |r| r.text("Method Not Allowed") }.render
|
57
|
+
# end
|
58
|
+
# end
|
12
59
|
def initialize(&block)
|
13
60
|
@handler = block
|
14
61
|
end
|
15
62
|
|
63
|
+
# Process an incoming HTTP request.
|
64
|
+
#
|
65
|
+
# This method is called by the Async::HTTP server for each incoming request.
|
66
|
+
# It wraps the raw request in a {Guaraci::Request} object and executes the
|
67
|
+
# configured handler block.
|
68
|
+
#
|
69
|
+
# @param request [Object] Raw HTTP request from Async::HTTP
|
70
|
+
# @return [Protocol::HTTP::Response] The response object returned by the handler
|
71
|
+
#
|
72
|
+
# @note This method is typically called internally by the server infrastructure
|
73
|
+
# and not directly by user code.
|
74
|
+
# @see https://github.com/socketry/async-http/blob/main/lib/async/http/server.rb Async::HTTP::Server
|
16
75
|
def call(request)
|
17
76
|
request = Request.new(request)
|
18
77
|
|
19
78
|
instance_exec(request, &@handler)
|
20
79
|
end
|
21
80
|
|
22
|
-
|
81
|
+
# Starts an HTTP server on the specified host and port.
|
82
|
+
# The server runs indefinitely until stopped.
|
83
|
+
#
|
84
|
+
# @param host [String] the host to bind to
|
85
|
+
# @param port [Integer] the port to listen on
|
86
|
+
# @param block [Proc] the request handler block
|
87
|
+
def self.run(host: "localhost", port: 8000, &block)
|
23
88
|
app = new(&block)
|
24
89
|
url = "http://#{host}:#{port}"
|
25
90
|
|
data/lib/guaraci/version.rb
CHANGED
data/lib/guaraci.rb
CHANGED
@@ -3,6 +3,30 @@
|
|
3
3
|
require_relative "./guaraci/server"
|
4
4
|
require_relative "./guaraci/response"
|
5
5
|
require_relative "./guaraci/request"
|
6
|
+
require_relative "./guaraci/version"
|
6
7
|
|
8
|
+
# # Guaraci is a minimalist Ruby web framework built on Async::HTTP.
|
9
|
+
# It provides a simple, clean API for building web applications without
|
10
|
+
# complex DSLs or extensive configuration.
|
11
|
+
#
|
12
|
+
# The framework embraces plain Ruby patterns and encourages the use of
|
13
|
+
# pattern matching for routing, making it perfect for modern Ruby applications.
|
14
|
+
#
|
15
|
+
# @example Basic application
|
16
|
+
# require 'guaraci'
|
17
|
+
#
|
18
|
+
# Guaraci::Server.run do |request|
|
19
|
+
# case [request.method, request.path_segments]
|
20
|
+
# in ['GET', []]
|
21
|
+
# Guaraci::Response.ok { |r| r.html("<h1>Welcome to Guaraci!</h1>") }.render
|
22
|
+
# in ['GET', ['api', 'health']]
|
23
|
+
# Guaraci::Response.ok { |r| r.json({status: "ok", timestamp: Time.now}) }.render
|
24
|
+
# else
|
25
|
+
# Guaraci::Response.new(404) { |r| r.json({error: "Not found"}) }.render
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# @author Guilherme Silva
|
30
|
+
# @version 1.0.0
|
7
31
|
module Guaraci
|
8
32
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: guaraci
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Guilherme Silva
|
@@ -51,6 +51,20 @@ dependencies:
|
|
51
51
|
- - ">="
|
52
52
|
- !ruby/object:Gem::Version
|
53
53
|
version: '0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: yard
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0.9'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0.9'
|
54
68
|
description: A very simple web framework made with async http
|
55
69
|
email:
|
56
70
|
- guilherme.gss@outlook.com.br
|