lennarb 1.5.0 → 1.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b091cfa5731b3dc0b46ecfa6712b9e3ceeae0eedcce9bc97133683162e12ce28
4
- data.tar.gz: 49a8957ed5b951e9e9b7d728fb72c9675e1c6d5eb22a897f2d305e106f02dd98
3
+ metadata.gz: 0b33c13f9c42ddb2d9593374c25fd6efca486c37b8cba0699031e01c61b2f408
4
+ data.tar.gz: 9e995281d4c034b933d62fc24e480ea160ee485b566f3bde0c648272d25ece5d
5
5
  SHA512:
6
- metadata.gz: f494fd50eefbe648c73be463054f14995af8d771d593571ca3908aaa8341502433e925d6df74e99c8427a1cad5473a28495d10e45fe4e54e18a10efd1b22edd9
7
- data.tar.gz: 4e858da099b1bdb88ad3593aa8354da285a4e33b4c59545dfb394ea84c6623f4d163c5500143fb86accb76356a1c35b67e9972d0a2667fd925dd79b6852e963c
6
+ metadata.gz: 95b23a6fa14cf0f55ccd91f75c2c094b97c9929652c099cb347a8996ba30b90cbc9da0d074707af14f7bec57d9c1420f8ed0256e3dcbaa1fee7984ed8ea2f268
7
+ data.tar.gz: ebb7666065e53c69dee117bb9167f22bf0de2e79ea06a4018240ca991edf1c38ff582921c09d523df48c790e75e543aa3d62e64bcaac8225a1660f24d58dd467
data/.gitignore CHANGED
@@ -18,4 +18,4 @@ coverage/
18
18
  tmp/
19
19
  !tmp/.keep
20
20
  .yardo/
21
- minitestfailures
21
+ .minitestfailures
data/README.pt-BR.md CHANGED
@@ -33,14 +33,14 @@ Um framework web leve, rápido e modular para Ruby baseado em Rack. **Lennarb**
33
33
  - Opções de configuração flexíveis
34
34
  - Duas opções de implementação:
35
35
  - `Lennarb::App`: Abordagem minimalista para controle completo
36
- - `Lennarb::Application`: Versão estendida com componentes comuns
36
+ - `Lennarb::Base`: Versão estendida, para montar várias aplicações
37
37
 
38
38
  ## Opções de Implementação
39
39
 
40
40
  Lennarb oferece duas abordagens de implementação para atender diferentes necessidades:
41
41
 
42
42
  - **Lennarb::App**: Abordagem minimalista para controle completo
43
- - **Lennarb::Application**: Versão estendida com componentes comuns
43
+ - **Lennarb::Base**: Versão estendida, para montar várias aplicações
44
44
 
