Renamed modpol/modpol directory to modpol_core for clarity and consistency

This commit is contained in:
Nathan Schneider
2021-12-18 14:50:41 -07:00
parent bd95fcf811
commit c2852b1bce
32 changed files with 6 additions and 8 deletions

21
modpol_core/api.lua Normal file

@@ -0,0 +1,21 @@
--call all files in this directory
local localdir = modpol.topdir
--Users
dofile (localdir .. "/users/users.lua")
--orgs
dofile (localdir .. "/orgs/base.lua")
dofile (localdir .. "/orgs/process.lua")
--interactions
dofile (localdir .. "/interactions/interactions.lua")
--modules
--TODO make this automatic and directory-based
dofile (localdir .. "/modules/add_child_org.lua")
dofile (localdir .. "/modules/consent.lua")
dofile (localdir .. "/modules/join_org_consent.lua")
dofile (localdir .. "/modules/remove_org_consent.lua")
dofile (localdir .. "/modules/remove_org.lua")

@@ -0,0 +1,245 @@
-- INTERACTIONS.LUA (CLI)
-- User interaction functions for Modular Politics
-- Called by modpol.lua
modpol.interactions = {}
-- DASHBOARDS
-- ==========
-- Function: modpol.interactions.dashboard(user)
-- Params: user (string)
-- Q: Should this return a menu of commands relevant to the specific user?
-- Output: Displays a menu of commands to the user
-- TKTK currently just prints all of modpol---needs major improvement
function modpol.interactions.dashboard(user)
-- adds user to root org if not already in it
if not modpol.instance:has_member(user) then
modpol.instance:add_member(user)
end
local all_users = modpol.list_users()
print('All orgs: (user orgs indicated by *)')
for id, org in ipairs(modpol.orgs.array) do
if type(org) == "table" then
local indicator = ""
if org:has_member(user) then indicator = "*" end
print('['..id..'] '..indicator..org.name)
end
end
print('All users: ' .. table.concat(all_users, ', '))
print()
print('Access which org id?')
local sel = io.read()
print()
if modpol.orgs.array[tonumber(sel)] then
local sel_org = modpol.orgs.array[tonumber(sel)].name
modpol.interactions.org_dashboard(user, sel_org)
else
print("Org id not found.")
end
end
-- Function: modpol.interactions.org_dashboard
-- Params: user (string), org_name (string)
-- Output: Displays a menu of org-specific commands to the user
function modpol.interactions.org_dashboard(user, org_name)
local org = modpol.orgs.get_org(org_name)
if not org then return nil end
-- identify parent
local parent = ""
if org.id == 1 then
parent = "none"
else
parent = modpol.orgs.get_org(org.parent).name
end
-- identify children
local children = {}
for k,v in ipairs(org.children) do
local this_child = modpol.orgs.get_org(v)
table.insert(children, this_child.name)
end
-- list available modules
local org_modules = {}
for k,v in pairs(org.modules) do
table.insert(org_modules, v.slug)
end
-- list pending actions
local process_msg = #org.processes .. " total actions"
if org.pending[user] then
process_msg = process_msg .. " (" .. #org.pending[user] .. " pending)"
else
process_msg = process_msg .. " (0 pending)"
end
-- set up output
print("Org: " .. org_name)
print("Parent: " .. parent)
print("Members: " .. table.concat(org.members, ", "))
print("Children: " .. table.concat(children, ", "))
print("Modules: " .. table.concat(org_modules, ", "))
print("Actions: " .. process_msg)
print()
print("Commands: (M)odules, (A)ctions")
local sel = io.read()
print()
if sel == 'm' or sel == 'M' then
print("Type module name: ")
local module_sel = io.read()
print()
local module_result = false
for k,v in ipairs(org_modules) do
if v == module_sel then
module_result = true
end
end
if module_result then
org:call_module(module_sel, user)
else
print("Error: Module not found.")
end
elseif sel == 'a' or sel == 'A' then
local processes = {}
print("All processes: (* indicates pending action)")
for k,v in ipairs(org.processes) do
local active = ''
if org.pending[user] then
if org.pending[user][v.id] then
active = '*'
end
end
end
print()
print("Interact with which one?")
local to_interact = io.read()
local process = org.processes[tonumber(to_interact)]
if not process then return end
if org:has_pending_actions(user) then
if org.pending[user][process.id] then
org:interact(process.id, user)
end
end
else
print("Command not found")
modpol.interactions.org_dashboard(user, org_name)
end
end
-- Function: modpol.interactions.policy_dashboard
-- input: user (string), org_id (int), policy (string)
-- if policy is nil, enables creating a new policy
-- output: opens a dashboard for viewing/editing policy details
-- TODO
-- Function: modpol.interactions.message
-- input: user (string), message (string)
-- output: prints message to CLI
function modpol.interactions.message(user, message)
print(user .. ": " .. message)
end
-- Function: modpol.interactions.text_query
-- input: User (string), Query (string), func (function)
-- func input: user input (string)
-- output: Applies "func" to user input
function modpol.interactions.text_query(user, query, func)
print(user .. ": " .. query)
answer = io.read()
func(answer)
end
-- Function: dropdown_query
-- input: user (string), label (string), options (table of strings), func(choice) (function)
-- func input: choice (string)
-- output: calls func on choice
function modpol.interactions.dropdown_query(user, label, options, func)
-- set up options
local options_display = ""
local options_number = 0
for k,v in ipairs(options) do
options_display = options_display .. k .. ". " ..
options[k] .. "\n"
options_number = options_number + 1
end
options_display = options_display .. "Select number:"
if options_number == 0 then
print("Error: No options given for dropdown")
return nil
end
-- begin displaying
print(user .. ": " .. label)
print(options_display)
-- read input and produce output
local answer
answer = io.read()
answer = tonumber(answer)
if answer then
if answer >= 1 and answer <= options_number then
print("Selection: " .. options[answer])
func(options[answer])
else
print("Error: Not in dropdown range")
return nil
end
else
print("Error: Must be a number")
return nil
end
end
-- Function: modpol.binary_poll_user(user, question)
-- Params: user (string), question (string), func (function)
-- func input: user input (string: y/n)
-- Output: Applies "func" to user input
function modpol.interactions.binary_poll_user(user, question, func)
local query = "Poll for " .. user .. " (y/n): ".. question
local answer
repeat
print(query)
answer = io.read()
until answer == "y" or answer == "n"
if answer == "y" then
modpol.interactions.message(user, "Response recorded")
func("Yes")
elseif answer == "n" then
modpol.interactions.message(user, "Response recorded")
func("No")
else
modpol.interactions.message(user, "Error: invalid response")
end
end
-- COMPLEX INTERACTIONS
-- ====================
-- Function: modpol.interactions.message_org
-- input: initiator (string), org_id (number), message (string)
-- output: broadcasts message to all org members
function modpol.interactions.message_org(initiator, org_id, message)
local org = modpol.orgs.get_org(org_id)
local users = org:list_members()
for k,v in ipairs(users) do
modpol.interactions.message(v, message)
end
end
-- Function: modpol.interactions.binary_poll_org
-- input: initator (user string), org_id (number)
-- output: gets question from initiator, asks all org members, broadcasts answers

