--[[ Copyright (c) 2012-2015 Kaarle Ritvanen See LICENSE file for license details --]] local topology = require('aconf.model.root').topology local class = require('aconf.object').class local pth = require('aconf.path') local tostr = require('aconf.persistence.util').tostring local util = require('aconf.util') local contains = util.contains 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 append_key_pred(path, key, value) return append_pred(path, key.." = '"..value.."'") end local function key(mode) if mode == 'value' then return '.' end if mode and stringy.startswith(mode, 'child-value:') then return mode:sub(13, -1) end end local function is_selector(mode) return mode and mode ~= 'parent-value' end local function aug_top(path) path = copy(path) table.insert(path, 1, 'augeas') return topology(path) end local function conv_path(path) local res = '/files' if #path == 0 then return res, nil, {} end path = copy(path) local top = aug_top{} local mode, parent local keys = {} repeat local comp = path[1] if mode then if mode == 'enumerate' then assert(type(comp) == 'number') res = append_pred(res, comp) else local k = key(mode) assert(k and type(comp) == 'string' and comp:match('^[%w %_%-%.%#]+$')) res = append_key_pred(res, k, comp) table.insert(keys, k) end parent = nil else parent = res res = res..'/'..comp keys = {} end top = top and top(pth.escape(comp)) mode = top and top.mode table.remove(path, 1) until #path == 0 if mode == 'parent-value' then assert(parent) res = parent end return res, mode, keys 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 tbl = tpe == 'table' if tbl and top.subtype == 'model' then mode = nil end local leaf = not tbl and not mode if mode == 'parent-value' then assert(not tbl) leaf = true end local matches = self.aug:match(apath..(not leaf and not mode and '/*' or '')) if #matches == 0 and not is_selector(mode) then return end if is_selector(mode) and #path > 1 and not self:get( array_without_last(path), true ) then return end if not tpe and not mode then if #matches > 1 then leaf = false mode = 'enumerate' 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 contains(keys, name) then name = nil end elseif mode == 'enumerate' then name = i else local k = key(mode) assert(k) name = self.aug:get(child..(k == '.' and '' or '/'..k)) 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 = table.unpack(mod) local function insert(path, new) local apath, mode, keys = conv_path(path) 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 is_selector(pmode) then gc[pth.join('/', table.unpack(parent))] = false if pmode == 'enumerate' then local count = #self.aug:match(ppath) while count < name do insert(parent, true) count = count + 1 end return apath, keys end end local matches = self.aug:match(apath) local count = #matches if count > 0 and not new then return apath, keys end if key(pmode) then apath = key(pmode) and append_pred( ppath, 'count('..key(pmode)..') = 0' ) or append_key_pred(ppath, '.', '') matches = self.aug:match(apath) assert(#matches < 2) apath = matches[1] or insert(parent, true) self.aug:set(apath..'/'..key(pmode), 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 contains(keys, '.')) then self.aug:set(apath, value ~= nil and tostr(value) or nil) end util.setdefault(gc, pth.join('/', table.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('/', table.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