diff options
Diffstat (limited to 'acf/model')
-rw-r--r-- | acf/model/field.lua | 120 | ||||
-rw-r--r-- | acf/model/init.lua | 206 | ||||
-rw-r--r-- | acf/model/model.lua | 102 | ||||
-rw-r--r-- | acf/model/node.lua | 129 |
4 files changed, 557 insertions, 0 deletions
diff --git a/acf/model/field.lua b/acf/model/field.lua new file mode 100644 index 0000000..cfa14b9 --- /dev/null +++ b/acf/model/field.lua @@ -0,0 +1,120 @@ +--[[ +Copyright (c) 2012 Kaarle Ritvanen +See LICENSE file for license details +--]] + +module(..., package.seeall) + +local node = require('acf.model.node') + +local object = require('acf.object') +local class = object.class +local super = object.super + +local map = require('acf.util').map + + +local function contains(list, value) + for k, v in ipairs(list) do if v == value then return true end end + return false +end + +local function auto_ui_name(name) + if not name then return end + return string.gsub(string.upper(string.sub(name, 1, 1))..string.sub(name, 2), + '_', ' ') +end + + +Field = class() + +function Field:init(params) + for k, v in pairs(params or {}) do if not self[k] then self[k] = v end end + + if self.choice and not self['ui-choice'] then + self['ui-choice'] = map(auto_ui_name, self.choice) + end + + if not self.widget then + self.widget = self.choice and 'combobox' or 'field' + end +end + +function Field:meta(txn, path, addr) + assert(self.dtype) + return { + name=self.name, + type=self.dtype, + required=self.required, + choice=self.choice, + description=self.description, + ['ui-name']=self['ui-name'] or auto_ui_name(self.name), + widget=self.widget, + ['ui-choice']=self['ui-choice'] + } +end + +function Field:load(txn, path, addr) + local value = txn:get(addr) + if value == nil then return self.default end + return value +end + +function Field:_validate(txn, path, value) + if self.required and value == nil then + error('Required value not set: '..path) + end + if self.choice and value ~= nil and not contains(self.choice, value) then + error('Invalid value for '..path..': '..value) + end + if value ~= nil then self:validate(txn, path, value) end + return value +end + +function Field:validate(txn, path, value) end + +function Field:save(txn, path, addr, value) + -- 2nd argument currenly not much used by backends + txn:set(addr, self.dtype, self:_validate(txn, path, value)) +end + +function Field:validate_saved(txn, path, addr) + self:save(txn, path, addr, self:load(txn, path, addr)) +end + + +TreeNode = class(Field) + +function TreeNode:save(txn, path, addr, value) + -- TODO hack, allow preserving old instance on parent update + if value == path then return end + + if object.isinstance(value, node.TreeNode) then + -- TODO clone if TreeNode has wrong path + assert(node.path(value) == path) + return + end + + txn:set(addr) + if value then + assert(type(value) == 'table') + txn:set(addr, 'table') + local new = self:load(txn, path, addr, true) + for k, v in pairs(value) do new[k] = v end + end +end + + +Model = class(TreeNode) + +function Model:init(params) + super(self, Model):init(params) + assert(self.model) + self.dtype = 'model' + self.widget = self.dtype +end + +function Model:load(txn, path, addr, create) + if not create and not txn:get(addr) then return end + return self.model(txn, path, addr) +end diff --git a/acf/model/init.lua b/acf/model/init.lua new file mode 100644 index 0000000..6934796 --- /dev/null +++ b/acf/model/init.lua @@ -0,0 +1,206 @@ +--[[ +Copyright (c) 2012 Kaarle Ritvanen +See LICENSE file for license details +--]] + +module(..., package.seeall) + +local fld = require('acf.model.field') +local Field = fld.Field + +local model = require('acf.model.model') +new = model.new +to_field = model.to_field + +node = require('acf.model.node') + +local object = require('acf.object') +local class = object.class +local super = object.super + +local pth = require('acf.path') +local map = require('acf.util').map + + +-- TODO object-specific actions + +-- TODO access control + + +local Primitive = class(Field) + +function Primitive:validate(txn, path, value) + local t = self.dtype + if type(value) ~= t then error('Not a '..t..': '..tostring(value)) end +end + + +String = class(Primitive) + +function String:init(params) + super(self, String):init(params) + self.dtype = 'string' +end + +function String:validate(txn, path, value) + if self['max-length'] and string.len(value) > self['max-length'] then + error('Maximum length exceeded: '..value) + end +end + +function String:meta(txn, path, addr) + local res = super(self, String):meta(txn, path, addr) + res['max-length'] = self['max-length'] + return res +end + + +Number = class(Primitive) + +function Number:init(params) + super(self, Number):init(params) + self.dtype = 'number' +end + +function Number:_validate(txn, path, value) + return tonumber(super(self, Number):_validate(txn, path, value)) +end + + +Integer = class(Number) + +function Integer:validate(txn, path, value) + if math.floor(value) ~= value then error('Not an integer: '..value) end +end + + +Boolean = class(Primitive) + +function Boolean:init(params) + super(self, Boolean):init(params) + self.dtype = 'boolean' + self.widget = self.dtype +end + + +Reference = class(Field) + +function Reference:init(params) + super(self, Reference):init(params) + self.dtype = 'reference' + if not self.scope then self.scope = '/' end +end + +function Reference:abs_scope(path) + return pth.to_absolute(self.scope, pth.parent(pth.parent(path))) +end + +function Reference:meta(txn, path, addr) + local res = super(self, Reference):meta(txn, path, addr) + res.scope = self:abs_scope(path) + + local base = txn:search(res.scope) + local objs = base and txn:get(getmetatable(base).addr) or {} + res.choice = map(function(p) return pth.join(res.scope, p) end, objs) + res['ui-choice'] = objs + + return res +end + +function Reference:follow(txn, path, value) + return txn:search(pth.join(self:abs_scope(path), value)) +end + +function Reference:load(txn, path, addr) + local ref = super(self, Reference):load(txn, path, addr) + return ref and self:follow(txn, path, ref) or nil +end + +function Reference:_validate(txn, path, value) + super(self, Reference):_validate(txn, path, value) + + if value == nil then return end + + if object.isinstance(value, node.TreeNode) then + value = getmetatable(value).path + end + if value and pth.is_absolute(value) then + local scope = self:abs_scope(path) + local prefix = scope..'/' + if not stringy.startswith(value, prefix) then + error('Reference out of scope ('..scope..')') + end + value = string.sub(value, string.len(prefix) + 1, -1) + end + + -- assume one-level ref for now + assert(not string.find(value, '/')) + if not self:follow(txn, path, value) then + error('Does not exist: '..path) + end + -- TODO check instance type + + return value +end + + +Model = fld.Model + + +Collection = class(fld.TreeNode) + +function Collection:init(params, ctype) + super(self, Collection):init(params) + assert(self.type) + self.ctype = ctype or node.Collection + self.dtype = 'collection' + self.widget = self.dtype +end + +function Collection:load(txn, path, addr) + -- automatically create missing collection (TODO: make this configurable?) + return self.ctype(txn, path, addr, to_field(self.type), self.required) +end + + +PrimitiveList = class(Collection) + +function PrimitiveList:init(params) + super(self, PrimitiveList):init(params, node.PrimitiveList) +end + + +-- experimental +Mixed = class(Collection) + +function Mixed:init() + super(self, Mixed):init({type=Mixed}, node.Mixed) +end + +function Mixed:load(txn, path, addr) + local value = Primitive.load(self, txn, path, addr) + if type(value) == 'table' then + return super(self, Mixed):load(txn, path, addr) + end + return value +end + +function Mixed:save(txn, path, addr, value) + if type(value) == 'table' then + super(self, Mixed):save(txn, path, addr, value) + else Primitive.save(self, txn, path, addr, value) end +end + + + +RootModel = new() + +function RootModel:init(txn) super(self, RootModel):init(txn, '/') end + +function register(name, path, field) + local field = to_field(field) + function field:compute(txn, pth, addr) + return self:load(txn, '/'..name, path) + end + RootModel[name] = field +end diff --git a/acf/model/model.lua b/acf/model/model.lua new file mode 100644 index 0000000..2efcb6f --- /dev/null +++ b/acf/model/model.lua @@ -0,0 +1,102 @@ +--[[ +Copyright (c) 2012 Kaarle Ritvanen +See LICENSE file for license details +--]] + +module(..., package.seeall) + +local fld = require('acf.model.field') +local Field = fld.Field + +local node = require('acf.model.node') + +local object = require('acf.object') +local class = object.class +local super = object.super +local isinstance = object.isinstance + +local util = require('acf.util') + + +function to_field(obj) + if object.issubclass(obj, Model) then return fld.Model{model=obj} end + return getmetatable(obj).class and obj or obj() +end + + +function new(base) + if not base then base = Model end + + local res = class(base) + res._fields = base == node.TreeNode and {} or util.copy(base._fields) + + local mt = util.copy(getmetatable(res)) + + function mt.__index(t, k) return base[k] end + + function mt.__newindex(t, k, v) + assert(v) + + local override = t[k] + if type(v) == 'table' then v = to_field(v) end + + rawset(t, k, v) + + if isinstance(v, Field) then + v.name = k + if not override then table.insert(t._fields, k) end + end + end + + setmetatable(res, mt) + + if isinstance(base, Model) then util.setdefaults(res, base) end + + return res +end + +Model = new(node.TreeNode) + +function Model:init(txn, path, addr) + super(self, Model):init(txn, path, addr) + + local mt = getmetatable(self) + + function mt.field(name) + local res = mt.class[name] + return isinstance(res, Field) and node.BoundField(self, res) or nil + end + + function mt.mmeta(name) return mt.field(name):meta() end + + mt.meta = {type='model', + fields=util.map(function(f) return mt.mmeta(f) end, + self._fields)} + + function mt.members() + return util.map(function(f) return f.name end, mt.meta.fields) + end + + function mt.__index(t, k) + local f = mt.field(k) + if f then + if f.compute then return f:compute() end + return f:load() + end + return mt.class[k] + end + + function mt.__newindex(t, k, v) + local f = mt.field(k) + if not f then error('Field named '..k..' does not exist') end + f:save(v) + txn.validate[mt.path] = function() self:validate() end + end +end + +function Model:validate() + local mt = getmetatable(self) + for _, name in ipairs(mt.members()) do + mt.field(name):validate_saved() + end +end diff --git a/acf/model/node.lua b/acf/model/node.lua new file mode 100644 index 0000000..39a0d92 --- /dev/null +++ b/acf/model/node.lua @@ -0,0 +1,129 @@ +--[[ +Copyright (c) 2012 Kaarle Ritvanen +See LICENSE file for license details +--]] + +module(..., package.seeall) + +local object = require('acf.object') +local class = object.class +local super = object.super + +local pth = require('acf.path') + + +BoundField = class() + +function BoundField:init(parent, field) + local pmt = getmetatable(parent) + local mt = {} + + function mt.__index(t, k) + local member = field[k] + if type(member) ~= 'function' then return member end + return function(self, ...) + local name + if field.name then name = field.name + else + name = arg[1] + table.remove(arg, 1) + end + return member(field, + pmt.txn, + pth.join(pmt.path, name), + pmt.addr and pth.join(pmt.addr, name), + unpack(arg)) + end + end + + setmetatable(self, mt) +end + + +TreeNode = class() + +function TreeNode:init(txn, path, addr) + local mt = getmetatable(self) + mt.txn = txn + mt.path = path + mt.addr = addr +end + +function TreeNode:search(path) + if #path == 0 then return self end + local next = path[1] + table.remove(path, 1) + return TreeNode.search(self[next], path) +end + + +Collection = class(TreeNode) + +function Collection:init(txn, path, addr, field, required) + super(self, Collection):init(txn, path, addr) + + if required then + txn.validate[path] = function() + if #txn:get(addr) == 0 then + error('Collection cannot be empty: '..path) + end + end + end + + self.init = nil + self.search = nil + + local mt = getmetatable(self) + + mt.field = BoundField(self, field) + mt.meta = {type='collection', members=mt.field:meta()} + function mt.mmeta(name) return mt.meta.members end + function mt.members() return txn:get(addr) or {} end + + function mt.__index(t, k) return mt.field:load(k) end + function mt.__newindex(t, k, v) mt.field:save(k, v) end +end + + +PrimitiveList = class(Collection) + +function PrimitiveList:init(txn, path, addr, field, required) + super(self, PrimitiveList):init(txn, path, addr, field, required) + + local mt = getmetatable(self) + local index = mt.__index + + function mt.__index(t, k) + if type(k) == 'number' then return index(t, k) end + + for i, j in ipairs(txn:get(addr) or {}) do + assert(i == tonumber(j)) + if mt.field:load(i) == k then return k end + end + error('Value does not exist: '..k) + end +end + + +-- experimental +Mixed = class(Collection) + +function Mixed:init(txn, path, addr, field, required) + super(self, Mixed):init(txn, path, addr, field, required) + -- TODO dynamic meta: list non-leaf children + getmetatable(self).meta = {type='mixed'} +end + + +local function meta_func(attr) + return function(node, ...) + local res = getmetatable(node)[attr] + if type(res) == 'function' then return res(unpack(arg)) end + return res + end +end + +members = meta_func('members') +meta = meta_func('meta') +mmeta = meta_func('mmeta') +path = meta_func('path') |