Skip to content
Permalink
master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
3901 lines (2931 sloc) 157 KB

System.Web

System.Web is a standalone class library provided by PLoop that needs to be loaded by a separate require "PLoop.System.Web, which provides a platform-independent web framework. System currently only supports UTF-8 encoding . Specific implementation needs to be provided for the platform .

Current implementations are: NgxLua for Openresty server, see PLoop.Browser for an example, which is used to display PLoop class libraries by reflection mechanism.

Contents

Test Environment

The web framework provided by PLoop is platform-independent, but to make it easier to demonstrate the framework's functionality, we chose Openresty as the testbed, and currently only NgxLua provides an implementation of the PLoop web framework for Openresty. If you expect to implement it on a specific server, you can check the NgxLua, which is not really complicated.

The test environment is CentOS 7 + Mysql 8 + Redis + Openresty.

  1. for CentOS 7, go to centos.org and download, and as a test environment, we recommend using Oracle VM VirtualBox to create a virtual machine and perform a minimal installation.

    If a virtual machine is used, the network card needs to be configured to open two, one is NAT network for accessing the external network, and the other is Host-Only network, so that the host can directly access the web server opened in the virtual machine through IP.

    After entering the system for the first time, it is best to check the /etc/sysconfig/network-scripts/ifcfg-enp0s8 file, you can use vi to edit it, make sure ONBOOT=yes, and load it on boot. It is best to fix the IP address (IPADDR) to facilitate host access.

    For simplicity, the following operations will be performed with the root account, and rights management will not be involved. 2.

  2. the server uses Openresty, installation can be referred to Openresty installation, or you can directly try the following operation.

    yum install yum-utils
    yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
    yum install openresty
    yum install openresty-resty

    After normal installation, add the path to the PATH, you can also write ~/.bashrc or ~/.bash_profile file, make sure to load it on boot.

    export PATH=/usr/local/openresty/bin:/usr/local/openresty/nginx/sbin:$PATH

    Test the following command, if the output is correct, it means the installation is normal and the path configuration is also normal.

    resty-e 'print("hello, world")'

    Newly installed systems may not have port 80 open, you need to enable it first.

    firewall-cmd --zone=public --add-port=80/tcp --permanent
  3. The database is Mysql 8.0, and the latest version corresponding to CentOS 7 is available from the Mysql website, or as follows.

    Installing Mysql 8.0 and setting up boot up

    yum install wget
    cd /tmp
    
    wget https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm
    md5sum mysql80-community-release-el7-3.noarch.rpm
    rpm -ivh mysql80-community-release-el7-3.noarch.rpm
    yum install mysql-server -y
    systemctl start mysqld
    systemctl status mysqld

    View temporary password for root@localhost

    grep 'temporary password' /var/log/mysqld.log
    
    mysql -u root -p

    You can change the root password and add the actual user by waiting for the temporary password to be obtained:

    ALTER USER 'root'@'localhost' IDENTIFIED BY 'new password';
    CREATE USER 'admin'@'%' IDENTIFIED WITH MYSQL_NATIVE_PASSWORD BY 'administrator password';
    GRANT ALL PRIVILEGES ON * . * TO 'admin'@'%';
    FLUSH PRIVILEGES;

    Then create a new PLoop database for testing.

    CREATE DATABASE PLoop;
    EXIT
  4. The caching database uses Redis, which allows the latest version to be taken from the website, with the following installation based on version 5.0.6.

    cd /usr/local
    mkdir redis
    cd redis
    
    wget https://github.com/antirez/redis/archive/5.0.6.tar.gz
    tar -zxvf 5.0.6.tar.gz
    
    yum install gcc
    cd redis-5.0.6
    
    make
    cd src
    make install

    After installation, you need to configure boot up

    cdd /etc
    mkdir redis
    cp /usr/local/redis/redis-5.0.6/utils/redis_init_script /etc/init.d/redisd
    cp /usr/local/redis/redis-5.0.6/redis.conf /etc/redis/6379.conf
    vi /etc/redis/6379.conf

    The last line is to edit the 6379.conf file, find the daemonize=no in it, change it to yes, then save and exit.

    Start the redis service:

    service redisd start

    So by default our Redis server is running on port 6379 of local 127.0.0.1.

  5. for users using Oracle VM VirtualBox, code can be written in Windows by sharing a folder, CentOS 7 shares this directory and can run the entire site more directly, when Debug mode is on. , edit the code file on Windows, the changes will also be instantly reflected on the site running on CentOS.

    First of all, you need to select the virtual machine you want to install, click Settings - Shared Folder, then add a local directory as a shared directory, assign a name of www (you can modify it yourself), and select AutoMount. OK it.

    Then, start the virtual machine, in the interface of the virtual machine, select Device - Install Enhancements, so that VBox will mount an iso and wait for installation.

    Preparation

    yum update
    yum install gcc kernel-devel make
    
    reboot

    Install VBox enhancements.

    mkdir /cdrom
    mount /dev/cdrom /cdrom
    
    /cdrom/VBoxLinuxAdditions.run

    Load the shared directory to ~/www, and for convenience, create a start.sh command file for starting nginx and a reload.sh command file for restarting nginx.

    cd ~
    mkdir www
    mount -t vboxsf www ~/www/
    
    cat > start.sh <<EOF
    mount -t vboxsf www ~/www/
    cd www
    nginx -p `pwd` /www -c conf/nginx.conf
    EOF
    
    cat > reload.sh <<EOF
    cd www
    nginx -p `pwd`/www -s stop
    nginx -p `pwd` /www -c conf/nginx.conf
    EOF
    
    cat > stop.sh <<EOF
    cd www
    nginx -p `pwd`/www -s stop
    EOF
    
    chmod +x start.sh
    chmod +x reload.sh
    chmod +x stop.sh

    Afterwards, the program is deployed to the shared directory via the

    cd ~
    ./start.sh

    Launching the site, via

    cd ~
    ./reload.sh

    to restart the site, and at this point we are done preparing our test environment. All file paths mentioned after this section use this shared folder as the root path.

Web App - System.Web.Application

Web App initialization

A Web App (System.Web.Application class object) is a single web service. A nginx service can run multiple web apps, each with its own independent configuration, routing, controllers, etc. We first build a Web App and start it in our test environment:

For simplicity, the following initialization operations are done in CentOS, copying the default nginx configuration and downloading the libraries PLoop and NgxLua:

cd ~/www/
mkdir conf
cp /usr/local/openresty/nginx/conf/* ~/www/conf

yum install git -y

git clone https://github.com/kurapica/PLoop
git clone https://github.com/kurapica/NgxLua

nginx.conf configuration

Then open the file /conf/nginx.conf (you can edit this file on Windows or any editor you like) and replace the contents with:

user root;

pid logs/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include mime.types;

    lua_shared_dict plbr_session_storage 10m;
    lua_shared_dict ngxlua_file_lock 100k;

    lua_package_path "${prefix}?.lua;${prefix}?/init.lua;/usr/local/openresty/lualib/?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua";

    init_by_lua_file ./conf/init.lua;

    server{
        listen       80;
        server_name  localhost;

        location / {
            root html;

            # MIME type determined by default_type:
            default_type 'text/html';

            content_by_lua ' PLBR.HttpContext():Process() ';
        }
    }
}

For test, we use root as the user. There are two shared tables defined above, plbr_session_storage is used to store the user session state, and ngxlua_file_lock is used to provide a mechanism for global locking, e.g. when loading code, or web templates, locks need to be added to avoid conflicts with multiple classes. However, after loading, unless the file changes require reloading, there is no need to use global locks, which will not affect the high concurrency of the server.

lua_package_path specifies the path to load the Lua library, ${prefix} is the root path of the website, that is ~/www/, so the PLoop and NgxLua saved below can be loaded normally, followed the common Lua library provided by the Openresty.

The init_by_lua_file specifies the initial Lua file to be used when starting nginx, where we usually load the PLoop and NgxLua libraries, and then load each web app.

Here we only load one web app for testing, so we only define a server, where content_by_lua is the entry point, and the PLBR is the namespace we will declare, usually a web app should have a separate namespace, the PLBR.HttpContext() will create a context, in order to use Lua on a multi-OS threaded platform, we usually need a context object to isolate the processing, the processing of the Http requests is processed by the context object, all objects created during this process can read and write values in the context object without conflicts.

Call the Process method of the context object will process the http request, like checking the router, rendering the view and etc.

init.lua initialization

Create the /conf/init.lua file, where we will load the base code part of the website, mainly the class library and the web app, but the web app implementation will be defined later.

PLOOP_PLATFORM_SETTINGS = { CORE_LOG_LEVEL = 3, ENABLE_CONTEXT_FEATURES = true, TYPE_VALIDATION_DISABLED = false, THREAD_SAFE_ITERATOR = true }

-- NgxLua will load PLoop.
require "NgxLua"

-- global configuration, no restrictions on which web app to use.
PLoop(function(_ENV)
    -----------------------------------------------------------------------
    -- Global Web Configuration
    -----------------------------------------------------------------------
    Web.Config = {
        Debug = true,
        LogLevel = System.Logger.LogLevel.Debug,
    }

    -----------------------------------------------------------------------
    -- Threadlock Manager
    -----------------------------------------------------------------------
    NgxLua.LockManager("ngxlua_file_lock")
end)

-- Load Web App, can load multiple Web Apps in order.
require "plbr"

Note that here ENABLE_CONTEXT_FEATURES is set to true in PLOOP_PLATFORM_SETTINGS. Web servers uses this option to avoid conflicts. Web.Config is the global configuration of the Web framework, not related to a single Web App, where Debug is set to true, when the resource file is modified(include the website template files), when you visit again, the system will reload, so the authors can get the changes directly. However, because that feature need to check the file modification time, and their dependencies (subclasses and superclasses) between the resource files, should only be used under the development stage, make sure to change it to false when put online.

The NgxLua.LockMananger uses the shared table provided by Openresty to implement thread locking, it doesn't have too many configurable properties, just keep the declaration handling like this. The ngxlua_file_lock used here is declared in nginx.conf.

Please make sure to enable the THREAD_SAFE_ITERATOR option, because the Openresty executes coroutines slightly differently, and if you don't enable it, the iterator error may result in an infinite return cannot resume dead coroutine.

Our first web app

Create the /plbr/ directory, which will be our actual project code directory, and first create the /plbr/init.lua file, which will hold all the files we need to load during initialization. These files are usually definitions for routes, models, common types, etc. These must be loaded at initialization, and cannot be reloaded during runtime (use the ./reload.sh above to re-initialize the entire site).

In this file, we will initialize our web app and load a routing file.

-- /plbr/init.lua
Application "PLBR" (function(_ENV))
    namespace "PLBR"

    -- Defining PLBR.HttpContext, which inherits NgxLua.HttpContext
    --
    -- Since all web features like routers are stored in the web application
    -- We need bind the web app to the context, so the context know how to handle
    -- the http reques with the web app, that's what done in its constructor.
    --
    -- Note that the application inherits `System.Module`, so the web app
    -- is used as the environment, here the `_ENV` is the `Application "PLBR"`
    class "HttpContext" {
        NgxLua.HttpContext,
        __ctor = function(self) self.Application = _ENV end
    }
end)

-- Save PLBR to _G
import "PLBR"

-- load routers in the route file
require "plbr.route"

Application is a class saved by the Web framework to _G, which inherits from System.Module, so it can be used as an environment, and also provides a tree-like code management mechanism (although it is rarely required). In it, we declare the PLBR namespace, distinguishing it from the Web App. We then define a Http context class that is unique to the Web App, which actually binds its objects to the Web App.

Then in the content_by_lua file of nginx.conf, we build the object of this context class, and then call the Process method to start the request execution processing. This part doesn't actually involve any specific business logic, because the processing we need to do varies depending on the request url, so first we need to be able to distribute the request based on the url, and as a first step, we need a routing file to manage it.

-- /plbr/route.lua
Application "PLBR" (function(_ENV))
    -- All configuration and resources belonging to the PLBR web app need to be defined in the web app environment.

    __Route__("/")
    __Text__() function hello(context)
        return "Hello world"
    end
end)

After saving, start the server (you will need to create a logs directory for the first time).

cd ~/www/
mkdir logs
cd ~
./start.sh

If booted, use ./reload.sh.

Then you can open a browser and enter the virtual machine address (ip addr, usually 192.168.56.xx for VBOX) or the address of your server to access it directly, or you can use the

curl http://localhost

You can get the output - Hello world. Our test environment is now ready to use. As you can see, the actual business logic is quite simple, despite all the prep work. We can do all the work created by the Web API using routing, but this is just the basics. Usually we don't put the main business in routing and handle it directly, we use the controller in MVC to handle the business.

So for any url used later, it will be in the form of /test, which you can spell as http://localhost/test or the specified IP of the form http://192.168.56.xx/test without special instructions.

Route Management

A route usually consists of two parts, the first is to match the route string used by the requesting url, either for exact matching or as a pattern string:

-- /plbr/route.lua
Application "PLBR" (function(_ENV))
    __Route__("/hello")
    __Text__() function hello(context)
        return "Hello world"
    end

    __Route__ ". *" __Text__()
    function parsetype(context)
        local target = context.Request.Url:gsub("/", ".") :sub(2, -1) -- /System/Threading -> System.Threading
        local ns = Namespace.GetNamespace(target)

        if ns then
            return target ... " is " ... tostring(getmetatable(ns))
        else
            return target ... " isn't a type"
        end
    end
end)

Using ./reload.sh to restart the server to see the results, route management is part of the initialization and is not dynamically loaded, so we need to restart the server to see the changes. Then we can test:

  • GET /hello - Hello world

  • GET /HELLO - Hello world

  • GET /System/NoneExist - System.NoneExist isn't a type

  • GET /System/Collections/List - System.Collections.List is class

Two types of route paths are used here, the first like /hello without a regular expression, which is a static route, and a dynamic route with a pattern string like . *, /test/*.lsp, which are dynamic routes with pattern strings.

When the request comes in, it will first match the static route with a case-insensitive rule, which is actually done using Lua's hash table, so no matter how many static routes there are, there are only 1 or 2 queries. If the match is successful, the request is handed off to the corresponding process for processing.

If the static route cannot be matched, the system will match the dynamic route based on the order in which it was registered, so the second route defined above will not be used to process /hello requests, although it will match all requests.

__Route__ is not a path that can only be used for bindings, its actual constructor function is

__Route__(String, HttpMethod/HttpMethod.ALL, Boolean/true)

The first argument is the access path, the second is HttpMethod, which is an enum type that contains all Http methods, see Web Constants, and the last is whether the route is case-insensitive. There is usually very little need to adjust the third parameter, focusing on the second:

-- /plbr/route.lua
Application "PLBR" (function(_ENV))
    __Route__("/test", HttpMethod.GET) __Text__()
    function getTest(context)
        return "GET TEST"
    end

    __Route__("/test", HttpMethod.POST) __Text__()
    function postTest(context)
        return "POST TEST"
    end
end)

This test is suitable for use with curl

curl http://localhost/test
curl http://localhost/test -X POST

The test can get two different return results, so that the same WEB API interface, we can combine HttpMethod to perform different operations.

The route itself does not handle the request, we define the route just to generate a set of routes for a web app, and then the request is passed to the web app, and the route manager distributes the request to each process based on the registered route.

The above route processing is accompanied by the use of the __Text__ attribute, which encapsulates the function as a process object, and the return value of this function will be output as the result of the request with content type as text/plain.

These features are described in detail below.

System.Web.__Text__

As described in the example above, the __Text__ feature is used to encapsulate the function as a request process object bound to a route, and the return value of the function is output to the client as text/plain type. In addition to the simple mode in the example above, it also supports iterators that output text to the client in multiple passes.

-- /plbr/route.lua
Application "PLBR" (function(_ENV))
    __Route__("/multitext", HttpMethod.GET)
    __Text__() __Iterator__()
    function MultiText(context)
        coroutine.field("This is part of the result\n")
        coroutine.field("This is another part of the result\n")
    end
end)

After restarting the server, access GET /multitext to see the effect. Both ends of the text are now output correctly.

System.Web.__Json__

The __Json__ feature is used to encapsulate the function as a request process object bound to the route, and the return value of the function will be serialized and passed to the client in the form of application/json.

-- /plbr/route.lua
Application "PLBR" (function(_ENV))
    __Route__("/data")
    __Json__() function getData(context)
        local data = {}

        for k, v in pairs(_G) do
            if type(v) == "table" and getmetatable(v) == nil then
                local item = {}

                for j, l in pairs(v) do
                    if type(l) == "function" then
                        item[#item + 1] = j
                    end
                end

                data[k] = item
            end
        end

        return data
    end
end)

You can see the result by accessing GET /data, which saves all the values in _G as key-value pairs of the table, and then saves all the names of the functions in the table as an array, as you can see in the result.

System.Web.__Redirect__

The __Redirect__ feature is used to redirect the url based on the return value.

-- /plbr/route.lua
Application "PLBR" (function(_ENV))
    __Route__("/text")
    __Text__() function gettext(context)
        return "Hello"
    end

    __Route__("/redirect")
    __Redirect__() function redirect(context)
        return "/text"
    end
end)

After that use your browser to access GET /redirect to see the address bar is redirected. (localhost remember to change to IP address)

System.Web.__View__

The __View__ feature is more complex than the above, it will register a view template or template file at the same time, the template will be generated as a view class, the result of the business function will be used as the initialization table (if provided) to construct the object of the view class, and the view class will render the content of the web page to the client.

-- /plbr/route.lua
Application "PLBR" (function(_ENV))
    __Route__("/index")
    __View__ "index.view" [[
        <html>
            <head>
                <title>@self.title</title>
            </head>
            <body>
                <p>@self.welcome</p>
            </body>
        </html>
    ]]
    function index(context)
        -- Contextual usage description to be explained in more detail later.
        local name = context.Request.QueryString.name

        return { title = "Hi " ... name, welcome = "Welcome here, " ... name }
    end
end)

Accessing GET /index?name=Ann resulted in the following result

<html>
    <head>
        <title>Hi Ann</title>
    </head>
    <body>
        <p>Welcome here, Ann</p>
    </body>
</html>

The __View__ feature is actually mainly used to bind template files, template files have a suffix like .view, such files are resource files, according to the suffix, the system will use a different resource loader to load these files, that's why in the above example, in addition to the HTML template, but also A filename needs to be provided, the name of this filename will be the class name of the generated view class, and the suffix name determines how the system will load this template.

The specific rules for loading the .view file will be detailed later, but for now, let's focus on the __View__ feature. Usually, changes to the routing file require a reboot of the server, but the resource file can be modified at any time when Debug mode is enabled to see the effect of the changes. Let's modify the above example to

-- /plbr/route.lua
Application "PLBR" (function(_ENV))
    __Route__("/index")
    __View__ "/view/index.view"
    function index(context)
        -- Contextual usage description to be explained in more detail later.
        local name = context.Request.QueryString.name

        return { title = "Hi " ... name, welcome = "Welcome here, " ... name }
    end
end)

Note that since we need to specify a file path here, please be sure to use an absolute path, as we usually have multiple resource files that are best separated by directories, so we are given a view directory here.

So, the actual view template should be stored in ~/www/html/view/index.view directory, and the rest of the resource files should be similar, omitting the ~/www part.

Then create the first view template file:

<! -- /html/view/index.view -->
<html>
    <head>
        <title>@self.title</title>
    </head>
    <body>
        <p>@self.welcome</p>
    </body>
</html>

First, restart the server, then visit GET /index?name=Ann again, after everything is OK, modify this template file to:

<! -- /html/view/index.view -->
<html>
    <head>
        <title>@self.title</title>
    </head>
    <body>
        <p>A modify version</p>
        <p>@self.welcome</p>
    </body>
</html>

Refreshing the page, you can see that the output has changed. Resource files like the view template file are all ready to be modified in development to see the results at any time.

Although because our current business logic is stored in the routing file, which causes modifying the business requires restarting the server, the MVC framework introduced later will completely solve these problems. Including the current fixed return method for each function that handles the results, this will be solved in MVC.

Returning to the __View__ feature, we do not need to specify a template file when declaring this feature; if no template file is specified, it will use the first return value of the function as the template path, and the second value as the initial table for constructing the template's corresponding view class (even if a template file is specified, if the first return value is a string, it will be used as the new template path):.

-- /plbr/route.lua
Application "PLBR" (function(_ENV))
    __Route__("/index")
    __View__() function index(context)
        if tonumber(context.Request.QueryString.ver) == 1 then
            return "/view/index_v1.view"
        else
            return "/view/index.view"
        end
    end
end)
<! -- /html/view/index.view -->
<html>
    <head>
        <title>Test web Current</title>
    </head>
    <body>
    </body>
</html>
<! -- /html/view/index_v1.view -->
<html>
    <head>
        <title>Test web V1</title>
    </head>
    <body>
    </body>
</html>

Accessing /index?ver=1 and /index will give different results. Usually .view is used as the View section in MVC. We'll look at the detailed rules for its use later. While the framework limits the rendering rules for the .view file, the system allows the custom rendering engine to implement other rules. This is described at the end.

System.Web.__Switch__

In practice, we also encounter problems where the front-end needs to determine the format of the data to be obtained, usually by returning the view result, or by returning Json data, if two API interfaces are specifically defined for this purpose. And the logic of the two interfaces is exactly the same, only the return method is different, which is not good for maintenance. So, the system also provides a __Switch__ feature, which is used in the same way as __View__.

The difference is that it will return the data according to the request's Accept field, if the request is application/json, then ignore the view template and return the data in Json format. If the request accepts text/html or text/plain, then the view template will be used to generate the content and return.

(Testing needs to be done with front-end code, omitted here)

System.Web.__File__

If a file needs to be generated dynamically, a combination of __File__ and __Iterator__ can also be used, with the __File__ feature stating that this process is used to output the contents of the file. The feature __Iterator__ ensures that the function runs as an iterator, which can output the result to the client by returning the value of yield multiple times, and we can rename the filename Assigned to __File__("download.csv") , but it's usually more convenient and intuitive to return the filename first in the function directly.

-- /plbr/route.lua
Application "PLBR" (function(_ENV))
    local yield = coroutine.yield

    __Route__("/download")
    __File__() __Iterator__()
    function download(context)
        yield("download.txt") -- returns the file name.

        -- Output document contents
        for i = 1, 10 do
            yield("This is line " .... ... i)
            yield("\n")
        end
    end
end)

Definition of Application

The following are resources declared by the System.Web.Application class:

Static Properties Type Description
ConfigSection System.Configuration.ConfigSection read-only, which provides the configuration definition of the Web App, see Configuring the System for details.
Property Type Description
_Application Web.Application Get the root Web app, because it inherits from System.Module, so it can realize the tree code management, which needs to be able to locate the root Web App, so the route, controller and other resources are only registered in the root Web App.
_Config Table Setup configuration is parsed using the ConfigSection configuration definition defined above, described later.
_Root String The root path of the web application, normaly nil, If it is set to /test, then it can only match the url of /test/*, which is used as a subsite.
_ErrorHandler Function Error handling function, the error occurred during the request code execution, will be passed into this function, usually can consider direct output to the client (for Debug) or error logging.
Method argument return description
Url2Path url path convert Url requests to the address of the web app (i.e. remove the root path of the subsite).
Path2Url path url to convert the specified address of the Web App to the Url request address (i.e., to splice the root path of the subsite).

These two methods are usually used internally by the system, but Path2Url may need to be used in the view to convert a specific address, as you can see in a later example.

Web App as a subsite

In /conf/init.lua we use Web.Config to configure the behavior of the entire framework, and in the same way we can configure web apps, with the Root in the config, we can run multiple web apps on a single nginx server, each corresponding to a subsite:

-- /plbr/init.lua
Application "PLBR" (function(_ENV))
    namespace "PLBR"

    class "HttpContext" {
        NgxLua.HttpContext,
        __ctor = function(self) self.Application = _ENV end
    }
end)

import "PLBR"

require "plbr.route"

-- the configuration file is loaded at the end so that new configuration
-- definitions added earlier (e.g., database connections, demonstrated later)
require "plbr.config"
-- /plbr/config.lua
Application "PLBR" (function(_ENV))
    _Config = {
        -- Subsite root path
        Root = "/sub",

        -- functions exported to view template files by default
        -- The functions defined in export can be used directly in the view.
        View = {
            Default = {
                export = {
                    Url = function (path) return _ENV:Path2Url(path) end,
                }
            }
        },
    }
end)
-- /plbr/route.lua
Application "PLBR" (function(_ENV))
    __Route__("/text")
    __Text__() function hello(context)
        return "hello world"
    end

    __Route__("/redirect")
    __Redirect__() function redirect(context)
        return "/text"
    end

    __Route__("/index")
    __View__[[/view/index.view]]
    function index() end
end)
<! -- /html/view/index.view -->
<html>
    <head>
        <title>Test web</title>
    </head>
    <body>
        <a href="@Url('/text')">test url</a>.
    </body>
</html>

After restarting the server, you can access GET /sub/text and GET /sub/redirect to get the result, and you can see that the web app is running in the /sub subsite. Our routing and redirection do not have to handle this part of the /sub address. This way, by adjusting the config.lua file, we can freely adjust the subsite path without affecting the execution of the web app.

By accessing Get /sub/index we can see the result as follows

<! -- /html/view/index.view -->
<html>
    <head>
        <title>Test web</title>
    </head>
    <body>
        <a href="/sub/text">test url</a>
    </body>
</html>

The path of the visible subsite will also be spliced up normally, note that the given address must be / to start, that is, the absolute path, processing will ignore the relative path. Because the system has no way to determine which url address is the root path, which url address needs to be attached to the root path of the subsite, so this operation needs to be done by yourself, the use of View in the configuration will be introduced in detail later.

Therefore, we can define multiple web apps, which can then be assigned to different Root addresses, to run multiple web apps on a single nginx server.

Web App Error Handling

In addition to Root, the web app can also configure an ErrorHandler (corresponding to the _ErrorHandler property) for binding error handling. Usually we would consider outputting the error to the client (during development) or to the log (during operation), but in Openresty, Lua errors are automatically logged to the log and only need to be thrown with error, so our example is in the form of outputting to the client:

-- /plbr/config.lua
Application "PLBR" (function(_ENV))
    _Config = {
        ErrorHandler = function(err, stack, context)
            context.Response.Write(err)
            context.Response:Close()
        end,
    }
end)
-- /plbr/route.lua
Application "PLBR" (function(_ENV))
    __Route__("/index")
    __View__[[/view/index.view]]
    function index(context) end
end)
<! -- /html/view/index.view -->
<html>
    <head>
        <title>Test web</title>
    </head>
    <body>
        <p>@self.Data.NoExist</p>
    </body>
</html>

Then visit GET /index, you get the error: /root/www/html/view/index.view:7: attempt to index field 'Data' (a nil value), which shows that even for views, errors can be located correctly. Of course, the actual error handling used is a bit more complex, for example, there is a special error handling page.

Its arguments take the form of error, the first being the error message, the second being the call stack level, and the third being the context object, which is ignored by error, but can be used in our other cases.

Web Constants

The following are the enumeration types defined under System.Web.

-- HTTP method
__Flags__() __Sealed__() __Default__ "GET"
enum "HttpMethod" {
    ALL = 0,
    "OPTIONS",
    "GET",
    "HEAD",
    "POST",
    "PUT",
    "DELETE",
    "TRACE",
    "CONNECT",
}

--- HTTP processing results
__Sealed__() __Default__ "OK"
enum "HTTP_STATUS" {
    CONTINUE = 100, --The request can continue.
    SWITCH_PROTOCOLS = 101, --The server has exchanged protocols in the escalation header.
    OK = 200, --The request completed successfully.
    CREATED = 201, --The request has completed and resulted in the creation of a new resource.
    ACCEPTED = 202, -The request has been accepted for processing, but processing has not yet completed.
    PARTIAL = 203, -The meta information returned in the entity header is not an identifiable collection from the originating server.
    NO_CONTENT = 204, --The server has completed the request, but there is no new information to send back.
    RESET_CONTENT = 205, -The request has completed and the client program should reset the document view that caused the request to be sent to allow the user to easily initiate another input operation.
    PARTIAL_CONTENT = 206, --The server has completed a partial fetch request for the resource.
    WEBDAV_MULTI_STATUS = 207, --This means that a single response has multiple status codes. The response body contains the Extensible Markup Language (XML) describing the status codes. For more information, see the http extension for distributed authoring.
    AMBIGUOUS = 300, --The requested resource is available in one or more locations.
    MOVED = 301, --The requested resource has been assigned a new permanent unified resource identifier (uri), and any future references to the resource should be completed using one of the returned uri.
    REDIRECT = 302, -The requested resource is temporarily located under another uri.
    REDIRECT_METHOD = 303, -The response to the request can be found under a different uri and should be retrieved using the get http verb on that resource.
    NOT_MODIFIED = 304, --The requested resource is not modified.
    USE_PROXY = 305, --The requested resource must be accessed through the proxy provided by the location field.
    REDIRECT_KEEP_VERB = 307, - The redirect request maintains the same HTTP verb http/1.1 behavior.
    BAD_REQUEST = 400, --The server could not process the request due to invalid syntax.
    DENIED = 401, --The requested resource requires user authentication.
    PAYMENT_REQ = 402, --not implemented in the http protocol.
    FORBIDDEN = 403, --The server understands the request, but cannot complete it.
    NOT_FOUND = 404, --The server did not find anything that matched the URI of the request.
    BAD_METHOD = 405, --The http verb is not allowed.
    NONE_ACCEPTABLE = 406, --Customer acceptable response was not found.
    PROXY_AUTH_REQ = 407, --Proxy authentication required.
    REQUEST_TIMEOUT = 408, --Server wait request timeout.
    CONFLICT = 409, --Unable to complete request due to conflict with current status of resource User should resubmit for more information.
    GONE = 410, --The requested resource is no longer available on the server, and the forwarding address is not known.
    LENGTH_REQUIRED = 411, --The server cannot accept the request if there is no defined content length.
    PRECOND_FAILED = 412, -The pre-condition given in one or more request header fields is calculated as false when tested on the server.
    REQUEST_TOO_LARGE = 413, -The server could not process the request because the requesting entity is larger than the server can handle.
    URI_TOO_LONG = 414, --The server could not serve the request because the request uri was longer than the server could interpret.
    UNSUPPORTED_MEDIA = 415, --The server cannot service the request because the format of the request entity is not supported by the request resource of the request method.
    RETRY_WITH = 449, -- The request should be retry after performing the appropriate action.
    SERVER_ERROR = 500, --The server encountered an unexpected condition and was unable to fulfill the request.
    NOT_SUPPORTED = 501, --The server does not support the functionality required to complete the request.
    BAD_GATEWAY = 502, --The server, while acting as a gateway or proxy, received an invalid response from an upstream server that was accessed while attempting to complete the request.
    SERVICE_UNAVAIL = 503, --The service is temporarily overloaded.
    GATEWAY_TIMEOUT = 504, --The request timed out while waiting for the gateway.
    VERSION_NOT_SUP = 505, --The server does not support the version of the HTTP protocol used in the request message.
}

In common web development, you usually only need to use HttpMethod for route binding.

Encoding and decoding

System.Web provides four common decoding and encoding methods.

method parameter description
HtmlEncode text:String, encode:System.Text.Encoding/System.Text.UTF8Encoding Encode the string
HtmlDecode text:String, encode:System.Text.Encoding/System.Text.UTF8Encoding Decode the string
UrlEncode text:String Encode the url
UrlDecode text:String Decode the url

Use cases are similar.

require "PLoop.System.Web"

PLoop(function(_ENV)
    -- note that System.Web is also a public namespace, so you can call these methods directly
    local str = HtmlEncode("<test>")
    print(str) -- &lt;test&gt;
    print(HtmlDecode(str)) -- <test>

    str = UrlEncode("/test?x=123")
    print(str) -- %2Ftest%3Fx%3D123
    print(UrlDecode(str)) -- /test?x=123
end)

Context - System.Web.HttpContext

In the above example, all process functions have a context as the first argument, and this value is an object of the class System.Web.HttpContext. In combination with the content_by_lua processing in nginx.conf, each Http request builds an Http context object and calls its Process method to start the process.

Each Http context object will have an unique Web App bound to it, and it will check all the routes registered in that Web App, match them, execute the corresponding process, and then return the result to the client. So, the Http context manages the entire Http request process, and also the processing process, the location where the shared data is stored, the existence of the context object can effectively avoid the problem of system thread conflicts caused by shared data.

So, we will see that this context object exists in every corner of the web framework. For the process processing functions bound to the route, they can read the information submitted by the Http request from the context and also output the result to the client via the context.

The System.Web.HttpContext is actually abstract, and our own definition of the context class above is inherited from NgxLua.HttpContext, which inherits System.Web.HttpContext to provide an implementation for Openresty.

System.Web.HttpContext declares the definition of the context class.

abstract property type description
Request System.Web.HttpRequest provides information on all Http requests, needs to be implemented for a specific platform.
Response System.Web.HttpResponse for outputting processing results to the client, needs to be implemented for a specific platform
SessionType - System.Web.HttpSession The type of http session used to auto generate the session of the context, default is the System.Web.HttpSession
Final property (cannot be overwritten) Type Description
Application System.Web.Application Web App to which the context object belongs, with independent routing, controllers and other resources.
ProcessPhase System.Web.IHttpContextHandler.ProcessPhase the current request processing stage, introduced later!
Session System.Web.HttpSession get user session object, can be used to save session data
IsInnerRequest Boolean Whether the context is used to process an internal request, in order to separate the business logic, we can consider using an internal request in the form of a url instead of a direct object call, then we need to be able to confirm whether it is an internal request, for example, internal requests do not need to verify user permissions and so on.
method parameter return description
Process none the process that initiates request processing, which is used for content_by_lua is the entry point for Http requests for this web framework.
ProcessInnerRequest url: String, params: Table/nil, HttpMethod/nil status: HTTP_STATUS, result starts an internal request, specifying url, request form and Http method.

ProcessInnerRequest is used to initiate an internal request, the result of the internal request will return two values, the first is the Http status of the process and the other result is usually the JSON data output using __Json__, but because it is an internal request, the output data will not be serialized, but returned directly, which helps to return the database process's Result set. Avoid over-serialization and deserialization operations.

Request - System.Web.HttpRequest

The System.Web.HttpRequest declares the object that contains all the Http request information, the implementation of which can be found in NgxLua's NgxLua.HttpRequest.

This class declares the following properties.

abstract property type description
ContentLength Number the length of the requested content.
ContentType String MIME type of the request.
Cookies Table Get the set of cookies requested by the client.
Form Table Get the form submitted by the client (non-GET operation).
HttpMethod HttpMethod Get Http Request Method
IsSecureConnection Boolean Is the request using HTTPS?
QueryString Table Get the request data submitted by the client(GET /xx?v=1&b=2)
RawUrl String Get the original request string.
Root String get the root directory of the request, for our example ~/www/html.
Url String get the url of the request

where Cookies, Form, QueryString all need to return the dictionary table containing the submitted key-value pairs.

Final Property Type Description
Context HttpContext the context object to which the request object belongs.
Handled Boolean Whether the request has already been processed.

Response - System.Web.HttpResponse

The System.Web.HttpResponse class declares the object that outputs the result to the client, the implementation of which can be found in NgxLua's NgxLua.HttpResponse.

This class declares the following properties and methods.

abstract property type description
ContentType String read and write the output MIME type, similar to __Text__ using text/plain.
RedirectLocation String Read and write redirect url
RequestRedirected Boolean Is the request already redirected
Write Function+System.Text.TextWriter The stream writer or function.
StatusCode HTTP_STATUS Read and write the status of the result.
Cookies System.Web.HttpCookies Output a set of cookies.

The Write property of the response object may be, function, or the stream output object may be used, so it is defined as a property, not a method. Use something like

context.ContentType = "text/plain"
context.Write("Hello world")
Final property Type Description
Context HttpContext the context object to which the response object belongs.
abstract method parameter description
SendHeaders Send Response Headers
Close Close response, end output

All three methods have no additional parameters; the required parameters can be obtained from the response object, such as the address of the redirect.

Method Parameters Description
Redirect url: String, code: HTTP_STATUS/HTTP_STATUS.REDIRECT, raw: Boolean/false Redirect to the specified address, note the third parameter raw, because the Web App may correspond to only one subsite, then the actual address The root path of the subsite needs to be stitched, and the third parameter needs to be passed to true if you wish to leave it unstitched.

Cookie Management - System.Web.HttpCookie & System.

The Cookies of the request object are simply hash tables that store the key-value pairs submitted by the client and are used similarly:

__Route__("/test", HttpMethod.GET)
__Json__() function test(context)
    return {
        User = { Name = context.Request.Cookies["name"] }
    }
end

The response object cookie is relatively complex, its Cookies property is abstract, but already has a default implementation, when reading it, will automatically create an object of the System.Web.HttpCookies class, which is a collection class, its elements are System.Web.HttpCookie object.

The HttpCookie object does not need to be created by yourself, it can be accessed directly using the specified cookie name as the field of the HttpCookies object, an automatically created HttpCookie object will be returned, we can set the value and timeout for it and so on:

__Route__("/test", HttpMethod.GET)
__Json__() function writecookie(context)
    context.Response.Cookies["ID"].Value = "TestUser1234"
    return {}
end

The following are the properties of HttpCookie:

Property Type Description
Domain String Get or set the domain to associate with the cookie.
Expires Date Gets or sets the expiration date and time of the cookie.
MaxAge Number Gets or sets the maximum lifetime of a cookie.
HasKeys Boolean Gets a value that indicates whether the cookie has a child key.
HttpOnly Boolean Gets or sets a value that specifies whether client scripts can access the cookie.
SameSite System.Web.HttpCookie.SameSiteValue Gets or sets whether the cookie should be restricted to first-party or same-site context
Name String Get or set the name of the cookie.
Path String Gets or sets the virtual path to be transferred with the current cookie.
Secure Boolean Gets or sets a value that indicates whether to use Secure Sockets Layer (SSL) to transmit the cookie, i.e., https only.
Value String Gets or sets a single cookie value.
Values Table Gets a collection of key/value pairs contained in a single cookie object.

Where SameSiteValue is defined as

__Sealed__() enum "SameSiteValue" {
    Strict = "Strict", -- disables third-party cookies, which will not be sent across sites
    Lax = "Lax", -- disables third-party cookies, which are generally not sent across sites, with the exception of GET requests.
}

We can assign a value to the due date using the Date type.

context.Response.Cookies["ID"].Expires = Date.Now:AddDays(30)

You can also add children to cookies.

context.Response.Cookies["ID"].Values["Age"] = 33
context.Response.Cookies["ID"].Values["Gender"] = "male"

The Cookies property is deferred and is created by the default value constructor factory only when used, which is also true for other object properties to reduce consumption.

User session - System.Web.HttpSession

Under Http's own mechanism, each request is independent, and in order to be able to track a particular client and user, we usually need to use multiple methods to determine this, but regardless of the method used by the client, the server side generally uses the user session.

The user session object is generally obtained through the Session property of the context object, and for a particular client, the user session object will have an unique ID for identification. The default user session object is created by System.Web.HttpSession.

The following is the session object's property declaration.

Final property Type Description
Context HttpContext the context object to which the user session belongs.
Items An indexer property for reading and writing key-value pairs, although it is not defined, it is preferable that the keys be strings and the values be serializable values, so that arbitrary storage methods can be used.
SessionID String Get the unique ID of the user session.
Timeout Date Read and Write Timeout Timeout
TimeoutChanged Boolean Whether the timeout time has been modified or not.
Canceled Boolean Whether the user session is canceled, the next time the user visits, a new ID will be used.
IsNewSession Boolean Whether the user session is a newly generated session, or the timeout time has been modified, in which case the new session ID needs to be output to the client (usually via a cookie).
ItemsChanged Boolean if the session saved key-value pairs have been modified, if so, you need to save these changes to the session data storage location.
RawItems Table the table that actually stores the key-value pairs, do not use it unless you have a special need.
IsTemporary Boolean Whether it is a temporary session, usually the cookies used to save the session will be cleared after the user closes the browser.

Of the above properties, usually only Items is used for the business part, while the other properties are managed by the framework.

__Route__("/test", HttpMethod.GET)
__Json__() function test(context)
    context.Session.Items.user = "TestUser1234"
    return {}
end

Note that Items cannot track multi-level changes, such as

context.Session.Items.UserList[1003] = { name = "king" }

Usually in this case, UserList needs to be replaced with.

local userList = System.Toolset.clone(context.Session.Items.UserList) or {}
userList[1003] = { name = "king" }
context.Session.Items.UserList = userList

The use of Session is relatively simple, you just need to read and write session variables via Items. The underlying issue is how to save the session's unique ID and timeout information to the client, and how to save the user session variables.

Usually these two parts are handled by two objects that act as managers, and the system declares two interfaces to handle these two parts respectively.

System.Web.ISessionIDManager

The ISessionIDManager declares the management of session IDs, including new, read, save, and validate sections.

It declares the following abstract methods.

abstract method parameter description
GetSessionID context: HttpContext reads the unique ID of the session from the context object.
CreateSessionID context: HttpContext create a unique ID for the context object.
RemoveSessionID context: HttpContext Remove the current session ID of the context object.
SaveSessionID context: HttpContext, session: HttpSession saves session for context object, i.e. saves session unique ID to client
ValidateSessionID id: String Verify SessionID

Usually we save the session ID in the form of a cookie in the client, which is the simpler and recommended way. Let's look at two implementations of it.

System.Web.GuidSessionIDManager

This implementation uses the value of the System.Guid construct as the session ID and saves it to the specified cookie to manage the lifetime of the session ID in conjunction with the timeout setting.

So, we can adjust the previous code file:

-- /plbr/init.lua
Application "PLBR" (function(_ENV))
    namespace "PLBR"

    class "HttpContext" {
        NgxLua.HttpContext,
        __ctor = function(self) self.Application = _ENV end
    }

    -- Session ID Manager
    GuidSessionIDManager { CookieName = "PLBR_SessionID", TimeoutMinutes = 1 * 24 * 60, Application = _ENV }
end)

import "PLBR"

-- load routing file
require "plbr.route"

require "plbr.config"
-- /plbr/route.lua
Application "PLBR" (function(_ENV))
    __Route__("/session")
    __Text__() function init(context)
        return context.Session.SessionID
    end
end)

Multiple accesses to GET /session will see an unique session ID. Note that when declaring these managers, the current web app needs to be passed for binding.

NgxLua.JWTSessionIDManager

This session ID manager is designed based on Json Web Token, which depends on resty.jwt, and can be installed using the following command

cd /usr/local/openresty/lualib/resty
wget https://raw.githubusercontent.com/jkeys089/lua-resty-hmac/master/lib/resty/hmac.lua
wget https://raw.githubusercontent.com/SkyLothar/lua-resty-jwt/master/lib/resty/jwt.lua
wget https://raw.githubusercontent.com/SkyLothar/lua-resty-jwt/master/lib/resty/jwt-validators.lua
wget https://raw.githubusercontent.com/SkyLothar/lua-resty-jwt/master/lib/resty/evp.lua

You can also install it yourself. After installation, change the init file in the above example to

-- /plbr/init.lua
Application "PLBR" (function(_ENV))
    namespace "PLBR"

    class "HttpContext" {
        NgxLua.HttpContext,
        __ctor = function(self) self.Application = _ENV end
    }

    -- Session ID Manager
    NgxLua.JWTSessionIDManager{ CookieName = "PLBR_JWT", TimeoutMinutes = 1 * 24 * 60, SecretKey = System.Guid.New():gsub("-", ""), HashAlgorithm = "HS256", Application = _ENV }

    -- session memory manager
    NgxLua.JWTSessionStorageProvider{ Application = _ENV }
end)

import "PLBR"

-- load routing file
require "plbr.route"

require "plbr.config"
-- /plbr/route.lua
Application "PLBR" (function(_ENV))
    __Route__("/session")
    __Text__() function init(context)
        context.Session.Items["User"] = "Ann"
        return "Please check your cookies"
    end
end)

In JWT design, both session ID and session variable will be encrypted and stored in cookies, so we need to provide session variable storage manager at the same time. When defining the JWT session ID manager, in addition to the regular cookie name, timeout minutes, we also need to specify the key and the hash algorithm, which is usually the default value when not exported, which is the value we provided to SecretKey and HashAlgorithm in the example, so it is usually not necessary to provide either (the session ID will be invalidated every time the server is restarted if (Fixed SecretKey will not expire).

After saving, restart the server, then go to Get /session, then open the debugger at F12 and look at the cookies.

PLBR_JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VyIjoiQW5uIiwiX3RpbWVvdXQiOjE1NzI4ODEyNDd9._fqPPE_ dL2lNV8SQUSI2OlnxdJBtedt1xzUrULwAJQg

Note that JWT's mechanism is unique in that it requires session variables to be written (usually user IDs) before they are saved to the client. One advantage of this mechanism is that the session variables are saved on the client side, and no session data needs to be saved on the server side, so there is no need to think about how to share session data when multiple servers are handling requests.

System.Web.ISessionStorageProvider

The ISessionStorageProvider interface declares how to implement session variable storage, and it declares the following abstract methods.

abstract method parameter description
Contains id: String whether or not the specified session ID has session variables stored in the store.
GetItems id: String session variable that returns the specified session ID.
RemoveItems id: String Clear Session Variables with Specified Session IDs
SetItems id: String, item: Table, timeout: Date/nil update session variable with specified session ID, can specify timeout.
ResetItems id: String, timeout: Date update the timeout of the session variable for the specified session ID.
TrySetItems id: String, item: Table, timeout: Date/nil Try to write a session variable to a session ID that does not exist in storage, return true if successful.

Four implementations are provided by the system.

System.Web.TableSessionStorageProvider

This is a direct use of Lua's table as storage, as it's not thread-safe and is for general testing only.

-- /plbr/init.lua
Application "PLBR" (function(_ENV))
    namespace "PLBR"

    class "HttpContext" {
        NgxLua.HttpContext,
        __ctor = function(self) self.Application = _ENV end
    }

    -- Session ID Manager
    GuidSessionIDManager { CookieName = "PLBR_SessionID", TimeoutMinutes = 1 * 24 * 60, Application = _ENV }
    TableSessionStorageProvider(_ENV)
end)

import "PLBR"

-- load routing file
require "plbr.route"

require "plbr.config"
-- /plbr/route.lua
Application "PLBR" (function(_ENV))
    __Route__("/session")
    __Text__() function init(context)
        context.Session.Items["ViewCount"] = (context.Session.Items["ViewCount"] or 0) + 1
        return "View Count: " ... context.Session.Items["ViewCount"]
    end
end)

Visit GET /session to see that the View Count will be counted correctly.

NgxLua.ShareSessionStorageProvider

This storage is borrowed from Openresty's shared dictionary, which is thread-safe, but limited in capacity and not suitable for large-scale use, only for small sites, please note that plbr_session_storage is declared in the nginx.conf file.

-- /plbr/init.lua
Application "PLBR" (function(_ENV))
    namespace "PLBR"

    class "HttpContext" {
        NgxLua.HttpContext,
        __ctor = function(self) self.Application = _ENV end
    }

    -- Session ID Manager
    GuidSessionIDManager { CookieName = "PLBR_SessionID", TimeoutMinutes = 1 * 24 * 60, Application = _ENV }
    ShareSessionStorageProvider("plbr_session_storage", _ENV)
end)

import "PLBR"

-- load routing file
require "plbr.route"

require "plbr.config"

Access GET /session to see that the View Count will be counted correctly.

NgxLua.RedisSessionStorageProvider

This store relies on resty.redis, usually Openresty comes with the installation, but if it fails to load, install it yourself. It uses the Redis server to store session variables.

-- /plbr/init.lua
Application "PLBR" (function(_ENV))
    namespace "PLBR"

    class "HttpContext" {
        NgxLua.HttpContext,
        __ctor = function(self) self.Application = _ENV end
    }

    -- Session ID Manager
    GuidSessionIDManager { CookieName = "PLBR_SessionID", TimeoutMinutes = 1 * 24 * 60, Application = _ENV }
    NgxLua.RedisSessionStorageProvider({ host = "127.0.0.1", port = 6379 }, _ENV)
end)

import "PLBR"

-- load routing file
require "plbr.route"

require "plbr.config"

Visit GET /session to see that the View Count will be counted correctly. This is suitable for medium sized sites, but it is always a burden on the server when the user size is large.

NgxLua.JWTSessionStorageProvider

This store is used with JWTSessionIDManager to store session variables in the client, which can greatly reduce the burden on the server, and the encrypted saving method can also prevent the client from arbitrarily modifying them.

However, because the stored variables can be interpreted, they cannot be used to store sensitive information.

Page rendering

The view template file is used to render output web pages, not just web pages actually, but also all kinds of text like js, css, etc. The system determines which resource loader to use by the suffix of the template file, while the resource loader of the view template file will use a rendering engine object to load the template text as a render class.

PLoop Currently there are only two rendering engines available: the System.Web.IRenderEngine anonymous class that acts as a static file rendering engine, i.e. exports the content directly, and the System.Web.PageRenderEngine rendering engine that used to load the ".view" files.

PLoop registers multiple view template file types with different extensions, but they all use System.Web.PageRenderEngine to generate view rendering classes, so they all follow the same rules.

Routing and resource files

In the above processing, we used __Text__, __Json__, __View__ to bind the route processing, but in reality, these are not required to be specified when declaring the route, overriding route.lua as

-- /plbr/route.lua
__Route__ "/.*.lsp"
function LuaServerPage(request)
    return request.Url
end

This is also a dynamic route to match url addresses with a specific suffix, e.g. GET /view/index.lsp, the system does not use . at the beginning of the suffix name for pattern matching.

Note that it does not bind to the __View__ feature, so the function is not handled as a request process; it accepts arguments from the HttpRequest object, not the context object, because it does not directly complete the business logic, nor does it require the HttpResponse response object for output.

This function returns the file address (i.e., /view/index.lsp), so the system loads it as a resource file, which is also dynamically loaded, and changes can be made in real time.

We'll use the .lsp file to introduce the default rendering engine's rendering rules, and then use the same rules for the .view template used in MVC.

Lua Server Page (.lsp)

.lsp is an earlier design for the template file type, usually we would choose the MVC framework, but this template is relatively simple to use.

To create our new template file.

<! -- /html/view/index.lsp -->
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>PLoop.System.Web Test Page</title>
        <script type="text/javascript" src="/js/jquery-2.1.4.min.js"></script>
    </head>
    <body>
        <p>
            This is the first PLoop.System.Web test page.
        </p>
    </body>
</html>

After restarting the server, we can GET /view/index.lsp to access the result, which is actually pure HTML, directly output. The PageRenderEngine will load the template file as a page rendering class, which will be used to render the resulting page.

Master Page (.master)

The PageRenderEngine does not check the HTML tags, but embeds its own logic according to specific rules, first of all, the rules that define the master template page.

Usually there are a large number of reuses of content for the same site, such as navigation bars, user logins, ad positions, overall page skeletons, etc. These reuses are also usually in the form of a tree. The best equivalent to a tree is a class inheritance system.

The .master suffix generates an abstract page rendering class that is only used to be inherited, so we can construct a new master template page.

<! -- /html/share/global.master -->
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>@{title My web site}</title>
        @{jspart <! -- javascript placeholder -->}
    </head>
    <body>
        @{body}
    </body>
</html>

The rules embedded in PageRenderEngine are all directives that start with @. Where something like @{title My Web site} declares a Web Part that needs to be implemented by inheritance, title is the name of the part, and the remaining My Web site is the default value. If the subclass is not implemented, the default value will be used for output.

There are three web parts defined in global.master above. Now we can implement them in /html/view/index.lsp, limited by the rule <! -- /html/view/index.lsp --> is no longer written.

/html/view/index.lsp

@{ master = "/share/global.master" }

@title{
    test page
}

@body{
    <p>
        This is a child page inherited from the parent template
    </p>
}

You need to restart the server to specify the master template page for the first time, otherwise the rendering method of the original page will override the rendering method of the master template, and you will not see the result.

The first line of this file is special, it is not loaded by the rendering engine. Before giving the file to the rendering engine, the system will check if there is a Lua table in the first line of the file.

It doesn't matter if you use @ at the beginning or // for the JS file. For view template files, @ will be more consistent.

In this configuration, when master is specified, the specified path will be inherited by this template file after generating the rendering class as the parent template file. Note that the above uses absolute path, so the actual path is ~/www/html/share/global.master, if you use relative path, like global.master, the corresponding path is ~/www/html/view/global.master.

To implement a web widget, we need to use @name{ and } at the beginning of two lines, and then fill in the output between these two lines with indentation. As mentioned before, the default rendering engine does not check HTML tags, etc., so it is difficult to confirm the start and end of a web part without checking indentation.

In practice, the web widget implementation is generated as a method of a rendering class with a name like Render_name, and the parameters and actual rendering logic are generated by the rendering engine. The Render method will call the rendering methods of each web part to render the entire page.

The Render method will call the rendering methods of each web part, which will render the whole page.

In the example above, the output would be

<! -- /html/share/global.master -->
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>Test page</title>
        <! -- javascript placeholder -->
    </head>
    <body>
        <p>
            This is a child page inherited from the parent template
        </p>
    </body>
</html>

Note that the rendering engine accumulates indentation to ensure that the content is output in a readable form, but see later how to remove all indentation and line feeds to reduce the length of the output content.

Super Class Web Parts

The .master file can specify other .master as the master template, and even the .lsp file can specify other .lsp files as master templates. There is no actual limitation to the parent template, .master is just an abstract class that is not suitable for direct output.

The relationship between the template and the master is the inheritance relationship between the last rendered class.

When overriding the definition of the web part of the parent template, if you need to output the web part of the parent template, you can use the form @{super:name}.

/view/index2.lsp

@{ master = "index.lsp" }

@body{
    @{super:body}
    <p>
        This is the a PLoop.System.Web test page.
    </p>
}

Here index.lsp is used as the master template and the body component is overridden. @{super:body} is used to output the contents of index.lsp and determines the location of the output. Visit GET /view/index2.lsp to see the results.

Mixing Lua code

To embed Lua business logic into HTML content for output, PageRenderEngine provides four ways.

Block

The local functions, rendering class methods, etc. can be considered as the pure Lua part of the rendering class definition, usually defined at the beginning of the template file:

/html/view/index.lsp

@{ master = "/share/global.master" }

@{
    local function rollDice(num, max, add)
        local sum = add

        for i = 1, num do
            sum = sum + math.random(max)
        end

        return sum
    end
}

@title{
    test page
}

@body{
    <p>
        Roll the dice 6d8 + 3: @rollDice(6, 8, 3)
    </p>
}

The block definition is wrapped by @{ and }, which must be pure Lua code. The local function rollDice was defined in the above example and will be used later in the Body widget.

Besides using the block definition at the beginning, it can also be used within rendering methods such as web parts which is more convenient if multiple lines of Lua code are used consecutively.

/html/view/index.lsp

@{ master = "/share/global.master" }

@title{
    test page
}

@body{
    <p>
        @ {
            local hour, msg = tonumber(os.date():match("(%d+):"))
            if hour < 10 then
                msg = "Good morning"
            elseif hour > 20 then
                msg = "Good night"
            else
                msg = "Have a nice day"
            end
        }
        @msg
    </p>
}

In this case, please take special care to maintain a good indent. Otherwise the system will not be able to determine the end position.

Inline

Inline Lua is used to print the result, usually starting with @ followed by a Lua expression, such as @rollDice(6, 8, 3). The result of this expression will be output directly.

Usually the engine can recognize function calls as well as complex expressions like self.Items:Map("x=>x.id"):Join(",") , but with a + b, there is no way to tell if the + b that follows is text or part of the expression, it has to be enclosed in brackets like @(a+b). .

If the output text needs to be encoded, you can start with @\.

/html/view/index.lsp

@{ master = "/share/global.master" }

@title{
    test page
}

@body{
    <p>
        Transcoding Test : @\"<test/>"
    </p>
}

The output result is

<! -- /html/share/global.master -->
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>Test page</title>
        <! -- javascript placeholder -->
    </head>
    <body>
        <p>
            Transcoding test : &lt;test/&gt;
        </p>
    </body>
</html>

Inline also supports the use of string definitions as expressions, but in any case, the use of brackets allows you to delineate expressions explicitly.

The result of the expression is serialized in the output, which is more convenient for JS.

/html/view/index.lsp

@{ master = "/share/global.master" }

@title{
    test page
}

@jspart{
    <script type="text/javascript">
        var data = @List(10):Map("x=>x^2"):ToList();
    </script>
}

@body{
    <p>
        Encode Test : @\"<test/>"
    </p>
}

The output result is

<! -- /html/share/global.master -->
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>Test page</title>
        <script type="text/javascript">
            var data = [1,4,9,16,25,36,49,64,81,100];
        </script>
    </head>
    <body>
        <p>
            Encode test : &lt;test/&gt;
        </p>
    </body>
</html>

Full-line

In order to control the output, we also need to be able to use Lua's control statements, such as if, for, etc., then we need to use the whole line of code mechanism. In other words, a whole line of code is used as Lua code.

Usually @> is used to indicate that the line is a full-line code segment. However, if it is followed by a Lua keyword, the @ is enough.

/html/view/index.lsp

@{ master = "/share/global.master" }

@title{
    test page
}

@body{
    @ local a, b = math.random(100), math.random(100)
    <p>@a + @b = @(a+b)</p>
    <p>
    @ local hour = Date.Now.
    @ if hour < 11 then
        Good morning.
    @ elseif hour > 20 then
        Good night.
    @ end
    </p>
}

As mentioned earlier, the Body component actually translates into a class method, and the full-line code snippet is naturally a structural control statement within the method. By using block, inline and full line code, we can easily mix Lua code and HTML output.

Mixed Method

Web parts are simply used to fill in the positions declared by the parent template, and since they are actually class methods, it is a natural operation to control the output via parameters. We need to provide a form that can be accompanied by parameters, and this is the mixed method.

/html/view/index.lsp

@{ master = "/share/global.master" }

@{
    local function appendVerSfx(path, version, suffix)
        return path ... suffix ... (version and "?v=" ... . tostring(version) or "")
    end
}

@javascript(name, version) {
    <script type="text/javascript" src="/js/@appendVerSfx(name, version, '.js')"></script>
}

@title{
    test page
}

@jspart{
    @{ javascript("jquery-2.1.4.min") }
    @{ javascript("index", 3) }
}

@body{
    <p>Mixed-method application</p>
}

The output result is:

<! -- /html/share/global.master -->
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>Test page</title>
        <script type="text/javascript" src="/js/jquery-2.1.4.min.js"></script>
        <script type="text/javascript" src="/js/index.js?v=3"></script>
    </head>
    <body>
        <p>Mixed-method application</p>
    </body>
</html>

The javascript we've defined here is a mixed method that works the same as the web part, but with an additional parameter passed in.

Helper Page (.helper)

Web parts usually exist as the skeleton (placeholder) of a web page, and mixed methods are usually used as helper functions, and they are usually functional in nature which is not free enough to take advantage of the inheritance mechanism of rendering classes.

The .helper file is generated as an interface so that it can be used by any page template file. Page template files that extend it can use these methods directly.

Some of the more common .helper files can be extended by a parent template page, so that the individual view files derived from the parent template page can used them directly.

We create a new helper file, and modify global.master.

/html/share/global.helper

@{
    local function appendVerSfx(path, version, suffix)
        return path ... suffix ... (version and "?v=" ... . tostring(version) or "")
    end
}

@javascript(name, version) {
    <script type="text/javascript" src="/js/@appendVerSfx(name, version, '.js')"></script>
}

/html/share/global.master

@{ helper = "global.helper" }

<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>@{title My web site}</title>
        @{jspart <! -- javascript placeholder -->}
    </head>
    <body>
        @{body}
    </body>
</html>

The above uses helper in the configuration table to specify the helper file, and multiple files can be separated by , signs because we can extend any number of interfaces.

So we can change the index.lsp file.

/html/view/index.lsp:

@{ master = "/share/global.master" }

@jspart{
    @{ javascript("jquery-2.1.4.min") }
    @{ javascript("index", 3) }
}

To avoid the effects of the previous definition, restart the server and access GET /view/index.lsp. The result will be

<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>My web site</title>
        <script type="text/javascript" src="/js/jquery-2.1.4.min.js"></script>
        <script type="text/javascript" src="/js/index.js?v=3"></script>
    </head>
    <body>

    </body>
</html>

Embed Page (.embed)

We can use @[path default text] to embed other files. Usually we use .embed as the suffix name of the embedded file, but this is not a qualification; it is no different from other .lsp.

/html/share/notice.embed

<h2>
    This is the embedded text.
</h2>

/html/view/index.lsp

@{ master = "/share/global.master" }

@title{
    test page
}

@body{
    @[/share/notice.embed <! -- Notice placeholder -->]
}

Accessing GET /view/index.lsp resulted in the following result

<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>Test page</title>
        <! -- javascript placeholder -->
    </head>
    <body>
        <h2>
            This is the embedded text.
        </h2>
    </body>
</html>

For the path and default values here, you can actually also use inline code form, in addition, files like .css and .js can also be used embedded, first we modify the help file.

/html/share/global.helper

@{
    local function appendVerSfx(path, version, suffix)
        return path ... suffix ... (version and "?v=" ... . tostring(version) or "")
    end
}

@javascript(name, version) {
    <script type="text/javascript" src="/js/@appendVerSfx(name, version, '.js')"></script>
}

@rawjs(name, version, id) {
    <script type="text/javascript">
        @[/js/@appendVerSfx(name, nil, '.js') // /js/@appendVerSfx(name, version, '.js')]
    </script>
}

Then modify

/html/view/index.lsp

@{ master = "/share/global.master" }

@title{
    test page
}

@jspart{
    @{ javascript("jquery-2.1.4.min") }
    @{ rawjs("index") }
}

@body{
    @[/share/notice.embed <! -- Notice placeholder -->]
}

First, you can directly access GET /view/index.lsp to get the

<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>Test page</title>
        <script type="text/javascript" src="/js/jquery-2.1.4.min.js"></script>
        <script type="text/javascript">
            // /js/index.js
        </script>
    </head>
    <body>
        <h2>
            This is the embedded text.
        </h2>
    </body>
</html>

The default value part is output here, so you can see that the specified name is output. Then we create a new js file.

//html/js/index.js
var test = 123;

Visit GET /view/index.lsp again to get the

<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>Test page</title>
        <script type="text/javascript" src="/js/jquery-2.1.4.min.js"></script>
        <script type="text/javascript">
            //html/js/index.js
            var test = 123;
        </script>
    </head>
    <body>
        <h2>
            This is the embedded text.
        </h2>
    </body>
</html>

css files can also be embedded in this way, the advantage of doing so is that, after modifying the js and css content, because it is embedded in the page output, the client will not be cached because the js and css files lead to changes can not be seen in real time, such as deployment, modified into a file link can be. This can be done by adjusting global.helper inside rawjs to achieve.

css and js are static files, loaded by the static rendering engine, of course, do not care about this detail.

Inner Request Page

Embedding pages is often used in js and css files, but rarely used to embed other view template files, because usually the template file is linked to a set of business logic, direct embedding flexibility is not enough.

In order to embed the output of other business, we need to use internal request, the result of the request is directly embedded to the client, for this we use the instructions of @[~path (param, httpmethod)].

Note that path and the parameter part that follows need to be separated by a space, because the path part can use inline code and can be thought of as a template string, while the (param, httpmethod) that follows is pure Lua code, so they need to be separated by a space.

param is a table containing the form, which can be nil. httpmethod is the HTTP method used.

The system will use path as the request path to complete an internal request processing, see an example of use below.

-- /plbr/route.lua
Application "PLBR" (function(_ENV))
    __Route__ "/. *.lsp"
    function LuaServerPage(request)
        return request.Url
    end

    __Route__ "/data" __Json__()
    function data(context)
        return { a = 1, b = 2, c = 3 }
    end
end)

/html/view/index.lsp

@{ master = "/share/global.master" }

@title{
    test page
}

@jspart{
    <script type="text/javascript">
        var data = @[~/data]
    </script>
}

@body{
    @[/share/notice.embed <! -- Notice placeholder -->]
}

After restarting, access GET /view/index.lsp and the results are

<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>Test page</title>
        <script type="text/javascript">
            var data = {"b":2, "a":1, "c":3}
        </script>
    </head>
    <body>
        <h2>
            This is the embedded text.
        </h2>
    </body>
</html>

We can see that here we are making an internal request, which is a better way to reuse server resources.

Lua Code file (.lua)

Usually the .lsp file, the part of the page that renders the page is not well suited to handle business logic and is only suitable for making simple rendering decisions. For business purposes, you can declare an OnLoad method for it, which is executed in front of the rendered page:

/html/view/index.lsp

@{ master = "/share/global.master" }

@{
    function OnLoad(self, context)
        -- Generate a cookie
        context.Response.Cookies["TestCookie"].Value = "Test"
        context.Response.Cookies["TestCookie"].Expires = System.Date.
    end
}

@title{
    test page
}

@jspart{

}

@body{
    @[/share/notice.embed <! -- Notice placeholder -->]
}

OnLoad here handles cookies, which receive contextual objects for easy processing. Typically, however, when the business logic is complex enough, it's more appropriate to separate this into separate code files.

-- /html/view/index.lua
class "Index" {} -- partial class declarations, as done by the view template file

-- Defining OnLoad methods
function Index:OnLoad()
    self.PageTitle = "Test Page"

    self.Data = {
        { Name = "Ann", Age = 12 },
        { Name = "King", Age = 32 },
        { Name = "July", Age = 22 },
        { Name = "Sam", Age = 30 },
    }
end

In the code file we generate the specific data and save it to the object.

/html/view/index.lsp

@{ master = "/share/global.master", code = "index.lua" }

@title{
    @self.PageTitle
}

@jspart{
    @{ javascript("jquery-2.1.4.min") }
}

@body{
    <table border="1">
        <thead>
            <tr>
                <th>Person Name</th>
                <th>Person Age</th>
            </tr>
        </thead>
        <tbody>
        @> for _, data in ipairs(self.Data) do
            <tr>
                <td style="background-color:cyan">@data.Name</td>
                <td>@data.Age</td>
            </tr>
        @> end
        </tbody>
    </table>
}

Code files can be specified in the configuration using code, usually in the same directory, so specifying a name is sufficient, and accessing it via a relative path will get it directly. The class name declared in the code file must be the same as the file name (case insensitive). This class will be completed when the template file is converted to a rendering class.

As explicitly mentioned before, web parts are essentially class methods, so the self used here is the object of the rendering class, which is given data in OnLoad. So, we can use this data to generate the result.

<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>Test Page</title>
        <script type="text/javascript" src="/js/jquery-2.1.4.min.js"></script>
    </head>
    <body>
        <table border="1">
            <thead>
                <tr>
                    <th>Person Name</th>
                    <th>Person Age</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td style="background-color:cyan">Ann</td>.
                    <td>12</td>
                </tr>
                <tr>
                    <td style="background-color:cyan">King</td>.
                    <td>32</td>
                </tr>
                <tr>
                    <td style="background-color:cyan">July</td>.
                    <td>22</td>
                </tr>
                <tr>
                    <td style="background-color:cyan">Sam</td>.
                    <td>30</td>
                </tr>
            </tbody>
        </table>
    </body>
</html>

Although .lsp can be relatively simple to output pages, in practice, it is not flexible enough, and we usually use the MVC framework, the description of .lsp here can all be used in the .view view template file in the MVC framework.

MVC Framework - System.Web.MVC

The features like routing, page rendering, and request context objects are relatively simple and can be difficult to deal with complex requirements. Typically we use an architecture designed on the MVC pattern.

Route Management

In MVC mode, routing is used to forward requests to the controller:

-- /plbr/route.lua
__Route__ "/{controller?|%a*}/{action?|%a*}/{id?/%d*}"
function MVC(context, controller, action, id)
    controller = controller ~= "" and controller or "home"
    action = action ~= "" and action or "index"
    id = tonumber(id)

    return ("/controller/%scontroller.lua"):format(controller), { Action = action, ID = id }
end

For specialization, the routing system provides a mechanism for pattern matching other than regular, which can take the form {name?|pattern}/.

  • {xxx} represents a capture.
  • {xxx?} means the capture will match an empty string, if it is empty, the / after it will also be ignored!
  • {xxxx|pattern} provides the pattern string for capture to force a match.

These captures are passed sequentially to the route processing function, which is responsible for stitching together the controller's path and passing in Action (required) and other information as the controller's initialization table to generate a new controller object to complete the request processing.

The following is an example of the request address and the corresponding controller.

Request URL Controller Address Initialization Table
/ /controller/homecontroller.lua { Action = "index" }
/user/12 /controller/usercontroller.lua { Action = "index", ID = 12 }
/user/account/12 /controller/usercontroller.lua { Action = account, ID = 12 }

System.Web.Controller

The controller file generates the controller class that is in charge of the business logic for the request. It is a resource file that can be modified in real time in development mode to see the results.

-- /html/controller/usercontroller.lua
class "UserController" (function(_ENV))
    inherit "Controller"

    local yield = coroutine.yield

    __Action__() __View__[[/view/user.view]]
    function index(self, context)
        return {
            -- special use, the real table contains the session items, for test only
            Data = context.Session.RawItems
        }
    end

    __Action__("data", HttpMethod.POST)
    function setdata(self, context)
        context.Session.Items[context.Request.Form.key] = context.Request.Form.value

        self:Redirect("/user") -- action to go to index
    end

    -- download test.csv file, use iterator to output results to client multiple times
    __Action__("download") __File__() __Iterator__()
    function download(self, context)
        yield("test.csv") -- since __File__ doesn't specify a filename, the first output will be the filename.

        yield("col1, col2, col3\n")

        for i = 1, 10 do
            for j = 1, 3 do
                yield(i * 10 + j)
                yield(",")
            end
            yield("\n")
        end
    end
end)

Then define the view

/html/view/user.view:

@{ master = "/share/global.master" }

@body{
    <table border="1">
        <thead>
            <tr>
                <th>Key</th>
                <th>Value</th>
            </tr>
        </thead>
        <tbody>
        @for key, data in pairs(self.Data) do
            <tr>
                <td style="background-color:cyan">@\key</td>.
                <td>@\data</td>
            </tr>
        @end
        </tbody>
    </table>
    <form action="/user/data" method="POST">
        <p>
            <span>Key</span><input type="text" name="key" value=""/>
        </p>
        <p>
            <span>value</span><input type="text" name="value" value=""/>
        </p>
        <input type="submit" value="submit"/>
    </form>
}

After restarting the server, you can access GET /user, after which you can output a new key-value pair, which will be saved in the session variable when you click submit, and after submitting, the client will be redirected to the /user path, so that you can directly use the index action to get the display content.

Here are some details.

  • The controller class name must match the file name (case insensitive)

  • The controller class must inherit System.Web.Controller.

  • The methods of the controller class need to use the __Action__ feature to bind the action. The action name (by default, the function name) and the HTTP method can be specified.

  • Multiple methods can bind to the same event, as long as they do not register the same HTTP method

  • Similar to using __View__ in a routing file, we can also specify the __Text__, __Json__, and __View__ properties for each action's handler to output the results in a particular format, noting that the processing of these properties is ignored if the method initiates a redirect operation before returning the results.

  • The __View__ feature is often used to qualify the output format of the processing method, and for convenience the controller class provides multiple methods to specify the output:

Methods Parameters Description
Text text/(iter, ...) output the specified text, or the text obtained from the iterator, to the client.
View path[, init] specify the path to the view template file and the initialization table, which will be output by the corresponding view rendering class.
Json object[, type] serialize the object to JSON data, output to the client
AutoSwitch path, init populate the initial data into the template or convert it to JSON data upon request and return it to the client.
File [name], text/(iter, ...) If no name is specified, the first string returned by the iterator will be used as the file name.
Redirect path[, raw] redirect
NotFound returns 404, no resources found.
Forbidden returns 403, disabling access to the resource
ServerError returns 500, the server error.

The view rules are the same as .lsp, so we don't repeat those examples. Let's focus on the Model. Also the __Switch__ is recommended that it is more convenient to return table data in the function, let the system determine the output.

Data - Model

Normally we would use the Abstract Data Entity Framework group for the Model section. This block mainly declares the mapping between the data entity class and the database table structure. This allows us to facilitate the processing of database data in the controller's business processes.

As an example, we create a user table in the PLoop database.

use PLoop;

CREATE TABLE IF NOT EXISTS `user`(`id` INT UNSIGNED AUTO_INCREMENT, `created` DATETIME NOT NULL, `name` VARCHAR(128), `telno` VARCHAR(128), PRIMARY KEY (`id`), UNIQUE IDX_name(`name`), UNIQUE IDX_telno(`telno`))ENGINE=InnoDB;

Actually, it is more convenient to automatically build a database based on the Model definition using a reflection mechanism, but currently only MySQL provides an interface for automatically building database tables based on the Model: MySQLConnection:DropAllTables() for the Delete All Tables and MySQLConnection:CreateNonExistTables(ns) used to create all non-existed datatables based on the ns namespace.

-- /plbr/database.lua
require "PLoop.System.Data"

Application "PLBR" (function(_ENV))
    -- Both libraries must be imported in order to use the features directly.
    import "System.Data"
    import "System.Configuration"

    __DataContext__()
    class "UserDataContext" (function(_ENV))

        export { NgxLua.MySQL.MySQLConnection, UserDataContext }

        -----------------------------------------------------------
        -- database connection configuration --
        -----------------------------------------------------------
        __Static__() property "ConnectionOption" { type = NgxLua.MySQL.ConnectionOption }

        -----------------------------------------------------------
        -- Construct methodology --
        -----------------------------------------------------------
        function __ctor(self)
            -- creating database connections
            self.Connection = MySQLConnection (UserDataContext.ConnectionOption)
        end

        -----------------------------------------------------------
        -- Data entity class --
        -----------------------------------------------------------
        __DataTable__{
            name = "user", indexes = {
                { fields = { "id" }, primary = true },
                { fields = { "name" }, unique = true },
                { fields = { "telno"}, unique = true },
            }
        }
        class "User" (function(_ENV))
            __DataField__{ autoincr = true }
            property "id" { type = NaturalNumber }

            __DataField__{ notnull = true }
            property "createdate" { type = Date }

            __DataField__()
            property "name" { type = String }

            __DataField__()
            property "telno" { type = String }
        end)
    end)

    -----------------------------------------------------------
    -- Configuration definitions for data connections --
    -----------------------------------------------------------
    __ConfigSection__(Application.ConfigSection.PLBR, "MySQL", NgxLua.MySQL.ConnectionOption)
    function applyMySqlSetting(field, value, app)
        UserDataContext.ConnectionOption = value
    end
end)

This file declares the UserDataContext database context class, which defines the User data entity class, and also declares the configuration definition of the database connection. Note that the above configuration definition is declared in Application.ConfigSection, so all web apps on the same server can actually use this configuration definition, but we can distinguish each web app by its PLBR name.

This configuration is set in config.lua as follows.

-- /plbr/config.lua
Application "PLBR" (function(_ENV))
    _Config = {
        PLBR = {
            MySQL = {
                host = "127.0.0.1",
                port = 3306,
                database = "PLoop",
                user = "admin",
                password = "Administrator password",
                charset = "utf8mb4",
                max_packet_size = 2 * 1024 * 1024,
            },
        },
        ErrorHandler = function(err, stack, context)
            context.Response.Write(err)
            context.Response:Close()
        end,
    }
end)

After the configuration file is loaded, the database connection is assigned to the UserDataContext context class.

This is followed by modifying init.lua to load the model file.

-- /plbr/init.lua
Application "PLBR" (function(_ENV))
    namespace "PLBR"

    class "HttpContext" {
        NgxLua.HttpContext,
        __ctor = function(self) self.Application = _ENV end
    }

    -- Session ID Manager
    NgxLua.JWTSessionIDManager{ CookieName = "PLBR_JWT", TimeoutMinutes = 1 * 24 * 60, Application = _ENV }

    -- session memory manager
    NgxLua.JWTSessionStorageProvider{ Application = _ENV }
end)

import "PLBR"

-- loading of data model files
require "plbr.database"

-- load routing file
require "plbr.route"

require "plbr.config"

Here is a simple example of registration.

/html/controller/usercontroller.lua

class "UserController" (function(_ENV)
    inherit "Controller"

    __Action__("index", HttpMethod.GET)
    function index(self, context)
        if context.Session.Items.userid then
            -- Already login
            with(UserDataContext())(function(ctx)
                local user = ctx.Users:Query{ id = context.Session.Items.userid }:First()
                self:View("/view/user.view", { User = user })
            end)
        else
            -- Redirect to login page
            self:View("/view/register.view")
        end
    end

    __Action__("register", HttpMethod.POST)
    function register(self, context)
        local telno = context.Request.Form.telno
        local name  = context.Request.Form.name

        with(UserDataContext())(function(ctx)
            local user = ctx.Users:Query{ telno = telno }:First()
            if user then
                context.Session.Items.userid = user.id
                return self:Redirect("/user")
            end

            with(ctx.Transaction)(function(trans)
                user = ctx.Users:Add{ telno = telno, name = name, createdate = Date.Now }
                ctx:SaveChanges()

                context.Session.Items.userid = user.id
                return self:Redirect("/user")
            end)
        end)
    end

    __Action__("logout", HttpMethod.GET)
    function logout(self, context)
        context.Session.Items.userid = nil
        self:Redirect("/user")
    end
end)

Then, we identify the user by saving userid in the user session. Here is the view file:

/html/view/user.view

@{ master = "/share/global.master" }

@body{
    <p>User name: @self.User.name</p>
    <p>Mobile number: @self.User.telno</p>
    <p>Create date: @self.User.created:ToString()</p>

    <a href="/user/logout">Logout</a>
}

/html/view/register.view

@{ master = "/share/global.master" }

@body{
    <form action="/user/register" method="POST">
        <p>
            <span>Mobile Number</span>
            <input type="text" name="telno"/>
        </p>
        <p>
            <span>Username</span>
            <input type="text" name="name"/>
        </p>
        <input type="submit" value="register"/>
    </form>
}

Then reboot the server (with the model file added) and access GET /user to try to register and log out. You can also check the database to see which users have been registered.

For more information on database handling, such as automatic caching, read [Abstract Data Entity Framework] (0.19.data.md) for more information.

Form validation

The controller in the MVC framework significantly reduces the use of routing, the need for functionality can simply add the controller. At this point, you can already begin to complete the basic web development functions. Here are some of the enhanced features provided by the framework. The first is the form validation feature.

Usually HTTP requests are accompanied by a request string (GET) or form (non-GET, such as POST, PUT), in order to avoid user error input (whether intentional or unintentional), we need to validate the form contains the request value, which is a very common and troublesome operation, completed by the developer itself is very tedious, and difficult to maintain.

To solve this problem, combined with the PLoop struct system, the Web framework provides the System.Web.__Form__ feature to complete the form validation, using the usercontroller.register action above as an example.

-- for localization, doesn't support multiple languages directly
-- if multiple languages are required, specify a constant to represent a string here.
-- and then replace it with the actual string in the display location.

-- Here is constant error message that can be replaced here
__Form__.RequireMessage = "%s cannot be null"
__Form__.NumberMessage = "%s must be numbers"

-- Define custom struct for different inputs that require an error message.
-- __base must be specified, especially the number type, which must be specified as at least Number
-- so that the system can complete the conversion automatically
struct "Telno" { __base = String,
    function (val)
        if #val ~= 11 or not val:match("^1%d+$") then return "%s is not a valid mobile number" end
    end
}

struct "UserName" { __base = String,
    function (val)
        if val:match("[%s%p]+") then return "%s cannot contain special characters" end
        if #val > 24 then return "%s length exceeds limit" end
    end
}

__Action__("register", HttpMethod.POST)
__Form__{
    telno = { type = Telno, require = true },
    name = { type = UserName, require = true },
}
function register(self, context, form, err)
    if err then
        return self:View("/view/register.view", { Form = form, Error = err })
    end

    with(UserDataContext())(function(ctx))
        local user = ctx.Users:Query{ telno = form.telno }:First()
        if user then
            context.Session.Items.userid = user.id
            return self:Redirect("/user")
        end

        with(ctx.Transaction)(function(trans)
            user = ctx.Users:Add{ telno = form.telno, name = form.name, createdate = Date.Now }
            ctx:SaveChanges()

            context.Session.Items.userid = user.id
            return self:Redirect("/user")
        end)
    end)
end

Modify register.view to handle errors.

/html/view/register.view

@{ master = "/share/global.master" }

@body{
    <form action="/user/register" method="POST">
        <p>
            <span>Mobile Number</span>
            <input type="text" name="telno" value="@(self.Form and self.Form.telno)"/>
            @if self.Error and self.Error.telno then
            <br><span style="color:red">@\self.Error.telno:format("phone number")</span>
            @end
        </p>
        <p>
            <span>Username</span>
            <input type="text" name="name" value="@(self.Form and self.Form.name)"/>
            @if self.Error and self.Error.name then
            <br><span style="color:red">@\self.Error.name:format("username")</span>
            @end
        </p>
        <input type="submit" value="register"/>
    </form>
}

After saving, re-register and output the error message at will, for example, enter 1 for mobile number and test.123 for user name, then the page returned after registration is:

<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>My web site</title>
        <! -- javascript placeholder -->
    </head>
    <body>
        <form action="/user/register" method="POST">
            <p>
                <span>Mobile Number</span>
                <input type="text" name="telno" value="1"/>
                <br><span style="color:red">Mobile phone number is not a valid mobile phone number</span>.
            </p>
            <p>
                <span>Username</span>
                <input type="text" name="name" value="test.123"/>
                <br><span style="color:red">Usernames cannot contain special characters</span>.
            </p>
            <input type="submit" value="register"/>
        </form>
    </body>
</html>

This allows us to display both the user input value and the error message to the user. Here are some details.

  • The definition table that follows __Form__ is essentially a structure definition, so the example above actually defines an anonymous structure:

    local formType = struct {
        telno = { type = Telno, require = true },
        name = { type = UserName, require = true },
    }

    So, the form validation is actually the validation of the request string or form through this anonymous structure, and if the validation fails, the failure message is saved in the err argument

  • Regardless of whether the form is validated or not, the form or request string is saved in the form argument and passed to the original function along with the err argument. We can determine if the form is validated or not by determining the existence of the err argument.

  • __Form__.RequireMessage, __Form__.NumberMessage and __Form__.DateMessage need to be specified as localized strings, although they can be replaced by constants representing strings.

  • Custom struct can be added to the validation method, and such custom struct can be saved in a separate file for easy management and reuse. Custom struct must be based directly or indirectly on String or Number as the base struct, so that the system will make a corresponding conversion of the uploaded values before validation.

  • The value types can only be struct types based on String or Number, for class type, only System.Date can be used, it accept timestamp or string like 2021-07-13 12:53:12.

In general, we only need to define member struct, but in practice complex forms are also supported.

__Form__ { -- Array struct with member struct as elements.
    person = struct {
        struct {
            name = String,
            age = Number,
        }
    }
}
__Action__() __Json__()
function query(self, context, form, err)
    return { Form = form, Error = err }
end

This means that the incoming form needs to contain a person key, the value of which is an array of tables with name and age fields for each element of the table (member struct). They actually match the form as designed below.

var form = {
    "person[1].name": "Ann",
    "person[1].age": 21,
    "person[2].name": "Ben",
    "person[2].age": 23,

}

// Or the more convenient Bulk Form.
var form = {
    person = [
        {
            "name": "Ann",
            "age" : 21,
        },
        {
            "name": "Ben",
            "age" : 23,
        }
    ]
}

This will allow you to design the form as an object. In addition, members declared as arrays can also accept single values, and the system will automatically encapsulate single values into arrays.

User authentication

Before we can do the actual processing, we also need to authenticate the user's permissions, which includes two things: whether the user is logged, and whether the user has the specified permissions. We can use the System.Web.__Login__ feature to do this, or we can do it ourselves. This is not mandatory, and like __Form__ is a framework enhancement.

Login Authentication

__Login__ can check if a specific key exists in the user session to determine if the user is logged in, and if not, redirect the visitor to the specified login page and keep the path of the previous visit so that after logging in, you can jump back.

To do this, we need to change the configuration file:

-- /plbr/config.lua
Application "PLBR" (function(_ENV))
    _Config = {
        PLBR = {
            MySQL = {
                host = "127.0.0.1",
                port = 3306,
                database = "PLoop",
                user = "admin",
                password = "Administrator password",
                charset = "utf8mb4",
                max_packet_size = 2 * 1024 * 1024,
            },
        },
        Validator = {
            Login = {
                Key = "userid",
                LoginPage = "/user/login",
                PathKey = "path",
            }
        },
        ErrorHandler = function(err, stack, context)
            context.Response.Write(err)
            context.Response:Close()
        end,
    }
end)

The settings for __Login__ are configured here via Validator.Login and consist of three configuration items:

  • Key - a specific key in the user's session that determines if the user is logged in or not
  • LoginPage - the address of the login page
  • PathKey - stores the key to the previously accessed address.

The above configuration redirects to GET /user/login?path=the previously accessed address if userid does not exist in the check session.

We can adjust the user controller:

-- /html/controller/usercontroller.lua
class "UserController" (function(_ENV))
    inherit "Controller"

    -----------------------------------------------------------
    -- Validation of structure definition --
    -----------------------------------------------------------
    __Form__.RequireMessage = "%s cannot be null"
    __Form__.NumberMessage = "%s must be numbers"

    struct "Telno" { __base = String,
        function (val)
            if #val ~= 11 or not val:match("^1%d+$") then return "%s is not a valid mobile number" end
        end
    }

    struct "UserName" { __base = String,
        function (val)
            if val:match("[%s%p]+") then return "%s cannot contain special characters" end
            if #val > 24 then return "%s length exceeds limit" end
        end
    }

    -----------------------------------------------------------
    -- Controller action definition --
    -----------------------------------------------------------
    __Login__()
    __Action__("index", HttpMethod.GET)
    function index(self, context)
        with(UserDataContext())(function(ctx))
            local user = ctx.Users:Query{ id = context.Session.Items.userid }:First()
            self:View("/view/user.view", { User = user })
        end)
    end

    __Action__("login", HttpMethod.GET)
    function login(self, context)
        self:View("/view/register.view")
    end

    __Action__("register", HttpMethod.POST)
    __Form__{
        telno = { type = Telno, require = true },
        name = { type = UserName, require = true },
    }
    function register(self, context, form, err)
        if err then
            return self:View("/view/register.view", { Form = form, Error = err })
        end

        with(UserDataContext())(function(ctx))
            local user = ctx.Users:Query{ telno = form.telno }:First()
            if user then
                context.Session.Items.userid = user.id
                return self:Redirect("/user")
            end

            with(ctx.Transaction)(function(trans)
                user = ctx.Users:Add{ telno = form.telno, name = form.name, createdate = Date.Now }
                ctx:SaveChanges()

                context.Session.Items.userid = user.id
                return self:Redirect("/user")
            end)
        end)
    end

    __Action__("logout", HttpMethod.GET)
    function logout(self, context)
        context.Session.Canceled = true
        self:Redirect("/user")
    end
end)

After logging out and accessing /GET /user, you will be redirected to /user/login?path=%2Fuser, and after logging in, please handle the redirecting function yourself.

Custom login authentication

The default login authentication relies on variables saved in the user session, but the actual situation is more complicated, such as disabling multiple logins, etc. The system cannot directly mention the configuration of similar processing, but the developer can provide login authentication processing methods to perform custom login authentication, and note that once set, the default checks will not be used.

-- /plbr/config.lua
Application "PLBR" (function(_ENV))
    _Config = {
        PLBR = {
            MySQL = {
                host = "127.0.0.1",
                port = 3306,
                database = "PLoop",
                user = "admin",
                password = "Administrator password",
                charset = "utf8mb4",
                max_packet_size = 2 * 1024 * 1024,
            },
        },
        Validator = {
            Login = {
                LoginChecker = function(context)
                    if context.Session.Items["userid"] ~= nil then
                        return true
                    else
                        return false
                    end
                end,
                LoginPage = "/user/login",
                PathKey = "path",
            }
        },
        ErrorHandler = function(err, stack, context)
            context.Response.Write(err)
            context.Response:Close()
        end,
    }
end)

This check method has only the context object as an argument, returning true means the user logged in and authenticated.

Permission authentication

After logging in, we need to authenticate users according to their rights, but the permission system should be implemented by the actual web application itself, so the system needs the Web App to provide the permission authentication processing itself, which is usually done in the configuration file.

-- /plbr/config.lua
Application "PLBR" (function(_ENV))
    _Config = {
        PLBR = {
            MySQL = {
                host = "127.0.0.1",
                port = 3306,
                database = "PLoop",
                user = "admin",
                password = "Administrator password",
                charset = "utf8mb4",
                max_packet_size = 2 * 1024 * 1024,
            },
        },
        Validator = {
            Login = {
                Key = "userid",
                LoginPage = "/user/login",
                PathKey = "path",

                AuthorityChecker= function (context, requirement, path)
                    local level = context.Session.Items.level or 0
                    if level >= requirement then
                        return true
                    else
                        return false, path or "/user/forbidden"
                    end
                end,
            }
        },
        ErrorHandler = function(err, stack, context)
            context.Response.Write(err)
            context.Response:Close()
        end,
    }
end)

Here we need to configure Validator.Login.AuthorityChecker, its first parameter is the context object, the next two parameters are the privilege values configured after __Login__, here, we use requirement as requirement level and path as return path.

This function returns true so the authentication passed, if it fails, you need to return a path to use as a redirect.

Then adjust the controller and look at an example (without repeating the code that already exists).

class "UserController" (function(_ENV))
    inherit "Controller"

    __Action__() __Login__(5)
    function noright(self, context)
    end

    __Action__() __Text__()
    function forbidden(self, context)
        return "You do not have permission to access this resource"
    end
end)

After logging in and accessing GET /user/noright, you will be redirected to /user/forbidden. User permissions need to be checked by the web app itself. __Login__ is only used to provide configuration options. You can do it yourself if you want.

Internal request validation

In actual website construction, we provide a lot of processing for internal requests only, we can use the __InnerRequest__ attriute to check if the request is a inner request, and returns 404 to rejects the normal request.

class "UserController" (function(_ENV))
    inherit "Controller"

    __InnerRequest__()
    __Action__()
    function getentity(self, context)
        -- omitted
    end
end)

Http context request processing flow

The above has been a complete introduction to the web framework, which can be directly applied to the development of web features. The following is the framework for the development of the part, including the request processor, custom rendering engine and so on. You can stop reading if you don't need to.

In many of the above types of processing Http requests, routing, controllers, user session managers, all extended the System.Web.IHttpContextHandler interface.

Http requests are managed by a context object, but their processing is completely defined by the IHttpContextHandler interface, and all objects that need to process the request must directly or indirectly extend this interface.

Also similar to using __Text__, __Josn__ and __View__ in routing files, which internally define anonymous classes that extend this interface, the object function is encapsulated in the object of these anonymous classes to implement the processing.

The following is the specific definition of this interface.

--- Stages of the request processing process
__Flags__() __Sealed__()
enum "System.Web.IHttpContextHandler.ProcessPhase" {
    "Init", -- initialization.
    "Head", -- processing and output Http Head
    "Body", -- processing and outputting Http Body
    "Final" - reclaims resources after processing, such as closing cached database connections, etc.
}

--- Prioritization of request processing, with each stage requiring the use of individual registered middleware in order of priority
__Sealed__() __Default__(0)
enum "System.Web.IHttpContextHandler.HandlerPriority" {
    Highest = 2,
    Higher = 1,
    Normal = 0,
    Lower = -1,
    Lowest = -2,
}
abstract_property type default description
IsRequestHandler Boolean false whether the request handler will check the request has been processed tag, this property is used only in the initialization phase, the request handler with this property is true will usually set the request Handled property, once set to true, other will check this tag will not be executed, such as the initialization of the authentication, and if it fails, the request is marked as processed, then the subsequent route processing will not be executed.
ProcessPhase ProcessPhase Head + Body Set the phase in which the request processor acts, the processor will be invoked only in the phase it registers.
Priority HandlerPriority Normal set the priority of the requesting processor, currently does not support phased priority setting, if necessary, it is better to split into two processors.
AsGlobalHandler Boolean false Is the request handler used globally, if so, it is used to process all requests and is not recycled, e.g. the route manager is used globally?
Application Read and write the Web App bound to this request processor.

The abstract property does not necessarily need to be implemented, and can usually be used directly.

abstract method parameter description
Process context: HttpContext, phase: ProcessPhase Execute request according to phase

The abstract method must be implemented, although it is not a mandatory requirement, but if it is not implemented, it is useless.

Method Parameters Description
RegisterToContext context: HttpContext/nil registers itself to the given or current context, as a temporary request processor for that context, a typical scenario is to route to load a specific controller file, and then use the corresponding controller class to generate the object, and then registers this object as a temporary request processor, after which it is Can process requests and output results to clients

There are typically three types of request processors.

  • Temporary request handlers - for example, static request handlers, controllers, view rendering classes such as .lsp, etc., generated directly in the routing file using a feature binding function such as __Json__, are temporarily registered to the context based on the request.

  • Web App Level Global Processors - register as global and specify the Web App's request processors, which only process requests corresponding to the Web App

  • Server-level global processors - there is no global request processor specified for the Web App, they handle all requests across the server and are not limited to which Web App is involved

Take for example the Server-level global processor that writes cookies to the client in NgxLua.

-- the handler to send cookies
IHttpContextHandler {
        ProcessPhase = IHttpContextHandler.ProcessPhase.Head,
        Priority = IHttpContextHandler.HandlerPriority.Lowest,
        AsGlobalHandler = true,
        Process = function(self, context, phase)
            if not context.IsInnerRequest then
                local cookies = context.Response.Cookies
                if next(cookies) then
                    local cache = {}
                    local cnt = 1
                    for name, cookie in pairs(cookies) do
                        cache[cnt] = tostring(cookie)
                        cnt = cnt + 1
                    end
                    ngx.header['Set-Cookie'] = cache
                end
            end
        end,
    }

The following are some key points.

  • Since no specific Web App is involved, this request processor is used for all Web Apps, outputting the code set cookies to the client.

  • Cookies must be output before the Http Head is sent to the client, so it must be done at the Head stage, and its priority is chosen lowest to ensure that other processing has ended.

  • IHttpContextHandler has an anonymous class, so it can be used to build objects directly, and the Process method must be implemented.

  • Note that Process checks for IsInnerRequest, this is to avoid, outputting cookies to the internal request, which is meaningless, as the internal request exists and needs to be taken care of when making a global request processor.

Here's a closer look at how the request is processed:

  • Init - the initialization phase, where temporary processors are usually registered
    • Execution of server-level global request processors
    • Execution of global request processors at the app level
  • Head -- the Head output phase, which is used to produce the output of the Head, such as the type of result, cookie information, etc.
    • Implementation of the interim request processor
    • Execution of global request processors at the app level
    • Execution of server-level global request processors
    • Sending a Head response to the client
  • Body -- Body output phase, used to generate Body output, such as JSON packets, rendered web page content, etc., if the Head outputs a redirect, then this phase is not executed.
    • Implementation of the interim request processor
    • Execution of global request processors at the app level
    • Execution of server-level global request processors
    • Close the request and end the output
  • Final -- termination phase, for release of resources, etc.
    • Implementation of the interim request processor
    • Execution of global request processors at the app level
    • Execution of server-level global request processors

In the controller's code, we typically use self:View(path, params) to output the result, but the actual call spans three phases.

  • When the View method is called, the output type text.html and other header information (cookies) set before the View is called are written to the output first.

  • Interrupt the processing of the View method, call the Process method that returns IHttpContextHandler, and then send the Head response, the controller object wakes up the View method in the Body phase to continue processing

  • The View method renders the page, outputs it to the client, and then interrupts itself again.

  • After IHttpContextHandler closes the request, it enters the termination phase and the controller object wakes up the View method.

  • The View method finishes processing and returns the call to the caller and the action method, which generally performs subsequent processing such as saving data to the cache, closing the database, etc., without requiring the user to wait.

These are the actual implementation details of the controller, but they are usually used without the developer's attention.

Error handling

In the previous demo, we provided error handling, which effectively tracks errors in the Lua file code, as well as the view template file, and then the web app determines the error handling scheme. In the above example, we output the error directly to the client, but combined with the description of the request processing phase above, the actual error handling is a bit more complex, and here is a more complete approach.

In the following example, the unmodified parts will not be repeated, so please cross-reference the modified example.

-- /plbr/config.lua
Application "PLBR" (function(_ENV))
    export { System.Web.IHttpContextHandler.ProcessPhase }

    _Config = {
        ErrorHandler = function(err, stack, context)
            if not context.IsInnerRequest then
                -- For non-internal requests, determine how to output to the client.
                if context.ProcessPhase == ProcessPhase.Init or context.ProcessPhase == ProcessPhase.Head then
                    -- can display an error page to the client
                    context:ProcessInnerRequest("/error", { error = err })
                    return context.Response:Close()
                elseif context.ProcessPhase == ProcessPhase.Body then
                    -- If you have already started outputting text, then output the error message as well for debugging.
                    -- When operating, an error throw to the log should be used.
                    context.Response.Write(err)
                    return context.Response:Close()
                end
            end

            -- Throwing out errors, whether at the termination stage or on internal request, to be handled externally or written to the log.
            error(err, stack or 0)
        end,
    }
end)
-- /plbr/route.lua
Application "PLBR" (function(_ENV))
    __Route__ "/error" __Form__{ error = String }
    __View__ "error.view" [[
        <html>
            <head>
                <title>Error</title>
            </head>
            <body>
                <p>@self.error</p>
            </body>
        </html>
    ]]
    function errorhandler(context, form, err)
        return form
    end

    __Route__ "/{controller?|%a*}/{action?|%a*}/{id?|%d*}"
    function MVC(context, controller, action, id)
        controller = controller ~= "" and controller or "home"
        action = action ~= "" and action or "index"
        id = tonumber(id)

        return ("/controller/%scontroller.lua"):format(controller), { Action = action, ID = id }
    end
end)

If we add an error:

/html/view/index.view:

@{ master = "/share/global.master" }

@body{
    <p>User name: @self.User.name</p>
    <p>Mobile number: @self.User.telno</p>
    <p>Create date: @self.User.create:ToString()</p>

    <a href="/user/logout">Logout</a>
}

When accessed this way, the error occurs in the Body phase, so the output is direct:

<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>My web site</title>
        <! -- javascript placeholder -->
    </head>
    <body>
        <p>Username: abcd</p>
        <p>Mobile number: 13400000011</p>
        <p>Creation date: /root/www/html/view/user.view:6: attempt to index field 'create' (a nil value)

We can quickly find the error location with the error message. Restore the template, try to make an error in the Head phase, modify the controller file.

-- /html/controller/usercontroller.lua
class "UserController" (function(_ENV))
    inherit "Controller"

    __Login__()
    __Action__("index", HttpMethod.GET)
    function index(self, context)
        with(UserDataContext())(function(ctx))
            local user = ctx.Uses:Query{ id = context.Session.Items.userid }:First()
            self:View("/view/user.view", { User = user })
        end)
    end
end)

Users is incorrectly written as Uses, access GET /user again, you can get:

<html>
    <head>
        <title>Error</title>
    </head>
    <body>
        <p>/root/www/html/controller/usercontroller.lua:30: attempt to index field 'Uses' (a nil value)</p>
    </body>
</html>

Then we get the full error page output. If you use it on demand.

Relative and absolute paths

In the above example, both absolute paths (starting with/to) and relative paths are involved wherever access to other files is involved. To summarize.

  • The root path/corresponding drive path is read from context.Request.Root, which is the root location set in nginx.conf.

  • When specifying a file path, the absolute path starting with /, i.e., the destination file path is obtained by merging the root path and the specified file path.

  • When specifying file streaks, relative paths that do not start with / are merged with the directory where the current file is located and the specified file path to get the destination file path (allowing ...' to point to a higher directory). is allowed to be used to point to the upper directory)

Configuration of the view

In each view template file, the first line is usually a configuration table, independent of the rendering engine used. In the example above, the configurations master, helper, code are used.

In fact, we can also provide a default web app-based configuration for the view template file, and the configuration that comes with the view file overrides the default configuration.

_Config = {
    View = {
        Default = {
            master = nil, -- assigns a master template to the view template file, which is not normally specified here.
            helper = nil, -- the helper file assigned to the view template file, which is not normally specified here.
            reload = nil, -- Specify whether to reload the template file, if the file has been modified, generally do not set, go to the server configuration Debug, please note that no matter set to true or false, the Debug setting is no longer valid.
            encode = true, -- specifies whether to transcode all the expressions output by the template, can be turned on if you are worried about the developer's omission.
            noindent = true, -- disable indentation, all indentation is removed when generating the rendering class, this can improve output performance and reduce the size of the transfer to the client.
            nolinebreak = true, -- disable line feeds, the system will remove all line feeds when generating the rendering class to improve efficiency and reduce the length of the transferred text, please note that when using JS code on the page, be sure to use `; ` and other spacing lines to avoid problems after the merge.
            linebreak = "\n", -- set line feed character
            engine = nil, -- default rendering engine, usually not set, but if you want to customize the rendering engine, this can be used instead of the System.
            asinterface = nil, -- generates file results as an interface, currently only `.helper` needs this configuration, so it is not set here.
            export = { -- exports a special set of functions that can be used directly in view templates
                error = function(message, target)
                    return Struct.GetErrorMessage(message, target)
                end,
            },
        },

        -- note Default above, that's common to all view templates, regardless of template type!
        -- we can also be more precise in configuring specific view template types, ViewPage is for all `.view` files.
        -- also, the configuration of the file itself is the highest priority and will override the configuration here!
        ViewPage = {
            reload = true,
        },
    },
}

The following are all the view template types, and the superclasses it inherits (make your own default configuration with the configuration of the superclasses).

Name Superclass File Suffix Description
Default .* Common to all templates
HtmlPage Default .* Common to all HTML templates
StaticFile Default .* Common to all static files
ViewPage HtmlPage .view View file generic
EmbedPage HtmlPage .embed Embed view file generic
PageHelper HtmlPage .helper help file generic
LuaServerPage HtmlPage .lsp LSP file general purpose
MasterPage HtmlPage .master generic master template file
CssFile StaticFile .css CSS File General
JavascriptFile StaticFile .js JS File General

Modify our configuration files like (keep the rest, add only)

-- /plbr/config.lua
Application "PLBR" (function(_ENV))
    _Config = {
        View = {
            Default = {
                noindent = true,
                nolinebreak = true,
            },
        },
    }
end)

Then accessing GET /user you can see the result as.

<html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><title>My web site</title><! -- javascript placeholder --></head><body><p>Username: abcd</p><p>Mobile number: 13400000011</p><p>Creation date: 2019-11-05 20:47:26</p><a href="/user/ logout">logout</a></body></html>

Evidently, indentation and line feeds are gone. Note that if you use this mode of output, JS line feeds must have a ; sign and comments need to be in the form of /* xxxx */ to ensure that there are no problems with single line output.