45
45
  Consulte a [documentação](https://aristotelesbr.github.io/lennarb/guides/getting-started/index) para detalhes sobre cada implementação.
46
46
 
@@ -63,26 +63,17 @@ gem install lennarb
63
63
  ```ruby
64
64
  require "lennarb"
65
65
 
66
- app = Lennarb::App.new do
67
- configure do
68
- mandary :database_url, string
69
- optional :port, integer, 9292
70
- optional :env, string, "development"
66
+ class App < Lennarb::App
67
+ config do
68
+ mandatory :database_url, string
69
+ optional :port, int, 9292
71
70
  end
72
71
 
73
- routes do
74
- get("/") do |req, res|
75
- res.html("<h1>Bem-vindo ao Lennarb!</h1>")
76
- end
77
-
78
- get("/hello/:name") do |req, res|
79
- name = req.params[:name]
80
- res.html("Olá, #{name}!")
81
- end
82
- end
72
+ get("/") { |req, res| res.html("<h1>Bem-vindo ao Lennarb!</h1>") }
73
+ get("/hello/:name") { |req, res| res.html("Olá, #{req.params[:name]}!") }
83
74
  end
84
75
 
85
- app.initialize!
76
+ run App.new.initialize!
86
77
  run app # Em config.ru
87
78
  ```
88
79
 
@@ -0,0 +1,62 @@
1
+ # Measures the per-request cost of the Lennarb request path.
2
+ #
3
+ # Run with: bundle exec ruby benchmark/hot_path.rb
4
+ #
5
+ # Reports requests per second and objects allocated per request for a static
6
+ # and a dynamic route, plus the isolated cost of route matching and context
7
+ # creation, so a regression can be attributed rather than guessed at.
8
+
9
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
10
+
11
+ require "lennarb"
12
+ require "benchmark"
13
+ require "stringio"
14
+
15
+ class BenchApp < Lennarb::App
16
+ get("/") { |req, res| res.text("ok") }
17
+ get("/users/:id/posts/:post_id") { |req, res| res.text(req.params[:id]) }
18
+ end
19
+
20
+ def rack_env(path)
21
+ {
22
+ "REQUEST_METHOD" => "GET",
23
+ "PATH_INFO" => path,
24
+ "QUERY_STRING" => "",
25
+ "SERVER_NAME" => "example.org",
26
+ "SERVER_PORT" => "80",
27
+ "rack.url_scheme" => "http",
28
+ "rack.input" => StringIO.new
29
+ }
30
+ end
31
+
32
+ def measure(label, iterations)
33
+ elapsed = Benchmark.realtime { iterations.times { yield } }
34
+ puts format("%-22s %9d req/s %6.2f us/req", label, iterations / elapsed, elapsed / iterations * 1e6)
35
+ end
36
+
37
+ def allocations(iterations)
38
+ GC.start
39
+ before = GC.stat(:total_allocated_objects)
40
+ iterations.times { yield }
41
+ (GC.stat(:total_allocated_objects) - before) / iterations.to_f
42
+ end
43
+
44
+ app = BenchApp.new.initialize!
45
+ handler = Lennarb::RequestHandler.new(app)
46
+
47
+ ITERATIONS = 100_000
48
+ static = rack_env("/")
49
+ dynamic = rack_env("/users/42/posts/7")
50
+
51
+ 2_000.times { handler.call(static.dup) }
52
+
53
+ puts "ruby #{RUBY_VERSION} (#{RUBY_PLATFORM}), lennarb #{Lennarb::VERSION}"
54
+ puts
55
+
56
+ measure("static route", ITERATIONS) { handler.call(static.dup) }
57
+ measure("dynamic route", ITERATIONS) { handler.call(dynamic.dup) }
58
+ measure("match_route only", ITERATIONS) { app.routes.match_route(["users", "42", "posts", "7"], :GET) }
59
+ measure("create_context only", ITERATIONS) { handler.send(:create_context) }
60
+
61
+ puts
62
+ puts format("objects per request %6.1f", allocations(10_000) { handler.call(dynamic.dup) })
data/changelog.md CHANGED
@@ -7,11 +7,146 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.5.1] - 2026-09-09
11
+
12
+ A patch release. No public API was added; everything here is a defect fix.
13
+
14
+ ### Fixed
15
+
16
+ - `Lennarb::Request#content_type` and `#content_length` now read the Rack
17
+ `CONTENT_TYPE` and `CONTENT_LENGTH` headers instead of the `HTTP_`-prefixed
18
+ names. `Request#json?` was never true and `#json_body` never parsed on a real
19
+ HTTP request.
20
+ - Exceptions raised inside a route handler no longer escape to Rack. They are
21
+ logged and answered with a 500, except in development, where they are
22
+ re-raised so `Rack::ShowExceptions` can render the backtrace.
23
+ - `app` inside a route handler no longer raises `SystemStackError`. The context
24
+ object defined `app` with a block whose `self` was rebound to the context, so
25
+ the call recursed into itself. Nothing had exercised it.
26
+ - `App#initialize!` no longer freezes the class-level routes. Each booted
27
+ instance holds its own deep, frozen snapshot, so registering a route after the
28
+ first boot works again -- it previously raised `RoutesFrozenError` and broke
29
+ test suites and development reload.
30
+ - `Routes#freeze` now freezes the whole route tree rather than only its root.
31
+ - Environment-scoped configuration at the class level -- `config(:production) do
32
+ ... end` inside a `Lennarb::App` subclass -- no longer raises `NameError`. The
33
+ class-level `config` referenced an `env` that only exists on instances, so only
34
+ the unscoped form worked. The instance and `Lennarb::Base` forms were
35
+ unaffected.
36
+ - The test suite runs green again. minitest 6 extracted `Minitest::Mock` and
37
+ `Object#stub` into the separate `minitest-mock` gem, which is now a
38
+ development dependency. Both are pinned to their major, so a future major
39
+ bump has to be a deliberate change rather than the result of a fresh resolve.
40
+ - Tests no longer leak `LENNA_ENV`/`APP_ENV`/`RACK_ENV` between each other,
41
+ which made results depend on minitest's random seed.
42
+ - `Request#host` no longer shadows `Rack::Request#host` with a worse version. It
43
+ returned the raw `Host` header, so it kept the port (`example.com:3000`) and
44
+ returned `nil` when there was no `Host` header instead of falling back to
45
+ `SERVER_NAME`. The override is removed and Rack's implementation applies.
46
+ - `Lennarb::Environment` equality is fixed in three ways. Two environments with
47
+ the same name were not `==` to each other; `equal?` was aliased to `==`, which
48
+ broke Ruby's object-identity contract in both directions (`env.equal?(env)` was
49
+ false and `env.equal?(:test)` was true); and `eql?` was inconsistent with
50
+ `hash`, so an environment did not work as a Hash key. `equal?` is no longer
51
+ overridden, and `eql?`/`hash` are now consistent.
52
+ - 22 YARD `@retrn` typos corrected to `@return` in `route_node.rb`,
53
+ `middleware_stack.rb`, `environment.rb` and `response.rb`. The tag was not
54
+ recognised, so those return types were missing from the published
55
+ documentation.
56
+ - The comments on `DuplicateRouteError`, `MissingEnvironmentVariable`,
57
+ `MissingCallable` and `RoutesFrozenError` all claimed the error was raised
58
+ when the app is initialized more than once. Each now describes what it is.
59
+ - **Route parameters are now URL-decoded.** `/u/John%20Doe` used to yield
60
+ `"John%20Doe"`; it now yields `"John Doe"`, and a percent-encoded slash stays
61
+ inside its segment instead of splitting the path. **If your application worked
62
+ around this by decoding route parameters itself, remove that workaround or you
63
+ will decode twice.** Segments are decoded with `Rack::Utils.unescape_path`, so
64
+ `+` is left alone, as it should be in a path.
65
+ - `.gitignore` now matches `.minitestfailures`.
66
+
67
+ ### Security
68
+
69
+ - **`ParameterFilter` matching is now case-insensitive.** It was built with a
70
+ case-sensitive `Regexp.union`, and `RequestLogger` is in the default
71
+ middleware stack in every environment and logs `request.params` at `info`. A
72
+ form field named `Password`, `Token` or `API_KEY` was written to the log in
73
+ cleartext, and from there to journald, CloudWatch or Datadog.
74
+ - **`Regexp` filters passed to `ParameterFilter` now work.** The docstring
75
+ documented them, but `filters.map(&:to_s)` stringified them and
76
+ `Regexp.union` escaped the result, so the filter matched only the literal text
77
+ `(?i-mx:password)`. Passing a Regexp to harden filtering disabled it entirely,
78
+ including for keys that had previously been filtered.
79
+ - **The default filter list now covers** `auth`, `credit`, `card_number`, `cvn`,
80
+ `iban`, `api`, `pin` and `session_id`. Previously `cvv` was filtered while
81
+ `card_number` was not, protecting the CVV and not the card number it guards.
82
+ - **`ParameterFilter#filter` no longer modifies the parameters it is given.**
83
+ `params.dup` is shallow and nested values were assigned in place, so an app
84
+ calling `filter(req.params)` for an error report found its own params replaced
85
+ by `"[FILTERED]"`.
86
+ - **`RequestLogger` now uses the logger configured on the app handling the
87
+ request**, resolved from the Rack env. It read `Lennarb::App.app.config.logger`,
88
+ and `App.app` is never assigned anywhere, so a configured logger was
89
+ unreachable and request lines always went to the process's stderr. Configuring
90
+ a redacting or file-scoped logger was therefore not an available mitigation.
91
+ - **The request path is escaped before being logged.** Control characters from
92
+ the client could otherwise forge log lines. Parameter values were already safe
93
+ because they go through `inspect`.
94
+ - **Booting without `LENNA_ENV`, `APP_ENV` or `RACK_ENV` now logs a warning.**
95
+ The environment defaults to `development`, which enables
96
+ `Rack::ShowExceptions`; a deployment that forgot to export the variable served
97
+ the entire Rack environment, `Authorization` and `Cookie` included, to anyone
98
+ who could trigger a 500. Changing the default itself is a breaking change and
99
+ is deferred to 1.6.0.
100
+ - **`Response#json` no longer echoes the exception message to the client**, which
101
+ could carry `inspect` output of the object being serialized, and now rescues
102
+ `JSON::JSONError` rather than `JSON::GeneratorError`: a circular or over-deep
103
+ object graph raises `JSON::NestingError`, which descends from `ParserError` and
104
+ escaped the rescue entirely.
105
+
106
+ ### Changed
107
+
108
+ - `RequestHandler` compiles the route execution context once per application
109
+ class instead of building an object with a fresh singleton class on every
110
+ request. On ruby 3.4.1 (arm64-darwin23): static route 424,302 to 753,914
111
+ req/s, dynamic route 175,364 to 221,795 req/s, `create_context` 1.37us to
112
+ 0.15us (24% to 3.3% of a request), 48 to 40 objects allocated per request.
113
+ The extra object over the 39 measured mid-release is the cost of decoding
114
+ route parameters, below.
115
+ - `App#routes` returns the instance's frozen snapshot after `initialize!`, so
116
+ `app.routes.equal?(App.routes)` is no longer true once the app is booted.
117
+ - Documentation now teaches subclassing `Lennarb::App` as the canonical form.
118
+ The previous quick start raised `NoMethodError`, and subclassing is the only
119
+ form isolated per application. The pt-BR quick start also called `configure`
120
+ (the method is `config`) and `mandary` (a typo for `mandatory`).
121
+ - Changelog no longer references `Lennarb::Application` or
122
+ `Lennarb::Routes::Mixin`, neither of which exists. The real APIs are
123
+ `Lennarb::Base` and `Lennarb::Base.mount`.
124
+
10
125
  ### Added
