lennarb 1.3.0 → 1.4.1

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,215 @@
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
+ MyApp = Lennarb::App.new do
31
+ routes
32
+ get '/' do |req, res|
33
+ res.status = 200
34
+ res.html('<h1>Welcome to Lennarb!</h1>')
35
+ end
36
+ end
37
+ end
38
+
39
+ MyApp.initialize!
40
+ run App
41
+ ```
42
+
43
+ Start the server:
44
+
45
+ ```bash
46
+ rackup
47
+ ```
48
+
49
+ Your application will be available at `http://localhost:9292`.
50
+
51
+ ## Core Concepts
52
+
53
+ ### Request Handling
54
+
55
+ Each route handler receives two arguments:
56
+
57
+ - `req`: A Request object wrapping the Rack environment
58
+ - `res`: A Response object for building the HTTP response
59
+
60
+ ### Response Types
61
+
62
+ Lennarb provides three main response helpers:
63
+
64
+ ```rb
65
+ app.get '/text' do |req, res|
66
+ res.text('Plain text response')
67
+ end
68
+
69
+ app.get '/html' do |req, res|
70
+ res.html('<h1>HTML response</h1>')
71
+ end
72
+
73
+ app.get '/json' do |req, res|
74
+ res.json('{"message": "JSON response"}')
75
+ end
76
+ ```
77
+
78
+ ### Redirects
79
+
80
+ ```ruby
81
+ app.get '/redirect' do |req, res|
82
+ res.redirect('/new-location', 302) # 302
83
+ end
84
+ ```
85
+
86
+ Routes are defined using HTTP method helpers:
87
+
88
+ ```ruby
89
+ Lennarb::App.new do
90
+ routes do
91
+ get '/' do |req, res|
92
+ res.html('Home page')
93
+ end
94
+
95
+ get '/users/:id' do |req, res|
96
+ user_id = req.params[:id]
97
+ res.json("{\"id\": #{user_id}}")
98
+ end
99
+ end
100
+ end
101
+ ```
102
+
103
+ ### Route Parameters
104
+
105
+ Parameters from dynamic route segments are available in `req.params`:
106
+
107
+ ```ruby
108
+ get '/hello/:name' do |req, res|
109
+ name = req.params[:name]
110
+ res.text("Hello, #{name}!")
111
+ end
112
+ ```
113
+
114
+ ## Thread Safety
115
+
116
+ Lennarb is thread-safe by design:
117
+
118
+ - All request processing is synchronized using a mutex
119
+ - The router tree is frozen after initialization
120
+ - Response objects are created per-request
121
+
122
+ ## Application Life-cycle
123
+
124
+ ### Initialization
125
+
126
+ ```ruby
127
+ Myapp = Lennarb::App.new do
128
+ # Define routes
129
+ routes do
130
+ end
131
+
132
+ # Define Configurations
133
+ config do
134
+ end
135
+ end
136
+
137
+ # Initialize and freeze the application
138
+ MyApp.initialize!
139
+ ```
140
+
141
+ The `initialize!` method:
142
+
143
+ - Loads environment-specific dependencies
144
+ - Freezes the route tree
145
+ - Freezes the Rack application
146
+
147
+ ### Environment
148
+
149
+ Lennarb uses the `LENNA_ENV` environment variable (defaults to "development"):
150
+
151
+ It can be set using the following environment variables:
152
+
153
+ - `LENNA_ENV`
154
+ - `APP_ENV`
155
+ - `RACK_ENV`
156
+
157
+ ```bash
158
+ LENNA_ENV=production rackup
159
+ ```
160
+
161
+ ## Error Handling
162
+
163
+ Lennarb provides basic error handling:
164
+
165
+ ```ruby
166
+ get '/api' do |req, res|
167
+ # Errors are caught and return 500 with error message
168
+ raise "Something went wrong"
169
+ end
170
+ ```
171
+
172
+ Default error responses:
173
+
174
+ - 404 for unmatched routes
175
+ - 500 for application errors
176
+
177
+ ## Best Practices
178
+
179
+ 1. **Always call initialize!**
180
+
181
+ ```ruby
182
+ app = Lennarb::App.new
183
+ app.initialize!
184
+ run app
185
+ ```
186
+
187
+ 2. **Set response status**
188
+
189
+ ```ruby
190
+ get '/api' do |req, res|
191
+ res.status = 200
192
+ res.json('{"status": "ok"}')
193
+ end
194
+ ```
195
+
196
+ 3. **Use appropriate response types**
197
+
198
+ ```ruby
199
+ # HTML for web pages
200
+ res.html('<h1>Web Page</h1>')
201
+
202
+ # JSON for APIs
203
+ res.json('{"data": "value"}')
204
+
205
+ # Text for simple responses
206
+ res.text('Hello')
207
+ ```
208
+
209
+ ## Support
210
+
211
+ For help and bug reports, please visit:
212
+
213
+ - GitHub Issues: [lennarb/issues](https://github.com/aristotelesbr/lennarb/issues)
214
+
215
+ 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
+ get '/' do |req, res|
14
+ res.html 'Hello World'
15
+ end
16
+ ```
17
+
18
+ ## Content Types
19
+
20
+ Lennarb supports the following content types:
21
+
22
+ ```ruby
23
+ # app.rb
24
+
25
+ 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
+ get '/' do |req, res|
47
+ res.write 'Hello World'
48
+ end
49
+ ```
50
+
51
+ JSON example:
52
+
53
+ ```ruby
54
+ # app.rb
55
+
56
+ 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
+ get '/' do |req, res|
80
+ # Stuff code here...
81
+ res.redirect '/hello'
82
+ end
83
+ ```
data/lennarb.gemspec ADDED
@@ -0,0 +1,47 @@
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_dependency "superconfig"
35
+ spec.add_development_dependency "bundler"
36
+ spec.add_development_dependency "covered"
37
+ spec.add_development_dependency "simplecov"
38
+ spec.add_development_dependency "minitest"
39
+ spec.add_development_dependency "minitest-utils"
40
+ spec.add_development_dependency "rack-test"
41
+ spec.add_development_dependency "rake"
42
+ spec.add_development_dependency "standard"
43
+ spec.add_development_dependency "standard-custom"
44
+ spec.add_development_dependency "standard-performance"
45
+ spec.add_development_dependency "m"
46
+ spec.add_development_dependency "debug"
47
+ end
@@ -0,0 +1,172 @@
1
+ module Lennarb
2
+ class App
3
+ # This error is raised whenever the app is initialized more than once.
4
+ AlreadyInitializedError = Class.new(StandardError)
5
+
6
+ # The root app directory of the app.
7
+ #
8
+ # @returns [Pathname]
9
+ #
10
+ attr_accessor :root
11
+
12
+ # The current environment. Defaults to "development".
13
+ # It can be set using the following environment variables:
14
+ #
15
+ # - `LENNA_ENV`
16
+ # - `APP_ENV`
17
+ # - `RACK_ENV`
18
+ #
19
+ # @returns [Lennarb::Environment]
20
+ #
21
+ attr_reader :env
22
+
23
+ def initialize(&)
24
+ @initialized = false
25
+ self.root = Pathname.pwd
26
+ self.env = compute_env
27
+ instance_eval(&) if block_given?
28
+ end
29
+
30
+ # Set the current environment. See {Lennarb::Environment} for more details.
31
+ #
32
+ # @parameter [Hash] env
33
+ #
34
+ def env=(env)
35
+ raise AlreadyInitializedError if initialized?
36
+
37
+ @env = Environment.new(env)
38
+ end
39
+
40
+ # Mount an app at a specific path.
41
+ #
42
+ # @parameter [Object] The controller|app to mount.
43
+ #
44
+ # @returns [void]
45
+ #
46
+ # @example
47
+ #
48
+ # class PostController
49
+ # extend Lennarb::Routes::Mixin
50
+ #
51
+ # get "/post/:id" do |req, res|
52
+ # res.text("Post ##{req.params[:id]}")
53
+ # end
54
+ # end
55
+ #
56
+ # MyApp = Lennarb::App.new do
57
+ # routes do
58
+ # mount PostController
59
+ # end
60
+ #
61
+ def mount(*controllers)
62
+ controllers.each do |controller|
63
+ raise ArgumentError, "Controller must respond to :routes" unless controller.respond_to?(:routes)
64
+
65
+ self.controllers << controller
66
+ end
67
+ end
68
+
69
+ # Define the app's configuration. See {Lennarb::Config}.
70
+ #
71
+ # @returns [Lennarb::Config]
72
+ #
73
+ # @example Run config on every environment
74
+ # app.config do
75
+ # mandatory :database_url, string
76
+ # end
77
+ #
78
+ # @example Run config on every a specific environment
79
+ # app.config :development do
80
+ # set :domain, "example.dev"
81
+ # end
82
+ #
83
+ # @example Run config on every a specific environment
84
+ # app.config :development, :test do
85
+ # set :domain, "example.dev"
86
+ # end
87
+ #
88
+ def config(*envs, &)
89
+ @config ||= Config.new
90
+
91
+ write = block_given? &&
92
+ (envs.map(&:to_sym).include?(env.to_sym) || envs.empty?)
93
+
94
+ @config.instance_eval(&) if write
95
+
96
+ @config
97
+ end
98
+
99
+ # Define the app's route. See {Lennarb::RouteNode} for more details.
100
+ #
101
+ # @returns [Lennarb::RouteNode]
102
+ #
103
+ def routes(&)
104
+ @routes ||= Routes.new
105
+ @routes.instance_eval(&) if block_given?
106
+ @routes
107
+ end
108
+
109
+ # The Rack app.
110
+ #
111
+ def app
112
+ @app ||= begin
113
+ request_handler = RequestHandler.new(self)
114
+
115
+ Rack::Builder.app do
116
+ run request_handler
117
+ end
118
+ end
119
+ end
120
+
121
+ # Store mounted app's
122
+ #
123
+ def controllers
124
+ @controllers ||= []
125
+ end
126
+ alias_method :mounted_apps, :controllers
127
+
128
+ # Check if the app is initialized.
129
+ #
130
+ # @returns [Boolean]
131
+ #
132
+ def initialized? = @initialized
133
+
134
+ # Initialize the app.
135
+ #
136
+ # @returns [void]
137
+ #
138
+ def initialize!
139
+ raise AlreadyInitializedError if initialized?
140
+
141
+ controllers.each do
142
+ routes.store.merge!(it.routes.store)
143
+ end
144
+
145
+ @initialized = true
146
+
147
+ app.freeze
148
+ routes.freeze
149
+ end
150
+
151
+ # Call the app.
152
+ #
153
+ # @parameter [Hash] env
154
+ #
155
+ def call(env)
156
+ env[RACK_LENNA_APP] = self
157
+ Dir.chdir(root) { return app.call(env) }
158
+ end
159
+
160
+ # Compute the current environment.
161
+ #
162
+ # @returns [String]
163
+ #
164
+ # @private
165
+ #
166
+ private def compute_env
167
+ env = ENV_NAMES.map { ENV[_1] }.compact.first.to_s
168
+
169
+ env.empty? ? "development" : env
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,34 @@
1
+ module Lennarb
2
+ # The configuration for the application.
3
+ # It uses {https://rubygems.org/gems/superconfig SuperConfig} to define the
4
+ # configuration.
5
+ class Config < SuperConfig::Base
6
+ MissingEnvironmentVariable = Class.new(StandardError)
7
+ MissingCallable = Class.new(StandardError)
8
+
9
+ undef_method :credential
10
+
11
+ def initialize(**)
12
+ block = proc { true }
13
+ super(**, &block)
14
+ end
15
+
16
+ # @private
17
+ def to_s = "#<Lennarb::Config>"
18
+
19
+ # @private
20
+ def mandatory(*, **)
21
+ super
22
+ rescue SuperConfig::MissingEnvironmentVariable => error
23
+ raise MissingEnvironmentVariable, error.message
24
+ end
25
+
26
+ # @private
27
+ def property(*, **, &)
28
+ super
29
+ rescue SuperConfig::MissingCallable
30
+ raise MissingCallable,
31
+ "arg[1] must respond to #call or a block must be provided"
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,6 @@
1
+ module Lennarb
2
+ RACK_LENNA_APP = "lennarb.app"
3
+ ENV_NAMES = %w[LENNA_ENV APP_ENV RACK_ENV]
4
+ HTTP_METHODS = %i[GET POST PUT PATCH DELETE HEAD OPTIONS]
5
+ CONTENT_TYPE = {HTML: "text/html", TEXT: "text/plain", JSON: "application/json"}
6
+ end