ACF mvc.lua reference: Difference between revisions

From Alpine Linux
(explain the pathologicial inheritance model)
m (Reverted edits by RuthHughes (talk) to last revision by Ttrask)
 
(16 intermediate revisions by 6 users not shown)
Line 1: Line 1:
= The Model Viewer Controller (mvc.lua) Module =
== mvc.lua function reference ==


mvc.lua is a proposed framework for building acf, the alpine configuration frameworkThis page demonstrates how to build a very simple application using mvc.lua and haserl.  This is the "Haserl Light Rail" framework
This lua module provides the basics for creating an mvc applicationIt is patterned
loosely after the Ruby on Rails pattern - but much more simplistic.


== Architecture ==
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.


mvc.lua has a ''':new''' constructor that creates a mvc "object".  Through the use of metatable ._index methods, these objects are nested to construct the MVC based application:
=== new( self, modname ) ===


[[image:Acf-mvc.png|ACF's MVC architecture]] [[media:acf-mvc.dia|dia source]]
Returns 3 values: an ''mvc'' table and 2 booleans ( true or false if the ''modname''-controller and ''modname''-model.lua were loaded) 


Each of these objects are the same - they are built from the core mvc:new function, but with "lua-style" oop.  (They inherit some properties from the parent.)
The ''mvc'' table contains:


When created with the ''':new''' constructor, a table is returned with the following subtables.
{|
 
! table !! used for  !! comments !! .__index points to
 
|-
{|-
|conf
|worker
|configuration items
|The controller (A table of controller methods), as well as an optional initializer.
|only created if a ''conf'' table does not exist in a parent
|-
| n/a
|model
|-
|The model (A table of model methods)
|clientdata
|-
|data sent from the client
|conf
|only created if a ''clientdata'' table does not exist in a parent
|A table of configuration parameters - application name, config file, application directory, selected prefix, controller, action, etc.  '''Note''' this table is automatically created only if it does not exist in any parent.
| n/a
|-
|-
|clientdata
|worker
|A table for user supplied data.  '''Note''' this table is automatically created only if it does not exist in any parent.
|the "controller" methods
|-
|if '''modname''' is given, then '''''modname'''-controller.lua'' module is loaded into this table.  Otherwise, an empty table is returned.
|view_resolver
|'''self''' (parent mvc object)
|A function that finds the view, and returns it as a function
|-
|-
|worker.mvc
|exception_handler
|special methods run by the mvc dispatch function
|A function that gets called if an error occurs
|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)
|other local methods
|-
  |as needed
|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 '''worker''', so this table can
inherit values from the parent table.


The inheritance for these tables is shown in the diagram below.  The diagram should be read from the "bottom" up:
If the '''''modname'''-controller.lua'' contains a .mvc.on_load function, the function is run before ''new'' returns.
 
[[image:Acf-inheritance.png|ACF's inheritance]] [[media:acf-inheritance.dia|dia source]]
 
This may appear slightly less than intuitive, but follows the way most applications naturally "want" things to resolve.


The above diagram is created with this code:
The .__index metamethods mean that this code will set up inheritance as shown in the diagram:


   require("mvc")
   require("mvc")
   MVC=mvc:new()
   MVC=mvc:new()
     APP=MVC:new()
     APP=MVC:new()
Line 53: Line 56:
       subcontroller=controller:new()
       subcontroller=controller:new()


 
If you try to run '''subcontroller.model.somefunction()''', and it does not exist, the inheritance will look for somefunction() in ...
if you try to run subcontroller.worker.somefunction(), and it does not exist, the inheritance will look for somefunction() in ...


# subcontroller.worker
# subcontroller.worker
Line 67: Line 69:
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.
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.


== The framework is not an application ==
The calling code should be sure to call the ''destroy'' function when done with this object.
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
=== destroy ( self ) ===
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'''
Calls the ''mvc.on_unload'' function, if it exists, to close any resources opened by the object.


-- the hostname model functions
=== dispatch ( self, prefix, controller, action, clientdata ) ===
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'' 
The gateway for executing controller actions in a protected xpcall.


Your controller and application did not specify a view resolver.
# If no controller specified, use default prefix/controller
The MVC framework has no view available. sorry.
# Creates a '''self:new ( prefix .. controller)''' mvc object
# If no action specified, use controller default_action
''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.
# runs any existing ''worker.mvc.pre_exec'' function
# runs the ''worker.'''action'''''
# runs any existing ''worker.mvc.post_exec'' function
# gets the view function from ''view_resolver''
# executes the view with the results of the ''worker.'''action'''''
# and calls ''destroy'' to destroy the mvc object


== Step 4. Create an Application Controller ==
If an error occurs, an exception_handler function is run.  If possible, the exception handler of the new mvc object is run, otherwise ''self::exception_handler'' function handles the error. If an exception occurs after creating the new mvc object, ''mvc.on_unload'' is guaranteed to run, but ''mvc.post_exec'' is not.


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.
=== soft_require ( self, modname ) ===


* Create '''/var/lib/app/helloworld-controller.lua'''
Looks for '''''modname'''.lua'' in ''self.conf.appdir'' and returns the results of a ''require()''  If the '''modname''' does not exist, returns nil.


-- The APPLICATION worker table.  This gets loaded once for the application
This function allows modules to be loaded without generating an exception if they do not exist.
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'''
=== read_config ( self, modname ) ===


<? local view = ... ?>
Looks in various places for a '''''modname'''.conf'' file and parses its contents into the '''self.conf''' table.
Content-type: text/html
<html>
<body>
&lt;h1>Hello World!&lt;/h1>
&lt;p>The hostname is <?= view.hostname ?>&lt;/p>
</body>
</html>


* Use your browser to go to ''http://<hostname>/cgi-bin/helloworld/hostname/get'' 
=== parse_path_info ( string ) ===


  Hello World!
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.
The hostname is Alpine


=== find_view ( appdir, prefix, controller, action, viewtype ) ===


''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)
Returns a string with the view filename for this combination of prefix/controller/action/viewtype or nil if no view file exists.


=== create_helper_library ( self ) ===


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.
Returns a table of function pointers to be passed to each view.


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!
=== auto_view (viewtable, viewlibrary, pageinfo, session) ===


== Step 5. Get User Input ==
This functions is used as the view of last resort. If no view file is found, this function is called to display the CFE resulting from the invoked action. The following 'viewtype's are supported:


* Set the hostname with: ''http://<hostname>/cgi-bin/helloworld/hostname/set?hostname=Desert'' 
* html
* json
* stream
* serialized


The following unhandled application error occured:
=== view_resolver ( self ) ===
/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:
Returns a function pointer to display the view, a table of functions made available to the view function, a table containing values of interest to the view, and a table containing the session data. The last three return values are designed to be the last three parameters to the view function pointer.


*  Edit '''/var/lib/app/helloworld-controller.lua''' and add this to the init function:
=== soft_traceback ( self, message ) ===


        -- The application must populate the clientdata table
If called with no arguments, returns a ''debug.traceback'', otherwise returns "message". 
                self.clientdata = FORM
* Set the hostname with: ''http://<hostname>/cgi-bin/helloworld/hostname/set?hostname=Desert''


Hello World!
=== exception_handler ( self, message ) ===
The hostname is Desert


Prints an error message and then re-asserts the exception.  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.


''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.
=== cfe ( table ) ===


== Step 6. Move the Controller and Application ==
Returns a table with ''value'', ''type'', and ''label'' values.  If an input table is given in the call, those key/value pairs are added as well.
Guarantees the view will not get a "nil" on value, type, or label.
This function is added to the global (_G) environment so it is available to the entire 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.
=== logevent ( message ) ===


Logs the message to syslog.


* mv /etc/helloworld /etc/acf
=== handle_clientdata ( form, clientdata ) ===
* 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 ==
This is a helper function for controllers to implement forms. It parses the form CFE and applies any changes submitted by the user in clientdata.


To build an application, you must provide:
=== handle_form ( self, getFunction, setFunction, clientdata, option, label, descr ) ===
This is a helper function for controllers to implement forms. It calls the getFunction to get the form CFE. If the form is submitted, it will call handle_clientdata and the setFunction to submit the form. The form CFE is returned.


# The config file
[[Category:ACF]] [[Category:Lua]]
# 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

Latest revision as of 12:30, 7 March 2016

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.

new( self, modname )

Returns 3 values: an mvc table and 2 booleans ( true or false if the modname-controller and modname-model.lua were loaded)

The mvc table contains:

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 worker, 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 ...

  1. subcontroller.worker
  2. controller
  3. controller.worker
  4. APP
  5. APP.worker
  6. MVC
  7. 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.

The calling code should be sure to call the destroy function when done with this object.

destroy ( self )

Calls the mvc.on_unload function, if it exists, to close any resources opened by the object.

dispatch ( self, prefix, controller, action, clientdata )

The gateway for executing controller actions in a protected xpcall.

  1. If no controller specified, use default prefix/controller
  2. Creates a self:new ( prefix .. controller) mvc object
  3. If no action specified, use controller default_action
  4. runs any existing worker.mvc.pre_exec function
  5. runs the worker.action
  6. runs any existing worker.mvc.post_exec function
  7. gets the view function from view_resolver
  8. executes the view with the results of the worker.action
  9. and calls destroy to destroy the mvc object

If an error occurs, an exception_handler function is run. If possible, the exception handler of the new mvc object is run, otherwise self::exception_handler function handles the error. If an exception occurs after creating the new mvc object, mvc.on_unload is guaranteed to run, but mvc.post_exec is not.

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.

read_config ( self, modname )

Looks in various places for a modname.conf file and parses its contents into the self.conf table.

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.

find_view ( appdir, prefix, controller, action, viewtype )

Returns a string with the view filename for this combination of prefix/controller/action/viewtype or nil if no view file exists.

create_helper_library ( self )

Returns a table of function pointers to be passed to each view.

auto_view (viewtable, viewlibrary, pageinfo, session)

This functions is used as the view of last resort. If no view file is found, this function is called to display the CFE resulting from the invoked action. The following 'viewtype's are supported:

  • html
  • json
  • stream
  • serialized

view_resolver ( self )

Returns a function pointer to display the view, a table of functions made available to the view function, a table containing values of interest to the view, and a table containing the session data. The last three return values are designed to be the last three parameters to the view function pointer.

soft_traceback ( self, message )

If called with no arguments, returns a debug.traceback, otherwise returns "message".

exception_handler ( self, message )

Prints an error message and then re-asserts the exception. 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.

cfe ( table )

Returns a table with value, type, and label values. If an input table is given in the call, those key/value pairs are added as well. Guarantees the view will not get a "nil" on value, type, or label. This function is added to the global (_G) environment so it is available to the entire application.

logevent ( message )

Logs the message to syslog.

handle_clientdata ( form, clientdata )

This is a helper function for controllers to implement forms. It parses the form CFE and applies any changes submitted by the user in clientdata.

handle_form ( self, getFunction, setFunction, clientdata, option, label, descr )

This is a helper function for controllers to implement forms. It calls the getFunction to get the form CFE. If the form is submitted, it will call handle_clientdata and the setFunction to submit the form. The form CFE is returned.