ACF mvc.lua example: Difference between revisions
RuthHughes (talk | contribs) (minor updates) |
m (Reverted edits by RuthHughes (talk) to last revision by Ttrask) |
||
Line 177: | Line 177: | ||
You have two options for creating an MVC based application of your own. | You have two options for creating an MVC based application of your own. | ||
1. Use the ''dispatch'' function. This is the method used by the web interface and the acf-cli | 1. Use the ''dispatch'' function. This is the method used by the web interface and the acf-cli application. The action to be dispatched is passed in a string format ''/prefix/controller/action'' and the input values are passed in as a clientinfo table. Output is written to stdout. | ||
2. Use the ''new'' function to load the desired controller and then directly call the controller actions. For handling forms and user input, the application will call the action once to retrieve the form cfe, then fill in the desired input values into the cfe and call the action again to submit the form and perform the desired action. The MVC application is responsible for any user interaction and display of the results. | |||
=== Use the Dispatch method === | |||
Todo | |||
=== Use the New method === | |||
Todo | |||
{{Obsolete}} | |||
4. Create a dispatch wrapper program, named '''helloworld.lua''' in the current directory: | |||
-- Simple CLI based mvc application | |||
-- this is to get around having to store | |||
-- the config file in /etc/helloworld/helloworld.conf | |||
ENV={} | |||
ENV.HOME="." | |||
-- load the module | |||
require("mvc") | |||
-- create an new "mvc object" | |||
MVC=mvc:new() | |||
-- load the config file so we can find the appdir | |||
MVC:read_config("helloworld") | |||
-- create an application container | |||
APP=MVC:new("app") | |||
-- dispatch the request | |||
APP.clientdata.hostname=arg[2] | |||
APP:dispatch( "", "hostname", (arg[1] or "")) | |||
-- destroy the mvc objects | |||
APP:destroy() | |||
MVC:destroy() | |||
This application loads the "mvc.lua" framework, creates an mvc "object" named "MVC", then reads the helloworld.conf file to find out where the app dir is (helloworld/app/). It then loads the '''app-controller.lua''' into a new "application level" object named '''APP'''. Finally, it sets the clientdata and dispatches the hostname-controller/model pair. | |||
5. Test the application: | |||
# hostname | |||
Alpine | |||
# lua helloworld.lua no-such-function foo | |||
The following unhandled application error occured: | |||
controller: "hostname" does not have a "no-such-function" action. | |||
# hostname | |||
Alpine | |||
# hostname | |||
Alpine | |||
# lua helloworld.lua update Alline | |||
Your controller and application did not specify a view resolver. | |||
The MVC framework has no view available. sorry. | |||
# hostname | |||
Alline | |||
Note in the second case the hostname ''was'' changed, although the application does not know how to report success. | |||
== Create a view resolver and view formatter == | |||
The view resolver is a function that returns a function that processes the view. The returned function receives input from the controller and generates the output to be displayed. | |||
We will build a very simple view resolver and view processor for our application. Add this to the end of '''helloworld/app/hostname-controller.lua''' | |||
local private = {} | |||
private.view = function (f) | |||
if (f.msg) then | |||
print( (f.value or "") .. " is not a valid hostname ") | |||
else | |||
print ("Hostname is currently " .. f.value ) | |||
end | |||
end | |||
view_resolver = function (self) | |||
return private.view | |||
end | |||
[[ | |||
Now we can test: | |||
# lua helloworld.lua update "1 2 3" | |||
1 2 3 is not a valid hostname | |||
# lua helloworld.lua update | |||
is not a valid hostname | |||
# lua helloworld.lua update Alpine | |||
Hostname is currently Alpine | |||
But we have two problems: | |||
1. We now have to make a view resolver and view function for every controller. If we add a ''date'' setting controller, we'll have to make a view resolver and view function for it, and so on. | |||
2. Perhaps more importantly, view_resolver is now an "action" in our appliction. Recall that invalid actions are captured, but try this: | |||
# lua helloworld.lua no_such_action Alpine | |||
The following unhandled application error occured: | |||
controller: "hostname" does not have a "no_such_action" action. | |||
# lua helloworld.lua view_resolver Alpine | |||
The following unhandled application error occured: | |||
./helloworld/app/hostname-controller.lua:27: attempt to index local 'f' (a function value) | |||
stack traceback: | |||
./helloworld/app/hostname-controller.lua:27: in function 'viewfunc' | |||
./mvc.lua:139: in function <./mvc.lua:90> | |||
[C]: in function 'xpcall' | |||
./mvc.lua:90: in function 'dispatch' | |||
helloworld.lua:23: in main chunk | |||
[C]: ? | |||
Because view_resolver is an action in the worker table, the '''mvc.lua''' runs it; but it returns a function, not a table, and causes an unhandled exception in the view. | |||
== Move the view resolver to the application level == | |||
The solution to both problems is to move the view resolver and the view function out of the controller's worker table, into the next higher level, in this case, the application's worker table: | |||
1. Delete the private.view and view_resolver functions from '''helloworld/app/hostname-controller.lua''' | |||
2. Add the following to '''helloworld/app/app-controller.lua''' | |||
local private = {} | |||
private.view = function ( controller, action, viewtable ) | |||
io.write(string.format("Controller: %s Action: %s\n", | |||
controller or "", action or "")) | |||
io.write ("Returned a table with the following values:\n") | |||
for k,v in pairs(viewtable) do | |||
io.write(string.format("%s\t%s\n", k, v)) | |||
end | |||
end | |||
view_resolver = function (self) | |||
return function (viewtable) | |||
return private.view (self.conf.controller, self.conf.action, viewtable) | |||
end | |||
end | |||
This creates a more "generic" view, but one that will work for any controller - not just '''hostname'''. | |||
Now things work as they should: | |||
# lua helloworld.lua update "one two" | |||
Controller: hostname Action: update | |||
Returned a table with the following values: | |||
value one two | |||
type string | |||
msg Hostname can contain alphanumerics only | |||
# lua helloworld.lua view_resolver "Alpine" | |||
The following unhandled application error occured: | |||
controller: "hostname" does not have a "view_resolver" action. | |||
= mvc load & exec special functions = | |||
The '''mvc.lua''' module has a provision for executing code on module load, prior to executing the controller's action, just after executing the controller's action, and on module unload. | |||
This is done with the mvc table in the controller. To demonstrate, let's add a few functions to '''helloworld/app/app-controller.lua''' | |||
mvc = {} | |||
mvc.on_load = function (self, parent) | |||
print ("This is the app controller's on_load function") | |||
end | |||
mvc.pre_exec = function (self) | |||
print ("This is the app controller's pre_exec function") | |||
end | |||
mvc.post_exec = function (self) | |||
print ("This is the app controller's post_exec function") | |||
end | |||
mvc.on_unload = function (self) | |||
print ("This is the app controller's on_unload function") | |||
end | |||
Now running our script shows when the functions get called: | |||
# lua helloworld.lua update "Alpine" | |||
This is the app controller's on_load function | |||
This is the app controller's pre_exec function | |||
This is the app controller's post_exec function | |||
Controller: hostname Action: update | |||
Returned a table with the following values: | |||
value Alpine | |||
type string | |||
This is the app controller's on_unload function | |||
We can add mvc functions to a specific controller, as well. Add this to '''helloworld/app/hostname-controller.lua''' | |||
mvc = {} | |||
mvc.on_load = function (self, parent) | |||
print ("This is the hostname controller's on_load function") | |||
end | |||
mvc.pre_exec = function (self) | |||
print ("This is the hostname controller's pre_exec function") | |||
end | |||
mvc.post_exec = function (self) | |||
print ("This is the hostname controller's post_exec function") | |||
end | |||
mvc.on_unload = function (self) | |||
print ("This is the hostname controller's on_unload function") | |||
end | |||
And this happens: | |||
# lua helloworld.lua update "Alpine" | |||
This is the app controller's on_load function | |||
This is the hostname controller's on_load function | |||
This is the hostname controller's pre_exec function | |||
This is the hostname controller's post_exec function | |||
This is the hostname controller's on_unload function | |||
Controller: hostname Action: update | |||
Returned a table with the following values: | |||
value Alpine | |||
type string | |||
This is the app controller's on_unload function | |||
Note that both the ''app'' and ''hostname'' '''on_load''' and '''on_unload''' functions were run, but only the ''hostname'' '''pre_exec''' and '''post_exec''' functions ran. This is because the pre and post exec functions are run as part of the "action", and the '''dispatch''' function looks in the lowest-level controller for the pre/post_exec function. Since ''hostname'' now defines those functions, it runs them. | |||
To run both the ''hostname'' and ''app'' pre_exec function, you must arrange for the ''hostname'' pre_exec function to call it's parent pre_exec: | |||
mvc = {} | |||
mvc.on_load = function (self, parent) | |||
print ("This is the hostname controller's on_load function") | |||
mvc.parent_pre_exec = parent.worker.mvc.pre_exec | |||
end | |||
mvc.pre_exec = function (self) | |||
mvc.parent_pre_exec (self) | |||
print ("This is the hostname controller's pre_exec function") | |||
end | |||
[[Category:ACF]] [[Category:Lua]] |
Revision as of 12:30, 7 March 2016
Set the hostname with mvc.lua
In this example we will create a simple hostname-setting command-line application using mvc.lua. Once the controller/model are built, you can use the same code to set the hostname via the web with a web-based application controller.
For this example, we will assume you have root access on the linux box you are running on (preferably an Alpine Linux box!)
Get the mvc.lua module
Get the mvc.lua module from the git repository.
wget http://git.alpinelinux.org/cgit/acf-core/plain/lua/mvc.lua
Create a model and controller
Create a file hostname-controller.lua, defining the functions that an "end user" could run. We will only create one action, edithostname, which will be used to read and update the hostname. Since the action makes changes to the system, it naturally takes the form of a 'form':
hostname-controller.lua
-- Controller for editing hostname local mymodule = {} mymodule.edithostname = function (self) return self.handle_form(self, self.model.get_hostname, self.model.set_hostname, self.clientdata, "Edit", "Edit Hostname", "Hostname Updated") end return mymodule
Create a file hostname-model.lua, defining the model functions to get and set the hostname. We return a cfe table for each function including the form with the one entry for hostname:
hostname-model.lua
-- Model functions for retrieving / setting the hostname local mymodule = {} -- Create a cfe defining the form for editing the hostname and containing the current value mymodule.get_hostname = function(self, clientdata) local retval = cfe({ type="group", value={}, label="Hostname" }) -- Warning - io.popen has security risks, never pass user data to io.popen local f = io.popen ("/bin/hostname") local n = f:read("*a") or "none" f:close() n=string.gsub(n, "\n$", "") retval.value.hostname = cfe({ value=n, label="Hostname" }) return retval end -- Set the hostname from the value contained in the cfe created by get_hostname mymodule.set_hostname = function(self, hostnameform, action) local success = true -- Check to make sure the name is valid if (hostnameform.value.hostname.value == "") then success = false hostnameform.value.hostname.errtxt = "Hostname must not be blank" elseif (#hostnameform.value.hostname.value > 16) then success = false hostnameform.value.hostname.errtxt = "Hostname must be 16 characters or less" elseif (string.find(hostnameform.value.hostname.value, "[^%w%_%-]")) then success = false hostnameform.value.hostname.errtxt = "Hostname can contain alphanumerics only" end -- If it is valid, set the hostname if ( success ) then local f = io.open("/etc/hostname", "w") if f then f:write(hostnameform.value.hostname.value .. "\n") f:close() end -- Warning - io.popen has security risks, never pass user data to io.popen f = io.popen ("/bin/hostname -F /etc/hostname") f:close() else hostnameform.errtxt = "Failed to set hostname" end return hostnameform end return mymodule
Optionally test the model code (without mvc.lua)
If you want, you can create a test.lua script to validate the model code works on its own:
test.lua
require("mvc") -- Needed for cfe function definition m=require("hostname-model") local form = m.get_hostname() form.value.hostname.value = arg[1] or "" form = m.set_hostname(nil, form) if form.errtxt then print("FAILED: "..form.value.hostname.errtxt or form.errtxt) end form = m.get_hostname() print(form.value.hostname.value)
You can then test this with:
#lua test.lua "Alpine" Alpine
#lua test.lua "Invalid Name" FAILED: Hostname can contain alphanumerics only Alpine
Add the package to the ACF framework
To make the model and controller work within the ACF mvc.lua framework, we must do several things.
1. Install the ACF core package:
apk add acf-core
Optionally, you can add the entire web-based ACF framework:
setup-acf
2. Modify the ACF configuration file to look in /etc/acf/app/ for additional packages. Edit the /etc/acf/acf.conf file to add the /etc/acf/app/ directory to the appdir comma-separated list:
appdir=/etc/acf/app/,/usr/share/acf/app/
3. Move the model and controller to the new package directory. We will call the package "test":
mkdir -p /etc/acf/app/test mv hostname-*.lua /etc/acf/app/test
4. Test the new package using the acf-cli application:
# acf-cli /test/hostname/edithostname result = {} result["label"] = "Edit Hostname" result["option"] = "Edit" result["type"] = "form" result["value"] = {} result["value"]["hostname"] = {} result["value"]["hostname"]["label"] = "Hostname" result["value"]["hostname"]["type"] = "text" result["value"]["hostname"]["value"] = "Alpine" # acf-cli /test/hostname/edithostname hostname=test submit=true result = {} result["descr"] = "Hostname Updated" result["label"] = "Edit Hostname" result["option"] = "Edit" result["type"] = "form" result["value"] = {} result["value"]["hostname"] = {} result["value"]["hostname"]["label"] = "Hostname" result["value"]["hostname"]["type"] = "text" result["value"]["hostname"]["value"] = "test"
Note that the first case read the existing hostname and the second case updated it. The output of the acf-cli application is a serialized version of the cfe form, which is good for testing but not too useful in real life.
Enable the package in the ACF web interface
1. Add the web-based ACF framework:
setup-acf
2. Configure all users to have access to the new hostname action. Edit "/etc/acf/app/test/hostname.roles" file and add GUEST permission for the edithostname action:
echo "GUEST=hostname/edithostname" > /etc/acf/app/test/hostname.roles
The new action should now be visible by browsing to https://IP-of-host/cgi-bin/acf/test/hostname/edithostname. Obviously you might want to reconsider providing GUEST access to this action, because this allows unauthenticated users to modify your hostname.
3. Add the new hostname action to the ACF menu. Edit "/etc/acf/app/test/hostname.menu" file and add a menu item:
echo "Test Hostname Edit edithostname" > /etc/acf/app/test/hostname.menu
You will need to log off from the ACF interface (or delete the session cookie) before the new menu item will be visible.
Make an MVC based application
You have two options for creating an MVC based application of your own.
1. Use the dispatch function. This is the method used by the web interface and the acf-cli application. The action to be dispatched is passed in a string format /prefix/controller/action and the input values are passed in as a clientinfo table. Output is written to stdout.
2. Use the new function to load the desired controller and then directly call the controller actions. For handling forms and user input, the application will call the action once to retrieve the form cfe, then fill in the desired input values into the cfe and call the action again to submit the form and perform the desired action. The MVC application is responsible for any user interaction and display of the results.
Use the Dispatch method
Todo
Use the New method
Todo
This material is obsolete ... Please feel free to help us make an up-to-date version. (Discuss) |
4. Create a dispatch wrapper program, named helloworld.lua in the current directory:
-- Simple CLI based mvc application -- this is to get around having to store -- the config file in /etc/helloworld/helloworld.conf ENV={} ENV.HOME="." -- load the module require("mvc") -- create an new "mvc object" MVC=mvc:new() -- load the config file so we can find the appdir MVC:read_config("helloworld") -- create an application container APP=MVC:new("app") -- dispatch the request APP.clientdata.hostname=arg[2] APP:dispatch( "", "hostname", (arg[1] or ""))
-- destroy the mvc objects APP:destroy() MVC:destroy()
This application loads the "mvc.lua" framework, creates an mvc "object" named "MVC", then reads the helloworld.conf file to find out where the app dir is (helloworld/app/). It then loads the app-controller.lua into a new "application level" object named APP. Finally, it sets the clientdata and dispatches the hostname-controller/model pair.
5. Test the application:
# hostname Alpine # lua helloworld.lua no-such-function foo The following unhandled application error occured: controller: "hostname" does not have a "no-such-function" action. # hostname Alpine
# hostname Alpine # lua helloworld.lua update Alline Your controller and application did not specify a view resolver. The MVC framework has no view available. sorry. # hostname Alline
Note in the second case the hostname was changed, although the application does not know how to report success.
Create a view resolver and view formatter
The view resolver is a function that returns a function that processes the view. The returned function receives input from the controller and generates the output to be displayed.
We will build a very simple view resolver and view processor for our application. Add this to the end of helloworld/app/hostname-controller.lua
local private = {} private.view = function (f) if (f.msg) then print( (f.value or "") .. " is not a valid hostname ") else print ("Hostname is currently " .. f.value ) end end view_resolver = function (self) return private.view end
Now we can test:
# lua helloworld.lua update "1 2 3" 1 2 3 is not a valid hostname # lua helloworld.lua update is not a valid hostname # lua helloworld.lua update Alpine Hostname is currently Alpine
But we have two problems:
1. We now have to make a view resolver and view function for every controller. If we add a date setting controller, we'll have to make a view resolver and view function for it, and so on.
2. Perhaps more importantly, view_resolver is now an "action" in our appliction. Recall that invalid actions are captured, but try this:
# lua helloworld.lua no_such_action Alpine The following unhandled application error occured: controller: "hostname" does not have a "no_such_action" action. # lua helloworld.lua view_resolver Alpine The following unhandled application error occured: ./helloworld/app/hostname-controller.lua:27: attempt to index local 'f' (a function value) stack traceback: ./helloworld/app/hostname-controller.lua:27: in function 'viewfunc' ./mvc.lua:139: in function <./mvc.lua:90> [C]: in function 'xpcall' ./mvc.lua:90: in function 'dispatch' helloworld.lua:23: in main chunk [C]: ?
Because view_resolver is an action in the worker table, the mvc.lua runs it; but it returns a function, not a table, and causes an unhandled exception in the view.
Move the view resolver to the application level
The solution to both problems is to move the view resolver and the view function out of the controller's worker table, into the next higher level, in this case, the application's worker table:
1. Delete the private.view and view_resolver functions from helloworld/app/hostname-controller.lua
2. Add the following to helloworld/app/app-controller.lua
local private = {} private.view = function ( controller, action, viewtable ) io.write(string.format("Controller: %s Action: %s\n", controller or "", action or "")) io.write ("Returned a table with the following values:\n")
for k,v in pairs(viewtable) do io.write(string.format("%s\t%s\n", k, v)) end end view_resolver = function (self) return function (viewtable) return private.view (self.conf.controller, self.conf.action, viewtable) end end
This creates a more "generic" view, but one that will work for any controller - not just hostname.
Now things work as they should:
# lua helloworld.lua update "one two" Controller: hostname Action: update Returned a table with the following values: value one two type string msg Hostname can contain alphanumerics only # lua helloworld.lua view_resolver "Alpine" The following unhandled application error occured: controller: "hostname" does not have a "view_resolver" action.
mvc load & exec special functions
The mvc.lua module has a provision for executing code on module load, prior to executing the controller's action, just after executing the controller's action, and on module unload.
This is done with the mvc table in the controller. To demonstrate, let's add a few functions to helloworld/app/app-controller.lua
mvc = {} mvc.on_load = function (self, parent) print ("This is the app controller's on_load function") end mvc.pre_exec = function (self) print ("This is the app controller's pre_exec function") end mvc.post_exec = function (self) print ("This is the app controller's post_exec function") end
mvc.on_unload = function (self) print ("This is the app controller's on_unload function") end
Now running our script shows when the functions get called:
# lua helloworld.lua update "Alpine" This is the app controller's on_load function This is the app controller's pre_exec function This is the app controller's post_exec function Controller: hostname Action: update Returned a table with the following values: value Alpine type string This is the app controller's on_unload function
We can add mvc functions to a specific controller, as well. Add this to helloworld/app/hostname-controller.lua
mvc = {} mvc.on_load = function (self, parent) print ("This is the hostname controller's on_load function") end mvc.pre_exec = function (self) print ("This is the hostname controller's pre_exec function") end mvc.post_exec = function (self) print ("This is the hostname controller's post_exec function") end mvc.on_unload = function (self) print ("This is the hostname controller's on_unload function") end
And this happens:
# lua helloworld.lua update "Alpine" This is the app controller's on_load function This is the hostname controller's on_load function This is the hostname controller's pre_exec function This is the hostname controller's post_exec function This is the hostname controller's on_unload function Controller: hostname Action: update Returned a table with the following values: value Alpine type string This is the app controller's on_unload function
Note that both the app and hostname on_load and on_unload functions were run, but only the hostname pre_exec and post_exec functions ran. This is because the pre and post exec functions are run as part of the "action", and the dispatch function looks in the lowest-level controller for the pre/post_exec function. Since hostname now defines those functions, it runs them.
To run both the hostname and app pre_exec function, you must arrange for the hostname pre_exec function to call it's parent pre_exec:
mvc = {} mvc.on_load = function (self, parent) print ("This is the hostname controller's on_load function") mvc.parent_pre_exec = parent.worker.mvc.pre_exec end mvc.pre_exec = function (self) mvc.parent_pre_exec (self) print ("This is the hostname controller's pre_exec function") end