103
modpol_core/modpol.lua Normal file

@@ -0,0 +1,103 @@
-- ===================================================================
-- /modpol.lua
-- Modular Politics (modpol) core
-- ===================================================================
-- Basic tables
-- Main API table
if not modpol then modpol = {} end
-- 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.
-- if your application has a different method of getting the script directory,
-- then feel free to overwrite this in an init.lua or other such file by first
-- defining the modpol table and then defining the get_script_dir() function
local get_script_dir = modpol.get_script_dir or 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 .. "/util/ocutil/ocutil.lua")
-- ===================================================================
-- Persistent storage
-- must implement modpol.load_storage() and modpol.store_data()
-- Select a storage method
-- -- preferably, declare this in the init.lua that calls modpol.lua This is the default.
-- Works with CLI:
modpol.storage_file_path = modpol.storage_file_path or topdir .. "/storage/storage-local.lua"
-- Works with Minetest 5:
--modpol.storage_file_path = modpol.storage_file_path or topdir .. "/storage/storage-mod_storage.lua")
--execute the storage file
dofile (modpol.storage_file_path)
-- If available, load persistent storage into active tables
modpol.load_storage()
-- ===================================================================
-- Modpol modules
modpol.modules = modpol.modules or {}
-- TKTK need to specify modules to include
-- ===================================================================
-- Modpol core features
dofile (topdir .. "/api.lua")
-- ===================================================================
-- Final checks
-- sets org metatable on load
if (modpol.orgs.array) then
for id, org in ipairs(modpol.orgs.array) do
if type(org) == 'table' then
setmetatable(org, modpol.orgs)
-- sets process metatable on load
for id, process in ipairs(org.processes) do
if type(process) == 'table' then
setmetatable(process, modpol.modules[process.type])
end
end
end
end
end
-- create instance if not present
modpol.instance = modpol.orgs.array[1] or modpol.orgs.init_instance()
modpol.ocutil.log ("modpol loaded")
-- ===================================================================
-- End of file.

@@ -0,0 +1,29 @@
--- @module add_child_org
-- Adds a child org
local add_child_org = {
name = "Add child org",
slug = "add_child_org",
desc = "Create a child org within the current one"
}
add_child_org.data = {
}
add_child_org.config = {
}
-- @function initiate
function add_child_org:initiate(result)
modpol.interactions.text_query(
self.initiator,"Child org name: ",
function(input)
self.org:add_org(input, self.initiator)
modpol.interactions.message_org(
self.initiator,
self.org.id,
"Child org created: "..input)
modpol.interactions.dashboard(self.initiator)
end)
end
--- (Required) Add to module table
modpol.modules.add_child_org = add_child_org

@@ -0,0 +1,52 @@
--- @module consent
-- A utility module for checking consent
local consent = {
name = "Consent",
slug = "consent",
desc = "Other modules can use to implement consent based decision making",
}
consent.data = {
votes = 0
}
consent.config = {
prompt = "Do you consent?",
votes_required = 1
}
function consent:initiate(result)
self.result = result
-- if org is empty, consent is given automatically
if self.org:get_member_count() == 0 then
self.result()
self.org:wipe_pending_actions(self.id)
else
-- otherwise, create poll
for id, member in pairs(self.org.members) do
self.org:add_pending_action(self.id, member, "callback")
end
end
end
function consent:callback(member)
modpol.interactions.binary_poll_user(
member,
self.config.prompt,
function (resp)
self.org:remove_pending_action(self.id,member)
if resp == "Yes" then
self.data.votes = self.data.votes + 1
end
if self.data.votes >= self.config.votes_required then
if self.result then self.result() end
self.org:wipe_pending_actions(self.id)
end
end
)
end
modpol.modules.consent = consent

@@ -0,0 +1,39 @@
join_org = {}
join_org.setup = {
name = "Join Org",
slug = "join_org",
desc = "If consent process is passed, initiator joins this org."
}
function join_org.initiate(initiator, org, result)
modpol.interactions.binary_poll_user(
initiator,
"Would you like to join " .. org.name,
function (resp)
if resp == "Yes" then
org:add_member(initiator)
end
end
)
for i, member in ipairs(org.members) do
org:add_pending_action(
member,
function ()
modpol.interactions.binary_poll_user(
member,
"Let " .. initiator .. " join " .. org.name .. "?",
function (resp)
end
)
end
)
end
if result then result() end
end
modpol.modules.join_org = join_org

@@ -0,0 +1,36 @@
--- Join org (consent)
-- A simple module that calls a consent process on an org to add a member.
-- Depends on the Consent module.
local join_org_consent = {
name = "Join this org",
slug = "join_org_consent",
desc = "Adds member with consent of all members."
}
join_org_consent.data = {
}
join_org_consent.config = {
}
function join_org_consent:initiate()
self.org:call_module(
"consent",
self.initiator,
{
prompt = "Allow " .. self.initiator .. " to join?",
votes_required = #self.org.members
},
function ()
self:complete()
end
)
end
function join_org_consent:complete()
self.org:add_member(self.initiator)
print("Added " .. self.initiator .. " to the org.")
end
modpol.modules.join_org_consent = join_org_consent

@@ -0,0 +1,27 @@
--- @module Remove Org
-- A simple module that calls a consent process on an org to remove it.
--- Main module table
remove_org = {
name = "Remove this org",
slug = "remove_org",
desc = "Removes an org if all members consent."
}
remove_org.config = {}
remove_org.data = {}
--- Initiate function
-- @function initiate
function remove_org:initiate(config, result)
modpol.interactions.message_org(
self.initiator,self.org.id,
"Removing org: "..self.org.name)
self.org:delete()
modpol.interactions.dashboard(self.initiator)
-- call result function
if result then result() end
end
modpol.modules.remove_org = remove_org

@@ -0,0 +1,39 @@
--- Remove org (consent)
-- A simple module that calls a consent process on an org to remove it.
-- Depends on the Consent module.
local remove_org_consent = {
name = "Remove this org",
slug = "remove_org_consent",
desc = "Removes an org if all members consent."
}
remove_org_consent.data = {
}
remove_org_consent.config = {
}
function remove_org_consent:initiate()
self.org:call_module(
"consent",
self.initiator,
{
prompt = "Remove org " .. self.org.name .. "?",
votes_required = #self.org.members
},
function ()
self:complete()
end
)
end
function remove_org_consent:complete()
modpol.interactions.message_org(
self.initiator, self.org.id,
"Removing org: " .. self.org.name)
self.org:delete()
modpol.interactions.dashboard(self.initiator)
end
modpol.modules.remove_org_consent = remove_org_consent

