Fullmoon Lua Web Framework

repository·master·Indexed 20 days ago

https://github.com/pkulchenko/fullmoon

A fast, minimalistic Lua web framework built on the Redbean portable web server. Fullmoon provides a 'batteries-included' experience for building cross-platform web applications in a single-file distributable format, featuring routing with parameters and splats, a template engine with JSON support, session and cookie management, response streaming, Server-Sent Events (SSE), and database schema migrations.

Tokens
10.5K
Snippets
33
Records
45
Agent score
23%

What's inside fullmoon

  1. Check Fullmoon project status and stability

    master

    Fullmoon is currently highly experimental, and all features are subject to change.

    Stability notes:

    • Core Components: Generally stable; updates are infrequent since v0.3.
    • Interfaces: Documented interfaces are more stable than undocumented ones.
    • Compatibility: Commits that modify interfaces are marked with a COMPAT label to help users identify potential breaking changes.
    • Deprecations: Obsolete methods may still exist in the codebase but will log a warning when used before eventual removal.
  2. Fullmoon Overview

    master

    Fullmoon is a fast, minimalistic web framework built on top of Redbean, a portable, single-file web server. It is designed to be lightweight (~1700 LOC) with no external dependencies, providing a 'batteries included' experience for Lua developers.

    Key features added by Fullmoon include:

    • Simple and flexible routing with parameters and custom filters.
    • A template engine with JSON support.
    • Response streaming and Server-Sent Events (SSE) support.
    • Cookie, header, and session management.
    • Multipart message processing for file uploads.
    • Form validation and Cron-based scheduling.
    • Database management with schema migrations.
    • Access to all underlying Redbean features (SSL, crypto, SQLite, etc.).
  3. Implement action handlers and route processing

    master

    An action handler is a function passed to setRoute. Multiple handlers can be chained for a single route. The processing stops as soon as a handler returns a non-false value.

    Handler Return Values:

    • true: Stops processing, sets specified headers, and returns the response body.
    • false or nil: Stops the current route's processing and proceeds to the next registered route (useful for middleware/filters).
    • string: Sends a 200 OK response with the string as the body. Content-Type is inferred.
    • function: Executes a serve* method and signals the end of processing.
    • Other values: Treated as true (a warning is logged).

    Chaining Handlers (Middleware Pattern):

    To implement logic that applies to multiple routes (like user authentication), return false from the first handler to allow subsequent handlers to run.

    local uroute = "/user/:id"
    fm.setRoute({uroute.."/*", method = {"GET", "POST", otherwise = 405}},
        function(r)
          -- retrieve user information and store in r.user
          return false -- continue handling to the next route
      end)
    fm.setRoute(fm.GET(uroute.."/view"), function(r) ... end)
  4. How Fullmoon templates work

    master

    Fullmoon's template engine allows mixing text with Lua statements and expressions. Templates are parsed during registration and converted into Lua functions for high performance.

    Key Characteristics:

    • Direct Rendering: Templates render directly to the output buffer (delegated to redbean) rather than returning a string.
    • Restricted Environment: Templates only have access to explicitly passed parameters or the vars and block tables.
    • Syntax Types:
      • {% statement %}: Executes Lua statements (e.g., if, for).
      • {%& expression %}: Renders a Lua expression as HTML-safe text.
      • {%= expression %}: Renders a Lua expression as-is (no escaping). Use with caution to avoid XSS.

    Core Functions:

    • setTemplate(name, text[, parameters]): Registers a template. Overwrites existing templates with the same name.
    • render(name, parameters): Renders a registered template using the provided parameters.
    fm.setTemplate("hello", "Hello, {%& title %}!")
    fm.render("hello", {title = "World"})
  5. How template blocks and inheritance work

    master

    Fullmoon uses a block-based system to enable composable layouts and component inheritance. You can define default content for a section using {% function block.name() %} ... {% end %} and then call it with {% block.name() %}.

    Child templates can overwrite these blocks by redefining the same function before rendering the parent template. This creates a render tree where the most specific (deepest) definition of a block is used.

    Key behaviors:

    • Overwriting: A child template defines a block to customize a parent's layout.
    • Nesting: Blocks can be nested within other blocks or Lua statements.
    • Optional Blocks: To prevent errors if a block is not defined anywhere in the render tree, check for its existence before calling: {% block.name and block.name() %}.
    • Super Reference: You can explicitly call a block from a specific template using the syntax {% block.<templatename>.<blockname>() %} to simulate a 'super' call.
    -- base/layout template
    {% function block.greet() %} -- 1. defines default "greet" block
      Hi
    {% end %}
    {% block.greet() %}          -- 2. calls "greet" block
    
    -- child template
    {% function block.greet() %} -- 3. defines "greet" block
      Hello
    {% end %}
    {% render('base') %}         -- 4. renders "base" template
    
    -- grandchild template
    {% function block.greet() %} -- 5. defines "greet" block
      Bye
    {% end %}
    {% render('child') %}        -- 6. renders "child" template
  6. How Fullmoon handles request routing

    master

    Fullmoon processes HTTP requests by matching the path URL against registered routes in the order they were defined. The routing lifecycle follows these steps:

    1. Path Matching: Matches the request URL against route URLs sequentially.
    2. Condition Verification: Checks any conditions associated with the matching route.
    3. Action Execution: Calls the specified action handler (a Lua function) if all conditions are met, passing a request table.
    4. Response Serving: Serves the response if the handler returns something other than false or nil.

    Important Behaviors:

    • Order Matters: Routes are evaluated in registration order. More specific routes (e.g., /user/bob) must be registered before more general routes (e.g., /user/:name) to ensure they are matched correctly.
    • Condition Failure: If a condition fails, routing for that route is aborted, and the next route is checked. However, a condition can set an otherwise value to trigger a specific response status code immediately.
    • 404 Handling: If no routes match, a 404 is returned. You can customize this by setting a custom 404 template using fm.setTemplate("404", "...").
  7. Use layouts and blocks for template composition

    master

    Fullmoon supports two patterns for complex layouts:

    1. Dynamic Template Selection

    Pass the name of the template you want to render as a parameter to a wrapper template.

    fm.setTemplate("header", "<h1>{% render(content, {title = title}) %}</h1>")
    fm.render("header", {title = 'World', content = 'hello_template_name'})

    2. Blocks (Inheritance Pattern)

    Define a 'layout' template that calls functions stored in the block table. 'Child' templates can then overwrite these functions in the block table before rendering the layout.

    Workflow:

    1. Layout: Defines a block function in block.name() and calls {% block.name() %} where it should appear.
    2. Child: Defines {% function block.name() %} ... {% end %} to provide new content, then calls render on the layout.

    Note: The block must be explicitly called from the base/layout template to be rendered.

    -- Layout template
    fm.setTemplate("header", "<h1\>{% function block.greet() %}Hi{% end %}{% block.greet() %}, {%& title %}!</h1>")
    
    -- Child template overwriting the block
    fm.setTemplate("hello", "{% function block.greet() %}Hello{% end %}{% render('header', {title=title}) %}")
    
    fm.render("hello", {title = 'World'}) -- renders <h1>Hello, World!</h1>
  8. Apply conditional filters to routes

    master

    Routes can be filtered using various request attributes. When a condition is not met, the routing engine skips the route and checks the next one.

    Supported conditions include:

    • method: HTTP method (e.g., GET, POST).
    • host: The Host header (use Host for the header, host for the request property).
    • scheme: The request protocol (e.g., http, https).
    • clientAddr / serverAddr: IP addresses (can use custom validators like fm.isLoopbackIp).
    • ContentType: Request headers (e.g., ContentType = "application/json"). Note: only the media type is compared; boundaries/charsets are ignored.
    • params: Request parameters (e.g., name = "Bob").
    • Headers: Any header can be checked using its name as a key (e.g., X-Custom-Header = "value").

    Precedence for overlapping names:

    1. Multi-word request headers (e.g., ContentType)
    2. Request parameters
    3. Request properties (method, port, host, etc.)
    4. Single-word request headers
  9. Quickstart: Create a basic Fullmoon application

    master

    A minimal Fullmoon application requires loading the module, defining a route, and calling run(). By default, the server listens on localhost:8080.

    local fm = require "fullmoon"
    
    -- Define a route that returns a simple string
    fm.setRoute("/hello", function(r) 
        return "Hello, world" 
    end)
    
    -- Start the server
    fm.run()
  10. Pass parameters and global variables to templates

    master

    Parameters can be passed to templates at two stages:

    1. Registration: Using setTemplate(name, text, parameters), where parameters act as defaults.
    2. Rendering: Using render(name, parameters), which overrides defaults.

    Global Variables: Use setTemplateVar(name, value) to make a value accessible to all templates via the vars table.

    Handling Undefined Values: By default, nil or false values render as empty strings. To enforce strictness, you can set a special if-nil variable in vars to trigger an error or log when a value is missing:

    fm.setTemplateVar('if-nil', function() error"missing value" end)
    -- Setting a global variable
    fm.setTemplateVar('title', 'World')
    fm.setTemplate("hello", "Hello, {%& vars.title %}!")
    fm.render("hello") -- renders `Hello, World!`
    
    -- Overriding via render
    fm.render("hello", {title = "All"}) -- renders `Hello, All!`
  11. Serve template content via routes

    master

    To serve a template as a response to a route, use fm.serveContent.

    Important: Do not use fm.render directly inside fm.setRoute for static templates, as fm.render will execute immediately during route setup rather than when the request is handled. Use fm.serveContent to ensure the template is processed lazily when a request arrives.

    If the template requires dynamic data from the request, wrap the fm.serveContent call in a function.

    fm.setTemplate("hello", "Hello, {%& name %}")
    fm.setRoute("/hello/:name", function(r)
        return fm.serveContent("hello", {name = r.params.name})
    end)
  12. Throw specific HTTP error responses from deep within calls

    master

    While a standard Lua error() results in a 500 Internal Server Error, you can trigger specific HTTP responses (like 404) from within nested function calls by using error(fm.serve*) functions. This is only supported when called from within an action handler.

    local function AnyOr404(res, err)
      if not res then error(err) end
      -- serve 404 when no record is returned
      if res == db.NONE then error(fm.serve404) end
      return res, err
    end
    
    fm.setRoute("/", function(r)
        local row = AnyOr404(dbm:fetchOne("SELECT id FROM test"))
        return row.id
    end)