diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..249cda9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/data \ No newline at end of file diff --git a/README.md b/README.md index 5b20f3a..89abfe7 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,57 @@ -# modpol: Modular Politics for Minetest +# modpol: Modular Politics Prototype for Minetest -This is a mod for [Minetest](https://minetest.net) that provides an API for diverse governance mechanisms. It seeks to implement the [Modular Politics](https://metagov.org/modpol) proposal. +This is a mod for [Minetest](https://minetest.net) that enables diverse governance mechanisms. It seeks to implement the [Modular Politics](https://metagov.org/modpol) proposal. Ideally, in the future, it +will be possible to use this framework to simulate governance in a +number of platform contexts. -This mod produces an API that can serve as a dependency for other mods that add specific governance functionalities. +This mod produces an API that can serve as a dependency for other mods that add specific governance functionalities. + +Currently modpol requires a Unix-style system. But it is intended to become more fully platform independent. Here, only the init.lua (and the optional storage-mod_storage.lua) files are Minetest-specific. For background information and project roadmap, see [the wiki](https://gitlab.com/medlabboulder/modpol/-/wikis/home). -## Functioning commands +## Command line -Most of these commands will later be buried under other commands that do more privilege checking. These are mainly for testing purposes. +To interact with the interpreter on Unix systems in CLI mode, install +lua or luajit and execute the following command in this directory: -* `/neworg [orgname]` - Create a new org +``` +$ lua +> dofile("modpol.lua") +``` + +## Minetest + +To use this in Minetest, simply install it as a Minetest mod. Minetest +will load init.lua. See the source code for information about chat +commands which can then be used. + +Most of these commands will later be buried under other commands that +do more privilege checking. These are mainly for testing purposes. + +* `/addorg [orgname]` - Create a new org * `/listorgs` - Lists the orgs (and their members) currently in existence -* `/rmorgs` - Reset orgs to just "instance" with all online players * `/listplayers` - Lists all the players currently in the game * `/joinorg [orgname]` - Adds the user to the specified org -* `/listmembers [orgname]` - Lists the players currently in the specified org * `/pollself [question]` - Asks the player a yes/no/abstain question ---- -Initiated by [Nathan Schneider](https://nathanschneider.info) of the [Media Enterprise Design Lab](https://colorado.edu/lab/medlab) at the University of Colorado Boulder, as part of the [Metagovernance Project](https://metagov.org), in collaboration with the Minetest community—see the initial [forum post](https://forum.minetest.net/viewtopic.php?f=47&t=26037). Contributions welcome. +## Storage + +By default, a data directory named "data" will be created in this directory. "/data" will contain a log file and serialized program data files. + +Another storage method may be chosen in modpol.lua. A StorageRef-based method for Minetest 5.* is included: storage-mod_storage.lua. + + +## Authorship + +Initiated by [Nathan Schneider](https://nathanschneider.info) of the [Media Enterprise Design Lab](https://colorado.edu/lab/medlab) at the University of Colorado Boulder, as part of the [Metagovernance Project](https://metagov.org). Based on the paper "[Modular Politics: Toward a Governance Layer for Online Communities](https://metagov.org/modpol)." + +We'd love to have more contributors, particularly from the Minetest community! Please join the conversation in the [Issues](https://gitlab.com/medlabboulder/modpol/-/issues) or the [Minetest.net forum](https://forum.minetest.net/viewtopic.php?f=47&t=26037). + +Thanks to contributors: Robert Kiraly [[OldCoder](https://github.com/oldcoder/)] (ocutils.lua, storage-local.lua, project refactoring) + +## Licenses + +* [Project](LICENSE.mt): MIT-based Hippocratic License +* [Lua Serpent Serializer](serpent/LICENSE.txt): MIT diff --git a/depends.txt b/depends.txt new file mode 100644 index 0000000..4ad96d5 --- /dev/null +++ b/depends.txt @@ -0,0 +1 @@ +default diff --git a/init.lua b/init.lua index d306c6f..7015904 100644 --- a/init.lua +++ b/init.lua @@ -1,227 +1,45 @@ ---[[ INITIALIZING: basics ]]-- +-- =================================================================== +-- /init.lua +-- Modular Politics (modpol) for Minetest +-- TKTK Maybe make this just a quick ref file and locate MT files elsewhere? +-- TKTK need to add player to orgs.instance with on_joinplayer --- global API table -modpol = { -} +-- =================================================================== +-- Load modpol system --- table for all active governance data -modpol.orgs = { -} +dofile(minetest.get_modpath("modpol") .. "/modpol.lua") --- record of governance interactions --- every state change should appear here -modpol.ledger = { -} +-- =================================================================== +-- Modular Politics functions +-- Overwriting default API functions with platform-specific ones +-- =================================================================== --- update from mod_storage --- https://dev.minetest.net/StorageRef -local mod_storage = minetest.get_mod_storage() --- load orgs -local stored_orgs = minetest.deserialize(mod_storage:get_string("orgs")) -if (stored_orgs ~= nil) then - modpol.orgs = stored_orgs -end --- load orgs -local stored_ledger = minetest.deserialize(mod_storage:get_string("ledger")) -if (stored_ledger ~= nil) then - modpol.ledger = stored_ledger -end - - ---[[ FUNCTIONS:basics ]]-- - --- record(message, org) --- writes all governance events to storage and ledger -function modpol.record(message, org) - -- record to ledger - table.insert(modpol.ledger, message) - -- record to org_ledger - if (modpol.orgs[org] ~= nil) then - local org_ledg = modpol.orgs[org]["ledger"] - if (org_ledg == nil) then - modpol.orgs[org]["ledger"] = {message} - else - modpol.orgs[org]["ledger"] = table.insert(org_ledg,message) - end - end - -- record to storage - mod_storage:set_string("orgs", minetest.serialize(modpol.orgs)) - mod_storage:set_string("ledger", minetest.serialize(modpol.ledger)) -end - ---[[ FUNCTIONS:orgs ]]-- - --- new_org() --- create an org and add it to the list -function modpol.new_org(orgName, orgMembers) - if (orgName == "") then -- blank orgName input - return "-!- Org needs a name" - end - if (modpol.orgs[orgName] == nil) then -- copy check - modpol.orgs[orgName] = {members = orgMembers} - else return "-!- Org already exists" - end - local message = "New org: " .. orgName .. - " (" .. table.concat(orgMembers, ", ") .. ")" - modpol.record(message, orgName) - return message -end - --- /neworg [name] --- creates a new org with the current user as sole member -minetest.register_chatcommand( - "neworg", { - privs = {}, - func = function(user, param) - local result = - modpol.new_org(param,{user}) - return true, result - end, -}) - --- rename_org() -function modpol.rename_org(oldName, newName) - -- TKTK - local message = "Org renamed: " .. oldName .. " > " .. newName - modpol.record(message, newName) -end - - --- /listorgs --- lists the orgs currently in the game -minetest.register_chatcommand( - "listorgs", { - privs = {}, - func = function(user, param) - local orglist = "" - for key, value in pairs(modpol.orgs) do - -- first, set up member list - local membs = modpol.orgs[key]["members"] - if (membs == nil) then membs = "" - else membs = " (" .. table.concat(membs, ", ") .. ")" - end - -- now, assemble the list - if (orglist == "") -- first element only - then orglist = key .. membs - else orglist = orglist .. ", " .. key .. membs - end - end - return true, "Orgs: " .. orglist - end, -}) - --- rm_orgs() --- removes all orgs -function modpol.rm_orgs() - modpol.orgs["instance"] = {members = modpol.list_members()} - local message = "Orgs purged" - modpol.record(message, nil) - return message -end - --- /rmorgs -minetest.register_chatcommand( - "rmorgs", { - privs = {}, - func = function(user, param) - modpol.orgs = {} - return true, modpol.rm_orgs() - end, -}) - ---[[ FUNCTIONS:users ]]-- - --- list_members(domain) --- produces a table with names of players currently in the game --- if empty, lists all players; if an org name, lists its members -function modpol.list_members(domain) - local members = {} - if (domain == nil) then -- no specified domain; all players +-- =================================================================== +-- Function: modpol.list_users(org) +-- Overwrites function at /users.lua +-- Params: +-- if nil, lists instance members; if an org name, lists its members +-- Output: a table with names of players currently in the game +modpol.list_users = function(org) + local users = {} + if (org == nil) then -- no specified org; all players for _,player in ipairs(minetest.get_connected_players()) do local name = player:get_player_name() - table.insert(members,name) + table.insert(users,name) end else -- if an org is specified - if (modpol.orgs[domain] ~= nil) then -- org exists - members = modpol.orgs[domain]["members"] + if (modpol.orgs[org] ~= nil) then -- org exists + users = modpol.orgs[org]["members"] end end return members end --- /listplayers -minetest.register_chatcommand( - "listplayers", { - privs = {}, - func = function(user) - local result = table.concat(modpol.list_members(),", ") - return true, "All players: " .. result - end, -}) - -function modpol.add_member(org, member) - if (modpol.orgs.org == nil) then - return "-!- No such org" - else - table.insert(modpol.orgs.org["members"], member) - local message = member .. " added to org " .. org - modpol.record(message, org) - return message - end - -end - --- /joinorg -minetest.register_chatcommand( - "joinorg", { - privs = {}, - func = function(user, param) - local result = modpol.add_member(param, user) - return true, result - end, -}) - - -function modpol.remove_member(org, member) --- remove from all child orgs also - local message = member .. " removed from org " .. org - modpol.record(message, org) - return message -end - --- /listmembers [org] --- lists the members of an org -minetest.register_chatcommand( - "listmembers", { - privs = {}, - func = function(user, param) - local orglist = modpol.list_members(param) - return true, param .. ": " .. table.concat(orglist,", ") - end, -}) - - --- PERMISSIONS FUNCTIONS - - --- PRIVILEGE FUNCTIONS --- Minetest-specific --- manages user privileges according to org membership - -function modpol.assign_privilege(org, privilege) --- add privilege to all members of an org -end - - -function modpol.remove_privilege(org, privilege) --- remove privilege from all members of an org, unless they have it from other orgs -end - ---[[ USER INTERACTIONS ]]-- - --- modpol.binary_poll_user(user, question) +-- =================================================================== +-- Function: modpol.binary_poll_user(user, question) +-- Overwrites function at /interactions.lua -- presents a yes/no/abstain poll to a user, returns answer -function modpol.binary_poll_user(user, question) +modpol.binary_poll_user = function(user, question) -- set up formspec local text = "Poll: " .. question local formspec = { @@ -238,6 +56,90 @@ function modpol.binary_poll_user(user, question) minetest.show_formspec(user, "modpol:binary_poll", formspec_string) end +-- =================================================================== +-- Minetest commands +-- =================================================================== + +local chat_table -- MT chat command definitions table +local regchat -- Chat-command registration function + +regchat = minetest.register_chatcommand + +-- =================================================================== +-- /addorg /add_org +-- This code defines a chat command which creates a new +-- "org". Presently, the command makes the user the sole member of the +-- "org". + +chat_table = { + privs = {} , + func = function (user, param) + local result = modpol.add_org (param, { user }) + return true, result + end +} +regchat ("addorg" , chat_table) +regchat ("add_org" , chat_table) + +-- =================================================================== +-- /listorg /listorgs /list_org /list_orgs +-- In Minetest mode, this code defines a chat command which lists the +-- existing "orgs". +-- The list shows one "org" per line in the following format: +-- org_name (member, member, ...) + +chat_table = { + privs = {} , + func = function (user, param) + return true, "Orgs:\n" .. modpol.list_orgs() + end +} + +regchat ("listorg" , chat_table) +regchat ("listorgs" , chat_table) +regchat ("list_org" , chat_table) +regchat ("list_orgs" , chat_table) + +-- =================================================================== +-- /listplayers +minetest.register_chatcommand( + "listplayers", { + privs = {}, + func = function(user) + local result = table.concat(modpol.list_users(),", ") + return true, "All players: " .. result + end, +}) + +-- =================================================================== +-- /joinorg +minetest.register_chatcommand( + "joinorg", { + privs = {}, + func = function(user, param) + local result = modpol.add_member(param, user) + return true, result + end, +}) + + +-- =================================================================== +-- /pollself [question] +-- asks the user a question specified in param +minetest.register_chatcommand( + "pollself", { + privs = {}, + func = function(user, param) + modpol.binary_poll_user(user, param) + return true, result + end, +}) + +-- =================================================================== +-- Minetest events +-- =================================================================== + +-- =================================================================== -- Receiving fields minetest.register_on_player_receive_fields(function (player, formname, fields) -- modpol:poll @@ -258,42 +160,5 @@ minetest.register_on_player_receive_fields(function (player, formname, fields) end end) --- /pollself [question] --- asks the user a question -minetest.register_chatcommand( - "pollself", { - privs = {}, - func = function(user, param) - modpol.binary_poll_user(user, param) - return true, result - end, -}) - ---[[ TKTK need to enable more complex ineractions - - checkboxes, radio - - write-in -]]-- - --- MESSAGE FUNCTIONS - -function modpol.org_message(org, speaker, message) - -- If org doesn't exit, broadcast to all - -- If org doesn't exist, don't broadcast - -- use: minetest.chat_send_player("player1", "This is a chat message for player1") -end --- register at chat command for this - - --- Add HUD interface that shows status: orgs, privileges https://rubenwardy.com/minetest_modding_book/en/players/hud.html - --- toggle it on and off - - - ---[[ INITIALIZING: post-functions ]]-- - --- create instance if not present -if (modpol.orgs["instance"] == nil) then - modpol.new_org("instance", modpol.list_members()) -end --- TKTK does this need to be on_joinplayer? still isn't adding the singleplayer +-- =================================================================== +-- End of file. diff --git a/interactions.lua b/interactions.lua new file mode 100644 index 0000000..3f15271 --- /dev/null +++ b/interactions.lua @@ -0,0 +1,25 @@ +-- =================================================================== +-- /orgs.lua +-- User interaction functions for Modular Politics +-- Called by modpol.lua + +-- =================================================================== +-- Function: modpol.binary_poll_user(user, question) +-- Params: user (string), question (string) +-- Output: +-- presents a yes/no/abstain poll to a user, returns answer +modpol.binary_poll_user = function(user, question) + local query = "Poll for " .. user .. " (y/n/a): ".. question + local answer + repeat + print(query) + answer= io.read() + until answer == "y" or answer == "n" or answer == "a" + if answer == "y" then + return "Yes" + elseif answer == "n" then + return "No" + else + return "Abstain" + end +end diff --git a/mod.conf b/mod.conf index fa46be3..c47e939 100644 --- a/mod.conf +++ b/mod.conf @@ -1,7 +1,8 @@ name = modpol +depends = default title = Modular Politics author = ntnsndr -description = A governance layer API for Minetest +description = A governance layer framework in Lua license = Hippocratic License forum = TBA version = 0.1 diff --git a/modpol.lua b/modpol.lua new file mode 100644 index 0000000..cc8a170 --- /dev/null +++ b/modpol.lua @@ -0,0 +1,77 @@ +-- =================================================================== +-- /modpol.lua +-- Modular Politics (modpol) core + +-- =================================================================== +-- Basic tables + +-- Main API table +modpol = { +} + +-- Table for all active governance data +modpol.orgs = { +} + +-- Record of every state change should appear here +modpol.ledger = { +} + +-- =================================================================== +-- Locate framework top-level directory. + +-- This function is intended for use under Linux and/or UNIX only. It +-- returns a relative or absolute path for the framework top-level +-- directory without a trailing slash. + +local get_script_dir = function() + local str = debug.getinfo (2, "S").source:sub (2) + str = str:match ("(.*/)") or "." + str = str:gsub ("/$", "", 1) + return str +end + +-- Absolute or relative path to script directory. +local topdir = get_script_dir() +modpol.topdir = topdir +print (topdir) + +-- =================================================================== +-- Load dependencies + +-- OldCoder utilities +dofile (topdir .. "/ocutil.lua") + +-- =================================================================== +-- Persistent storage +-- must implement modpol.load_storage() and modpol.store_data() + +-- Select a storage method +-- Works with CLI: +dofile (topdir .. "/storage-local.lua") +-- Works with Minetest 5: +-- dofile (topdir .. "/storage-mod_storage.lua") + +-- If available, load persistent storage into active tables +modpol.load_storage() + +-- =================================================================== +-- ModPol core features + +dofile (topdir .. "/users.lua") +dofile (topdir .. "/orgs.lua") +dofile (topdir .. "/interactions.lua") +-- messaging functions + +-- =================================================================== +-- Final checks + +-- create instance if not present +if (modpol.orgs["instance"] == nil) then + modpol.add_org("instance", modpol.list_users()) +end + +ocutil.log ("modpol loaded") + +-- =================================================================== +-- End of file. diff --git a/ocutil.lua b/ocutil.lua new file mode 100644 index 0000000..7d4e3f1 --- /dev/null +++ b/ocutil.lua @@ -0,0 +1,743 @@ +-- =================================================================== +-- Overview. + +-- This file is "top/ocutil.lua". + +-- This file provides a collection of largely portable Lua utility +-- functions. The collection is descended from one assembled by Old- +-- Coder (Robert Kiraly). + +-- =================================================================== + +ocutil = {} + +ocutil.log_console = true -- Flag: Copy log to console +ocutil.logdir = nil -- Absolute path for log-file direc- + -- tory (or nil) + +-- =================================================================== + +-- Function: ocutil.fixnil +-- Params: string s +-- Outputs: +-- +-- If the string is nil , returns "(nil)" +-- if the string is empty , returns "(empty)" +-- Otherwise, returns the string as it stands +-- +-- This function can be used to clarify -- and/or avoid crashes in -- +-- debug messages. + +-- =================================================================== + +ocutil.fixnil = function (s) + if s == nil then s = "(nil)" + elseif s == "" then s = "(empty)" + end + return s +end + +-- =================================================================== + +-- Function: ocutil.setlogdir +-- Params: string path +-- Outputs: +-- +-- The input string should be the absolute path for a writable direc- +-- tory that already exists. This function tells "ocutil.log" to put +-- log files in that directory. + +-- =================================================================== + +ocutil.setlogdir = function (path) + if path ~= nil and path ~= "" then + ocutil.logdir = path + end +end + +-- =================================================================== + +-- Function: ocutil.setlogname +-- Params: string name +-- Outputs: +-- +-- The input string should be a filename without a path component. +-- This function tells "ocutil.log" to use the specified filename for +-- the main log file. +-- +-- "ocutil.setlogdir" must be called separately to set the log-file +-- directory. + +-- =================================================================== + +ocutil.setlogname = function (name) + if name ~= nil and name ~= "" then + ocutil.logname = name + end +end + +-- =================================================================== + +-- Function: ocutil.log +-- Params: string s +-- Outputs: +-- +-- Logs the specified string. Appends a newline if not already pre- +-- sent. + +-- =================================================================== + +ocutil.log = function (s) + s = ocutil.fixnil (s) + s = s:gsub ("\n$", "", 1) -- Remove trailing newline initially + + if ocutil.log_console then + print (s) + end + + if ocutil.logdir ~= nil and + ocutil.logname ~= nil then + s = s .. "\n" -- Add trailing newline + + local logpath = ocutil.logdir .. "/" .. ocutil.logname + ocutil.file_append (logpath, s) + end +end + +-- =================================================================== + +-- Function: ocutil.fatal_error +-- Params: string s +-- Outputs: +-- +-- Logs "Fatal error: " plus the specified string. Appends a newline +-- if not already present. Terminates the program. + +-- =================================================================== + +ocutil.fatal_error = function (s) + ocutil.log ("Fatal Error: " .. s) + os.exit (1) + return nil -- Shouldn't be reached +end + +-- =================================================================== + +-- Function: ocutil.panic +-- Params: string s +-- Outputs: +-- +-- Logs "Internal error: " plus the specified string. Appends a new- +-- line if not already present. Terminates the program. + +-- =================================================================== + +ocutil.panic = function (s) + ocutil.log ("Internal Error: " .. s) + os.exit (1) + return nil -- Shouldn't be reached +end + +-- =================================================================== + +-- Function: ocutil.str_empty +-- Params: string x +-- Outputs: +-- +-- Returns true if the string is nil or empty +-- Returns false otherwise + +-- =================================================================== + +ocutil.str_empty = function (x) + if x == nil or x == "" then + return true + else + return false + end +end + +-- =================================================================== + +-- Function: ocutil.str_nonempty +-- Params: string x +-- Outputs: +-- +-- Returns true if the string is nil or empty +-- Returns false otherwise + +-- =================================================================== + +ocutil.str_nonempty = function (x) + if x == nil or x == "" then + return false + else + return true + end +end + +-- =================================================================== + +-- Function: ocutil.table_empty +-- Params: table tab +-- Outputs: +-- +-- Returns true if the table is empty +-- Returns false otherwise + +-- =================================================================== + +ocutil.table_empty = function (tab) + local next = next + if next (tab) == nil then return true end + return false +end + +-- =================================================================== + +-- Function: ocutil.table_nonempty +-- Params: table tab +-- Outputs: +-- +-- Returns false if the table is empty +-- Returns true otherwise + +-- =================================================================== + +ocutil.table_nonempty = function (tab) + if ocutil.table_empty (tab) then return false end + return true +end + +-- =================================================================== + +-- Function: ocutil.string_contains +-- Params: strings a, b +-- Outputs: +-- +-- Returns true if the 1st string contains the 2nd one +-- Returns false otherwise + +-- =================================================================== + +ocutil.str_contains = function (a, b) + if string.match (a, b) then + return true + else + return false + end +end + +-- =================================================================== + +-- Function: ocutil.str_false +-- Params: string x +-- Outputs: +-- +-- This function checks the string for an explicit false (or equiva- +-- lent) setting (as opposed to empty or nil). If such a setting is +-- found, true is returned. Otherwise, false is returned. + +-- =================================================================== + +ocutil.str_false = function (x) + if x == "false" or x == "no" or + x == "off" or x == "0" or + x == false or x == 0 then + return true + else + return false + end +end + +-- =================================================================== + +-- Function: ocutil.str_true +-- Params: string x +-- Outputs: +-- +-- This function checks the string for an explicit true (or equiva- +-- lent) setting. If such a setting is found, true is returned. Other- +-- wise, false is returned. + +-- =================================================================== + +ocutil.str_true = function (x) + if x == "true" or x == "yes" or + x == "on" or x == "1" or + x == true or x == 1 then + return true + else + return false + end +end + +-- =================================================================== + +-- Function: ocutil.starts_with +-- Params: strings String, Start +-- Outputs: +-- +-- Returns true if the 1st string starts with the 2nd one +-- Returns false otherwise + +-- =================================================================== + +ocutil.starts_with = function (String, Start) + if string.sub (String, 1, string.len (Start)) == Start then + return true + else + return false + end +end + +-- =================================================================== + +-- Function: ocutil.not_starts_with +-- Params: strings String, Start +-- Outputs: +-- +-- Returns false if the 1st string starts with the 2nd one +-- Returns true otherwise + +-- =================================================================== + +ocutil.not_starts_with = function (String, Start) + if string.sub (String, 1, string.len (Start)) == Start then + return false + else + return true + end +end + +-- =================================================================== + +-- Function: ocutil.ends_with +-- Params: strings String, End +-- Outputs: +-- +-- Returns true if the 1st string ends with the 2nd one +-- Returns false otherwise +-- As a special case, returns true if the 2nd string is empty + +-- =================================================================== + +ocutil.ends_with = function (String, End) + return End == '' or string.sub (String, + -string.len (End)) == End +end + +-- =================================================================== + +-- Function: ocutil.not_ends_with +-- Params: strings String, End +-- Outputs: +-- Returns false if the 1st string ends with the 2nd one +-- Returns true otherwise +-- As a special case, returns false if the 2nd string is empty + +-- =================================================================== + +ocutil.not_ends_with = function (String, End) + if ocutil.ends_with (String, End) then + return false + else + return true + end +end + +-- =================================================================== + +-- Function: ocutil.firstch +-- Params: string str +-- Outputs: +-- Returns the 1st character of the string + +-- =================================================================== + +ocutil.firstch = function (str) + return string.sub (str, 1, 1) +end + +-- =================================================================== + +-- Function: ocutil.first_to_upper +-- Params: string str +-- Outputs: +-- Returns the 1st character of the string in upper case + +-- =================================================================== + +ocutil.first_to_upper = function (str) + return (str:gsub ("^%l", string.upper)) +end + +-- =================================================================== + +-- Function: ocutil.all_first_to_upper +-- Params: string str, flag cvtspace +-- Outputs: +-- +-- Converts the 1st character of each word in the string to upper +-- case and returns the result. Words are delimited by spaces or un- +-- derscores. If the flag is true, also replaces spaces with under- +-- scores. + +-- =================================================================== + +ocutil.all_first_to_upper = function (str, cvtspace) + + str = str:gsub ("^%l", string.upper) + str = str:gsub ("[_ ]%l", + function (a) return string.upper (a) end) + + if ocutil.str_true (cvtspace) then + str = str:gsub ("_", " ") + end + + return (str) +end + +-- =================================================================== + +-- Function: ocutil.strtok +-- Params: strings source, delimitch +-- Outputs: +-- +-- Breaks the source string up into a list of words and returns the +-- list. +-- +-- If "delimitch" is omitted, nil, or empty, words are delimited by +-- spaces. Otherwise, words are delimited by any of the characters in +-- "delimitch". + +-- =================================================================== + +ocutil.strtok = function (source, delimitch) + if delimitch == nil or + delimitch == "" then delimitch = " " end + + local parts = {} + local pattern = '([^' .. delimitch .. ']+)' + + string.gsub (source, pattern, + function (value) + value = value:gsub ("^ *", "") + value = value:gsub (" *$", "") + parts [#parts + 1] = value + end) + + return parts +end + +-- =================================================================== + +-- Function: ocutil.swap_key_value +-- Params: table itable +-- Outputs: +-- Turns keys into values and vice versa and returns the resulting +-- table. + +-- =================================================================== + +ocutil.swap_key_value = function (itable) + if itable == nil then return nil end + local otable = {} + for key, value in pairs (itable) do otable [value] = key end + return otable +end + +-- =================================================================== + +-- Function: ocutil.ktable_to_vtable +-- Params: table ktable +-- Outputs: +-- +-- "ktable_to_vtable" is short for "convert key table to value table". +-- +-- The input table is assumed to be an arbitrary key-based list of the +-- general form: +-- { dog="woof", apple="red", book="read" } +-- +-- This function takes the keys and returns them as an integer-indexed +-- array with the former keys stored now as values. The array is sort- +-- ed based on the former keys. The original values in the input table +-- are discarded. Sample output: +-- +-- { "apple", "book", "dog", ... } + +-- Here's a complete code fragment which demonstrates operation of +-- this function: + +-- local tabby = { dog="woof", apple="red", book="read" } +-- +-- print ("\nInput table:") +-- for ii, value in pairs (tabby) do +-- print (ii .. ") " .. tabby [ii]) +-- end +-- +-- tabby = ocutil.ktable_to_vtable (tabby) +-- +-- print ("\nOutput table:") +-- for ii, value in ipairs (tabby) do +-- print (ii .. ") " .. tabby [ii]) +-- end + +-- =================================================================== + +ocutil.ktable_to_vtable = function (ktable) + local vtable = {} + if ktable == nil then return vtable end + + for key, _ in pairs (ktable) do + table.insert (vtable, key) + end + + table.sort (vtable) + return vtable +end + +-- =================================================================== + +-- Function: ocutil.vtable_to_ktable +-- Params: table vtable, scalar set_to +-- Outputs: +-- +-- "vtable_to_ktable" is short for "convert value table to key table". +-- +-- The input table is assumed to be an integer-indexed array of scalar +-- values. +-- +-- This function creates a general table that holds the scalar values +-- stored now as keys and returns the new table. The new table maps +-- each of the keys to the value specified by "set_to". +-- +-- If "set_to" is omitted or nil, it defaults to boolean true. + +-- Here's a complete code fragment which demonstrates operation of +-- this function: +-- +-- local tabby = { "apple", "book", "dog" } +-- +-- print ("\nInput table:") +-- for ii, value in ipairs (tabby) do +-- print (ii .. ") " .. tabby [ii]) +-- end +-- +-- tabby = ocutil.vtable_to_ktable (tabby, 42) +-- +-- print ("\nOutput table:") +-- for ii, value in pairs (tabby) do +-- print (ii .. ") " .. tabby [ii]) +-- end + +-- =================================================================== + +ocutil.vtable_to_ktable = function (vtable, set_to) + + local ktable = {} + if vtable == nil then return ktable end + if set_to == nil then set_to = true end + + for _, value in pairs (vtable) do + ktable [value] = set_to + end + + return ktable +end + +-- =================================================================== + +-- Function: ocutil.make_set +-- Params: array harry +-- Outputs: +-- +-- This function makes a set out of an array. Specifically, it returns +-- a new table with the array's values as keys. The new table maps +-- each key to boolean true. +-- +-- Here's a complete code fragment which demonstrates operation of +-- this function: +-- +-- local color_set = ocutil.make_set { "red", "green", "blue" } +-- if color_set ["red"] ~= nil then print ("Supported color") end + +-- =================================================================== + +ocutil.make_set = function (list) + local set = {} + for _, l in ipairs (list) do set [l] = true end + return set +end + +-- =================================================================== + +-- Function: ocutil.pos_to_str +-- Params: position-table pos +-- Outputs: +-- +-- This function takes a table of the following form as input: +-- { x=25.0, y=-135.5, z=2750.0 } +-- +-- It returns the x-y-z values, separated by commas, as a string. + +-- =================================================================== + +ocutil.pos_to_str = function (pos) + return pos.x .. "," .. pos.y .. "," .. pos.z +end + +-- =================================================================== + +-- Function: ocutil.table_length +-- Params: table tabby +-- Outputs: Returns number of entries in table +-- +-- Note: This function works for tables in general as well as for int- +-- eger-indexed tables (i.e., arrays). + +-- =================================================================== + +ocutil.table_length = function (tabby) + local count = 0 + for _ in pairs (tabby) do count = count+1 end + return count +end + +-- =================================================================== + +-- Function: ocutil.clone_table +-- Params: table tabby +-- Outputs: +-- +-- This function returns an independent clone of the input table. It +-- should work correctly for most cases, but special cases that re- +-- quire additional work may exist. +-- +-- This function may also be called as "ocutil.table_clone". + +-- =================================================================== + +ocutil.clone_table = function (tabby) + if tabby == nil then return nil end + local copy = {} + for k, v in pairs (tabby) do + if type (v) == 'table' then v = ocutil.clone_table (v) end + copy [k] = v + end + return copy +end + +ocutil.table_clone = ocutil.clone_table + +-- =================================================================== + +-- Function: ocutil.file_exists +-- Params: string path +-- Outputs: +-- +-- The input string should be the relative or absolute pathname for a +-- data file. If the file is read-accessible, this function returns +-- true. Otherwise, it returns false. + +-- =================================================================== + +ocutil.file_exists = function (path) + local file, err + file, err = io.open (path, "rb") + if err then return false end + file:close() + return true +end + +-- =================================================================== + +-- Function: ocutil.file_missing +-- Params: string path +-- Outputs: +-- +-- The input string should be the relative or absolute pathname for a +-- data file. If the file is read-accessible, this function returns +-- false. Otherwise, it returns true. + +-- =================================================================== + +ocutil.file_missing = function (path) + if ocutil.file_exists (path) then return false end + return true +end + +-- =================================================================== + +-- Function: ocutil.file_read +-- Params: string path +-- Outputs: +-- +-- The input string should be the absolute pathname for a data file. +-- If the file is read-accessible, this function returns the contents +-- of the file. Otherwise, it returns nil. +-- +-- Warning: There is presently no check for a maximum file size. + +-- =================================================================== + +ocutil.file_read = function (path) + local file, err + file, err = io.open (path, "rb") + if err then return nil end + local data = file:read ("*a") + file:close() + return data +end + +-- =================================================================== + +-- Function: ocutil.file_write +-- Params: strings path, data +-- Outputs: +-- +-- The 1st string should be the absolute pathname for a data file. The +-- file doesn't need to exist initially but the directory that will +-- contain it does need to exist. +-- +-- This function writes the 2nd string to the file. Existing stored +-- data is discarded. +-- +-- This function returns true if successful and false otherwise. + +-- =================================================================== + +ocutil.file_write = function (path, data) + local file, err + file, err = io.open (path, "wb") + if err then return false end + io.output (file) + io.write (data) + io.close (file) + return true +end + +-- =================================================================== + +-- Function: ocutil.file_append +-- Params: strings path, data +-- Outputs: +-- +-- This function is identical to "ocutil.file_write" except for one +-- difference: It appends to existing files as opposed to overwrites +-- them. + +-- =================================================================== + +ocutil.file_append = function (path, data) + local file, err + file, err = io.open (path, "ab") + if err then return false end + io.output (file) + io.write (data) + io.close (file) + return true +end + +-- =================================================================== +-- End of file. diff --git a/orgs.lua b/orgs.lua new file mode 100644 index 0000000..25f8081 --- /dev/null +++ b/orgs.lua @@ -0,0 +1,192 @@ +-- =================================================================== +-- /orgs.lua +-- Org-related functions for Modular Politics +-- Called by modpol.lua + +-- =================================================================== +-- Function: modpol.record +-- Params: strings msg, org +-- Outputs: +-- +-- "msg" specifies an event and/or status message. +-- "org" specifies an "org" name. +-- +-- This function adds the message to a global ledger and, if "org" +-- specifies a valid "org", to an "org"-specific ledger. Both the mem- +-- ory-resident and on-disk copies of the data structures used are up- +-- dated. + +modpol.record = function (org_name, msg) + -- Record in global ledger + table.insert (modpol.ledger, msg) + -- Record in "org"-specific ledger + if modpol.orgs [org_name] ~= nil then + local org_ledger = modpol.orgs [org_name]["ledger"] + if org_ledger == nil then + modpol.orgs [org_name]["ledger"] = { msg } + else + modpol.orgs [org_name]["ledger"] = + table.insert (org_ledger, msg) + end + end + + modpol.store_data() -- Copy data to disk +end + +-- =================================================================== +-- Function: modpol.add_org +-- Params: string name, table members +-- Output: +-- +-- This function creates an "org". It returns either an error message +-- starting with "-!-" or a success message. +-- +-- The string parameter specifies the "org" name. +-- +-- The specified table should be an integer-indexed array of member +-- names. + +modpol.add_org = function (org_name, members) + local str + + if ocutil.str_empty (org_name) then + return "Error: Org needs a name" + end + + ocutil.log ("Adding org " .. org_name) + if modpol.orgs [org_name] ~= nil then + str = "Error: Org " .. org_name .. " already exists" + ocutil.log (str) + return str + end + + modpol.orgs [org_name] = { members=members } + local msg = "New org: " .. org_name .. + " (" .. table.concat (members, ", ") .. ")" + + modpol.record (org_name, msg) + return msg +end + +-- =================================================================== +-- Function: modpol.list_orgs +-- Params: None +-- Output: +-- This function returns a text-format list of "orgs". The list shows +-- one "org" per line in the following format: +-- org_name (member, member, ...) +-- If there are no "orgs", the output is an empty string. + +modpol.list_orgs = function() + local outbuf = "" + local str + for org_name, org_data in pairs (modpol.orgs) do + -- Process next "org" + -- Build string version of member list + local memcat = org_data ["members"] + if ocutil.str_empty (memcat) then + memcat = "(empty)" + else + memcat = "(" .. table.concat (memcat, ", ") .. ")" + end + -- Build output string + outbuf = outbuf .. org_name .. " " .. memcat .. "\n" + end + return outbuf +end + +-- =================================================================== +-- Function: modpol.reset_orgs +-- Params: None +-- Removes all orgs and recreates blank org "instance" with all +-- current users +-- returns confirmation message + +modpol.reset_orgs = function() + local users = list_users() + modpol.orgs = {} + local message = "Orgs purged" + modpol.record(nil, message) + modpol.add_org("instance", users) + return message +end + +-- =================================================================== +-- Function: modpol.add_member +-- Params: org (string), member (string) +-- Output: +-- Adds the specified member to the specified org +-- Returns confirmation or error message + +modpol.add_member = function(org, member) + if (modpol.orgs[org] == nil) then + return "Error: No such org" + else + table.insert(modpol.orgs[org]["members"], member) + local message = member .. " added to org " .. org + modpol.record(message, org) + return message + end +end + +-- =================================================================== +-- Function: modpol.is_member +-- Params: org (string), member (string) +-- Output: Boolean depending on membership or nil/error message + +modpol.is_member = function(org, member) + if (modpol.orgs[org] == nil) then + return nil, "Error: No such org" + else + local member_table = modpol.orgs[org]["members"] + if member_table then + for index, value in pairs(member_table) do + if value == member then + -- match found + return true + end + end + -- no match found + return false + end + return false + end +end + +-- =================================================================== +-- Function: modpol.remove_member +-- Params: org (string), member (string) +-- Output: +-- Removes the specified member from the specified org +-- Returns confirmation or error message + +function modpol.remove_member(org, member) + local message = "Error: No such org" + if (modpol.orgs[org] == nil) then + return nil, message + else + local member_table = modpol.orgs[org]["members"] + if member_table then + for index, value in pairs(member_table) do + if value == member then + -- match found, set to nil + value = nil + message = member .. " removed from org " .. org + return message + end + end + end + -- no match found (or org has no members) + message = member .. " not found in org " .. org + return nil, message + end +end + +-- =================================================================== +-- TKTK: +-- + rename_org(old_name, new_name) +-- + + + +-- =================================================================== +-- End of file. diff --git a/serpent/LICENSE.txt b/serpent/LICENSE.txt new file mode 100644 index 0000000..a4c529c --- /dev/null +++ b/serpent/LICENSE.txt @@ -0,0 +1,21 @@ +Serpent source is released under the MIT License + +Copyright (c) 2012-2018 Paul Kulchenko (paul@kulchenko.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/serpent/README.md b/serpent/README.md new file mode 100644 index 0000000..ca49ea5 --- /dev/null +++ b/serpent/README.md @@ -0,0 +1,293 @@ +# Serpent + +Lua serializer and pretty printer. + +## Features + +* Human readable: + * Provides single-line and multi-line output. + * Nested tables are properly indented in the multi-line output. + * Numerical keys are listed first. + * Keys are (optionally) sorted alphanumerically. + * Array part skips keys (`{'a', 'b'}` instead of `{[1] = 'a', [2] = 'b'}`). + * `nil` values are included when expected (`{1, nil, 3}` instead of `{1, [3]=3}`). + * Keys use short notation (`{foo = 'foo'}` instead of `{['foo'] = 'foo'}`). + * Shared references and self-references are marked in the output. +* Machine readable: provides reliable deserialization using `loadstring()`. +* Supports deeply nested tables. +* Supports tables with self-references. +* Keeps shared tables and functions shared after de/serialization. +* Supports function serialization using `string.dump()`. +* Supports serialization of global functions. +* Supports `__tostring` and `__serialize` metamethods. +* Escapes new-line `\010` and end-of-file control `\026` characters in strings. +* Configurable with options and custom formatters. + +## Usage + +```lua +local serpent = require("serpent") +local a = {1, nil, 3, x=1, ['true'] = 2, [not true]=3} +a[a] = a -- self-reference with a table as key and value + +print(serpent.dump(a)) -- full serialization +print(serpent.line(a)) -- single line, no self-ref section +print(serpent.block(a)) -- multi-line indented, no self-ref section + +local fun, err = loadstring(serpent.dump(a)) +if err then error(err) end +local copy = fun() + +-- or using serpent.load: +local ok, copy = serpent.load(serpent.dump(a)) +print(ok and copy[3] == a[3]) +``` + +## Functions + +Serpent provides three functions that are shortcuts to the same +internal function, but set different options by default: + +* `dump(a[, {...}])` -- full serialization; sets `name`, `compact` and `sparse` options; +* `line(a[, {...}])` -- single line pretty printing, no self-ref section; sets `sortkeys` and `comment` options; +* `block(a[, {...}])` -- multi-line indented pretty printing, no self-ref section; sets `indent`, `sortkeys`, and `comment` options. + +Note that `line` and `block` functions return pretty-printed data structures and if you want to deserialize them, you need to add `return` before running them through `loadstring`. +For example: `loadstring('return '..require('mobdebug').line("foo"))() == "foo"`. + +While you can use `loadstring` or `load` functions to load serialized fragments, Serpent also provides `load` function that adds safety checks and reports an error if there is any executable code in the fragment. + +* `ok, res = serpent.load(str[, {safe = true}])` -- loads serialized fragment; you need to pass `{safe = false}` as the second value if you want to turn safety checks off. + +Similar to `pcall` and `loadstring` calls, `load` returns status as the first value and the result or the error message as the second value. + +## Options + +* indent (string) -- indentation; triggers long multi-line output. +* comment (true/false/maxlevel) -- provide stringified value in a comment (up to `maxlevel` of depth). +* sortkeys (true/false/function) -- sort keys. +* sparse (true/false) -- force sparse encoding (no nil filling based on `#t`). +* compact (true/false) -- remove spaces. +* fatal (true/False) -- raise fatal error on non-serilizable values. +* nocode (true/False) -- disable bytecode serialization for easy comparison. +* nohuge (true/False) -- disable checking numbers against undefined and huge values. +* maxlevel (number) -- specify max level up to which to expand nested tables. +* maxnum (number) -- specify max number of elements in a table. +* maxlength (number) -- specify max length for all table elements. +* metatostring (True/false) -- use `__tostring` metamethod when serializing tables (**v0.29**); +set to `false` to disable and serialize the table as is, even when `__tostring` is present. +* numformat (string; "%.17g") -- specify format for numeric values as shortest possible round-trippable double (**v0.30**). +Use "%.16g" for better readability and "%.17g" (the default value) to preserve floating point precision. +* valignore (table) -- allows to specify a list of values to ignore (as keys). +* keyallow (table) -- allows to specify the list of keys to be serialized. +Any keys not in this list are not included in final output (as keys). +* keyignore (table) -- allows to specity the list of keys to ignore in serialization. +* valtypeignore (table) -- allows to specify a list of value *types* to ignore (as keys). +* custom (function) -- provide custom output for tables. +* name (string) -- name; triggers full serialization with self-ref section. + +These options can be provided as a second parameter to Serpent functions. + +```lua +block(a, {fatal = true}) +line(a, {nocode = true, valignore = {[arrayToIgnore] = true}}) +function todiff(a) return dump(a, {nocode = true, indent = ' '}) end +``` + +Serpent functions set these options to different default values: + +* `dump` sets `compact` and `sparse` to `true`; +* `line` sets `sortkeys` and `comment` to `true`; +* `block` sets `sortkeys` and `comment` to `true` and `indent` to `' '`. + +## Metatables with __tostring and __serialize methods + +If a table or a userdata value has `__tostring` or `__serialize` method, the method will be used to serialize the value. +If `__serialize` method is present, it will be called with the value as a parameter. +if `__serialize` method is not present, but `__tostring` is, then `tostring` will be called with the value as a parameter. +In both cases, the result will be serialized, so `__serialize` method can return a table, that will be serialize and replace the original value. + +## Sorting + +A custom sort function can be provided to sort the contents of tables. The function takes 2 parameters, the first being the table (a list) with the keys, the second the original table. It should modify the first table in-place, and return nothing. +For example, the following call will apply a sort function identical to the standard sort, except that it will not distinguish between lower- and uppercase. + +```lua +local mysort = function(k, o) -- k=keys, o=original table + local maxn, to = 12, {number = 'a', string = 'b'} + local function padnum(d) return ("%0"..maxn.."d"):format(d) end + local sort = function(a,b) + -- this -vvvvvvvvvv- is needed to sort array keys first + return ((k[a] and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum))):upper() + < ((k[b] and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum))):upper() + end + table.sort(k, sort) +end + +local content = { some = 1, input = 2, To = 3, serialize = 4 } +local result = require('serpent').block(content, {sortkeys = mysort}) +``` + +## Formatters + +Serpent supports a way to provide a custom formatter that allows to fully +customize the output. The formatter takes four values: + +* tag -- the name of the current element with '=' or an empty string in case of array index, +* head -- an opening table bracket `{` and associated indentation and newline (if any), +* body -- table elements concatenated into a string using commas and indentation/newlines (if any), +* tail -- a closing table bracket `}` and associated indentation and newline (if any), and +* level -- the current level. + +For example, the following call will apply +`Foo{bar} notation to its output (used by Metalua to display ASTs): + +```lua +print((require "serpent").block(ast, {comment = false, custom = + function(tag,head,body,tail) + local out = head..body..tail + if tag:find('^lineinfo') then + out = out:gsub("\n%s+", "") -- collapse lineinfo to one line + elseif tag == '' then + body = body:gsub('%s*lineinfo = [^\n]+', '') + local _,_,atag = body:find('tag = "(%w+)"%s*$') + if atag then + out = "`"..atag..head.. body:gsub('%s*tag = "%w+"%s*$', '')..tail + out = out:gsub("\n%s+", ""):gsub(",}","}") + else out = head..body..tail end + end + return tag..out + end})) +``` + +## Limitations + +* Doesn't handle userdata (except filehandles in `io.*` table). +* Threads, function upvalues/environments, and metatables are not serialized. + +## Performance + +A simple performance test against `serialize.lua` from metalua, `pretty.write` +from Penlight, and `tserialize.lua` from lua-nucleo is included in `t/bench.lua`. + +These are the results from one of the runs: + +* nucleo (1000): 0.256s +* metalua (1000): 0.177s +* serpent (1000): 0.22s +* serpent (1000): 0.161s -- no comments, no string escapes, no math.huge check +* penlight (1000): 0.132s + +Serpent does additional processing to escape `\010` and `\026` characters in +strings (to address http://lua-users.org/lists/lua-l/2007-07/msg00362.html, +which is already fixed in Lua 5.2) and to check all numbers for `math.huge`. +The seconds number excludes this processing to put it on an equal footing +with other modules that skip these checks (`nucleo` still checks for `math.huge`). + +## Author + +Paul Kulchenko (paul@kulchenko.com) + +## License + +See LICENSE file. + +## History + +### v0.30 (Sep 01 2017) + - Updated `pairs` to avoid using `__pairs` in Lua 5.2+. + - Added `metatostring` option to disable `__tostring` processing during serialization. + - Added `level` to the parameters of the custom function (closes #25). + - Added `maxlength` option to limit the space taken by table elements. + - Optimized serialization of functions when `nocode` option is specified. + - Added protection from `__serialize` check failing (pkulchenko/ZeroBraneStudio#732). + - Added `keyignore` option for the serializer. + - Added check for environments that may not have 'default' tables as tables. + - Added numeric format to preserve floating point precision (closes #17). + - Added using `debug.metatable` when available. + - Improved handling of failures in `__tostring` (pkulchenko/ZeroBraneStudio#446). + +### v0.28 (May 06 2015) + - Switched to a method proposed by @SoniEx2 to disallow function calls (#15). + - Added more `tostring` for Lua 5.3 support (pkulchenko/ZeroBraneStudio#401). + - Updated environment handling to localize the impact (#15). + - Added setting env to protect against assigning global functions (closes #15). + - Updated tests to work with Lua 5.3. + - Added explicit `tostring` for Lua 5.3 with `LUA_NOCVTN2S` set (pkulchenko/ZeroBraneStudio#401). + - Fixed crash when not all Lua standard libraries are loaded (thanks to Tommy Nguyen). + - Improved Lua 5.2 support for serialized functions. + +### v0.27 (Jan 11 2014) + - Fixed order of elements in the array part with `sortkeys=true` (fixes #13). + - Updated custom formatter documentation (closes #11). + - Added `load` function to deserialize; updated documentation (closes #9). + +### v0.26 (Nov 05 2013) + - Added `load` function that (safely) loads serialized/pretty-printed values. + - Updated documentation. + +### v0.25 (Sep 29 2013) + - Added `maxnum` option to limit the number of elements in tables. + - Optimized processing of tables with numeric indexes. + +### v0.24 (Jun 12 2013) + - Fixed an issue with missing numerical keys (fixes #8). + - Fixed an issue with luaffi that returns `getmetatable(ffi.C)` as `true`. + +### v0.23 (Mar 24 2013) + - Added support for `cdata` type in LuaJIT (thanks to [Evan](https://github.com/neomantra)). + - Added comment to indicate incomplete output. + - Added support for metatables with __serialize method. + - Added handling of metatables with __tostring method. + - Fixed an issue with having too many locals in self-reference section. + - Fixed emitting premature circular reference in self-reference section, which caused invalid serialization. + - Modified the sort function signature to also pass the original table, so not only keys are available when sorting, but also the values in the original table. + +### v0.22 (Jan 15 2013) + - Added ability to process __tostring results that may be non-string values. + +### v0.21 (Jan 08 2013) + - Added `keyallow` and `valtypeignore` options (thanks to Jess Telford). + - Renamed `ignore` to `valignore`. + +### v0.19 (Nov 16 2012) + - Fixed an issue with serializing shared functions as keys. + - Added serialization of metatables using __tostring (when present). + +### v0.18 (Sep 13 2012) + - Fixed an issue with serializing data structures with circular references that require emitting temporary variables. + - Fixed an issue with serializing keys pointing to shared references. + - Improved overall serialization logic to inline values when possible. + +### v0.17 (Sep 12 2012) + - Fixed an issue with serializing userdata that doesn't provide tostring(). + +### v0.16 (Aug 28 2012) + - Removed confusing --[[err]] comment from serialized results. + - Added a short comment to serialized functions when the body is skipped. + +### v0.15 (Jun 17 2012) + - Added `ignore` option to allow ignoring table values. + - Added `comment=num` option to set the max level up to which add comments. + - Changed all comments (except math.huge) to be controlled by `comment` option. + +### v0.14 (Jun 13 2012) + - Fixed an issue with string keys with numeric values `['3']` getting mixed + with real numeric keys (only with `sortkeys` option set to `true`). + - Fixed an issue with negative and real value numeric keys being misplaced. + +### v0.13 (Jun 13 2012) + - Added `maxlevel` option. + - Fixed key sorting such that `true` and `'true'` are always sorted in + the same order (for a more stable output). + - Removed addresses from names of temporary variables (for stable output). + +### v0.12 (Jun 12 2012) + - Added options to configure serialization process. + - Added `goto` to the list of keywords for Lua 5.2. + - Changed interface to dump/line/block methods. + - Changed `math.huge` to 1/0 for better portability. + - Replaced \010 with \n for better readability. + +### v0.10 (Jun 03 2012) + - First public release. diff --git a/serpent/serpent-git.tar.bz2 b/serpent/serpent-git.tar.bz2 new file mode 100644 index 0000000..73cb87d Binary files /dev/null and b/serpent/serpent-git.tar.bz2 differ diff --git a/serpent/serpent.lua b/serpent/serpent.lua new file mode 100644 index 0000000..17e1884 --- /dev/null +++ b/serpent/serpent.lua @@ -0,0 +1,148 @@ +local n, v = "serpent", "0.302" -- (C) 2012-18 Paul Kulchenko; MIT License +local c, d = "Paul Kulchenko", "Lua serializer and pretty printer" +local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'} +local badtype = {thread = true, userdata = true, cdata = true} +local getmetatable = debug and debug.getmetatable or getmetatable +local pairs = function(t) return next, t end -- avoid using __pairs in Lua 5.2+ +local keyword, globals, G = {}, {}, (_G or _ENV) +for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false', + 'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', + 'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end +for k,v in pairs(G) do globals[v] = k end -- build func to name mapping +for _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) do + for k,v in pairs(type(G[g]) == 'table' and G[g] or {}) do globals[v] = g..'.'..k end end + +local function s(t, opts) + local name, indent, fatal, maxnum = opts.name, opts.indent, opts.fatal, opts.maxnum + local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge + local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge) + local maxlen, metatostring = tonumber(opts.maxlength), opts.metatostring + local iname, comm = '_'..(name or ''), opts.comment and (tonumber(opts.comment) or math.huge) + local numformat = opts.numformat or "%.17g" + local seen, sref, syms, symn = {}, {'local '..iname..'={}'}, {}, 0 + local function gensym(val) return '_'..(tostring(tostring(val)):gsub("[^%w]",""):gsub("(%d%w+)", + -- tostring(val) is needed because __tostring may return a non-string value + function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return tostring(syms[s]) end)) end + local function safestr(s) return type(s) == "number" and tostring(huge and snum[tostring(s)] or numformat:format(s)) + or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026 + or ("%q"):format(s):gsub("\010","n"):gsub("\026","\\026") end + local function comment(s,l) return comm and (l or 0) < comm and ' --[['..select(2, pcall(tostring, s))..']]' or '' end + local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal + and safestr(select(2, pcall(tostring, s))) or error("Can't serialize "..tostring(s)) end + local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r'] + local n = name == nil and '' or name + local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n] + local safe = plain and n or '['..safestr(n)..']' + return (path or '')..(plain and path and '.' or '')..safe, safe end + local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(k, o, n) -- k=keys, o=originaltable, n=padding + local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'} + local function padnum(d) return ("%0"..tostring(maxn).."d"):format(tonumber(d)) end + table.sort(k, function(a,b) + -- sort numeric keys first: k[key] is not nil for numerical keys + return (k[a] ~= nil and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum)) + < (k[b] ~= nil and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end + local function val2str(t, name, indent, insref, path, plainindex, level) + local ttype, level, mt = type(t), (level or 0), getmetatable(t) + local spath, sname = safename(path, name) + local tag = plainindex and + ((type(name) == "number") and '' or name..space..'='..space) or + (name ~= nil and sname..space..'='..space or '') + if seen[t] then -- already seen this element + sref[#sref+1] = spath..space..'='..space..seen[t] + return tag..'nil'..comment('ref', level) end + -- protect from those cases where __tostring may fail + if type(mt) == 'table' and metatostring ~= false then + local to, tr = pcall(function() return mt.__tostring(t) end) + local so, sr = pcall(function() return mt.__serialize(t) end) + if (to or so) then -- knows how to serialize itself + seen[t] = insref or spath + t = so and sr or tr + ttype = type(t) + end -- new value falls through to be serialized + end + if ttype == "table" then + if level >= maxl then return tag..'{}'..comment('maxlvl', level) end + seen[t] = insref or spath + if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty + if maxlen and maxlen < 0 then return tag..'{}'..comment('maxlen', level) end + local maxn, o, out = math.min(#t, maxnum or #t), {}, {} + for key = 1, maxn do o[key] = key end + if not maxnum or #o < maxnum then + local n = #o -- n = n + 1; o[n] is much faster than o[#o+1] on large tables + for key in pairs(t) do if o[key] ~= key then n = n + 1; o[n] = key end end end + if maxnum and #o > maxnum then o[maxnum+1] = nil end + if opts.sortkeys and #o > maxn then alphanumsort(o, t, opts.sortkeys) end + local sparse = sparse and #o > maxn -- disable sparsness if only numeric keys (shorter output) + for n, key in ipairs(o) do + local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse + if opts.valignore and opts.valignore[value] -- skip ignored values; do nothing + or opts.keyallow and not opts.keyallow[key] + or opts.keyignore and opts.keyignore[key] + or opts.valtypeignore and opts.valtypeignore[type(value)] -- skipping ignored value types + or sparse and value == nil then -- skipping nils; do nothing + elseif ktype == 'table' or ktype == 'function' or badtype[ktype] then + if not seen[key] and not globals[key] then + sref[#sref+1] = 'placeholder' + local sname = safename(iname, gensym(key)) -- iname is table for local variables + sref[#sref] = val2str(key,sname,indent,sname,iname,true) end + sref[#sref+1] = 'placeholder' + local path = seen[t]..'['..tostring(seen[key] or globals[key] or gensym(key))..']' + sref[#sref] = path..space..'='..space..tostring(seen[value] or val2str(value,nil,indent,path)) + else + out[#out+1] = val2str(value,key,indent,nil,seen[t],plainindex,level+1) + if maxlen then + maxlen = maxlen - #out[#out] + if maxlen < 0 then break end + end + end + end + local prefix = string.rep(indent or '', level) + local head = indent and '{\n'..prefix..indent or '{' + local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space)) + local tail = indent and "\n"..prefix..'}' or '}' + return (custom and custom(tag,head,body,tail,level) or tag..head..body..tail)..comment(t, level) + elseif badtype[ttype] then + seen[t] = insref or spath + return tag..globerr(t, level) + elseif ttype == 'function' then + seen[t] = insref or spath + if opts.nocode then return tag.."function() --[[..skipped..]] end"..comment(t, level) end + local ok, res = pcall(string.dump, t) + local func = ok and "((loadstring or load)("..safestr(res)..",'@serialized'))"..comment(t, level) + return tag..(func or globerr(t, level)) + else return tag..safestr(t) end -- handle all other types + end + local sepr = indent and "\n" or ";"..space + local body = val2str(t, name, indent) -- this call also populates sref + local tail = #sref>1 and table.concat(sref, sepr)..sepr or '' + local warn = opts.comment and #sref>1 and space.."--[[incomplete output with shared/self-references skipped]]" or '' + return not name and body..warn or "do local "..body..sepr..tail.."return "..name..sepr.."end" +end + +local function deserialize(data, opts) + local env = (opts and opts.safe == false) and G + or setmetatable({}, { + __index = function(t,k) return t end, + __call = function(t,...) error("cannot call functions") end + }) + local f, res = (loadstring or load)('return '..data, nil, nil, env) + if not f then f, res = (loadstring or load)(data, nil, nil, env) end + if not f then return f, res end + if setfenv then setfenv(f, env) end + return pcall(f) +end + +local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end + +if false then +return { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s, + load = deserialize, + dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end, + line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end, + block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = true}, opts)) end } +end + +modpol.serpent.load = deserialize +modpol.serpent.dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end +modpol.serpent.line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end +modpol.serpent.block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = true}, opts)) end diff --git a/storage-local.lua b/storage-local.lua new file mode 100644 index 0000000..d1b0999 --- /dev/null +++ b/storage-local.lua @@ -0,0 +1,117 @@ +-- =================================================================== +-- /storage-local.lua +-- Persistent storage in /data using Serpent Serializer +-- Unix systems only + +-- =================================================================== +-- Set directories and filenames + +modpol.datadir = modpol.topdir .. "/data" +modpol.file_ledger = modpol.datadir .. "/ledger.dat" +modpol.file_orgs = modpol.datadir .. "/orgs.dat" + +os.execute ("mkdir -p " .. modpol.datadir) + +ocutil.setlogdir (modpol.datadir) +ocutil.setlogname ("modpol.log") + +-- =================================================================== +-- Set up the Serpent Serializer functions. + +modpol.serpent = {} +dofile (modpol.topdir .. "/serpent/serpent.lua") + +-- =================================================================== +-- This function stores "ledger" data to disk. + +local store_ledger = function() + local ok = ocutil.file_write (modpol.file_ledger, +modpol.serpent.dump (modpol.ledger)) + + if ok ~= true then + ocutil.fatal_error ("store_data: ledger") + end + + local nn = ocutil.table_length (modpol.ledger) + local str = "entries" + if nn == 1 then str = "entry" end + ocutil.log (nn .. " global ledger entries stored to disk") +end + +-- =================================================================== +-- This function stores "orgs" data to disk. + +local store_orgs = function() + local ok = ocutil.file_write (modpol.file_orgs, +modpol.serpent.dump (modpol.orgs)) + + if ok ~= true then + ocutil.fatal_error ("store_data: orgs") + end + + local nn = ocutil.table_length (modpol.orgs) + local str = "entries" + if nn == 1 then str = "entry" end + ocutil.log (nn .. " orgs stored to disk") +end + +-- =================================================================== +-- This function stores data to disk. + +modpol.store_data = function() + store_ledger() + store_orgs() +end + +-- =================================================================== +-- This function loads "ledger" data from disk. + +local load_ledger = function() + local obj = ocutil.file_read (modpol.file_ledger ) + if obj ~= nil then + local func, err = load (obj) + if err then + ocutil.fatal_error ("load_data: ledger" ) + end + modpol.ledger = func() + + local nn = ocutil.table_length (modpol.ledger) + local str = "entries" + if nn == 1 then str = "entry" end + ocutil.log (nn .. " global ledger entries loaded from disk") + else + ocutil.log ("No stored global ledger data found") + end +end + +-- =================================================================== +-- This function loads "orgs" data from disk. + +local load_orgs = function() + local obj = ocutil.file_read (modpol.file_orgs ) + if obj ~= nil then + local func, err = load (obj) + if err then + ocutil.fatal_error ("load_data: orgs" ) + end + modpol.orgs = func() + + local nn = ocutil.table_length (modpol.orgs) + local str = "entries" + if nn == 1 then str = "entry" end + ocutil.log (nn .. " orgs loaded from disk") + else + ocutil.log ("No stored orgs data found") + end +end + +-- =================================================================== +-- This function loads stored data from disk. + +modpol.load_storage = function() + load_ledger() + load_orgs() +end + +-- =================================================================== +-- End of file. diff --git a/storage-mod_storage.lua b/storage-mod_storage.lua new file mode 100644 index 0000000..8d57b97 --- /dev/null +++ b/storage-mod_storage.lua @@ -0,0 +1,26 @@ +-- =================================================================== +-- /storage-mod_storage.lua +-- Persistent storage via Minetest's mod_storage method +-- See https://dev.minetest.net/StorageRef + +-- Loads content of stored orgs and ledger from mod_storage +modpol.load_storage = function() + local mod_storage = minetest.get_mod_storage() + -- load orgs + local stored_orgs = minetest.deserialize(mod_storage:get_string("orgs")) + if (stored_orgs ~= nil) then + modpol.orgs = stored_orgs + end + -- load orgs + local stored_ledger = minetest.deserialize(mod_storage:get_string("ledger")) + if (stored_ledger ~= nil) then + modpol.ledger = stored_ledger + end +end + +-- Stores content of current orgs and ledger to mod_storage +modpol.store_data = function() + -- write to storage + mod_storage:set_string("orgs", minetest.serialize(modpol.orgs)) + mod_storage:set_string("ledger", minetest.serialize(modpol.ledger)) +end diff --git a/users.lua b/users.lua new file mode 100644 index 0000000..b4aa337 --- /dev/null +++ b/users.lua @@ -0,0 +1,29 @@ +-- =================================================================== +-- /users.lua +-- User-related functions for Modular Politics +-- Called by modpol.lua + +-- =================================================================== +-- Function: modpol.list_users +-- Params: org +-- Outputs: Table of user names +-- +-- This may be overwritten by the platform-specific interface + +modpol.list_users = function(org) + local users = {} + if (org == nil) then -- no specified org; all players + if modpol.orgs["instance"] + and modpol.orgs["instance"]["members"] then + -- if instance exists and has membership + users = modpol.orgs["instance"]["members"] + else + users = {} + end + else -- if an org is specified + if (modpol.orgs[org] ~= nil) then -- org exists + users = modpol.orgs[org]["members"] + end + end + return users +end