@@ -0,0 +1,49 @@
--- module_template
-- @module module_template
--- (Required): data table containing name and description of the module
-- @field name "Human-readable name"
-- @field slug "Same as module class name"
-- @field desc "Description of the module"
local module_template = {
name = "Module Human-Readable Name",
slug = "template",
desc = "Description of the module"
}
--- (Required) Data for module
-- Variables that module uses during the course of a process
-- Can be blank
module_template.data = {
}
--- (Required): config for module
-- Defines the input parameters to the module initiate function.
-- Can be blank
-- When calling a module from within another module,
-- variables not defined in config will be ignored.
-- Default values set in config can be overridden
-- @field field_1 ex: votes_required, default = 5
-- @field field_2 ex: voting_type, default = "majority"
module_template.config = {
field_1 = 5
field_2 = "majority"
}
--- (Required): initiate function
-- Modules have access to the following instance variables:
-- <li><code>self.org</code> (the org the module was called in),</li>
-- <li><code>self.initiator</code> (the user that callced the module),</li>
-- <li><code>self.id</code> (the process id of the module instance)</li>
-- @param config (optional) If user wants to override fields in the config table
-- @param result (optional) Callback if this module is embedded in other modules
-- @function initiate
function module_template:initiate(config, result)
-- call interaction functions here!
-- call result function
if result then result() end
end
--- (Required) Add to module table
modpol.modules.module_template = module_template

318
modpol_core/orgs/base.lua Normal file

@@ -0,0 +1,318 @@
--- Orgs: Base
-- Basic functions for orgs
modpol.orgs = modpol.orgs or
{
count = 1,
array = {}
}
-- sets modpol.orgs as its own fallback
modpol.orgs.__index = modpol.orgs
function temp_org()
return {
id = nil,
name = nil,
modules = modpol.modules,
processes = {},
pending = {},
members = {},
parent = nil,
children = {}
}
end
-- ==================================================
-- returns org when given its id or name
function modpol.orgs.get_org(arg)
if type(arg) == 'string' then
for id, org in ipairs(modpol.orgs.array) do
if org.name == arg then
return org
end
end
elseif type(arg) == 'number' then
return modpol.orgs.array[arg]
end
return nil
end
-- ===============================================
-- returns a table list of all org names
function modpol.orgs.list_all()
local org_table
for k, v in ipairs(modpol.orgs.array) do
if type(v) == 'table' then
if org_table then
table.insert(org_table, v.name)
else
org_table = {v.name}
end
end
end
return org_table
end
-- Function: modpol.orgs.user_orgs(user)
-- input: user (string)
-- output: table of strings of org names
function modpol.orgs.user_orgs(user)
local all_orgs = modpol.orgs.list_all()
local user_orgs = {}
for i,v in ipairs(all_orgs) do
local this_table = modpol.orgs.get_org(v)
if this_table:has_member(user) then
table.insert(user_orgs,v)
end
end
return user_orgs
end
-- ===========================================
-- deletes all orgs except for the instance
function modpol.orgs.reset()
for id, org in ipairs(modpol.orgs.array) do
if id > 1 then
modpol.orgs.array[id] = "removed"
end
end
modpol.orgs.array[1] = nil
modpol.instance = modpol.orgs.init_instance()
modpol.ocutil.log('Reset all orgs')
modpol.orgs:record('Resetting all orgs', 'org_reset')
end
-- ===================================================
-- initializes the instance (root org)
-- can only be run once, as only one instance can exist
function modpol.orgs.init_instance()
local error_msg
if modpol.orgs.array[1] then
modpol.ocutil.log('Error in orgs.init_instance -> instance has already been initialized')
return false
end
local instance = temp_org()
instance.id = 1
instance.name = "root"
setmetatable(instance, modpol.orgs)
-- adding instance to org list
modpol.orgs.array[1] = instance
modpol.ocutil.log('Initialized the instance root org')
modpol.orgs:record('Initialized the instance root org', 'create_instance')
return instance
end
-- FUNCTIONS BEYOND HERE OPERATE ON ORG OBJECTS
-- =======================================================
-- records a log message to the modpol ledger
function modpol.orgs:record(msg, entry_type)
local entry = {
timestamp = '',
entry_type = nil,
action_msg = '',
org_name = '',
org_id = nil,
}
if type(msg) == 'string' and not(modpol.ocutil.str_empty(msg)) then
entry.action_msg = msg
else
modpol.ocutil.log('Error in ' .. self.name .. ':record -> msg must be a non empty string')
return false
end
if type(entry_type) == 'string' and not(modpol.ocutil.str_empty(entry_type)) then
entry.entry_type = entry_type
else
modpol.ocutil.log('Error in ' .. self.name .. ':record -> entry_type must be a non empty string')
modpol.ocutil.log(msg, entry_type)
return false
end
entry.timestamp = os.time()
entry.org_id = self.id
entry.org_name = self.name
table.insert(modpol.ledger, entry)
modpol.store_data()
end
-- ==================================================
-- adds a new sub org to the org it is called on
-- input: name (string), user (string)
-- ex: instance:add_org('town hall')
function modpol.orgs:add_org(name, user)
if self.id == nil then
modpol.ocutil.log('Error in ' .. self.name .. ':add_org -> add_org can only be called by another org')
return false
end
if modpol.ocutil.str_empty(name) then
modpol.ocutil.log('Error in ' .. self.name .. ':add_org -> org name is required')
return false
end
if modpol.orgs.get_org(name) then
modpol.ocutil.log('Error in ' .. self.name .. ':add_org -> org name is already being used')
return false
end
-- creating the child sub org
modpol.orgs.count = modpol.orgs.count + 1
local child_org = temp_org()
child_org.id = modpol.orgs.count
child_org.name = name
child_org.parent = self.id
child_org.processes = {}
child_org.modules = self.modules
setmetatable(child_org, modpol.orgs)
-- adding child id to list of children
table.insert(self.children, child_org.id)
-- adding child to org list
modpol.orgs.array[child_org.id] = child_org
-- adding creator of org as the first member
child_org:add_member(user)
self:record('created sub org ' .. name, 'add_org')
modpol.ocutil.log('Created ' .. name .. ' (suborg of ' .. self.name .. ')')
return child_org
end
-- ========================================
-- recursively deletes an org and its suborgs
-- leaves entry in modpol.orgs.array as a string "removed"
-- note: "reason" param was removed, can be added back
function modpol.orgs:delete()
if self.id == 1 then
modpol.ocutil.log('Error in ' .. self.name .. ':delete -> cannot delete instance')
return false
end
if #self.children > 0 then
for i, child_id in pairs(self.children) do
local child = modpol.orgs.get_org(child_id)
modpol.ocutil.log(child_id, child)
child:delete()
end
end
modpol.orgs.array[self.id] = 'removed'
modpol.ocutil.log('Deleted org ' .. self.name .. ': ' .. self.id)
self:record('Deleted ' .. self.name .. ' and all child orgs', 'del_org')
end
-- ===========================================
-- internal function to get the index of a member name
function modpol.orgs:get_member_index(member)
for k, v in ipairs(self.members) do
if v == member then
return k
end
end
end
-- ===========================================
-- adds a user to an org
function modpol.orgs:add_member(user)
for id, name in ipairs(self.members) do
if user == name then
modpol.ocutil.log('Error in ' .. self.name .. ':add_member -> user already in org')
return false
end
end
-- trys to fill in empty spots first
local empty_index = self:get_member_index('')
if empty_index then
self.members[empty_index] = user
else
-- adds to end if no empty spots
table.insert(self.members, user)
end
modpol.ocutil.log('Added member ' .. user .. ' to ' .. self.name)
self:record('Added member ' .. user, 'add_member')
end
-- =======================================
-- removes a user from an org
function modpol.orgs:remove_member(user)
-- sets the array index to an empty string so that consecutive list is preserved
-- empty spots will get filled in by new members
local user_index = self:get_member_index(user)
if user_index then
self.members[user_index] = ''
else
modpol.ocutil.log('Error in ' .. self.name .. ':remove_member -> user not in org')
end
modpol.ocutil.log('Removed member ' .. user .. ' from ' .. self.name)
self:record('Removed member ' .. user, 'del_member')
end
-- ===========================================
-- boolean check whether user is an org
function modpol.orgs:has_member(user)
local user_index = self:get_member_index(user)
if user_index then
return true
else
return false
end
end
-- ==================================
-- Function: modpol.orgs:list_members()
-- output: a table of the names (string) of members
function modpol.orgs:list_members()
local members = {}
for k, v in ipairs(self.members) do
table.insert(members, v)
end
return members
end
-- ==============================
-- because member list uses lazy deletion, using #org.members will not show an accurate number
function modpol.orgs:get_member_count()
local count = 0
for k, v in ipairs(self.members) do
-- the empty string represents a deleted member in the members list
if v ~= '' then
count = count + 1
end
end
return count
end
-- ====================================
-- adds a new policy to the policy table
-- must define the policy type, process associated with it, and whether the request must be made by an org member
function modpol.orgs:set_policy(policy_type, process_type, must_be_member)
local new_policy = {
process_type = process_type,
must_be_member = must_be_member
}
self.policies[policy_type] = new_policy
modpol.ocutil.log('Added policy for ' .. policy_type .. ' in ' .. self.name)
self:record('Added policy for ' .. policy_type, 'set_policy')
end

@@ -0,0 +1,98 @@
-- TODO: NEEDS TO BE REWRITTEN AS A LIBRARY NOT A MODULE
modpol.orgs.consent = {}
-- sets consent to its own callback
modpol.orgs.consent.__index = modpol.orgs.consent
function temp_consent_process()
return {
type = "consent",
id = nil,
org_id = nil,
request_id = nil,
total_votes = 0,
majority_to_pass = 0.51,
votes_needed = nil,
votes_yes = {},
votes_no = {}
}
end
-- ===============================================
-- function to create a new consent process to resolve a pending process
function modpol.orgs.consent:new_process(id, request_id, org_id)
local process = temp_consent_process()
process.id = id
process.request_id = request_id
process.org_id = org_id
setmetatable(process, modpol.orgs.consent)
modpol.ocutil.log('Created new process #' .. id .. ' for request id #' .. request_id)
local p_org = modpol.orgs.get_org(org_id)
for i, member in ipairs(p_org.members) do
p_org:add_pending_action(id, member)
end
process.votes_needed = math.ceil(process.majority_to_pass * p_org:get_member_count())
return process
end
-- ============================
-- interact function for the consent module
-- input: user (string)
function modpol.orgs.consent:interact(user)
-- TODO this needs more context on the vote at hand
modpol.interactions.binary_poll_user(
user, "Do you consent?",
function(vote)
if vote == 'Yes' then
self:approve(user, true)
elseif vote == 'No' then
self:approve(user, false)
end
end)
end
-- ======================================================
-- function for users to vote on a pending request
function modpol.orgs.consent:approve(user, decision)
if not modpol.orgs.get_org(self.org_id):has_member(user) then
modpol.ocutil.log('Error in consent:approve -> user not a member of the org')
return
end
if decision then
table.insert(self.votes_yes, user)
modpol.ocutil.log('User ' .. user .. ' voted yes on request #' .. self.request_id)
else
table.insert(self.votes_no, user)
modpol.ocutil.log('User ' .. user .. ' voted no on request #' .. self.request_id)
end
self.total_votes = self.total_votes + 1
local p_org = modpol.orgs.get_org(self.org_id)
p_org:remove_pending_action(self.id, user)
self:update_status()
end
-- ===================================================
-- determines whether process has finished and resolves request if it has (unfinished)
function modpol.orgs.consent:update_status()
local process_org = modpol.orgs.get_org(self.org_id)
if #self.votes_yes >= self.votes_needed then
modpol.ocutil.log('Request #' .. self.request_id .. ' passes')
process_org:resolve_request(self.request_id, true)
elseif #self.votes_no >= self.votes_needed then
modpol.ocutil.log('Request #' .. self.request_id .. ' fails to pass')
process_org:resolve_request(self.request_id, false)
else
modpol.ocutil.log('Waiting for more votes...')
end
end

@@ -0,0 +1,48 @@
--[[
modpol calls this function when someone starts a new request for this module
--]]
function module:initiate(org) {
form = {
{
"name": "to_remove",
"display": "Which user should be removed?",
"type": "drop-down",
"values": org:list_members()
},
{
"name": "reason",
"type": "text-box",
"prompt": "Reason for removing member:"
}
}
return form
}
--[[
modpol prompts the user with this form, and after receiving the data asynchronously
the returned form would look like:
{
"to_remove": luke,
"reason": stealing food
}
based on provided "name" fields and the input given by the user
now module:request is called
--]]
function module:request(form) {
self.data = form
}
--[[
after the module request function runs, modpol will initiate the consent process
if consent is approved, the implement function is called
--]]
function module:implement() {
org:remove_member(self.data.to_remove)
}