Custom rendering engine

PLoop only provides two rendering engines System.Web.IRenderEngine for static files, and System.Web.PageRenderEngine for our view template files.

But PLoop provides an interface for custom rendering engines, first, let's see the following implementation of the static file rendering engine.

namespace "System.Web"

--- Rules for rendering conversion to code
__Sealed__() __AutoIndex__()
enum "RenderContentType" {
    "RecordLine",
    "StaticText",
    "NewLine",
    "LuaCode",
    "Expression",
    "EncodeExpression"
    "MixMethodStart",
    "MixMethodEnd",
    "CallMixMethod",
    "RenderOther",
    "InnerRequest",
}

__Sealed__() __AnonymousClass__()
interface "IRenderEngine"(function (_ENV)
    extend "Iterable"

    -- This method is already implemented, so it's not necessary or advisable to override it.
    __Iterator__()
    function GetIterator(self, reader) return self:ParseLines(reader) end

    -- initialization based on file configuration (based on the configuration corresponding to the file type and the default configuration)
    __Abstract__() function Init(self, loader, config) end

    --- reads the object from the stream, parses all output rows, and returns all rendering processing actions
    -- in practice, the rendering class is generated using the rules specified by RenderContentType.
    __Abstract__() function ParseLines(self, reader)
        local yield = coroutine.yield

        -- the use of yields is a good way to keep the context of the process.

        -- First, every rendering class should define or inherit (via a parent template) to a Render method, which is the starting point for rendering.
        yield(RenderContentType.MixMethodStart, "Render")

        for line in reader:ReadLines() do
            line = line:gsub("%s+$", "")

            -- each line read needs to be recorded using RecordLine first.
            -- so that the system can match the line numbers on both sides to give the error location when the rendering class makes an error
            yield(RenderContentType.RecordLine, line)

            -- since it is a static file, just output the text line as static text.
            yield(RenderContentType.StaticText, line)

            -- give a line break at the end, which creates a good display style
            -- indentation is handled automatically, no engine processing required
            yield(RenderContentType.NewLine)
        end

        -- end of Render method definition
        yield(RenderContentType.MixMethodEnd)

        -- file parsing is finished, then the system automatically generates the type according to the configuration.
    end
end)

