gluon-firmware/package/gluon-web/luasrc/usr/lib/lua/gluon/web/model.lua

498 lines
8.7 KiB
Lua

-- Copyright 2008 Steven Barth <steven@midlink.org>
-- Copyright 2017 Matthias Schiffer <mschiffer@universe-factory.net>
-- Licensed to the public under the Apache License 2.0.
module("gluon.web.model", package.seeall)
local util = require "gluon.web.util"
local fs = require "nixio.fs"
local datatypes = require "gluon.web.model.datatypes"
local dispatcher = require "gluon.web.dispatcher"
local class = util.class
local instanceof = util.instanceof
FORM_NODATA = 0
FORM_VALID = 1
FORM_INVALID = -1
-- Loads a model from given file, creating an environment and returns it
function load(name, renderer, pkg)
local modeldir = util.libpath() .. "/model/"
if not fs.access(modeldir..name..".lua") then
error("Model '" .. name .. "' not found!")
end
local func = assert(loadfile(modeldir..name..".lua"))
local i18n = setmetatable({
i18n = renderer.i18n
}, {
__index = renderer.i18n(pkg)
})
setfenv(func, setmetatable({}, {__index =
function(tbl, key)
return _M[key] or i18n[key] or _G[key]
end
}))
local models = { func() }
for k, model in ipairs(models) do
if not instanceof(model, Node) then
error("model definition returned an invalid model object")
end
model.index = k
end
return models
end
local function parse_datatype(code)
local match, arg, arg2
match, arg, arg2 = code:match('^([^%(]+)%(([^,]+),([^%)]+)%)$')
if match then
return datatypes[match], {arg, arg2}
end
match, arg = code:match('^([^%(]+)%(([^%)]+)%)$')
if match then
return datatypes[match], {arg}
end
return datatypes[code], {}
end
local function verify_datatype(dt, value)
if dt then
local c, args = parse_datatype(dt)
assert(c, "Invalid datatype")
return c(value, unpack(args))
end
return true
end
Node = class()
function Node:__init__(title, description, name)
self.children = {}
self.title = title or ""
self.description = description or ""
self.name = name
self.index = nil
self.parent = nil
self.package = 'gluon-web'
end
function Node:append(obj)
table.insert(self.children, obj)
obj.index = #self.children
obj.parent = self
end
function Node:id_suffix()
return self.name or (self.index and tostring(self.index)) or '_'
end
function Node:id()
local prefix = self.parent and self.parent:id() or "id"
return prefix.."."..self:id_suffix()
end
function Node:parse(http)
for _, child in ipairs(self.children) do
child:parse(http)
end
end
function Node:render(renderer, scope)
if self.template then
local env = setmetatable({
self = self,
id = self:id(),
scope = scope,
}, {__index = scope})
renderer.render(self.template, env, self.package)
end
end
function Node:render_children(renderer, scope)
for _, node in ipairs(self.children) do
node:render(renderer, scope)
end
end
function Node:resolve_depends()
local updated = false
for _, node in ipairs(self.children) do
update = updated or node:resolve_depends()
end
return updated
end
function Node:handle()
for _, node in ipairs(self.children) do
node:handle()
end
end
Template = class(Node)
function Template:__init__(template)
Node.__init__(self)
self.template = template
end
Form = class(Node)
function Form:__init__(...)
Node.__init__(self, ...)
self.template = "model/form"
end
function Form:submitstate(http)
return http:getenv("REQUEST_METHOD") == "POST" and http:formvalue(self:id()) ~= nil
end
function Form:parse(http)
if not self:submitstate(http) then
self.state = FORM_NODATA
return
end
Node.parse(self, http)
while self:resolve_depends() do end
for _, s in ipairs(self.children) do
for _, v in ipairs(s.children) do
if v.state == FORM_INVALID then
self.state = FORM_INVALID
return
end
end
end
self.state = FORM_VALID
end
function Form:handle()
if self.state == FORM_VALID then
Node.handle(self)
self:write()
end
end
function Form:write()
end
function Form:section(t, ...)
assert(instanceof(t, Section), "class must be a descendent of Section")
local obj = t(...)
self:append(obj)
return obj
end
Section = class(Node)
function Section:__init__(...)
Node.__init__(self, ...)
self.fields = {}
self.template = "model/section"
end
function Section:option(t, option, title, description, ...)
assert(instanceof(t, AbstractValue), "class must be a descendant of AbstractValue")
local obj = t(title, description, option, ...)
self:append(obj)
self.fields[option] = obj
return obj
end
AbstractValue = class(Node)
function AbstractValue:__init__(option, ...)
Node.__init__(self, option, ...)
self.deps = {}
self.default = nil
self.size = nil
self.optional = false
self.template = "model/valuewrapper"
self.state = FORM_NODATA
end
function AbstractValue:depends(field, value)
local deps
if instanceof(field, Node) then
deps = { [field] = value }
else
deps = field
end
table.insert(self.deps, deps)
end
function AbstractValue:deplist(section, deplist)
local deps = {}
for _, d in ipairs(deplist or self.deps) do
local a = {}
for k, v in pairs(d) do
a[k:id()] = v
end
table.insert(deps, a)
end
if next(deps) then
return deps
end
end
function AbstractValue:defaultvalue()
return self.default
end
function AbstractValue:formvalue(http)
return http:formvalue(self:id())
end
function AbstractValue:cfgvalue()
if self.state == FORM_NODATA then
return self:defaultvalue()
else
return self.data
end
end
function AbstractValue:add_error(type, msg)
self.error = msg or type
if type == "invalid" then
self.tag_invalid = true
elseif type == "missing" then
self.tag_missing = true
end
self.state = FORM_INVALID
end
function AbstractValue:reset()
self.error = nil
self.tag_invalid = nil
self.tag_missing = nil
self.data = nil
self.state = FORM_NODATA
end
function AbstractValue:parse(http)
self.data = self:formvalue(http)
local ok, err = self:validate()
if not ok then
if type(self.data) ~= "string" or #self.data > 0 then
self:add_error("invalid", err)
else
self:add_error("missing", err)
end
return
end
self.state = FORM_VALID
end
function AbstractValue:resolve_depends()
if self.state == FORM_NODATA or #self.deps == 0 then
return false
end
for _, d in ipairs(self.deps) do
local valid = true
for k, v in pairs(d) do
if k.state ~= FORM_VALID or k.data ~= v then
valid = false
break
end
end
if valid then return false end
end
self:reset()
return true
end
function AbstractValue:validate()
if self.data and verify_datatype(self.datatype, self.data) then
return true
end
if type(self.data) == "string" and #self.data == 0 then
self.data = nil
end
if self.data == nil then
return self.optional
end
return false
end
function AbstractValue:handle()
if self.state == FORM_VALID then
self:write(self.data)
end
end
function AbstractValue:write(value)
end
Value = class(AbstractValue)
function Value:__init__(...)
AbstractValue.__init__(self, ...)
self.subtemplate = "model/value"
end
Flag = class(AbstractValue)
function Flag:__init__(...)
AbstractValue.__init__(self, ...)
self.subtemplate = "model/fvalue"
self.default = false
end
function Flag:formvalue(http)
return http:formvalue(self:id()) ~= nil
end
function Flag:validate()
return true
end
ListValue = class(AbstractValue)
function ListValue:__init__(...)
AbstractValue.__init__(self, ...)
self.subtemplate = "model/lvalue"
self.size = 1
self.widget = "select"
self.keys = {}
self.entry_list = {}
end
function ListValue:value(key, val, ...)
key = tostring(key)
if self.keys[key] then
return
end
self.keys[key] = true
val = val or key
table.insert(self.entry_list, {
key = key,
value = tostring(val),
deps = {...},
})
end
function ListValue:entries()
local ret = {unpack(self.entry_list)}
if self:cfgvalue() == nil or self.optional then
table.insert(ret, 1, {
key = '',
value = '',
deps = {},
})
end
return ret
end
function ListValue:validate()
if self.keys[self.data] then
return true
end
if type(self.data) == "string" and #self.data == 0 then
self.data = nil
end
if self.data == nil then
return self.optional
end
return false
end
DynamicList = class(AbstractValue)
function DynamicList:__init__(...)
AbstractValue.__init__(self, ...)
self.subtemplate = "model/dynlist"
end
function DynamicList:defaultvalue()
local value = self.default
if type(value) == "table" then
return value
else
return { value }
end
end
function DynamicList:formvalue(http)
return http:formvaluetable(self:id())
end
function DynamicList:validate()
if self.data == nil then
self.data = {}
end
if #self.data == 0 then
return self.optional
end
for _, v in ipairs(self.data) do
if not verify_datatype(self.datatype, v) then
return false
end
end
return true
end
TextValue = class(AbstractValue)
function TextValue:__init__(...)
AbstractValue.__init__(self, ...)
self.subtemplate = "model/tvalue"
end