lennarb 1.2.0 → 1.4.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.
@@ -0,0 +1,201 @@
1
+ # Getting Started with Lennarb
2
+
3
+ ## Overview
4
+
5
+ Lennarb is a minimalist, thread-safe Rack-based web framework for Ruby that focuses on simplicity and performance. It provides a clean routing DSL and straightforward request/response handling.
6
+
7
+ ## Installation
8
+
9
+ Add Lennarb to your project's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'lennarb'
13
+ ```
14
+
15
+ Or install it directly via RubyGems:
16
+
17
+ ```bash
18
+ gem install lennarb
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ### Basic Application
24
+
25
+ Create a new file named `config.ru`:
26
+
27
+ ```ruby
28
+ require 'lennarb'
29
+
30
+ app = Lennarb.new do |app|
31
+ app.get '/' do |req, res|
32
+ res.status = 200
33
+ res.html('<h1>Welcome to Lennarb!</h1>')
34
+ end
35
+ end
36
+
37
+ app.initializer!
38
+ run app
39
+ ```
40
+
41
+ Start the server:
42
+
43
+ ```bash
44
+ rackup
45
+ ```
46
+
47
+ Your application will be available at `http://localhost:9292`.
48
+
49
+ ## Core Concepts
50
+
51
+ ### Request Handling
52
+
53
+ Each route handler receives two arguments:
54
+
55
+ - `req`: A Request object wrapping the Rack environment
56
+ - `res`: A Response object for building the HTTP response
57
+
58
+ ### Response Types
59
+
60
+ Lennarb provides three main response helpers:
61
+
62
+ ```rb
63
+ app.get '/text' do |req, res|
64
+ res.text('Plain text response')
65
+ end
66
+
67
+ app.get '/html' do |req, res|
68
+ res.html('<h1>HTML response</h1>')
69
+ end
70
+
71
+ app.get '/json' do |req, res|
72
+ res.json('{"message": "JSON response"}')
73
+ end
74
+ ```
75
+
76
+ ### Redirects
77
+
78
+ ```ruby
79
+ app.get '/redirect' do |req, res|
80
+ res.redirect('/new-location', 302) # 302
81
+ end
82
+ ```
83
+
84
+ Routes are defined using HTTP method helpers:
85
+
86
+ ```ruby
87
+ app = Lennarb.new do |l|
88
+ # Basic route
89
+ l.get '/' do |req, res|
90
+ res.html('Home page')
91
+ end
92
+
93
+ # Route with parameters
94
+ l.get '/users/:id' do |req, res|
95
+ user_id = req.params[:id]
96
+ res.json("{\"id\": #{user_id}}")
97
+ end
98
+ end
99
+ ```
100
+
101
+ ### Route Parameters
102
+
103
+ Parameters from dynamic route segments are available in `req.params`:
104
+
105
+ ```ruby
106
+ app.get '/hello/:name' do |req, res|
107
+ name = req.params[:name]
108
+ res.text("Hello, #{name}!")
109
+ end
110
+ ```
111
+
112
+ ## Thread Safety
113
+
114
+ Lennarb is thread-safe by design:
115
+
116
+ - All request processing is synchronized using a mutex
117
+ - The router tree is frozen after initialization
118
+ - Response objects are created per-request
119
+
120
+ ## Application Lifecycle
121
+
122
+ ### Initialization
123
+
124
+ ```ruby
125
+ app = Lennarb.new do |l|
126
+ # Define routes and configuration
127
+ end
128
+
129
+ # Initialize and freeze the application
130
+ app.initializer!
131
+ ```
132
+
133
+ The `initializer!` method:
134
+
135
+ - Loads environment-specific dependencies
136
+ - Freezes the route tree
137
+ - Freezes the Rack application
138
+
139
+ ### Environment
140
+
141
+ Lennarb uses the `LENNA_ENV` environment variable (defaults to "development"):
142
+
143
+ ```bash
144
+ LENNA_ENV=production rackup
145
+ ```
146
+
147
+ ## Error Handling
148
+
149
+ Lennarb provides basic error handling:
150
+
151
+ ```ruby
152
+ app.get '/api' do |req, res|
153
+ # Errors are caught and return 500 with error message
154
+ raise "Something went wrong"
155
+ end
156
+ ```
157
+
158
+ Default error responses:
159
+
160
+ - 404 for unmatched routes
161
+ - 500 for application errors
162
+
163
+ ## Best Practices
164
+
165
+ 1. **Always call initializer!**
166
+
167
+ ```ruby
168
+ app = Lennarb.new { |l| ... }
169
+ app.initializer!
170
+ run app
171
+ ```
172
+
173
+ 2. **Set response status**
174
+
175
+ ```ruby
176
+ app.get '/api' do |req, res|
177
+ res.status = 200
178
+ res.json('{"status": "ok"}')
179
+ end
180
+ ```
181
+
182
+ 3. **Use appropriate response types**
183
+
184
+ ```ruby
185
+ # HTML for web pages
186
+ res.html('<h1>Web Page</h1>')
187
+
188
+ # JSON for APIs
189
+ res.json('{"data": "value"}')
190
+
191
+ # Text for simple responses
192
+ res.text('Hello')
193
+ ```
194
+
195
+ ## Support
196
+
197
+ For help and bug reports, please visit:
198
+
199
+ - GitHub Issues: [lennarb/issues](https://github.com/aristotelesbr/lennarb/issues)
200
+
201
+ Now you can run your app!
data/guides/links.yaml ADDED
@@ -0,0 +1,6 @@
1
+ getting-started:
2
+ order: 1
3
+ performance:
4
+ order: 2
5
+ response:
6
+ order: 3
@@ -0,0 +1,120 @@
1
+ # Performance
2
+
3
+ The **Lennarb** is very fast. The following benchmarks were performed on a MacBook Pro (Retina, 13-inch, Early 2013) with 2,7 GHz Intel Core i7 and 8 GB 1867 MHz DDR3. Based on [jeremyevans/r10k](https://github.com/jeremyevans/r10k) using the following. All tests are performed using the **Ruby 3.3.0**
4
+
5
+ ## Benchmark results
6
+
7
+ This document contains the benchmarks comparing **Lennarb** with other routers based on Rack. Metrics evaluated include Requests per Second, Initial memory usage and Startup time.
8
+
9
+ ### 1. Requests per Second (RPS)
10
+
11
+ ![RPS](https://raw.githubusercontent.com/aristotelesbr/lennarb/main/benchmark/rps.png)
12
+
13
+ | Position | Application | 10 RPS | 100 RPS | 1.000 RPS | 10.000 RPS |
14
+ | -------- | ----------- | ---------- | ---------- | --------- | ---------- |
15
+ | 1 | Lenna | 126.252,36 | 108.086,55 | 87.111,91 | 68.460,64 |
16
+ | 2 | Roda | 123.360,37 | 88.380,56 | 66.990,77 | 48.108,29 |
17
+ | 3 | Syro | 114.105,38 | 80.909,39 | 61.415,86 | 46.639,81 |
18
+ | 4 | Hanami-API | 68.089,18 | 52.851,88 | 40.801,78 | 27.996,00 |
19
+
20
+ This table ranks the routers by the number of requests they can process per second. Higher numbers indicate better performance.
21
+
22
+ ### 2. Initial memory usage (in KB)
23
+
24
+ ![Memory](https://raw.githubusercontent.com/aristotelesbr/lennarb/main/benchmark/memory.png)
25
+
26
+ | Position | Application | 10 KB | 100 KB | 1.000 KB | 10.000 KB |
27
+ | -------- | ----------- | ------ | ------ | -------- | --------- |
28
+ | 1 | Syro | 12,160 | 12,544 | 16,460 | 49,692 |
29
+ | 2 | Lenna | 14,464 | 14,720 | 18,232 | 56,812 |
30
+ | 3 | Roda | 15,104 | 15,104 | 18,220 | 49,900 |
31
+ | 4 | Hanami-API | 15,744 | 16,128 | 20,888 | 64,824 |
32
+
33
+ This table shows the initial memory usage in KB. Lower values indicate lower memory consumption.
34
+
35
+ ### 3. Startup time (in seconds)
36
+
37
+ ![Startup](https://raw.githubusercontent.com/aristotelesbr/lennarb/main/benchmark/runtime_with_startup.png)
38
+
39
+ | Position | Application | 10 seg | 100 seg | 1.000 seg | 10.000 seg |
40
+ | -------- | ----------- | ------ | ------- | --------- | ---------- |
41
+ | 1 | Syro | 0.274 | 0.347 | 0.455 | 0.997 |
42
+ | 2 | Lenna | 0.289 | 0.312 | 0.393 | 0.914 |
43
+ | 3 | Roda | 0.294 | 0.378 | 0.467 | 0.918 |
44
+ | 4 | Hanami-API | 0.445 | 0.550 | 0.808 | 3.074 |
45
+
46
+ This table shows the startup time in seconds. Lower values indicate faster startup times.
47
+
48
+ ## Graphs
49
+
50
+ See the graphs in the `benchmarks` directory of the lennarb project.
51
+
52
+ ## Steps to run the benchmarks
53
+
54
+ ### 1. Install the router gem you want to test
55
+
56
+ ```bash
57
+ gem install lennarb
58
+ gem install syro
59
+ gem install roda
60
+ ```
61
+
62
+ ### 2. Clone the jeremyevans/r10k repository
63
+
64
+ ```bash
65
+ git clone https://github.com/jeremyevans/r10k
66
+ ```
67
+
68
+ ### 3. Create a new file in the `r10k` directory
69
+
70
+ In the `r10k` directory, create a new file called `lennarb.rb` into `builders` directory with the code below:
71
+
72
+ ```bash
73
+ touch r10k/builders/lennarb.rb
74
+ ```
75
+
76
+ Put the code below into `lennarb.rb` file:
77
+
78
+ ```rb
79
+ # frozen_string_literal: true
80
+
81
+ # Released under the MIT License.
82
+ # Copyright, 2024, by Aristóteles Coutinho.
83
+
84
+ lennarb_routes =
85
+ lambda do |f, level, prefix, calc_path, lvars|
86
+ base = BASE_ROUTE.dup
87
+ ROUTES_PER_LEVEL.times do
88
+ route = "#{prefix}#{base}"
89
+ if level == 1
90
+ params = lvars.map { |lvar| "\#{req.params[:#{lvar}]}" }
91
+ .join('-')
92
+ f.puts " app.get '#{route}/:#{lvars.last}' do |req, res|"
93
+ f.puts " body = \"#{calc_path[1..]}#{base}-#{params}\""
94
+ f.puts ' res.html body'
95
+ f.puts ' end'
96
+ else
97
+ lennarb_routes.call(f, level - 1, "#{route}/:#{lvars.last}/", "#{calc_path}#{base}/", lvars + [lvars.last.succ])
98
+ end
99
+ base.succ!
100
+ end
101
+ end
102
+
103
+ File.open("#{File.dirname(__FILE__)}/../apps/lennarb_#{LEVELS}_#{ROUTES_PER_LEVEL}.rb", 'wb') do |f|
104
+ f.puts '# frozen_string_literal: true'
105
+ f.puts "require 'lennarb'"
106
+ f.puts 'app = Lennarb.new'
107
+ lennarb_routes.call(f, LEVELS, '/', '/', ['a'])
108
+ f.puts 'App = app'
109
+ end
110
+ ```
111
+
112
+ ### 4. Run the benchmarks
113
+
114
+ ```bash
115
+ bundle exec rake bench graphs R10K_APPS="lennarb syro roda"
116
+ ```
117
+
118
+ ## Conclusion
119
+
120
+ These numbers are just a small reference, **Lennarb** is not a framework, it is a router. In my opinion, **Roda** is the best router for Ruby because it has many interesting features, such as a middleware manager, and very good development performance.
@@ -0,0 +1,83 @@
1
+ # Response
2
+
3
+ This is the response guide.
4
+ The `res` object is used to send a response to the client. The Lennarb use a custom response object to send responses to the client. The `res` object is an instance of {Lennarb::Response}.
5
+
6
+ ## Usage
7
+
8
+ You can use the `res` object to send a response to the client.
9
+
10
+ ```ruby
11
+ # app.rb
12
+
13
+ app.get '/' do |req, res|
14
+ res.html 'Hello World'
15
+ end
16
+ ```
17
+
18
+ ## Content Types
19
+
20
+ Lenna supports the following content types:
21
+
22
+ ```ruby
23
+ # app.rb
24
+
25
+ app.get '/' do |req, res|
26
+ res.html 'Hello World'
27
+ res.json '{"message": "Hello World"}'
28
+ res.text 'Hello World'
29
+ end
30
+ ```
31
+
32
+ But you can also set your own content type:
33
+
34
+ ```ruby
35
+ res['content-type'] = 'text/markdown'
36
+ res.write '# Hello World'
37
+ ```
38
+
39
+ ## The write method
40
+
41
+ You can use the `res.write` method to write to the response body:
42
+
43
+ ```ruby
44
+ # app.rb
45
+
46
+ app.get '/' do |req, res|
47
+ res.write 'Hello World'
48
+ end
49
+ ```
50
+
51
+ JSON example:
52
+
53
+ ```ruby
54
+ # app.rb
55
+
56
+ app.post '/posts' do |req, res|
57
+ req.params # => { name: 'Lenna' }
58
+ name = req.params[:name]
59
+
60
+ res.write({ data: { name: } }.to_json) # This will write to the response body
61
+ end
62
+ ```
63
+
64
+ ## Status Codes
65
+
66
+ You can set the status code using the `res.status` method:
67
+
68
+ ```ruby
69
+ res.status 200
70
+ ```
71
+
72
+ ## Redirects
73
+
74
+ You can redirect the client using the `res.redirect` method:
75
+
76
+ ```ruby
77
+ # app.ruby
78
+
79
+ app.get '/' do |req, res|
80
+ # Stuff code here...
81
+ res.redirect '/hello'
82
+ end
83
+ ```
data/lennarb.gemspec ADDED
@@ -0,0 +1,44 @@
1
+ require_relative "lib/lennarb/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "lennarb"
5
+ spec.version = Lennarb::VERSION
6
+
7
+ spec.summary = <<~DESC
8
+ Lennarb provides a lightweight yet robust solution for web routing in Ruby, focusing on performance and simplicity.
9
+ DESC
10
+ spec.authors = ["Aristóteles Coutinho"]
11
+ spec.license = "MIT"
12
+ spec.homepage = "https://aristotelesbr.github.io/lennarb"
13
+ spec.metadata = {
14
+ "allowed_push_host" => "https://rubygems.org",
15
+ "changelog_uri" => "https://github.com/aristotelesbr/lennarb/blob/master/changelog.md",
16
+ "homepage_uri" => "https://aristotelesbr.github.io/lennarb",
17
+ "rubygems_mfa_required" => "true",
18
+ "source_code_uri" => "https://github.com/aristotelesbr/lennarb"
19
+ }
20
+
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
+ `git ls-files -z`
23
+ .split("\x0")
24
+ .reject { |f| f.match(%r{^(test|features)/}) }
25
+ end
26
+
27
+ spec.bindir = "exe"
28
+ spec.executables = ["lenna"]
29
+ spec.require_paths = ["lib"]
30
+
31
+ spec.add_dependency "bigdecimal"
32
+ spec.add_dependency "colorize", "~> 1.1"
33
+ spec.add_dependency "rack", "~> 3.1"
34
+ spec.add_development_dependency "bundler"
35
+ spec.add_development_dependency "covered"
36
+ spec.add_development_dependency "simplecov"
37
+ spec.add_development_dependency "minitest"
38
+ spec.add_development_dependency "rack-test"
39
+ spec.add_development_dependency "rake"
40
+ spec.add_development_dependency "standard"
41
+ spec.add_development_dependency "standard-custom"
42
+ spec.add_development_dependency "standard-performance"
43
+ spec.add_development_dependency "m"
44
+ end
@@ -0,0 +1 @@
1
+ HTTP_METHODS = %i[GET POST PUT PATCH DELETE HEAD OPTIONS].freeze
@@ -1,44 +1,86 @@
1
- # frozen_string_literal: true
1
+ class Lennarb
2
+ class Request < Rack::Request
3
+ # The environment variables of the request
4
+ #
5
+ # @returns [Hash]
6
+ #
7
+ attr_reader :env
2
8
 
3
- # Released under the MIT License.
4
- # Copyright, 2023-2024, by Aristóteles Coutinho.
9
+ # Initialize the request object
10
+ #
11
+ # @parameter [Hash] env
12
+ # @parameter [Hash] route_params
13
+ #
14
+ # @returns [Request]
15
+ #
16
+ def initialize(env, route_params = {})
17
+ super(env)
18
+ @route_params = route_params
19
+ end
5
20
 
6
- class Lennarb
7
- class Request < Rack::Request
8
- # Initialize the request object
9
- #
10
- # @parameter [Hash] env
11
- # @parameter [Hash] route_params
12
- #
13
- # @returns [Request]
14
- #
15
- def initialize(env, route_params = {})
16
- super(env)
17
- @route_params = route_params
18
- end
19
-
20
- # Get the request body
21
- #
22
- # @returns [String]
23
- #
24
- def params
25
- @params ||= super.merge(@route_params)
26
- end
27
-
28
- # Read the body of the request
29
- #
30
- # @returns [String]
31
- #
32
- def body = @body ||= super.read
33
-
34
- private
35
-
36
- # Get the query string parameters
37
- #
38
- # @returns [String]
39
- #
40
- def query_params
41
- @query_params ||= Rack::Utils.parse_nested_query(query_string)
42
- end
43
- end
21
+ # Get the request body
22
+ #
23
+ # @returns [String]
24
+ #
25
+ def params = @params ||= super.merge(@route_params)&.transform_keys(&:to_sym)
26
+
27
+ # Get the request path
28
+ #
29
+ # @returns [String]
30
+ #
31
+ def path = @path ||= super.split("?").first
32
+
33
+ # Read the body of the request
34
+ #
35
+ # @returns [String]
36
+ #
37
+ def body = @body ||= super.read
38
+
39
+ # Get the query parameters
40
+ #
41
+ # @returns [Hash]
42
+ #
43
+ def query_params
44
+ @query_params ||= Rack::Utils.parse_nested_query(query_string).transform_keys(&:to_sym)
45
+ end
46
+
47
+ # Get the headers of the request
48
+ #
49
+ def headers
50
+ @headers ||= env.select { |key, _| key.start_with?("HTTP_") }
51
+ end
52
+
53
+ def ip = ip_address
54
+
55
+ def secure? = scheme == "https"
56
+
57
+ def user_agent = headers["HTTP_USER_AGENT"]
58
+
59
+ def accept = headers["HTTP_ACCEPT"]
60
+
61
+ def referer = headers["HTTP_REFERER"]
62
+
63
+ def host = headers["HTTP_HOST"]
64
+
65
+ def content_length = headers["HTTP_CONTENT_LENGTH"]
66
+
67
+ def content_type = headers["HTTP_CONTENT_TYPE"]
68
+
69
+ def xhr? = headers["HTTP_X_REQUESTED_WITH"]&.casecmp("XMLHttpRequest")&.zero?
70
+
71
+ def []=(key, value)
72
+ env[key] = value
73
+ end
74
+
75
+ def [](key)
76
+ env[key]
77
+ end
78
+
79
+ private
80
+
81
+ def ip_address
82
+ forwarded_for = headers["HTTP_X_FORWARDED_FOR"]
83
+ forwarded_for ? forwarded_for.split(",").first.strip : env["REMOTE_ADDR"]
84
+ end
85
+ end
44
86
  end