@@ -0,0 +1,102 @@
--- Process functions for orgs
function modpol.orgs:call_module(module_slug, initiator, config, result)
if not modpol.modules[module_slug] then
modpol.ocutil.log('Error in ' .. self.name .. ':call_module -> module "' .. module_slug .. '" not found')
return
end
local empty_index = nil
-- linear search for empty process slots (lazy deletion)
for k, v in ipairs(self.processes) do
if v == 'deleted' then
empty_index = k
break
end
end
local index
-- attempts to fill empty spots in list, otherwise appends to end
if empty_index then
index = empty_index
else
index = #self.processes + 1
end
local module = modpol.modules[module_slug]
-- sets default values for undeclared config variables
if #module.config > 0 then
for k, v in pairs(module.config) do
if config[k] == nil then
config[k] = v
end
end
end
-- setting default params
local new_process = {
metatable = {__index = module},
initiator = initiator,
org = self,
id = index,
config = config,
data = module.data,
slug = module_slug
}
setmetatable(new_process, new_process.metatable)
self.processes[index] = new_process
self.processes[index]:initiate(result)
return index
end
function modpol.orgs:delete_process(id)
self.processes[id] = 'deleted'
end
function modpol.orgs:add_pending_action(process_id, user, callback)
self.pending[user] = self.pending[user] or {}
self.pending[user][process_id] = callback
end
function modpol.orgs:remove_pending_action(process_id, user)
if self.pending[user] then
self.pending[user][process_id] = nil
end
end
function modpol.orgs:wipe_pending_actions(process_id)
for user in pairs(self.pending) do
self.pending[user][process_id] = nil
end
end
function modpol.orgs:has_pending_actions(user)
-- next() will return the next pair in a table
-- if next() returns nil, the table is empty
if not self.pending[user] then
return false
else
if not next(self.pending[user]) then
return false
else
return true
end
end
end
function modpol.orgs:interact(process_id, user)
local process = self.processes[process_id]
modpol.interactions.message(user,"hi!")
if self.pending[user] then
modpol.interactions.message(user,"id: "..process_id)
local callback = self.pending[user][process_id]
if callback then
modpol.interactions.message(user,"la!")
process[callback](process, user)
end
end
end