If you want to customize a rendering engine, you need to extend the IRenderEngine interface and implement two methods yourself

abstract method parameter description
Init loader: IOutputLoader, config: RenderConfig initialize with a file loader (usually don't mind) and configuration (e.g. read config.noindent), it is rarely necessary to implement this method.
ParseLines reader: TextReader fetches all the lines from the stream read object, and returns the rendering action and parameters according to the text via yield.

The following are the rendering actions and parameters defined by RenderContentType.

Action Parameter Description
RecordLine line:String records the current line so that the system can match the error line of the converted code to the definition line.
StaticText text:String output static text
NewLine Output Line Feed
LuaCode line:String output Lua code, such as if xxx then
Expression exp:String Output Expression
EncodeExpression exp:String expose an expression to be transcoded.
MixMethodStart name:String begins the definition of the mix method, the web part is actually a mix method, which is actually a class method, but with special handling.
MixMethodEnd ends the definition of the mix method.
CallMixMethod name:String, params:String, default:String, issupercall:Boolean callMixMethod, if undefined, output default value, if issupercall is specified, call the mother template processing, similar to @{ suepr:body}
RenderOther path:String, params:String, default:String rendering other files, e.g. @[/share/notice.embed]
InnerRequest url:String, params:String Execute InnerRequest

Here is a relatively complex example where we define a new template, and a new format.

/html/view/index.wf

html
    head
        title
            > my web site
    body
        div #mainDiv .center
            p #random style='width:100px'
                > random is {{ math.random(10000) }} pts

We need to define two classes, one for parsing the engine of this template and the other for loading the file, adding the file reference first.

-- /plbr/init.lua
Application "PLBR" (function(_ENV))
    namespace "PLBR"

    class "HttpContext" {
        NgxLua.HttpContext,
        __ctor = function(self) self.Application = _ENV end
    }

    -- Session ID Manager
    NgxLua.JWTSessionIDManager{ CookieName = "PLBR_JWT", TimeoutMinutes = 1 * 24 * 60, Application = _ENV }

    -- session memory manager
    NgxLua.JWTSessionStorageProvider{ Application = _ENV }
end)

