From ab1f281566f69457dc3b1cfc5474bb6665673df1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arist=C3=B3teles?= Date: Fri, 17 May 2024 17:52:15 -0300 Subject: [PATCH 1/8] test(plugin): ensure basic plugin system --- test/lib/lennarb/test_plugin.rb | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 test/lib/lennarb/test_plugin.rb diff --git a/test/lib/lennarb/test_plugin.rb b/test/lib/lennarb/test_plugin.rb new file mode 100644 index 0000000..0d258ae --- /dev/null +++ b/test/lib/lennarb/test_plugin.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2023-2024, by Aristóteles Coutinho. + +require 'test_helper' + +class Lennarb + class TestPlugin < Minitest::Test + def setup + @plugin_name = :test_plugin + @plugin_module = Module.new do + def test_method + "test" + end + end + + Lennarb::Plugin.register(@plugin_name, @plugin_module) + end + + def teardown + Lennarb::Plugin.plugins.clear + end + + def test_register_plugin + assert_equal @plugin_module, Lennarb::Plugin.plugins[@plugin_name] + end + + def test_load_plugin + loaded_plugin = Lennarb::Plugin.load(@plugin_name) + assert_equal @plugin_module, loaded_plugin + end + + def test_load_unregistered_plugin_raises_error + assert_raises(RuntimeError) { Lennarb::Plugin.load(:nonexistent_plugin) } + end + end +end From 51abc99e932b9badcbafe2857c36c71c83bb8471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arist=C3=B3teles?= Date: Fri, 17 May 2024 17:52:40 -0300 Subject: [PATCH 2/8] feat(plugin): add basic plugin system --- lib/lennarb.rb | 8 ++++++-- lib/lennarb/plugin.rb | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 lib/lennarb/plugin.rb diff --git a/lib/lennarb.rb b/lib/lennarb.rb index 1124668..a61b2fa 100644 --- a/lib/lennarb.rb +++ b/lib/lennarb.rb @@ -3,8 +3,6 @@ # Released under the MIT License. # Copyright, 2023-2024, by Aristóteles Coutinho. -ENV['RACK_ENV'] ||= 'development' - # Core extensions # require 'pathname' @@ -13,6 +11,7 @@ # Base class for Lennarb # require_relative 'lennarb/application/base' +require_relative 'lennarb/plugin' require_relative 'lennarb/request' require_relative 'lennarb/response' require_relative 'lennarb/route_node' @@ -91,6 +90,11 @@ 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) + plugin_module = Lennarb::Plugin.load(plugin_name) + extend plugin_module + end + private # Add a route diff --git a/lib/lennarb/plugin.rb b/lib/lennarb/plugin.rb new file mode 100644 index 0000000..b2ed3b4 --- /dev/null +++ b/lib/lennarb/plugin.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2023-2024, by Aristóteles Coutinho. + +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 From f6613c7456499e32842cf32a8855f7b35b4798c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arist=C3=B3teles?= Date: Fri, 17 May 2024 17:54:07 -0300 Subject: [PATCH 3/8] test(base): ensure plugins works with base app --- test/lib/lennarb/application/test_base.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/lib/lennarb/application/test_base.rb b/test/lib/lennarb/application/test_base.rb index 94491f9..d0835fe 100644 --- a/test/lib/lennarb/application/test_base.rb +++ b/test/lib/lennarb/application/test_base.rb @@ -10,7 +10,15 @@ module Application class TestBase < Minitest::Test include Rack::Test::Methods + module TestPlugin + def test_plugin_method = 'Plugin Method Executed' + end + + Lennarb::Plugin.register(:test_plugin, TestPlugin) + class MyApp < Lennarb::Application::Base + plugin :test_plugin + before do |context| context['x-before-hook'] = 'Before Hook' end @@ -28,6 +36,11 @@ class MyApp < Lennarb::Application::Base res.status = 201 res.html('POST Response') end + + get '/plugin' do |_req, res| + res.status = 200 + res.html(test_plugin_method) + end end def app = MyApp.run! @@ -81,6 +94,13 @@ def test_render_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 + class MockedMiddleware def initialize(app) @app = app From 05c0435aa20f0cd175e79b12595f1a400d1d7520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arist=C3=B3teles?= Date: Fri, 17 May 2024 17:54:40 -0300 Subject: [PATCH 4/8] feat(base): add plugin support --- lib/lennarb/application/base.rb | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/lib/lennarb/application/base.rb b/lib/lennarb/application/base.rb index 6671826..77f6723 100644 --- a/lib/lennarb/application/base.rb +++ b/lib/lennarb/application/base.rb @@ -41,7 +41,7 @@ def self.inherited(subclass) subclass.instance_variable_set(:@_before_hooks, Lennarb::RouteNode.new) end - def self.get(...) = @_route.get(...) + def self.get(path, &block) = @_route.get(path, &block) def self.put(...) = @_route.put(...) def self.post(...) = @_route.post(...) def self.head(...) = @_route.head(...) @@ -107,24 +107,14 @@ def self.after(path = nil, &block) # - Rack::MethodOverride # - Rack::Head # - Rack::ContentLength - # - Rack::Static # def self.run! stack = Rack::Builder.new - use Rack::ShowExceptions - use Rack::Lint + use Rack::ShowExceptions if test? || development? use Rack::MethodOverride use Rack::Head use Rack::ContentLength - use Rack::Static, - root: 'public', - urls: ['/404.html', '/500.html'], - header_rules: [ - [200, %w[html], { 'content-type' => 'text/html; charset=utf-8' }], - [400, %w[html], { 'content-type' => 'text/html; charset=utf-8' }], - [500, %w[html], { 'content-type' => 'text/html; charset=utf-8' }] - ] middlewares.each do |(middleware, args, block)| stack.use(middleware, *args, &block) @@ -143,11 +133,6 @@ def self.run! puts e.message.red puts e.backtrace raise e - end.then do |response| - case response - in [400..499, _, _] then render_not_found - else response - end end end end @@ -157,8 +142,8 @@ def self.run! stack.to_app end - def self.test? = ENV['RACK_ENV'] == 'test' || ENV['LENNARB_ENV'] == 'test' - def self.production? = ENV['RACK_ENV'] == 'production' || ENV['LENNARB_ENV'] == 'production' + def self.test? = ENV['RACK_ENV'] == 'test' || ENV['LENNARB_ENV'] == 'test' + def self.production? = ENV['RACK_ENV'] == 'production' || ENV['LENNARB_ENV'] == 'production' def self.development? = ENV['RACK_ENV'] == 'development' || ENV['LENNARB_ENV'] == 'development' # Render a not found @@ -246,7 +231,9 @@ def self.parse_path(env) = env[Rack::PATH_INFO]&.split('/')&.reject(&:empty?) # def self.html_request?(env) = env['HTTP_ACCEPT']&.include?('text/html') - private_class_method :execute_hooks, :execute_global_hooks, :execute_route_hooks, :parse_path, :html_request? + def self.plugin(plugin_name) = @_route.plugin(plugin_name) + + private_class_method :execute_hooks, :execute_global_hooks, :execute_route_hooks, :parse_path, :html_request? end end end From 0e2a4101b254d212f6dcd56226c33a630b67456b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arist=C3=B3teles?= Date: Fri, 17 May 2024 17:55:05 -0300 Subject: [PATCH 5/8] chore: bump version --- lib/lennarb/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/lennarb/version.rb b/lib/lennarb/version.rb index 333c5d9..8e852bb 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 = '0.5.1' + VERSION = '0.6.0' public_constant :VERSION end From 7f7cbc4bd3f7f86bfe720d7f012300bc63fbe701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arist=C3=B3teles?= Date: Fri, 17 May 2024 18:14:45 -0300 Subject: [PATCH 6/8] fix: fix software design --- lib/lennarb.rb | 6 + lib/lennarb/application/base.rb | 352 ++++++++++++++++---------------- lib/lennarb/version.rb | 2 +- 3 files changed, 186 insertions(+), 174 deletions(-) diff --git a/lib/lennarb.rb b/lib/lennarb.rb index a61b2fa..51595f5 100644 --- a/lib/lennarb.rb +++ b/lib/lennarb.rb @@ -90,6 +90,12 @@ 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) + # Register a plugin + # + # @parameter [String | Symbol] plugin_name + # + # @returns [void] + # def plugin(plugin_name) plugin_module = Lennarb::Plugin.load(plugin_name) extend plugin_module diff --git a/lib/lennarb/application/base.rb b/lib/lennarb/application/base.rb index 77f6723..009a78c 100644 --- a/lib/lennarb/application/base.rb +++ b/lib/lennarb/application/base.rb @@ -32,97 +32,97 @@ class Base # # @returns [Base] # - def self.inherited(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 self.get(path, &block) = @_route.get(path, &block) - def self.put(...) = @_route.put(...) - def self.post(...) = @_route.post(...) - def self.head(...) = @_route.head(...) - def self.match(...) = @_route.match(...) - def self.patch(...) = @_route.patch(...) - def self.delete(...) = @_route.delete(...) - def self.options(...) = @_route.options(...) - - # @returns [Array] middlewares - def self.middlewares = @_middlewares - - # Use a middleware - # - # @parameter [Object] middleware - # @parameter [Array] args - # @parameter [Block] block - # - # @returns [Array] middlewares - # - def self.use(middleware, *args, &block) - @_middlewares << [middleware, args, block] - end - # Add a before hook - # - # @parameter [String] path - # @parameter [Block] block - # - def self.before(path = nil, &block) - if path - parts = path.split('/').reject(&:empty?) - @_before_hooks.add_route(parts, :before, block) - else - @_global_before_hooks << block + class << self + def inherited(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 - end - # Add a after hook - # - # @parameter [String] path - # @parameter [Block] block - # - def self.after(path = nil, &block) - if path - parts = path.split('/').reject(&:empty?) - @_after_hooks.add_route(parts, :after, block) - else - @_global_after_hooks << block + def get(path, &) = @_route.get(path, &) + 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 + + # 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 - 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::Lint - # - Rack::MethodOverride - # - Rack::Head - # - Rack::ContentLength - # - def self.run! - stack = Rack::Builder.new - use Rack::ShowExceptions if test? || development? - use Rack::MethodOverride - use Rack::Head - use Rack::ContentLength + # 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 - middlewares.each do |(middleware, args, block)| - stack.use(middleware, *args, &block) + # 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 - stack.run ->(env) do - catch(:halt) do - begin + # 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 + + stack.run ->(env) do + catch(:halt) do execute_hooks(@_before_hooks, env, :before) res = @_route.call(env) execute_hooks(@_after_hooks, env, :after) @@ -135,105 +135,111 @@ def self.run! raise e end end - end - @_route.freeze! + @_route.freeze! - stack.to_app - end - - def self.test? = ENV['RACK_ENV'] == 'test' || ENV['LENNARB_ENV'] == 'test' - def self.production? = ENV['RACK_ENV'] == 'production' || ENV['LENNARB_ENV'] == 'production' - def self.development? = ENV['RACK_ENV'] == 'development' || ENV['LENNARB_ENV'] == 'development' - - # Render a not found - # - # @returns [void] - # - def self.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 self.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 + stack.to_app + end - # Redirect to a path - # - # @parameter [String] path - # @parameter [Integer] status default is 302 - # - def self.redirect(path, status = 302) = throw :halt, [status, { 'location' => path }, []] + 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 - # Execute the hooks - # - # @parameter [RouteNode] hook_route - # @parameter [Hash] env - # @parameter [Symbol] action - # - # @returns [void] - # - def self.execute_hooks(hook_route, env, action) - execute_global_hooks(env, action) + # 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 - execute_route_hooks(hook_route, env, action) - end + # Redirect to a path + # + # @parameter [String] path + # @parameter [Integer] status default is 302 + # + def redirect(path, status = 302) = throw :halt, [status, { 'location' => path }, []] + + # To use a plugin + # + # @parameter [String | Symbol] plugin_name + # + # @returns [void] + # + def plugin(plugin_name) = @_route.plugin(plugin_name) + + 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 self.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 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 self.execute_route_hooks(hook_route, env, action) - parts = parse_path(env) - return unless parts + # 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 - block, = hook_route.match_route(parts, action) - block&.call(env) + # 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 - - # Parse the path - # - # @parameter [Hash] env - # - # @returns [Array] parts - # - def self.parse_path(env) = env[Rack::PATH_INFO]&.split('/')&.reject(&:empty?) - - # Check if the request is a HTML request - # - # @parameter [Hash] env - # - # @returns [Boolean] - # - def self.html_request?(env) = env['HTTP_ACCEPT']&.include?('text/html') - - def self.plugin(plugin_name) = @_route.plugin(plugin_name) - - private_class_method :execute_hooks, :execute_global_hooks, :execute_route_hooks, :parse_path, :html_request? end end end diff --git a/lib/lennarb/version.rb b/lib/lennarb/version.rb index 8e852bb..5aae1a8 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 = '0.6.0' + VERSION = '0.6.1' public_constant :VERSION end From b020d08d93d03d71420d963f1bdb45626a41be2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arist=C3=B3teles?= Date: Fri, 17 May 2024 18:15:16 -0300 Subject: [PATCH 7/8] docs: add plugin guides --- guides/plugin/readme.md | 69 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 guides/plugin/readme.md diff --git a/guides/plugin/readme.md b/guides/plugin/readme.md new file mode 100644 index 0000000..2256a08 --- /dev/null +++ b/guides/plugin/readme.md @@ -0,0 +1,69 @@ +# Plugin System + +## 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. + +## Implementation Details + +The plugin system is implemented using a module, `Lennarb::Plugin`, which centralizes the registration and loading of plugins. + +### Module: `Plugins` + +```ruby +class Lennarb + module Plugins + @plugins = {} + + def self.register_plugin(name, mod) + @plugins[name] = mod + end + + def self.load_plugin(name) + @plugins[name] || raise("Plugin #{name} did not register itself correctly") + end + + def self.plugins + @plugins + end + end +end +``` + +## Usage + +### Registering a Plugin + +To register a plugin, define a module with the desired functionality and register it with the Plugin module. + +```ruby +module MyCustomPlugin + def custom_method + "Custom Method Executed" + end +end + +Plugins.register(:my_custom_plugin, MyCustomPlugin) +``` + +### Load and Use a Plugin + +To load and use a plugin in your Lennarb application, call the plugin method in your application class. + +```ruby +class MyApp < Lennarb::Application::Base + plugin :my_custom_plugin + + get '/custom' do |_req, res| + res.status = 200 + res.html(custom_method) + end +end +``` + +In this example, the custom_method defined in `MyCustomPlugin` is available in the routes of `MyApp`. + +## Conclusion + +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. + From 525f50c7198f6ee53dc4adf6327e70bd78bf69ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arist=C3=B3teles?= Date: Fri, 17 May 2024 18:20:03 -0300 Subject: [PATCH 8/8] docs: update changelog --- changelog.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/changelog.md b/changelog.md index 384bec8..c782c0f 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.6.1] - 2024-05-17 + +### Added + +- Add `Lennarb::Plugin` module to manage the plugins in the project. Now, the `Lennarb` class is the main class of the project. +- Add `Lennarb::Plugin::Base` class to be the base class of the plugins in the project. +- Add simple guide to use `Lenn` plugins. See [guides/plugins/readme.md](guides/plugins/readme.md) for more details. + ## [0.4.4] - 2024-04-02 ### Added