@@ -0,0 +1,153 @@
old_request_format = {
user=user, -- requesting user
type="add_member", -- action
params={user} -- action params
}
old_process_format = {
type = "consent", -- delete
id = nil,
org_id = nil,
request_id = nil, -- delete
-- consent config
majority_to_pass = 0.51, -- voting threshold
votes_needed = nil,
-- consent data
total_votes = 0,
votes_yes = {},
votes_no = {}
}
new_process_format = {
initiator = "user",
status = "request",
org_id = 12314,
module = "create_child_org", -- policy table lookup
process_id = 8347,
timestamp = 1632850133, -- look into supporting other formats, overrides (turn based, etc.)
data = {
child_org_name = "oligarchy"
},
consent = {
-- voter eligibilty frozen by action table invites
start_time = 384179234,
member_count = 14,
votes_yes = {},
votes_no = {}
}
}
-- initialize values
function init_consent(policy) {
self.start_time = os.time()
self.member_count = modpol.orgs.get_org(self.org_id):get_member_count()
if policy.duration then
-- mintest.after(time, func, ...args)
-- should override modpol callback function
register_callback(self.start_time + policy.duration)
end
if (duration and (consent_ratio or yes_threshold or no_threshold)) or (yes_threshold) or (consent_ratio) then
-- well formed policy
else
-- invalid
end
}
-- update vote count
function update_consent(user, approve) {
if approve then
table.insert(yes_votes, user)
else
table.insert(no_votes, user)
if not duration then
eval_consent()
end
}
-- evaluate state of vote
function eval_consent() {
consent_ratio = #yes_votes / (#yes_votes + #no_votes)
quorum = (#yes_votes + #no_votes) / member_count
if policy.duration then
if policy.consent_ratio then
if policy.quorum then
if quorum < policy.quorum then
fail()
end
end
if consent_ratio >= policy.consent_ratio then
pass()
else
fail()
end
elseif policy.yes_threshold then
if #yes_votes >= policy.yes_threshold then
pass()
else
fail()
end
elseif policy.no_threshold then
if #no_votes <= policy.no_threshold then
fail()
else
pass()
end
end
elseif policy.yes_threshold then
if policy.no_threshold then
if #no_votes >= policy.no_threshold then
fail()
end
if #yes_votes >= policy.yes_threshold then
pass()
end
elseif policy.consent_ratio and policy.quorum then
if quorum >= policy.quorum then
if consent_ratio >= policy.consent_ratio then
pass()
else
fail()
end
end
end
}
policy_table_format = {
"create_child_org": {
defer_to = nil,
-- duration
duration = nil, -- evaluates end conditions when reached
-- thesholds
no_threshold = nil, -- fails if reached
yes_threshold = nil, -- succeeds if reached
--ratios
consent_ratio = nil, -- % of voters
quorum = nil, -- % of members that vote
}
"create_child_org": {
consent_threshold = 0.51,
max_duration = 89324, -- seconds until vote closes if threshold not reached, or nil for no limit
defer = nil, -- org id to defer to, or nil
}
}

@@ -0,0 +1,278 @@
-- THIS IS NOW DEPRECATED, TO MERGE INTO PROCESS
-- REQUESTS
-- Provides request functionality to org
-- TODO
-- include consent functionality, available to modules
-- load available modules in modpol.lua
-- make policies a set of options for consent functions
-- create simple modules for core functions: change_policy, add_member, start_org, join_org, close_org
-- CONSENT
-- Provides consent functionality for modules
-- define default params
modpol.default_policy = {
target_org = nil,
time_limit = nil,
vote_threshold = 1 -- a ratio of the total number of members
}
-- PROCESSES
-- functions that enable requests to create processes
-- ================================
-- creates a new process linked to a request id
function modpol.orgs:create_process(process_type, request_id)
if not modpol.modules[process_type] then
modpol.ocutil.log('Process type "' .. process_type .. '" does not exist')
return
end
local empty_index = nil
-- linear search for empty process slots (lazy deletion)
for k, v in ipairs(self.processes) do
if v == 'deleted' then
empty_index = k
break
end
end
local index
-- attempts to fill empty spots in list, otherwise appends to end
if empty_index then
index = empty_index
else
index = #self.processes + 1
end
-- retrieving requested module
local module = modpol.modules[process_type]
local new_process = module:new_process(index, request_id, self.id)
self.processes[index] = new_process
modpol.ocutil.log('Created process #' .. index .. ' - ' .. process_type .. ' for ' .. self.name)
self:record('Created process #' .. index .. ' - ' .. process_type, 'add_process')
return index
end
-- ===========================
-- adds a new pending action to the org's table
function modpol.orgs:add_pending_action(process_id, user)
-- adds tables if they don't exist already
self.pending[user] = self.pending[user] or {}
-- flagging actual action
self.pending[user][process_id] = true
local message =
modpol.interactions.message(user, "New pending action in " .. self.name)
modpol.ocutil.log("Added pending action to " .. user .. " in process #" .. process_id .. ' from ' .. self.name)
self:record('Added pending action to ' .. user .. ' in process #' .. process_id, "add_pending_action")
end
-- ========================
-- removes a pending action from the org's table
function modpol.orgs:remove_pending_action(process_id, user)
if self.pending[user] then
self.pending[user][process_id] = nil
modpol.ocutil.log("Removed pending action from " .. user .. " in process #" .. process_id .. ' from ' .. self.name)
self:record('Removed pending action from ' .. user .. ' in process #' .. process_id, "del_pending_action")
else
modpol.ocutil.log("Could not remove pending action from " .. user .. " in process #" .. process_id)
end
end
-- =====================
-- removes all pending actions for a given process id from all users
function modpol.orgs:wipe_pending_actions(process_id)
for user in pairs(self.pending) do
self.pending[user][process_id] = nil
end
modpol.ocutil.log("Removed all pending actions for process #" .. process_id)
self:record('Removed all pending actions for process #' .. process_id, "del_pending_action")
end
-- ======================
-- returns a boolean to indicate whether a user has any active pending actions
function modpol.orgs:has_pending_actions(user)
-- next() will return the next pair in a table
-- if next() returns nil, the table is empty
if not self.pending[user] then
return false
else
if not next(self.pending[user]) then
return false
else
return true
end
end
end
-- ===========================
-- compares to requests to see if they are identical
function modpol.orgs.comp_req(req1, req2)
-- compares request type
if req1.type ~= req2.type then
return false
else
-- comparing parameters
-- we can assume the number of params is the same as this is checked in the make_request func
for k, v in ipairs(req1.params) do
if v ~= req2.params[k] then
return false
end
end
end
return true
end
-- ===============================
-- returns string of all active requests
function modpol.orgs:list_request()
local str
for id, req in ipairs(self.requests) do
if req ~= "deleted" then
if str then
str = str .. '\n' .. req.type .. ' (' .. req.user .. ') '
else
str = req.type .. ' (' .. req.user .. ') '
end
end
end
return str
end
-- ===============================
-- if the request was approved, the associated function is called, otherwise it is deleted
-- TODO Rather than hard-coding functions below, this should be given an arbitrary result function based on the request
function modpol.orgs:resolve_request(request_id, approve)
-- wipe actions
self:wipe_pending_actions(request_id)
if approve then
local request = self.requests[request_id]
local p = request.params
-- there's probably a way to clean this up, the issue is the varying number of commands
-- ex: self['add_member'](self, 'member_name')
-- not sure if this is safe, more testing to do
-- self[request.type](self, p[1], p[2], p[3])
if request.type == "add_org" then
self:add_org(request.params[1], request.user)
elseif request.type == "delete" then
self:delete()
elseif request.type == "add_member" then
self:add_member(request.params[1])
elseif request.type == "remove_member" then
self:remove_member(request.params[1])
end
end
self.processes[request_id] = "deleted"
modpol.ocutil.log('Deleted process #' .. request_id)
self.requests[request_id] = "deleted"
modpol.ocutil.log("Resolved request #" .. request_id .. ' in ' .. self.name)
self:record("Resolved request #" .. request_id, "resolve_request")
end
-- ================================
-- tries to make a request to the org
function modpol.orgs:make_request(request)
-- checking to see if identical request already exists
for k, v in ipairs(self.requests) do
if self.comp_req(request, v) == true then
modpol.ocutil.log('Error in ' .. self.name .. ':make_request -> request has already been made')
return false
end
end
-- checking to see if user is able to make request
local requested_policy = self.policies[request.type]
-- if not the instance org (instance's don't have parents)
if self.id ~= 1 then
local parent_policy = modpol.orgs.get_org(self.parent).policies[request.type]
-- tries to use org's policy table, defers to parent otherwise
if not requested_policy then
modpol.ocutil.log(request.type .. ' policy not found, deferring to parent org')
requested_policy = parent_policy
if not parent_policy then
modpol.ocutil.log('Error in ' .. self.name .. ':make_request -> parent policy undefined')
return false
end
end
-- fails if instance policy undefined
else
if not requested_policy then
modpol.ocutil.log('Error in ' .. self.name .. ':make_request -> policy undefined')
return false
end
end
-- make sure user is allowed to make request
if requested_policy.must_be_member and not self:has_member(request.user) then
modpol.ocutil.log('Error in ' .. self.name .. ':make_request -> user must be org member to make this request')
return false
end
local empty_index = nil
-- linear search for empty process slots (lazy deletion)
for k, v in ipairs(self.requests) do
if v == 'deleted' then
empty_index = k
break
end
end
-- attempts to fill empty spots in list, otherwise appends to end
local request_id = nil
if empty_index then
self.requests[empty_index] = request
request_id = empty_index
else
table.insert(self.requests, request)
request_id = #self.requests
end
modpol.interactions.message(request.user, "Request made by " .. request.user .. " to " .. request.type .. " in " .. self.name)
modpol.ocutil.log("Request made by " .. request.user .. " to " .. request.type .. " in " .. self.name)
self:record("Request made by " .. request.user .. " to " .. request.type, "make_request")
-- launching process tied to this request
local process_id = self:create_process(requested_policy.process_type, request_id)
-- returns process id of processes launched by this request
return process_id
end
-- wrapper for process:interact function, ensures that user actually has a pending action for that process
function modpol.orgs:interact(process_id, user)
process = self.processes[process_id]
if self.pending[user] then
if self.pending[user][process_id] == true then
process:interact(user)
else
modpol.ocutil.log("Cannot interact with process, user does not have a valid pending action")
end
else
modpol.ocutil.log("Cannot interact with process, user does not have any pending actions")
end
end

@@ -0,0 +1,144 @@
-- ===================================================================
-- /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"
modpol.file_old_ledgers = modpol.datadir .. "/old_ledgers.dat"
os.execute ("mkdir -p " .. modpol.datadir)
modpol.ocutil.setlogdir (modpol.datadir)
modpol.ocutil.setlogname ("modpol.log")
-- ===================================================================
-- Set up the Serpent Serializer functions.
modpol.serpent = {}
dofile (modpol.topdir .. "/util/serpent/serpent.lua")
-- ===================================================================
-- This function stores "ledger" data to disk.
local store_ledger = function(verbose)
local ok = modpol.ocutil.file_write (modpol.file_ledger, modpol.serpent.dump (modpol.ledger))
if ok ~= true then
modpol.ocutil.fatal_error ("store_data: ledger")
end
local nn = modpol.ocutil.table_length (modpol.ledger)
local str = "entries"
if nn == 1 then str = "entry" end
if verbose then modpol.ocutil.log (nn .. " global ledger entries stored to disk") end
end
-- ===================================================================
-- This function stores "orgs" data to disk.
local store_orgs = function()
local ok = modpol.ocutil.file_write (modpol.file_orgs,
modpol.serpent.dump (modpol.orgs))
if ok ~= true then
modpol.ocutil.fatal_error ("store_data: orgs")
end
local nn = modpol.ocutil.table_length (modpol.orgs.array)
local str = "entries"
if nn == 1 then str = "entry" end
if verbose then modpol.ocutil.log (nn .. " orgs stored to disk") end
end
-- ===================================================================
-- This function stores data to disk.
modpol.store_data = function(verbose)
store_ledger(verbose)
store_orgs(verbose)
end
-- ===================================================================
-- This function loads "ledger" data from disk.
local load_ledger = function()
local obj = modpol.ocutil.file_read (modpol.file_ledger )
if obj ~= nil then
local func, err = load (obj)
if err then
modpol.ocutil.fatal_error ("load_data: ledger" )
end
modpol.ledger = func()
local nn = modpol.ocutil.table_length (modpol.ledger)
local str = "entries"
if nn == 1 then str = "entry" end
modpol.ocutil.log (nn .. " global ledger entries loaded from disk")
else
modpol.ocutil.log ("No stored global ledger data found")
end
end
-- ===================================================================
-- This function loads "orgs" data from disk.
local load_orgs = function()
local obj = modpol.ocutil.file_read (modpol.file_orgs )
if obj ~= nil then
local func, err = load (obj)
if err then
modpol.ocutil.fatal_error ("load_data: orgs" )
end
modpol.orgs = func()
-- this block resets the metatable after being loaded in so that the class functions work
-- for id, org in ipairs(modpol.orgs.array) do
-- setmetatable(org, modpol.orgs)
-- end
local nn = modpol.ocutil.table_length (modpol.orgs.array)
local str = "entries"
if nn == 1 then str = "entry" end
modpol.ocutil.log (nn .. " orgs loaded from disk")
else
modpol.ocutil.log ("No stored orgs data found")
end
end
-- ===================================================================
-- This function loads "old_ledgers" data from disk.
local load_old_ledgers = function()
local obj = modpol.ocutil.file_read (modpol.file_old_ledgers )
if obj ~= nil then
local func, err = load (obj)
if err then
modpol.ocutil.fatal_error ("load_data: old_ledgers" )
end
modpol.old_ledgers = func()
local nn = modpol.ocutil.table_length (modpol.old_ledgers)
local str = "entries"
if nn == 1 then str = "entry" end
modpol.ocutil.log (nn .. " old ledgers loaded from disk")
else
modpol.ocutil.log ("No stored old ledgers data found")
end
end
-- ===================================================================
-- This function loads stored data from disk.
modpol.load_storage = function()
load_ledger()
load_orgs()
-- load_old_ledgers()
end
-- ===================================================================
-- End of file.

@@ -0,0 +1,13 @@
dofile('../modpol.lua');
modpol.orgs.reset()
test_org = modpol.instance:add_org('test_org', 'luke')
test_org:add_member('nathan')
print(table.concat(test_org:list_members(), ", "))
test_org:call_module(
"join_org_consent",
"paul"
)

@@ -0,0 +1,15 @@
dofile('../modpol.lua');
modpol.orgs.reset()
test_org = modpol.instance:add_org('test_org', 'luke')
test_org:add_member('nathan')
print(table.concat(test_org:list_members(), ", "))
-- modpol.modules.join_org.initiate("paul", test_org)
test_org:call_module(
"join_org_class",
"paul"
)

@@ -0,0 +1,23 @@
dofile("../modpol.lua")
print('\nRemoving existing orgs')
modpol.orgs.reset()
print('\nCreating an org called "test_org"')
test_org = modpol.instance:add_org('test_org', 'luke')
print('\nTrying to create an org with the same name')
duplicate = modpol.instance:add_org('test_org', 'luke')
print('\nAdding user "nathan" to test_org')
test_org:add_member('nathan')
print('\nTrying to add duplicate user to test_org')
test_org:add_member('nathan')
print('\nRemoving user "nathan" from test_org')
test_org:remove_member('nathan')
print('\nTrying to remove user "nathan" from empty member list')
test_org:remove_member('nathan')

@@ -0,0 +1,53 @@
dofile('../modpol.lua');
modpol.orgs.reset()
test_org = modpol.instance:add_org('test_org', 'lukvmil')
test_org:add_member('luke')
test_org:add_member('nathan')
test_org:set_policy("add_member", "consent", false);
new_request = {
user = "josh",
type = "add_member",
params = {"josh"}
}
request_id = test_org:make_request(new_request)
-- process_id = test_org:create_process("consent", request_id)
for id, process in ipairs(test_org.processes) do
process:approve('luke', true)
process:approve('nathan', true)
end
-- process = test_org.processes[process_id]
-- process:approve("luke", true)
-- process:approve("nathan", true)
modpol.instance:set_policy("add_org", "consent", false);
new_request = {
user = "luke",
type = "add_org",
params = {"new_org"}
}
modpol.instance:add_member('luke')
modpol.instance:add_member('josh')
modpol.instance:add_member('nathan')
request_id = modpol.instance:make_request(new_request)
modpol.instance:make_request({
user="luke",
type="add_org",
params={"second_org"}
})
modpol.interactions.login()
for id, process in ipairs(modpol.instance.processes) do
-- process:approve('luke', true)
process:approve('josh', true)
end

@@ -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.instance
and modpol.instance.members then
-- if instance exists and has membership
users = modpol.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

@@ -0,0 +1,743 @@
-- ===================================================================
-- Overview.
-- This file is "top/modpol.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).
-- ===================================================================
modpol.ocutil = {}
modpol.ocutil.log_console = true -- Flag: Copy log to console
modpol.ocutil.logdir = nil -- Absolute path for log-file direc-
-- tory (or nil)
-- ===================================================================
-- Function: modpol.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.
-- ===================================================================
modpol.ocutil.fixnil = function (s)
if s == nil then s = "(nil)"
elseif s == "" then s = "(empty)"
end
return s
end
-- ===================================================================
-- Function: modpol.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 "modpol.ocutil.log" to put
-- log files in that directory.
-- ===================================================================
modpol.ocutil.setlogdir = function (path)
if path ~= nil and path ~= "" then
modpol.ocutil.logdir = path
end
end
-- ===================================================================
-- Function: modpol.ocutil.setlogname
-- Params: string name
-- Outputs:
--
-- The input string should be a filename without a path component.
-- This function tells "modpol.ocutil.log" to use the specified filename for
-- the main log file.
--
-- "modpol.ocutil.setlogdir" must be called separately to set the log-file
-- directory.
-- ===================================================================
modpol.ocutil.setlogname = function (name)
if name ~= nil and name ~= "" then
modpol.ocutil.logname = name
end
end
-- ===================================================================
-- Function: modpol.ocutil.log
-- Params: string s
-- Outputs:
--
-- Logs the specified string. Appends a newline if not already pre-
-- sent.
-- ===================================================================
modpol.ocutil.log = function (s)
s = modpol.ocutil.fixnil (s)
s = s:gsub ("\n$", "", 1) -- Remove trailing newline initially
if modpol.ocutil.log_console then
print (s)
end
if modpol.ocutil.logdir ~= nil and
modpol.ocutil.logname ~= nil then
s = s .. "\n" -- Add trailing newline
local logpath = modpol.ocutil.logdir .. "/" .. modpol.ocutil.logname
modpol.ocutil.file_append (logpath, s)
end
end
-- ===================================================================
-- Function: modpol.ocutil.fatal_error
-- Params: string s
-- Outputs:
--
-- Logs "Fatal error: " plus the specified string. Appends a newline
-- if not already present. Terminates the program.
-- ===================================================================
modpol.ocutil.fatal_error = function (s)
modpol.ocutil.log ("Fatal Error: " .. s)
os.exit (1)
return nil -- Shouldn't be reached
end
-- ===================================================================
-- Function: modpol.ocutil.panic
-- Params: string s
-- Outputs:
--
-- Logs "Internal error: " plus the specified string. Appends a new-
-- line if not already present. Terminates the program.
-- ===================================================================
modpol.ocutil.panic = function (s)
modpol.ocutil.log ("Internal Error: " .. s)
os.exit (1)
return nil -- Shouldn't be reached
end
-- ===================================================================
-- Function: modpol.ocutil.str_empty
-- Params: string x
-- Outputs:
--
-- Returns true if the string is nil or empty
-- Returns false otherwise
-- ===================================================================
modpol.ocutil.str_empty = function (x)
if x == nil or x == "" then
return true
else
return false
end
end
-- ===================================================================
-- Function: modpol.ocutil.str_nonempty
-- Params: string x
-- Outputs:
--
-- Returns true if the string is nil or empty
-- Returns false otherwise
-- ===================================================================
modpol.ocutil.str_nonempty = function (x)
if x == nil or x == "" then
return false
else
return true
end
end
-- ===================================================================
-- Function: modpol.ocutil.table_empty
-- Params: table tab
-- Outputs:
--
-- Returns true if the table is empty
-- Returns false otherwise
-- ===================================================================
modpol.ocutil.table_empty = function (tab)
local next = next
if next (tab) == nil then return true end
return false
end
-- ===================================================================
-- Function: modpol.ocutil.table_nonempty
-- Params: table tab
-- Outputs:
--
-- Returns false if the table is empty
-- Returns true otherwise
-- ===================================================================
modpol.ocutil.table_nonempty = function (tab)
if modpol.ocutil.table_empty (tab) then return false end
return true
end
-- ===================================================================
-- Function: modpol.ocutil.string_contains
-- Params: strings a, b
-- Outputs:
--
-- Returns true if the 1st string contains the 2nd one
-- Returns false otherwise
-- ===================================================================
modpol.ocutil.str_contains = function (a, b)
if string.match (a, b) then
return true
else
return false
end
end
-- ===================================================================
-- Function: modpol.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.
-- ===================================================================
modpol.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: modpol.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.
-- ===================================================================
modpol.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: modpol.ocutil.starts_with
-- Params: strings String, Start
-- Outputs:
--
-- Returns true if the 1st string starts with the 2nd one
-- Returns false otherwise
-- ===================================================================
modpol.ocutil.starts_with = function (String, Start)
if string.sub (String, 1, string.len (Start)) == Start then
return true
else
return false
end
end
-- ===================================================================
-- Function: modpol.ocutil.not_starts_with
-- Params: strings String, Start
-- Outputs:
--
-- Returns false if the 1st string starts with the 2nd one
-- Returns true otherwise
-- ===================================================================
modpol.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: modpol.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
-- ===================================================================
modpol.ocutil.ends_with = function (String, End)
return End == '' or string.sub (String,
-string.len (End)) == End
end
-- ===================================================================
-- Function: modpol.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
-- ===================================================================
modpol.ocutil.not_ends_with = function (String, End)
if modpol.ocutil.ends_with (String, End) then
return false
else
return true
end
end
-- ===================================================================
-- Function: modpol.ocutil.firstch
-- Params: string str
-- Outputs:
-- Returns the 1st character of the string
-- ===================================================================
modpol.ocutil.firstch = function (str)
return string.sub (str, 1, 1)
end
-- ===================================================================
-- Function: modpol.ocutil.first_to_upper
-- Params: string str
-- Outputs:
-- Returns the 1st character of the string in upper case
-- ===================================================================
modpol.ocutil.first_to_upper = function (str)
return (str:gsub ("^%l", string.upper))
end
-- ===================================================================
-- Function: modpol.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.
-- ===================================================================
modpol.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 modpol.ocutil.str_true (cvtspace) then
str = str:gsub ("_", " ")
end
return (str)
end
-- ===================================================================
-- Function: modpol.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".
-- ===================================================================
modpol.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: modpol.ocutil.swap_key_value
-- Params: table itable
-- Outputs:
-- Turns keys into values and vice versa and returns the resulting
-- table.
-- ===================================================================
modpol.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: modpol.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 = modpol.ocutil.ktable_to_vtable (tabby)
--
-- print ("\nOutput table:")
-- for ii, value in ipairs (tabby) do
-- print (ii .. ") " .. tabby [ii])
-- end
-- ===================================================================
modpol.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: modpol.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 = modpol.ocutil.vtable_to_ktable (tabby, 42)
--
-- print ("\nOutput table:")
-- for ii, value in pairs (tabby) do
-- print (ii .. ") " .. tabby [ii])
-- end
-- ===================================================================
modpol.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: modpol.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 = modpol.ocutil.make_set { "red", "green", "blue" }
-- if color_set ["red"] ~= nil then print ("Supported color") end
-- ===================================================================
modpol.ocutil.make_set = function (list)
local set = {}
for _, l in ipairs (list) do set [l] = true end
return set
end
-- ===================================================================
-- Function: modpol.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.
-- ===================================================================
modpol.ocutil.pos_to_str = function (pos)
return pos.x .. "," .. pos.y .. "," .. pos.z
end
-- ===================================================================
-- Function: modpol.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).
-- ===================================================================
modpol.ocutil.table_length = function (tabby)
local count = 0
for _ in pairs (tabby) do count = count+1 end
return count
end
-- ===================================================================
-- Function: modpol.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 "modpol.ocutil.table_clone".
-- ===================================================================
modpol.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 = modpol.ocutil.clone_table (v) end
copy [k] = v
end
return copy
end
modpol.ocutil.table_clone = modpol.ocutil.clone_table
-- ===================================================================
-- Function: modpol.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.
-- ===================================================================
modpol.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: modpol.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.
-- ===================================================================
modpol.ocutil.file_missing = function (path)
if modpol.ocutil.file_exists (path) then return false end
return true
end
-- ===================================================================
-- Function: modpol.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.
-- ===================================================================
modpol.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: modpol.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.
-- ===================================================================
modpol.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: modpol.ocutil.file_append
-- Params: strings path, data
-- Outputs:
--
-- This function is identical to "modpol.ocutil.file_write" except for one
-- difference: It appends to existing files as opposed to overwrites
-- them.
-- ===================================================================
modpol.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.

@@ -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.

@@ -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.

Binary file not shown.

@@ -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