Skip to content

Instantly share code, notes, and snippets.

@HyroVitalyProtago
Last active September 12, 2015 14:56
Show Gist options
  • Save HyroVitalyProtago/7802bd00e7d56d480379 to your computer and use it in GitHub Desktop.
Save HyroVitalyProtago/7802bd00e7d56d480379 to your computer and use it in GitHub Desktop.
Required is a little framework for Codea to manage local and gist dependencies
automatically created by Required (0.0.31) from HyroVitalyProtago
[email protected]
https://gist.github.com/7802bd00e7d56d480379
return (function()
return {
get = function()
local ret, content = pcall(function()
return assert(loadstring(readProjectTab('Required:local')))()
end)
return (ret and type(content) == 'table' and content) or {}
end,
save = function(t)
return saveProjectTab('Required:local', 'return json.decode([[' .. json.encode(t, {indent=true}) .. ']])')
end
}
end)
return (function(prnt, err, __preloads)
local Loader = required.import 'Loader'
local Env = required.import 'Env'
local Data = required.import 'Data'
local GistLoader = required.import 'GistLoader'
local Version = required.import 'Version'
local context = {}
-- todo don't just use required.import, use a cached base in function of projects, dependencies, ...
local function run(name, content)
if name:sub(1,1) == '#' then -- file for the name
elseif name == "package.json" then -- todo?
else
local moduleName = table.peek(context)..(name:match('(%w-).lua'))
local f = assert(loadstring(content))
local env = setmetatable({}, {__index = function(env, o)
if o == "required" then
return { -- override required module
path = function() print("required.path disable") end,
import = function(m)
if m:find("%w-%.%w-%.%w+") then -- external module
err("not implemented yet")
else
return required.import(table.peek(context) .. m)
end
end
}
else
return _G[o]
end
end})
f = Env.set(f, env) -- run f with another environnement
__preloads[moduleName] = { f, env }
end
end
-- todo download librairies for a faster setup phase
local gl = GistLoader(run)
return Loader(function(self, data, callback)
local typ, owner, project, version = data:match("(.-)@(.-):(.-):(.*)")
if not typ then -- lazy verification for owner, project, version
err("invalid dependency format\n\tformat: typ@owner:project:version\n\texample: gist@HyroVitalyProtago:Btn:*)")
end
prnt(string.format("required.fetch(%s, %s, %s, %s)", typ, owner, project, version))
-- get current repository (stocked in local)
local rdata = Data.get()
if rdata.name == owner then -- project self owned
local prj = rdata.repo[owner].projects[project] or err("project not found in your repository...")
table.push(context, owner .. "." .. project .. ".")
gl:push({id=prj.id, sha=Version.getShaOfBetterVersion(version, prj.versions)}):load(callback)
else
err("Not implemented yet")
end
end)
end)
return (function()
local function findenv(f)
local level = 1
repeat
local name, value = debug.getupvalue(f, level)
if name == '_ENV' then
return level, value
end
level = level + 1
until name == nil
return nil
end
local function getfenv(f)
return(select(2, findenv(f)) or _G)
end
local function setfenv(f, t)
local level = findenv(f)
if level then
debug.setupvalue(f, level, t)
end
return f
end
return {
get = getfenv,
set = setfenv
}
end)
return (function(prnt, err)
local Data = required.import 'Data'
local Pkg = required.import 'Pkg'
local GistLoader = required.import 'GistLoader'
local Version = required.import 'Version'
local function saveTab(name, content)
if name:sub(1,1) == '#' then -- file for the name
elseif name == "package.json" then
Pkg.save(json.decode(content))
else
saveProjectTab(name:match('(%w-).lua'), content)
end
end
local gl = GistLoader(saveTab)
local function fetch(dependency) -- typ@owner:project:version
local typ, owner, project, version = dependency:match("(.-)@(.-):(.-):(.*)")
if not typ then -- lazy verification for owner, project, version
err("invalid dependency format\n\tformat: typ@owner:project:version\n\texample: gist@HyroVitalyProtago:Btn:*)")
end
prnt(string.format("required.fetch(%s, %s, %s, %s)", typ, owner, project, version))
-- get current repository (stocked in local)
local rdata = Data.get()
if rdata.name == owner then -- project self owned
local prj = rdata.repo[owner].projects[project] or err("project not found in your repository...")
gl:push({id=prj.id, sha=Version.getShaOfBetterVersion(version, prj.versions)}):load(function()
close() -- or restart?
end)
else
err("Not implemented yet")
end
end
return fetch
end)
return (function()
local Gist = {
debug = true,
location = "https://api.github.com/",
accept = "application/vnd.github.v3+json",
token = readGlobalData("gist"),
list = {},
comments = {}
}
local function request(url, callback, opts)
opts = opts or {}
opts.headers = opts.headers or {}
-- set default headers
opts.headers.Accept = opts.headers.Accept or Gist.accept
opts.headers.Authorization = opts.headers.Authorization or (Gist.token and "token " .. Gist.token)
-- json encode data
opts.data = opts.data and json.encode(opts.data)
-- set path absolute
url = Gist.location .. url
-- debug mode
if Gist.debug then print("[gist]", opts.method or "GET ", url) end
-- success and fail callbacks
local success = type(callback) == "table" and callback.success or callback
local fail = type(callback) == "table" and callback.fail or alert
http.request(url, function(data, status, headers)
success(json.decode(data), status, headers)
end, fail, opts)
end
local function method(name)
return function(t)
t = t or {}
t.method = name
return t
end
end
local post = method("POST")
local put = method("PUT")
local delete = method("DELETE")
local patch = method("PATCH")
local function required(tbl, ...) -- assert if a required field is missing
tbl = tbl or {}
for _,field in ipairs({...}) do
assert(tbl[field], "required field missing : " .. field)
end
return tbl
end
local function data(args, ...)
local tbl = {}
for _,v in ipairs({...}) do
tbl[v] = args[v]
end
return tbl
end
-- GITHUB TIMESTAMP (YYYY-MM-DDTHH:MM:SSZ) to os.time
function Gist.gtimestamp(githubTime)
githubTime = githubTime:sub(1, #githubTime-1) -- remove Z
githubTime = Utilities.explode("T", githubTime)
githubTime[1] = Utilities.explode("-", githubTime[1])
githubTime[2] = Utilities.explode(":", githubTime[2])
return os.time({
year = tonumber(githubTime[1][1]),
month = tonumber(githubTime[1][2]),
day = tonumber(githubTime[1][3]),
hour = tonumber(githubTime[2][1]),
min = tonumber(githubTime[2][2]),
sec = tonumber(githubTime[2][3])
})
end
-- List gists
function Gist.user(callback)
request("user", callback)
end
-- List gists
function Gist.list.user(user, callback)
request("users/"..user.."/gists", callback)
end
function Gist.list.all(callback) -- return all public gists if called anonymously
request("gists", callback)
end
function Gist.list.allPublic(callback)
request("gists/public", callback)
end
function Gist.list.starred(callback)
request("gists/starred", callback)
end
-- Get a single gist
function Gist.get(id, callback)
request("gists/"..id, callback)
end
-- Get a single revision
function Gist.revision(id, sha, callback)
request("gists/"..id.."/"..sha, callback)
end
-- Create a gist
function Gist.create(arg, callback)
request("gists", callback, post({ data = data(required(arg, "public", "files"), "public", "files", "description") }))
end
-- Edit a gist
function Gist.edit(id, arg, callback)
request("gists/"..id, callback, patch({ data = data(required(arg, "files"), "files", "description") }))
end
-- Star a gist
function Gist.star(id, callback)
request("gists/"..id.."/star", callback, put())
end
-- Unstar a gist
function Gist.unstar(id, callback)
request("gists/"..id.."/star", callback, delete())
end
-- Check if a gist is starred
function Gist.checkStar(id, callback)
request("gists/"..id.."/star", callback)
end
-- Fork a gist
function Gist.fork(id, callback)
request("gists/"..id.."/forks", callback, post())
end
-- Delete a gist
function Gist.delete(id, callback)
request("gists/"..id, callback, delete())
end
--- GISTS COMMENTS ---
-- List comments on a gist
function Gist.comments.list(gist_id, callback)
request("gists/"..gist_id.."/comments", callback)
end
-- Get a single comment
function Gist.comments.get(gist_id, id, callback)
request("gists/"..gist_id.."/comments/"..id, callback)
end
-- Create a comment
function Gist.comments.create(gist_id, body, callback)
request("gists/"..gist_id.."/comments", callback, post({ data={ body=body }}))
end
-- Edit a comment
function Gist.comments.edit(gist_id, id, body, callback)
request("gists/"..gist_id.."/comments/"..id, callback, patch({ data={ body=body }}))
end
-- Delete a comment
function Gist.comments.delete(gist_id, id, callback)
request("gists/"..gist_id.."/comments/"..id, callback, delete())
end
return Gist
end)
return (function(prnt, err)
local Gist = required.import 'Gist'
local Loader = required.import 'Loader'
local GistLoader = class(Loader) -- iterator on gist files
-- todo replace name by infos
function GistLoader:init(gf) -- function called on each gist file (name, content)
self.gf = gf
Loader.init(self, function(self, data, callback)
local function success(gdata, status, headers)
for k,v in pairs(gdata.files) do
if not v.truncated then
self.gf(k, v.content)
else
self:push({k=k, raw_url=gdata.raw_url})
end
end
callback()
end
if data.sha then
Gist.revision(data.id, data.sha, { success = success, fail = err })
elseif data.id then
Gist.get(data.id, { success = success, fail = err })
elseif data.raw_url then
http.request(data.raw_url, function(content)
self.gf(data.k, content)
callback()
end, err)
end
end)
end
return GistLoader
end)
return (function()
--[[ Downloader
function setup()
http.request("https://api.github.com/gists/7802bd00e7d56d480379", function(data, status, headers)
http.request(json.decode(data).files["Installer.lua"].raw_url, function(data)
(assert(loadstring(data))()()).download()
end, alert)
end, alert)
end
--]]
return {
install = function() -- todo
-- 1. Generate a token with gist access
-- https://help.github.com/articles/creating-an-access-token-for-command-line-use/
--> Response get username and show a message like (welcome <username>!)
-- 2. Required generate a repository as a gist for store access of public projects (fork the main repository)
-- 3. You can use this project to navigate in public repositories (and run projects!)
-- 4. When you create a project, don't forget to add Required as a dependency
-- 5. When you launch a project for the first time, you need to configure it (name, description)
-- - you can create manualy a package tab with informations in it
-- - you can use parameters facilities when the project is launched
-- 6. All tabs should be like this:
-- return (function()
-- local dependency = required.import 'dependency'
--
-- local module = {}
-- ...
-- return module (optional)
-- end)
-- > This is useful because there is no problems with tab orders
-- ...
end,
download = function()
-- set up draw for waiting
font("HelveticaNeue-Light")
fontSize(128)
fill(50)
function draw()
background(200)
text("{ Loading }", WIDTH*.5, HEIGHT*.5)
end
-- hard coded loader
local stack = {} -- stack for truncated files (name, raw_url)
local function download_raw()
if not next(stack) then -- if there is no truncated file => done!
return close() -- restart ?
end
local ku = table.remove(stack, #stack) -- get the next truncated file
http.request(ku[2], function(data)
saveProjectTab(ku[1]:match('(%w-).lua'), data) -- save it
download_raw() -- download the next
end, alert)
end
local function success(data, status, headers)
-- 2. for each file in data.files, if no truncated, save it
data = json.decode(data)
for k,v in pairs(data.files) do
if k:sub(1,1) == '#' then -- file for the name
elseif k == 'package.json' then -- special case for package
saveProjectTab('package', 'return json.decode([[' .. v.content .. ']])') -- todo check for truncated
elseif not v.truncated then
saveProjectTab(k:match('(%w-).lua'), v.content)
else
stack[#stack+1] = {k, v.raw_url}
end
end
-- 3. download all truncated files
download_raw()
end
-- 1. Get Required project
http.request("https://api.github.com/gists/7802bd00e7d56d480379", success, alert)
end
}
end)
return (function()
local Loader = class()
function Loader:init(f)
self.f = f -- function(data, callback)
self.data = {}
end
function Loader:load(callback)
if not table.any(self.data) then
return callback()
end
self:f(table.pop(self.data), function()
self:load(callback)
end)
end
function Loader:push(o)
table.push(self.data, o)
return self
end
function Loader:pushAll(tbl)
for _,o in ipairs(tbl) do
table.push(self.data, o)
end
return self
end
return Loader
end)
return (function()
local Data = required.import 'Data'
local data = Data.get()
local name = "> " .. (data.name or "unauthenticated user") .. " <"
function setup()
-- displayMode(FULLSCREEN)
--[[
required.repo(function(repo)
data = repo.projects
fontSize(32)
function draw()
background(200)
text(table.tostring(data), WIDTH*.5, HEIGHT*.5)
end
end)
--]]
-- todo
-- - projects manager (list, details, run, update, comment, star, ...)
-- - dowloaded dependencies manager (list, details, remove, update)
font("HelveticaNeue-UltraLight")
end
function draw()
background(200)
fontSize(128)
text("{ Required }", WIDTH*.5, HEIGHT*.55)
fontSize(64)
text(name, WIDTH*.5, HEIGHT*.4)
end
end)
{
"author":"HyroVitalyProtago",
"name":"Required",
"ignore":["local"],
"description":"Required is a little framework for Codea to manage local and gist dependencies",
"id":"7802bd00e7d56d480379",
"codea":"2.3.1",
"version":"0.0.31"
}
return (function(prnt, err)
-- todo licence [MIT, GPL, Apache, NoLicense], assets (, images & videos ?)
-- README.md, ...
-- get content of package tab in project
local function get(project)
local ret, content = pcall(function()
local data = readProjectTab((project and project .. ':' or '')..'package')
return assert(loadstring(data))()
end)
return (ret and type(content) == 'table' and content) -- todo assert(name, description, version, ...)
end
-- save updated package info
local keyorder = {"id", "name", "description", "author", "version", "codea", "ignore", "dependencies"}
local function save(t)
saveProjectTab('package', 'return json.decode([[' .. json.encode(t, {indent=true, keyorder=keyorder}) .. ']])')
end
-- export pkg for gist into files table
local function export(files)
local rpkg = get('Required')
local pkg = get() -- last saved package
files['# ' .. pkg.name .. ' ' .. pkg.version] = {
content = 'automatically created by Required (' .. rpkg.version .. ') from HyroVitalyProtago\n'
.. '[email protected]\n'
.. 'https://gist.github.com/' .. rpkg.id
}
files['package.json'] = { content=json.encode(pkg, {indent=true}) }
return files
end
return {
get = get,
save = save,
export = export
}
end)
return (function(prnt, err)
local Data = required.import 'Data'
local Gist = required.import 'Gist'
-- todo transform content in trie structure
local function format(repo)
return repo
end
local function get(callback)
local repo_id = Data.get().repo[data.name].id
Gist.get(repo_id, {
success = function(data)
local jsn = data.files["repo.json"]
if not jsn.truncated then
callback(format(json.decode(jsn.content)))
else
http.request(data.raw_url, function(data)
callback(format(json.decode(data)))
end, err)
end
end,
fail = function()
err("can't get the repository...")
end
})
end
return {
get = get
}
end)
required = {}
--[[
interface required {
void path(string project) -- add the project to the path
table import(string project) -- import each file of a project, return a table with filename as keys
void fetch(string gist_id) -- download the gist in the current project
[todo] table list() -- return all downloaded project
void repo(callback(table)) -- return the repository
ui {
void create() -- create the gist and link-it (id in the package file)
void update() -- search new repositories and add-it in the repository
-- (available only if the project has been created)
void open() -- open the project in the in-app browser
void patch() -- push a new revision of the project
void minor() -- push a new revision of the project
void major() -- push a new revision of the project
[todo] void release() -- add the current version into repository (a published version shouldn't be removed)
}
}
--]]
-- local mime = require 'mime'
-- local b64 = { encode=mime.b64, decode=mime.unb64 }
local DBG = true
local app = {} -- load all functions before launch the project
local __setup -- setup of the running application
-- prepend [required] in print
local function prnt(...)
if DBG then print("[required]", ...) end
end
-- prepend [required] in errors
local function err(...)
error("[required] "..table.concat({...}, ' '))
end
-- easier addition to package.path
function required.path(p)
package.path = package.path .. ";" .. os.getenv('HOME') .. '/Documents/' .. p .. '.codea/?.lua'
end
-- add Required to the path
required.path 'Required'
-- contains loaded modules
local __modules = {}
-- contains all preloaded modules for dependencies
local __preloads = {}
-- based on require for loading libraries
function required.import(module, ...) -- args can be used only for the first load (useful for partial modules)
if not __modules[module] then -- first load of
if module:find(".-%..-%..+") then -- external dependency
local res = __preloads[module][1]()
__modules[module] = res and res() or __preloads[module][2] -- module or environnement
else -- internal dependency
__modules[module] = (require(module))(...) or true -- run file only one time (even if it return nothing)
end
end
return __modules[module]
end
required.import 'Table' -- globals
local Gist = required.import 'Gist'
local Data = required.import 'Data'
local Env = required.import 'Env'
local Version = required.import 'Version'
local Dependencies = required.import('Dependencies', prnt, err, __preloads)
local Pkg = required.import('Pkg', prnt, err)
local Repo = required.import('Repo', prnt, err)
required.repo = Repo.get
required.fetch = required.import('Fetch', prnt, err)
-- create a new gist
local function create(pkg, files, callback)
Gist.create({
public = true,
description = pkg.description,
files = Pkg.export(files) -- files with pkg informations
}, {
success = function(data, status, headers)
prnt('gist successfully created!')
prnt('id: ', data.id)
pkg.id = data.id
Pkg.save(pkg)
-- todo edit the package on gist for adding the id
callback(data, status, headers)
end,
fail = function(data, status, headers)
prnt('gist creation failed')
err(data, status, headers)
end
})
end
-- edit a gist
local function edit(pkg, files, callback)
-- fetch gist for remove old files
Gist.get(pkg.id, {
success = function(data, status, headers)
for k,_ in pairs(data.files) do
if not files[k] then
files[k] = json.null
prnt('remove file: '..k)
end
end
-- todo check for differences, if nothing is different, alert "UP-TO-DATE"
Gist.edit(pkg.id, {
description = pkg.description,
files = Pkg.export(files) -- files with pkg informations
}, {
success = function(data, status, headers)
prnt('gist successfully edited!')
callback(data, status, headers)
end,
fail = function(data, status, headers)
prnt('gist edition failed')
err(data, status, headers)
end
})
end,
fail = function(data, status, headers)
err(data, status, headers)
end
})
end
-- set up parameters
local function parameters()
local pkg = Pkg.get()
if not pkg then return end -- disable parameters
local files = {}
local tabs = listProjectTabs()
for _,tab in pairs(tabs) do
if tab ~= 'package' and (not pkg.ignore or not table.mem(tab, pkg.ignore)) then -- skip package and ignored file
files[tab .. '.lua'] = { content=readProjectTab(tab) }
end
end
local function callback(data, status, headers)
parameter.action('Open', function()
openURL(data.html_url, true)
end)
end
if pkg.id then -- todo and differences?
parameter.action('Open', function() openURL('https://gist.github.com/'..pkg.id, true) end)
for i,v in ipairs({"Patch", "Minor", "Major"}) do
parameter.action(v, function()
parameter.clear()
pkg.version = Version[v:lower().."Up"](pkg.version)
Pkg.save(pkg)
edit(pkg, files, callback)
end)
end
else
parameter.action('Create', function()
parameter.clear()
create(pkg, files, callback)
end)
end
end
-- launch application
local function launch()
prnt("set up parameters")
parameters()
prnt("launch app")
setmetatable(_G, nil) -- remove protection on _G
for k,v in pairs(app) do _G[k] = v end -- load all globals declared
collectgarbage() -- free all unused memory allocated for required
if __setup then
__setup()
else
local main = required.import 'Main'
if main and type(main) == "table" and main.setup then -- if main is also a module
main.setup()
elseif setup then -- if main is a module, but use globals
setup() -- never reached statement ?
end
end
end
-- setup of required
local function setup()
displayMode(OVERLAY)
prnt("setup")
if not Data.get().first then -- first load of Required
-- todo display welcome, ...
Data.save({
first = true,
-- todo ...
})
end
local info = Pkg.get()
if not info then
-- display: package file required for version control
return launch()
end
-- prnt("save package information")
for _,k in pairs({'Author', 'Description'}) do
saveProjectInfo(k, info[k:lower()])
end
-- todo check no redondances or conflicts (versions, ...)
Dependencies:pushAll(info.dependencies or {}):load(launch)
end
local function draw()
background(200)
font("HelveticaNeue-UltraLight")
fontSize(128)
fill(50)
text("{ Required }", WIDTH*.5, HEIGHT*.5)
end
-- protect the setup function of required
setmetatable(_G, {
__index = function(tbl, k)
if k == "setup" then
return setup
elseif k == "draw" then
return draw
end
end,
__newindex = function(tbl, k, v)
prnt('set global',k,v)
if k == "setup" then
__setup = v
else
app[k] = v
end
end
})
return (function()
-- todo test all, remove unused/useless
-- todo real functionnal paradigm (return function if not all parameters) or change parameters orders (table in first)
-- table to string
function table.tostring(t, i)
local acc = ""
i = i or 0
for k, v in pairs(t) do
local f = "\n" .. string.rep(" ", i) .. k .. ": "
if type(v) == "table" then
acc = acc .. f .. table.tostring(v, i+1)
else
acc = acc .. f .. tostring(v)
end
end
if not next(t) then
acc = acc .. "{}"
end
return acc
end
-- print entire table
function table.print(t)
print(table.tostring(t))
end
-- deepcopy on table
function table.deepcopy(orig)
local orig_type, copy = type(orig)
if orig_type == 'table' then
copy = {}
for orig_key, orig_value in next, orig, nil do
copy[table.deepcopy(orig_key)] = table.deepcopy(orig_value)
end
setmetatable(copy, table.deepcopy(getmetatable(orig)))
else -- number, string, boolean, etc
copy = orig
end
return copy
end
-- return the real size of a table based on pairs
function table.count(t)
local i = 0
for _ in pairs(t) do
i = i + 1
end
return i
end
-- return the real size of a table based on ipairs
function table.length(t)
local i = 0
for _ in pairs(t) do
i = i + 1
end
return i
end
-- Stack utilities
function table.push(tbl, o) tbl[#tbl+1] = o end
function table.pop(tbl) return table.remove(tbl, #tbl) end
function table.peek(tbl) return tbl[#tbl] end
function table.any(tbl)
for _ in ipairs(tbl) do
return true
end
return false
end
-- Merge one or more arrays
-- If the input arrays have the same string keys,
-- then the later value for that key will overwrite the previous one.
-- If, however, the arrays contain numeric keys,
-- the later value will not overwrite the original value, but will be appended.
-- Values in the input array with numeric keys will be renumbered with
-- incrementing keys starting from zero in the result array.
function table.merge(...)
local acc = {}
for _,t in pairs({...}) do
for k,v in pairs(t) do
if type(k) == "number" then
acc[#acc+1] = v
else
acc[k] = v
end
end
end
return acc
end
-- Exchanges all keys with their associated values
function table.flip(tbl)
local acc = {}
for k, v in pairs(tbl) do
acc[v] = k
end
return acc
end
-- table with OCaml functionnal paradigm
-- val rev : 'a list -> 'a list
-- List reversal.
function table.rev(tbl)
local ret = {}
for i = 1,math.floor(#tbl*.5) do
ret[#tbl-i+1] = tbl[i]
end
return ret
end
-- val append : 'a list -> 'a list -> 'a list
-- Append the second list to the first.
function table.append(tbl1, tbl2)
local ret = table.deepcopy(tbl1)
for _,v in ipairs(tbl2) do
ret[#ret+1] = v
end
return ret
end
-- val rev_append : 'a list -> 'a list -> 'a list
-- equivalent to table.append(table.rev(tbl1), table.rev(tbl2))
function table.rev_append(tbl1, tbl2) -- todo more efficient
return table.append(table.rev(tbl1), table.rev(tbl2))
end
-- val flatten : 'a list list -> 'a list
-- Concatenate a list of lists. The elements of the argument are all concatenated together (in the same order) to give the result.
function table.flatten(tbls)
local ret = {}
for i = 1,#tbls do
table.append(ret, tbls[i])
end
return ret
end
-- Iterators --
-- val iter : ('a -> unit) -> 'a list -> unit
-- List.iter f [a1; ...; an] applies function f in turn to a1; ...; an. It is equivalent to begin f a1; f a2; ...; f an; () end.
function table.iter(f, tbl)
for _,v in pairs(tbl) do
f(v)
end
end
-- val iteri : (int -> 'a -> unit) -> 'a list -> unit
-- Same as List.iter, but the function is applied to the index of the element as first argument
-- (counting from 1), and the element itself as second argument.
function table.iteri(f, tbl)
for i,v in ipairs(tbl) do
f(i,v)
end
end
-- val map : ('a -> 'b) -> 'a list -> 'b list
-- List.map f [a1; ...; an] applies function f to a1, ..., an, and builds the list [f a1; ...; f an] with the results returned by f.
function table.map(f, tbl)
local ret = {}
for k,v in pairs(tbl) do
ret[k] = f(v)
end
return ret
end
-- val mapi : (int -> 'a -> 'b) -> 'a list -> 'b list
-- Same as List.map, but the function is applied to the index of the element as first argument (counting from 1),
-- and the element itself as second argument.
function table.mapi(f, tbl)
local ret = {}
for i,v in ipairs(tbl) do
ret[i] = f(i, v)
end
return ret
end
-- val rev_map : ('a -> 'b) -> 'a list -> 'b list
-- equivalent to table.rev(table.map(f, tbl))
function table.rev_map(f, tbl) -- todo more efficient
return table.rev(table.map(f, tbl))
end
-- val fold_left : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a
-- List.fold_left f a [b1; ...; bn] is f (... (f (f a b1) b2) ...) bn.
function table.fold_left(f, pacc, tbl)
local acc = table.deepcopy(pacc)
for i,v in ipairs(tbl) do
acc = f(acc, v)
end
return acc
end
-- val fold_right : ('a -> 'b -> 'b) -> 'a list -> 'b -> 'b
-- List.fold_right f [a1; ...; an] b is f a1 (f a2 (... (f an b) ...)).
function table.fold_right(f, pacc, tbl)
local acc = table.deepcopy(pacc)
for i,v in ipairs(table.rev(tbl)) do
acc = f(acc, v)
end
return acc
end
-- Iterators on two lists --
-- val iter2 : ('a -> 'b -> unit) -> 'a list -> 'b list -> unit
-- List.iter2 f [a1; ...; an] [b1; ...; bn] calls in turn f a1 b1; ...; f an bn.
-- Raise Invalid_argument if the two lists have different lengths.
function table.iter2(f, tbl1, tbl2)
assert(#tbl1 == #tbl2, "table.iter2: two lists must have same lengths")
for i = 1,#tbl1 do
f(tbl1[i], tbl2[i])
end
end
-- val map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list
-- List.map2 f [a1; ...; an] [b1; ...; bn] is [f a1 b1; ...; f an bn].
-- Raise Invalid_argument if the two lists have different lengths.
function table.map2(f, tbl1, tbl2)
assert(#tbl1 == #tbl2, "table.map2: two lists must have same lengths")
local ret = {}
for i = 1,#tbl1 do
ret[#ret + 1] = f(tbl1[i], tbl2[i])
end
return ret
end
-- val rev_map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list
-- equivalent to table.rev(table.map2(f, tbl1, tbl2))
function table.rev_map2(f, tbl1, tbl2) -- todo more efficient
return table.rev(table.map2(f, tbl1, tbl2))
end
-- val fold_left2 : ('a -> 'b -> 'c -> 'a) -> 'a -> 'b list -> 'c list -> 'a
-- List.fold_left2 f a [b1; ...; bn] [c1; ...; cn] is f (... (f (f a b1 c1) b2 c2) ...) bn cn.
-- Raise Invalid_argument if the two lists have different lengths.
function table.fold_left2(f, pacc, tbl1, tbl2)
assert(#tbl1 == #tbl2, "table.fold_left2: two lists must have same lengths")
local acc = table.deepcopy(pacc)
for i = 1,#tbl1 do
acc = f(acc, tbl1[i], tbl2[i])
end
return acc
end
-- val fold_right2 : ('a -> 'b -> 'c -> 'c) -> 'a list -> 'b list -> 'c -> 'c
-- List.fold_right2 f [a1; ...; an] [b1; ...; bn] c is f a1 b1 (f a2 b2 (... (f an bn c) ...)).
-- Raise Invalid_argument if the two lists have different lengths.
function table.fold_right2(f, pacc, tbl1, tbl2)
assert(#tbl1 == #tbl2, "table.fold_right2: two lists must have same lengths")
local acc = table.deepcopy(pacc)
for i = #tbl1,1,-1 do
acc = f(acc, tbl1[i], tbl2[i])
end
return acc
end
-- List scanning --
-- val for_all : ('a -> bool) -> 'a list -> bool
-- for_all p [a1; ...; an] checks if all elements of the list satisfy the predicate p.
-- That is, it returns (p a1) && (p a2) && ... && (p an).
function table.for_all(f, tbl)
for i,v in pairs(tbl) do
if not f(v) then
return false
end
end
return true
end
-- val exists : ('a -> bool) -> 'a list -> bool
-- exists p [a1; ...; an] checks if at least one element of the list satisfies the predicate p.
-- That is, it returns (p a1) || (p a2) || ... || (p an).
function table.exists(f, tbl)
for i,v in pairs(tbl) do
if f(v) then
return true
end
end
return false
end
-- val for_all2 : ('a -> 'b -> bool) -> 'a list -> 'b list -> bool
-- Same as List.for_all, but for a two-argument predicate. Raise Invalid_argument if the two lists have different lengths.
-- val exists2 : ('a -> 'b -> bool) -> 'a list -> 'b list -> bool
-- Same as List.exists, but for a two-argument predicate. Raise Invalid_argument if the two lists have different lengths.
-- val mem : 'a -> 'a list -> bool
-- mem a l is true if and only if a is equal to an element of l.
function table.mem(v, tbl)
return table.exists(function(e)
return e == v
end, tbl)
end
-- val memq : 'a -> 'a list -> bool
-- Same as List.mem, but uses physical equality instead of structural equality to compare list elements.
-- List searching --
-- val find : ('a -> bool) -> 'a list -> 'a
-- find p l returns the first element of the list l that satisfies the predicate p.
-- Raise Not_found if there is no value that satisfies p in the list l.
function table.find(f, tbl)
for _,v in pairs(tbl) do
if f(v) then
return v
end
end
return nil
end
-- val filter : ('a -> bool) -> 'a list -> 'a list
-- filter p l returns all the elements of the list l that satisfy the predicate p.
-- The order of the elements in the input list is preserved.
function table.filter(f, tbl)
local ret = {}
for _,v in pairs(tbl) do
if f(v) then
ret[#ret + 1] = v
end
end
return ret
end
-- val partition : ('a -> bool) -> 'a list -> 'a list * 'a list
-- partition p l returns a pair of lists (l1, l2), where l1 is the list of all the elements of l that satisfy the predicate p,
-- and l2 is the list of all the elements of l that do not satisfy p. The order of the elements in the input list is preserved.
function table.partition(f, tbl)
local ret1, ret2 = {}, {}
for _,v in ipairs(tbl) do
if f(v) then
ret1[#ret1 + 1] = v
else
ret2[#ret2 + 1] = v
end
end
return ret1, ret2
end
-- Association lists --
-- val mem_assoc : 'a -> ('a * 'b) list -> bool
-- Same as List.assoc, but simply return true if a binding exists, and false if no bindings exist for the given key.
function table.mem_assoc(k, tbl)
return tbl[k] ~= nil
end
-- val remove_assoc : 'a -> ('a * 'b) list -> ('a * 'b) list
-- remove_assoc a l returns the list of pairs l without the first pair with key a, if any.
function table.remove_assoc(k, tbl)
local ret = table.deepcopy(tbl)
ret[k] = nil
return ret
end
-- Lists of pairs --
-- val split : ('a * 'b) list -> 'a list * 'b list
-- Transform a list of pairs into a pair of lists: split [(a1,b1); ...; (an,bn)] is ([a1; ...; an], [b1; ...; bn]).
function table.split(tbl)
local ret1, ret2 = {}, {}
for k,v in pairs(tbl) do
ret1[#ret1 + 1] = k
ret2[#ret2 + 1] = v
end
return ret1, ret2
end
-- val combine : 'a list -> 'b list -> ('a * 'b) list
-- Transform a pair of lists into a list of pairs: combine [a1; ...; an] [b1; ...; bn] is [(a1,b1); ...; (an,bn)].
-- Raise Invalid_argument if the two lists have different lengths.
function table.combine(tbl1, tbl2)
assert(#tbl1 == #tbl2, "table.combine: two lists must have same lengths")
local ret = {}
for i = 1,#tbl1 do
ret[tbl1[i]] = tbl2[i]
end
return ret
end
end)
return (function()
local function fromString(str)
assert(str and type(str) == "string" and str:find('%d-%.%d-%.%d+'))
return str:match('(%d+)%.(%d+)%.(%d+)')
end
local function toString(major, minor, patch)
return string.format('%d.%d.%d', major, minor, patch)
end
local function majorUp(str)
local major, minor, patch = fromString(str)
return toString(major+1, 0, 0)
end
local function minorUp(str)
local major, minor, patch = fromString(str)
return toString(major, minor+1, 0)
end
local function patchUp(str)
local major, minor, patch = fromString(str)
return toString(major, minor, patch+1)
end
-- return the value of the biggest key
local function getValueOfMaxKey(tbl)
local kMax = next(tbl)
for k,_ in pairs(tbl) do
if tonumber(k) > tonumber(kMax) then
kMax = k
end
end
return tbl[kMax]
end
-- todo clean code
-- return the sha of the better version
local function getShaOfBetterVersion(version, versions)
local betterVersion
if version:find("%d+%.%d+%.%d+") then -- MAJOR.MINOR.PATCH
local major, minor, patch = version:match("(%d-)%.(%d-)%.(.*)")
betterVersion = versions[major] and versions[major][minor] and versions[major][minor][patch]
elseif version:find("%d+%.%d+%.%*") then -- MAJOR.MINOR
local major, minor = version:match("(%d-)%.(%d-)%.%*")
betterVersion = versions[major] and versions[major][minor] and getValueOfMaxKey(versions[major][minor])
elseif version:find("%d+%.%*") then -- MAJOR
local major = version:match("(%d-).*")
betterVersion = versions[major] and getValueOfMaxKey(getValueOfMaxKey(versions[major]))
elseif version:find("%*") then -- LATEST
betterVersion = getValueOfMaxKey(getValueOfMaxKey(getValueOfMaxKey(versions))) -- or return nil?
else
error("bad formatted version\n\texpected: * | MAJOR.* | MAJOR.MINOR.* | MAJOR.MINOR.PATCH\n\texample: 1.32.*")
end
return betterVersion or err(table.format("version %s not found", version))
end
return {
fromString = fromString,
toString = toString,
majorUp = majorUp,
minorUp = minorUp,
patchUp = patchUp,
getShaOfBetterVersion = getShaOfBetterVersion
}
end)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment