revisionator.lua

--- The revisionator provides a standardized way of migrating configurations between revisions
-- for instance, it will track what the currently applied revision number is, and when you tell
-- tell it to migrate, it will apply every individual migration between the currently applied
-- revision and the latest/current revision. This should allow for more seamlessly moving from
-- an older version of a package to a new one.
-- @classmod revisionator
-- @author Damian Monogue <demonnic@gmail.com>
-- @copyright 2023
-- @license MIT, see https://raw.githubusercontent.com/demonnic/MDK/main/src/scripts/LICENSE.lua
local revisionator = {
  name = "Revisionator",
  patches = {},
}
revisionator.__index = revisionator
local dataDir = getMudletHomeDir() .. "/revisionator"
revisionator.dataDir = dataDir
if not io.exists(dataDir) then
  local ok,err = lfs.mkdir(dataDir)
  if not ok then
    printDebug(f"Error creating the directory for storing applied revisions: {err}", true)
  end
end

--- Creates a new revisionator
-- @tparam table options the options to create the revisionator with.
-- <table class="tg">
-- <thead>
--   <tr>
--     <th>option name</th>
--     <th>description</th>
--     <th>default</th>
--   </tr>
-- </thead>
-- <tbody>
--   <tr>
--     <td class="tg-1">name</td>
--     <td class="tg-1">The name of the revisionator. This is absolutely required, as the name is used for tracking the currently applied patch level</td>
--     <td class="tg-1">raises an error if not provided</td>
--   </tr>
--   <tr>
--     <td class="tg-2">patches</td>
--     <td class="tg-2">A table of patch functions. It is traversed using ipairs, so must be in the form of {function1, function2, function3} etc. If you do not provide it, you can add the patches by calling :addPatch for each patch in order.</td>
--     <td class="tg-2">{}</td>
--   </tr>
--</tbody>
--</table>
function revisionator:new(options)
  options = options or {}
  local optionsType = type(options)
  if optionsType ~= "table" then
    printError(f"revisionator:new bad argument #1 type, options as table expected, got {optionsType}", true, true)
  end
  if not options.name then
    printError("revisionator:new(options) options must include a 'name' key as this is used as part of tracking the applied patch level.", true, true)
  end
  local me = table.deepcopy(options)
  setmetatable(me, self)
  return me
end

--- Get the currently applied revision from file
--- @treturn[1] number the revision number currently applied, or 0 if it can't read a current version
--- @treturn[2] nil nil
--- @treturn[2] string error message
function revisionator:getAppliedPatch()
  local fileName = f"{self.dataDir}/{self.name}.txt"
  debugc(fileName)
  local revision = 0
  if io.exists(fileName) then
    local file = io.open(fileName, "r")
    local fileContents = file:read("*a")
    file:close()
    local revNumber = tonumber(fileContents)
    if revNumber then
      revision = revNumber
    else
      return nil, f"Error while attempting to read current patch version from file: {fileName}\nThe contents of the file are {fileContents} and it was unable to be converted to a revision number"
    end
  end
  return revision
end

--- go through all the patches in order and apply any which are still necessary
--- @treturn boolean true if it successfully applied patches, false if it was already at the latest patch level
--- @error error message
function revisionator:migrate()
  local applied,err = self:getAppliedPatch()
  if not applied then
    printError(err, true, true)
  end
  local patches = self.patches
  if applied >= #patches then
    return false
  end
  for revision, patch in ipairs(patches) do
    if applied < revision then
      local ok, err = pcall(patch)
      if not ok then
        self:setAppliedPatch(revision - 1)
        return nil, f"Error while running patch #{revision}: {err}"
      end
    end
  end
  self:setAppliedPatch(#patches)
  return true
end

--- add a patch to the table of patches
--- @tparam function  func the function to run as the patch
--- @number[opt] position which patch to insert it as? If not supplied, inserts it as the last patch. Which is usually what you want.
function revisionator:addPatch(func, position)
  if position then
    table.insert(self.patches, position, func)
  else
    table.insert(self.patches, func)
  end
end

--- Remove a patch from the table of patches
--- this is primarily used for testing
--- @local
--- @number[opt] patchNumber the patch number to remove. Will remove the last item if not provided.
function revisionator:removePatch(patchNumber)
  table.remove(self.patches, patchNumber)
end

--- set the currently applied patch number
-- only directly called for testing
--- @local
--- @number patchNumber the patch number to set as the currently applied patch
function revisionator:setAppliedPatch(patchNumber)
  local fileName = f"{self.dataDir}/{self.name}.txt"
  local revFile, err = io.open(fileName, "w+")
  if not revFile then
    printError(err, true, true)
  end
  revFile:write(patchNumber)
  revFile:close()
end

return revisionator
generated by LDoc 1.5.0 Last updated 2023-05-29 18:41:27