import "PLBR"

-- loading of data model files
require "plbr.database"

-- loading of new template types
require "plbr.waterfall"

-- load routing file
require "plbr.route"

-- load configuration files
require "plbr.config"

Then create:

-- /plbr/waterfall.lua
Application "PLBR"(function(_ENV)
    import "System.IO.Resource"
    import "System.Web"

    -- Define the rendering engine
    class "WaterFallEngine"(function (_ENV)
        extend "IRenderEngine"

        local yield = coroutine.yield

        local NOT_CHILD = 0
        local NODE_ELE = 1
        local TEXT_ELE = 2

        local function parseLine(self, reader, chkSpace, isFirstChild)
            line = self.CurrentLine or reader:ReadLine()

            self.CurrentLine = nil

            if not line then return NOT_CHILD end
            line = line:gsub("%s+$", "")

            yield(RenderContentType.RecordLine, line)

            local space, tag, ct = line:match("^(%s*)(%S+)%s*(.*)$")

            space = space or ""

            if tag == ">" then
                -- Display text
                -- For example "random is {{ math.random(10000) }} pts"
                local startp = 1
                local expSt, expEd = ct:find("{{.-}}", startp)

                while expSt do
                    -- "random is" is static text
                    yield(RenderContentType.StaticText, ct:sub(startp, expSt-1))

                    -- math.random(10000) is the expression
                    yield(RenderContentType.Expression, ct:sub(expSt + 2, expEd-2))

                    startp = expEd + 1
                    expSt, expEd = ct:find("{{.-}}", startp)
                end

                -- The remaining "pts" is also static text
                yield(RenderContentType.StaticText, ct:sub(startp))

                return TEXT_ELE
            end

            -- Check if it is a child node
            if not chkSpace or #space> #chkSpace then
                -- If it is a child node
                -- for "div #mainDiv .center"

                if chkSpace then
                    if isFirstChild then
                        -- If it is a child node and is the first element, you need to add a new row first
                        yield(RenderContentType.NewLine)
                    end

                    -- ""Blank is also static text and needs to be output (otherwise the system cannot ensure indentation)
                    yield(RenderContentType.StaticText, space)
                end

                -- Get id, class and other attributes
                local id, cls = "", ""
                local cache = {tag}

                -- Get and remove id
                ct = ct:gsub("#(%w+)", function(w) id = w return "" end)
                if #id> 0 then
                    table.insert(cache, ([[id="%s" name="%s"]]):format(id, id))
                end

                -- Get and remove class
                ct = ct:gsub("%.(%w+)", function(w) cls = cls .. w .. "," return "" end)
                if #cls> 0 then
                    table.insert(cache, ([[class="%s"]]):format(cls:sub(1, -2)))
                end

                -- Get the remaining attributes
                ct = ct:gsub("^%s*(.-)%s*$", "%1")
                if ct and #ct> 0 then
                    table.insert(cache, ct)
                end

                -- "div #mainDiv .center" -> Static text: <div id="mainDiv" name="mainDiv" class="center">
                yield(RenderContentType.StaticText, "<" .. table.concat(cache, "") .. ">")

                -- Check the next line
                local firstNode = true

                while true do
                    local ret = parseLine(self, reader, space, firstNode)

                    firstNode = false

                    if ret == TEXT_ELE then
                        -- Close HTML tags
                        yield(RenderContentType.StaticText, "</" .. tag .. ">")

                        -- Generate a new line
                        yield(RenderContentType.NewLine)

                        return NODE_ELE
                    elseif ret == NODE_ELE then
                        -- Continue processing child nodes
                    else
                        -- Reach the end of the node
                        if #space> 0 then
                            yield(RenderContentType.StaticText, space)
                        end

                        -- Close tab
                        yield(RenderContentType.StaticText, "</" .. tag .. ">")

                        -- Generate a new line
                        yield(RenderContentType.NewLine)

                        return NODE_ELE
                    end
                end
            else
                -- This is not a child node, return to the node before NOT_CHILD is closed
                self.CurrentLine = line

                return NOT_CHILD
            end
        end

        function ParseLines(self, reader)
            yield(RenderContentType.MixMethodStart, "Render")

            parseLine(self, reader)

            yield(RenderContentType.MixMethodEnd)
        end
    end)


    -- defines the view file loading class and binds the suffix name, the function is mainly performed by `__PageRender__`.
    -- later, in `_Config.View`, a separate configuration can be defined for `WaterFallPage`.
    -- note that the last { engine = WaterFallEngine } is the default configuration specified for `WaterFallPage`.
    -- this is where the rendering engine is specified.
    __ResourceLoader__"wf" -- Register for the .wf files
    __PageRender__("WaterFallPage", IOutputLoader, { engine = WaterFallEngine })
    class "System.Web.WaterFallPage" { IOutputLoader }

    -- for simplicity's sake, make a route
    __Route__ "/index"
    __View__ "/view/index.wf"
    function index(context)
        return { name = "Ann" }
    end
end)

