ACF mvc.lua example: Difference between revisions

From Alpine Linux
(Category:Lua)
(Update the controller and model to current standards)
Line 8: Line 8:
== Get the mvc.lua module ==  
== Get the mvc.lua module ==  


svn "export" the mvc.lua module. Export will grab a copy without all the extra hidden "version control" stuff
Get the mvc.lua module from the git repository.


  svn export svn://svn.alpinelinux.org/acf/core/trunk/www/cgi-bin/mvc.lua
  wget http://git.alpinelinux.org/cgit/acf-core/plain/lua/mvc.lua


== Create a model and controller ==
== Create a model and controller ==


Create a file '''hostname-model.lua''', defining the module functions to set and read the hostname.  We return a table for each function including the value, error message and type of the value ("String" in the case of the hostname).
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 for 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.update_hostname, self.clientdata, "Edit", "Edit Hostname", "Hostname Updated")
end                                 
return mymodule
 
Create a file '''hostname-model.lua''', defining the model functions to get and update the hostname.  We return a cfe table for each function including the form with the one entry for hostname:


=== hostname-model.lua ===
=== hostname-model.lua ===
  -- Model functions for retrieving / setting the hostname
  -- Model functions for retrieving / setting the hostname
  module ( ..., package.seeall )
  local mymodule = {}
   
   
  -- All functions return a table with
  -- Create a cfe defining the form for editing the hostname and containing the current value
-- A value, the type of the value, and a message if there was an error
mymodule.get_hostname = function(self, clientdata)
        local retval = {}
   
   
  local hosttype={ type="string" }
        local f = io.popen ("/bin/hostname")
        local n = f:read("*a") or "none"
        f:close()
        n=string.gsub(n, "\n$", "")
   
        retval.hostname = cfe({ value=n, label="Hostname" })
        return cfe({ type="group", value=retval, label="Hostname" })
end
   
   
  update= function ( name )
  -- Update the hostname from the value contained in the cfe created by get_hostname
         -- Check to make sure the name is valid
mymodule.update_hostname = function(self, hostnameform, action)
         local success = true
   
   
         if (name == nil) then
        -- Check to make sure the name is valid
                 hosttype.msg = "Hostname cannot be nil"
         if (hostnameform.value.hostname.value == "") then
         elseif (#name > 16) then
                 success = false
                 hosttype.msg = "Hostname must be less than 16 chars"
                hostnameform.value.hostname.errtxt = "Hostname must not be blank"
         elseif (string.find(name, "[^%w%_%-]")) then
         elseif (#hostnameform.value.hostname.value > 16) then
                 hosttype.msg = "Hostname can contain alphanumerics only"
                 success = false
                hostnameform.value.hostname.errtxt = "Hostname must be less than 16 chars"
         elseif (string.find(hostnameform.value.hostname.value, "[^%w%_%-]")) then
                 success = false
                hostnameform.value.hostname.errtxt = "Hostname can contain alphanumerics only"
         end
         end
   
   
         -- If it is, set the hostname
         -- If it is, set the hostname
         if (hosttype.msg == nil ) then
         if ( success ) then
                 local f = io.open("/etc/hostname", "w")
                 local f = io.open("/etc/hostname", "w")
                 if f then
                 if f then
                         f:write(name .. "\n")
                         f:write(hostnameform.value.hostname.value .. "\n")
                         f:close()
                         f:close()
                 end
                 end
                 f = io.popen ("/bin/hostname -F /etc/hostname")
                 f = io.popen ("/bin/hostname -F /etc/hostname")
                 f:close()
                 f:close()
                return read()
        -- Otherwise, return the error message
         else
         else
                 hosttype.value = name
                 hostnameform.errtxt = "Failed to update hostname"
                return hosttype
         end
         end
end
   
   
read= function ()
         return hostnameform
        local f = io.popen ("/bin/hostname")
        local n = f:read("*a") or "none"
        f:close()
        n=string.gsub(n, "\n$", "")
        hosttype.value = n
         return (hosttype)
  end
  end
Create a file '''hostname-controller.lua''', defining the functions that an "end user" could run.  We define '''C'''reate '''R'''ead '''U'''pdate '''D'''elete as standard actions:
=== hostname-controller.lua ===
-- hostname controller code
module ( ... , package.seeall )
create = function (self )     
        return self.model.update(self.clientdata.hostname)
end                                 
   
read = function (self)
        return self.model.read()
end                           
   
   
  update =  create
  return mymodule
   
delete = function (self )
        self.clientdata.hostname=""                     
        return self.worker:create()     
end


== Optionally test the model code (without mvc.lua)  ==
== Optionally test the model code (without mvc.lua)  ==
Line 95: Line 85:


=== test.lua ===
=== test.lua ===
  m=require("hostname-model")              
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.update_hostname(nil, form)
   
   
  print(m.update(arg[1]).msg)
  if form.errtxt then
  print(m.read().value)
        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:
You can then test this with:


  #lua test.lua "Alpine"
  #lua test.lua "Alpine"
  nil
   Alpine
   Alpine


  #lua test.lua "Invalid Name"
  #lua test.lua "Invalid Name"
   Hostname can contain alphanumerics only
   FAILED: Hostname can contain alphanumerics only
   Alpine
   Alpine



Revision as of 15:15, 30 September 2013

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 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 for 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.update_hostname, self.clientdata, "Edit", "Edit Hostname", "Hostname Updated")
end                                   

return mymodule

Create a file hostname-model.lua, defining the model functions to get and update 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 = {}

        local f = io.popen ("/bin/hostname")
        local n = f:read("*a") or "none"
        f:close()
        n=string.gsub(n, "\n$", "")

        retval.hostname = cfe({ value=n, label="Hostname" })

        return cfe({ type="group", value=retval, label="Hostname" })
end

-- Update the hostname from the value contained in the cfe created by get_hostname
mymodule.update_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 less than 16 chars"
        elseif (string.find(hostnameform.value.hostname.value, "[^%w%_%-]")) then
                success = false
                hostnameform.value.hostname.errtxt = "Hostname can contain alphanumerics only"
        end

        -- If it is, 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
                f = io.popen ("/bin/hostname -F /etc/hostname")
                f:close()
        else
                hostnameform.errtxt = "Failed to update 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.update_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

Make an MVC based application

To make the model and and controller work within the mvc.lua framework, we must do serveral things.

1. Create a configuration file. We'll call the application helloworld, so edit helloworld.conf and add:

appdir=helloworld/app/

2. Move the model and controller to the helloworld app directory:

mkdir -p helloworld/app
mv hostname-*.lua helloworld/app

3. Create an application level controller in the helloworld/app directory, named helloworld/app/app-controller.lua

module ( ..., package.seeall)
-- application specific functions will go here

Nothing else needs to go in this controller for now.

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