From 9b568aa8ec2f6977c7355a2bc3f9e7a49262edab Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Mon, 27 Mar 2023 13:00:45 +0200 Subject: [PATCH] Respect tostring metamethods when printing --- .justfile | 4 + .lune/hello_lune.luau | 225 +++++++++++++++++++++++ CHANGELOG.md | 4 + docs/pages/home/Writing-Scripts.md | 2 +- packages/lib/src/lua/stdio/formatting.rs | 32 ++++ 5 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 .lune/hello_lune.luau diff --git a/.justfile b/.justfile index 5760c8c..f3e20cd 100644 --- a/.justfile +++ b/.justfile @@ -2,6 +2,10 @@ run-test TEST_NAME: cargo run -- "tests/{{TEST_NAME}}" +# Run an individual file using the Lune CLI +run-file FILE_NAME: + cargo run -- "{{FILE_NAME}}" + # Run tests for the Lune library test: cargo test --package lune -- --test-threads 1 diff --git a/.lune/hello_lune.luau b/.lune/hello_lune.luau new file mode 100644 index 0000000..8e49e46 --- /dev/null +++ b/.lune/hello_lune.luau @@ -0,0 +1,225 @@ +--[[ + EXAMPLE #1 + + Using arguments given to the program +]] + +if #process.args > 0 then + print("Got arguments:") + print(process.args) + if #process.args > 3 then + error("Too many arguments!") + end +else + print("Got no arguments ☚ī¸") +end + +--[[ + EXAMPLE #2 + + Using the stdio library to prompt for terminal input +]] + +local text = stdio.prompt("text", "Please write some text") + +print("You wrote '" .. text .. "'!") + +local confirmed = stdio.prompt("confirm", "Please confirm that you wrote some text") +if confirmed == false then + error("You didn't confirm!") +else + print("Confirmed!") +end + +--[[ + EXAMPLE #3 + + Get & set environment variables + + Checks if environment variables are empty or not, + prints out ❌ if empty and ✅ if they have a value +]] + +print("Reading current environment 🔎") + +-- Environment variables can be read directly +assert(process.env.PATH ~= nil, "Missing PATH") +assert(process.env.PWD ~= nil, "Missing PWD") + +-- And they can also be accessed using Luau's generalized iteration (but not pairs()) +for key, value in process.env do + local box = if value and value ~= "" then "✅" else "❌" + print(string.format("[%s] %s", box, key)) +end + +--[[ + EXAMPLE #4 + + Spawning concurrent tasks + + These tasks will run at the same time as other Lua code which lets you do primitive multitasking +]] + +task.spawn(function() + print("Spawned a task that will run instantly but not block") + task.wait(5) +end) + +print("Spawning a delayed task that will run in 5 seconds") +task.delay(5, function() + print("...") + task.wait(1) + print("Hello again!") + task.wait(1) + print("Goodbye again! 🌙") +end) + +--[[ + EXAMPLE #5 + + Read files in the current directory + + This prints out directory & file names with some fancy icons +]] + +print("Reading current dir 🗂ī¸") +local entries = fs.readDir(".") + +-- NOTE: We have to do this outside of the sort function +-- to avoid yielding across the metamethod boundary, all +-- of the filesystem APIs are asynchronous and yielding +local entryIsDir = {} +for _, entry in entries do + entryIsDir[entry] = fs.isDir(entry) +end + +-- Sort prioritizing directories first, then alphabetically +table.sort(entries, function(entry0, entry1) + if entryIsDir[entry0] ~= entryIsDir[entry1] then + return entryIsDir[entry0] + end + return entry0 < entry1 +end) + +-- Make sure we got some known files that should always exist +assert(table.find(entries, "Cargo.toml") ~= nil, "Missing Cargo.toml") +assert(table.find(entries, "Cargo.lock") ~= nil, "Missing Cargo.lock") + +-- Print the pretty stuff +for _, entry in entries do + if fs.isDir(entry) then + print("📁 " .. entry) + else + print("📄 " .. entry) + end +end + +--[[ + EXAMPLE #6 + + Call out to another program / executable + + You can also get creative and combine this with example #6 to spawn several programs at the same time! +]] + +print("Sending 4 pings to google 🌏") +local result = process.spawn("ping", { + "google.com", + "-c 4", +}) + +--[[ + EXAMPLE #7 + + Using the result of a spawned process, exiting the process + + This looks scary with lots of weird symbols, but, it's just some Lua-style pattern matching + to parse the lines of "min/avg/max/stddev = W/X/Y/Z ms" that the ping program outputs to us +]] + +if result.ok then + assert(#result.stdout > 0, "Result output was empty") + local min, avg, max, stddev = string.match( + result.stdout, + "min/avg/max/stddev = ([%d%.]+)/([%d%.]+)/([%d%.]+)/([%d%.]+) ms" + ) + print(string.format("Minimum ping time: %.3fms", assert(tonumber(min)))) + print(string.format("Maximum ping time: %.3fms", assert(tonumber(max)))) + print(string.format("Average ping time: %.3fms", assert(tonumber(avg)))) + print(string.format("Standard deviation: %.3fms", assert(tonumber(stddev)))) +else + print("Failed to send ping to google!") + print(result.stderr) + process.exit(result.code) +end + +--[[ + EXAMPLE #8 + + Using the built-in networking library, encoding & decoding json +]] + +print("Sending PATCH request to web API 📤") +local apiResult = net.request({ + url = "https://jsonplaceholder.typicode.com/posts/1", + method = "PATCH", + headers = { + ["Content-Type"] = "application/json", + }, + body = net.jsonEncode({ + title = "foo", + body = "bar", + }), +}) + +if not apiResult.ok then + print("Failed to send network request!") + print(string.format("%d (%s)", apiResult.statusCode, apiResult.statusMessage)) + print(apiResult.body) + process.exit(1) +end + +type ApiResponse = { + id: number, + title: string, + body: string, + userId: number, +} + +local apiResponse: ApiResponse = net.jsonDecode(apiResult.body) +assert(apiResponse.title == "foo", "Invalid json response") +assert(apiResponse.body == "bar", "Invalid json response") +print("Got valid JSON response with changes applied") + +--[[ + EXAMPLE #9 + + Using the stdio library to print pretty +]] + +print("Printing with pretty colors and auto-formatting 🎨") + +print(stdio.color("blue") .. string.rep("—", 22) .. stdio.color("reset")) + +printinfo("API response:", apiResponse) +warn({ + Oh = { + No = { + TooMuch = { + Nesting = { + "Will not print", + }, + }, + }, + }, +}) + +print(stdio.color("blue") .. string.rep("—", 22) .. stdio.color("reset")) + +--[[ + EXAMPLE #10 + + Saying goodbye 😔 +]] + +print("Goodbye, lune! 🌙") diff --git a/CHANGELOG.md b/CHANGELOG.md index d6be909..d7d290c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +- Functions such as `print`, `warn`, ... now respect `__tostring` metamethods + ### Fixed - Fixed issues with CFrame math operations diff --git a/docs/pages/home/Writing-Scripts.md b/docs/pages/home/Writing-Scripts.md index 2d14429..c987936 100644 --- a/docs/pages/home/Writing-Scripts.md +++ b/docs/pages/home/Writing-Scripts.md @@ -261,7 +261,7 @@ print("Printing with pretty colors and auto-formatting 🎨") print(stdio.color("blue") .. string.rep("—", 22) .. stdio.color("reset")) -info("API response:", apiResponse) +printinfo("API response:", apiResponse) warn({ Oh = { No = { diff --git a/packages/lib/src/lua/stdio/formatting.rs b/packages/lib/src/lua/stdio/formatting.rs index ea526b2..ba2d40f 100644 --- a/packages/lib/src/lua/stdio/formatting.rs +++ b/packages/lib/src/lua/stdio/formatting.rs @@ -127,6 +127,8 @@ pub fn pretty_format_value( LuaValue::Table(ref tab) => { if depth >= MAX_FORMAT_DEPTH { write!(buffer, "{}", STYLE_DIM.apply_to("{ ... }"))?; + } else if let Some(s) = call_table_tostring_metamethod(tab) { + write!(buffer, "{s}")?; } else { let mut is_empty = false; let depth_indent = INDENT.repeat(depth); @@ -172,6 +174,8 @@ pub fn pretty_format_value( // to lua and pretend to be normal lua // threads for compatibility purposes write!(buffer, "{}", COLOR_PURPLE.apply_to(""))? + } else if let Some(s) = call_userdata_tostring_metamethod(u) { + write!(buffer, "{s}")? } else { write!(buffer, "{}", COLOR_PURPLE.apply_to(""))? } @@ -433,3 +437,31 @@ fn fix_error_nitpicks(full_message: String) -> String { "'[C]' - function require", ) } + +fn call_table_tostring_metamethod<'a>(tab: &'a LuaTable<'a>) -> Option { + let f = match tab.get_metatable() { + None => None, + Some(meta) => match meta.get::<_, LuaFunction>(LuaMetaMethod::ToString.name()) { + Ok(method) => Some(method), + Err(_) => None, + }, + }?; + match f.call::<_, String>(()) { + Ok(res) => Some(res), + Err(_) => None, + } +} + +fn call_userdata_tostring_metamethod<'a>(tab: &'a LuaAnyUserData<'a>) -> Option { + let f = match tab.get_metatable() { + Err(_) => None, + Ok(meta) => match meta.get::<_, LuaFunction>(LuaMetaMethod::ToString.name()) { + Ok(method) => Some(method), + Err(_) => None, + }, + }?; + match f.call::<_, String>(()) { + Ok(res) => Some(res), + Err(_) => None, + } +}