ACF mvc.lua reference
mvc.lua function reference
This lua module provides the basics for creating an mvc application. It is patterned loosely after the Ruby on Rails pattern - but much more simplistic.
The general pattern is to use the mvc new function to create a set of tables, and then use 'new within those tables to create sub "objects". By using metatable .__index methods, function references flow up through the parent tables. So the application is structured something like this:
ACF's MVC architecture dia source
new( self, modname)
returns an "mvc" table with the following sub-tables:
table | used for | comments | .__index points to |
---|---|---|---|
conf | configuration items | only created if a conf table does not exist in a parent | n/a |
clientdata | data sent from the client | only created if a clientdata table does not exist in a parent | n/a |
worker | the "controller" methods | if modname is given, then modname-controller.lua module is loaded
into this table. Otherwise, an empty table is returned. |
self (parent mvc object) |
worker.mvc | special methods run by the mvc dispatch function | If the modname-controller.lua module does not initalize a .mvc table,
an empty one is created |
self.mvc (parent mvc object's mvc table) |
model | the "model" methods | if modname is given, then modname-model.lua module is loaded
into this table. Otherwise, an empty table is returned. |
worker (this mvc object's worker table) |
The returned table has a .__index method that points to self, so this table can inherit values from the parent table.
If the modname-controller.lua contains a .mvc.on_load function, the function is run before new returns.
The .__index metamethods mean that this code will set up inheritance as shown in the diagram:
require("mvc") MVC=mvc:new() APP=MVC:new() controller=APP:new() subcontroller=controller:new()
If you try to run subcontroller.model.somefunction(), and it does not exist, the inheritance will look for somefunction() in ...
- subcontroller.worker
- controller
- controller.worker
- APP
- APP.worker
- MVC
- MVC.worker
This allows, for instance, the application to set a default method that is available to all child controllers. The reason the model looks to its parent worker table first is that controller methods are usually in the worker table, and models do not normally inherit from each other.
dispatch (self, prefix, controller, action )
The gateway for executing controller actions in a protected xpcall.
- Creates a self:new ( prefix .. controller) mvc object
- runs any existing worker.mvc.pre_exec function
- runs the worker.action
- runs any existing worker.mvc.post_exec function
- sends the results of the worker.action to the view_resolver
- and executes the view
This function does not return. If an error occurs, the exception_handler function is run.
basename ( string, suffix )
Lua implementation of basename.1 Returns string with any leading directory components removed. If specified, also remove a trailing suffix.
dirname ( string )
Lua implementation of dirname.1 Returns string with its trailing /component removed.
exception_handler ( self, message )
Prints an error message. Called if the xpcall in dispatch has an error. This is the exception_handler of last resort. The application should provide a more robust exception handler.
parse_path_info ( string )
Returns 3 strings: a prefix, controller, and action. Given a string in the format of a URI or pathspec, returns the basename as the action, the last component of the dirname as the controller, and the rest as the prefix. Missing components are returned as empty strings.
read_config ( self, modname )
Looks in various places for a modname.conf file and parses its contents into the self.conf table.
soft_require ( self, modname )
Looks for modname.lua in self.conf.appdir and returns the results of a require() If the modname does not exist, returns nil.
This function allows modules to be loaded without generating an exception if they do not exist.
soft_traceback ( self, message )
If called with no arguments, returns a debug.traceback, otherwise returns "message".
view_resolver (self )
Returns a function that prints "you do not have a view resolver". The application should provide a more robust view_resolver.
The framework is not an application
mvc.lua provides everything to build a web-based mvc application, but it is not a full application. Normally, the application will call :dispatch(), which performs the following steps:
- Determine the application name
- Load the configuration file if found
- Parse the prefix, controller and action
- Create an application object
- Run the application's worker.init function (if found)
- Create a controller object
- Run the controller's worker.init function (if found)
- Run the contoller's worker.[action] method
- Run the view, based on the controller's view_resolver function
The application must supply a view_resolver function. It is assumed the worker.[action] method will return a table. This table is passed to the view_resolver and the view render functions. The format of the table is up to the application.
Building a Sample Application
For this demo, we will use alpine, busybox httpd, and haserl to create a simple "hostname" setting application. The Application name will be "helloworld", and it will have one controller, "hostname". The application code will reside at /var/lib/app.
Step 1 - Get the web interface going
- Edit /etc/inetd.conf and add the following line:
www stream tcp nowait root /bin/busybox httpd -h /var/lib/www
- kill -HUP $( pidof inetd )
- mkdir -p /var/lib/www/cgi-bin
- mkdir -p /etc/helloworld
- Use your browser to make sure that http://<hostname>/cgi-bin/helloword results in a 404 error.
- copy mvc.lua to /var/lib/www/cgi-bin 'or to /usr/local/lib/lua/5.1
- create /var/lib/www/cgi-bin/helloworld:
#!/usr/bin/haserl --shell=lua <? require("mvc") mvc.dispatch(mvc:new()) ?>
- chmod +x /var/lib/www/cgi-bin/helloworld
- Use your browser to go to http://<hostname>/cgi-bin/helloworld/hostname/get
If you see this, everything is working!
The following unhandled application error occured: ./mvc.lua:62: attempt to call field '?' (a nil value) stack traceback: ./mvc.lua:62: in function <./mvc.lua:52> [C]: in function 'xpcall' ./mvc.lua:52: in function 'dispatch' [string "..."]:6: in main chunk
What's happened: We created the basic application, and the mvc object ran, caught and reported the error.
Step 2. Configure the Application
- Edit /etc/helloworld/helloworld.conf and add:
# The helloworld configuration parameters appdir = /var/lib/app/
(The trailing / is important)
What's happened: We told the application where it will reside. The :new method looks in several places for a configuration file named with the appname (helloworld)
Step 3. Create the model and controller
- Create /var/lib/app/hostname-controller.lua
-- The "hostname" controller module( ... , package.seeall) -- Initialization goes here, if there were any. This controller has no specific -- initialization steps init = function (self, parent) end -- Public methods. Any of these methods can be called directly from the client -- <prefix>/hostname/get get = function (self) return ( { hostname = self.model:get() } ) end -- <prefix>/hostname/set set = function (self) return ( { hostname = self.model:set(self.clientdata.hostname) } ) end
- Create /var/lib/app/hostname-model.lua
-- the hostname model functions module( ... , package.seeall) -- The model does not have an initializer. Use the controller (worker) for that. -- Public methods. Use a local table for private methods get = function (self) local f = io.popen("/bin/hostname") local n = f:read("*a") or "unknown" f:close() return ( n ) end set = function (self, name) local f = io.open("/etc/hostname", "w") if f then f:write(name) f:close() end f = io.popen ("/bin/hostname -F /etc/hostname") f:close() return get(self) end
- Use your browser to go to http://<hostname>/cgi-bin/helloworld/hostname/get
Your controller and application did not specify a view resolver. The MVC framework has no view available. sorry.
What Happened: The mvc framework ran your controller (which ran your model code)!!! Success! Unfortunately, the framework doesn't know how to render the results, so we get the generic mvc:view_resolver results.
Step 4. Create an Application Controller
We could fix the problem by creating a view_resolver in hostname-controller.lua, but that would only work for that one controller. Instead, we will create a method in the application controller that will apply to all controllers.
- Create /var/lib/app/helloworld-controller.lua
-- The APPLICATION worker table. This gets loaded once for the application module( ... , package.seeall) -- A table for private methods local private = {} -- This function gets run when this module is loaded. Note that in this case, -- "self" refers to the object as a whole. In the other cases, "self" will -- refer to the "self.worker" table objects init = function (self, parent) -- do some final fixup on ourselves -- the framework doesn't provide a view_resolver, so we do self.view_resolver = private.view_resolver end -- Returns the function to render the view. We make it private so that nobody calls it as a method. private.view_resolver = function(self) local filename, file filename = self.conf.appdir .. self.conf.prefix .. self.conf.controller .. "-view.lsp" file = io.open(filename) if file then file:close() return haserl.loadfile(filename) else return function() end -- return a blank screen end end
- Create a /var/lib/app/hostname-view.lsp
<? local view = ... ?> Content-type: text/html <html> <body> <h1>Hello World!</h1> <p>The hostname is <?= view.hostname ?></p> </body> </html>
- Use your browser to go to http://<hostname>/cgi-bin/helloworld/hostname/get
Hello World! The hostname is Alpine
What Happened: All functions in a module are normally public methods. The init function in the Application controller is actually <sometable>.worker.init. Since we don't want the view_resolver to be a public method (e.g. http://hostname/cgi-bin/helloworld/hostname/view_resover) we put it in a private table. We then override the Application's view_resolver method with our new method. Note that the active view_resolver is a method of the object itself, not of the object's worker table, so we must set self.view_resolver. (The init function is the only function where self is the the object as a whole. All other functions receive self as object.worker)
The view resolver uses haserl's loadfile function to process the "lsp" style page. We make the function generic so that it will work with any controller. We then define a simple .lsp file.
When the application runs, it runs all the way through, looks for a hostname.view_resolver function, and when it can't find one, uses the application.view_resolver function instead. Life is good!
Step 5. Get User Input
- Set the hostname with: http://<hostname>/cgi-bin/helloworld/hostname/set?hostname=Desert
The following unhandled application error occured: /var/lib/app//hostname-model.lua:18: bad argument #1 to 'write' (string expected, got nil) stack traceback: [C]: in function 'write' /var/lib/app//hostname-model.lua:18: in function 'set' /var/lib/app//hostname-controller.lua:19: in function '?' ./mvc.lua:62: in function <./mvc.lua:52> [C]: in function 'xpcall' ./mvc.lua:52: in function 'dispatch' [string "..."]:6: in main chunk
The mvc framework does not know how to get user input. That is the application's responsibility. So let's edit the application controller and add clientdata:
- Edit /var/lib/app/helloworld-controller.lua and add this to the init function:
-- The application must populate the clientdata table self.clientdata = FORM
- Set the hostname with: http://<hostname>/cgi-bin/helloworld/hostname/set?hostname=Desert
Hello World! The hostname is Desert
What Happened: Since the application could be CLI, Web, or GUI, the framework makes no assumptions on how to get user input to the controller. So the Application controller must do that. In this case, we just accept FORM from haserl.
Step 6. Move the Controller and Application
This step is just to show that the framework is flexible. We're going to change the name of the application and where the controller is, at the same time.
- mv /etc/helloworld /etc/acf
- mv /etc/acf/helloworld.conf /etc/acf/acf.conf
- mv /var/lib/www/cgi-bin/helloworld /var/lib/www/cgi-bin/acf
- mkdir /var/lib/app/alpine-base
- mv /var/lib/app/hostname* /var/lib/app/alpine-base
- mv /var/lib/app/helloworld-controller.lua /var/lib/app/acf-controller.lua
- Go to http://<hostname>/cgi-bin/acf/alpine-base/hostname/get
Summary
To build an application, you must provide:
- The config file
- A <application>-controller.lua - with a view_resolver (and exception_handler, if desired)
- <controller>-controller.lua and optionally <controller>-model.lua
- A function to produce the view output