--[[ Copyright (c) 2012-2015 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 subclass of -- @{Field}, an instance of such, or another **<Model>**. -- @function new -- @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. -- @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 Model: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. An instance of @{node.TreeNode} represents a non-leaf -- object in the data model, in the context of a specific -- transaction. Model objects themselves are a special case of such -- objects. 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