#include "wrap-string.hpp" #include "keywords.hpp" #include "util.hpp" static LuaToken token_error("error"); static LuaToken token_found("found"); LuaKeywordParser::LuaKeywordParser(const LuaCoreStack &LS0, LuaSlot slot) : keytab(slot.index()), LS(LS0.state()) { lua_State *L = LS0.state(); lua_pushnil(L); found.index_ = lua_gettop(L); lua_pushnil(L); error.index_ = lua_gettop(L); lua_pushnil(L); key.index_ = lua_gettop(L); lua_pushnil(L); val.index_ = lua_gettop(L); istable = LS.istable(keytab); if (istable) { LS.rawget(found, keytab, token_found); if (!LS.istable(found)) { LS.set(found, LuaNewTable); LS.rawset(keytab, token_found, found); } } } bool LuaKeywordParser::optional(LuaSlot out, std::string_view kw) { if (!istable) { LS.set(out, LuaNil); return false; } LS.rawget(out, keytab, kw); if (!LS.isnil(out)) { LS.rawset(found, kw, true); return true; } else { return false; } }; bool LuaKeywordParser::required(LuaSlot out, std::string_view kw) { if (!istable) { LS.set(out, LuaNil); return false; } LS.rawget(out, keytab, kw); if (!LS.isnil(out)) { LS.rawset(found, kw, true); return true; } else { LS.rawget(error, keytab, token_error); if (!LS.isstring(error)) { LS.rawset(keytab, token_error, util::ss("required keyword argument not present: ", kw)); } return false; } }; eng::string LuaKeywordParser::check() { if (!istable) { return "keyword arguments must be a table"; } LS.rawget(error, keytab, token_error); auto str = LS.trystring(error); if (str.has_value()) return str.value(); return ""; } eng::string LuaKeywordParser::final_check() { if (!istable) { return "keyword arguments must be a table"; } LS.rawget(error, keytab, token_error); auto str = LS.trystring(error); if (str.has_value()) return str.value(); LS.set(key, LuaNil); while (LS.next(keytab, key, val)) { if (LS.istoken(key)) { continue; } auto kw = LS.trystringview(key); if (!kw.has_value()) { return "keyword arguments include a non-string key"; } LS.rawget(val, found, key); if (!LS.rawequal(val, true)) { return util::ss("unrecognized keyword argument: ", kw.value()); } } return ""; } void LuaKeywordParser::check_throw() { eng::string err = check(); if (!err.empty()) { luaL_error(LS.state(), "%s", err.c_str()); } } void LuaKeywordParser::final_check_throw() { eng::string err = final_check(); if (!err.empty()) { luaL_error(LS.state(), "%s", err.c_str()); } } static const char *kwdoc = "|Parse lua keyword arguments." "|" "|The keywords module is used to help parse keyword arguments" "|for functions. The error handling is strong, it throws useful" "|error messages when the caller passes in incorrect arguments." "|This is a typical example of how it is used:" "|" "| function drawrect(args)" "| local x1,y1 = keywords.required(args, 'x1', 'y1')" "| local x2,y2 = keywords.required(args, 'x2', 'y2')" "| local color = keywords.optional(args, 'color')" "| keywords.finalcheckthrow(args)" "| ..." "| end" "|" "|The function above expects four required keyword arguments and one" "|optional one. The example code above handles all the following" "|error cases:" "|" "| * That 'args' actually is a table." "| * That the required keywords x1,y1,x2,y2 are all present." "| * That the table doesn't contain extraneous unknown keywords." "|" "|The functions keywords.required and keywords.optional don't throw" "|lua errors. Instead, they store status information in the keyword" "|table itself. Using the status information, the function" "|keywords.finalcheckthrow will throw an error if anything went wrong" "|along the way." "|" "|You can check errors when you're not finished parsing using the" "|function keywords.checkthrow() instead of keywords.finalcheckthrow()." "|Unlike finalcheckthrow, this function doesn't verify the absence of" "|extraneous keywords, which can only be done at the end." "|" "|If you don't want to throw errors at all, you can use" "|keywords.finalcheck() instead of keywords.finalcheckthrow()." "|This returns the error message, rather than throwing it." "|"; LuaDefine(keywords_optional, "table, keyword, keyword, keyword...", kwdoc) { LuaArg table; LuaExtraArgs keywords; LuaDefStack LS(L, table, keywords); LuaKeywordParser KP(LS, table); if (keywords.size() < 1) { luaL_error(L, "expected at least one keyword"); return 0; } for (int i = 0; i < keywords.size(); i++) { eng::string kw = LS.ckstring(keywords[i], "keyword"); KP.optional(keywords[i], kw); } // Return the results without using LS.result, because it doesn't // support multiple return values. lua_settop(L, keywords[keywords.size() - 1].index()); return keywords.size(); } LuaDefine(keywords_required, "table, keyword, keyword, keyword...", kwdoc) { LuaArg table; LuaExtraArgs keywords; LuaDefStack LS(L, table, keywords); LuaKeywordParser KP(LS, table); if (keywords.size() < 1) { luaL_error(L, "expected at least one keyword"); return 0; } for (int i = 0; i < keywords.size(); i++) { eng::string kw = LS.ckstring(keywords[i], "keyword"); KP.required(keywords[i], kw); } // Return the results without using LS.result, because it doesn't // support multiple return values. lua_settop(L, keywords[keywords.size() - 1].index()); return keywords.size(); } LuaDefine(keywords_check, "table", kwdoc) { LuaArg table; LuaRet result; LuaDefStack LS(L, table, result); LuaKeywordParser KP(LS, table); eng::string err = KP.check(); if (!err.empty()) { LS.set(result, err); } return LS.result(); } LuaDefine(keywords_finalcheck, "table", kwdoc) { LuaArg table; LuaRet result; LuaDefStack LS(L, table, result); LuaKeywordParser KP(LS, table); eng::string err = KP.final_check(); if (!err.empty()) { LS.set(result, err); } return LS.result(); } LuaDefine(keywords_checkthrow, "table", kwdoc) { LuaArg table; LuaDefStack LS(L, table); LuaKeywordParser KP(LS, table); KP.check_throw(); return LS.result(); } LuaDefine(keywords_finalcheckthrow, "table", kwdoc) { LuaArg table; LuaDefStack LS(L, table); LuaKeywordParser KP(LS, table); KP.final_check_throw(); return LS.result(); }