Created
May 21, 2013 19:20
-
-
Save loopspace/5622444 to your computer and use it in GitHub Desktop.
CModule Release v1.00 -Module loading facility.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
v0.1.0: (5/21/2013) | |
* added search path for locating relative module paths; search path can be | |
set/inspected using cmodule.path(); pass no args for a list of projects in the path, | |
or pass a variable list of projects, i.e. cmodule.path("project1", "project2", "project3"). | |
search path may only be set once. | |
* added optional fallback project param to cimport/cload and cmodule.loaded/exists/unload; | |
if a module is not found in the running project or in the search path, an attempt will | |
be made to load from the fallback project if specified. | |
* __pathto no longer returns fully qualified path to current module when no | |
module name is specified; use __pathto(__file) instead. | |
* no longer generating module closures for cload; use cload(__proj "MyModule") instead. | |
* renamed overridden module cimport to cinclude; cinclude only takes a module | |
name, not an absolute path; it is intended for loading from the containing project | |
only (it is syntactic sugar for cimport(__pathto "MyModule")) | |
* memoized module cinclude/__pathto closures, which can reduce memory | |
footprint considerably, and speed up loading/importing as well by not having | |
to create 2 closures per model per owning project (now it's 2 closures per owning project) | |
* removed redundant cmodule.import and cmodule.load. use cimport/cload instead. | |
v0.0.9: (5/17/2013): | |
* removed cmodule.null. it is no longer necessary since module | |
module returns are no longer weak referenced, and all modules | |
loaded with cimport are kept alive unless explicitly unloaded. | |
* replaced cmodule.nocache with cmodule.cache; to achieve the same effect, | |
put this line of code in your module file: _M[cmodule.cache] = false | |
v0.0.8 (5/17/2013): | |
* cmodule now keeps strong references to module return values | |
* added API cmodule.unload() to unload a loaded module. | |
v0.0.7 (5/17/2013): | |
* cimport/cload now only return 1 value, the value returned from the loaded module | |
* added cmodule.resolve, which returns the project and file names for the given path | |
if it is valid, nil otherwise. | |
v0.0.6 (5/17/2013): | |
* cmodule.loaded now auto-applies the .lua extension if no extension is specified. | |
* cmodule.loaded auto pre-pends the running project name if no project is specified | |
v0.0.5 (5/16/2013): | |
* fixed bug from 0.0.4 where cmodule would not return nil for newly loaded | |
modules that return cmodule.null | |
* added function cmodule.gexport, that accepts a table of key value pairs | |
that are batch exported to the global environment | |
* add value cmodule.nocache that can be returned from a module to indicate | |
* that cmodule should not cache the module (useful for running once-off scripts) | |
v0.0.4 (5/15/2013): | |
* added unique null type, accessible via cmodule.null | |
* cimport will now return nil for modules that return cmodule.null | |
--]] | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- cmodule | |
-- v0.1.0 | |
-- todo: | |
-- * investigate better ways to handle dependency cycles | |
-- * support exported projects | |
-- - detect whether program is running in Codea or exported project | |
-- - reverse engineer exported project file layout | |
-- - load from alternate location when running an exported project | |
-- import global functions | |
local loadstring, fopen = loadstring, io.open | |
local insert = table.insert | |
local format, rep = string.format, string.rep | |
local setfenv, setmetatable = setfenv, setmetatable | |
local pairs, ipairs = pairs, ipairs | |
-- loaded modules | |
local _modules = {} | |
-- search path projects | |
local _path = nil | |
-- unique module environment key to specify caching options | |
local function opt_cache() return opt_cache end | |
-- path to Codea's document's directory | |
local _docs = os.getenv("HOME") .. "/Documents/" | |
-- name of currently running project | |
local _this = nil -- set by cmodule() | |
-- boilerplate function headers for modules | |
local _importHeader = "return function(_M, __proj, __file, __pathto, cinclude)" | |
local _loadHeader = "return function(__proj, __file, __pathto)" | |
-- convert codea path to filesystem path | |
local function _fspath(p, f) | |
return _docs .. p .. ".codea/" .. f | |
end | |
local function _appendExt(f) | |
return f:find('%.') and f or (f..".lua") | |
end | |
local function _removeExt(f) | |
local dot = f:find('.', 1, true) | |
return dot and f:sub(1, dot-1) or f | |
end | |
-- parse for module name when compiler exceptions occur | |
local function _shortsource(s, maxlength) | |
local max = 60 | |
s, maxlength = s:sub(1, max), maxlength or 30 | |
max = 1 + s:len() | |
local n, c | |
for i = 1, max do | |
n, c = i, s:sub(i, i) | |
if not(c == ' ' or c == '\n') then | |
break | |
end | |
end | |
if n == max then return '' end | |
local start = n | |
for i = start, max do | |
c = s:sub(i, i) | |
if not(c == '\n' or i >= start + maxlength) then | |
n = i | |
else break end | |
end | |
return s:sub(start, n) | |
end | |
-- TODO: when Codea allows optional tab execution, | |
-- we'll need to take into account added boilerplate line | |
-- at the top of the module when reporting errors | |
local function _traceback(minlevel) | |
minlevel = minlevel or 2 | |
local t = {} | |
for level = minlevel, math.huge do | |
local info = debug.getinfo(level, "Sln") | |
if not info then break end | |
if info.what == "C" then | |
insert(t, 'C function'); | |
else | |
local name, desc = info.name, _shortsource(info.source) | |
if desc == '' then desc = 'unknown' end | |
desc = '[' .. desc .. ']' | |
if name then desc = desc .. ":" .. name end | |
local currentline = info.currentline | |
if info.currentline == -1 then currentline = '?' end | |
s = desc .. ":" .. currentline .. "\n" | |
insert(t, s) | |
end | |
end | |
return "\ntrace:\n" .. table.concat(t) | |
end | |
-- remove the block comment wrapper, and | |
-- wrap the contents of a file with our boilerplate code | |
local function _wrap(s, p, t, header) | |
-- TODO: present error on malformed long comment wrapper | |
-- find the first occurrence of a long comment, so | |
-- we can see how many = are in it to refine our search | |
local b, e = s:find('--%[[%=]*%[') | |
local ne = rep('[%=]', (e-b)-3) | |
-- do end first | |
b, e = s:find('--%]' .. ne .. '%]') | |
e = s:find('\n', e, true) | |
local cs = s:sub(b, e):gsub('%]', '%%%]') | |
s = s:gsub(cs, 'end') | |
-- now do function header | |
b, e = s:find('--%[' .. ne .. '%[') | |
e = s:find('\n', e, true) | |
-- wrap the source and return it | |
local oc = '--[[ ' .. p .. ':' .. t .. ' --]] ' | |
cs = s:sub(b, e-1):gsub('%[', '%%%[') | |
return s:gsub(cs, oc..header) | |
end | |
-- check to see if a module has already been loaded; | |
-- if so, return it's data | |
local function _findLoaded(cpath, fallback) | |
local spos = cpath:find(":", 1, true) | |
if spos ~= nil then | |
cpath = _appendExt(cpath) | |
return _modules[cpath], true, cpath, cpath:sub(1, spos-1), cpath:sub(spos+1) | |
else | |
-- check running project first | |
local file, cpath = cpath, _appendExt(_this .. ":" .. cpath) | |
local mod = _modules[cpath] | |
if mod ~= nil then | |
return mod, false, cpath, _this, cpath:sub(#_this+2) | |
end | |
-- now check all of the projects in the search path | |
if _path then | |
local project | |
for i = 1, #_path do | |
project = _path[i] | |
cpath = _appendExt(project .. ":" .. file) | |
mod = _modules[cpath] | |
if mod ~= nil then | |
return mod, false, cpath, project, cpath:sub(#project+2) | |
end | |
end | |
end | |
-- check fallback | |
if fallback then | |
cpath = _appendExt(fallback .. ":" .. file) | |
mod = _modules[cpath] | |
if mod ~= nil then | |
return mod, false, cpath, fallback, cpath:sub(#fallback+2) | |
end | |
end | |
-- module is not loaded | |
return nil, false, nil, nil, _appendExt(file) | |
end | |
end | |
-- search for a module's file in the search chain | |
local function _search(f, fallback) | |
-- check running project first | |
local file = fopen(_fspath(_this, f), "r") | |
if file ~= nil then | |
return _this, file | |
end | |
-- check search path | |
if _path then | |
local p = _this | |
for i = 1, #_path do | |
p = _path[i] | |
file = fopen(_fspath(p, f), "r") | |
if file ~= nil then | |
return p, file | |
end | |
end | |
end | |
-- check fallback | |
if fallback then | |
file = fopen(_fspath(fallback, f), "r") | |
if file ~= nil then | |
return fallback, file | |
end | |
end | |
end | |
-- load a module given a file, project name, file name, and module header | |
local function _readmodule(file, p, f, header) | |
local s = file:read("*a") | |
file:close() | |
local tab = _removeExt(f) | |
local chunk, e = loadstring(_wrap(s, p, tab, header), tab) | |
if e then | |
error(format("\n\n%s\n%s", e, _traceback(4))) | |
end | |
return chunk | |
end | |
-- forward declare for _include | |
local import | |
-- memoize overridden cinclude closures | |
-- since we only need one per owning project. | |
local _overrides = {} | |
-- provide syntactic sugar for cimport when overriding | |
-- search path to load from containing project. e.g.: | |
-- cinclude "SomeModule" | |
-- is the same as | |
-- cimport(__proj "SomeModule") | |
local function _override(prefix) | |
local override = _overrides[prefix] | |
if not override then | |
override = function(modulename) | |
return _import(prefix .. modulename) | |
end | |
_overrides[prefix] = override | |
end | |
return override | |
end | |
-- default module metatable/index | |
local _defaultMT = {__index = _G} | |
-- memoize __pathto closures, since we only need | |
-- one per owning project | |
local _pathto = {} | |
-- prepare sandboxed environment for loaded modules | |
local function _makeEnv(p, f, e) | |
e = setmetatable({}, e and {__index = e} or _defaultMT) | |
local prefix = p .. ":" | |
local pathto = _pathto[prefix] | |
if not pathto then | |
pathto = function(f) return prefix .. f end | |
_pathto[prefix] = pathto | |
end | |
return e, pathto, prefix | |
end | |
-- return a string listing the current project path | |
local function _pathString() | |
local spath | |
if _path and #_path > 0 then | |
spath = "{" | |
for i, v in ipairs(_path) do | |
spath = spath .. v | |
if i < #_path then | |
spath = spath .. ", " | |
end | |
end | |
return spath .. "}" | |
end | |
return "{none}" | |
end | |
-- keep track of modules that are currently loading, | |
-- so that we can detect dependency cycles | |
local _loading = {} | |
-- import a module. imported module is memoized so | |
-- successive imports do not have to re-load the module | |
-- each time. If nothing is returned, the module environment | |
-- itself will be imported (i.e. the module's _M table) | |
function _import(codeapath, fallback) | |
local mod, absolute, cpath, p, f = _findLoaded(codeapath) | |
if mod == nil then | |
local file | |
if absolute then | |
if p then | |
file = fopen(_fspath(p, f), "r") | |
end | |
else | |
p, file = _search(f, fallback) | |
if p then cpath = p .. ":" .. f end | |
end | |
if not file then | |
local spath = _pathString() | |
error(format("Module not found: %s\n\nin search path:%s\n\nfallback:%s\n\n%s", codeapath, | |
spath, fallback or "none", _traceback(3))) | |
end | |
if _loading[cpath] then | |
error(format("circular dependency detected loading %s\n%s", cpath, _traceback(3))) | |
end | |
_loading[cpath] = true | |
local loaded = _readmodule(file, p, f, _importHeader)() | |
local env, pathto, prefix = _makeEnv(p, f) | |
mod = setfenv(loaded, env)(env, p, f, pathto, _override(prefix)) or env | |
_loading[cpath] = nil | |
local cache = env[opt_cache] | |
if cache then | |
env[opt_cache] = nil | |
if cache == false then | |
return mod | |
end | |
end | |
_modules[cpath] = mod | |
end | |
return mod | |
end | |
-- load a data module. if an environment is provided, it will | |
-- be the sandboxed module environment. if no environment | |
-- is provided, the global table will be exposed to the | |
-- module environment. | |
local function _load(codeapath, environment, fallback) | |
if not fallback and type(environment) == "string" then | |
fallback, environment = environment | |
end | |
local spos, p, f, cpath, file = codeapath:find(":", 1, true) | |
if spos ~= nil then | |
cpath = _appendExt(codeapath) | |
p, f = cpath:sub(1, spos-1), cpath:sub(spos+1) | |
file = fopen(_fspath(p, f), "r") | |
else | |
f = _appendExt(codeapath) | |
p, file = _search(f, fallback) | |
if p then cpath = p .. ":" .. f end | |
end | |
if not file then | |
local spath = _pathString() | |
error(format("Module not found: %s\n\nin search path:%s\n\nfallback:%s\n\n%s", codeapath, | |
spath or "{none}", fallback or "none", _traceback(3))) | |
end | |
if _loading[cpath] then | |
error(format("circular dependency detected loading %s\n%s", cpath, _traceback(3))) | |
end | |
_loading[cpath] = true | |
local loaded = _readmodule(file, p, f, _loadHeader)() | |
local env, pathto = _makeEnv(p, f, environment) | |
mod = setfenv(loaded, env)(p, f, pathto) or env | |
_loading[cpath] = nil | |
return mod | |
end | |
--------------------------- | |
-- exports | |
--------------------------- | |
-- import a source module; identical to cmodule.import | |
_G.cimport = _import | |
-- load a data module; identical to cmodule.load | |
_G.cload = _load | |
-- cmodule utitlities | |
_G.cmodule = setmetatable({ | |
-- set/get the project path. the path may only be set once | |
path = function(...) | |
local narg = select("#", ...) | |
-- return the current path if no params are specified | |
if narg == 0 then | |
-- return a copy of the path so it can't be modified | |
return _path and {unpack(_path)} or nil | |
end | |
if _path then | |
error("cmodule.path may only be set once") | |
end | |
_path = {...} | |
end, | |
-- return the name of the running project | |
project = function() return _this end, | |
-- unload a loaded module | |
-- params: module name or absolute module path | |
unload = function(codeapath, fallback) | |
local mod, _, cpath = _findLoaded(codeapath, fallback) | |
if mod then | |
_modules[cpath] = nil | |
end | |
end, | |
-- params: module name or absolute module path | |
-- returns: true if a module can be located and loaded; else false | |
exists = function(codeapath, fallback) | |
local spos = codeapath:find(":", 1, true) | |
local absolute = (spos ~= nil) | |
local p, f, cpath, file | |
if absolute then | |
cpath = _appendExt(codeapath) | |
p, f = cpath:sub(1, spos-1), cpath:sub(spos+1) | |
file = fopen(_fspath(p, f), "r") | |
else | |
f = _appendExt(codeapath) | |
p, file = _search(f, fallback) | |
if p then cpath = p .. ":" .. f end | |
end | |
if file then | |
file:close() | |
return true, cpath | |
end | |
return false | |
end, | |
-- params: none, module name, or absolute module path | |
-- returns: if no params are specified, array of loaded modules; | |
-- otherwise, module path if module is found, or nil if not found | |
loaded = function(codeapath, fallback) | |
if codeapath then | |
local _, _, cpath = _findLoaded(codeapath, fallback) | |
return cpath | |
else | |
local loaded = {} | |
for k in pairs(_modules) do insert(loaded, k) end | |
return loaded | |
end | |
end, | |
-- split an absolute path into separate project and file components; | |
-- if relative path is used, currently running project name is appended | |
-- params: absolute module path or module name in current project | |
-- returns: project name, tab name | |
resolve = function(cpath) | |
cpath = _appendExt(cpath) | |
local i = cpath:find(":", 1, true) | |
if i then | |
return cpath:sub(1, i-1), cpath:sub(i+1), cpath | |
end | |
return _this, cpath, _this .. ":" .. cpath | |
end, | |
-- utility to export multiple variables to the global namespace | |
-- params: table containing key/value pairs to be exported to _G | |
gexport = function(exports) | |
for k, v in pairs(exports) do | |
_G[k] = v | |
end | |
end, | |
-- by default, all modules loaded by cimport are cached. | |
-- to disable caching for a module, add this to your module file: | |
-- _M[cmodule.cache] = false | |
cache = opt_cache, | |
}, { | |
-- params: project name. | |
-- returns: cmodule.import, as a convenience. | |
-- notes: this should be executed before any | |
-- other code in your program. | |
__call = function (_, thisProject) | |
-- set currently running project name | |
_this = thisProject | |
-- return import to allow for a chained initialization/import, | |
-- for example: cmodule "myProject" "setup" | |
return _import | |
end, | |
}) | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- Main | |
-- see cmodule_tests at: https://gist.github.com/apendley/5594141 | |
-- for very basic examples of cmodule usage; | |
-- better tests are on the way. | |
GIST_VERSION = 1 | |
function setup() | |
AutoGist.setProjectInfo("CModule","toadkick","Module loading facility.",GIST_VERSION) | |
AutoGist.backup(true) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment