diff --git a/.rubocop.yml b/.rubocop.yml
index f30fa3a..0f7aeb5 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -14,12 +14,6 @@ Naming/RescuedExceptionsVariableName:
Layout/LineLength:
Max: 120
-Layout/IndentationStyle:
- EnforcedStyle: tabs
-
-Layout/IndentationWidth:
- Width: 1
-
Layout/MultilineMethodCallIndentation:
Enabled: false
diff --git a/Rakefile b/Rakefile
index 8940be1..05bf4e7 100644
--- a/Rakefile
+++ b/Rakefile
@@ -3,9 +3,9 @@
require 'rake/testtask'
Rake::TestTask.new do |t|
- t.libs << 'test'
- t.test_files = FileList['test/**/test_*.rb']
- t.verbose = true
+ t.libs << 'test'
+ t.test_files = FileList['test/**/test_*.rb']
+ t.verbose = true
end
task default: :test
diff --git a/bake/test.rb b/bake/test.rb
index 233e9db..724ba5d 100644
--- a/bake/test.rb
+++ b/bake/test.rb
@@ -10,12 +10,12 @@
#
# @parameter test [String] the path to file
def test(test: nil)
- test_dir = 'test'
+ test_dir = 'test'
- all_tests_command = "Dir.glob(\"./#{test_dir}/**/test_*.rb\").each { require _1 }"
- test_command = test ? "ruby -I#{test_dir} #{test}" : "ruby -I#{test_dir} -e '#{all_tests_command}'"
+ all_tests_command = "Dir.glob(\"./#{test_dir}/**/test_*.rb\").each { require _1 }"
+ test_command = test ? "ruby -I#{test_dir} #{test}" : "ruby -I#{test_dir} -e '#{all_tests_command}'"
- stdout, _stderr, _status = Open3.capture3(test_command)
+ stdout, _stderr, _status = Open3.capture3(test_command)
- puts stdout.green
+ puts stdout.green
end
diff --git a/changelog.md b/changelog.md
index c782c0f..0d835df 100644
--- a/changelog.md
+++ b/changelog.md
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [1.3.0] - 2024-11-21
+
+### Added
+
+- Add `Lennarb::Plugin` module to manage the plugins in the project. Now, the `Lennarb` class is the main class of the project.
+
+- Automatically loads plugins from the default directory
+
+- Supports custom plugin directories via `LENNARB_PLUGINS_PATH`
+
+- Configurable through environment variables
+
+### Changed
+
+- Change the `finish` method from `Lennarb` class to call `halt(@res.finish)` method to finish the response.
+
+### Removed
+
+- Remove `Lennarb::ApplicationBase` class from the project. Now, the `Lennarb` class is the main class of the project.
+
## [0.6.1] - 2024-05-17
### Added
diff --git a/guides/getting-started/readme.md b/guides/getting-started/readme.md
index 43de44e..361d4ed 100644
--- a/guides/getting-started/readme.md
+++ b/guides/getting-started/readme.md
@@ -1,299 +1,286 @@
-# Getting Started
+# Getting Started with Lennarb
-This guide show you how to use the `lennarb`
+## Overview
+Lennarb is a lightweight, Rack-based web framework for Ruby that emphasizes simplicity and flexibility. It provides a clean DSL for routing, middleware support, and various ways to structure your web applications.
-## Instalation
+## Installation
-Add the gem to your project:
+Add Lennarb to your project's Gemfile:
+```ruby
+gem 'lennarb'
+```
+Or install it directly via RubyGems:
```bash
-$ gem add lennarb
+$ gem install lennarb
```
-## Usage
+## Quick Start
-A basic app looks like this:
+### Basic Application
+Create a new file named `config.ru`:
```ruby
-# config.ru
-
require 'lennarb'
-app = Lenna.new
-
-app.get '/' do |req, res|
- res.html 'Hello World'
+app = Lennarb.new do |app|
+ app.get '/' do |req, res|
+ res.status = 200
+ res.html('
Welcome to Lennarb!
')
+ end
end
run app
```
-You can also use a block to define the app:
-
-```ruby
-# config.ru
-
-require 'lennarb'
-
-app = Lenna.new do |app|
- app.get '/' do |req, res|
- res.html 'Hello World'
- end
-end
-
-run app
+Start the server:
+```bash
+$ rackup
```
-## How to use `Lennarb::Application::Base` class
+Your application will be available at `http://localhost:9292`.
-You can also use the `Lennarb::Application::Base` class to define your app:
+## Application Structure
-```ruby
-# config.ru
-
-require 'lennarb'
+### Class-Based Applications
+For larger applications, you can use a class-based structure:
-class App < Lennarb::Application::Base
+```ruby
+class MyApp < Lennarb
+ # Routes
+ get '/' do |req, res|
+ res.status = 200
+ res.html('Welcome to MyApp!
')
+ end
+
+ post '/users' do |req, res|
+ user = create_user(req.params)
+ res.status = 201
+ res.json(user.to_json)
+ end
end
-```
-## Define routes
-
-When you use the `Lennarb::Application::Base` class, you can use the following methods:
-
-- `get`
-- `post`
-- `put`
-- `delete`
-- `patch`
-- `head`
-- `options`
-
-```ruby
# config.ru
-
-require 'lennarb'
-
-class App < Lennarb::Application::Base
- get '/' do |req, res|
- res.html 'Hello World'
- end
-end
+run MyApp.freeze!
```
-## Run!
-
-If you use the `Lennarb::Application::Base` class, you can use the `run!` method to run your app:
+### Instance-Based Applications
+For simpler applications or prototypes:
```ruby
-# config.ru
-
-require 'lennarb'
-
-class App < Lennarb::Application::Base
- get '/' do |req, res|
- res.html 'Hello World'
- end
+app = Lennarb.new do |l|
+ l.get '/hello/:name' do |req, res|
+ name = req.params[:name]
+ res.status = 200
+ res.text("Hello, #{name}!")
+ end
end
-App.run!
-```
-
-By default, the `run!` method does the following:
-
-- Freeze the app routes
-- Default middlewares:
- - `Lennarb::Middlewares::Logger`
- - `Lennarb::Middlewares::Static`
- - `Lennarb::Middlewares::NotFound`
- - `Lennarb::Middlewares::Lint`L
- - `Lennarb::Middlewares::ShowExceptions`
- - `Lennarb::Middlewares::MethodOverride`
-
-After that, you can run your app with the `rackup` command:
-
-```bash
-$ rackup
+run app
```
-## Mount route controllers
+## Routing
-You can use the `mount` method to mount route controllers:
+### Available HTTP Methods
+Lennarb supports all standard HTTP methods:
+- `get(path, &block)`
+- `post(path, &block)`
+- `put(path, &block)`
+- `patch(path, &block)`
+- `delete(path, &block)`
+- `head(path, &block)`
+- `options(path, &block)`
-Create a route controller, in this example, we'll create a `UsersController`, and define your routes:
+### Route Parameters
+Routes can include dynamic parameters:
```ruby
-# ../whatwever/users.rb
-
-require 'lennarb'
-
-class UsersController < Lennarb::Application::Base
- get '/users' do |req, res|
- res.html 'List of users'
- end
+class API < Lennarb
+ get '/users/:id' do |req, res|
+ user_id = req.params[:id]
+ user = User.find(user_id)
+
+ res.status = 200
+ res.json(user.to_json)
+ end
end
```
-Now, you can use the `mount` method to mount the `UsersController` on your app:
+### Response Helpers
+Lennarb provides convenient response helpers:
```ruby
-# config.ru
-
-require 'lennarb'
-require_relative './whatwever/users'
-
-class Application < Lennarb::Application::Base
- mount UsersController
+class App < Lennarb
+ get '/html' do |req, res|
+ res.html('HTML Response
')
+ end
+
+ get '/json' do |req, res|
+ res.json({ message: 'JSON Response' })
+ end
+
+ get '/text' do |req, res|
+ res.text('Plain Text Response')
+ end
+
+ get '/redirect' do |req, res|
+ res.redirect('/new-location')
+ end
end
```
-Completed! You can now execute your application using distinct controllers.
+## Middleware Support
-馃毃 **IMPORTANT:** The `mount` method does not modify the hooks of the mounted controller. This means that the hooks of the mounted controller will not be executed in the context of the main application.
+The plugin system is compatible with Rack middleware. To add middleware using the `use` method:
```ruby
-# ../whatwever/users.rb
-
-require 'lennarb'
-
-class UsersController < Lennarb::Application::Base
- before do |req, res|
- puts 'UsersController before' # This will be executed in global context
- end
+class App < Lennarb
+ # Built-in Rack middleware
+ use Rack::Logger
+ use Rack::Session::Cookie, secret: 'your_secret'
+
+ # Custom middleware
+ use MyCustomMiddleware, option1: 'value1'
+
+ get '/' do |req, res|
+ # Access middleware features
+ logger = env['rack.logger']
+ logger.info 'Processing request...'
+
+ res.status = 200
+ res.text('Hello World!')
+ end
end
```
-We recommend you to use the `before` and `after` methods to define callbacks in the main application or specific routes. Ex. `before('/users')` will be executed only in the `UsersController` routes.
-
-## Hooks
-
-You can use the `before` and `after` methods to define callbacks:
+## Application Lifecycle
+### Initialization
```ruby
-# config.run
-
-require 'lennarb'
-
-class App < Lennarb::Application::Base
- before do |req, res|
- puts 'Before'
- end
-
- get '/' do |req, res|
- res.html 'Hello World'
- end
-
- after do |req, res|
- puts 'After'
- end
+class App < Lennarb
+ # Configuration code here
+
+ def initialize
+ super
+ # Custom initialization code
+ end
end
-
-run App.run!
```
-You can also use the `before` and `after` methods to define callbacks for specific routes:
+### Freezing the Application
+Call `freeze!` to finalize your application configuration:
```ruby
# config.ru
-
-require 'lennarb'
-
-class App < Lennarb::Application::Base
- before '/about' do |req, res|
- puts 'Before about'
- end
-
- get '/about' do |req, res|
- res.html 'About'
- end
-
- after '/about' do |req, res|
- puts 'After about'
- end
-end
+app = MyApp.freeze!
+run app
```
-## Middlewares
+After freezing:
+- No new routes can be added
+- No new middleware can be added
+- The application becomes thread-safe
-You can use the `use` method to add middlewares:
+## Development vs Production
+### Development
```ruby
-
# config.ru
+require './app'
-require 'lennarb'
-
-class App < Lennarb::Application::Base
- use Lennarb::Middlewares::Logger
- use Lennarb::Middlewares::Static
-
- get '/' do |req, res|
- res.html 'Hello World'
- end
+if ENV['RACK_ENV'] == 'development'
+ use Rack::Reloader
+ use Rack::ShowExceptions
end
-run App.run!
+run App.freeze!
```
-## Not Found
-
-You can use the `render_not_found` method to define a custom 404 page in the hooks context:
-
+### Production
```ruby
-require 'lennarb'
+# config.ru
+require './app'
-class App < Lennarb::Application::Base
- before do |context|
- render_not_found if context['PATH_INFO'] == '/not_found'
- end
+if ENV['RACK_ENV'] == 'production'
+ use Rack::CommonLogger
+ use Rack::Runtime
end
-```
-You can pass your custom 404 page:
+run App.freeze!
+```
-```ruby
-before do |context|
-
- HTML = <<~HTML
-
-
-
- 404 Not Found
-
-
- 404 Not Found
-
-
- HTML
-
- render_not_found HTML if context['PATH_INFO'] == '/not_found'
-end
+## Best Practices
+
+1. **Route Organization**
+ ```ruby
+ class App < Lennarb
+ # Group related routes together
+ # API routes
+ get '/api/users' do |req, res|
+ # Handle API request
+ end
+
+ # Web routes
+ get '/web/dashboard' do |req, res|
+ # Handle web request
+ end
+ end
+ ```
+
+2. **Error Handling**
+ ```ruby
+ class App < Lennarb
+ get '/protected' do |req, res|
+ raise AuthenticationError unless authenticated?(req)
+ res.text('Secret content')
+ rescue AuthenticationError
+ res.status = 401
+ res.json({ error: 'Unauthorized' })
+ end
+ end
+ ```
+
+3. **Modular Design**
+ ```ruby
+ # Split large applications into modules
+ class AdminApp < Lennarb
+ # Admin-specific routes
+ end
+
+ class MainApp < Lennarb
+ plugin :mount
+ mount AdminApp, at: '/admin'
+ end
+ ```
+
+## Running Your Application
+
+### Basic Usage
+```bash
+$ rackup
```
-Lastly, you can create a custom 404 page and put in the `public` folder:
+### With Environment Configuration
+```bash
+$ RACK_ENV=production rackup -p 3000
+```
-```sh
-touch public/404.html
+### With Custom Config File
+```bash
+$ rackup custom_config.ru
```
-Now the default 404 page is your custom 404 page.
+## Next Steps
-After that, you can run your app with the `rackup` command:
+- Explore the [Plugin System](plugins.md) for extending functionality
+- Learn about [Middleware Integration](middleware.md)
+- Check out [Advanced Routing](routing.md)
+- Read the [API Documentation](api.md)
-```bash
-$ rackup
+## Support
-Puma starting in single mode...
-* Puma version: 6.4.0 (ruby 3.2.2-p53) ("The Eagle of Durango")
-* Min threads: 0
-* Max threads: 5
-* Environment: development
-* PID: 94360
-* Listening on http://127.0.0.1:9292
-* Listening on http://[::1]:9292
-Use Ctrl-C to stop
-^C- Gracefully stopping, waiting for requests to finish
-=== puma shutdown: 2023-12-19 08:54:26 -0300 ===
-```
+For help and bug reports, please visit:
+- GitHub Issues: [lennarb/issues](https://github.com/your-repo/lennarb/issues)
+- Documentation: [lennarb.github.io](https://your-docs-site/lennarb)
Done! Now you can run your app!
diff --git a/guides/plugin/readme.md b/guides/plugin/readme.md
index 2dc65a3..b7a4ab2 100644
--- a/guides/plugin/readme.md
+++ b/guides/plugin/readme.md
@@ -2,81 +2,212 @@
## Overview
-The Lennarb Plugin System allows you to extend the functionality of your Lennarb application by registering and loading plugins. This system is designed to be simple and flexible, enabling you to add custom behaviors and features to your application effortlessly.
+The Lennarb Plugin System provides a modular way to extend your application's functionality. Built with simplicity and flexibility in mind, it allows you to seamlessly integrate new features and behaviors into your Lennarb applications through a clean and intuitive API.
+
+## Core Features
+
+- Simple plugin registration and loading
+- Support for default and custom plugin directories
+- Environment-based configuration
+- Thread-safe plugin management
+- Inheritance-aware plugin system
## Implementation Details
-The plugin system is implemented using a module, `Lennarb::Plugin`, which centralizes the registration and loading of plugins.
+### The Plugin Module
-### Module: `Plugin`
+The core of the plugin system is implemented in the `Lennarb::Plugin` module, which provides the following key functionality:
+
+#### Registry Management
```ruby
-class Lennarb
- module Plugin
- @plugins = {}
-
- def self.register(name, mod)
- @plugins[name] = mod
- end
-
- def self.load(name)
- @plugins[name] || raise("Plugin #{name} did not register itself correctly")
- end
-
- def self.plugins
- @plugins
- end
- end
-end
+Lennarb::Plugin.register(:plugin_name, PluginModule)
+```
+
+- Maintains a central registry of all available plugins
+- Ensures unique plugin names using symbols as identifiers
+- Thread-safe registration process
+
+#### Plugin Loading
+
+```ruby
+Lennarb::Plugin.load(:plugin_name)
+```
+
+- Loads plugins on demand
+- Raises `Lennarb::Plugin::Error` if plugin is not found
+- Handles plugin dependencies automatically
+
+#### Default Plugin Management
+
+```ruby
+Lennarb::Plugin.load_defaults!
```
-## Usage
+- Automatically loads plugins from the default directory
+- Supports custom plugin directories via `LENNARB_PLUGINS_PATH`
+- Configurable through environment variables
-### Registering a Plugin
+## Creating Plugins
-To register a plugin, define a module with the desired functionality and register it with the Plugin module.
+### Basic Plugin Structure
```ruby
-module MyCustomPlugin
- def custom_method
- "Custom Method Executed"
+module MyPlugin
+ module ClassMethods
+ def plugin_class_method
+ # Class-level functionality
+ end
+ end
+
+ module InstanceMethods
+ def plugin_instance_method
+ # Instance-level functionality
+ end
+ end
+
+ def self.configure(app, options = {})
+ # Plugin configuration logic
end
end
-Lennarb::Plugin.register(:my_custom_plugin, MyCustomPlugin)
+Lennarb::Plugin.register(:my_plugin, MyPlugin)
```
-### Load and Use a Plugin
+### Plugin Components
+
+1. **ClassMethods**: Extended into the Lennarb application class
+2. **InstanceMethods**: Included in the Lennarb application instance
+3. **configure**: Called when the plugin is loaded (optional)
+
+## Using Plugins
-To load and use a plugin in your Lennarb application, call the plugin method in your application class.
+### In a Class-based Application
```ruby
-Lennarb.new do |app|
- app.plugin :my_custom_plugin
+class MyApp < Lennarb
+ plugin :hooks
+ plugin :mount
- app.get '/custom' do |req, res|
- res.status = 200
- res.html(custom_method)
- end
+ before '/admin/*' do |req, res|
+ # Authentication logic
+ end
+
+ get '/users' do |req, res|
+ res.status = 200
+ res.json(users: ['John', 'Jane'])
+ end
end
```
-And if you are using the `Lennarb::Application::Base` class, you can use the `plugin` method directly in your application class.
+### In a Direct Instance
```ruby
-class MyApp < Lennarb::Application::Base
- plugin :my_custom_plugin
+app = Lennarb.new do |l|
+ l.plugin :hooks
- get '/custom' do |_req, res|
+ l.get '/hello' do |req, res|
res.status = 200
- res.html(custom_method)
+ res.text('Hello, World!')
end
end
```
-In this example, the custom_method defined in `MyCustomPlugin` is available in the routes of `MyApp`.
+### Plugin Configuration
+
+```ruby
+class MyApp < Lennarb
+ plugin :my_plugin, option1: 'value1', option2: 'value2'
+end
+```
+
+## Built-in Plugins
+
+### Hooks Plugin
+
+Provides before and after hooks for request processing:
+
+```ruby
+plugin :hooks
+
+before '/admin/*' do |req, res|
+ authenticate_admin(req)
+end
+
+after '*' do |req, res|
+ log_request(req)
+end
+```
+
+### Mount Plugin
+
+Enables mounting other Lennarb applications:
+
+```ruby
+plugin :mount
+
+mount AdminApp, at: '/admin'
+mount ApiV1, at: '/api/v1'
+```
+
+## Environment Configuration
+
+The plugin system can be configured through environment variables:
+
+- `LENNARB_PLUGINS_PATH`: Custom plugin directory paths (colon-separated)
+- `LENNARB_AUTO_LOAD_DEFAULTS`: Enable/disable automatic loading of default plugins (default: true)
+
+Example:
+
+```bash
+export LENNARB_PLUGINS_PATH="/app/plugins:/lib/plugins"
+export LENNARB_AUTO_LOAD_DEFAULTS="true"
+```
+
+## Best Practices
+
+1. **Plugin Naming**
+
+ - Use descriptive, lowercase names
+ - Avoid conflicts with existing plugins
+ - Use underscores for multi-word names
+
+2. **Error Handling**
+
+ - Implement proper error handling in plugin code
+ - Raise meaningful exceptions when appropriate
+ - Document potential errors
+
+3. **Configuration**
+
+ - Keep plugin configuration simple and optional
+ - Provide sensible defaults
+ - Document configuration options
+
+4. **Testing**
+ - Write tests for your plugins
+ - Test plugin interactions
+ - Verify thread safety
+
+## Troubleshooting
+
+Common issues and solutions:
+
+1. **Plugin Not Found**
+
+ ```ruby
+ Lennarb::Plugin::Error: Plugin my_plugin not found
+ ```
-## Conclusion
+ - Verify plugin is properly registered
+ - Check plugin file location
+ - Ensure proper require statements
-The Lennarb Plugin System provides a simple and flexible way to extend your application's functionality. By registering and loading plugins, you can easily add custom behaviors and features to your Lennarb application.
+2. **Plugin Directory Issues**
+ ```ruby
+ Lennarb::Plugin::Error: Plugin directory '/path/to/plugins' does not exist
+ ```
+ - Verify directory exists
+ - Check permissions
+ - Validate LENNARB_PLUGINS_PATH
diff --git a/guides/response/readme.md b/guides/response/readme.md
index 8e44721..24d8873 100644
--- a/guides/response/readme.md
+++ b/guides/response/readme.md
@@ -68,7 +68,7 @@ You can set headers using the `res.header` method:
# app.rb
app.get '/' do |req, res|
- res.headers['Content-Type'] = 'text/plain'
+ res['Content-Type'] = 'text/plain'
res.write 'Hello World'
end
```
@@ -89,7 +89,7 @@ You can redirect the client using the `res.redirect` method:
# app.ruby
app.get '/' do |req, res|
- # Stuff code here...
+ # Stuff code here...
res.redirect '/hello'
end
```
diff --git a/lennarb.gemspec b/lennarb.gemspec
index 4032675..c521273 100644
--- a/lennarb.gemspec
+++ b/lennarb.gemspec
@@ -3,42 +3,41 @@
require_relative 'lib/lennarb/version'
Gem::Specification.new do |spec|
- spec.name = 'lennarb'
- spec.version = Lennarb::VERSION
-
- spec.summary = <<~DESC
- Lennarb provides a lightweight yet robust solution for web routing in Ruby, focusing on performance and simplicity.
- DESC
- spec.authors = ['Arist贸teles Coutinho']
- spec.license = 'MIT'
-
- spec.homepage = 'https://aristotelesbr.github.io/lennarb'
-
- spec.metadata = {
- 'allowed_push_host' => 'https://rubygems.org',
- 'changelog_uri' => 'https://github.com/aristotelesbr/lennarb/blob/master/changelog.md',
- 'homepage_uri' => 'https://aristotelesbr.github.io/lennarb',
- 'rubygems_mfa_required' => 'true',
- 'source_code_uri' => 'https://github.com/aristotelesbr/lennarb'
- }
-
- spec.files = Dir['{exe,lib}/**/*', '*.md', base: __dir__]
-
- spec.bindir = 'exe'
- spec.executables = ['lenna']
-
- spec.required_ruby_version = '>= 3.1'
-
- spec.add_dependency 'colorize', '~> 1.1'
- spec.add_dependency 'rack', '~> 3.0', '>= 3.0.8'
-
- spec.add_development_dependency 'bake', '~> 0.18.2'
- spec.add_development_dependency 'bundler', '~> 2.2'
- spec.add_development_dependency 'covered', '~> 0.25.1'
- spec.add_development_dependency 'minitest', '~> 5.20'
- spec.add_development_dependency 'puma', '~> 6.4'
- spec.add_development_dependency 'rack-test', '~> 2.1'
- spec.add_development_dependency 'rake', '~> 13.1'
- spec.add_development_dependency 'rubocop', '~> 1.59'
- spec.add_development_dependency 'rubocop-minitest', '~> 0.33.0'
+ spec.name = 'lennarb'
+ spec.version = Lennarb::VERSION
+
+ spec.summary = <<~DESC
+ Lennarb provides a lightweight yet robust solution for web routing in Ruby, focusing on performance and simplicity.
+ DESC
+ spec.authors = ['Arist贸teles Coutinho']
+ spec.license = 'MIT'
+
+ spec.homepage = 'https://aristotelesbr.github.io/lennarb'
+
+ spec.metadata = {
+ 'allowed_push_host' => 'https://rubygems.org',
+ 'changelog_uri' => 'https://github.com/aristotelesbr/lennarb/blob/master/changelog.md',
+ 'homepage_uri' => 'https://aristotelesbr.github.io/lennarb',
+ 'rubygems_mfa_required' => 'true',
+ 'source_code_uri' => 'https://github.com/aristotelesbr/lennarb'
+ }
+
+ spec.files = Dir['{exe,lib}/**/*', '*.md', base: __dir__]
+
+ spec.bindir = 'exe'
+ spec.executables = ['lenna']
+
+ spec.required_ruby_version = '>= 3.1'
+
+ spec.add_dependency 'colorize', '~> 1.1'
+ spec.add_dependency 'rack', '~> 3.0', '>= 3.0.8'
+
+ spec.add_development_dependency 'bake', '~> 0.18.2'
+ spec.add_development_dependency 'bundler', '~> 2.2'
+ spec.add_development_dependency 'covered', '~> 0.25.1'
+ spec.add_development_dependency 'minitest', '~> 5.20'
+ spec.add_development_dependency 'rack-test', '~> 2.1'
+ spec.add_development_dependency 'rake', '~> 13.1'
+ spec.add_development_dependency 'rubocop', '~> 1.59'
+ spec.add_development_dependency 'rubocop-minitest', '~> 0.33.0'
end
diff --git a/lib/lennarb.rb b/lib/lennarb.rb
index 3641848..803fdb2 100644
--- a/lib/lennarb.rb
+++ b/lib/lennarb.rb
@@ -8,9 +8,6 @@
require 'pathname'
require 'rack'
-# Base class for Lennarb
-#
-require_relative 'lennarb/application/base'
require_relative 'lennarb/plugin'
require_relative 'lennarb/request'
require_relative 'lennarb/response'
@@ -18,127 +15,144 @@
require_relative 'lennarb/version'
class Lennarb
- # Error class
- #
- class LennarbError < StandardError; end
-
- # @attribute [r] root
- # @returns [RouteNode]
- #
- attr_reader :_root
-
- # @attribute [r] applied_plugins
- # @returns [Array]
- #
- attr_reader :_applied_plugins
-
- # Initialize the application
- #
- # @yield { ... } The application
- #
- # @returns [Lennarb]
- #
- def initialize
- @_root = RouteNode.new
- @_applied_plugins = []
- yield self if block_given?
- end
-
- # Split a path into parts
- #
- # @parameter [String] path
- #
- # @returns [Array] parts. Ex. ['users', ':id']
- #
- SplitPath = ->(path) { path.split('/').reject(&:empty?) }
- private_constant :SplitPath
-
- # Call the application
- #
- # @parameter [Hash] env
- #
- # @returns [Array] response
- #
- def call(env)
- http_method = env[Rack::REQUEST_METHOD].to_sym
- parts = SplitPath[env[Rack::PATH_INFO]]
-
- block, params = @_root.match_route(parts, http_method)
- return [404, { 'content-type' => 'text/plain' }, ['Not Found']] unless block
-
- @res = Response.new
- req = Request.new(env, params)
-
- catch(:halt) do
- instance_exec(req, @res, &block)
- @res.finish
- end
- end
-
- # Freeze the routes
- #
- # @returns [void]
- #
- def freeze! = @_root.freeze
-
- # Add a routes
- #
- # @parameter [String] path
- # @parameter [Proc] block
- #
- # @returns [void]
- #
- def get(path, &block) = add_route(path, :GET, block)
- def put(path, &block) = add_route(path, :PUT, block)
- def post(path, &block) = add_route(path, :POST, block)
- def head(path, &block) = add_route(path, :HEAD, block)
- def patch(path, &block) = add_route(path, :PATCH, block)
- def delete(path, &block) = add_route(path, :DELETE, block)
- def options(path, &block) = add_route(path, :OPTIONS, block)
-
- # Add plugin to extend the router
- #
- # @parameter [String] plugin_name
- # @parameter [args] *args
- # @parameter [Block] block
- #
- # @returns [void]
- #
- def plugin(plugin_name, *, &)
- return if @_applied_plugins.include?(plugin_name)
-
- plugin_module = Plugin.load(plugin_name)
- extend plugin_module::InstanceMethods if defined?(plugin_module::InstanceMethods)
- self.class.extend plugin_module::ClassMethods if defined?(plugin_module::ClassMethods)
- plugin_module.setup(self.class, *, &) if plugin_module.respond_to?(:setup)
-
- @_applied_plugins << plugin_name
- end
-
- # Merge the other RouteNode into the current one
- #
- # @parameter other [RouteNode] The other RouteNode to merge into the current one
- #
- # @return [void]
- #
- def merge!(other)
- raise "Expected a Lennarb instance, got #{other.class}" unless other.is_a?(Lennarb)
-
- @_root.merge!(other._root)
+ class LennarbError < StandardError; end
+
+ attr_reader :_root, :_plugins, :_loaded_plugins, :_middlewares, :_app
+
+ def self.use(middleware, *args, &block)
+ @_middlewares ||= []
+ @_middlewares << [middleware, args, block]
+ end
+
+ def self.get(path, &block) = add_route(path, :GET, block)
+ def self.put(path, &block) = add_route(path, :PUT, block)
+ def self.post(path, &block) = add_route(path, :POST, block)
+ def self.head(path, &block) = add_route(path, :HEAD, block)
+ def self.patch(path, &block) = add_route(path, :PATCH, block)
+ def self.delete(path, &block) = add_route(path, :DELETE, block)
+ def self.options(path, &block) = add_route(path, :OPTIONS, block)
+
+ def self.inherited(subclass)
+ super
+ subclass.instance_variable_set(:@_root, RouteNode.new)
+ subclass.instance_variable_set(:@_plugins, [])
+ subclass.instance_variable_set(:@_middlewares, @_middlewares&.dup || [])
+
+ Plugin.load_defaults! if Plugin.load_defaults?
+ end
+
+ def self.plugin(plugin_name, *, &)
+ @_loaded_plugins ||= {}
+ @_plugins ||= []
+
+ return if @_loaded_plugins.key?(plugin_name)
+
+ plugin_module = Plugin.load(plugin_name)
+ plugin_module.configure(self, *, &) if plugin_module.respond_to?(:configure)
+
+ @_loaded_plugins[plugin_name] = plugin_module
+ @_plugins << plugin_name
+ end
+
+ def self.freeze!
+ app = new
+ app.freeze!
+ app
+ end
+
+ def self.add_route(path, http_method, block)
+ @_root ||= RouteNode.new
+ parts = path.split('/').reject(&:empty?)
+ @_root.add_route(parts, http_method, block)
+ end
+
+ private_class_method :add_route
+
+ def initialize
+ @_mutex = Mutex.new
+ @_root = self.class.instance_variable_get(:@_root)&.dup || RouteNode.new
+ @_plugins = self.class.instance_variable_get(:@_plugins)&.dup || []
+ @_loaded_plugins = self.class.instance_variable_get(:@_loaded_plugins)&.dup || {}
+ @_middlewares = self.class.instance_variable_get(:@_middlewares)&.dup || []
+
+ build_app
+
+ yield self if block_given?
end
- private
-
- # Add a route
- #
- # @parameter [String] path
- # @parameter [String] http_method
- # @parameter [Proc] block
- #
- # @returns [void]
- #
- def add_route(path, http_method, block)
- parts = SplitPath[path]
- @_root.add_route(parts, http_method, block)
- end
+ def call(env) = @_mutex.synchronize { @_app.call(env) }
+
+ def freeze!
+ return self if @_mounted
+
+ @_root.freeze
+ @_plugins.freeze
+ @_loaded_plugins.freeze
+ @_middlewares.freeze
+ @_app.freeze if @_app.respond_to?(:freeze)
+ self
+ end
+
+ def get(path, &block) = add_route(path, :GET, block)
+ def put(path, &block) = add_route(path, :PUT, block)
+ def post(path, &block) = add_route(path, :POST, block)
+ def head(path, &block) = add_route(path, :HEAD, block)
+ def patch(path, &block) = add_route(path, :PATCH, block)
+ def delete(path, &block) = add_route(path, :DELETE, block)
+ def options(path, &block) = add_route(path, :OPTIONS, block)
+
+ def plugin(plugin_name, *, &)
+ return if @_loaded_plugins.key?(plugin_name)
+
+ plugin_module = Plugin.load(plugin_name)
+ self.class.extend plugin_module::ClassMethods if plugin_module.const_defined?(:ClassMethods)
+ self.class.include plugin_module::InstanceMethods if plugin_module.const_defined?(:InstanceMethods)
+ plugin_module.configure(self, *, &) if plugin_module.respond_to?(:configure)
+ @_loaded_plugins[plugin_name] = plugin_module
+ @_plugins << plugin_name
+ end
+
+ private
+
+ def build_app
+ @_app = method(:process_request)
+
+ @_middlewares.reverse_each do |middleware, args, block|
+ @_app = middleware.new(@_app, *args, &block)
+ end
+ end
+
+ def process_request(env)
+ http_method = env[Rack::REQUEST_METHOD].to_sym
+ parts = env[Rack::PATH_INFO].split('/').reject(&:empty?)
+
+ block, params = @_root.match_route(parts, http_method)
+ return not_found unless block
+
+ res = Response.new
+ req = Request.new(env, params)
+
+ catch(:halt) do
+ instance_exec(req, res, &block)
+ res.finish
+ end
+ rescue StandardError => e
+ handle_error(e)
+ end
+
+ def handle_error(error)
+ case error
+ when ArgumentError
+ [400, { 'content-type' => 'text/plain' }, ["Bad Request: #{error.message}"]]
+ else
+ [500, { 'content-type' => 'text/plain' }, ["Internal Server Error: #{error.message}"]]
+ end
+ end
+
+ def not_found = [404, { 'content-type' => 'text/plain' }, ['Not Found']]
+
+ def add_route(path, http_method, block)
+ parts = path.split('/').reject(&:empty?)
+ @_root.add_route(parts, http_method, block)
+ end
end
diff --git a/lib/lennarb/application/base.rb b/lib/lennarb/application/base.rb
deleted file mode 100644
index d3a3506..0000000
--- a/lib/lennarb/application/base.rb
+++ /dev/null
@@ -1,283 +0,0 @@
-# frozen_string_literal: true
-
-# Released under the MIT License.
-# Copyright, 2023-2024, by Arist贸teles Coutinho.
-
-require 'colorize'
-
-class Lennarb
- module Application
- class Base
- # @attribute [r] _route
- # @returns [RouteNode]
- #
- # @attribute [r] _middlewares
- # @returns [Array]
- #
- # @attribute [r] _global_after_hooks
- # @returns [Array]
- #
- # @attribute [r] _global_before_hooks
- # @returns [Array]
- #
- # @attribute [r] _after_hooks
- # @returns [RouteNode]
- #
- # @attribute [r] _before_hooks
- # @returns [RouteNode]
- #
- attr_accessor :_route, :_global_after_hooks, :_global_before_hooks, :_after_hooks, :_before_hooks
-
- # Initialize the Application
- #
- # @returns [Base]
- #
- class << self
- def inherited(subclass)
- super
- _applications << subclass
- subclass.instance_variable_set(:@_route, Lennarb.new)
- subclass.instance_variable_set(:@_middlewares, [])
- subclass.instance_variable_set(:@_global_after_hooks, [])
- subclass.instance_variable_set(:@_global_before_hooks, [])
- subclass.instance_variable_set(:@_after_hooks, Lennarb::RouteNode.new)
- subclass.instance_variable_set(:@_before_hooks, Lennarb::RouteNode.new)
- end
-
- def get(...) = @_route.get(...)
- def put(...) = @_route.put(...)
- def post(...) = @_route.post(...)
- def head(...) = @_route.head(...)
- def match(...) = @_route.match(...)
- def patch(...) = @_route.patch(...)
- def delete(...) = @_route.delete(...)
- def options(...) = @_route.options(...)
-
- # @returns [Array] middlewares
- #
- def _middlewares = @_middlewares ||= []
-
- # @returns [Array] applications
- #
- def _applications = @_applications ||= []
-
- # Mount a controller
- #
- # @parameter [Class] controller
- #
- def mount(controller_class)
- _applications << controller_class
- puts "Mounted controller: #{controller_class}"
- end
-
- # Use a middleware
- #
- # @parameter [Object] middleware
- # @parameter [Array] args
- # @parameter [Block] block
- #
- # @returns [Array] middlewares
- #
- def use(middleware, *args, &block)
- @_middlewares << [middleware, args, block]
- end
-
- # Add a before hook
- #
- # @parameter [String] path
- # @parameter [Block] block
- #
- def before(path = nil, &block)
- if path
- parts = path.split('/').reject(&:empty?)
- @_before_hooks.add_route(parts, :before, block)
- else
- @_global_before_hooks << block
- end
- end
-
- # Add a after hook
- #
- # @parameter [String] path
- # @parameter [Block] block
- #
- def after(path = nil, &block)
- if path
- parts = path.split('/').reject(&:empty?)
- @_after_hooks.add_route(parts, :after, block)
- else
- @_global_after_hooks << block
- end
- end
-
- # Run the Application
- #
- # @returns [Base] self
- #
- # When you use this method, the application will be frozen. And you can't add more routes after that.
- # This method is used to run the application in a Rack server so, you can use the `rackup` command
- # to run the application.
- # Ex. rackup -p 3000
- # This command will use the following middleware:
- # - Rack::ShowExceptions
- # - Rack::MethodOverride
- # - Rack::Head
- # - Rack::ContentLength
- #
- def run!
- stack = Rack::Builder.new
-
- use Rack::ShowExceptions if test? || development?
- use Rack::MethodOverride
- use Rack::Head
- use Rack::ContentLength
-
- _middlewares.each do |(middleware, args, block)|
- stack.use(middleware, *args, &block)
- end
-
- _applications.each do |app|
- app_route = app.instance_variable_get(:@_route)
-
- app_after_hooks = app.instance_variable_get(:@_after_hooks)
- app_before_hooks = app.instance_variable_get(:@_before_hooks)
- global_after_hooks = app.instance_variable_get(:@_global_after_hooks)
- global_before_hooks = app.instance_variable_get(:@_global_before_hooks)
-
- @_route.merge!(app_route)
- @_before_hooks.merge!(app_before_hooks)
- @_after_hooks.merge!(app_after_hooks)
-
- @_global_before_hooks.concat(global_before_hooks)
- @_global_after_hooks.concat(global_after_hooks)
- end
-
- stack.run ->(env) do
- catch(:halt) do
- execute_hooks(@_before_hooks, env, :before)
- res = @_route.call(env)
- execute_hooks(@_after_hooks, env, :after)
- res
- rescue StandardError => e
- render_error if production?
-
- puts e.message.red
- puts e.backtrace
- raise e
- end
- end
-
- @_route.freeze!
- stack.to_app
- end
-
- def test? = ENV['RACK_ENV'] == 'test' || ENV['LENNARB_ENV'] == 'test'
- def production? = ENV['RACK_ENV'] == 'production' || ENV['LENNARB_ENV'] == 'production'
- def development? = ENV['RACK_ENV'] == 'development' || ENV['LENNARB_ENV'] == 'development'
-
- # Render a not found
- #
- # @returns [void]
- #
- def render_not_found(content = nil)
- default = File.exist?('public/404.html')
- body = content || default || 'Not Found'
- throw :halt, [404, { 'content-type' => 'text/html' }, [body]]
- end
-
- # Render an error
- #
- # @returns [void]
- #
- def render_error(content = nil)
- default = File.exist?('public/500.html')
- body = content || default || 'Internal Server Error'
- throw :halt, [500, { 'content-type' => 'text/html' }, [body]]
- end
-
- # Redirect to a path
- #
- # @parameter [String] path
- # @parameter [Integer] status default is 302
- #
- def redirect(path, status = 302) = throw :halt, [status, { 'location' => path }, []]
-
- # Include a plugin in the application
- #
- # @parameter [String] plugin_name
- # @parameter [Array] args
- # @parameter [Block] block
- #
- # @returns [void]
- #
- def plugin(plugin_name, *, &)
- @_route.plugin(plugin_name, *, &)
- plugin_module = @_route.class::Plugin.load(plugin_name)
-
- include plugin_module::InstanceMethods if plugin_module.const_defined?(:InstanceMethods)
- extend plugin_module::ClassMethods if plugin_module.const_defined?(:ClassMethods)
- end
-
- private
-
- # Execute the hooks
- #
- # @parameter [RouteNode] hook_route
- # @parameter [Hash] env
- # @parameter [Symbol] action
- #
- # @returns [void]
- #
- def execute_hooks(hook_route, env, action)
- execute_global_hooks(env, action)
-
- execute_route_hooks(hook_route, env, action)
- end
-
- # Execute the global hooks
- #
- # @parameter [Hash] env
- # @parameter [Symbol] action
- #
- # @returns [void]
- #
- def execute_global_hooks(env, action)
- global_hooks = action == :before ? @_global_before_hooks : @_global_after_hooks
- global_hooks.each { |hook| hook.call(env) }
- end
-
- # Execute the route hooks
- #
- # @parameter [RouteNode] hook_route
- # @parameter [Hash] env
- # @parameter [Symbol] action
- #
- # @returns [void]
- #
- def execute_route_hooks(hook_route, env, action)
- parts = parse_path(env)
- return unless parts
-
- block, = hook_route.match_route(parts, action)
- block&.call(env)
- end
-
- # Parse the path
- #
- # @parameter [Hash] env
- #
- # @returns [Array] parts
- #
- def parse_path(env) = env[Rack::PATH_INFO]&.split('/')&.reject(&:empty?)
-
- # Check if the request is a HTML request
- #
- # @parameter [Hash] env
- #
- # @returns [Boolean]
- #
- def html_request?(env) = env['HTTP_ACCEPT']&.include?('text/html')
- end
- end
- end
-end
diff --git a/lib/lennarb/plugin.rb b/lib/lennarb/plugin.rb
index 07d956b..2c51d7a 100644
--- a/lib/lennarb/plugin.rb
+++ b/lib/lennarb/plugin.rb
@@ -1,35 +1,49 @@
# frozen_string_literal: true
-# Released under the MIT License.
-# Copyright, 2023-2024, by Arist贸teles Coutinho.
-
class Lennarb
- module Plugin
- @plugins = {}
-
- # Register a plugin
- #
- # @parameter [String] name
- # @parameter [Module] mod
- #
- # @returns [void]
- #
- def self.register(name, mod)
- @plugins[name] = mod
- end
-
- # Load a plugin
- #
- # @parameter [String] name
- #
- # @returns [Module] plugin
- #
- def self.load(name)
- @plugins[name] || raise(LennarbError, "Plugin #{name} did not register itself correctly")
- end
-
- # @returns [Hash] plugins
- #
- def self.plugins = @plugins
- end
+ module Plugin
+ class Error < StandardError; end
+
+ @registry = {}
+ @defaults_loaded = false
+
+ class << self
+ attr_reader :registry
+
+ def register(name, mod)
+ registry[name.to_sym] = mod
+ end
+
+ def load(name)
+ registry[name.to_sym] || raise(Error, "Plugin #{name} not found")
+ end
+
+ def load_defaults!
+ return if @defaults_loaded
+
+ # 1. Register default plugins
+ plugins_path = File.expand_path('plugins', __dir__)
+ load_plugins_from_directory(plugins_path)
+
+ # # 2. Register custom plugins
+ ENV.fetch('LENNARB_PLUGINS_PATH', nil)&.split(File::PATH_SEPARATOR)&.each do |path|
+ load_plugins_from_directory(path)
+ end
+
+ @defaults_loaded = true
+ end
+
+ def load_defaults?
+ ENV.fetch('LENNARB_AUTO_LOAD_DEFAULTS', 'true') == 'true'
+ end
+
+ private
+
+ def load_plugins_from_directory(path)
+ raise Error, "Plugin directory '#{path}' does not exist" unless File.directory?(path)
+
+ Dir["#{path}/**/*.rb"].each { require _1 }
+ end
+ end
+ end
end
diff --git a/lib/lennarb/plugins/hooks.rb b/lib/lennarb/plugins/hooks.rb
new file mode 100644
index 0000000..76c547f
--- /dev/null
+++ b/lib/lennarb/plugins/hooks.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+class Lennarb
+ module Plugins
+ module Hooks
+ def self.configure(app)
+ app.instance_variable_set(:@_before_hooks, {})
+ app.instance_variable_set(:@_after_hooks, {})
+ app.extend(ClassMethods)
+ app.include(InstanceMethods)
+ end
+
+ module ClassMethods
+ def before(paths = '*', &block)
+ paths = Array(paths)
+ @_before_hooks ||= {}
+
+ paths.each do |path|
+ @_before_hooks[path] ||= []
+ @_before_hooks[path] << block
+ end
+ end
+
+ def after(paths = '*', &block)
+ paths = Array(paths)
+ @_after_hooks ||= {}
+
+ paths.each do |path|
+ @_after_hooks[path] ||= []
+ @_after_hooks[path] << block
+ end
+ end
+
+ def inherited(subclass)
+ super
+ subclass.instance_variable_set(:@_before_hooks, @_before_hooks&.dup || {})
+ subclass.instance_variable_set(:@_after_hooks, @_after_hooks&.dup || {})
+ end
+ end
+
+ module InstanceMethods
+ def call(env)
+ catch(:halt) do
+ req = Lennarb::Request.new(env)
+ res = Lennarb::Response.new
+
+ path = env[Rack::PATH_INFO]
+
+ execute_hooks(self.class.instance_variable_get(:@_before_hooks), '*', req, res)
+
+ execute_matching_hooks(self.class.instance_variable_get(:@_before_hooks), path, req, res)
+
+ status, headers, body = super
+ res.status = status
+ headers.each { |k, v| res[k] = v }
+ body.each { |chunk| res.write(chunk) }
+
+ execute_matching_hooks(self.class.instance_variable_get(:@_after_hooks), path, req, res)
+
+ execute_hooks(self.class.instance_variable_get(:@_after_hooks), '*', req, res)
+
+ res.finish
+ end
+ rescue StandardError => e
+ handle_error(e)
+ end
+
+ private
+
+ def execute_hooks(hooks, path, req, res)
+ return unless hooks&.key?(path)
+
+ hooks[path].each do |hook|
+ instance_exec(req, res, &hook)
+ end
+ end
+
+ def execute_matching_hooks(hooks, current_path, req, res)
+ return unless hooks
+
+ hooks.each do |pattern, pattern_hooks|
+ next if pattern == '*'
+
+ next unless matches_pattern?(pattern, current_path)
+
+ pattern_hooks.each do |hook|
+ instance_exec(req, res, &hook)
+ end
+ end
+ end
+
+ def matches_pattern?(pattern, path)
+ return true if pattern == path
+
+ pattern_parts = pattern.split('/')
+ path_parts = path.split('/')
+
+ return false if pattern_parts.length != path_parts.length
+
+ pattern_parts.zip(path_parts).all? do |pattern_part, path_part|
+ pattern_part.start_with?(':') || pattern_part == path_part
+ end
+ end
+
+ def handle_error(error)
+ case error
+ when ArgumentError
+ [400, { 'Content-Type' => 'text/plain' }, ["Bad Request: #{error.message}"]]
+ else
+ [500, { 'Content-Type' => 'text/plain' }, ['Internal Server Error']]
+ end
+ end
+ end
+ end
+ Lennarb::Plugin.register(:hooks, Hooks)
+ end
+end
diff --git a/lib/lennarb/plugins/mount.rb b/lib/lennarb/plugins/mount.rb
new file mode 100644
index 0000000..3acf1ac
--- /dev/null
+++ b/lib/lennarb/plugins/mount.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+class Lennarb
+ module Plugins
+ module Mount
+ def self.configure(app)
+ app.instance_variable_set(:@_mounted_apps, {})
+ app.extend(ClassMethods)
+ app.include(InstanceMethods)
+ end
+
+ module ClassMethods
+ def mount(app_class, at:)
+ raise ArgumentError, 'Expected a Lennarb class' unless app_class.is_a?(Class) && app_class <= Lennarb
+ raise ArgumentError, 'Mount path must start with /' unless at.start_with?('/')
+
+ @_mounted_apps ||= {}
+ normalized_path = normalize_mount_path(at)
+
+ mounted_app = app_class.new
+ mounted_app.freeze!
+
+ @_mounted_apps[normalized_path] = mounted_app
+ end
+
+ private
+
+ def normalize_mount_path(path)
+ path.chomp('/')
+ end
+ end
+
+ module InstanceMethods
+ def call(env)
+ path_info = env[Rack::PATH_INFO]
+
+ self.class.instance_variable_get(:@_mounted_apps)&.each do |mount_path, app|
+ next unless path_info.start_with?("#{mount_path}/") || path_info == mount_path
+
+ env[Rack::PATH_INFO] = path_info[mount_path.length..]
+ env[Rack::PATH_INFO] = '/' if env[Rack::PATH_INFO].empty?
+ env[Rack::SCRIPT_NAME] = "#{env[Rack::SCRIPT_NAME]}#{mount_path}"
+
+ return app.call(env)
+ end
+
+ super
+ rescue StandardError => e
+ handle_error(e)
+ end
+
+ private
+
+ def handle_error(error)
+ case error
+ when ArgumentError
+ [400, { 'content-type' => 'text/plain' }, ["Bad Request: #{error.message}"]]
+ else
+ [500, { 'content-type' => 'text/plain' }, ['Internal Server Error']]
+ end
+ end
+ end
+ end
+ Lennarb::Plugin.register(:mount, Mount)
+ end
+end
diff --git a/lib/lennarb/request.rb b/lib/lennarb/request.rb
index 6dcea53..ab4b379 100644
--- a/lib/lennarb/request.rb
+++ b/lib/lennarb/request.rb
@@ -4,41 +4,80 @@
# Copyright, 2023-2024, by Arist贸teles Coutinho.
class Lennarb
- class Request < Rack::Request
- # Initialize the request object
- #
- # @parameter [Hash] env
- # @parameter [Hash] route_params
- #
- # @returns [Request]
- #
- def initialize(env, route_params = {})
- super(env)
- @route_params = route_params
- end
-
- # Get the request body
- #
- # @returns [String]
- #
- def params
- @params ||= super.merge(@route_params)
- end
-
- # Read the body of the request
- #
- # @returns [String]
- #
- def body = @body ||= super.read
-
- private
-
- # Get the query string parameters
- #
- # @returns [String]
- #
- def query_params
- @query_params ||= Rack::Utils.parse_nested_query(query_string)
- end
- end
+ class Request < Rack::Request
+ # The environment variables of the request
+ #
+ # @returns [Hash]
+ #
+ attr_reader :env
+
+ # Initialize the request object
+ #
+ # @parameter [Hash] env
+ # @parameter [Hash] route_params
+ #
+ # @returns [Request]
+ #
+ def initialize(env, route_params = {})
+ super(env)
+ @route_params = route_params
+ end
+
+ # Get the request body
+ #
+ # @returns [String]
+ #
+ def params = @params ||= super.merge(@route_params)&.transform_keys(&:to_sym)
+
+ # Get the request path
+ #
+ # @returns [String]
+ #
+ def path = @path ||= super.split('?').first
+
+ # Read the body of the request
+ #
+ # @returns [String]
+ #
+ def body = @body ||= super.read
+
+ # Get the query parameters
+ #
+ # @returns [Hash]
+ #
+ def query_params
+ @query_params ||= Rack::Utils.parse_nested_query(query_string).transform_keys(&:to_sym)
+ end
+
+ # Get the headers of the request
+ #
+ def headers
+ @headers ||= env.select { |key, _| key.start_with?('HTTP_') }
+ end
+
+ def ip = ip_address
+ def secure? = scheme == 'https'
+ def user_agent = headers['HTTP_USER_AGENT']
+ def accept = headers['HTTP_ACCEPT']
+ def referer = headers['HTTP_REFERER']
+ def host = headers['HTTP_HOST']
+ def content_length = headers['HTTP_CONTENT_LENGTH']
+ def content_type = headers['HTTP_CONTENT_TYPE']
+ def xhr? = headers['HTTP_X_REQUESTED_WITH']&.casecmp('XMLHttpRequest')&.zero?
+
+ def []=(key, value)
+ env[key] = value
+ end
+
+ def [](key)
+ env[key]
+ end
+
+ private
+
+ def ip_address
+ forwarded_for = headers['HTTP_X_FORWARDED_FOR']
+ forwarded_for ? forwarded_for.split(',').first.strip : env['REMOTE_ADDR']
+ end
+ end
end
diff --git a/lib/lennarb/response.rb b/lib/lennarb/response.rb
index e86dadb..c0434b0 100644
--- a/lib/lennarb/response.rb
+++ b/lib/lennarb/response.rb
@@ -4,137 +4,137 @@
# Copyright, 2023-2024, by Arist贸teles Coutinho.
class Lennarb
- class Response
- # @!attribute [rw] status
- # @returns [Integer]
- #
- attr_accessor :status
-
- # @!attribute [r] body
- # @returns [Array]
- #
- attr_reader :body
-
- # @!attribute [r] headers
- # @returns [Hash]
- #
- attr_reader :headers
-
- # @!attribute [r] length
- # @returns [Integer]
- #
- attr_reader :length
-
- # Constants
- #
- LOCATION = 'location'
- private_constant :LOCATION
-
- CONTENT_TYPE = 'content-type'
- private_constant :CONTENT_TYPE
-
- CONTENT_LENGTH = 'content-length'
- private_constant :CONTENT_LENGTH
-
- ContentType = { HTML: 'text/html', TEXT: 'text/plain', JSON: 'application/json' }.freeze
- private_constant :ContentType
-
- # Initialize the response object
- #
- # @returns [Response]
- #
- def initialize
- @status = 404
- @headers = {}
- @body = []
- @length = 0
- end
-
- # Set the response header
- #
- # @parameter [String] key
- #
- # @returns [String] value
- #
- def [](key)
- @headers[key]
- end
-
- # Get the response header
- #
- # @parameter [String] key
- # @parameter [String] value
- #
- # @returns [String] value
- #
- def []=(key, value)
- @headers[key] = value
- end
-
- # Write to the response body
- #
- # @parameter [String] str
- #
- # @returns [String] str
- #
- def write(str)
- str = str.to_s
- @length += str.bytesize
- @headers[CONTENT_LENGTH] = @length.to_s
- @body << str
- end
-
- # Set the response type to text
- #
- # @parameter [String] str
- #
- # @returns [String] str
- #
- def text(str)
- @headers[CONTENT_TYPE] = ContentType[:TEXT]
- write(str)
- end
-
- # Set the response type to html
- #
- # @parameter [String] str
- #
- # @returns [String] str
- #
- def html(str)
- @headers[CONTENT_TYPE] = ContentType[:HTML]
- write(str)
- end
-
- # Set the response type to json
- #
- # @parameter [String] str
- #
- # @returns [String] str
- #
- def json(str)
- @headers[CONTENT_TYPE] = ContentType[:JSON]
- write(str)
- end
-
- # Redirect the response
- #
- # @parameter [String] path
- # @parameter [Integer] status, default: 302
- #
- def redirect(path, status = 302)
- @headers[LOCATION] = path
- @status = status
-
- throw :halt, finish
- end
-
- # Finish the response
- #
- # @returns [Array] response
- #
- def finish
- [@status, @headers, @body]
- end
- end
+ class Response
+ # @!attribute [rw] status
+ # @returns [Integer]
+ #
+ attr_accessor :status
+
+ # @!attribute [r] body
+ # @returns [Array]
+ #
+ attr_reader :body
+
+ # @!attribute [r] headers
+ # @returns [Hash]
+ #
+ attr_reader :headers
+
+ # @!attribute [r] length
+ # @returns [Integer]
+ #
+ attr_reader :length
+
+ # Constants
+ #
+ LOCATION = 'location'
+ private_constant :LOCATION
+
+ CONTENT_TYPE = 'content-type'
+ private_constant :CONTENT_TYPE
+
+ CONTENT_LENGTH = 'content-length'
+ private_constant :CONTENT_LENGTH
+
+ ContentType = { HTML: 'text/html', TEXT: 'text/plain', JSON: 'application/json' }.freeze
+ private_constant :ContentType
+
+ # Initialize the response object
+ #
+ # @returns [Response]
+ #
+ def initialize
+ @status = 404
+ @headers = {}
+ @body = []
+ @length = 0
+ end
+
+ # Set the response header
+ #
+ # @parameter [String] key
+ #
+ # @returns [String] value
+ #
+ def [](key)
+ @headers[key]
+ end
+
+ # Get the response header
+ #
+ # @parameter [String] key
+ # @parameter [String] value
+ #
+ # @returns [String] value
+ #
+ def []=(key, value)
+ @headers[key] = value
+ end
+
+ # Write to the response body
+ #
+ # @parameter [String] str
+ #
+ # @returns [String] str
+ #
+ def write(str)
+ str = str.to_s
+ @length += str.bytesize
+ @headers[CONTENT_LENGTH] = @length.to_s
+ @body << str
+ end
+
+ # Set the response type to text
+ #
+ # @parameter [String] str
+ #
+ # @returns [String] str
+ #
+ def text(str)
+ @headers[CONTENT_TYPE] = ContentType[:TEXT]
+ write(str)
+ end
+
+ # Set the response type to html
+ #
+ # @parameter [String] str
+ #
+ # @returns [String] str
+ #
+ def html(str)
+ @headers[CONTENT_TYPE] = ContentType[:HTML]
+ write(str)
+ end
+
+ # Set the response type to json
+ #
+ # @parameter [String] str
+ #
+ # @returns [String] str
+ #
+ def json(str)
+ @headers[CONTENT_TYPE] = ContentType[:JSON]
+ write(str)
+ end
+
+ # Redirect the response
+ #
+ # @parameter [String] path
+ # @parameter [Integer] status, default: 302
+ #
+ def redirect(path, status = 302)
+ @headers[LOCATION] = path
+ @status = status
+
+ throw :halt, finish
+ end
+
+ # Finish the response
+ #
+ # @returns [Array] response
+ #
+ def finish
+ [@status, @headers, @body]
+ end
+ end
end
diff --git a/lib/lennarb/route_node.rb b/lib/lennarb/route_node.rb
index e0c4e4e..4cbb511 100644
--- a/lib/lennarb/route_node.rb
+++ b/lib/lennarb/route_node.rb
@@ -4,81 +4,63 @@
# Copyright, 2023-2024, by Arist贸teles Coutinho.
class Lennarb
- class RouteNode
- attr_accessor :static_children, :dynamic_children, :blocks, :param_key
+ class RouteNode
+ attr_accessor :static_children, :dynamic_children, :blocks, :param_key
- # Initializes the RouteNode class.
- #
- # @return [RouteNode]
- #
- def initialize
- @blocks = {}
- @param_key = nil
- @static_children = {}
- @dynamic_children = {}
- end
+ def initialize
+ @blocks = {}
+ @param_key = nil
+ @static_children = {}
+ @dynamic_children = {}
+ end
- # Add a route to the route node
- #
- # @parameter parts [Array] The parts of the route
- # @parameter http_method [Symbol] The HTTP method of the route
- # @parameter block [Proc] The block to be executed when the route is matched
- #
- # @return [void]
- #
- def add_route(parts, http_method, block)
- current_node = self
+ def add_route(parts, http_method, block)
+ current_node = self
- parts.each do |part|
- if part.start_with?(':')
- param_sym = part[1..].to_sym
- current_node.dynamic_children[param_sym] ||= RouteNode.new
- dynamic_node = current_node.dynamic_children[param_sym]
- dynamic_node.param_key = param_sym
- current_node = dynamic_node
- else
- current_node.static_children[part] ||= RouteNode.new
- current_node = current_node.static_children[part]
- end
- end
+ parts.each do |part|
+ if part.start_with?(':')
+ param_sym = part[1..].to_sym
+ current_node.dynamic_children[param_sym] ||= RouteNode.new
+ dynamic_node = current_node.dynamic_children[param_sym]
+ dynamic_node.param_key = param_sym
+ current_node = dynamic_node
+ else
+ current_node.static_children[part] ||= RouteNode.new
+ current_node = current_node.static_children[part]
+ end
+ end
- current_node.blocks[http_method] = block
- end
+ current_node.blocks[http_method] = block
+ end
- def match_route(parts, http_method, params: {})
- if parts.empty?
- return [blocks[http_method], params] if blocks[http_method]
- else
- part = parts.first
- rest = parts[1..]
+ def match_route(parts, http_method, params: {})
+ if parts.empty?
+ return [blocks[http_method], params] if blocks[http_method]
+ else
+ part = parts.first
+ rest = parts[1..]
- if static_children.key?(part)
- result_block, result_params = static_children[part].match_route(rest, http_method, params:)
- return [result_block, result_params] if result_block
- end
+ if static_children.key?(part)
+ result_block, result_params = static_children[part].match_route(rest, http_method, params:)
+ return [result_block, result_params] if result_block
+ end
- dynamic_children.each_value do |dyn_node|
- new_params = params.dup
- new_params[dyn_node.param_key] = part
- result_block, result_params = dyn_node.match_route(rest, http_method, params: new_params)
+ dynamic_children.each_value do |dyn_node|
+ new_params = params.dup
+ new_params[dyn_node.param_key] = part
+ result_block, result_params = dyn_node.match_route(rest, http_method, params: new_params)
- return [result_block, result_params] if result_block
- end
- end
+ return [result_block, result_params] if result_block
+ end
+ end
- [nil, nil]
- end
+ [nil, nil]
+ end
- # Merge the other RouteNode into the current one
- #
- # @parameter other [RouteNode] The other RouteNode to merge into the current one
- #
- # @return [void]
- #
- def merge!(other)
- self.static_children.merge!(other.static_children)
- self.dynamic_children.merge!(other.dynamic_children)
- self.blocks.merge!(other.blocks)
- end
- end
+ def merge!(other)
+ static_children.merge!(other.static_children)
+ dynamic_children.merge!(other.dynamic_children)
+ blocks.merge!(other.blocks)
+ end
+ end
end
diff --git a/lib/lennarb/version.rb b/lib/lennarb/version.rb
index 5b33469..d67d007 100644
--- a/lib/lennarb/version.rb
+++ b/lib/lennarb/version.rb
@@ -4,7 +4,7 @@
# Copyright, 2023-2024, by Arist贸teles Coutinho.
class Lennarb
- VERSION = '1.2.0'
+ VERSION = '1.3.0'
- public_constant :VERSION
+ public_constant :VERSION
end
diff --git a/license.md b/license.md
index 19dd72a..c4c3d41 100644
--- a/license.md
+++ b/license.md
@@ -1,7 +1,6 @@
# MIT License
-Copyright, 2023-2024, by Arist贸teles Coutinho.
-Copyright, 2023, by aristotelesbr.
+Copyright (c) 2023-2025 Arist贸tels Coutinho
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/test/lib/lennarb/application/test_base.rb b/test/lib/lennarb/application/test_base.rb
index 9e58470..9e017f4 100644
--- a/test/lib/lennarb/application/test_base.rb
+++ b/test/lib/lennarb/application/test_base.rb
@@ -1,229 +1,55 @@
# frozen_string_literal: true
-# Released under the MIT License.
-# Copyright, 2023-2024, by Arist贸teles Coutinho.
-
-require 'test_helper'
require 'json'
+require 'test_helper'
class Lennarb
- module Application
- class TestBase < Minitest::Test
- include Rack::Test::Methods
-
- module TestPlugin
- module InstanceMethods
- def test_plugin_method = 'Plugin Method Executed'
- end
-
- module ClassMethods
- def test_plugin_class_method = 'Plugin Class Method Executed'
- end
-
- def self.setup(base_class, *_args)
- base_class.include InstanceMethods
- base_class.extend ClassMethods
- end
- end
-
- Lennarb::Plugin.register(:test_plugin, TestPlugin)
-
- class UsersController < Lennarb::Application::Base
- plugin :test_plugin
+ module Application
+ class TestSimplifiedBase < Minitest::Test
+ include Rack::Test::Methods
- before do |req, res|
- req['x-users-controller-before-hook'] = 'Users Controller Before Hook'
- end
-
- after do |req, res|
- req['x-users-controller-after-hook'] = 'Users Controller After Hook'
- end
-
- get '/users/test' do |_req, res|
+ class TestApp < Lennarb
+ get '/hello' do |_req, res|
res.status = 200
- res.html('Users Controller Response')
+ res.text('Hello World')
end
- get '/users/plugin' do |_req, res|
- res.status = 200
- res.html(test_plugin_method)
+ post '/users' do |_req, res|
+ res.status = 201
+ res.json({ status: 'created' }.to_json)
end
end
- class PostsController < Lennarb::Application::Base
- get '/posts/test' do |_req, res|
- res.status = 200
- res.html('Posts Controller Response')
- end
- end
-
- class MyApp < Lennarb::Application::Base
- mount UsersController
- mount PostsController
-
- plugin :test_plugin
-
- test_plugin_class_method
-
- before do |context|
- context['x-before-hook'] = 'Before Hook'
- end
-
- after do |context|
- context['x-after-hook'] = 'After Hook'
- end
-
- get '/' do |_req, res|
- res.status = 200
- res.html('GET Response')
- end
+ def app = TestApp.freeze!
- post '/' do |_req, res|
- res.status = 201
- res.html('POST Response')
- end
+ def test_get_request
+ get '/hello'
- post '/json' do |req, res|
- begin
- res.status = 201
- JSON.parse(req.body)
- res.json(req.body)
- rescue JSON::ParserError
- res.status = 500
- result = { error: 'Invalid JSON body' }.to_json
- res.json(result)
- end
- end
-
- get '/plugin' do |_req, res|
- res.status = 200
- res.html(test_plugin_method)
- end
- end
-
- def app = MyApp.run!
-
- def test_users_controller
- get '/users/test'
-
- assert_predicate last_response, :ok?
- assert_equal 'Users Controller Response', last_response.body
+ assert_equal 200, last_response.status
+ assert_equal 'Hello World', last_response.body
+ assert_equal 'text/plain', last_response.headers['content-type']
end
-
- def test_posts_controller
- get '/posts/test'
-
- assert_predicate last_response, :ok?
- assert_equal 'Posts Controller Response', last_response.body
- end
-
- def test_get
- get '/'
- assert_predicate last_response, :ok?
- assert_equal 'GET Response', last_response.body
- end
+ def test_post_request
+ post '/users'
- def test_post
- post '/'
-
- assert_predicate last_response, :created?
- assert_equal 'POST Response', last_response.body
- end
-
- def test_post_with_valid_json_body
- json_body = '{"key":"value"}'
- headers = { 'CONTENT_TYPE' => 'application/json' }
-
- post '/json', json_body, headers
-
- assert_equal 201, last_response.status
- body = JSON.parse(last_response.body)
- assert_equal({ "key" => "value" }, body)
- end
-
- def test_post_with_invalid_json_body
- json_body = '{"key":"value'
-
- headers = { 'CONTENT_TYPE' => 'application/json' }
-
- post '/json', json_body, headers
-
- assert_equal 500, last_response.status
- assert_equal({ "error" => "Invalid JSON body" }, JSON.parse(last_response.body))
- end
-
- def test_before_hooks
- get '/'
-
- assert_predicate last_response, :ok?
- assert_equal 'Before Hook', last_request.env['x-before-hook']
- end
-
- def test_after_hooks
- get '/'
-
- assert_predicate last_response, :ok?
- assert_equal 'After Hook', last_request.env['x-after-hook']
- end
-
- def test_mount_hooks_must_be_executed
- get '/users/test'
-
- assert_equal 'Before Hook', last_request.env['x-before-hook']
- assert_equal 'After Hook', last_request.env['x-after-hook']
- assert_equal 'Users Controller Before Hook', last_request.env['x-users-controller-before-hook']
- assert_equal 'Users Controller After Hook', last_request.env['x-users-controller-after-hook']
- end
-
- def test_mount_routes_with_plugins_must_be_executed
- get '/users/plugin'
-
- assert_predicate last_response, :ok?
- assert_equal 'Plugin Method Executed', last_response.body
- end
-
- def test_enviroment
- ENV['LENNARB_ENV'] = 'test'
-
- assert_predicate MyApp, :test?
-
- ENV['LENNARB_ENV'] = 'production'
-
- assert_predicate MyApp, :production?
-
- ENV['LENNARB_ENV'] = 'development'
-
- assert_predicate MyApp, :development?
- end
-
- def test_render_not_found
- get '/not-found'
-
- assert_predicate last_response, :not_found?
- assert_equal 'Not Found', last_response.body
- end
-
- def test_plugin_method_execution
- get '/plugin'
-
- assert_predicate last_response, :ok?
- assert_equal 'Plugin Method Executed', last_response.body
- end
+ assert_equal 201, last_response.status
+ assert_equal ({ 'status' => 'created' }), JSON.parse(last_response.body)
+ assert_equal 'application/json', last_response.headers['content-type']
+ end
- class MockedMiddleware
- def initialize(app)
- @app = app
- end
+ def test_not_found
+ get '/nonexistent'
- def call(env) = @app.call(env)
- end
+ assert_equal 404, last_response.status
+ assert_equal 'Not Found', last_response.body
+ end
- def test_middlewares
- MyApp.use(MockedMiddleware)
+ def test_method_not_allowed
+ post '/hello'
- assert_includes MyApp._middlewares, [Lennarb::Application::TestBase::MockedMiddleware, [], nil]
- end
- end
- end
+ assert_equal 404, last_response.status
+ end
+ end
+ end
end
diff --git a/test/lib/lennarb/application/test_hooks.rb b/test/lib/lennarb/application/test_hooks.rb
new file mode 100644
index 0000000..02bafd6
--- /dev/null
+++ b/test/lib/lennarb/application/test_hooks.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class Lennarb
+ module Application
+ class HooksTest < Minitest::Test
+ include Rack::Test::Methods
+
+ class AdminController < Lennarb
+ plugin :hooks
+
+ before ['/', '/:id'] do |_req, res|
+ @admin_authenticated = true
+ res['x-admin-authenticated'] = 'x-admin-authenticated'
+ end
+
+ after '/:id' do |_req, res|
+ res.write(' - Processed')
+ end
+
+ get '/' do |_req, res|
+ res.status = 200
+ res.text("Admin Dashboard#{@admin_authenticated ? ' (Authenticated)' : ''}")
+ end
+
+ get '/:id' do |req, res|
+ res.status = 200
+ res.text("Admin User #{req.params[:id]}#{@admin_authenticated ? ' (Authenticated)' : ''}")
+ end
+ end
+
+ class MainApp < Lennarb
+ plugin :mount
+ plugin :hooks
+
+ before '*' do |_req, _res|
+ @global_executed = true
+ end
+
+ after '*' do |_req, res|
+ res.write(' - Done')
+ res['x-global-executed'] = 'x-global-executed'
+ end
+
+ mount AdminController, at: '/admin'
+
+ get '/' do |_req, res|
+ res.status = 200
+ res.text("Home#{@global_executed ? ' (Global)' : ''}")
+ end
+ end
+
+ def app
+ @app ||= MainApp.freeze!
+ end
+
+ def test_global_hooks
+ get '/'
+
+ assert_equal 200, last_response.status
+ assert_equal 'Home (Global) - Done', last_response.body
+ assert_equal 'x-global-executed', last_response.headers['x-global-executed']
+ end
+
+ def test_admin_dashboard_with_hooks
+ get '/admin'
+
+ assert_equal 200, last_response.status
+ assert_equal 'Admin Dashboard (Authenticated) - Done', last_response.body
+ assert_equal 'x-admin-authenticated', last_response.headers['x-admin-authenticated']
+ end
+
+ def test_admin_user_with_hooks
+ get '/admin/123'
+
+ assert_equal 200, last_response.status
+ assert_equal 'Admin User 123 (Authenticated) - Processed - Done', last_response.body
+ assert_equal 'x-admin-authenticated', last_response.headers['x-admin-authenticated']
+ end
+ end
+ end
+end
diff --git a/test/lib/lennarb/application/test_middleware.rb b/test/lib/lennarb/application/test_middleware.rb
new file mode 100644
index 0000000..f049823
--- /dev/null
+++ b/test/lib/lennarb/application/test_middleware.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class Lennarb
+ module Application
+ class MiddlewareTest < Minitest::Test
+ include Rack::Test::Methods
+
+ class TimingMiddleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ start_time = Time.now
+ status, headers, body = @app.call(env)
+ duration = Integer(((Time.now - start_time) * 1000), 10)
+
+ headers = headers.dup # Garantimos que headers seja mut谩vel
+ headers['X-Response-Time'] = duration.to_s
+ [status, headers, body]
+ end
+ end
+
+ class AuthMiddleware
+ def initialize(app, options = {})
+ @app = app
+ @protected_paths = options[:protected_paths] || []
+ end
+
+ def call(env)
+ path = env[Rack::PATH_INFO]
+
+ if protected_route?(path)
+ authenticate_request(env)
+ else
+ @app.call(env)
+ end
+ end
+
+ private
+
+ def protected_route?(path)
+ @protected_paths.any? { |protected_path| path.start_with?(protected_path) }
+ end
+
+ def authenticate_request(env)
+ token = env['HTTP_AUTHORIZATION']
+
+ if token == 'secret-token'
+ status, headers, body = @app.call(env)
+ headers = headers.dup # Garantimos que headers seja mut谩vel
+ headers['X-Auth-Valid'] = 'true'
+ [status, headers, body]
+ else
+ [401, { 'Content-Type' => 'text/plain' }, ['Unauthorized']]
+ end
+ end
+ end
+
+ class TestApp < Lennarb
+ use TimingMiddleware
+ use AuthMiddleware, protected_paths: ['/protected']
+
+ get '/protected' do |_req, res|
+ res.status = 200
+ res.text('Protected Resource')
+ end
+
+ get '/public' do |_req, res|
+ res.status = 200
+ res.text('Public Resource')
+ end
+
+ get '/error' do |_req, _res|
+ raise StandardError, 'Something went wrong'
+ end
+ end
+
+ def app
+ @app ||= TestApp.freeze!
+ end
+
+ def test_public_route
+ get '/public'
+
+ assert_equal 200, last_response.status
+ assert_equal 'Public Resource', last_response.body
+ assert last_response.headers['X-Response-Time'], 'Should have timing header'
+ end
+
+ def test_protected_route_with_valid_token
+ get '/protected', {}, { 'HTTP_AUTHORIZATION' => 'secret-token' }
+
+ assert_equal 200, last_response.status
+ assert_equal 'Protected Resource', last_response.body
+ assert_equal 'true', last_response.headers['X-Auth-Valid']
+ assert last_response.headers['X-Response-Time'], 'Should have timing header'
+ end
+
+ def test_protected_route_without_token
+ get '/protected'
+
+ assert_equal 401, last_response.status
+ assert_equal 'Unauthorized', last_response.body
+ end
+
+ def test_error_handling
+ get '/error'
+
+ assert_equal 500, last_response.status
+ assert_match(/Internal Server Error/, last_response.body)
+ assert last_response.headers['X-Response-Time'], 'Should have timing header'
+ end
+
+ def test_middleware_chain_order
+ get '/protected', {}, { 'HTTP_AUTHORIZATION' => 'secret-token' }
+
+ assert_equal 200, last_response.status
+ assert last_response.headers['X-Response-Time'], 'Timing middleware should be executed'
+ assert_equal 'true', last_response.headers['X-Auth-Valid'], 'Auth middleware should be executed'
+ end
+ end
+ end
+end
diff --git a/test/lib/lennarb/application/test_mount.rb b/test/lib/lennarb/application/test_mount.rb
new file mode 100644
index 0000000..d0897f5
--- /dev/null
+++ b/test/lib/lennarb/application/test_mount.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class Lennarb
+ class MountTest < Minitest::Test
+ include Rack::Test::Methods
+
+ class AdminController < Lennarb
+ get '/' do |_req, res|
+ res.status = 200
+ res.text('Admin Dashboard')
+ end
+
+ get '/users' do |_req, res|
+ res.status = 200
+ res.text('Admin Users')
+ end
+
+ get '/users/:id' do |req, res|
+ res.status = 200
+ res.text("User #{req.params[:id]}")
+ end
+ end
+
+ class ApiController < Lennarb
+ get '/status' do |_req, res|
+ res.status = 200
+ res.json({ status: 'ok' })
+ end
+ end
+
+ class MainApp < Lennarb
+ plugin :mount
+
+ mount AdminController, at: '/admin'
+ mount ApiController, at: '/api'
+
+ get '/' do |_req, res|
+ res.status = 200
+ res.text('Home')
+ end
+ end
+
+ def app = MainApp.freeze!
+
+ def test_routes_with_prefix
+ get '/'
+
+ assert_equal 200, last_response.status
+ assert_equal 'Home', last_response.body
+ end
+
+ def test_admin_dashboard
+ get '/admin'
+
+ assert_equal 200, last_response.status
+ assert_equal 'Admin Dashboard', last_response.body
+ end
+
+ def test_admin_users
+ get '/admin/users'
+
+ assert_equal 200, last_response.status
+ assert_equal 'Admin Users', last_response.body
+ end
+ end
+end
diff --git a/test/lib/lennarb/application/test_plugin.rb b/test/lib/lennarb/application/test_plugin.rb
new file mode 100644
index 0000000..4cf4c33
--- /dev/null
+++ b/test/lib/lennarb/application/test_plugin.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class Lennarb
+ module Application
+ class TestPlugin < Minitest::Test
+ include Rack::Test::Methods
+
+ module TextHelpers
+ module InstanceMethods
+ def trim_text(text)
+ text.strip
+ end
+ end
+
+ def self.configure(app, *)
+ app.include(InstanceMethods)
+ end
+ end
+
+ Lennarb::Plugin.register(:text_helpers, TextHelpers)
+
+ class TestApp < Lennarb
+ plugin :text_helpers
+
+ get '/api/users' do |_req, res|
+ result_from_other_service = ' some text with spaces '
+
+ result = trim_text(result_from_other_service)
+ res.status = 200
+ res.text(result)
+ end
+ end
+
+ def app = TestApp.freeze!
+
+ def test_plugin_extended_route
+ get '/api/users'
+
+ assert_equal 200, last_response.status
+ assert_equal 'some text with spaces', last_response.body
+ end
+ end
+ end
+end
diff --git a/test/lib/lennarb/test_plugin.rb b/test/lib/lennarb/test_plugin.rb
index 418d973..46a63b4 100644
--- a/test/lib/lennarb/test_plugin.rb
+++ b/test/lib/lennarb/test_plugin.rb
@@ -6,42 +6,63 @@
require 'test_helper'
class Lennarb
- class TestPlugin < Minitest::Test
- module SimplePlugin
- module InstanceMethods
- def test_instance_method
- 'instance method executed'
- end
- end
-
- module ClassMethods
- def test_class_method
- 'class method executed'
- end
- end
- end
-
- def setup
- @plugin_name = :test_plugin
- Lennarb::Plugin.register(@plugin_name, SimplePlugin)
- end
-
- def teardown
- Lennarb::Plugin.plugins.clear
- end
-
- def test_register_plugin
- assert_equal SimplePlugin, Lennarb::Plugin.plugins[@plugin_name]
- end
-
- def test_load_plugin
- loaded_plugin = Lennarb::Plugin.load(@plugin_name)
-
- assert_equal SimplePlugin, loaded_plugin
- end
-
- def test_load_unregistered_plugin_raises_error
- assert_raises(LennarbError) { Lennarb::Plugin.load(:nonexistent_plugin) }
- end
- end
+ class TestPlugin < Minitest::Test
+ module SimplePlugin
+ module InstanceMethods
+ def test_instance_method
+ 'instance method executed'
+ end
+ end
+
+ module ClassMethods
+ def test_class_method
+ 'class method executed'
+ end
+ end
+ end
+
+ def setup
+ @plugin_name = :test_plugin
+ Lennarb::Plugin.register(@plugin_name, SimplePlugin)
+ end
+
+ def teardown
+ Lennarb::Plugin.registry.clear
+ end
+
+ def test_register_plugin
+ assert_equal SimplePlugin, Lennarb::Plugin.registry[@plugin_name]
+ end
+
+ def test_load_plugin
+ loaded_plugin = Lennarb::Plugin.load(@plugin_name)
+
+ assert_equal SimplePlugin, loaded_plugin
+ end
+
+ def test_load_unregistered_plugin_raises_error
+ assert_raises(Lennarb::Plugin::Error) { Lennarb::Plugin.load(:nonexistent_plugin) }
+ end
+
+ def test_load_defaults
+ Lennarb::Plugin.load_defaults!
+
+ assert Lennarb::Plugin.instance_variable_get(:@defaults_loaded)
+ end
+
+ def test_load_defaults_does_not_reload
+ Lennarb::Plugin.load_defaults!
+ Lennarb::Plugin.load_defaults!
+
+ assert Lennarb::Plugin.instance_variable_get(:@defaults_loaded)
+ end
+
+ def test_load_defaults_environment_variable
+ ENV['LENNARB_AUTO_LOAD_DEFAULTS'] = 'false'
+
+ refute_predicate Lennarb::Plugin, :load_defaults?
+ ensure
+ ENV.delete('LENNARB_AUTO_LOAD_DEFAULTS')
+ end
+ end
end
diff --git a/test/lib/lennarb/test_request.rb b/test/lib/lennarb/test_request.rb
index 064c2ea..522cb0a 100644
--- a/test/lib/lennarb/test_request.rb
+++ b/test/lib/lennarb/test_request.rb
@@ -6,47 +6,53 @@
require 'test_helper'
class Lennarb
- class TestRequest < Minitest::Test
- def test_initialize
- request = Lennarb::Request.new({})
+ class TestRequest < Minitest::Test
+ def test_initialize
+ request = Lennarb::Request.new({})
- assert_instance_of(Lennarb::Request, request)
- end
+ assert_instance_of(Lennarb::Request, request)
+ end
- def test_params
- request = Lennarb::Request.new({ 'QUERY_STRING' => 'foo=bar' })
+ def test_params
+ request = Lennarb::Request.new({ 'QUERY_STRING' => 'foo=bar' })
- assert_equal({ 'foo' => 'bar' }, request.params)
- end
+ assert_equal({ foo: 'bar' }, request.params)
+ end
- def test_query_params
- request = Lennarb::Request.new({ 'QUERY_STRING' => 'foo=bar' })
+ def test_query_params
+ request = Lennarb::Request.new({ 'QUERY_STRING' => 'foo=bar' })
- assert_equal({ 'foo' => 'bar' }, request.__send__(:query_params))
- end
+ assert_equal({ foo: 'bar' }, request.__send__(:query_params))
+ end
- def test_query_params_with_empty_query_string
- request = Lennarb::Request.new({})
+ def test_query_params_with_empty_query_string
+ request = Lennarb::Request.new({})
- assert_empty(request.__send__(:query_params))
- end
+ assert_empty(request.__send__(:query_params))
+ end
- def test_content_type
- request = Lennarb::Request.new({ 'CONTENT_TYPE' => 'application/json' })
+ def test_content_type
+ request = Lennarb::Request.new({ 'HTTP_CONTENT_TYPE' => 'application/json' })
- assert_equal('application/json', request.content_type)
- end
+ assert_equal('application/json', request.content_type)
+ end
- def test_content_length
- request = Lennarb::Request.new({ 'CONTENT_LENGTH' => '42' })
+ def test_content_length
+ request = Lennarb::Request.new({ 'HTTP_CONTENT_LENGTH' => '42' })
- assert_equal('42', request.content_length)
- end
+ assert_equal('42', request.content_length)
+ end
- def test_body
- request = Lennarb::Request.new({ 'rack.input' => StringIO.new('foo') })
+ def test_body
+ request = Lennarb::Request.new({ 'rack.input' => StringIO.new('foo') })
- assert_equal('foo', request.body)
- end
- end
+ assert_equal('foo', request.body)
+ end
+
+ def test_path
+ request = Lennarb::Request.new({ 'PATH_INFO' => '/foo' })
+
+ assert_equal('/foo', request.path)
+ end
+ end
end
diff --git a/test/lib/lennarb/test_response.rb b/test/lib/lennarb/test_response.rb
index 45f01ab..9566bb2 100644
--- a/test/lib/lennarb/test_response.rb
+++ b/test/lib/lennarb/test_response.rb
@@ -6,64 +6,64 @@
require 'test_helper'
class Lennarb
- class TestResponse < Minitest::Test
- def test_default_instance_variables
- response = Lennarb::Response.new
+ class TestResponse < Minitest::Test
+ def test_default_instance_variables
+ response = Lennarb::Response.new
- assert_equal 404, response.status
- assert_empty(response.headers)
- assert_empty response.body
- assert_equal 0, response.length
- end
+ assert_equal 404, response.status
+ assert_empty(response.headers)
+ assert_empty response.body
+ assert_equal 0, response.length
+ end
- def test_set_and_get_response_header
- response = Lennarb::Response.new
+ def test_set_and_get_response_header
+ response = Lennarb::Response.new
- response['location'] = '/'
+ response['location'] = '/'
- assert_equal '/', response['location']
- end
+ assert_equal '/', response['location']
+ end
- def test_write_to_response_body
- response = Lennarb::Response.new
+ def test_write_to_response_body
+ response = Lennarb::Response.new
- response.write('Hello World!')
+ response.write('Hello World!')
- assert_equal 'Hello World!', response.body.first
- end
+ assert_equal 'Hello World!', response.body.first
+ end
- def test_set_response_status
- response = Lennarb::Response.new
+ def test_set_response_status
+ response = Lennarb::Response.new
- response.status = 200
+ response.status = 200
- assert_equal 200, response.status
- end
+ assert_equal 200, response.status
+ end
- def test_set_response_content_type
- response = Lennarb::Response.new
+ def test_set_response_content_type
+ response = Lennarb::Response.new
- response['content-type'] = 'text/html'
+ response['content-type'] = 'text/html'
- assert_equal 'text/html', response['content-type']
- end
+ assert_equal 'text/html', response['content-type']
+ end
- def test_set_response_content_length
- response = Lennarb::Response.new
+ def test_set_response_content_length
+ response = Lennarb::Response.new
- response['content-length'] = 12
+ response['content-length'] = 12
- assert_equal 12, response['content-length']
- end
+ assert_equal 12, response['content-length']
+ end
- def test_finish_response
- response = Lennarb::Response.new
- response['content-type'] = 'text/plain'
+ def test_finish_response
+ response = Lennarb::Response.new
+ response['content-type'] = 'text/plain'
- response.finish
+ response.finish
- assert_equal 0, response.length
- assert_equal 'text/plain', response['content-type']
- end
- end
+ assert_equal 0, response.length
+ assert_equal 'text/plain', response['content-type']
+ end
+ end
end
diff --git a/test/lib/lennarb/test_route_node.rb b/test/lib/lennarb/test_route_node.rb
index e5a6073..910223a 100644
--- a/test/lib/lennarb/test_route_node.rb
+++ b/test/lib/lennarb/test_route_node.rb
@@ -6,74 +6,74 @@
require 'test_helper'
class Lennarb
- class RouteNodeTest < Minitest::Test
- def setup
- @route_node = Lennarb::RouteNode.new
+ class RouteNodeTest < Minitest::Test
+ def setup
+ @route_node = Lennarb::RouteNode.new
- @route_node.add_route(['posts'], 'GET', proc { 'List of posts' })
- @route_node.add_route(['posts', ':id'], 'GET', proc { |id| "Post #{id}" })
- end
+ @route_node.add_route(['posts'], 'GET', proc { 'List of posts' })
+ @route_node.add_route(['posts', ':id'], 'GET', proc { |id| "Post #{id}" })
+ end
- def test_add_route
- assert @route_node.static_children.key?('posts')
- dynamic_node = @route_node.static_children['posts'].dynamic_children[:id]
+ def test_add_route
+ assert @route_node.static_children.key?('posts')
+ dynamic_node = @route_node.static_children['posts'].dynamic_children[:id]
- refute_nil dynamic_node
- end
+ refute_nil dynamic_node
+ end
- def test_match_valid_route
- block, params = @route_node.match_route(['posts'], 'GET')
+ def test_match_valid_route
+ block, params = @route_node.match_route(['posts'], 'GET')
- assert_equal 'List of posts', block.call
- assert_empty params
- end
+ assert_equal 'List of posts', block.call
+ assert_empty params
+ end
- def test_match_route_with_parameters
- block, params = @route_node.match_route(%w[posts 123], 'GET')
+ def test_match_route_with_parameters
+ block, params = @route_node.match_route(%w[posts 123], 'GET')
- assert_equal 'Post 123', block.call(params[:id])
- assert_equal({ id: '123' }, params)
- end
+ assert_equal 'Post 123', block.call(params[:id])
+ assert_equal({ id: '123' }, params)
+ end
- def test_match_invalid_route
- block, params = @route_node.match_route(['unknown'], 'GET')
+ def test_match_invalid_route
+ block, params = @route_node.match_route(['unknown'], 'GET')
- assert_nil block
- assert_nil params
- end
+ assert_nil block
+ assert_nil params
+ end
- def test_different_variables_in_common_nested_routes
- router = Lennarb::RouteNode.new
- router.add_route(['foo', ':foo'], 'GET', proc { 'foo' })
- router.add_route(%w[foo special], 'GET', proc { 'special' })
- router.add_route(['foo', ':id', 'bar'], 'GET', proc { 'bar' })
+ def test_different_variables_in_common_nested_routes
+ router = Lennarb::RouteNode.new
+ router.add_route(['foo', ':foo'], 'GET', proc { 'foo' })
+ router.add_route(%w[foo special], 'GET', proc { 'special' })
+ router.add_route(['foo', ':id', 'bar'], 'GET', proc { 'bar' })
- _, params = router.match_route(%w[foo 23], 'GET')
+ _, params = router.match_route(%w[foo 23], 'GET')
- assert_equal({ foo: '23' }, params)
- _, params = router.match_route(%w[foo special], 'GET')
+ assert_equal({ foo: '23' }, params)
+ _, params = router.match_route(%w[foo special], 'GET')
- assert_empty(params)
- _, params = router.match_route(%w[foo 24 bar], 'GET')
+ assert_empty(params)
+ _, params = router.match_route(%w[foo 24 bar], 'GET')
- assert_equal({ id: '24' }, params)
- end
+ assert_equal({ id: '24' }, params)
+ end
- def test_merge
- router = Lennarb::RouteNode.new
- router.add_route(['posts'], 'GET', proc { 'List of posts' })
- router.add_route(['posts', ':id'], 'GET', proc { |id| "Post #{id}" })
+ def test_merge
+ router = Lennarb::RouteNode.new
+ router.add_route(['posts'], 'GET', proc { 'List of posts' })
+ router.add_route(['posts', ':id'], 'GET', proc { |id| "Post #{id}" })
- router.merge!(router)
- end
+ router.merge!(router)
+ end
- def test_merge_variables_in_different_routes
- router = Lennarb::RouteNode.new
- router.add_route(['posts'], 'GET', proc { 'List of posts' })
- router.add_route(['posts', ':id'], 'GET', proc { |id| "Post #{id}" })
- router.add_route(['posts', ':id', 'comments'], 'GET', proc { |id| "Comments of post #{id}" })
+ def test_merge_variables_in_different_routes
+ router = Lennarb::RouteNode.new
+ router.add_route(['posts'], 'GET', proc { 'List of posts' })
+ router.add_route(['posts', ':id'], 'GET', proc { |id| "Post #{id}" })
+ router.add_route(['posts', ':id', 'comments'], 'GET', proc { |id| "Comments of post #{id}" })
- router.merge!(router)
- end
- end
+ router.merge!(router)
+ end
+ end
end
diff --git a/test/lib/test_lennarb.rb b/test/lib/test_lennarb.rb
index ea51caf..8cf9bca 100644
--- a/test/lib/test_lennarb.rb
+++ b/test/lib/test_lennarb.rb
@@ -7,78 +7,84 @@
require 'test_helper'
class TestLennarb < Minitest::Test
- include Rack::Test::Methods
-
- def test_version
- version = Lennarb::VERSION
-
- assert_kind_of String, version
- end
-
- def test_initialize
- app = Lennarb.new
-
- assert_kind_of Lennarb, app
- assert_respond_to app, :call
- end
-
- def test_initialize_with_block
- app = Lennarb.new { |route| route.get('/users') { |_req, res| res.text('Hello World!') } }
-
- assert_kind_of Lennarb, app
- assert_respond_to app, :call
- end
-
- def app
- Lennarb.new do |route|
- route.get '/users' do |_req, res|
- res[Rack::CONTENT_TYPE] = 'text/html'
- res.status = 200
- res.html('Hello World!')
- end
-
- route.get '/users/:id' do |req, res|
- id = req.params[:id]
-
- res[Rack::CONTENT_TYPE] = 'text/plain'
- res.status = 200
- res.text("User #{id}")
- end
-
- route.post '/users' do |req, res|
- name = req.params['name']
- res[Rack::CONTENT_TYPE] = 'application/json'
- res.status = 201
-
- res.write({ name: }.to_json)
- end
- end
- end
-
- def test_get
- get '/users'
-
- assert_equal 200, last_response.status
- assert_equal 'Hello World!', last_response.body
- assert_equal 'text/html', last_response.headers[Rack::CONTENT_TYPE]
- end
-
- def test_get_with_paramsf
- get '/users/1'
-
- assert_equal 200, last_response.status
- assert_equal 'User 1', last_response.body
- assert_equal 'text/plain', last_response.headers[Rack::CONTENT_TYPE]
- end
-
- def test_post
- post '/users'
-
- params = { name: 'Arist贸teles Coutinho' }
- post '/users', params
-
- assert_equal 201, last_response.status
- assert_equal params, ::JSON.parse(last_response.body, symbolize_names: true)
- assert_equal 'application/json', last_response.headers[Rack::CONTENT_TYPE]
- end
+ include Rack::Test::Methods
+
+ def test_version
+ version = Lennarb::VERSION
+
+ assert_kind_of String, version
+ end
+
+ def test_initialize
+ app = Lennarb.new
+
+ assert_kind_of Lennarb, app
+ assert_respond_to app, :call
+ end
+
+ def test_initialize_with_block
+ app = Lennarb.new { |route| route.get('/users') { |_req, res| res.text('Hello World!') } }
+
+ assert_kind_of Lennarb, app
+ assert_respond_to app, :call
+ end
+
+ def app
+ Lennarb.new do |route|
+ route.get '/users' do |_req, res|
+ res[Rack::CONTENT_TYPE] = 'text/html'
+ res.status = 200
+ res.html('Hello World!')
+ end
+
+ route.get '/users/:id' do |req, res|
+ id = req.params[:id]
+
+ res[Rack::CONTENT_TYPE] = 'text/plain'
+ res.status = 200
+ res.text("User #{id}")
+ end
+
+ route.post '/users' do |req, res|
+ name = req.params[:name]
+ res[Rack::CONTENT_TYPE] = 'application/json'
+ res.status = 201
+
+ res.write({ name: }.to_json)
+ end
+ end
+ end
+
+ def test_get
+ get '/users'
+
+ assert_equal 200, last_response.status
+ assert_equal 'Hello World!', last_response.body
+ assert_equal 'text/html', last_response.headers[Rack::CONTENT_TYPE]
+ end
+
+ def test_get_with_paramsf
+ get '/users/1'
+
+ assert_equal 200, last_response.status
+ assert_equal 'User 1', last_response.body
+ assert_equal 'text/plain', last_response.headers[Rack::CONTENT_TYPE]
+ end
+
+ def test_post
+ params = { name: 'Arist贸teles Coutinho' }
+ post '/users', params
+
+ assert_equal 201, last_response.status
+ assert_equal params, JSON.parse(last_response.body, symbolize_names: true)
+ assert_equal 'application/json', last_response.headers[Rack::CONTENT_TYPE]
+ end
+
+ def test_not_found
+ get '/not_found'
+
+ assert_equal 404, last_response.status
+ assert_equal 'Not Found', last_response.body
+ assert_equal 'text/plain', last_response.headers[Rack::CONTENT_TYPE]
+ end
end