mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-28 07:44:56 +00:00
739 lines
24 KiB
TypeScript
739 lines
24 KiB
TypeScript
/**
|
|
* Lua Test Runner Integration
|
|
* TypeScript utilities for running Lua tests from packages
|
|
*/
|
|
|
|
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }
|
|
|
|
export interface LuaTestResult {
|
|
name: string
|
|
status: 'passed' | 'failed' | 'skipped' | 'pending'
|
|
error?: string
|
|
expected?: JsonValue
|
|
actual?: JsonValue
|
|
duration: number
|
|
}
|
|
|
|
export interface LuaSuiteResult {
|
|
name: string
|
|
tests: LuaTestResult[]
|
|
nested: LuaSuiteResult[]
|
|
stats: {
|
|
total: number
|
|
passed: number
|
|
failed: number
|
|
skipped: number
|
|
duration: number
|
|
}
|
|
}
|
|
|
|
export interface LuaTestRunResult {
|
|
suites: LuaSuiteResult[]
|
|
stats: {
|
|
total: number
|
|
passed: number
|
|
failed: number
|
|
skipped: number
|
|
duration: number
|
|
suiteCount: number
|
|
success: boolean
|
|
}
|
|
timestamp: string
|
|
}
|
|
|
|
export interface LuaTestConfig {
|
|
timeout?: number
|
|
verbose?: boolean
|
|
stopOnFirstFailure?: boolean
|
|
filter?: string
|
|
}
|
|
|
|
export interface TestCaseFiles {
|
|
[filename: string]: string // filename -> JSON content
|
|
}
|
|
|
|
/**
|
|
* Generates Lua code that runs tests and returns results
|
|
* This code uses the lua_test framework modules
|
|
*/
|
|
export function generateTestRunnerCode(
|
|
testCode: string,
|
|
config: LuaTestConfig = {},
|
|
testCaseFiles: TestCaseFiles = {}
|
|
): string {
|
|
const configJson = JSON.stringify(config)
|
|
const testCaseFilesJson = JSON.stringify(testCaseFiles)
|
|
|
|
return `
|
|
-- Load lua_test framework modules
|
|
local framework = (function()
|
|
${getFrameworkModule()}
|
|
end)()
|
|
|
|
local assertions = (function()
|
|
${getAssertionsModule()}
|
|
end)()
|
|
|
|
local mocks = (function()
|
|
${getMocksModule()}
|
|
end)()
|
|
|
|
local runner = (function()
|
|
${getRunnerModule()}
|
|
end)()
|
|
|
|
local helpers = (function()
|
|
${getHelpersModule()}
|
|
end)()
|
|
|
|
local json_parser = (function()
|
|
${getJsonParserModule()}
|
|
end)()
|
|
|
|
-- Test case files loaded from package
|
|
local _test_case_files = ${testCaseFilesJson}
|
|
|
|
-- Load test cases from a JSON file
|
|
function load_cases(filename, path)
|
|
local content = _test_case_files[filename]
|
|
if not content then
|
|
error("Test case file not found: " .. filename)
|
|
end
|
|
local data = json_parser.decode(content)
|
|
if path then
|
|
for segment in string.gmatch(path, "[^%.]+") do
|
|
if data and type(data) == "table" then
|
|
data = data[segment]
|
|
else
|
|
error("Invalid path: " .. path)
|
|
end
|
|
end
|
|
end
|
|
return data or {}
|
|
end
|
|
|
|
-- Configure framework
|
|
local config = ${configJson}
|
|
if config.timeout then framework.configure({ timeout = config.timeout }) end
|
|
if config.stopOnFirstFailure then framework.configure({ stopOnFirstFailure = config.stopOnFirstFailure }) end
|
|
if config.filter then framework.configure({ filter = config.filter }) end
|
|
|
|
-- Expose test DSL globally for the test code
|
|
describe = framework.describe
|
|
it = framework.it
|
|
xit = framework.xit
|
|
fit = framework.fit
|
|
beforeAll = framework.beforeAll
|
|
afterAll = framework.afterAll
|
|
beforeEach = framework.beforeEach
|
|
afterEach = framework.afterEach
|
|
it_each = framework.it_each
|
|
fit_each = framework.fit_each
|
|
xit_each = framework.xit_each
|
|
expect = assertions.expect
|
|
assertTrue = assertions.assertTrue
|
|
assertFalse = assertions.assertFalse
|
|
assertEqual = assertions.assertEqual
|
|
assertNotEqual = assertions.assertNotEqual
|
|
assertNil = assertions.assertNil
|
|
assertNotNil = assertions.assertNotNil
|
|
fail = assertions.fail
|
|
mock = mocks
|
|
|
|
-- Reset any previous test state
|
|
framework.reset()
|
|
|
|
-- Execute the test definitions
|
|
${testCode}
|
|
|
|
-- Run all registered tests
|
|
local results = runner.runAll(framework.getSuites(), config)
|
|
|
|
-- Return results
|
|
return results
|
|
`
|
|
}
|
|
|
|
/**
|
|
* Inlined framework module code (simplified for embedding)
|
|
*/
|
|
function getFrameworkModule(): string {
|
|
return `
|
|
local M = {}
|
|
M._suites = {}
|
|
M._currentSuite = nil
|
|
M._config = { timeout = 5000, verbose = true, stopOnFirstFailure = false, filter = nil }
|
|
|
|
function M.createSuite(name)
|
|
return { name = name, tests = {}, beforeAll = nil, afterAll = nil, beforeEach = nil, afterEach = nil, nested = {}, parent = nil }
|
|
end
|
|
|
|
function M.describe(name, fn)
|
|
local parentSuite = M._currentSuite
|
|
local suite = M.createSuite(name)
|
|
suite.parent = parentSuite
|
|
if parentSuite then parentSuite.nested[#parentSuite.nested + 1] = suite
|
|
else M._suites[#M._suites + 1] = suite end
|
|
M._currentSuite = suite
|
|
fn()
|
|
M._currentSuite = parentSuite
|
|
return suite
|
|
end
|
|
|
|
function M.it(name, fn)
|
|
if not M._currentSuite then error("it() must be called inside describe()") end
|
|
local test = { name = name, fn = fn, status = "pending", error = nil, duration = 0, skipped = false }
|
|
M._currentSuite.tests[#M._currentSuite.tests + 1] = test
|
|
return test
|
|
end
|
|
|
|
function M.xit(name, fn)
|
|
if not M._currentSuite then error("xit() must be called inside describe()") end
|
|
local test = { name = name, fn = fn, status = "skipped", skipped = true }
|
|
M._currentSuite.tests[#M._currentSuite.tests + 1] = test
|
|
return test
|
|
end
|
|
|
|
function M.fit(name, fn)
|
|
local test = M.it(name, fn)
|
|
test.only = true
|
|
return test
|
|
end
|
|
|
|
-- Parameterized test helper
|
|
local function create_each(testFn)
|
|
return function(cases)
|
|
return function(nameTemplate, fn)
|
|
for _, testCase in ipairs(cases) do
|
|
local name = nameTemplate
|
|
for key, value in pairs(testCase) do
|
|
local strValue = type(value) == "table" and "[table]" or tostring(value)
|
|
name = string.gsub(name, "%$" .. key, strValue)
|
|
end
|
|
testFn(name, function() fn(testCase) end)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function M.it_each(cases) return create_each(M.it)(cases) end
|
|
function M.fit_each(cases) return create_each(M.fit)(cases) end
|
|
function M.xit_each(cases) return create_each(M.xit)(cases) end
|
|
|
|
function M.beforeAll(fn) if M._currentSuite then M._currentSuite.beforeAll = fn end end
|
|
function M.afterAll(fn) if M._currentSuite then M._currentSuite.afterAll = fn end end
|
|
function M.beforeEach(fn) if M._currentSuite then M._currentSuite.beforeEach = fn end end
|
|
function M.afterEach(fn) if M._currentSuite then M._currentSuite.afterEach = fn end end
|
|
|
|
function M.configure(options)
|
|
for k, v in pairs(options) do
|
|
if M._config[k] ~= nil then M._config[k] = v end
|
|
end
|
|
end
|
|
|
|
function M.getSuites() return M._suites end
|
|
function M.reset() M._suites = {} M._currentSuite = nil end
|
|
function M.getConfig() return M._config end
|
|
|
|
return M
|
|
`
|
|
}
|
|
|
|
function getAssertionsModule(): string {
|
|
return `
|
|
local M = {}
|
|
|
|
local function stringify(value)
|
|
local t = type(value)
|
|
if t == "string" then return '"' .. value .. '"'
|
|
elseif t == "table" then
|
|
local parts = {}
|
|
for k, v in pairs(value) do parts[#parts + 1] = tostring(k) .. "=" .. stringify(v) end
|
|
return "{" .. table.concat(parts, ", ") .. "}"
|
|
elseif t == "nil" then return "nil"
|
|
else return tostring(value) end
|
|
end
|
|
|
|
local function deepEqual(a, b)
|
|
if type(a) ~= type(b) then return false end
|
|
if type(a) ~= "table" then return a == b end
|
|
for k, v in pairs(a) do if not deepEqual(v, b[k]) then return false end end
|
|
for k, _ in pairs(b) do if a[k] == nil then return false end end
|
|
return true
|
|
end
|
|
|
|
local function assertionError(message, expected, actual)
|
|
return { type = "AssertionError", message = message, expected = expected, actual = actual }
|
|
end
|
|
|
|
function M.expect(actual)
|
|
local expectation = { actual = actual, negated = false }
|
|
expectation.never = setmetatable({}, { __index = function(_, key) expectation.negated = true return expectation[key] end })
|
|
|
|
function expectation.toBe(expected)
|
|
local pass = actual == expected
|
|
if expectation.negated then pass = not pass end
|
|
if not pass then
|
|
local msg = expectation.negated and "Expected " .. stringify(actual) .. " not to be " .. stringify(expected) or "Expected " .. stringify(actual) .. " to be " .. stringify(expected)
|
|
error(assertionError(msg, expected, actual))
|
|
end
|
|
return true
|
|
end
|
|
|
|
function expectation.toEqual(expected)
|
|
local pass = deepEqual(actual, expected)
|
|
if expectation.negated then pass = not pass end
|
|
if not pass then error(assertionError(expectation.negated and "Expected values not to be deeply equal" or "Expected values to be deeply equal", expected, actual)) end
|
|
return true
|
|
end
|
|
|
|
function expectation.toBeNil()
|
|
local pass = actual == nil
|
|
if expectation.negated then pass = not pass end
|
|
if not pass then error(assertionError(expectation.negated and "Expected not nil" or "Expected nil", nil, actual)) end
|
|
return true
|
|
end
|
|
|
|
function expectation.toBeTruthy()
|
|
local pass = actual and true or false
|
|
if expectation.negated then pass = not pass end
|
|
if not pass then error(assertionError(expectation.negated and "Expected falsy" or "Expected truthy", "truthy", actual)) end
|
|
return true
|
|
end
|
|
|
|
function expectation.toBeFalsy()
|
|
local pass = not actual
|
|
if expectation.negated then pass = not pass end
|
|
if not pass then error(assertionError(expectation.negated and "Expected truthy" or "Expected falsy", "falsy", actual)) end
|
|
return true
|
|
end
|
|
|
|
function expectation.toBeType(expectedType)
|
|
local actualType = type(actual)
|
|
local pass = actualType == expectedType
|
|
if expectation.negated then pass = not pass end
|
|
if not pass then error(assertionError("Type mismatch", expectedType, actualType)) end
|
|
return true
|
|
end
|
|
|
|
function expectation.toContain(expected)
|
|
local pass = false
|
|
if type(actual) == "string" and type(expected) == "string" then pass = string.find(actual, expected, 1, true) ~= nil
|
|
elseif type(actual) == "table" then
|
|
for _, v in pairs(actual) do if deepEqual(v, expected) then pass = true break end end
|
|
end
|
|
if expectation.negated then pass = not pass end
|
|
if not pass then error(assertionError("toContain failed", expected, actual)) end
|
|
return true
|
|
end
|
|
|
|
function expectation.toHaveLength(expectedLength)
|
|
local actualLength = type(actual) == "string" and #actual or (type(actual) == "table" and #actual or 0)
|
|
local pass = actualLength == expectedLength
|
|
if expectation.negated then pass = not pass end
|
|
if not pass then error(assertionError("Length mismatch", expectedLength, actualLength)) end
|
|
return true
|
|
end
|
|
|
|
function expectation.toBeGreaterThan(expected)
|
|
local pass = actual > expected
|
|
if expectation.negated then pass = not pass end
|
|
if not pass then error(assertionError("toBeGreaterThan failed", "> " .. expected, actual)) end
|
|
return true
|
|
end
|
|
|
|
function expectation.toBeLessThan(expected)
|
|
local pass = actual < expected
|
|
if expectation.negated then pass = not pass end
|
|
if not pass then error(assertionError("toBeLessThan failed", "< " .. expected, actual)) end
|
|
return true
|
|
end
|
|
|
|
function expectation.toBeCloseTo(expected, precision)
|
|
precision = precision or 2
|
|
local diff = math.abs(actual - expected)
|
|
local pass = diff < (10 ^ -precision) / 2
|
|
if expectation.negated then pass = not pass end
|
|
if not pass then error(assertionError("toBeCloseTo failed", expected, actual)) end
|
|
return true
|
|
end
|
|
|
|
function expectation.toMatch(pattern)
|
|
local pass = string.match(actual, pattern) ~= nil
|
|
if expectation.negated then pass = not pass end
|
|
if not pass then error(assertionError("Pattern match failed", pattern, actual)) end
|
|
return true
|
|
end
|
|
|
|
function expectation.toThrow(expectedMessage)
|
|
local success, err = pcall(actual)
|
|
local pass = not success
|
|
if pass and expectedMessage then
|
|
local errMsg = type(err) == "table" and err.message or tostring(err)
|
|
pass = string.find(errMsg, expectedMessage, 1, true) ~= nil
|
|
end
|
|
if expectation.negated then pass = not pass end
|
|
if not pass then error(assertionError("toThrow failed", expectedMessage or "error", success and "no error" or err)) end
|
|
return true
|
|
end
|
|
|
|
function expectation.toHaveProperty(key, value)
|
|
local pass = actual[key] ~= nil
|
|
if pass and value ~= nil then pass = deepEqual(actual[key], value) end
|
|
if expectation.negated then pass = not pass end
|
|
if not pass then error(assertionError("toHaveProperty failed", value, actual[key])) end
|
|
return true
|
|
end
|
|
|
|
return expectation
|
|
end
|
|
|
|
function M.assertTrue(value, message) if not value then error(assertionError(message or "Expected true", true, value)) end end
|
|
function M.assertFalse(value, message) if value then error(assertionError(message or "Expected false", false, value)) end end
|
|
function M.assertEqual(actual, expected, message) if actual ~= expected then error(assertionError(message or "Values not equal", expected, actual)) end end
|
|
function M.assertNotEqual(actual, expected, message) if actual == expected then error(assertionError(message or "Values should not be equal", "not equal", actual)) end end
|
|
function M.assertNil(value, message) if value ~= nil then error(assertionError(message or "Expected nil", nil, value)) end end
|
|
function M.assertNotNil(value, message) if value == nil then error(assertionError(message or "Expected not nil", "not nil", nil)) end end
|
|
function M.fail(message) error(assertionError(message or "Test failed", nil, nil)) end
|
|
|
|
return M
|
|
`
|
|
}
|
|
|
|
function getMocksModule(): string {
|
|
return `
|
|
local M = {}
|
|
|
|
function M.fn(implementation)
|
|
local mock = { calls = {}, results = {}, implementation = implementation }
|
|
|
|
local callable = function(...)
|
|
local args = {...}
|
|
mock.calls[#mock.calls + 1] = args
|
|
local result = mock.implementation and {mock.implementation(...)} or {}
|
|
mock.results[#mock.results + 1] = result
|
|
return table.unpack(result)
|
|
end
|
|
|
|
return setmetatable({}, {
|
|
__call = function(_, ...) return callable(...) end,
|
|
__index = {
|
|
getCallCount = function() return #mock.calls end,
|
|
wasCalled = function() return #mock.calls > 0 end,
|
|
wasCalledWith = function(...)
|
|
local expectedArgs = {...}
|
|
for _, callArgs in ipairs(mock.calls) do
|
|
local match = true
|
|
for i, expected in ipairs(expectedArgs) do if callArgs[i] ~= expected then match = false break end end
|
|
if match then return true end
|
|
end
|
|
return false
|
|
end,
|
|
getCall = function(index) return mock.calls[index] end,
|
|
getLastCall = function() return mock.calls[#mock.calls] end,
|
|
getCalls = function() return mock.calls end,
|
|
getResults = function() return mock.results end,
|
|
reset = function() mock.calls = {} mock.results = {} end,
|
|
mockReturnValue = function(value) mock.implementation = function() return value end end,
|
|
mockImplementation = function(fn) mock.implementation = fn end,
|
|
mockRestore = function() mock.implementation = nil mock.calls = {} mock.results = {} end
|
|
}
|
|
})
|
|
end
|
|
|
|
function M.spyOn(obj, methodName)
|
|
local original = obj[methodName]
|
|
local spy = M.fn(original)
|
|
local meta = getmetatable(spy).__index
|
|
local originalRestore = meta.mockRestore
|
|
meta.mockRestore = function() obj[methodName] = original originalRestore() end
|
|
obj[methodName] = function(...) return spy(...) end
|
|
return spy
|
|
end
|
|
|
|
return M
|
|
`
|
|
}
|
|
|
|
function getRunnerModule(): string {
|
|
return `
|
|
local M = {}
|
|
|
|
M.STATUS = { PASSED = "passed", FAILED = "failed", SKIPPED = "skipped", PENDING = "pending" }
|
|
|
|
function M.runTest(test, hooks)
|
|
local result = { name = test.name, status = M.STATUS.PENDING, error = nil, duration = 0 }
|
|
if test.skipped then result.status = M.STATUS.SKIPPED return result end
|
|
|
|
local startTime = os.clock()
|
|
if hooks.beforeEach then
|
|
local success, err = pcall(hooks.beforeEach)
|
|
if not success then
|
|
result.status = M.STATUS.FAILED
|
|
result.error = "beforeEach failed: " .. tostring(err)
|
|
result.duration = (os.clock() - startTime) * 1000
|
|
return result
|
|
end
|
|
end
|
|
|
|
local success, err = pcall(test.fn)
|
|
if hooks.afterEach then pcall(hooks.afterEach) end
|
|
result.duration = (os.clock() - startTime) * 1000
|
|
|
|
if success then result.status = M.STATUS.PASSED
|
|
else
|
|
result.status = M.STATUS.FAILED
|
|
if type(err) == "table" and err.type == "AssertionError" then
|
|
result.error = err.message
|
|
result.expected = err.expected
|
|
result.actual = err.actual
|
|
else result.error = tostring(err) end
|
|
end
|
|
return result
|
|
end
|
|
|
|
function M.runSuite(suite, config, parentHooks)
|
|
local results = { name = suite.name, tests = {}, nested = {}, stats = { total = 0, passed = 0, failed = 0, skipped = 0, duration = 0 } }
|
|
config = config or {}
|
|
parentHooks = parentHooks or {}
|
|
local startTime = os.clock()
|
|
local hooks = { beforeEach = suite.beforeEach or parentHooks.beforeEach, afterEach = suite.afterEach or parentHooks.afterEach }
|
|
|
|
if suite.beforeAll then
|
|
local success, err = pcall(suite.beforeAll)
|
|
if not success then
|
|
for _, test in ipairs(suite.tests) do
|
|
results.tests[#results.tests + 1] = { name = test.name, status = M.STATUS.FAILED, error = "beforeAll failed: " .. tostring(err), duration = 0 }
|
|
results.stats.failed = results.stats.failed + 1
|
|
results.stats.total = results.stats.total + 1
|
|
end
|
|
results.stats.duration = (os.clock() - startTime) * 1000
|
|
return results
|
|
end
|
|
end
|
|
|
|
local hasOnly = false
|
|
for _, test in ipairs(suite.tests) do if test.only then hasOnly = true break end end
|
|
|
|
for _, test in ipairs(suite.tests) do
|
|
if hasOnly and not test.only then
|
|
results.tests[#results.tests + 1] = { name = test.name, status = M.STATUS.SKIPPED, duration = 0 }
|
|
results.stats.skipped = results.stats.skipped + 1
|
|
elseif config.filter and not string.find(test.name, config.filter, 1, true) then
|
|
results.tests[#results.tests + 1] = { name = test.name, status = M.STATUS.SKIPPED, duration = 0 }
|
|
results.stats.skipped = results.stats.skipped + 1
|
|
else
|
|
local testResult = M.runTest(test, hooks)
|
|
results.tests[#results.tests + 1] = testResult
|
|
if testResult.status == M.STATUS.PASSED then results.stats.passed = results.stats.passed + 1
|
|
elseif testResult.status == M.STATUS.FAILED then
|
|
results.stats.failed = results.stats.failed + 1
|
|
if config.stopOnFirstFailure then break end
|
|
elseif testResult.status == M.STATUS.SKIPPED then results.stats.skipped = results.stats.skipped + 1 end
|
|
end
|
|
results.stats.total = results.stats.total + 1
|
|
end
|
|
|
|
for _, nestedSuite in ipairs(suite.nested) do
|
|
local nestedResults = M.runSuite(nestedSuite, config, hooks)
|
|
results.nested[#results.nested + 1] = nestedResults
|
|
results.stats.total = results.stats.total + nestedResults.stats.total
|
|
results.stats.passed = results.stats.passed + nestedResults.stats.passed
|
|
results.stats.failed = results.stats.failed + nestedResults.stats.failed
|
|
results.stats.skipped = results.stats.skipped + nestedResults.stats.skipped
|
|
end
|
|
|
|
if suite.afterAll then pcall(suite.afterAll) end
|
|
results.stats.duration = (os.clock() - startTime) * 1000
|
|
return results
|
|
end
|
|
|
|
function M.runAll(suites, config)
|
|
local allResults = { suites = {}, stats = { total = 0, passed = 0, failed = 0, skipped = 0, duration = 0, suiteCount = 0 }, timestamp = os.date("%Y-%m-%dT%H:%M:%S") }
|
|
local startTime = os.clock()
|
|
|
|
for _, suite in ipairs(suites) do
|
|
local suiteResults = M.runSuite(suite, config)
|
|
allResults.suites[#allResults.suites + 1] = suiteResults
|
|
allResults.stats.suiteCount = allResults.stats.suiteCount + 1
|
|
allResults.stats.total = allResults.stats.total + suiteResults.stats.total
|
|
allResults.stats.passed = allResults.stats.passed + suiteResults.stats.passed
|
|
allResults.stats.failed = allResults.stats.failed + suiteResults.stats.failed
|
|
allResults.stats.skipped = allResults.stats.skipped + suiteResults.stats.skipped
|
|
end
|
|
|
|
allResults.stats.duration = (os.clock() - startTime) * 1000
|
|
allResults.stats.success = allResults.stats.failed == 0
|
|
return allResults
|
|
end
|
|
|
|
return M
|
|
`
|
|
}
|
|
|
|
function getHelpersModule(): string {
|
|
return `
|
|
local M = {}
|
|
|
|
function M.generateTestData(template, count)
|
|
local data = {}
|
|
count = count or 10
|
|
for i = 1, count do
|
|
local item = {}
|
|
for k, v in pairs(template) do
|
|
if type(v) == "function" then item[k] = v(i)
|
|
elseif type(v) == "string" and v:match("^%$") then
|
|
local varName = v:sub(2)
|
|
if varName == "index" then item[k] = i
|
|
elseif varName == "random" then item[k] = math.random(1, 1000)
|
|
else item[k] = v end
|
|
else item[k] = v end
|
|
end
|
|
data[#data + 1] = item
|
|
end
|
|
return data
|
|
end
|
|
|
|
M.table = {}
|
|
function M.table.clone(t)
|
|
if type(t) ~= "table" then return t end
|
|
local copy = {}
|
|
for k, v in pairs(t) do copy[k] = M.table.clone(v) end
|
|
return setmetatable(copy, getmetatable(t))
|
|
end
|
|
|
|
function M.table.merge(...)
|
|
local result = {}
|
|
for _, t in ipairs({...}) do
|
|
if type(t) == "table" then for k, v in pairs(t) do result[k] = v end end
|
|
end
|
|
return result
|
|
end
|
|
|
|
function M.table.keys(t)
|
|
local keys = {}
|
|
for k in pairs(t) do keys[#keys + 1] = k end
|
|
return keys
|
|
end
|
|
|
|
function M.table.values(t)
|
|
local values = {}
|
|
for _, v in pairs(t) do values[#values + 1] = v end
|
|
return values
|
|
end
|
|
|
|
function M.table.size(t)
|
|
local count = 0
|
|
for _ in pairs(t) do count = count + 1 end
|
|
return count
|
|
end
|
|
|
|
M.string = {}
|
|
function M.string.trim(s) return s:match("^%s*(.-)%s*$") end
|
|
function M.string.split(s, delimiter)
|
|
delimiter = delimiter or "%s"
|
|
local result = {}
|
|
for match in (s .. delimiter):gmatch("(.-)" .. delimiter) do result[#result + 1] = match end
|
|
return result
|
|
end
|
|
function M.string.startsWith(s, prefix) return s:sub(1, #prefix) == prefix end
|
|
function M.string.endsWith(s, suffix) return suffix == "" or s:sub(-#suffix) == suffix end
|
|
|
|
return M
|
|
`
|
|
}
|
|
|
|
function getJsonParserModule(): string {
|
|
return `
|
|
local M = {}
|
|
|
|
function M.decode(str)
|
|
local pos = 1
|
|
|
|
local function skip_whitespace()
|
|
while pos <= #str and str:sub(pos, pos):match("%s") do pos = pos + 1 end
|
|
end
|
|
|
|
local function parse_string()
|
|
pos = pos + 1
|
|
local start = pos
|
|
while pos <= #str do
|
|
local c = str:sub(pos, pos)
|
|
if c == '"' then
|
|
local result = str:sub(start, pos - 1)
|
|
pos = pos + 1
|
|
result = result:gsub("\\\\n", "\\n"):gsub("\\\\t", "\\t"):gsub("\\\\r", "\\r")
|
|
result = result:gsub('\\\\"', '"'):gsub("\\\\\\\\\\\\\\\\", "\\\\")
|
|
return result
|
|
elseif c == "\\\\" then pos = pos + 2
|
|
else pos = pos + 1 end
|
|
end
|
|
error("Unterminated string")
|
|
end
|
|
|
|
local function parse_number()
|
|
local start = pos
|
|
while pos <= #str and str:sub(pos, pos):match("[%d%.eE%+%-]") do pos = pos + 1 end
|
|
return tonumber(str:sub(start, pos - 1))
|
|
end
|
|
|
|
local parse_value
|
|
|
|
local function parse_array()
|
|
pos = pos + 1
|
|
local arr = {}
|
|
skip_whitespace()
|
|
if str:sub(pos, pos) == "]" then pos = pos + 1 return arr end
|
|
while true do
|
|
skip_whitespace()
|
|
arr[#arr + 1] = parse_value()
|
|
skip_whitespace()
|
|
local c = str:sub(pos, pos)
|
|
if c == "]" then pos = pos + 1 return arr
|
|
elseif c == "," then pos = pos + 1
|
|
else error("Expected ',' or ']' at pos " .. pos) end
|
|
end
|
|
end
|
|
|
|
local function parse_object()
|
|
pos = pos + 1
|
|
local obj = {}
|
|
skip_whitespace()
|
|
if str:sub(pos, pos) == "}" then pos = pos + 1 return obj end
|
|
while true do
|
|
skip_whitespace()
|
|
if str:sub(pos, pos) ~= '"' then error("Expected string key at pos " .. pos) end
|
|
local key = parse_string()
|
|
skip_whitespace()
|
|
if str:sub(pos, pos) ~= ":" then error("Expected ':' at pos " .. pos) end
|
|
pos = pos + 1
|
|
skip_whitespace()
|
|
obj[key] = parse_value()
|
|
skip_whitespace()
|
|
local c = str:sub(pos, pos)
|
|
if c == "}" then pos = pos + 1 return obj
|
|
elseif c == "," then pos = pos + 1
|
|
else error("Expected ',' or '}' at pos " .. pos) end
|
|
end
|
|
end
|
|
|
|
parse_value = function()
|
|
skip_whitespace()
|
|
local c = str:sub(pos, pos)
|
|
if c == '"' then return parse_string()
|
|
elseif c == "{" then return parse_object()
|
|
elseif c == "[" then return parse_array()
|
|
elseif c == "t" and str:sub(pos, pos + 3) == "true" then pos = pos + 4 return true
|
|
elseif c == "f" and str:sub(pos, pos + 4) == "false" then pos = pos + 5 return false
|
|
elseif c == "n" and str:sub(pos, pos + 3) == "null" then pos = pos + 4 return nil
|
|
elseif c:match("[%d%-]") then return parse_number()
|
|
else error("Unexpected char '" .. c .. "' at pos " .. pos) end
|
|
end
|
|
|
|
return parse_value()
|
|
end
|
|
|
|
return M
|
|
`
|
|
}
|