--[[
Copyright (c) 2012-2016 Kaarle Ritvanen
See LICENSE file for license details
--]]
--- @module aconf.model
local M = {}
local raise = require('aconf.error').raise
local fld = require('aconf.model.field')
local Field = fld.Field
local Member = fld.Member
local Model = fld.Model
local normalize_name = fld.normalize_name
local node = require('aconf.model.node')
local BoundMember = node.BoundMember
local object = require('aconf.object')
local class = object.class
local super = object.super
local isinstance = object.isinstance
local pth = require('aconf.path')
local util = require('aconf.util')
local copy = util.copy
local map = util.map
local update = util.update
function M.to_field(obj, params, cls)
if object.issubclass(obj, M.Model) then
return Model(update(params, {model=obj}))
end
local res = object.toinstance(obj, params)
assert(isinstance(res, cls or Field))
return res
end
M.Action = class(Member)
function M.Action:init(params)
super(self, M.Action):init(params)
if not self.func then error('Function not defined for action') end
if self.field then
assert(type(self.field) == 'table')
self.field = M.to_field(self.field)
self.field.addr = node.null_addr
end
getmetatable(self).__newindex = function(t, k, v)
assert(k == 'name')
rawset(t, k, v)
if t.field then t.field.name = v end
end
end
function M.Action:meta(context)
local res = super(self, M.Action):meta(context)
if self.field then res.arg = self.field:meta(context) end
return res
end
--- create a new model, representing a data model with a pre-defined
-- structure. The model's fields can be defined by assigning it
-- key–value pairs. The key will be the name of the field and
-- the value will determine its type. The value shall be a [field
-- class](#Overview_of_field_classes), an instance of field, or
-- another **<Model>**.
-- @param base (optional **<Model>**) base model
-- inherited by the new model.
-- @return **<Model>** new model
function M.new(base)
if not base then base = M.Model end
local res = class(base)
res._members = base == node.TreeNode and {} or copy(base._members)
local mt = copy(getmetatable(res))
function mt.__index(t, k) return base[k] end
function mt.__newindex(t, k, v)
assert(v)
k = normalize_name(k)
local override = t[k]
if type(v) == 'table' then v = M.to_field(v, nil, Member) end
rawset(t, k, v)
if isinstance(v, Member) then
v.name = k
if not override then table.insert(t._members, k) end
end
end
setmetatable(res, mt)
if isinstance(base, M.Model) then util.setdefaults(res, base) end
return res
end
--- Model objects. Each of these represents an actual model
-- object in the data model, in the context of a specific transaction.
-- These are instances of @{node.TreeNode} and the model class created
-- e.g. using @{new}.
-- @section Model
M.Model = M.new(node.TreeNode)
function M.Model:init(context)
super(self, M.Model):init(context, {dtype='model', editable=true})
local mt = getmetatable(self)
function mt.member(name, loose, tpe)
local m = mt.class[name]
if not tpe then tpe = Member end
if not isinstance(m, tpe) then m = nil end
if m == nil then
if loose then return end
raise(mt.path, 'Does not exist: '..name)
end
return BoundMember(self, name, m)
end
local function _members(tpe)
local res = {}
for _, name in ipairs(self._members) do
local m = mt.member(name, true, tpe)
if m then table.insert(res, m) end
end
return res
end
function mt.topology()
local res = {}
for _, f in ipairs(_members(Field)) do util.extend(res, f:topology()) end
return res
end
function mt.load(k, options)
local v = mt.class[k]
k = normalize_name(k)
if not v then v = mt.class[k] end
if isinstance(v, Field) then
return BoundMember(self, k, v):load(options)
end
assert(mt.txn)
if isinstance(v, M.Action) then
mt.check_permission(k)
local f = v.field and BoundMember(self, k, v.field)
if options.create then return f and f:load(options) end
return function(var)
if f then f:save(var)
elseif var ~= nil then
raise(
pth.join(mt.path, v.name),
'Action does not accept an input argument'
)
end
local res = v.func(mt.escalate, f and f:load())
if f then f:_save() end
return res
end
end
if options.private then return v end
end
if self.is_removable then
function mt.removable()
return mt.has_permission('delete') and self:is_removable()
end
end
function mt.removing_permitted() return mt.has_permission('modify') end
local value_removable = mt.value_removable
function mt.value_removable(v)
return type(v) == 'table' and value_removable(v)
end
if not mt.txn then return end
function mt.mmeta(name) return mt.member(name):meta() end
function mt.save(k, v)
k = normalize_name(k)
local field = mt.member(k, false, Field)
if v == nil and isinstance(field, fld.TreeNode) then
mt.check_removable(k)
else mt.check_permission('modify') end
return field:save(v)
end
local function tmeta(tpe)
return map(function(m) return m:meta() end, _members(tpe))
end
function mt.init_meta(meta)
update(
meta,
{
fields=tmeta(Field),
actions=util.filter(
function(a) return mt.has_permission(a.name) end,
tmeta(M.Action)
)
}
)
end
function mt.members()
return map(function(f) return f.name end, tmeta(Field))
end
function mt.validate()
for _, f in ipairs(_members(Field)) do
if self:match(f.condition or {}) then f:validate_saved()
elseif f:_editable() then f:_save() end
end
if self.validate then self:validate() end
end
if self.has_permission then
function mt._has_permission(permission)
return self:has_permission(mt.txn.user, permission)
end
end
local tload = getmetatable(mt.escalate).load
for _, f in ipairs(_members(Model)) do tload(f.name) end
end
--- fetch an object in the context of the model object's transaction.
-- @function fetch
-- @tparam string path path of the object to fetch. A path relative to
-- the model object's own path may be used.
-- @return (**primitive** or @{node.TreeNode}) fetched
-- object
function M.Model:fetch(path, create)
return getmetatable(self).fetch(path, create)
end
function M.Model:search(path) return getmetatable(self).search(path) end
function M.Model:match(filter)
local tload = getmetatable(getmetatable(self).escalate).load
for k, v in pairs(filter) do
if not util.contains(v, tload(k)) then return false end
end
return true
end
return M