#pragma once #include #include #include "util/Types.h" #include "util/TupleSub.h" #include "util/FunctionTraits.h" /** SFINAE Helpers. */ namespace { /** Struct used for identifying variants in SFINAE. */ template struct is_variant : std::false_type {}; template struct is_variant> : std::true_type {}; /** Const bool used for identifying variants in SFINAE. */ template inline constexpr bool is_variant_v = is_variant::value; /** Struct used for identifying optionals in SFINAE. */ template struct is_optional : std::false_type {}; template struct is_optional> : std::true_type {}; /** Const bool used for identifying optionals in SFINAE. */ template inline constexpr bool is_optional_v = is_optional::value; } /** * Exposes a method to parse a string containing recursive functions, * e.g: "tint(0, crop(0, 0, 16, 16, my_texture))". * Functions are defined using template parameters and coerced from the string, * executed in the order that they are used in the string and used to create an output. * * @tparam R - The type that parse() and all declared functions will return. * @tparam C - An optional context type, which an instance of can be passed to parse() and accessed in functions. */ template class StringParser { /** A type alias for a vector of string parameters that will be passed into a Fn. */ using STR_ARGS = const vec&; /** A wrapped function that will accept an STR_ARGS and a Context, and execute a defined function with them. */ using PARSE_FN = std::function; public: /** The data type of this parser. */ typedef R Data; /** The context type of this parser. Will be nullptr_t if no context type is specified. */ typedef C Ctx; explicit StringParser() = default; /** If set to true, when an unknown function is read it will be passed to the literal function as a string. */ void setUnknownFnsAreLiteral(bool state) { unknownFnsAreLiteral = state; } /** * Adds a function to the functions map. * For a function to be valid, it must only have parameters that are integral, floating point, * strings, the Data type, or optionals & variants of them, and return a Data instance. * It may have a reference to the context as its first argument. * * @param name - The name of the function. * @param fn - The function. */ template ::args_type, std::enable_if_t, C&>, bool> = true> void addFn(const string& name, const L& fn) { functions.emplace(name, [=, this](C& ctx, STR_ARGS strArgs) { Args args = {}; parseStrArgs(strArgs, args, ctx); return std::apply(fn, args); }); } /** * Specialization for functions containing context. * For some reason, trying to do a constexpr if to do this logic * conditionally in the above function doesn't work. * * @param name - The name of the function. * @param fn - The function. */ template ::args_type, std::enable_if_t, C&>, bool> = true> void addFn(const string& name, const L& fn) { functions.emplace(name, [=, this](C& ctx, STR_ARGS strArgs) { tuple_sub_t args = {}; parseStrArgs>(strArgs, args, ctx); return std::apply(fn, std::tuple_cat(std::tuple(ctx), args)); }); } /** * Parses a string using the functions defined. * This method may only be called if no context was specified. * * @param str - The string to parse. * @returns the parsed result. */ const R parse(string str) const { const nullptr_t ctx {}; str.erase(std::remove_if(str.begin(), str.end(), isspace), str.end()); return std::move(parseRaw(str, const_cast(ctx))); } /** * Parses a string using the functions defined. * * @param str - The string to parse. * @param ctx - The context to parse with, passed to all functions that use Ctx. * @returns the parsed result. */ const R parse(string str, C& ctx) const { str.erase(std::remove_if(str.begin(), str.end(), isspace), str.end()); return std::move(parseRaw(str, ctx)); } private: /** * Parses a the string segment provided as a function or string literal. * * @param str - The string segment to parse. * @param ctx - The context to parse with. * @returns the parsed result. */ inline const R parseRaw(const std::string_view str, C& ctx) const { if (strIsFunction(str)) { let func = parseFunction(str); const let f = functions.find(string(func.first)); if (f == functions.end()) { if (!unknownFnsAreLiteral) throw std::invalid_argument( "Unknown function '" + string(func.first) + "'."); } else return f->second(ctx, func.second); } const let& f = functions.find("_"); if (f == functions.end()) throw std::invalid_argument("Literal specified with no literal function."); return f->second(ctx, { str }); } /** * Given a string segment starting and a start position pointing to an open parenthesis, * finds the index of matching closing parenthesis. * * @param str - The string segment to search through. * @param start - The position of the opening parenthesis to start at. * @returns the index of the start parenthesis' closing parenthesis. * @throws if the parentheses are unbalanced. */ const usize findClosingParen(const std::string_view& str, usize start) const { usize levels = 0; for (usize i = start + 1; i < str.size(); i++) { if (str[i] == '(') levels++; else if (str[i] == ')') { if (levels > 0) levels--; else return i; } } throw std::invalid_argument("Mismatched parentheses."); } /** * Checks if the string segment provided is a function call. * * @param str - the string segment to check. * @returns a boolean indicating if the string segment is a function call. */ const bool strIsFunction(const std::string_view& str) const { return str.find_first_of('(') != string::npos; } /** * Parses a function and finds its name and parameters. * Assumes that the string is a function, which should be checked beforehand with strIsFunction. * * @param str - The string to parse as a function. * @returns a pair containing the function name, and a vector of parameter strings. */ const std::pair> parseFunction(const std::string_view& str) const { let firstParen = str.find_first_of('('); let name = str.substr(0, firstParen); vec args {}; let end = findClosingParen(str, firstParen); let s = firstParen + 1; while (s <= end) { let nextParen = str.find_first_of('(', s); let nextComma = str.find_first_of(',', s); if (nextParen != string::npos && nextParen < nextComma) nextComma = findClosingParen(str, nextParen) + 1; if (nextComma == string::npos) nextComma = end; args.emplace_back(str.substr(s, nextComma - s)); s = nextComma + 1; } return { name, args }; } /** * Parses a string argument from an STR_ARGS. * * @tparam T - std::string * @tparam I - The index of the parameter to parse in the args vector. * @param args - An STR_ARGS to pull the parameter from. * @returns the string value. * @throws if the parameter cannot be interpreted as a string, or if there were not enough parameters. */ template , bool> = true> T parseStrArg(STR_ARGS args, C&) { if (I >= args.size()) throw std::invalid_argument("Not enough parameters."); return string(args[I]); } /** * Parses an integral argument from an STR_ARGS. * * @tparam T - An integral type, e.g u8, i32, usize... * @tparam I - The index of the parameter to parse in the args vector. * @param args - An STR_ARGS to pull the parameter from. * @returns the integral value. * @throws if the parameter cannot be interpreted as an integer, or if there were not enough parameters. */ template , bool> = true> T parseStrArg(STR_ARGS args, C&) { if (I >= args.size()) throw std::invalid_argument("Not enough parameters."); return std::stoi(args[I].data()); } /** * Parses a floating point argument from an STR_ARGS. * * @tparam T - A floating point type, i.e f32 or f64. * @tparam I - The index of the parameter to parse in the args vector. * @param args - An STR_ARGS to pull the parameter from. * @returns the floating point value. * @throws if the parameter cannot be interpreted as a floating point, or if there were not enough parameters. */ template , bool> = true> T parseStrArg(STR_ARGS args, C&) { if (I >= args.size()) throw std::invalid_argument("Not enough parameters."); return std::stod(args[I].data()); } /** * Parses a Data type argument from an STR_ARGS. * A parse function will have a Data type argument if it wants to accept the output from another function. * * @tparam T - The Data type. * @tparam I - The index of the parameter to parse in the args vector. * @param args - An STR_ARGS to pull the parameter from. * @param ctx - The context to pass into the parse functions. * @returns the Data result from whatever inner functions were executed. * @throws if the parameter cannot be parsed as a Data type, or if there were not enough parameters. */ template , bool> = true> T parseStrArg(STR_ARGS args, C& ctx) { if (I >= args.size()) throw std::invalid_argument("Not enough parameters."); return parseRaw(parseStrArg(args, ctx), ctx); } /** * Compile-time iterates over a variant type, checking if the parameter specified * matches any of the types in it. If one matches, it is parsed and returned. * * @tparam T - A variant type containing other parsable types. * @tparam I - The index of the parameter to parse in the **args vector.** * @tparam VI - The index of the variant to try to parse. * @param args - An STR_ARGS to pull the parameter from. * @param ctx - The context to pass into the parse functions. * @returns a variant containing the first type that the parameter can be parsed as. * @throws if the parameter cannot be parsed as any of the variant types. */ template inline T parseStrVariantArg(STR_ARGS args, C& ctx) { try { return parseStrArg, I>(args, ctx); } catch (...) {} if constexpr (VI + 1 < std::variant_size_v) { return parseStrVariantArg(args, ctx); } else { throw std::invalid_argument("Argument does not match types required."); } } /** * Parses a variant argument from an STR_ARGS. * The variant can contain any other parsable types. * * @tparam T - The variant type. * @tparam I - The index of the parameter to parse in the args vector. * @param args - An STR_ARGS to pull the parameter from. * @param ctx - The context to pass into the parse functions. * @returns a variant containing the first type that the parameter can be parsed as. * @throws if the parameter cannot be parsed as any of the variant types, or if there were not enough parameters. */ template, bool> = true> T parseStrArg(STR_ARGS args, C& ctx) { if (I >= args.size()) throw std::invalid_argument("Not enough parameters."); return parseStrVariantArg(args, ctx); } /** * Parses an optional argument from an STR_ARGS. * The optional can contain any parsable type, including variants and other optionals.* * * but why the hell would you do that? * * @tparam T - The optional type. * @tparam I - The index of the parameter to parse in the args vector. * @param args - An STR_ARGS to pull the parameter from. * @param ctx - The context to pass into the parse function. * @returns an optional that may contain a value. */ template, bool> = true> T parseStrArg(STR_ARGS args, C& ctx) { if (I >= args.size() || args[I].empty()) return {}; return parseStrArg(args, ctx); } /** * Compile-time iterates over a tuple of the argument types and parses them, * inserting the parsed parameters into the args tuple referenced. * * @tparam T - The tuple type of the arguments. * @tparam I - The index of the current parameter being parsed. Used for iterating. * @param strArgs - The STR_ARGS storing the string parameters. * @param args - The args tuple to insert the parsed arguments into. * @param ctx - The context to pass into parse functions. * @throws if any of the parameters cannot be parsed, see parseStrArg implementations for details. */ template inline void parseStrArgs(STR_ARGS strArgs, T& args, C& ctx) { std::get(args) = parseStrArg, I>(strArgs, ctx); if constexpr (I + 1 < std::tuple_size_v) parseStrArgs(strArgs, args, ctx); } /** A map of parse functions, indexed by their name. */ std::unordered_map functions {}; /** If true, unknown functions will be parsed as string literals, if false, an error will be thrown. */ bool unknownFnsAreLiteral = false; };