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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 872f02c23c21487409ca93acc9c6fb36fd25d54f8a03f0c6ffe502f9b36e61a6
4
- data.tar.gz: 42e120be9876e28afdec28d8673287d927d8a9a4fbff3011fec36ccd08c98b84
3
+ metadata.gz: 68c9e80fa2a0485fef35d157487369ecb47a9768d70e09d68245b99866a20e84
4
+ data.tar.gz: e63b52cdab47b82f80f741cbef4bc0b75615ca4c2054c6f47dc2e935df5ae783
5
5
  SHA512:
6
- metadata.gz: b8c423cb5d52ebd4420ca37959bd0b0e147b238ddc2882e5145664b29059cc10fdf5908f61e8928500ceb94707873aaba1d9d6aa7779a433e32f70d3f948152c
7
- data.tar.gz: d9404e2dd3929e1b2c950f37412b495a9a0d61b77801c33de8b4b3e718aa72561b8b100dd49f49f472fed908d32cc3fba7ebacb8770a178e740efcd5a5d3258b
6
+ metadata.gz: 3eba3b02f82c5391b76644cde67d36790f3865e2643356cd0f6bcd2132e143fec02908319c98ed5c469673f207dd9bcdd923bc3795eefc0af702ac9e1c2e2188
7
+ data.tar.gz: 48521a8e90e75fade5e384d879c9ecf5d7f6c1242c8ef176fead81eee7e583bda0cf020ff833fa5b8bf2c804c5857debb96fe833afe7fbe78b0794d912d43f77
data/README.md CHANGED
@@ -1,39 +1,168 @@
1
- # Guaraci
1
+ # ☀️ Guaraci
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ [![Static Badge](https://img.shields.io/badge/rubygems-guaraci-brightgreen)](https://rubygems.org/gems/guaraci)
4
+ [![Gem Version](https://badge.fury.io/rb/guaraci.svg)](https://badge.fury.io/rb/guaraci)
4
5
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/guaraci`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.4.0-red.svg)](https://ruby-lang.org)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
22
+ In your gemfiile:
10
23
 
11
- Install the gem and add to the application's Gemfile by executing:
24
+ ```ruby
25
+ gem 'guaraci'
26
+ ```
27
+
28
+ And run:
12
29
 
13
30
  ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
31
+ bundle install
15
32
  ```
16
33
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
34
+ Or:
18
35
 
19
36
  ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
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
- ## Usage
118
+ ## Examples
24
119
 
25
- TODO: Write usage instructions here
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
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
158
+ ## Acknowledgements
32
159
 
33
- ## Contributing
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
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/guaraci.
163
+ ## 📄 License
164
+ [MIT License](https://opensource.org/licenses/MIT).
36
165
 
37
- ## License
166
+ ---
38
167
 
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
168
+ Thank you so much, feel free to contact me ☀️ [Guilherme Silva](https://github.com/glmsilva)
@@ -1,41 +1,186 @@
1
- # frozen_string_literal
1
+ # frozen_string_literal: true
2
2
 
3
- require 'json'
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
- attr_reader :method, :path_segments, :raw_request
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.split('/').reject(&:empty?)
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.read
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::ParseError
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.query || ""
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.split('&').map { |q| q.split('=') }.to_a
183
+ query&.split("&")&.map { |q| q.split("=") }.to_a
39
184
  end
40
185
  end
41
186
  end
@@ -1,30 +1,226 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
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
- attr_reader :status, :body, :headers
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
- def json(object)
22
- @headers["Content-Type"] = "application/json"
23
- @body = [JSON.dump((object))]
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
- def to_a
27
- [@status, @headers, @body]
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
@@ -1,25 +1,90 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './request.rb'
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
- def self.run(host: 'localhost', port: 8000, &block)
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Guaraci
4
- VERSION = "0.1.0"
4
+ VERSION = "1.0.0"
5
5
  end
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: 0.1.0
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