diff --git a/config.ld b/config.ld new file mode 100644 index 0000000..e4341e1 --- /dev/null +++ b/config.ld @@ -0,0 +1,16 @@ +file = { + 'src', + exclude = {'src/test'} +} + +dir = 'docs' +format = 'markdown' + +readme = 'README.md' +title = "WiFibox firmware documentation" +project = "Wifibox firmware" +description = "This project provides the REST API which is part of the Doodle3D wifibox." + +alias('p', 'param') + +all = true diff --git a/doxify.sh b/doxify.sh index 5465787..06703eb 100755 --- a/doxify.sh +++ b/doxify.sh @@ -10,7 +10,16 @@ HTML_PATH=$WIFIBOX_BASE_DIR/docs SRC_DIR=$WIFIBOX_BASE_DIR/src FILESPEC=$WIFIBOX_BASE_DIR/src #replace by config.ld so we can also specify README.md? -$LDOC -d $HTML_PATH $FILESPEC -a -f markdown $@ +LUA_VERSION=`lua -v 2>&1 | awk -F" " '{print $2}'` + +echo $LUA_VERSION | grep -q "^5.2" +if [ $? -ne 0 ]; then + echo "Lua 5.2 is needed to run this script (as well as luarocks), you have $LUA_VERSION." + exit 1 +fi + +#$LDOC -d $HTML_PATH $FILESPEC -a -f markdown $@ +$LDOC . if [ $? -eq 127 ]; then echo "$0: It looks like the ldoc program could not be found, please configure the LDOC variable correctly and make sure ldoc is installed on your system." diff --git a/src/conf_defaults.lua b/src/conf_defaults.lua index fd4ebbb..bb10f01 100644 --- a/src/conf_defaults.lua +++ b/src/conf_defaults.lua @@ -1,34 +1,40 @@ ---[[-- - TODO: finish documentation - This file contains all valid configuration keys, their default values and optional constraints. - The table names are used as configuration key names, where underscores ('_') may be used to denote semi-categories. - The settings interface replaces periods ('.') by underscores so for instance 'network.ap.address' will - be translated to 'network_ap_address'. Multi-word names should be notated as camelCase. - Valid fields for the tables are: - - default: the default value (used when the key is not set in UCI config) - - type: used for basic type checking, one of bool, int, float or string - - description: A descriptive text usable by API clients - - min, max, regex: optional constraints (min and max constrain value for numbers, or length for strings) - - NOTE that the all-caps definitions will be changed into configuration keys, or moved to a different location -]]-- +--- +-- This file contains all valid configuration keys, their default values and optional constraints. +-- The table names are used as configuration key names, where underscores ('`_`') may be used to denote semi-categories. +-- The settings interface replaces periods ('`.`') by underscores so for instance `network.ap.address` will +-- be translated to `network_ap_address`. Multi-word names should be notated as camelCase. +-- +-- Valid fields for the tables are: +-- +-- - _default_: the default value (used when the key is not set in UCI config) +-- - _type_: used for basic type checking, one of bool, int, float or string +-- - _description_: A descriptive text usable by API clients +-- - _min_, _max_, _regex_: optional constraints (min and max constrain value for numbers, or length for strings) +-- - _isValid_: an optional function which should return true for valid values and false for invalid ones +-- +-- The configuration keys themselves document themselves rather well, hence they are not included in the generated documentation. +-- +-- NOTE: the all-caps definitions should be changed into configuration keys, or moved to a better location. local printer = require('util.printer') local log = require('util.logger') local utils = require('util.utils') local M = {} ---NOTE: pcall protects from invocation exceptions, which is what we need except ---during debugging. This flag replaces them with a normal call so we can inspect stack traces. +--- This constant should only be true during development. It replaces `pcall` by regular `call`. +-- Pcall protects the script from invocation exceptions, which is what we need except during debugging. +-- When this flag is true, normal calls will be used so we can inspect stack traces. M.DEBUG_PCALLS = false ---This enables debugging of the REST API from the command-line, specify the path and optionally the request method as follows: 'p=/mod/func rq=POST' +--- This constant enables debugging of the REST API from the command-line by emulating GET/POST requests. +-- Specify the path and optionally the request method as follows: `d3dapi p=/mod/func r=POST`. M.DEBUG_API = true ---REST responses will contain 'module' and 'function' keys describing what was requested +--- If enabled, REST responses will contain 'module' and 'function' keys describing what was requested. M.API_INCLUDE_ENDPOINT_INFO = false -M.API_BASE_URL_PATH = 'doodle3d.com' -- includes any base path if necessary (e.g. 'localhost/~user') +--- This base path is used in @{rest.response}. It includes any base path if necessary (e.g. 'localhost/~user'). +M.API_BASE_URL_PATH = 'doodle3d.com' M.network_ap_ssid = { default = 'Doodle3D-%%MAC_ADDR_TAIL%%', @@ -50,7 +56,7 @@ M.network_ap_key = { type = 'string', description = 'Access Point security key', isValid = function(value) - if value == "" then + if value == "" then return true; elseif value:len() < 8 then return false, "too short" diff --git a/src/rest/request.lua b/src/rest/request.lua index 54f5cce..cc707bc 100644 --- a/src/rest/request.lua +++ b/src/rest/request.lua @@ -1,3 +1,5 @@ +--- +-- This object represents an HTTP request object, part of the REST API. local util = require('util.utils') -- required for string:split() local urlcode = require('util.urlcode') local confDefaults = require('conf_defaults') diff --git a/src/rest/response.lua b/src/rest/response.lua index c22aa95..77b67ec 100644 --- a/src/rest/response.lua +++ b/src/rest/response.lua @@ -1,3 +1,7 @@ +--- +-- The REST response object handles all operations necessary to generate a HTTP response. +-- It knows about the request, ensures the correct HTTP headers are present and +-- allows to set a status (one of `success`, `fail` or `error`) and addtional data. local JSON = require('util/JSON') local settings = require('util.settings') local defaults = require('conf_defaults') @@ -10,6 +14,9 @@ local REQUEST_ID_ARGUMENT = 'rq_id' M.httpStatusCode, M.httpStatusText, M.contentType = nil, nil, nil M.binaryData, M.binarySavename = nil, nil +--- Print a HTTP header line with the given type and value. +-- @string headerType +-- @string headerValue local function printHeaderLine(headerType, headerValue) io.write(headerType .. ": " .. headerValue .. "\r\n") end @@ -21,7 +28,10 @@ setmetatable(M, { end }) ---requestObject should always be passed (except on init failure, when it is not yet available) +--- Instantiates a new response object initialized with the given @{request} object. +-- The request object should always be passed, except on init failure, when it is not yet available. +-- @tparam request requestObject The object representing the HTTP request. +-- @treturn response A newly instantiated response object. function M.new(requestObject) local self = setmetatable({}, M) self.body = { status = nil, data = {} } @@ -29,9 +39,9 @@ function M.new(requestObject) self:setContentType('text/plain;charset=UTF-8') --self:setContentType('application/json;charset=UTF-8') - -- a queue for functions to be executed when the response has bin given - -- needed for api calls like network/associate, which requires a restart of the webserver - self.postResponseQueue = {} + -- A queue for functions to be executed when the response has been given. + -- Needed for api calls like network/associate, which requires a restart of the webserver. + self.postResponseQueue = {} if requestObject ~= nil then local rqId = requestObject:get(REQUEST_ID_ARGUMENT) @@ -46,25 +56,37 @@ function M.new(requestObject) return self end +--- Set HTTP status (the default is `200 OK`). +-- @number code The status code. +-- @string text The status text, this should of course correspond to the given code. function M:setHttpStatus(code, text) if code ~= nil then self.httpStatusCode = code end if text ~= nil then self.httpStatusText = text end end +--- Set the HTTP content type (the default is `text/plain;charset=UTF-8`). +-- @string contentType The content type to set. function M:setContentType(contentType) if contentType ~= nil then self.contentType = contentType end end +--- Set REST status to success. +-- @string[opt] msg An optional human-readable message to include with the status. function M:setSuccess(msg) self.body.status = 'success' if msg ~= '' then self.body.msg = msg end end +--- Set REST status to failure. +-- @string[opt] msg An optional human-readable message to include with the status. function M:setFail(msg) self.body.status = 'fail' if msg ~= '' then self.body.msg = msg end end +--- Set REST status to error. +-- A reference to the API documentation will also be included. +-- @string[opt] msg An optional human-readable message to include with the status. function M:setError(msg) self.body.status = 'error' if msg ~= '' then self.body.msg = msg end @@ -72,17 +94,31 @@ function M:setError(msg) self:addData('more_info', 'http://' .. defaults.API_BASE_URL_PATH .. '/wiki/wiki/communication-api') end ---NOTE: with this method, to add nested data, it is necessary to precreate the table and add it with its root key ---(e.g.: response:addData('data', {f1=3, f2='x'})) +--- Adds a data item to the response, this will be included under the `data` item of the json text. +-- +-- NOTE: To add nested data with this method, it is necessary to precreate the table +-- and then add that with its root key. (e.g., `response:addData('f_values', {f1=3, f2='x'})`) +-- +-- After calling this, any binary data set by @{M:setBinaryFileData} will not be sent anymore. +-- +-- @string k The key of the item to set. +-- @param v The value to set. function M:addData(k, v) self.body.data[k] = v self.binaryData = nil end +--- Queue a function for execution after the response has been passed back to the webserver. +-- +-- Note that this is not useful in many cases since the webserver will not actually send +-- the response until this script finishes. So for instance if the queue contains code +-- to restart the webserver, the response will never be sent out. +-- @func fn The function to queue. function M:addPostResponseFunction(fn) table.insert(self.postResponseQueue, fn) end +--- Call all function on the post-response queue, see @{M:addPostResponseFunction} for details and a side-note. function M:executePostResponseQueue() --local utils = require('util.utils') --local log = require('util.logger') @@ -91,16 +127,23 @@ function M:executePostResponseQueue() for i,fn in ipairs(self.postResponseQueue) do fn() end end +--- Returns an API url pointing to @{conf_defaults.API_BASE_URL_PATH}, which is quite useless. +-- @string mod +-- @string func +-- @treturn string A not-so-useful URL. function M:apiURL(mod, func) if not mod then return nil end if func then func = '/' .. func else func = "" end return 'http://' .. defaults.API_BASE_URL_PATH .. '/cgi-bin/d3dapi/' .. mod .. func end +--- Returns the body data contained in this object as [JSON](http://www.json.org/). +-- @treturn string The JSON data. function M:serializeAsJson() return JSON:encode(self.body) end +--- Writes HTTP headers, followed by an HTTP body containing JSON data to stdout. function M:send() printHeaderLine("Status", self.httpStatusCode .. " " .. self.httpStatusText) printHeaderLine("Content-Type", self.contentType) @@ -116,6 +159,14 @@ function M:send() end end +--- Sets the response object to return binary data instead of JSON as its body. +-- +-- After calling this, REST data and status will not be sent anymore. +-- @string rFile The file on the local file system to read the data from. +-- @string saveName The file name to suggest the user to save the data in. +-- @string contentType The content type of the data. +-- @treturn bool|nil True, or nil in which case the second argument will be set. +-- @treturn ?string An error message if the first argument is nil. function M:setBinaryFileData(rFile, saveName, contentType) if type(rFile) ~= 'string' or rFile:len() == 0 then return false end diff --git a/src/script/d3d-updater.lua b/src/script/d3d-updater.lua index 13aa64c..695a09f 100755 --- a/src/script/d3d-updater.lua +++ b/src/script/d3d-updater.lua @@ -1,5 +1,8 @@ #!/usr/bin/env lua +--- This script provides an interface to upgrade or downgrade the Doodle3D wifibox. +-- It can both be used as a standalone command-line tool and as a Lua library. + -- TODO/NOTES: -- add to status: validImage: none| (can use checkValidImage for this) -- any more TODO's across this file? @@ -7,9 +10,8 @@ -- MAYBE/LATER: -- add API calls to retrieve a list of all versions with their info (i.e., the result of getAvailableVersions) --- wget: add provision (in verbose mode?) to use -v instead of -q and disable output redirection +-- wget: add provision (in verbose mode?) to use '-v' instead of '-q' and disable output redirection -- document index file format (Version first, then in any order: Files: sysup; factory, FileSize: sysup; factory, MD5: sysup; factory, ChangelogStart:, ..., ChangelogEnd:) --- remove /etc/wifibox-version on macbook... -- copy improved fileSize back to utils (add unit tests!) -- create new utils usable by updater as well as api? (remove dependencies on uci and logger etc) -- note: take care not to print any text in module functions, as this breaks http responses @@ -17,18 +19,39 @@ local M = {} --- NOTE: 'INSTALLED' will never be returned (and probably neither will 'INSTALLING') since in that case the device is flashing or rebooting -M.STATE = { NONE = 1, DOWNLOADING = 2, DOWNLOAD_FAILED = 3, IMAGE_READY = 4, INSTALLING = 5, INSTALLED = 6, INSTALL_FAILED = 7 } +--- Possible states the updater can be in, they are stored in @{STATE_FILE}. +-- @table STATE +M.STATE = { + NONE = 1, -- @{STATE_FILE} does not exist + DOWNLOADING = 2, -- downloading is started but not finished yet + DOWNLOAD_FAILED = 3, -- download failed (often occurs when the wifibox is not connected to internet) + IMAGE_READY = 4, -- download succeeded and the image is valid + INSTALLING = 5, -- image is being installed (this state will probably never be returned since the box is flashing/rebooting) + INSTALLED = 6, -- image has been installed successfully (this state will never be returned since the box will reboot) + INSTALL_FAILED = 7 -- installation failed +} + +-- Names for the states in @{STATE}, these are returned through the REST API. M.STATE_NAMES = { [M.STATE.NONE] = 'none', [M.STATE.DOWNLOADING] = 'downloading', [M.STATE.DOWNLOAD_FAILED] = 'download_failed', [M.STATE.IMAGE_READY] = 'image_ready', [M.STATE.INSTALLING] = 'installing', [M.STATE.INSTALLED] = 'installed', [M.STATE.INSTALL_FAILED] = 'install_failed' } +--- The base URL to use for finding update files. +-- This URL will usually contain both an OpenWRT feed directory and an `images`-directory. +-- This script uses only the latter, and expects to find the file @{IMAGE_INDEX_FILE} there. M.DEFAULT_BASE_URL = 'http://doodle3d.com/updates' --M.DEFAULT_BASE_URL = 'http://localhost/~USERNAME/wifibox/updates' + +--- The index file containing metadata on update images. M.IMAGE_INDEX_FILE = 'wifibox-image.index' + +--- Path to the updater cache. M.CACHE_PATH = '/tmp/d3d-updater' + +--- Name of the file to store current state in, this file resides in @{CACHE_PATH}. M.STATE_FILE = 'update-state' + M.WGET_OPTIONS = "-q -t 1 -T 30" --M.WGET_OPTIONS = "-v -t 1 -T 30" @@ -43,7 +66,10 @@ local baseUrl = M.DEFAULT_BASE_URL -- default, can be overwritten by M.setBaseUr -- LOCAL FUNCTIONS -- --------------------- --- use level==1 for important messages, 0 for regular messages and -1 for less important messages +--- Log a message with the given level, if logging is enabled for that level. +-- Messages will be written to [stdout](http://www.cplusplus.com/reference/cstdio/stdout/), or logged using the logger set with @{setLogger}. +-- @number lvl Level to log to, use 1 for important messages, 0 for regular messages and -1 for less important messages. +-- @string msg The message to log. local function P(lvl, msg) if log then if lvl == -1 then log:debug(msg) @@ -54,15 +80,28 @@ local function P(lvl, msg) end end +--- Log a debug message, this function wraps @{P}. +-- The message will be logged with level -1 and be prefixed with '(DBG)'. +-- @string msg The message to log. local function D(msg) P(-1, (log and msg or "(DBG) " .. msg)) end +--- Log an error. +-- Messages will be written to [stderr](http://www.cplusplus.com/reference/cstdio/stderr/), or logged using the logger set with @{setLogger}. +-- @string msg The message to log. local function E(msg) if log then log:error(msg) else io.stderr:write(msg .. '\n') end end --- splits the return status from os.execute (see: http://stackoverflow.com/questions/16158436/how-to-shift-and-mask-bits-from-integer-in-lua) +--- Splits the return status from `os.execute`, which consists of two bytes. +-- +-- `os.execute` internally calls [system](http://linux.die.net/man/3/system), +-- which usually returns the command exit status as high byte (see [WEXITSTATUS](http://linux.die.net/man/2/wait)). +-- Furthermore, see [shifting bits in Lua](http://stackoverflow.com/questions/16158436/how-to-shift-and-mask-bits-from-integer-in-lua). +-- @number exitStatus The combined exit status. +-- @treturn number The command exit status. +-- @treturn number The `os.execute`/[system](http://linux.die.net/man/3/system) return status. local function splitExitStatus(exitStatus) if exitStatus == -1 then return -1,-1 end local cmdStatus = math.floor(exitStatus / 256) @@ -70,6 +109,9 @@ local function splitExitStatus(exitStatus) return cmdStatus, systemStatus end +--- Returns a human-readable message for a [wget exit status](http://www.gnu.org/software/wget/manual/wget.html#Exit-Status). +-- @number exitStatus An exit status from wget. +-- @treturn string|number Either the status followed by a description, or a message indicating the call was interrupted, or just the status if it was not recognized. local function wgetStatusToString(exitStatus) local wgetStatus,systemStatus = splitExitStatus(exitStatus) @@ -96,6 +138,9 @@ local function wgetStatusToString(exitStatus) end end +--- Creates the updater cache directory. +-- @return bool|nil True, or nil on error. +-- @return ?string A message in case of error. local function createCacheDirectory() if os.execute('mkdir -p ' .. M.CACHE_PATH) ~= 0 then return nil,"Error: could not create cache directory '" .. M.CACHE_PATH .. "'" @@ -103,6 +148,9 @@ local function createCacheDirectory() return true end +--- Retrieves the current updater state code and message from @{STATE_FILE}. +-- @treturn STATE The current state code (@{STATE}.NONE if no state has been set). +-- @treturn string The current state message (empty string if no state has been set). local function getState() local file,msg = io.open(M.CACHE_PATH .. '/' .. M.STATE_FILE, 'r') if not file then return M.STATE.NONE,"" end @@ -113,13 +161,23 @@ local function getState() return code,msg end --- trim whitespace from both ends of string (from http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76) +--- Trims whitespace from both ends of a string. +-- See [this Lua snippet](http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76). +-- @string s The text to trim. +-- @treturn string s, with whitespace trimmed. local function trim(s) if type(s) ~= 'string' then return s end return (s:find('^%s*$') and '' or s:match('^%s*(.*%S)')) end --- from utils.lua +--- Read the contents of a file. +-- +-- TODO: this file has been copied from @{util.utils}.lua and should be merged again. +-- @string filePath The file to read. +-- @bool trimResult Whether or not to trim the read data. +-- @treturn bool|nil True, or nil on error. +-- @treturn ?string A descriptive message on error. +-- @treturn ?number TODO: find out why this value is returned. local function readFile(filePath, trimResult) local f, msg, nr = io.open(filePath, 'r') if not f then return nil,msg,nr end @@ -134,7 +192,11 @@ local function readFile(filePath, trimResult) return res end --- from utils.lua +--- Reports whether or not a file exists. +-- +-- TODO: this file has been copied from @{util.utils}.lua and should be merged again. +-- @string file The file to report about. +-- @treturn bool True if the file exists, false otherwise. local function exists(file) if not file or type(file) ~= 'string' or file:len() == 0 then return nil, "file must be a non-empty string" @@ -145,8 +207,11 @@ local function exists(file) return r ~= nil end --- from utils.lua ---argument: either an open file or a filename +--- Reports the size of a file or file handle. +-- +-- TODO: this file has been copied from @{util.utils}.lua and should be merged again. +-- @param file A file path or open file handle. +-- @treturn number Size of the file. local function fileSize(file) local size = nil if type(file) == 'file' then @@ -164,18 +229,27 @@ local function fileSize(file) return size end --- returns return value of command +--- Runs an arbitrary shell command. +-- @string command The command to run. +-- @bool dryRun Only log a message if true, otherwise run the command and log a message. +-- @treturn number Exit status of of command or -1 if dryRun is true. local function runCommand(command, dryRun) D("about to run: '" .. command .. "'") return (not dryRun) and os.execute(command) or -1 end +--- Removes a file. +-- @string filePath The file to remove. local function removeFile(filePath) return runCommand('rm ' .. filePath) end --- returns return value of wget (or nil if saveDir is nil or empty), filename is optional --- NOTE: leaving out filename will cause issues with files not being overwritten but suffixed with '.1', '.2',etc instead +--- Downloads a file and stores it locally. +-- @string url The full URL to download. +-- @string saveDir The path at which to save the downloaded file. +-- @string[opt] filename File name to save as, note that leaving this out has issues with files not being overwritten but suffixed with '.1', '.2',etc instead. +-- @treturn number|nil Exit status of wget command or nil on error. +-- @treturn ?string Descriptive message if saveDir is nil or empty. local function downloadFile(url, saveDir, filename) if not saveDir or saveDir:len() == 0 then return nil, "saveDir must be non-empty" end local outArg = (filename:len() > 0) and (' -O' .. filename) or '' @@ -186,6 +260,10 @@ local function downloadFile(url, saveDir, filename) end end +--- Parses command-line arguments and returns a table containing information distilled from them. +-- @tab arglist A table in the same form as the [arg table](http://www.lua.org/pil/1.4.html) created by Lua. +-- @treturn tabla|nil A table containing information on what to do, or nil if invalid arguments were specified. +-- @treturn ?string Descriptive message on error. local function parseCommandlineArguments(arglist) local result = { verbosity = 0, baseUrl = M.DEFAULT_BASE_URL, action = nil } local nextIsVersion, nextIsUrl = false, false @@ -233,18 +311,35 @@ end -- MODULE FUNCTIONS -- ---------------------- +--- Enables use of the given @{util.logger} object, otherwise `stdout`/`stderr` will be used. +-- @tparam util.logger logger The logger to log future messages to. function M.setLogger(logger) log = logger end +--- Controls whether or not to use pre-existing files over (re-)downloading them. +-- +-- Note that the mechanism is currently naive, (e.g., there are no mechanisms like maximum cache age). +-- @bool use If true, try not to download anything unless necessary. function M.setUseCache(use) useCache = use end +--- Sets the base URL to use for finding update images, defaults to @{DEFAULT_BASE_URL}. +-- @string url The new base URL to use. function M.setBaseUrl(url) baseUrl = url end +--- Returns a table with information about current update status of the wifibox. +-- +-- The result table will contain at least the current version, current state code and text. +-- If the box has internet access, it will also include the newest version available. +-- If an image is currently being downloaded, progress information will also be included. +-- +-- @treturn bool True if status has been determined fully, false if not. +-- @treturn table The result table. +-- @treturn ?string Descriptive message in case the result table is not complete. function M.getStatus() if not baseUrl then baseUrl = M.DEFAULT_BASE_URL end local unknownVersion = { major = 0, minor = 0, patch = 0 } @@ -253,7 +348,7 @@ function M.getStatus() result.currentVersion = M.getCurrentVersion() result.stateCode, result.stateText = getState() result.stateCode = tonumber(result.stateCode) - + local verTable,msg = M.getAvailableVersions() if not verTable then D("could not obtain available versions (" .. msg .. ")") @@ -263,7 +358,7 @@ function M.getStatus() local newest = verTable and verTable[#verTable] result.newestVersion = newest and newest.version or unknownVersion - + if result.stateCode == M.STATE.DOWNLOADING then result.progress = fileSize(M.CACHE_PATH .. '/' .. newest.sysupgradeFilename) if not result.progress then result.progress = 0 end -- in case the file does not exist yet (which yields nil) @@ -525,6 +620,10 @@ end -- MAIN -- ---------- +--- The main function which will be called in standalone mode. +-- At the end of the file, this function will be invoked only if `arg` is defined, +-- so this file can also be used as a library. +-- Command-line arguments are expected to be present in the global `arg` variable. local function main() local argTable,msg = parseCommandlineArguments(arg) @@ -662,7 +761,7 @@ local function main() os.exit(0) end --- only execute the main function if an arg table is present, this enables usage both as module and as standalone script +--- Only execute the main function if an arg table is present, this enables usage both as module and as standalone script. if arg ~= nil then main() end return M diff --git a/src/util/logger.lua b/src/util/logger.lua index 00c940f..6824337 100644 --- a/src/util/logger.lua +++ b/src/util/logger.lua @@ -1,8 +1,5 @@ ---[[-- - TODO: ldoc: @{} ref in init() tformat - TODO: use macros/type definitions to document rest modules (to auto-match things like 'M._NAME%')? - TODO: finish documentation -]] +--- +-- Logging facilities. local utils = require('util.utils') @@ -10,7 +7,8 @@ local M = {} local logLevel, logVerbose, logStream ---- Available log levels. +--- Available log levels +-- @table LEVEL M.LEVEL = { 'debug', -- for debug messages 'info', -- for informational messages @@ -34,10 +32,10 @@ local function log(level, msg, verbose) local name = i.name or "(nil)" local vVal = 'nil' local m = (type(msg) == 'string') and msg or utils.dump(msg) - + if v then logStream:write(now .. " (" .. M.LEVEL[level] .. ") " .. m .. " [" .. name .. "@" .. i.short_src .. ":" .. i.linedefined .. "]\n") else logStream:write(now .. " (" .. M.LEVEL[level] .. ") " .. m .. "\n") end - + logStream:flush() end end diff --git a/src/util/settings.lua b/src/util/settings.lua index fbb6127..f7be765 100644 --- a/src/util/settings.lua +++ b/src/util/settings.lua @@ -1,15 +1,9 @@ ---[[-- - The settings interface reads and writes configuration keys using UCI. - All keys have pre-defined defaults in @{conf_defaults} which will be used - if no value is stored in the UCI config. The UCI config file is - '/etc/config/wifibox'. - The default values guarantee there will always be a set of reasonable settings - to use and provide a clear overview of all existing configuration keys as well. - - By the way, returning correct values in get()/fromUciValue() for booleans has - been fixed at a relatively convenient time purely thanks to the unit tests... - just to indicate how useful they are. :) -]] +--- +-- The settings interface reads and writes configuration keys using [UCI](http://wiki.openwrt.org/doc/uci). +-- All keys have pre-defined defaults in @{conf_defaults} which will be used +-- if no value is stored in the UCI config. The UCI config file is `/etc/config/wifibox`. +-- The default values guarantee there will always be a set of reasonable settings +-- to use and provide a clear overview of all existing configuration keys as well. local uci = require('uci').cursor() local utils = require('util.utils') local baseconfig = require('conf_defaults') @@ -18,26 +12,26 @@ local log = require('util.logger') local M = {} ---- UCI config name (i.e. file under /etc/config) +--- UCI config name (i.e., file under `/etc/config`) local UCI_CONFIG_NAME = 'wifibox' --- Absolute path to the UCI config file local UCI_CONFIG_FILE = '/etc/config/' .. UCI_CONFIG_NAME ---- Section type that will be used in UCI\_CONFIG\_FILE +--- [Section type](http://wiki.openwrt.org/doc/techref/uci#about.uci.structure) that will be used in @{UCI_CONFIG_FILE} local UCI_CONFIG_TYPE = 'settings' ---- Section name that will be used for 'public' settings (as predefined in conf_defaults.lua) in UCI\_CONFIG\_FILE +--- [Section name](http://wiki.openwrt.org/doc/techref/uci#about.uci.structure) that will be used for 'public' settings (as predefined in conf_defaults.lua) in @{UCI_CONFIG_FILE} local UCI_CONFIG_SECTION = 'general' ---- Section name that will be used for 'firmware-local' settings in UCI\_CONFIG\_FILE +--- [Section name](http://wiki.openwrt.org/doc/techref/uci#about.uci.structure) that will be used for 'firmware-local' settings in @{UCI_CONFIG_FILE} local UCI_CONFIG_SYSTEM_SECTION = 'system' local ERR_NO_SUCH_KEY = "key does not exist" --- Returns a key with all periods ('.') replaced by underscores ('_'). --- @tparam string key The key for which to substitute dots. +-- @string key The key for which to substitute dots. -- @return The substituted key, or the key parameter itself if it is not of type 'string'. local function replaceDots(key) if type(key) ~= 'string' then return key end @@ -46,7 +40,7 @@ local function replaceDots(key) end --- Returns a key with all underscores ('_') replaced by periods ('.'). --- @tparam string key The key for which to substitute underscores. +-- @string key The key for which to substitute underscores. -- @return The substituted key, or the key parameter itself if it is not of type 'string'. local function replaceUnderscores(key) if type(key) ~= 'string' then return key end @@ -57,13 +51,13 @@ end --- Converts a lua value to equivalent representation for UCI. -- Boolean values are converted to '1' and '0', everything else is converted to a string. -- --- @param v The value to convert. --- @param vType The type of the given value. +-- @p v The value to convert. +-- @p vType The type of the given value. -- @return A value usable to write to UCI. local function toUciValue(v, vType) if vType == 'bool' then return v and '1' or '0' end - if(vType == 'string') then - v = v:gsub('[\n\r]', '\\n') + if(vType == 'string') then + v = v:gsub('[\n\r]', '\\n') end return tostring(v) @@ -73,33 +67,33 @@ end -- For boolean, '1' is converted to true and everything else to false. Floats -- and ints are converted to numbers and everything else will be returned as is. -- --- @param v The value to convert. --- @param vType The type of the given value. +-- @p v The value to convert. +-- @p vType The type of the given value. -- @return A lua value typed correctly with regard to the vType parameter. local function fromUciValue(v, vType) if v == nil then return nil end - + if vType == 'bool' then return (v == '1') and true or false elseif vType == 'float' or vType == 'int' then return tonumber(v) elseif vType == 'string' then - v = v:gsub('\\n', '\n') + v = v:gsub('\\n', '\n') return v else return v end - + end --- Reports whether a value is valid given the constraints specified in a base table. --- @param value The value to test. --- @tparam table baseTable The base table to use constraint data from (min,max,regex). +-- @p value The value to test. +-- @tab baseTable The base table to use constraint data from (min,max,regex). -- @treturn bool Returns true if the value is valid, false if it is not. local function isValid(value, baseTable) local varType, min, max, regex, isValid = baseTable.type, baseTable.min, baseTable.max, baseTable.regex, baseTable.isValid - if isValid then + if isValid then local ok,msg = isValid(value) if msg == nil then msg = "invalid value" end return ok or nil,msg @@ -107,7 +101,7 @@ local function isValid(value, baseTable) if varType == 'bool' then return type(value) == 'boolean' or nil,"invalid bool value" - + elseif varType == 'int' or varType == 'float' then local numValue = tonumber(value) if numValue == nil then @@ -119,7 +113,7 @@ local function isValid(value, baseTable) elseif max and numValue > max then return nil, "too high" end - + elseif varType == 'string' then local ok = true if min and value:len() < min then @@ -128,14 +122,14 @@ local function isValid(value, baseTable) return nil,"too long" elseif regex and value:match(regex) == nil then return nil,"invalid value" - end + end end return true end ---- Looks up the table in conf_defaults.lua corresponding to a key. --- @tparam string key The key for which to return the base table. +--- Looks up the table in @{conf_defaults}.lua corresponding to a key. +-- @string key The key for which to return the base table. -- @treturn table The base table for key, or nil if it does not exist. local function getBaseKeyTable(key) local base = baseconfig[key] @@ -144,20 +138,20 @@ end --- Returns the value of the requested key if it exists. --- @param key The key to return the associated value for. +-- @p key The key to return the associated value for. -- @return The associated value, beware (!) that this may be boolean false for keys of 'bool' type. function M.get(key) key = replaceDots(key) local base = getBaseKeyTable(key) - + if not base then return nil,ERR_NO_SUCH_KEY end - + local v = base.default local uciV = fromUciValue(uci:get(UCI_CONFIG_NAME, UCI_CONFIG_SECTION, key), base.type) local actualV = v if uciV ~= nil then actualV = uciV end - + return actualV end @@ -175,7 +169,7 @@ function M.getAll() end --- Reports whether or not a key exists. --- @tparam string key The key to find. +-- @string key The key to find. -- @treturn bool True if the key exists, false if not. function M.exists(key) key = replaceDots(key) @@ -187,7 +181,7 @@ end -- if for instance, the default is 'abc', and UCI contains a configured value of -- 'abc' as well, that key is _not_ a default value. -- --- @tparam string key The key to report about. +-- @string key The key to report about. -- @treturn bool True if the key is currently at its default value, false if not. function M.isDefault(key) key = replaceDots(key) @@ -196,8 +190,8 @@ function M.isDefault(key) end --- Sets a key to a new value or reverts it to the default value. --- @tparam string key The key to set. --- @param value The value or set, or nil to revert key to its default value. +-- @string key The key to set. +-- @p[opt=nil] value The value or set, or nil to revert key to its default value. -- @treturn bool|nil True if everything went well, nil in case of error. -- @treturn ?string Error message in case first return value is nil (invalid key). function M.set(key, value) @@ -206,10 +200,10 @@ function M.set(key, value) local r = utils.create(UCI_CONFIG_FILE) uci:set(UCI_CONFIG_NAME, UCI_CONFIG_SECTION, UCI_CONFIG_TYPE) - + local base = getBaseKeyTable(key) if not base then return nil,ERR_NO_SUCH_KEY end - + if M.isDefault(key) and value == nil then return true end -- key is default already --log:info(" not default") local current = uci:get(UCI_CONFIG_NAME, UCI_CONFIG_SECTION, key) @@ -222,7 +216,7 @@ function M.set(key, value) end elseif base.type == 'int' or base.type == 'float' then value = tonumber(value) - if(value == nil) then + if(value == nil) then return nil,"Value isn't a valid int or float" end end @@ -232,7 +226,7 @@ function M.set(key, value) if not valid then return nil,m end - + if fromUciValue(current, base.type) == value then return true end if value ~= nil then @@ -240,13 +234,13 @@ function M.set(key, value) else uci:delete(UCI_CONFIG_NAME, UCI_CONFIG_SECTION, key) end - + uci:commit(UCI_CONFIG_NAME) return true end --- Returns a UCI configuration key from the system section. --- @tparam string key The key for which to return the value, must be non-empty. +-- @string key The key for which to return the value, must be non-empty. -- @return Requested value or false if it does not exist or nil on invalid key. function M.getSystemKey(key) if type(key) ~= 'string' or key:len() == 0 then return nil end @@ -254,21 +248,21 @@ function M.getSystemKey(key) return v or false end ---- Sets the given key to the given value. +--- Sets the value of a UCI key in the system section. -- Note that unlike the public settings, system keys are untyped and value must -- be of type string; UCI generally uses '1' and '0' for boolean values. --- @tparam string key The key to set, must be non-empty. --- @tparam string value The value to set key to. --- @return True on success or false if key or value arguments are invalid. +-- @string key The key to set, must be non-empty. +-- @string value The value to set key to. +-- @return True on success or nil if key or value arguments are invalid. function M.setSystemKey(key, value) if type(key) ~= 'string' or key:len() == 0 then return nil end if type(value) ~= 'string' then return nil end - + local r = utils.create(UCI_CONFIG_FILE) -- make sure the file exists for uci to write to uci:set(UCI_CONFIG_NAME, UCI_CONFIG_SYSTEM_SECTION, UCI_CONFIG_TYPE) uci:set(UCI_CONFIG_NAME, UCI_CONFIG_SYSTEM_SECTION, key, value) uci:commit(UCI_CONFIG_NAME) - + return true end diff --git a/src/util/utils.lua b/src/util/utils.lua index 674794a..92210cf 100644 --- a/src/util/utils.lua +++ b/src/util/utils.lua @@ -1,7 +1,6 @@ ---[[-- - TODO: finish documentation - The unavoidable collection of utility functions. -]] +--- +-- The unavoidable collection of utility functions. +-- TODO: use macros/type definitions to document rest modules (to auto-match things like 'M._NAME%')? local M = {} @@ -25,7 +24,7 @@ end function M.toboolean(s) if not s then return false end - + local b = type(s) == 'string' and s:lower() or s local textTrue = (b == '1' or b == 't' or b == 'true') local boolTrue = (type(b) == 'boolean' and b == true) @@ -63,7 +62,7 @@ function M.exists(file) if not file or type(file) ~= 'string' or file:len() == 0 then return nil, "file must be a non-empty string" end - + local r = io.open(file, 'r') -- ignore returned message if r then r:close() end return r ~= nil @@ -72,16 +71,16 @@ end --creates and returns true if not exists, returns false it does, nil+msg on error function M.create(file) local r,m = M.exists(file) - + if r == nil then return r,m elseif r == true then return true end - + r,m = io.open(file, 'a') -- append mode is probably safer in case the file does exist after all if not r then return r,m end - + r:close() return true end @@ -96,10 +95,10 @@ end function M.readFile(filePath) local f, msg, nr = io.open(filePath, 'r') if not f then return nil,msg,nr end - + local res = f:read('*all') f:close() - + return res end