11
126
 
12
- - Add `Lennarb::Application` class to be the base class of the "standard" implementation of the Lennarb framework.
127
+ - `benchmark/hot_path.rb`, so the performance claims can be reproduced.
128
+ - `required_ruby_version = ">= 3.4"` in the gemspec. The code uses `it`, the
129
+ implicit block parameter introduced in Ruby 3.4, and the README has always
130
+ promised 3.4+, but no version constraint was declared. RubyGems would install
131
+ the gem on an older Ruby and the first `require` failed with a `SyntaxError`
132
+ rather than a clear resolution error. This has been missing since 0.1.0.
133
+
134
+ ### Known limitations
135
+
136
+ - Two applications created with `Lennarb::App.new` without subclassing still
137
+ share the class's route definitions. Subclass to isolate them.
138
+ - Hooks and helpers are still stored per app class and are shared the same way.
139
+ Tracked in [#87](https://github.com/aristotelesbr/lennarb/issues/87); they are
140
+ meant to become an opt-in mechanism rather than machinery every application
141
+ carries.
142
+
143
+ ## [1.5.0] - 2025-04-19
144
+
145
+ ### Added
146
+
147
+ - Add `Lennarb::Base` class to be the base class of the "standard" implementation of the Lennarb framework, for mounting several applications behind one middleware stack.
13
148
  - Add middleware support to Lennarb::App class.
14
- - Add `middleware` support to the `Lennarb::Application` with default middlewares.
149
+ - Add `middleware` support to `Lennarb::Base` with default middlewares.
15
150
  - Add files to centralize the errors of the project.
16
151
  - Add CODE_OF_CONDUCT.md in English and Portuguese
17
152
  - Add CONTRIBUTING.md in English and Portuguese
@@ -52,20 +187,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
52
187
  - Add support to mount routes. Now, you can centralize the routes in a single file and mount them in the main application. Ex.
53
188
 
54
189
  ```rb
55
- class PostsController
56
- extend Lennarb::Routes::Mixin
57
-
190
+ class Posts < Lennarb::App
58
191
  get '/posts' do |req, res|
59
192
  res.html('Posts')
60
193
  end
61
194
  end
62
195
 
63
- SampleApp = Lennarb.new do |router|
64
- mount PostsController
196
+ class Application < Lennarb::Base
197
+ mount Posts, at: '/'
65
198
  end
66
199
  ```
67
200
 
68
- The `mount` method will add the routes from the `PostsController` class to the main application. You can use the `mount` method with multiple classes, ex. `mount PostsController, CommentsController`.
201
+ `Lennarb::Base.mount` registers a `Lennarb::App` subclass at a path, behind the
202
+ base application's middleware stack. Call it once per application you want to
203
+ mount.
69
204
 
70
205
  - Add `Lennarb::Environment` module to manage the environment variables in the project. Now, the `Lennarb` class is the main class of the project.
71
206
  - Add `Lennarb::Config` module to manage the configuration in the project. Now, the `Lennarb` class is the main class of the project.
@@ -27,17 +27,14 @@ Create a new file named `config.ru`:
27
27
  ```ruby
28
28
  require 'lennarb'
29
29
 
30
- MyApp = Lennarb::App.new do
31
- routes do
32
- get '/' do |req, res|
33
- res.status = 200
34
- res.html('<h1>Welcome to Lennarb!</h1>')
35
- end
30
+ class MyApp < Lennarb::App
31
+ get '/' do |req, res|
32
+ res.status = 200
33
+ res.html('<h1>Welcome to Lennarb!</h1>')
36
34
  end
37
35
  end
38
36
 
39
- MyApp.initialize!
40
- run MyApp
37
+ run MyApp.new.initialize!
41
38
  ```
42
39
 
43
40
  Start the server:
@@ -86,16 +83,13 @@ end
86
83
  Routes are defined using HTTP method helpers:
87
84
 
88
85
  ```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
86
+ class MyApp < Lennarb::App
87
+ get '/' do |req, res|
88
+ res.html('Home page')
89
+ end
90
+
91
+ get '/users/:id' do |req, res|
92
+ res.json({id: req.params[:id]})
99
93
  end
100
94
  end
101
95
  ```
@@ -124,18 +118,19 @@ Lennarb is thread-safe by design:
124
118
  ### Initialization
125
119
 
126
120
  ```ruby
127
- MyApp = Lennarb::App.new do
128
- # Define routes
129
- routes do
121
+ class MyApp < Lennarb::App
122
+ # Define configuration
123
+ config do
130
124
  end
131
125
 
132
- # Define Configurations
133
- config do
126
+ # Define routes
127
+ get '/' do |req, res|
128
+ res.text('ok')
134
129
  end
135
130
  end
136
131
 
137
- # Initialize and freeze the application
138
- MyApp.initialize!
132
+ # Boot the application: it snapshots and freezes its routes
133
+ MyApp.new.initialize!
139
134
  ```
140
135
 
141
136
  The `initialize!` method:
@@ -1,5 +1,9 @@
1
1
  # Performance
2
2
 
3
+ > The comparison tables below were produced in 2024 on a 2013 MacBook Pro with
4
+ > Ruby 3.3.0. They are kept for historical reference. To measure the current
5
+ > request path on your own machine, run `bundle exec ruby benchmark/hot_path.rb`.
6
+
3
7
  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
8
 
5
9
  ## Benchmark results
data/lennarb.gemspec CHANGED
@@ -24,6 +24,11 @@ Gem::Specification.new do |spec|
24
24
  .reject { |f| f.match(%r{^(test|features)/}) }
25
25
  end
26
26
 
27
+ # lib/lennarb/routes.rb uses `it`, the implicit block parameter, which is
28
+ # Ruby 3.4+. Without this, RubyGems installs happily on an older Ruby and the
29
+ # first require fails with a SyntaxError instead of a clear resolution error.
30
+ spec.required_ruby_version = ">= 3.4"
31
+
27
32
  spec.bindir = "exe"
28
33
  spec.executables = ["lenna"]
29
34
  spec.require_paths = ["lib"]
@@ -36,7 +41,12 @@ Gem::Specification.new do |spec|
36
41
  spec.add_development_dependency "bundler"
37
42
  spec.add_development_dependency "simplecov"
38
43
  spec.add_development_dependency "simplecov-json"
39
- spec.add_development_dependency "minitest"
44
+ # Pinned to the major: minitest 6 removed Minitest::Mock and Object#stub, which
45
+ # silently broke CI for five months because nothing pins and gems.locked is
46
+ # gitignored. A major bump must be a deliberate change, not a fresh resolve.
47
+ spec.add_development_dependency "minitest", "~> 6.0"
48
+ # minitest 6 extracted Minitest::Mock and Object#stub into their own gem.
49
+ spec.add_development_dependency "minitest-mock", "~> 5.27"
40
50
  spec.add_development_dependency "minitest-utils"
41
51
  spec.add_development_dependency "rack-test"
42
52
  spec.add_development_dependency "rake"
data/lib/lennarb/app.rb CHANGED
@@ -75,12 +75,24 @@ module Lennarb
75
75
  @config ||= Config.new(self)
76
76
 
77
77
  if block_given?
78
- write = envs.empty? || envs.map(&:to_sym).include?(env.name)
78
+ write = envs.empty? || envs.map(&:to_sym).include?(compute_env_name)
79
79
  @config.instance_eval(&) if write
80
80
  end
81
81
 
82
82
  @config
83
83
  end
84
+
85
+ # The environment name computed from ENV.
86
+ #
87
+ # A class has no env of its own, so environment-scoped config blocks
88
+ # resolve it here.
89
+ #
90
+ # @return [Symbol] Environment name
91
+ # @api private
92
+ private def compute_env_name
93
+ name = ENV_NAMES.map { |var| ENV[var] }.compact.first.to_s
94
+ (name.empty? ? "development" : name).to_sym
95
+ end
84
96
  end
85
97
 
86
98
  # Instance methods
@@ -179,7 +191,9 @@ module Lennarb
179
191
  self.class.instance_exec(&block)
180
192
  end
181
193
 
182
- self.class.routes
194
+ # Before boot, route definitions go to the class. After boot, this
195
+ # instance serves from its own frozen snapshot.
196
+ @routes || self.class.routes
183
197
  end
184
198
 
185
199
  # Get/define configuration
@@ -205,8 +219,15 @@ module Lennarb
205
219
  def initialize!
206
220
  raise AlreadyInitializedError if @initialized
207
221
 
222
+ warn_about_defaulted_env
223
+
224
+ # Snapshot the class routes into this instance and freeze only the copy,
225
+ # so booting an app never freezes process-global class state.
226
+ @routes = Routes.new
227
+ @routes.merge!(self.class.routes)
228
+ @routes.freeze
229
+
208
230
  @initialized = true
209
- routes.freeze
210
231
  self
211
232
  end
212
233
 
@@ -242,6 +263,26 @@ module Lennarb
242
263
  stack
243
264
  end
244
265
 
266
+ # Warn when the environment was defaulted rather than chosen.
267
+ #
268
+ # Defaulting to development is a fail-open: development enables
269
+ # Rack::ShowExceptions, which renders the whole Rack environment -- the
270
+ # Authorization and Cookie headers included -- on any unhandled error. A
271
+ # deployment that simply forgot to export the variable would serve that to
272
+ # anyone able to trigger a 500.
273
+ #
274
+ # @return [void]
275
+ private def warn_about_defaulted_env
276
+ return if ENV_NAMES.any? { |name| ENV[name] }
277
+
278
+ config.logger.warn do
279
+ "No #{ENV_NAMES.join(", ")} is set, so the environment defaults to " \
280
+ "development. Rack::ShowExceptions is enabled and will render the " \
281
+ "full Rack environment, including Authorization and Cookie headers, " \
282
+ "on any unhandled error. Set one of these variables in production."
283
+ end
284
+ end
285
+
245
286
  # Compute environment from ENV variables
246
287
  #
247
288
  # @return [String] Environment name
@@ -40,23 +40,39 @@ module Lennarb
40
40
 
41
41
  # Implements equality for the environment.
42
42
  #
43
- def ==(other) = name == other || name.to_s == other
44
- alias_method :eql?, :==
45
- alias_method :equal?, :==
43
+ # Compares by name, so an environment equals its name as a Symbol or a
44
+ # String, and equals another environment with the same name.
45
+ #
46
+ # `equal?` is deliberately not aliased here: in Ruby it means object
47
+ # identity and overriding it broke that contract in both directions.
48
+ #
49
+ def ==(other) = name == other || name.to_s == other.to_s
46
50
  alias_method :===, :==
47
51
 
52
+ # Value equality, kept consistent with {#hash} so an environment behaves as
53
+ # a Hash key.
54
+ #
55
+ # @param other [Object]
56
+ # @return [Boolean]
57
+ #
58
+ def eql?(other) = other.is_a?(Environment) && name == other.name
59
+
60
+ # @return [Integer]
61
+ #
62
+ def hash = name.hash
63
+
48
64
  # Returns the name of the environment as a symbol.
49
- # @retrn [Symbol]
65
+ # @return [Symbol]
50
66
  #
51
67
  def to_sym = name
52
68
 
53
69
  # Returns the name of the environment as a string.
54
- # @retrn [String]
70
+ # @return [String]
55
71
  #
56
72
  def to_s = name.to_s
57
73
 
58
74
  # Returns the name of the environment as a string.
59
- # @retrn [String]
75
+ # @return [String]
60
76
  def inspect = to_s.inspect
61
77
 
62
78
  # Yields a block if the environment is the same as the given environment.
@@ -1,17 +1,19 @@
1
1
  module Lennarb
2
- # Lite implementation of app.
2
+ # Base class for errors raised by Lennarb.
3
3
  #
4
4
  Error = Class.new(StandardError)
5
- # This error is raised whenever the app is initialized more than once.
5
+ # Raised when merging routes would define the same HTTP method twice for the
6
+ # same path.
6
7
  #
7
8
  DuplicateRouteError = Class.new(StandardError)
8
- # This error is raised whenever the app is initialized more than once.
9
+ # Raised when a mandatory configuration value has no environment variable set.
9
10
  #
10
11
  MissingEnvironmentVariable = Class.new(StandardError)
11
- # This error is raised whenever the app is initialized more than once.
12
+ # Raised when a configuration property is declared without something callable
13
+ # to compute it.
12
14
  #
13
15
  MissingCallable = Class.new(StandardError)
14
- # This error is raised whenever the app is initialized more than once.
16
+ # Raised when a route is registered after the routes have been frozen.
15
17
  #
16
18
  RoutesFrozenError = Class.new(RuntimeError)
17
19
  end
@@ -13,8 +13,17 @@ module Lennarb
13
13
  @app = app
14
14
  end
15
15
 
16
- # Get logger from application configuration
17
- def logger = Lennarb::App.app.config.logger
16
+ # Get the logger for this request.
17
+ #
18
+ # Resolved from the app handling the request, which App#call publishes in
19
+ # the Rack env, so an app's configured logger is actually used. Falls back
20
+ # to the class-level default when there is no app in the env.
21
+ #
22
+ # @param [Hash, nil] env Rack environment
23
+ # @return [Object] The logger
24
+ def logger(env = nil)
25
+ env&.[](RACK_LENNA_APP)&.config&.logger || Lennarb::App.app.config.logger
26
+ end
18
27
 
19
28
  # Process the request and log information
20
29
  #
@@ -28,7 +37,7 @@ module Lennarb
28
37
 
29
38
  duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
30
39
 
31
- log_request(request, status, headers, duration)
40
+ log_request(request, status, headers, duration, env)
32
41
 
33
42
  [status, headers, body]
34
43
  end
@@ -52,18 +61,20 @@ module Lennarb
52
61
  end
53
62
 
54
63
  # Log the complete request
55
- def log_request(request, status, headers, duration)
56
- logger.info { request_line(request, duration, status) }
64
+ def log_request(request, status, headers, duration, env = nil)
65
+ log = logger(env)
66
+
67
+ log.info { request_line(request, duration, status) }
57
68
 
58
- logger.info { status_line(status) }
69
+ log.info { status_line(status) }
59
70
 
60
71
  if request.params.any?
61
- logger.info { params_line(request.params) }
72
+ log.info { params_line(request.params) }
62
73
  end
63
74
 
64
- if headers["Location"]
65
- logger.info { redirect_line(headers["Location"]) }
66
- end
75
+ # Rack 3 header names are lowercase; Response#redirect writes "location".
76
+ location = headers["location"]
77
+ log.info { redirect_line(location) } if location
67
78
  end
68
79
 
69
80
  # Format the request line
@@ -92,9 +103,16 @@ module Lennarb
92
103
  "Redirect: #{location}".colorize(:yellow)
93
104
  end
94
105
 
95
- # Filter the request path
106
+ # Escape control characters in the request path.
107
+ #
108
+ # The path comes from the client, and an unescaped newline or ANSI escape
109
+ # would let it forge log lines. Parameter values are already safe because
110
+ # they go through #inspect.
111
+ #
112
+ # @param [String] path Request path
113
+ # @return [String] Path safe to write to a log
96
114
  def filter_path(path)
97
- path
115
+ path.to_s.gsub(/[[:cntrl:]]/) { |char| format("\\x%02X", char.ord) }
98
116
  end
99
117
 
100
118
  # Filter request parameters
@@ -19,7 +19,7 @@ module Lennarb
19
19
  # @param [Array] args
20
20
  # @param [Proc] block
21
21
  #
22
- # @retrn [void]
22
+ # @return [void]
23
23
  #
24
24
  def use(middleware, *args, &block)
25
25
  @store << [middleware, args, block]
@@ -31,7 +31,7 @@ module Lennarb
31
31
  # @param [Array] args
32
32
  # @param [Proc] block
33
33
  #
34
- # @retrn [void]
34
+ # @return [void]
35
35
  #
36
36
  def unshift(middleware, *args, &block)
37
37
  @store.unshift([middleware, args, block])
@@ -39,7 +39,7 @@ module Lennarb
39
39
 
40
40
  # Clear the middleware stack.
41
41
  #
42
- # @retrn [void]
42
+ # @return [void]
43
43
  #
44
44
  def clear
45
45
  @store.clear
@@ -47,7 +47,7 @@ module Lennarb
47
47
 
48
48
  # Convert the middleware stack to an array.
49
49
  #
50
- # @retrn [Array]
50
+ # @return [Array]
51
51
  #
52
52
  def to_a
53
53
  @store
@@ -1,8 +1,8 @@
1
1
  module Lennarb
2
- # Filtra parâmetros sensíveis de logs e exceções.
3
- # Útil para evitar o vazamento de informações confidenciais.
2
+ # Filters sensitive parameters from logs and exceptions.
3
+ # Useful for preventing the leakage of confidential information.
4
4
  #
5
- # Por padrão, as seguintes chaves de parâmetros são filtradas:
5
+ # By default, the following parameter keys are filtered:
6
6
  #
7
7
  # - `passw`
8
8
  # - `email`
@@ -17,6 +17,16 @@ module Lennarb
17
17
  # - `cvv`
18
18
  # - `cvc`
19
19
  # - `signature`
20
+ # - `auth`
21
+ # - `credit`
22
+ # - `card_number`
23
+ # - `cvn`
24
+ # - `iban`
25
+ # - `api`
26
+ # - `pin`
27
+ # - `session_id`
28
+ #
29
+ # Matching is case-insensitive, so `Password` and `API_KEY` are filtered too.
20
30
  #
21
31
  # @example
22
32
  # filter = Lennarb::ParameterFilter.new
@@ -30,47 +40,50 @@ module Lennarb
30
40
  # @api private
31
41
  DEFAULT_FILTERS = %w[
32
42
  passw email secret token _key crypt salt certificate otp ssn cvv cvc
33
- signature
43
+ signature auth credit card_number cvn iban api pin session_id
34
44
  ].freeze
35
45
 
36
- # Inicializa um novo filtro de parâmetros
46
+ # Initialize a new parameter filter
47
+ #
48
+ # Regexp filters are used as given. Anything else is matched as a literal
49
+ # substring of the key, case-insensitively.
37
50
  #
38
- # @param [Array<String, Regexp>] filters Lista de padrões para filtrar
51
+ # @param [Array<String, Symbol, Regexp>] filters List of patterns to filter
39
52
  def initialize(filters = DEFAULT_FILTERS)
40
- @filter = Regexp.union(filters.map(&:to_s))
53
+ union = Regexp.union(filters.map { |pattern| pattern.is_a?(Regexp) ? pattern : pattern.to_s })
54
+ @filter = Regexp.new(union.source, Regexp::IGNORECASE)
41
55
  end
42
56
 
43
- # Filtra os parâmetros conforme o filtro configurado
57
+ # Filter parameters according to the configured filter
44
58
  #
45
- # @param [Hash, Array] params Parâmetros a serem filtrados
46
- # @param [String] mask Valor que substituirá os parâmetros filtrados
47
- # @return [Hash, Array] Parâmetros filtrados
59
+ # @param [Hash, Array] params Parameters to be filtered
60
+ # @param [String] mask Value that will replace filtered parameters
61
+ # @return [Hash, Array] Filtered parameters
48
62
  def filter(params, mask: DEFAULT_MASK)
49
- filter_object(params.dup, mask)
63
+ filter_object(params, mask)
50
64
  end
51
65
 
52
66
  private
53
67
 
54
- # Filtra recursivamente um objeto (hash ou array)
68
+ # Recursively filter an object (hash or array).
55
69
  #
56
- # @param [Object] object Objeto a ser filtrado
57
- # @param [String] mask Valor que substituirá os parâmetros filtrados
58
- # @return [Object] Objeto filtrado
70
+ # Builds new containers rather than writing into the ones it was given, so
71
+ # the caller's parameters are never modified.
72
+ #
73
+ # @param [Object] object Object to be filtered
74
+ # @param [String] mask Value that will replace filtered parameters
75
+ # @return [Object] Filtered object
59
76
  def filter_object(object, mask)
60
77
  case object
61
78
  when Hash
62
- object.each do |key, value|
63
- object[key] = if key.to_s.match?(@filter)
64
- mask
65
- else
66
- filter_object(value, mask)
67
- end
79
+ object.each_with_object({}) do |(key, value), result|
80
+ result[key] = key.to_s.match?(@filter) ? mask : filter_object(value, mask)
68
81
  end
69
82
  when Array
70
- object = object.map { filter_object(it, mask) }
83
+ object.map { filter_object(it, mask) }
84
+ else
85
+ object
71
86
  end
72
-
73
- object
74
87
  end
75
88
  end
76
89
  end
@@ -125,20 +125,12 @@ module Lennarb
125
125
  env["HTTP_REFERER"]
126
126
  end
127
127
 
128
- # Get the host header
129
- #
130
- # @return [String, nil]
131
- #
132
- def host
133
- env["HTTP_HOST"]
134
- end
135
-
136
128
  # Get the content length header
137
129
  #
138
130
  # @return [String, nil]
139
131
  #
140
132
  def content_length
141
- env["HTTP_CONTENT_LENGTH"]
133
+ env["CONTENT_LENGTH"]
142
134
  end
143
135
 
144
136
  # Get the content type header
@@ -146,7 +138,7 @@ module Lennarb
146
138
  # @return [String, nil]
147
139
  #
148
140
  def content_type
149
- env["HTTP_CONTENT_TYPE"]
141
+ env["CONTENT_TYPE"]
150
142
  end
151
143
 
152
144
  # Check if the request is an XHR request
@@ -17,7 +17,7 @@ module Lennarb
17
17
  # @return [Array] Rack response [status, headers, body]
18
18
  def call(env)
19
19
  http_method = env[Rack::REQUEST_METHOD].to_sym
20
- parts = env[Rack::PATH_INFO].split("/").reject(&:empty?)
20
+ parts = split_path(env[Rack::PATH_INFO])
21
21
  block, params = app.routes.match_route(parts, http_method)
22
22
 
23
23
  return [404, {"content-type" => CONTENT_TYPE[:TEXT]}, ["Not Found"]] unless block
@@ -35,27 +35,62 @@ module Lennarb
35
35
  Hooks.execute(context, app.class, :after, req, res)
36
36
 
37
37
  res.finish
38
- rescue Lennarb::Error => e
38
+ rescue => e
39
+ # In development, let the exception through so Rack::ShowExceptions --
40
+ # already in App#default_middleware_stack -- can render the backtrace.
41
+ raise if app.env.development?
42
+
39
43
  app.class.config.logger.error("Error: #{e.message}")
40
44
  app.class.config.logger.error(e.backtrace.first)
41
- [500, {"content-type" => "text/plain"}, ["Internal Server Error"]]
45
+ [500, {"content-type" => CONTENT_TYPE[:TEXT]}, ["Internal Server Error"]]
42
46
  end
43
47
  end
44
48
 
45
49
  private
46
50
 
47
- # Create a context object with app's helper methods
51
+ # Split a request path into decoded segments.
48
52
  #
49
- # @return [Object] A context object with helper methods
50
- def create_context
51
- context = Object.new
53
+ # Segments are decoded after splitting, so a percent-encoded slash stays
54
+ # inside its segment instead of splitting the path. The escape check keeps
55
+ # the common path free of the decoding cost.
56
+ #
57
+ # @param [String] path The raw PATH_INFO
58
+ # @return [Array<String>] The decoded segments
59
+ def split_path(path)
60
+ parts = path.split("/").reject(&:empty?)
61
+ return parts unless path.include?("%")
52
62
 
53
- context.define_singleton_method(:app) { app }
63
+ parts.map! { |part| Rack::Utils.unescape_path(part) }
64
+ end
65
+
66
+ # The context class for this app, compiled once.
67
+ #
68
+ # Safe to memoize: Helpers.for always returns the same Module object for a
69
+ # given app class, and Ruby's include is live, so helpers defined after the
70
+ # first request still resolve through it.
71
+ #
72
+ # @return [Class] The context class
73
+ def context_class
74
+ @context_class ||= begin
75
+ helpers_module = Helpers.for(app.class)
54
76
 
55
- helpers_module = Helpers.for(app.class)
56
- context.extend(helpers_module) if helpers_module
77
+ Class.new do
78
+ def initialize(app)
79
+ @app = app
80
+ end
57
81
 
58
- context
82
+ attr_reader :app
83
+
84
+ include helpers_module
85
+ end
86
+ end
87
+ end
88
+
89
+ # Create a context object with app's helper methods
90
+ #
91
+ # @return [Object] A context object with helper methods
92
+ def create_context
93
+ context_class.new(app)
59
94
  end
60
95
  end
61
96
  end
@@ -8,17 +8,17 @@ module Lennarb
8
8
  attr_accessor :status
9
9
 
10
10
  # @!attribute [r] body
11
- # @retrn [Array]
11
+ # @return [Array]
12
12
  #
13
13
  attr_reader :body
14
14
 
15
15
  # @!attribute [r] headers
16
- # @retrn [Hash]
16
+ # @return [Hash]
17
17
  #
18
18
  attr_reader :headers
19
19
 
20
20
  # @!attribute [r] length
21
- # @retrn [Integer]
21
+ # @return [Integer]
22
22
  #
23
23
  attr_reader :length
24
24
 
@@ -35,7 +35,7 @@ module Lennarb
35
35
 
36
36
  # Initialize the response object
37
37
  #
38
- # @retrn [Response]
38
+ # @return [Response]
39
39
  #
40
40
  def initialize
41
41
  @status = 200
@@ -48,7 +48,7 @@ module Lennarb
48
48
  #
49
49
  # @param [String] key
50
50
  #
51
- # @retrn [String] value
51
+ # @return [String] value
52
52
  #
53
53
  def [](key)
54
54
  @headers[key]
@@ -59,7 +59,7 @@ module Lennarb
59
59
  # @param [String] key
60
60
  # @param [String] value
61
61
  #
62
- # @retrn [String] value
62
+ # @return [String] value
63
63
  #
64
64
  def []=(key, value)
65
65
  @headers[key] = value
@@ -69,7 +69,7 @@ module Lennarb
69
69
  #
70
70
  # @param [String] str
71
71
  #
72
- # @retrn [String] str
72
+ # @return [String] str
73
73
  #
74
74
  def write(str)
75
75
  str = str.to_s
@@ -82,7 +82,7 @@ module Lennarb
82
82
  #
83
83
  # @param [String] str
84
84
  #
85
- # @retrn [String] str
85
+ # @return [String] str
86
86
  #
87
87
  def text(str)
88
88
  @headers[CONTENT_TYPE] = Lennarb::CONTENT_TYPE[:TEXT]
@@ -93,7 +93,7 @@ module Lennarb
93
93
  #
94
94
  # @param [String] str
95
95
  #
96
- # @retrn [String] str
96
+ # @return [String] str
97
97
  #
98
98
  def html(str)
99
99
  @headers[CONTENT_TYPE] = Lennarb::CONTENT_TYPE[:HTML]
@@ -104,16 +104,22 @@ module Lennarb
104
104
  #
105
105
  # @param [String] str
106
106
  #
107
- # @retrn [String] str
107
+ # @return [String] str
108
108
  #
109
109
  def json(str)
110
110
  json_str = JSON.generate(str)
111
111
  @headers[CONTENT_TYPE] = Lennarb::CONTENT_TYPE[:JSON]
112
112
  write(json_str)
113
- rescue JSON::GeneratorError => e
113
+ # Rescues JSON::JSONError rather than JSON::GeneratorError: a circular or
114
+ # over-deep object graph raises JSON::NestingError, which descends from
115
+ # ParserError, not GeneratorError, and so escaped this rescue entirely.
116
+ #
117
+ # The body is static because the exception message can carry `inspect`
118
+ # output of the object being serialized, which may hold credentials.
119
+ rescue JSON::JSONError
114
120
  @status = 500
115
121
  @headers[CONTENT_TYPE] = Lennarb::CONTENT_TYPE[:TEXT]
116
- write("JSON generation error: #{e.message}")
122
+ write("JSON generation error")
117
123
  end
118
124
 
119
125
  # Redirect the response
@@ -130,7 +136,7 @@ module Lennarb
130
136
 
131
137
  # Finish the response
132
138
  #
133
- # @retrn [Array] response
139
+ # @return [Array] response
134
140
  #
135
141
  def finish
136
142
  [@status, @headers, @body]
@@ -13,7 +13,7 @@ module Lennarb
13
13
 
14
14
  # Initialize the route node.
15
15
  #
16
- # @retrn [RouteNode]
16
+ # @return [RouteNode]
17
17
  #
18
18
  def initialize
19
19
  @blocks = {}
@@ -28,7 +28,7 @@ module Lennarb
28
28
  # @param http_method [String] The HTTP method.
29
29
  # @param block [Proc] The block to be executed when the route is matched.
30
30
  #
31
- # @retrn [void]
31
+ # @return [void]
32
32
  #
33
33
  def add_route(parts, http_method, block)
34
34
  current_node = self
@@ -55,7 +55,7 @@ module Lennarb
55
55
  # @param http_method [String] The HTTP method.
56
56
  # @param params [Hash] The parameters of the route.
57
57
  #
58
- # @retrn [Array<Proc, Hash>]
58
+ # @return [Array<Proc, Hash>]
59
59
  #
60
60
  def match_route(parts, http_method, params: {})
61
61
  if parts.empty?
@@ -85,7 +85,7 @@ module Lennarb
85
85
  #
86
86
  # @param other [RouteNode] The other route node.
87
87
  #
88
- # @retrn [void|DuplicateRouteError]
88
+ # @return [void|DuplicateRouteError]
89
89
  #
90
90
  def merge!(other)
91
91
  other.blocks.each do |http_method, block|
@@ -35,12 +35,28 @@ module Lennarb
35
35
  @store.match_route(parts, http_method)
36
36
  end
37
37
 
38
- # Freeze the routes to prevent further modification
38
+ # Copy the routes from another Routes instance into this one.
39
+ #
40
+ # The copy is deep: node objects are rebuilt rather than shared, so routes
41
+ # registered on the source afterwards cannot leak into this instance.
42
+ #
43
+ # @param other [Routes] The routes to copy from
44
+ # @return [self]
45
+ # @api private
46
+ def merge!(other)
47
+ @store.merge!(deep_copy(other.store))
48
+ self
49
+ end
50
+
51
+ # Freeze the routes to prevent further modification.
52
+ #
53
+ # Freezes the whole tree, not only the root, so a frozen copy really is
54
+ # immutable.
39
55
  #
40
56
  # @return [self] The frozen routes
41
57
  def freeze
42
58
  @frozen = true
43
- @store.freeze
59
+ deep_freeze(@store)
44
60
  self
45
61
  end
46
62
 
@@ -63,5 +79,39 @@ module Lennarb
63
79
  parts = path.split("/").reject(&:empty?)
64
80
  @store.add_route(parts, http_method, block)
65
81
  end
82
+
83
+ # The underlying route tree.
84
+ #
85
+ # Protected so that merge! can reach a sibling instance's store without
86
+ # exposing the tree publicly.
87
+ #
88
+ # @return [RouteNode]
89
+ # @api private
90
+ protected attr_reader :store
91
+
92
+ # Rebuild a route tree, sharing no node objects with the original.
93
+ #
94
+ # @param node [RouteNode] The node to copy
95
+ # @return [RouteNode] The copy
96
+ def deep_copy(node)
97
+ copy = RouteNode.new
98
+ copy.param_key = node.param_key
99
+ copy.blocks = node.blocks.dup
100
+
101
+ node.static_children.each { |part, child| copy.static_children[part] = deep_copy(child) }
102
+ node.dynamic_children.each { |param, child| copy.dynamic_children[param] = deep_copy(child) }
103
+
104
+ copy
105
+ end
106
+
107
+ # Freeze a route tree from the leaves up.
108
+ #
109
+ # @param node [RouteNode] The node to freeze
110
+ # @return [RouteNode] The frozen node
111
+ def deep_freeze(node)
112
+ node.static_children.each_value { deep_freeze(it) }
113
+ node.dynamic_children.each_value { deep_freeze(it) }
114
+ node.freeze
115
+ end
66
116
  end
67
117
  end
@@ -1,3 +1,3 @@
1
1
  module Lennarb # :nodoc:
2
- VERSION = "1.5.0" # :nodoc:
2
+ VERSION = "1.5.1" # :nodoc:
3
3
  end
data/readme.md CHANGED
@@ -63,23 +63,18 @@ Create a simple application with routes:
63
63
  ```ruby
64
64
  require "lennarb"
65
65
 
66
- app = Lennarb::App.new do
67
- routes do
68
- get("/") do |req, res|
69
- res.html("<h1>Welcome to Lennarb!</h1>")
70
- end
71
-
72
- get("/hello/:name") do |req, res|
73
- name = req.params[:name]
74
- res.html("Hello, #{name}!")
75
- end
76
- end
66
+ class App < Lennarb::App
67
+ get("/") { |req, res| res.html("<h1>Welcome to Lennarb!</h1>") }
68
+ get("/hello/:name") { |req, res| res.html("Hello, #{req.params[:name]}!") }
69
+ post("/users") { |req, res| res.json(id: 1, **req.json_body) }
77
70
  end
78
71
 
79
- app.initialize!
80
- run app # In config.ru
72
+ run App.new.initialize! # In config.ru
81
73
  ```
82
74
 
75
+ Subclassing is the canonical form: each subclass gets its own routes, so
76
+ several applications can coexist in one process.
77
+
83
78
  Start with: `rackup`
84
79
 
85
80
  ## Basic Usage
@@ -98,15 +93,13 @@ class MyApp < Lennarb::App
98
93
  optional :port, int, 9292
99
94
  end
100
95
 
101
- # Define routes
102
- routes do
103
- get("/") do |req, res|
104
- res.html("<h1>Welcome!</h1>")
105
- end
96
+ get("/") do |req, res|
97
+ res.html("<h1>Welcome!</h1>")
98
+ end
106
99
 
107
- post("/users") do |req, res|
108
- # Access request data
109
- data = req.body
100
+ post("/users") do |req, res|
101
+ # Access request data
102
+ data = req.body
110
103
  res.json({status: "created", data: data})
111
104
  end
112
105
  end
@@ -160,18 +153,14 @@ For larger applications, use `Lennarb::Base` to mount multiple apps:
160
153
 
161
154
  ```ruby
162
155
  class API < Lennarb::App
163
- routes do
164
- get("/users") do |req, res|
165
- res.json([{id: 1, name: "Alice"}, {id: 2, name: "Bob"}])
166
- end
156
+ get("/users") do |req, res|
157
+ res.json([{id: 1, name: "Alice"}, {id: 2, name: "Bob"}])
167
158
  end
168
159
  end
169
160
 
170
- class Admin < Lennarb::App
171
- routes do
172
- get("/dashboard") do |req, res|
173
- res.html("<h1>Admin Dashboard</h1>")
174
- end
161
+ class Admin < Lennarb::App
162
+ get("/dashboard") do |req, res|
163
+ res.html("<h1>Admin Dashboard</h1>")
175
164
  end
176
165
  end
177
166
 
@@ -193,11 +182,10 @@ run Application.new.initialize!
193
182
 
194
183
  For more detailed information, please see:
195
184
 
196
- - [Getting Started](https://aristotelesbr.github.io/lennarb/guides/getting-started/index) - Setup and first steps
197
- - [Response](https://aristotelesbr.github.io/lennarb/guides/response/index.html) - Response handling
198
- - [Request](https://aristotelesbr.github.io/lennarb/guides/request/index.html) - Request handling
199
- - [Mounting Applications](https://aristotelesbr.github.io/lennarb/guides/mounting-applications/index.html) - Working with multiple apps
200
- - [Performance](https://aristotelesbr.github.io/lennarb/guides/performance/index.html) - Benchmarks showing Lennarb's routing algorithm efficiency
185
+ - [Getting Started](https://github.com/aristotelesbr/lennarb/tree/main/guides/getting-started) - Setup and first steps
186
+ - [Response](https://github.com/aristotelesbr/lennarb/tree/main/guides/response) - Response handling
187
+ - [Mounting Applications](https://github.com/aristotelesbr/lennarb/tree/main/guides/mounting-applications) - Working with multiple apps
188
+ - [Performance](https://github.com/aristotelesbr/lennarb/tree/main/guides/performance) - Benchmarks showing Lennarb's routing algorithm efficiency
201
189
 
202
190
  ## Contributing
203
191
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lennarb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aristóteles Coutinho
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-19 00:00:00.000000000 Z
10
+ date: 2026-09-09 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bigdecimal
@@ -125,16 +125,30 @@ dependencies:
125
125
  name: minitest
126
126
  requirement: !ruby/object:Gem::Requirement
127
127
  requirements:
128
- - - ">="
128
+ - - "~>"
129
129
  - !ruby/object:Gem::Version
130
- version: '0'
130
+ version: '6.0'
131
131
  type: :development
132
132
  prerelease: false
133
133
  version_requirements: !ruby/object:Gem::Requirement
134
134
  requirements:
135
- - - ">="
135
+ - - "~>"
136
136
  - !ruby/object:Gem::Version
137
- version: '0'
137
+ version: '6.0'
138
+ - !ruby/object:Gem::Dependency
139
+ name: minitest-mock
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '5.27'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '5.27'
138
152
  - !ruby/object:Gem::Dependency
139
153
  name: minitest-utils
140
154
  requirement: !ruby/object:Gem::Requirement
@@ -254,6 +268,7 @@ files:
254
268
  - LICENCE
255
269
  - README.pt-BR.md
256
270
  - Rakefile
271
+ - benchmark/hot_path.rb
257
272
  - benchmark/memory.png
258
273
  - benchmark/rps.png
259
274
  - benchmark/runtime_with_startup.png
@@ -307,7 +322,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
307
322
  requirements:
308
323
  - - ">="
309
324
  - !ruby/object:Gem::Version
310
- version: '0'
325
+ version: '3.4'
311
326
  required_rubygems_version: !ruby/object:Gem::Requirement
312
327
  requirements:
313
328
  - - ">="