--[[ Copyright (c) 2012-2013 Kaarle Ritvanen See LICENSE file for license details --]] local topology = require('acf2.model.root').topology local class = require('acf2.object').class local pth = require('acf2.path') local tostr = require('acf2.persistence.util').tostring local util = require('acf2.util') local copy = util.copy local stringy = require('stringy') local function array_join(tbl, value) local res = copy(tbl) table.insert(res, value) return res end local function array_without_last(tbl) local res = copy(tbl) res[#res] = nil return res end local function basename(path) local res, pred = path:match('^.*/([#%w._-]+)([^/]*)$') assert(res) assert(res ~= '#') assert(pred == '' or pred:match('^%[.+%]$')) return res end local function append_pred(path, pred) return path..'['..pred..']' end local function key_mode(mode) return mode and stringy.startswith(mode, '@') end local function key(mode) assert(key_mode(mode)) return mode == '@' and '.' or mode:sub(2, -1) end local function append_key_pred(path, mode, value) return append_pred(path, key(mode).." = '"..value.."'") end local function conv_path(path) local res = '/files' if #path == 0 then return res, nil, {} end path = copy(path) local mode local keys = {} repeat local comp = path[1] if comp == '#' or key_mode(comp) then assert(not mode) mode = comp elseif not mode then res = res..'/'..comp keys = {} else if mode == '#' then assert(type(comp) == 'number') res = append_pred(res, comp) else assert(type(comp) == 'string' and comp:match('^[%w _-%.]+$')) res = append_key_pred(res, mode, comp) table.insert(keys, key(mode)) end mode = nil end table.remove(path, 1) until #path == 0 return res, mode, keys end local function aug_top(path) path = copy(path) table.insert(path, 1, 'augeas') return topology(path) end local backend = class() function backend:init() self.aug = require('augeas').init() end function backend:get(path, top) local apath, mode, keys = conv_path(path) local existence = top == true if existence or not top then top = aug_top(path) end local tpe = top and top.type local leaf = tpe ~= 'table' and not mode local matches = self.aug:match(apath..(not leaf and not mode and '/*' or '')) if #matches == 0 and not mode then return end if mode and #path > 1 and not self:get( array_without_last(array_without_last(path)), true ) then return end if not tpe and not mode then if #matches > 1 then leaf = false mode = '#' else local children = self.aug:match(apath..'/*') if #children > 0 then leaf = false matches = children end end end if leaf then assert(#matches == 1) return self.aug:get(apath) end if existence then return true end local names = {} for i, child in ipairs(matches) do local name if not mode then name = basename(child) name = tonumber(name) or name if util.contains(keys, name) then name = nil end elseif mode == '#' then name = i else name = self.aug:get(child..(mode == '@' and '' or '/'..key(mode))) end if name and self:get(array_join(path, name), true) then names[name] = true end end return util.keys(names) end function backend:set(mods) local gc = {} for _, mod in ipairs(mods) do local path, value = unpack(mod) local function insert(path, new) local apath, mode, keys = conv_path(path) if mode then path[#path] = nil end if #path == 0 then return apath, keys end local name = path[#path] local parent = array_without_last(path) local ppath, pmode = conv_path(parent) if pmode then gc[pth.join(unpack(array_without_last(parent)))] = false end if pmode == '#' then local count = #self.aug:match(ppath) while count < name do insert(parent, true) count = count + 1 end return apath, keys end local matches = self.aug:match(apath) local count = #matches if count > 0 and not new then return apath, keys end if key_mode(pmode) then apath = pmode == '@' and append_key_pred( ppath, '@', '' ) or append_pred(ppath, 'count('..key(pmode)..') = 0') matches = self.aug:match(apath) assert(#matches < 2) apath = matches[1] or insert(parent, true) local key = key(pmode) self.aug:set(apath..'/'..key, name) return apath, keys end if #matches == 0 then matches = self.aug:match(ppath..'/*') local function order(path) local top = aug_top(path) return top and top.order end local ord = order(path) for _, sibling in ipairs(matches) do local sord = order(array_join(parent, basename(sibling))) if sord and sord > ord then self.aug:insert(sibling, name, true) return apath, keys end end end if #matches == 0 then if new then self.aug:set(apath, nil) end return apath, keys end self.aug:insert(matches[#matches], name) return append_pred(apath, count + 1), keys end local apath, keys = insert(path) local is_table = type(value) == 'table' if not (is_table or util.contains(keys, '.')) then self.aug:set(apath, value ~= nil and tostr(value) or nil) end util.setdefault(gc, pth.join(unpack(path)), true) end for path, _ in pairs(gc) do local p = pth.split(path) while #p > 0 do local value = self:get(p) if ( type(value) == 'string' and value ~= '' ) or ( type(value) == 'table' and #value > 0 ) then break end if gc[pth.join(unpack(p))] ~= false then self.aug:rm(conv_path(p)) end p[#p] = nil end end if self.aug:save() ~= 0 then print('Augeas save failed') for _, ep in ipairs(self.aug:match('/augeas//error')) do print(ep, self.aug:get(ep)) end assert(false) end end return backend