To check the generated render class, we can open the temporary directory settings:

-- /plbr/config.lua
Application "PLBR" (function(_ENV))
    export { System.Web.IHttpContextHandler.ProcessPhase }

    _Config = {
        View = {
            Temporary = "/temp",
        },
    }
end)

Please create the directory ~/www/html/temp, which will hold the code that the view file is converted to.

So, restarting nginx and accessing GET /index gives you.

<html>
    <head>
        <title>my web site</title>
    </head>
    <body>
        <div id="mainDiv" name="mainDiv" class="center">
            <p id="random" name="random" style='width:100px'>random is 2377 pts</p>
        </div>
    </body>
</html>

Open the /html/temp/index.wf.lua file, which is the internal code of our rendering class.

local _PL_HtmlEncode, tostring, select = System.Web.HtmlEncode, System.Web.ParseString, select

function Render(self, _PL_write, _PL_indent)
    _PL_indent = _PL_indent or ""
    _PL_write(_PL_indent)
    _PL_write("<html>")
    _PL_write("\n")
    _PL_write(_PL_indent)
    _PL_write("\9")
    _PL_write("<head>")
    _PL_write("\n")
    _PL_write(_PL_indent)
    _PL_write("\9\9")
    _PL_write("<title>")
    _PL_write("my web site")
    _PL_write("</title>")
    _PL_write("\n")
    _PL_write(_PL_indent)
    _PL_write("\9")
    _PL_write("</head>")
    _PL_write("\n")
    _PL_write(_PL_indent)
    _PL_write("\9")
    _PL_write("<body>")
    _PL_write("\n")
    _PL_write(_PL_indent)
    _PL_write("\9\9")
    _PL_write("<div id=\"mainDiv\" name=\"mainDiv\" class=\"center\">")
    _PL_write("\n")
    _PL_write(_PL_indent)
    _PL_write("\9\9\9")
    _PL_write("<p id=\"random\" name=\"random\" style='width:100px'>")
    _PL_write("random is ")
    _PL_write(tostring( math.random(10000) ))
    _PL_write(" pts")
    _PL_write("</p>")
    _PL_write("\n")
    _PL_write(_PL_indent)
    _PL_write("\9\9")
    _PL_write("</div>")
    _PL_write("\n")
    _PL_write(_PL_indent)
    _PL_write("\9")
    _PL_write("</body>")
    _PL_write("\n")
    _PL_write(_PL_indent)
    _PL_write("</html>")
end

Along with this temp file, you can check how the engine is working. Of course, it's a bit more complicated than that, but if you don't need it, the default engine will suffice.