diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b60e128..c5ebff1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,7 +24,7 @@ jobs: components: rustfmt - name: Install Just - uses: extractions/setup-just@v1 + uses: extractions/setup-just@v2 - name: Install Tooling uses: ok-nick/setup-aftman@v0.4.2 @@ -41,7 +41,7 @@ jobs: uses: actions/checkout@v4 - name: Install Just - uses: extractions/setup-just@v1 + uses: extractions/setup-just@v2 - name: Install Tooling uses: ok-nick/setup-aftman@v0.4.2 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml deleted file mode 100644 index 13fc2be..0000000 --- a/.github/workflows/publish.yaml +++ /dev/null @@ -1,24 +0,0 @@ -name: Publish - -on: - push: - branches: - - "main" - workflow_dispatch: - -jobs: - publish: - name: Publish - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - - name: Publish to crates.io - uses: katyo/publish-crates@v2 - with: - registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} - ignore-unpublished-changes: true diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 85b25f1..12f0e91 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -21,14 +21,32 @@ jobs: uses: actions/checkout@v4 - name: Get version from manifest - uses: SebRollen/toml-action@9062fbef52816d61278d24ce53c8070440e1e8dd + uses: SebRollen/toml-action@v1.2.0 id: get_version with: file: Cargo.toml field: package.version - build: + dry-run: + name: Dry-run needs: ["init"] + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Publish (dry-run) + uses: katyo/publish-crates@v2 + with: + dry-run: true + check-repo: true + registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + build: + needs: ["init", "dry-run"] strategy: fail-fast: false matrix: @@ -70,7 +88,7 @@ jobs: targets: ${{ matrix.cargo-target }} - name: Install Just - uses: extractions/setup-just@v1 + uses: extractions/setup-just@v2 - name: Install build tooling (aarch64-unknown-linux-gnu) if: matrix.cargo-target == 'aarch64-unknown-linux-gnu' @@ -86,24 +104,24 @@ jobs: run: just zip-release ${{ matrix.cargo-target }} - name: Upload release artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ matrix.artifact-name }} path: release.zip - release: - name: Release + release-github: + name: Release (GitHub) runs-on: ubuntu-latest - needs: ["init", "build"] + needs: ["init", "dry-run", "build"] steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install Just - uses: extractions/setup-just@v1 + uses: extractions/setup-just@v2 - name: Download releases - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: ./releases @@ -111,7 +129,7 @@ jobs: run: just unpack-releases "./releases" - name: Create release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -120,3 +138,21 @@ jobs: fail_on_unmatched_files: true files: ./releases/*.zip draft: true + + release-crates: + name: Release (crates.io) + runs-on: ubuntu-latest + needs: ["init", "dry-run", "build"] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Publish crates + uses: katyo/publish-crates@v2 + with: + dry-run: false + check-repo: true + registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index 2bc2755..b4455c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,9 +163,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.8" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dbbf24db18d609b1462965249abdf49129ccad073ec257da372adc83259c60" +checksum = "9c90a406b4495d129f00461241616194cb8a032c8d1c53c657f0961d5f8e0498" dependencies = [ "brotli", "flate2", @@ -205,17 +205,6 @@ version = "4.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" -[[package]] -name = "async-trait" -version = "0.1.80" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.60", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -324,9 +313,9 @@ dependencies = [ [[package]] name = "brotli" -version = "4.0.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "125740193d7fee5cc63ab9e16c2fdc4e07c74ba755cc53b327d6ea029e9fc569" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -335,9 +324,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "3.0.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65622a320492e09b5e0ac436b14c54ff68199bac392d0e89a6832c4518eea525" +checksum = "e6221fe77a248b9117d431ad93761222e1cf8ff282d9d1d5d9f53d6299a1cf76" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -759,12 +748,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" -[[package]] -name = "either" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" - [[package]] name = "encode_unicode" version = "0.3.6" @@ -1367,15 +1350,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.11" @@ -1484,57 +1458,203 @@ name = "lune" version = "0.8.3" dependencies = [ "anyhow", - "async-compression", - "async-trait", - "blocking", - "bstr", - "chrono", - "chrono_lc", "clap", "console", "dialoguer", "directories", - "dunce", "env_logger", "futures-util", + "include_dir", + "lune-roblox", + "lune-std", + "lune-utils", + "mlua", + "mlua-luau-scheduler", + "once_cell", + "reqwest", + "rustyline", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber", + "zip_next", +] + +[[package]] +name = "lune-roblox" +version = "0.1.0" +dependencies = [ "glam", + "lune-utils", + "mlua", + "once_cell", + "rand", + "rbx_binary", + "rbx_dom_weak", + "rbx_reflection", + "rbx_reflection_database", + "rbx_xml", + "thiserror", +] + +[[package]] +name = "lune-std" +version = "0.1.0" +dependencies = [ + "lune-std-datetime", + "lune-std-fs", + "lune-std-luau", + "lune-std-net", + "lune-std-process", + "lune-std-regex", + "lune-std-roblox", + "lune-std-serde", + "lune-std-stdio", + "lune-std-task", + "lune-utils", + "mlua", + "mlua-luau-scheduler", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "lune-std-datetime" +version = "0.1.0" +dependencies = [ + "chrono", + "chrono_lc", + "lune-utils", + "mlua", + "thiserror", +] + +[[package]] +name = "lune-std-fs" +version = "0.1.0" +dependencies = [ + "bstr", + "lune-std-datetime", + "lune-utils", + "mlua", + "tokio", +] + +[[package]] +name = "lune-std-luau" +version = "0.1.0" +dependencies = [ + "lune-utils", + "mlua", +] + +[[package]] +name = "lune-std-net" +version = "0.1.0" +dependencies = [ + "bstr", + "futures-util", "http 1.1.0", "http-body-util", "hyper 1.3.1", "hyper-tungstenite", "hyper-util", - "include_dir", - "itertools", - "lz4_flex", + "lune-std-serde", + "lune-utils", + "mlua", + "mlua-luau-scheduler", + "reqwest", + "tokio", + "tokio-tungstenite", + "urlencoding", +] + +[[package]] +name = "lune-std-process" +version = "0.1.0" +dependencies = [ + "directories", + "lune-utils", + "mlua", + "mlua-luau-scheduler", + "os_str_bytes", + "pin-project", + "tokio", +] + +[[package]] +name = "lune-std-regex" +version = "0.1.0" +dependencies = [ + "lune-utils", + "mlua", + "regex", + "self_cell", +] + +[[package]] +name = "lune-std-roblox" +version = "0.1.0" +dependencies = [ + "lune-roblox", + "lune-utils", "mlua", "mlua-luau-scheduler", "once_cell", - "os_str_bytes", - "path-clean", - "pathdiff", - "pin-project", - "rand", - "rbx_binary", "rbx_cookie", - "rbx_dom_weak", - "rbx_reflection", - "rbx_reflection_database", - "rbx_xml", - "regex", - "reqwest", - "rustyline", - "self_cell", +] + +[[package]] +name = "lune-std-serde" +version = "0.1.0" +dependencies = [ + "async-compression", + "bstr", + "lune-utils", + "lz4", + "mlua", "serde", "serde_json", "serde_yaml", - "thiserror", "tokio", - "tokio-tungstenite", "toml", - "tracing", - "tracing-subscriber", - "urlencoding", - "zip_next", +] + +[[package]] +name = "lune-std-stdio" +version = "0.1.0" +dependencies = [ + "dialoguer", + "lune-utils", + "mlua", + "mlua-luau-scheduler", + "tokio", +] + +[[package]] +name = "lune-std-task" +version = "0.1.0" +dependencies = [ + "lune-utils", + "mlua", + "mlua-luau-scheduler", + "tokio", +] + +[[package]] +name = "lune-utils" +version = "0.1.0" +dependencies = [ + "console", + "dunce", + "mlua", + "once_cell", + "path-clean", + "pathdiff", + "tokio", ] [[package]] @@ -1557,15 +1677,6 @@ dependencies = [ "libc", ] -[[package]] -name = "lz4_flex" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" -dependencies = [ - "twox-hash", -] - [[package]] name = "lzma-rs" version = "0.3.0" @@ -2624,12 +2735,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "stdweb" version = "0.4.20" @@ -2882,7 +2987,6 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "tracing", "windows-sys 0.48.0", ] @@ -3100,16 +3204,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "twox-hash" -version = "1.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" -dependencies = [ - "cfg-if", - "static_assertions", -] - [[package]] name = "typed-arena" version = "2.0.2" diff --git a/Cargo.toml b/Cargo.toml index 8199082..904de54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,41 +1,21 @@ -[package] -name = "lune" -version = "0.8.3" -edition = "2021" -license = "MPL-2.0" -repository = "https://github.com/lune-org/lune" -description = "A standalone Luau runtime" -readme = "README.md" -keywords = ["cli", "lua", "luau", "runtime"] -categories = ["command-line-interface"] - -[[bin]] -name = "lune" -path = "src/main.rs" - -[lib] -name = "lune" -path = "src/lib.rs" - -[features] -default = ["cli", "roblox"] -cli = [ - "dep:anyhow", - "dep:env_logger", - "dep:clap", - "dep:include_dir", - "dep:rustyline", - "dep:zip_next", -] -roblox = [ - "dep:glam", - "dep:rand", - "dep:rbx_cookie", - "dep:rbx_binary", - "dep:rbx_dom_weak", - "dep:rbx_reflection", - "dep:rbx_reflection_database", - "dep:rbx_xml", +[workspace] +resolver = "2" +default-members = ["crates/lune"] +members = [ + "crates/lune", + "crates/lune-roblox", + "crates/lune-std", + "crates/lune-std-datetime", + "crates/lune-std-fs", + "crates/lune-std-luau", + "crates/lune-std-net", + "crates/lune-std-process", + "crates/lune-std-regex", + "crates/lune-std-roblox", + "crates/lune-std-serde", + "crates/lune-std-stdio", + "crates/lune-std-task", + "crates/lune-utils", ] # Profile for building the release binary, with the following options set: @@ -53,99 +33,31 @@ opt-level = "z" strip = true lto = true -# All of the dependencies for Lune. +# Lints for all crates in the workspace # -# Dependencies are categorized as following: -# -# 1. General dependencies with no specific features set -# 2. Large / core dependencies that have many different crates and / or features set -# 3. Dependencies for specific features of Lune, eg. the CLI or massive Roblox builtin library -# -[dependencies] -console = "0.15" -directories = "5.0" -futures-util = "0.3" -once_cell = "1.17" -thiserror = "1.0" -async-trait = "0.1" -dialoguer = "0.11" -dunce = "1.0" -lz4_flex = "0.11" -path-clean = "1.0" -pathdiff = "0.2" -pin-project = "1.0" -urlencoding = "2.1" -bstr = "1.9" -regex = "1.10" -self_cell = "1.0" +# 1. Error on all lints by default, then make cargo + clippy pedantic lints just warn +# 2. Selectively allow some lints that are _too_ pedantic, such as: +# - Casts between number types +# - Module naming conventions +# - Imports and multiple dependency versions +[workspace.lints.clippy] +all = { level = "deny", priority = -3 } +cargo = { level = "warn", priority = -2 } +pedantic = { level = "warn", priority = -1 } -### RUNTIME +cast_lossless = { level = "allow", priority = 1 } +cast_possible_truncation = { level = "allow", priority = 1 } +cast_possible_wrap = { level = "allow", priority = 1 } +cast_precision_loss = { level = "allow", priority = 1 } +cast_sign_loss = { level = "allow", priority = 1 } -blocking = "1.5" -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -tokio = { version = "1.24", features = ["full", "tracing"] } -os_str_bytes = { version = "7.0", features = ["conversions"] } +similar_names = { level = "allow", priority = 1 } +unnecessary_wraps = { level = "allow", priority = 1 } +unnested_or_patterns = { level = "allow", priority = 1 } +unreadable_literal = { level = "allow", priority = 1 } -mlua-luau-scheduler = { version = "0.0.2" } -mlua = { version = "0.9.7", features = [ - "luau", - "luau-jit", - "async", - "serialize", -] } - -### SERDE - -async-compression = { version = "0.4", features = [ - "tokio", - "brotli", - "deflate", - "gzip", - "zlib", -] } -serde = { version = "1.0", features = ["derive"] } -serde_json = { version = "1.0", features = ["preserve_order"] } -serde_yaml = "0.9" -toml = { version = "0.8", features = ["preserve_order"] } - -### NET - -hyper = { version = "1.1", features = ["full"] } -hyper-util = { version = "0.1", features = ["full"] } -http = "1.0" -http-body-util = { version = "0.1" } -hyper-tungstenite = { version = "0.13" } - -reqwest = { version = "0.11", default-features = false, features = [ - "rustls-tls", -] } - -tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] } - -### DATETIME -chrono = "=0.4.34" # NOTE: 0.4.35 does not compile with chrono_lc -chrono_lc = "0.1" - -### CLI - -anyhow = { optional = true, version = "1.0" } -env_logger = { optional = true, version = "0.11" } -itertools = "0.12" -clap = { optional = true, version = "4.1", features = ["derive"] } -include_dir = { optional = true, version = "0.7", features = ["glob"] } -rustyline = { optional = true, version = "14.0" } -zip_next = { optional = true, version = "1.1" } - -### ROBLOX - -glam = { optional = true, version = "0.27" } -rand = { optional = true, version = "0.8" } - -rbx_cookie = { optional = true, version = "0.1.4", default-features = false } - -rbx_binary = { optional = true, version = "0.7.3" } -rbx_dom_weak = { optional = true, version = "2.6.0" } -rbx_reflection = { optional = true, version = "4.4.0" } -rbx_reflection_database = { optional = true, version = "0.2.9" } -rbx_xml = { optional = true, version = "0.13.2" } +multiple_crate_versions = { level = "allow", priority = 1 } +module_inception = { level = "allow", priority = 1 } +module_name_repetitions = { level = "allow", priority = 1 } +needless_pass_by_value = { level = "allow", priority = 1 } +wildcard_imports = { level = "allow", priority = 1 } diff --git a/aftman.toml b/aftman.toml index 38cb739..3b4fcc9 100644 --- a/aftman.toml +++ b/aftman.toml @@ -1,4 +1,4 @@ [tools] -luau-lsp = "JohnnyMorganz/luau-lsp@1.27.0" -selene = "Kampfkarren/selene@0.26.1" -stylua = "JohnnyMorganz/StyLua@0.19.1" +luau-lsp = "JohnnyMorganz/luau-lsp@1.29.0" +selene = "Kampfkarren/selene@0.27.1" +stylua = "JohnnyMorganz/StyLua@0.20.0" diff --git a/crates/lune-roblox/Cargo.toml b/crates/lune-roblox/Cargo.toml new file mode 100644 index 0000000..88eb441 --- /dev/null +++ b/crates/lune-roblox/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "lune-roblox" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[lib] +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +mlua = { version = "0.9.7", features = ["luau"] } + +glam = "0.27" +rand = "0.8" +thiserror = "1.0" +once_cell = "1.17" + +rbx_binary = "0.7.3" +rbx_dom_weak = "2.6.0" +rbx_reflection = "4.4.0" +rbx_reflection_database = "0.2.9" +rbx_xml = "0.13.2" + +lune-utils = { version = "0.1.0", path = "../lune-utils" } diff --git a/src/roblox/datatypes/attributes.rs b/crates/lune-roblox/src/datatypes/attributes.rs similarity index 81% rename from src/roblox/datatypes/attributes.rs rename to crates/lune-roblox/src/datatypes/attributes.rs index d827801..7d5bd26 100644 --- a/src/roblox/datatypes/attributes.rs +++ b/crates/lune-roblox/src/datatypes/attributes.rs @@ -4,6 +4,15 @@ use rbx_dom_weak::types::{Variant as DomValue, VariantType as DomType}; use super::extension::DomValueExt; +/** + Checks if the given name is a valid attribute name. + + # Errors + + - If the name starts with the prefix "RBX". + - If the name contains any characters other than alphanumeric characters and underscore. + - If the name is longer than 100 characters. +*/ pub fn ensure_valid_attribute_name(name: impl AsRef) -> LuaResult<()> { let name = name.as_ref(); if name.to_ascii_uppercase().starts_with("RBX") { @@ -23,6 +32,13 @@ pub fn ensure_valid_attribute_name(name: impl AsRef) -> LuaResult<()> { } } +/** + Checks if the given value is a valid attribute value. + + # Errors + + - If the value is not a valid attribute type. +*/ pub fn ensure_valid_attribute_value(value: &DomValue) -> LuaResult<()> { let is_valid = matches!( value.ty(), diff --git a/src/roblox/datatypes/conversion.rs b/crates/lune-roblox/src/datatypes/conversion.rs similarity index 98% rename from src/roblox/datatypes/conversion.rs rename to crates/lune-roblox/src/datatypes/conversion.rs index f9b0a8e..f38ecc5 100644 --- a/src/roblox/datatypes/conversion.rs +++ b/crates/lune-roblox/src/datatypes/conversion.rs @@ -2,7 +2,7 @@ use mlua::prelude::*; use rbx_dom_weak::types::{Variant as DomValue, VariantType as DomType}; -use crate::roblox::{datatypes::extension::DomValueExt, instance::Instance}; +use crate::{datatypes::extension::DomValueExt, instance::Instance}; use super::*; @@ -65,8 +65,10 @@ impl<'lua> DomValueToLua<'lua> for LuaValue<'lua> { // NOTE: Some values are either optional or default and we should handle // that properly here since the userdata conversion above will always fail - DomValue::OptionalCFrame(None) => Ok(LuaValue::Nil), - DomValue::PhysicalProperties(dom::PhysicalProperties::Default) => Ok(LuaValue::Nil), + DomValue::OptionalCFrame(None) + | DomValue::PhysicalProperties(dom::PhysicalProperties::Default) => { + Ok(LuaValue::Nil) + } _ => Err(e), }, diff --git a/src/roblox/datatypes/extension.rs b/crates/lune-roblox/src/datatypes/extension.rs similarity index 97% rename from src/roblox/datatypes/extension.rs rename to crates/lune-roblox/src/datatypes/extension.rs index 30df47c..6f83242 100644 --- a/src/roblox/datatypes/extension.rs +++ b/crates/lune-roblox/src/datatypes/extension.rs @@ -6,6 +6,7 @@ pub(crate) trait DomValueExt { impl DomValueExt for DomType { fn variant_name(&self) -> Option<&'static str> { + #[allow(clippy::enum_glob_use)] use DomType::*; Some(match self { Attributes => "Attributes", diff --git a/src/roblox/datatypes/mod.rs b/crates/lune-roblox/src/datatypes/mod.rs similarity index 82% rename from src/roblox/datatypes/mod.rs rename to crates/lune-roblox/src/datatypes/mod.rs index 5432c50..4da4715 100644 --- a/src/roblox/datatypes/mod.rs +++ b/crates/lune-roblox/src/datatypes/mod.rs @@ -10,4 +10,4 @@ mod util; use result::*; -pub use crate::roblox::shared::userdata::*; +pub use crate::shared::userdata::*; diff --git a/src/roblox/datatypes/result.rs b/crates/lune-roblox/src/datatypes/result.rs similarity index 100% rename from src/roblox/datatypes/result.rs rename to crates/lune-roblox/src/datatypes/result.rs diff --git a/src/roblox/datatypes/types/axes.rs b/crates/lune-roblox/src/datatypes/types/axes.rs similarity index 95% rename from src/roblox/datatypes/types/axes.rs rename to crates/lune-roblox/src/datatypes/types/axes.rs index 0db23f3..e9aa950 100644 --- a/src/roblox/datatypes/types/axes.rs +++ b/crates/lune-roblox/src/datatypes/types/axes.rs @@ -3,7 +3,9 @@ use core::fmt; use mlua::prelude::*; use rbx_dom_weak::types::Axes as DomAxes; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::{super::*, EnumItem}; @@ -52,8 +54,7 @@ impl LuaExportsTable<'_> for Axes { check(&e); } else { return Err(LuaError::RuntimeError(format!( - "Expected argument #{} to be an EnumItem, got userdata", - index + "Expected argument #{index} to be an EnumItem, got userdata", ))); } } else { diff --git a/src/roblox/datatypes/types/brick_color.rs b/crates/lune-roblox/src/datatypes/types/brick_color.rs similarity index 99% rename from src/roblox/datatypes/types/brick_color.rs rename to crates/lune-roblox/src/datatypes/types/brick_color.rs index 8fa6f86..297793b 100644 --- a/src/roblox/datatypes/types/brick_color.rs +++ b/crates/lune-roblox/src/datatypes/types/brick_color.rs @@ -4,14 +4,16 @@ use mlua::prelude::*; use rand::seq::SliceRandom; use rbx_dom_weak::types::BrickColor as DomBrickColor; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::{super::*, Color3}; /** An implementation of the [BrickColor](https://create.roblox.com/docs/reference/engine/datatypes/BrickColor) Roblox datatype. - This implements all documented properties, methods & constructors of the BrickColor class as of March 2023. + This implements all documented properties, methods & constructors of the `BrickColor` class as of March 2023. */ #[derive(Debug, Clone, Copy, PartialEq)] pub struct BrickColor { diff --git a/src/roblox/datatypes/types/cframe.rs b/crates/lune-roblox/src/datatypes/types/cframe.rs similarity index 92% rename from src/roblox/datatypes/types/cframe.rs rename to crates/lune-roblox/src/datatypes/types/cframe.rs index 4d7cbdf..adb8d88 100644 --- a/src/roblox/datatypes/types/cframe.rs +++ b/crates/lune-roblox/src/datatypes/types/cframe.rs @@ -1,3 +1,5 @@ +#![allow(clippy::items_after_statements)] + use core::fmt; use std::ops; @@ -5,7 +7,9 @@ use glam::{EulerRot, Mat3, Mat4, Quat, Vec3}; use mlua::{prelude::*, Variadic}; use rbx_dom_weak::types::{CFrame as DomCFrame, Matrix3 as DomMatrix3, Vector3 as DomVector3}; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::{super::*, Vector3}; @@ -14,7 +18,7 @@ use super::{super::*, Vector3}; Roblox datatype, backed by [`glam::Mat4`]. This implements all documented properties, methods & - constructors of the CFrame class as of March 2023. + constructors of the `CFrame` class as of March 2023. */ #[derive(Debug, Clone, Copy, PartialEq)] pub struct CFrame(pub Mat4); @@ -42,6 +46,7 @@ impl CFrame { impl LuaExportsTable<'_> for CFrame { const EXPORT_NAME: &'static str = "CFrame"; + #[allow(clippy::too_many_lines)] fn create_exports_table(lua: &Lua) -> LuaResult { let cframe_angles = |_, (rx, ry, rz): (f32, f32, f32)| { Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz))) @@ -68,8 +73,7 @@ impl LuaExportsTable<'_> for CFrame { Ok(CFrame(Mat4::from_cols( rx.0.extend(0.0), ry.0.extend(0.0), - rz.map(|r| r.0) - .unwrap_or_else(|| rx.0.cross(ry.0).normalize()) + rz.map_or_else(|| rx.0.cross(ry.0).normalize(), |r| r.0) .extend(0.0), pos.0.extend(1.0), ))) @@ -195,6 +199,7 @@ impl LuaUserData for CFrame { }); } + #[allow(clippy::too_many_lines)] fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { // Methods methods.add_method("Inverse", |_, this, ()| Ok(this.inverse())); @@ -226,34 +231,49 @@ impl LuaUserData for CFrame { methods.add_method( "ToWorldSpace", |_, this, rhs: Variadic>| { - Ok(Variadic::from_iter(rhs.into_iter().map(|cf| *this * *cf))) + Ok(rhs + .into_iter() + .map(|cf| *this * *cf) + .collect::>()) }, ); methods.add_method( "ToObjectSpace", |_, this, rhs: Variadic>| { let inverse = this.inverse(); - Ok(Variadic::from_iter(rhs.into_iter().map(|cf| inverse * *cf))) + Ok(rhs + .into_iter() + .map(|cf| inverse * *cf) + .collect::>()) }, ); methods.add_method( "PointToWorldSpace", |_, this, rhs: Variadic>| { - Ok(Variadic::from_iter(rhs.into_iter().map(|v3| *this * *v3))) + Ok(rhs + .into_iter() + .map(|v3| *this * *v3) + .collect::>()) }, ); methods.add_method( "PointToObjectSpace", |_, this, rhs: Variadic>| { let inverse = this.inverse(); - Ok(Variadic::from_iter(rhs.into_iter().map(|v3| inverse * *v3))) + Ok(rhs + .into_iter() + .map(|v3| inverse * *v3) + .collect::>()) }, ); methods.add_method( "VectorToWorldSpace", |_, this, rhs: Variadic>| { let result = *this - Vector3(this.position()); - Ok(Variadic::from_iter(rhs.into_iter().map(|v3| result * *v3))) + Ok(rhs + .into_iter() + .map(|v3| result * *v3) + .collect::>()) }, ); methods.add_method( @@ -261,8 +281,10 @@ impl LuaUserData for CFrame { |_, this, rhs: Variadic>| { let inverse = this.inverse(); let result = inverse - Vector3(inverse.position()); - - Ok(Variadic::from_iter(rhs.into_iter().map(|v3| result * *v3))) + Ok(rhs + .into_iter() + .map(|v3| result * *v3) + .collect::>()) }, ); #[rustfmt::skip] @@ -445,7 +467,7 @@ mod cframe_test { Vec3::new(1.0, 2.0, 3.0).extend(1.0), )); - assert_eq!(CFrame::from(dom_cframe), cframe) + assert_eq!(CFrame::from(dom_cframe), cframe); } #[test] @@ -466,6 +488,6 @@ mod cframe_test { ), ); - assert_eq!(DomCFrame::from(cframe), dom_cframe) + assert_eq!(DomCFrame::from(cframe), dom_cframe); } } diff --git a/src/roblox/datatypes/types/color3.rs b/crates/lune-roblox/src/datatypes/types/color3.rs similarity index 97% rename from src/roblox/datatypes/types/color3.rs rename to crates/lune-roblox/src/datatypes/types/color3.rs index 33eba6d..8523732 100644 --- a/src/roblox/datatypes/types/color3.rs +++ b/crates/lune-roblox/src/datatypes/types/color3.rs @@ -1,3 +1,5 @@ +#![allow(clippy::many_single_char_names)] + use core::fmt; use std::ops; @@ -5,7 +7,9 @@ use glam::Vec3; use mlua::prelude::*; use rbx_dom_weak::types::{Color3 as DomColor3, Color3uint8 as DomColor3uint8}; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::super::*; @@ -85,8 +89,7 @@ impl LuaExportsTable<'_> for Color3 { b: (b as f32) / 255f32, }), _ => Err(LuaError::RuntimeError(format!( - "Hex color string '{}' contains invalid character", - trimmed + "Hex color string '{trimmed}' contains invalid character", ))), } }; @@ -151,6 +154,7 @@ impl LuaUserData for Color3 { let max = r.max(g).max(b); let diff = max - min; + #[allow(clippy::float_cmp)] let hue = (match max { max if max == min => 0.0, max if max == r => (g - b) / diff + (if g < b { 6.0 } else { 0.0 }), diff --git a/src/roblox/datatypes/types/color_sequence.rs b/crates/lune-roblox/src/datatypes/types/color_sequence.rs similarity index 93% rename from src/roblox/datatypes/types/color_sequence.rs rename to crates/lune-roblox/src/datatypes/types/color_sequence.rs index a8eb8ab..f2cc1b4 100644 --- a/src/roblox/datatypes/types/color_sequence.rs +++ b/crates/lune-roblox/src/datatypes/types/color_sequence.rs @@ -5,14 +5,16 @@ use rbx_dom_weak::types::{ ColorSequence as DomColorSequence, ColorSequenceKeypoint as DomColorSequenceKeypoint, }; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::{super::*, Color3, ColorSequenceKeypoint}; /** An implementation of the [ColorSequence](https://create.roblox.com/docs/reference/engine/datatypes/ColorSequence) Roblox datatype. - This implements all documented properties, methods & constructors of the ColorSequence class as of March 2023. + This implements all documented properties, methods & constructors of the `ColorSequence` class as of March 2023. */ #[derive(Debug, Clone, PartialEq)] pub struct ColorSequence { @@ -87,9 +89,9 @@ impl fmt::Display for ColorSequence { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for (index, keypoint) in self.keypoints.iter().enumerate() { if index < self.keypoints.len() - 1 { - write!(f, "{}, ", keypoint)?; + write!(f, "{keypoint}, ")?; } else { - write!(f, "{}", keypoint)?; + write!(f, "{keypoint}")?; } } Ok(()) @@ -102,7 +104,7 @@ impl From for ColorSequence { keypoints: v .keypoints .iter() - .cloned() + .copied() .map(ColorSequenceKeypoint::from) .collect(), } @@ -115,7 +117,7 @@ impl From for DomColorSequence { keypoints: v .keypoints .iter() - .cloned() + .copied() .map(DomColorSequenceKeypoint::from) .collect(), } diff --git a/src/roblox/datatypes/types/color_sequence_keypoint.rs b/crates/lune-roblox/src/datatypes/types/color_sequence_keypoint.rs similarity index 94% rename from src/roblox/datatypes/types/color_sequence_keypoint.rs rename to crates/lune-roblox/src/datatypes/types/color_sequence_keypoint.rs index a90b6e7..c52b30e 100644 --- a/src/roblox/datatypes/types/color_sequence_keypoint.rs +++ b/crates/lune-roblox/src/datatypes/types/color_sequence_keypoint.rs @@ -3,14 +3,16 @@ use core::fmt; use mlua::prelude::*; use rbx_dom_weak::types::ColorSequenceKeypoint as DomColorSequenceKeypoint; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::{super::*, Color3}; /** An implementation of the [ColorSequenceKeypoint](https://create.roblox.com/docs/reference/engine/datatypes/ColorSequenceKeypoint) Roblox datatype. - This implements all documented properties, methods & constructors of the ColorSequenceKeypoint class as of March 2023. + This implements all documented properties, methods & constructors of the `ColorSequenceKeypoint` class as of March 2023. */ #[derive(Debug, Clone, Copy, PartialEq)] pub struct ColorSequenceKeypoint { diff --git a/src/roblox/datatypes/types/enum.rs b/crates/lune-roblox/src/datatypes/types/enum.rs similarity index 100% rename from src/roblox/datatypes/types/enum.rs rename to crates/lune-roblox/src/datatypes/types/enum.rs diff --git a/src/roblox/datatypes/types/enum_item.rs b/crates/lune-roblox/src/datatypes/types/enum_item.rs similarity index 98% rename from src/roblox/datatypes/types/enum_item.rs rename to crates/lune-roblox/src/datatypes/types/enum_item.rs index 8a3633c..7089ccc 100644 --- a/src/roblox/datatypes/types/enum_item.rs +++ b/crates/lune-roblox/src/datatypes/types/enum_item.rs @@ -8,7 +8,7 @@ use super::{super::*, Enum}; /** An implementation of the [EnumItem](https://create.roblox.com/docs/reference/engine/datatypes/EnumItem) Roblox datatype. - This implements all documented properties, methods & constructors of the EnumItem class as of March 2023. + This implements all documented properties, methods & constructors of the `EnumItem` class as of March 2023. */ #[derive(Debug, Clone)] pub struct EnumItem { diff --git a/src/roblox/datatypes/types/enums.rs b/crates/lune-roblox/src/datatypes/types/enums.rs similarity index 94% rename from src/roblox/datatypes/types/enums.rs rename to crates/lune-roblox/src/datatypes/types/enums.rs index 5ce78d7..1a93597 100644 --- a/src/roblox/datatypes/types/enums.rs +++ b/crates/lune-roblox/src/datatypes/types/enums.rs @@ -24,8 +24,7 @@ impl LuaUserData for Enums { |_, _, name: String| match Enum::from_name(&name) { Some(e) => Ok(e), None => Err(LuaError::RuntimeError(format!( - "The enum '{}' does not exist", - name + "The enum '{name}' does not exist", ))), }, ); diff --git a/src/roblox/datatypes/types/faces.rs b/crates/lune-roblox/src/datatypes/types/faces.rs similarity index 95% rename from src/roblox/datatypes/types/faces.rs rename to crates/lune-roblox/src/datatypes/types/faces.rs index b23ff72..61a6cd9 100644 --- a/src/roblox/datatypes/types/faces.rs +++ b/crates/lune-roblox/src/datatypes/types/faces.rs @@ -1,9 +1,13 @@ +#![allow(clippy::struct_excessive_bools)] + use core::fmt; use mlua::prelude::*; use rbx_dom_weak::types::Faces as DomFaces; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::{super::*, EnumItem}; @@ -54,8 +58,7 @@ impl LuaExportsTable<'_> for Faces { check(&e); } else { return Err(LuaError::RuntimeError(format!( - "Expected argument #{} to be an EnumItem, got userdata", - index + "Expected argument #{index} to be an EnumItem, got userdata", ))); } } else { diff --git a/src/roblox/datatypes/types/font.rs b/crates/lune-roblox/src/datatypes/types/font.rs similarity index 96% rename from src/roblox/datatypes/types/font.rs rename to crates/lune-roblox/src/datatypes/types/font.rs index d3235d8..8848fea 100644 --- a/src/roblox/datatypes/types/font.rs +++ b/crates/lune-roblox/src/datatypes/types/font.rs @@ -6,7 +6,9 @@ use rbx_dom_weak::types::{ Font as DomFont, FontStyle as DomFontStyle, FontWeight as DomFontWeight, }; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::{super::*, EnumItem}; @@ -62,7 +64,7 @@ impl LuaExportsTable<'_> for Font { let font_from_name = |_, (file, weight, style): (String, Option, Option)| { Ok(Font { - family: format!("rbxasset://fonts/families/{}.json", file), + family: format!("rbxasset://fonts/families/{file}.json"), weight: weight.unwrap_or_default(), style: style.unwrap_or_default(), cached_id: None, @@ -72,7 +74,7 @@ impl LuaExportsTable<'_> for Font { let font_from_id = |_, (id, weight, style): (i32, Option, Option)| { Ok(Font { - family: format!("rbxassetid://{}", id), + family: format!("rbxassetid://{id}"), weight: weight.unwrap_or_default(), style: style.unwrap_or_default(), cached_id: None, @@ -206,7 +208,7 @@ pub(crate) enum FontWeight { } impl FontWeight { - pub(crate) fn as_u16(&self) -> u16 { + pub(crate) fn as_u16(self) -> u16 { match self { Self::Thin => 100, Self::ExtraLight => 200, @@ -288,12 +290,11 @@ impl<'lua> FromLua<'lua> for FontWeight { if value.parent.desc.name == "FontWeight" { if let Ok(value) = FontWeight::from_str(&value.name) { return Ok(value); - } else { - message = Some(format!( - "Found unknown Enum.FontWeight value '{}'", - value.name - )); } + message = Some(format!( + "Found unknown Enum.FontWeight value '{}'", + value.name + )); } else { message = Some(format!( "Expected Enum.FontWeight, got Enum.{}", @@ -316,7 +317,7 @@ impl<'lua> IntoLua<'lua> for FontWeight { None => Err(LuaError::ToLuaConversionError { from: "FontWeight", to: "EnumItem", - message: Some(format!("Found unknown Enum.FontWeight value '{}'", self)), + message: Some(format!("Found unknown Enum.FontWeight value '{self}'")), }), } } @@ -329,7 +330,7 @@ pub(crate) enum FontStyle { } impl FontStyle { - pub(crate) fn as_u8(&self) -> u8 { + pub(crate) fn as_u8(self) -> u8 { match self { Self::Normal => 0, Self::Italic => 1, @@ -383,12 +384,11 @@ impl<'lua> FromLua<'lua> for FontStyle { if value.parent.desc.name == "FontStyle" { if let Ok(value) = FontStyle::from_str(&value.name) { return Ok(value); - } else { - message = Some(format!( - "Found unknown Enum.FontStyle value '{}'", - value.name - )); } + message = Some(format!( + "Found unknown Enum.FontStyle value '{}'", + value.name + )); } else { message = Some(format!( "Expected Enum.FontStyle, got Enum.{}", @@ -411,7 +411,7 @@ impl<'lua> IntoLua<'lua> for FontStyle { None => Err(LuaError::ToLuaConversionError { from: "FontStyle", to: "EnumItem", - message: Some(format!("Found unknown Enum.FontStyle value '{}'", self)), + message: Some(format!("Found unknown Enum.FontStyle value '{self}'")), }), } } diff --git a/src/roblox/datatypes/types/mod.rs b/crates/lune-roblox/src/datatypes/types/mod.rs similarity index 100% rename from src/roblox/datatypes/types/mod.rs rename to crates/lune-roblox/src/datatypes/types/mod.rs diff --git a/src/roblox/datatypes/types/number_range.rs b/crates/lune-roblox/src/datatypes/types/number_range.rs similarity index 94% rename from src/roblox/datatypes/types/number_range.rs rename to crates/lune-roblox/src/datatypes/types/number_range.rs index b3b5ac9..ad839d6 100644 --- a/src/roblox/datatypes/types/number_range.rs +++ b/crates/lune-roblox/src/datatypes/types/number_range.rs @@ -3,14 +3,16 @@ use core::fmt; use mlua::prelude::*; use rbx_dom_weak::types::NumberRange as DomNumberRange; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::super::*; /** An implementation of the [NumberRange](https://create.roblox.com/docs/reference/engine/datatypes/NumberRange) Roblox datatype. - This implements all documented properties, methods & constructors of the NumberRange class as of March 2023. + This implements all documented properties, methods & constructors of the `NumberRange` class as of March 2023. */ #[derive(Debug, Clone, Copy, PartialEq)] pub struct NumberRange { diff --git a/src/roblox/datatypes/types/number_sequence.rs b/crates/lune-roblox/src/datatypes/types/number_sequence.rs similarity index 93% rename from src/roblox/datatypes/types/number_sequence.rs rename to crates/lune-roblox/src/datatypes/types/number_sequence.rs index f6b80bb..12d7374 100644 --- a/src/roblox/datatypes/types/number_sequence.rs +++ b/crates/lune-roblox/src/datatypes/types/number_sequence.rs @@ -5,14 +5,16 @@ use rbx_dom_weak::types::{ NumberSequence as DomNumberSequence, NumberSequenceKeypoint as DomNumberSequenceKeypoint, }; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::{super::*, NumberSequenceKeypoint}; /** An implementation of the [NumberSequence](https://create.roblox.com/docs/reference/engine/datatypes/NumberSequence) Roblox datatype. - This implements all documented properties, methods & constructors of the NumberSequence class as of March 2023. + This implements all documented properties, methods & constructors of the `NumberSequence` class as of March 2023. */ #[derive(Debug, Clone, PartialEq)] pub struct NumberSequence { @@ -91,9 +93,9 @@ impl fmt::Display for NumberSequence { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for (index, keypoint) in self.keypoints.iter().enumerate() { if index < self.keypoints.len() - 1 { - write!(f, "{}, ", keypoint)?; + write!(f, "{keypoint}, ")?; } else { - write!(f, "{}", keypoint)?; + write!(f, "{keypoint}")?; } } Ok(()) @@ -106,7 +108,7 @@ impl From for NumberSequence { keypoints: v .keypoints .iter() - .cloned() + .copied() .map(NumberSequenceKeypoint::from) .collect(), } @@ -119,7 +121,7 @@ impl From for DomNumberSequence { keypoints: v .keypoints .iter() - .cloned() + .copied() .map(DomNumberSequenceKeypoint::from) .collect(), } diff --git a/src/roblox/datatypes/types/number_sequence_keypoint.rs b/crates/lune-roblox/src/datatypes/types/number_sequence_keypoint.rs similarity index 94% rename from src/roblox/datatypes/types/number_sequence_keypoint.rs rename to crates/lune-roblox/src/datatypes/types/number_sequence_keypoint.rs index be9aa1c..9ec43e5 100644 --- a/src/roblox/datatypes/types/number_sequence_keypoint.rs +++ b/crates/lune-roblox/src/datatypes/types/number_sequence_keypoint.rs @@ -3,14 +3,16 @@ use core::fmt; use mlua::prelude::*; use rbx_dom_weak::types::NumberSequenceKeypoint as DomNumberSequenceKeypoint; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::super::*; /** An implementation of the [NumberSequenceKeypoint](https://create.roblox.com/docs/reference/engine/datatypes/NumberSequenceKeypoint) Roblox datatype. - This implements all documented properties, methods & constructors of the NumberSequenceKeypoint class as of March 2023. + This implements all documented properties, methods & constructors of the `NumberSequenceKeypoint` class as of March 2023. */ #[derive(Debug, Clone, Copy, PartialEq)] pub struct NumberSequenceKeypoint { diff --git a/src/roblox/datatypes/types/physical_properties.rs b/crates/lune-roblox/src/datatypes/types/physical_properties.rs similarity index 98% rename from src/roblox/datatypes/types/physical_properties.rs rename to crates/lune-roblox/src/datatypes/types/physical_properties.rs index 198c9ce..fd558eb 100644 --- a/src/roblox/datatypes/types/physical_properties.rs +++ b/crates/lune-roblox/src/datatypes/types/physical_properties.rs @@ -3,14 +3,16 @@ use core::fmt; use mlua::prelude::*; use rbx_dom_weak::types::CustomPhysicalProperties as DomCustomPhysicalProperties; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::{super::*, EnumItem}; /** An implementation of the [PhysicalProperties](https://create.roblox.com/docs/reference/engine/datatypes/PhysicalProperties) Roblox datatype. - This implements all documented properties, methods & constructors of the PhysicalProperties class as of March 2023. + This implements all documented properties, methods & constructors of the `PhysicalProperties` class as of March 2023. */ #[derive(Debug, Clone, Copy, PartialEq)] pub struct PhysicalProperties { diff --git a/src/roblox/datatypes/types/ray.rs b/crates/lune-roblox/src/datatypes/types/ray.rs similarity index 97% rename from src/roblox/datatypes/types/ray.rs rename to crates/lune-roblox/src/datatypes/types/ray.rs index 68aba6e..23a44d7 100644 --- a/src/roblox/datatypes/types/ray.rs +++ b/crates/lune-roblox/src/datatypes/types/ray.rs @@ -4,7 +4,9 @@ use glam::Vec3; use mlua::prelude::*; use rbx_dom_weak::types::Ray as DomRay; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::{super::*, Vector3}; diff --git a/src/roblox/datatypes/types/rect.rs b/crates/lune-roblox/src/datatypes/types/rect.rs similarity index 98% rename from src/roblox/datatypes/types/rect.rs rename to crates/lune-roblox/src/datatypes/types/rect.rs index b305184..0fa805c 100644 --- a/src/roblox/datatypes/types/rect.rs +++ b/crates/lune-roblox/src/datatypes/types/rect.rs @@ -5,7 +5,9 @@ use glam::Vec2; use mlua::prelude::*; use rbx_dom_weak::types::Rect as DomRect; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::{super::*, Vector2}; diff --git a/src/roblox/datatypes/types/region3.rs b/crates/lune-roblox/src/datatypes/types/region3.rs similarity index 97% rename from src/roblox/datatypes/types/region3.rs rename to crates/lune-roblox/src/datatypes/types/region3.rs index 9c9bdf0..cb44df7 100644 --- a/src/roblox/datatypes/types/region3.rs +++ b/crates/lune-roblox/src/datatypes/types/region3.rs @@ -4,7 +4,9 @@ use glam::{Mat4, Vec3}; use mlua::prelude::*; use rbx_dom_weak::types::Region3 as DomRegion3; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::{super::*, CFrame, Vector3}; diff --git a/src/roblox/datatypes/types/region3int16.rs b/crates/lune-roblox/src/datatypes/types/region3int16.rs similarity index 96% rename from src/roblox/datatypes/types/region3int16.rs rename to crates/lune-roblox/src/datatypes/types/region3int16.rs index d64ffde..61075b0 100644 --- a/src/roblox/datatypes/types/region3int16.rs +++ b/crates/lune-roblox/src/datatypes/types/region3int16.rs @@ -4,7 +4,9 @@ use glam::IVec3; use mlua::prelude::*; use rbx_dom_weak::types::Region3int16 as DomRegion3int16; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::{super::*, Vector3int16}; diff --git a/src/roblox/datatypes/types/udim.rs b/crates/lune-roblox/src/datatypes/types/udim.rs similarity index 96% rename from src/roblox/datatypes/types/udim.rs rename to crates/lune-roblox/src/datatypes/types/udim.rs index b390c0c..ac728e8 100644 --- a/src/roblox/datatypes/types/udim.rs +++ b/crates/lune-roblox/src/datatypes/types/udim.rs @@ -4,14 +4,16 @@ use std::ops; use mlua::prelude::*; use rbx_dom_weak::types::UDim as DomUDim; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::super::*; /** An implementation of the [UDim](https://create.roblox.com/docs/reference/engine/datatypes/UDim) Roblox datatype. - This implements all documented properties, methods & constructors of the UDim class as of March 2023. + This implements all documented properties, methods & constructors of the `UDim` class as of March 2023. */ #[derive(Debug, Clone, Copy, PartialEq)] pub struct UDim { diff --git a/src/roblox/datatypes/types/udim2.rs b/crates/lune-roblox/src/datatypes/types/udim2.rs similarity index 97% rename from src/roblox/datatypes/types/udim2.rs rename to crates/lune-roblox/src/datatypes/types/udim2.rs index 4a41f98..df78be4 100644 --- a/src/roblox/datatypes/types/udim2.rs +++ b/crates/lune-roblox/src/datatypes/types/udim2.rs @@ -1,3 +1,5 @@ +#![allow(clippy::items_after_statements)] + use core::fmt; use std::ops; @@ -5,14 +7,16 @@ use glam::Vec2; use mlua::prelude::*; use rbx_dom_weak::types::UDim2 as DomUDim2; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::{super::*, UDim}; /** An implementation of the [UDim2](https://create.roblox.com/docs/reference/engine/datatypes/UDim2) Roblox datatype. - This implements all documented properties, methods & constructors of the UDim2 class as of March 2023. + This implements all documented properties, methods & constructors of the `UDim2` class as of March 2023. */ #[derive(Debug, Clone, Copy, PartialEq)] pub struct UDim2 { diff --git a/src/roblox/datatypes/types/vector2.rs b/crates/lune-roblox/src/datatypes/types/vector2.rs similarity index 98% rename from src/roblox/datatypes/types/vector2.rs rename to crates/lune-roblox/src/datatypes/types/vector2.rs index 0eaad3d..e0c352e 100644 --- a/src/roblox/datatypes/types/vector2.rs +++ b/crates/lune-roblox/src/datatypes/types/vector2.rs @@ -5,7 +5,9 @@ use glam::{Vec2, Vec3}; use mlua::prelude::*; use rbx_dom_weak::types::Vector2 as DomVector2; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::super::*; diff --git a/src/roblox/datatypes/types/vector2int16.rs b/crates/lune-roblox/src/datatypes/types/vector2int16.rs similarity index 98% rename from src/roblox/datatypes/types/vector2int16.rs rename to crates/lune-roblox/src/datatypes/types/vector2int16.rs index 1193428..31931a0 100644 --- a/src/roblox/datatypes/types/vector2int16.rs +++ b/crates/lune-roblox/src/datatypes/types/vector2int16.rs @@ -5,7 +5,9 @@ use glam::IVec2; use mlua::prelude::*; use rbx_dom_weak::types::Vector2int16 as DomVector2int16; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::super::*; diff --git a/src/roblox/datatypes/types/vector3.rs b/crates/lune-roblox/src/datatypes/types/vector3.rs similarity index 95% rename from src/roblox/datatypes/types/vector3.rs rename to crates/lune-roblox/src/datatypes/types/vector3.rs index ef0307e..32387d3 100644 --- a/src/roblox/datatypes/types/vector3.rs +++ b/crates/lune-roblox/src/datatypes/types/vector3.rs @@ -5,10 +5,9 @@ use glam::Vec3; use mlua::prelude::*; use rbx_dom_weak::types::Vector3 as DomVector3; -use crate::{ - lune::util::TableBuilder, - roblox::{datatypes::util::round_float_decimal, exports::LuaExportsTable}, -}; +use lune_utils::TableBuilder; + +use crate::{datatypes::util::round_float_decimal, exports::LuaExportsTable}; use super::{super::*, EnumItem}; @@ -37,8 +36,7 @@ impl LuaExportsTable<'_> for Vector3 { "Z" => Vector3(Vec3::Z), name => { return Err(LuaError::RuntimeError(format!( - "Axis '{}' is not known", - name + "Axis '{name}' is not known", ))) } }) @@ -61,8 +59,7 @@ impl LuaExportsTable<'_> for Vector3 { "Back" => Vector3(Vec3::Z), name => { return Err(LuaError::RuntimeError(format!( - "NormalId '{}' is not known", - name + "NormalId '{name}' is not known", ))) } }) diff --git a/src/roblox/datatypes/types/vector3int16.rs b/crates/lune-roblox/src/datatypes/types/vector3int16.rs similarity index 98% rename from src/roblox/datatypes/types/vector3int16.rs rename to crates/lune-roblox/src/datatypes/types/vector3int16.rs index d62e8ff..b8f4f31 100644 --- a/src/roblox/datatypes/types/vector3int16.rs +++ b/crates/lune-roblox/src/datatypes/types/vector3int16.rs @@ -5,7 +5,9 @@ use glam::IVec3; use mlua::prelude::*; use rbx_dom_weak::types::Vector3int16 as DomVector3int16; -use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; use super::super::*; diff --git a/src/roblox/datatypes/util.rs b/crates/lune-roblox/src/datatypes/util.rs similarity index 100% rename from src/roblox/datatypes/util.rs rename to crates/lune-roblox/src/datatypes/util.rs diff --git a/src/roblox/document/error.rs b/crates/lune-roblox/src/document/error.rs similarity index 100% rename from src/roblox/document/error.rs rename to crates/lune-roblox/src/document/error.rs diff --git a/src/roblox/document/format.rs b/crates/lune-roblox/src/document/format.rs similarity index 100% rename from src/roblox/document/format.rs rename to crates/lune-roblox/src/document/format.rs diff --git a/src/roblox/document/kind.rs b/crates/lune-roblox/src/document/kind.rs similarity index 98% rename from src/roblox/document/kind.rs rename to crates/lune-roblox/src/document/kind.rs index 0339578..eee19ba 100644 --- a/src/roblox/document/kind.rs +++ b/crates/lune-roblox/src/document/kind.rs @@ -2,7 +2,7 @@ use std::path::Path; use rbx_dom_weak::WeakDom; -use crate::roblox::shared::instance::class_is_a_service; +use crate::shared::instance::class_is_a_service; /** A document kind specifier. @@ -58,6 +58,7 @@ impl DocumentKind { Returns `None` if the given dom is empty and as such can not have its kind inferred. */ + #[must_use] pub fn from_weak_dom(dom: &WeakDom) -> Option { let mut has_top_level_child = false; let mut has_top_level_service = false; diff --git a/src/roblox/document/mod.rs b/crates/lune-roblox/src/document/mod.rs similarity index 91% rename from src/roblox/document/mod.rs rename to crates/lune-roblox/src/document/mod.rs index 4f5cf00..5ef3c64 100644 --- a/src/roblox/document/mod.rs +++ b/crates/lune-roblox/src/document/mod.rs @@ -15,7 +15,7 @@ pub use kind::*; use postprocessing::*; -use crate::roblox::instance::{data_model, Instance}; +use crate::instance::{data_model, Instance}; pub type DocumentResult = Result; @@ -78,6 +78,7 @@ impl Document { | Model | Binary | `rbxm` | | Model | Xml | `rbxmx` | */ + #[must_use] #[rustfmt::skip] pub fn canonical_extension(kind: DocumentKind, format: DocumentFormat) -> &'static str { match (kind, format) { @@ -113,6 +114,10 @@ impl Document { Note that detection of model vs place file is heavily dependent on the structure of the file, and a model file with services in it will detect as a place file, so if possible using [`Document::from_bytes`] with an explicit kind should be preferred. + + # Errors + + Errors if the given bytes are not a valid roblox file. */ pub fn from_bytes_auto(bytes: impl AsRef<[u8]>) -> DocumentResult { let (format, dom) = Self::from_bytes_inner(bytes)?; @@ -125,6 +130,10 @@ impl Document { This will automatically handle and detect if the document should be decoded using a roblox binary or roblox xml format. + + # Errors + + Errors if the given bytes are not a valid roblox file or not of the given kind. */ pub fn from_bytes(bytes: impl AsRef<[u8]>, kind: DocumentKind) -> DocumentResult { let (format, dom) = Self::from_bytes_inner(bytes)?; @@ -138,6 +147,10 @@ impl Document { This will use the same format that the document was created with, meaning if the document is a binary document the output will be binary, and vice versa for xml and other future formats. + + # Errors + + Errors if the document can not be encoded. */ pub fn to_bytes(&self) -> DocumentResult> { self.to_bytes_with_format(self.format) @@ -146,6 +159,10 @@ impl Document { /** Encodes the document as a vector of bytes, to be written to a file or sent over the network. + + # Errors + + Errors if the document can not be encoded. */ pub fn to_bytes_with_format(&self, format: DocumentFormat) -> DocumentResult> { let mut bytes = Vec::new(); @@ -172,6 +189,7 @@ impl Document { /** Gets the kind this document was created with. */ + #[must_use] pub fn kind(&self) -> DocumentKind { self.kind } @@ -179,6 +197,7 @@ impl Document { /** Gets the format this document was created with. */ + #[must_use] pub fn format(&self) -> DocumentFormat { self.format } @@ -186,14 +205,17 @@ impl Document { /** Gets the file extension for this document. */ + #[must_use] pub fn extension(&self) -> &'static str { Self::canonical_extension(self.kind, self.format) } /** - Creates a DataModel instance out of this place document. + Creates a `DataModel` instance out of this place document. - Will error if the document is not a place. + # Errors + + Errors if the document is not a place. */ pub fn into_data_model_instance(mut self) -> DocumentResult { if self.kind != DocumentKind::Place { @@ -219,7 +241,9 @@ impl Document { /** Creates an array of instances out of this model document. - Will error if the document is not a model. + # Errors + + Errors if the document is not a model. */ pub fn into_instance_array(mut self) -> DocumentResult> { if self.kind != DocumentKind::Model { @@ -237,9 +261,11 @@ impl Document { } /** - Creates a place document out of a DataModel instance. + Creates a place document out of a `DataModel` instance. - Will error if the instance is not a DataModel. + # Errors + + Errors if the instance is not a `DataModel`. */ pub fn from_data_model_instance(i: Instance) -> DocumentResult { if i.get_class_name() != data_model::CLASS_NAME { @@ -266,7 +292,9 @@ impl Document { /** Creates a model document out of an array of instances. - Will error if any of the instances is a DataModel. + # Errors + + Errors if any of the instances is a `DataModel`. */ pub fn from_instance_array(v: Vec) -> DocumentResult { for i in &v { diff --git a/src/roblox/document/postprocessing.rs b/crates/lune-roblox/src/document/postprocessing.rs similarity index 96% rename from src/roblox/document/postprocessing.rs rename to crates/lune-roblox/src/document/postprocessing.rs index afa5b3f..69481f5 100644 --- a/src/roblox/document/postprocessing.rs +++ b/crates/lune-roblox/src/document/postprocessing.rs @@ -3,7 +3,7 @@ use rbx_dom_weak::{ Instance as DomInstance, WeakDom, }; -use crate::roblox::shared::instance::class_is_a; +use crate::shared::instance::class_is_a; pub fn postprocess_dom_for_place(_dom: &mut WeakDom) { // Nothing here yet diff --git a/src/roblox/exports.rs b/crates/lune-roblox/src/exports.rs similarity index 100% rename from src/roblox/exports.rs rename to crates/lune-roblox/src/exports.rs diff --git a/src/roblox/instance/base.rs b/crates/lune-roblox/src/instance/base.rs similarity index 93% rename from src/roblox/instance/base.rs rename to crates/lune-roblox/src/instance/base.rs index 594952e..cc35373 100644 --- a/src/roblox/instance/base.rs +++ b/crates/lune-roblox/src/instance/base.rs @@ -1,3 +1,5 @@ +#![allow(clippy::items_after_statements)] + use mlua::prelude::*; use rbx_dom_weak::{ @@ -5,7 +7,7 @@ use rbx_dom_weak::{ Instance as DomInstance, }; -use crate::roblox::{ +use crate::{ datatypes::{ attributes::{ensure_valid_attribute_name, ensure_valid_attribute_value}, conversion::{DomValueToLua, LuaToDomValue}, @@ -17,6 +19,7 @@ use crate::roblox::{ use super::{data_model, registry::InstanceRegistry, Instance}; +#[allow(clippy::too_many_lines)] pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) { m.add_meta_method(LuaMetaMethod::ToString, |lua, this, ()| { ensure_not_destroyed(this)?; @@ -142,7 +145,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) { ensure_not_destroyed(this)?; let attributes = this.get_attributes(); let tab = lua.create_table_with_capacity(0, attributes.len())?; - for (key, value) in attributes.into_iter() { + for (key, value) in attributes { tab.set(key, LuaValue::dom_value_to_lua(lua, &value)?)?; } Ok(tab) @@ -227,8 +230,7 @@ fn instance_property_get<'lua>( if let DomValue::Enum(enum_value) = prop { let enum_name = info.enum_name.ok_or_else(|| { LuaError::RuntimeError(format!( - "Failed to get property '{}' - encountered unknown enum", - prop_name + "Failed to get property '{prop_name}' - encountered unknown enum", )) })?; EnumItem::from_enum_name_and_value(&enum_name, enum_value.to_u32()) @@ -246,8 +248,7 @@ fn instance_property_get<'lua>( EnumItem::from_enum_name_and_value(&enum_name, enum_value) .ok_or_else(|| { LuaError::RuntimeError(format!( - "Failed to get property '{}' - Enum.{} does not contain numeric value {}", - prop_name, enum_name, enum_value + "Failed to get property '{prop_name}' - Enum.{enum_name} does not contain numeric value {enum_value}", )) })? .into_lua(lua) @@ -258,14 +259,12 @@ fn instance_property_get<'lua>( Ok(LuaValue::Nil) } else { Err(LuaError::RuntimeError(format!( - "Failed to get property '{}' - missing default value", - prop_name + "Failed to get property '{prop_name}' - missing default value", ))) } } else { Err(LuaError::RuntimeError(format!( - "Failed to get property '{}' - malformed property info", - prop_name + "Failed to get property '{prop_name}' - malformed property info", ))) } } else if let Some(inst) = this.find_child(|inst| inst.name == prop_name) { @@ -276,8 +275,7 @@ fn instance_property_get<'lua>( Ok(LuaValue::Function(method)) } else { Err(LuaError::RuntimeError(format!( - "{} is not a valid member of {}", - prop_name, this + "{prop_name} is not a valid member of {this}", ))) } } @@ -347,16 +345,14 @@ fn instance_property_set<'lua>( } } else { Err(LuaError::RuntimeError(format!( - "Failed to set property '{}' - malformed property info", - prop_name + "Failed to set property '{prop_name}' - malformed property info", ))) } } else if let Some(setter) = InstanceRegistry::find_property_setter(lua, this, &prop_name) { setter.call((this.clone(), prop_value)) } else { Err(LuaError::RuntimeError(format!( - "{} is not a valid member of {}", - prop_name, this + "{prop_name} is not a valid member of {this}", ))) } } diff --git a/src/roblox/instance/data_model.rs b/crates/lune-roblox/src/instance/data_model.rs similarity index 89% rename from src/roblox/instance/data_model.rs rename to crates/lune-roblox/src/instance/data_model.rs index 08d99be..cfbc9d5 100644 --- a/src/roblox/instance/data_model.rs +++ b/crates/lune-roblox/src/instance/data_model.rs @@ -1,6 +1,6 @@ use mlua::prelude::*; -use crate::roblox::shared::{ +use crate::shared::{ classes::{ add_class_restricted_getter, add_class_restricted_method, get_or_create_property_ref_instance, @@ -33,7 +33,7 @@ fn data_model_get_workspace(_: &Lua, this: &Instance) -> LuaResult { } /** - Gets or creates a service for this DataModel. + Gets or creates a service for this `DataModel`. ### See Also * [`GetService`](https://create.roblox.com/docs/reference/engine/classes/ServiceProvider#GetService) @@ -42,8 +42,7 @@ fn data_model_get_workspace(_: &Lua, this: &Instance) -> LuaResult { fn data_model_get_service(_: &Lua, this: &Instance, service_name: String) -> LuaResult { if matches!(class_is_a_service(&service_name), None | Some(false)) { Err(LuaError::RuntimeError(format!( - "'{}' is not a valid service name", - service_name + "'{service_name}' is not a valid service name", ))) } else if let Some(service) = this.find_child(|child| child.class == service_name) { Ok(service) @@ -55,7 +54,7 @@ fn data_model_get_service(_: &Lua, this: &Instance, service_name: String) -> Lua } /** - Gets a service for this DataModel, if it exists. + Gets a service for this `DataModel`, if it exists. ### See Also * [`FindService`](https://create.roblox.com/docs/reference/engine/classes/ServiceProvider#FindService) @@ -68,8 +67,7 @@ fn data_model_find_service( ) -> LuaResult> { if matches!(class_is_a_service(&service_name), None | Some(false)) { Err(LuaError::RuntimeError(format!( - "'{}' is not a valid service name", - service_name + "'{service_name}' is not a valid service name", ))) } else if let Some(service) = this.find_child(|child| child.class == service_name) { Ok(Some(service)) diff --git a/src/roblox/instance/mod.rs b/crates/lune-roblox/src/instance/mod.rs similarity index 96% rename from src/roblox/instance/mod.rs rename to crates/lune-roblox/src/instance/mod.rs index 6473b04..bc75f2f 100644 --- a/src/roblox/instance/mod.rs +++ b/crates/lune-roblox/src/instance/mod.rs @@ -1,3 +1,5 @@ +#![allow(clippy::missing_panics_doc)] + use std::{ collections::{BTreeMap, VecDeque}, fmt, @@ -12,10 +14,11 @@ use rbx_dom_weak::{ Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, WeakDom, }; +use lune_utils::TableBuilder; + use crate::{ - lune::util::TableBuilder, - roblox::exports::LuaExportsTable, - roblox::shared::instance::{class_exists, class_is_a}, + exports::LuaExportsTable, + shared::instance::{class_exists, class_is_a}, }; pub(crate) mod base; @@ -54,9 +57,10 @@ impl Instance { .get_by_ref(dom_ref) .expect("Failed to find instance in document"); - if instance.referent() == dom.root_ref() { - panic!("Instances can not be created from dom roots") - } + assert!( + !(instance.referent() == dom.root_ref()), + "Instances can not be created from dom roots" + ); Self { dom_ref, @@ -76,9 +80,10 @@ impl Instance { let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); if let Some(instance) = dom.get_by_ref(dom_ref) { - if instance.referent() == dom.root_ref() { - panic!("Instances can not be created from dom roots") - } + assert!( + !(instance.referent() == dom.root_ref()), + "Instances can not be created from dom roots" + ); Some(Self { dom_ref, @@ -154,7 +159,7 @@ impl Instance { let cloned = dom.clone_multiple_into_external(referents, external_dom); - for referent in cloned.iter() { + for referent in &cloned { external_dom.transfer_within(*referent, external_dom.root_ref()); } @@ -171,7 +176,8 @@ impl Instance { * [`Clone`](https://create.roblox.com/docs/reference/engine/classes/Instance#Clone) on the Roblox Developer Hub */ - pub fn clone_instance(&self) -> Instance { + #[must_use] + pub fn clone_instance(&self) -> Self { let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document"); let new_ref = dom.clone_within(self.dom_ref); drop(dom); // Self::new needs mutex handle, drop it first @@ -254,6 +260,7 @@ impl Instance { * [`ClassName`](https://create.roblox.com/docs/reference/engine/classes/Instance#ClassName) on the Roblox Developer Hub */ + #[must_use] pub fn get_class_name(&self) -> &str { self.class_name.as_str() } @@ -286,7 +293,7 @@ impl Instance { dom.get_by_ref_mut(self.dom_ref) .expect("Failed to find instance in document") - .name = name.into() + .name = name.into(); } /** @@ -326,9 +333,7 @@ impl Instance { pub fn set_parent(&self, parent: Option) { let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document"); - let parent_ref = parent - .map(|parent| parent.dom_ref) - .unwrap_or_else(|| dom.root_ref()); + let parent_ref = parent.map_or_else(|| dom.root_ref(), |parent| parent.dom_ref); dom.transfer_within(self.dom_ref, parent_ref); } @@ -663,9 +668,8 @@ impl Instance { if predicate(ancestor) { drop(dom); // Self::new needs mutex handle, drop it first return Some(Self::new(ancestor_ref)); - } else { - ancestor_ref = ancestor.parent(); } + ancestor_ref = ancestor.parent(); } None @@ -699,9 +703,8 @@ impl Instance { let queue_ref = queue_item.referent(); drop(dom); // Self::new needs mutex handle, drop it first return Some(Self::new(queue_ref)); - } else { - queue.extend(queue_item.children()) } + queue.extend(queue_item.children()); } None @@ -717,8 +720,7 @@ impl LuaExportsTable<'_> for Instance { Instance::new_orphaned(class_name).into_lua(lua) } else { Err(LuaError::RuntimeError(format!( - "Failed to create Instance - '{}' is not a valid class name", - class_name + "Failed to create Instance - '{class_name}' is not a valid class name", ))) } }; @@ -756,7 +758,7 @@ impl LuaUserData for Instance { impl Hash for Instance { fn hash(&self, state: &mut H) { - self.dom_ref.hash(state) + self.dom_ref.hash(state); } } diff --git a/src/roblox/instance/registry.rs b/crates/lune-roblox/src/instance/registry.rs similarity index 86% rename from src/roblox/instance/registry.rs rename to crates/lune-roblox/src/instance/registry.rs index 1dfd6f0..11aa175 100644 --- a/src/roblox/instance/registry.rs +++ b/crates/lune-roblox/src/instance/registry.rs @@ -51,6 +51,13 @@ impl InstanceRegistry { .expect("Missing InstanceRegistry in app data") } + /** + Inserts a method into the instance registry. + + # Errors + + - If the method already exists in the registry. + */ pub fn insert_method<'lua>( lua: &'lua Lua, class_name: &str, @@ -80,6 +87,13 @@ impl InstanceRegistry { Ok(()) } + /** + Inserts a property getter into the instance registry. + + # Errors + + - If the property already exists in the registry. + */ pub fn insert_property_getter<'lua>( lua: &'lua Lua, class_name: &str, @@ -109,6 +123,13 @@ impl InstanceRegistry { Ok(()) } + /** + Inserts a property setter into the instance registry. + + # Errors + + - If the property already exists in the registry. + */ pub fn insert_property_setter<'lua>( lua: &'lua Lua, class_name: &str, @@ -138,6 +159,12 @@ impl InstanceRegistry { Ok(()) } + /** + Finds a method in the instance registry. + + Returns `None` if the method is not found. + */ + #[must_use] pub fn find_method<'lua>( lua: &'lua Lua, instance: &Instance, @@ -159,6 +186,12 @@ impl InstanceRegistry { }) } + /** + Finds a property getter in the instance registry. + + Returns `None` if the property getter is not found. + */ + #[must_use] pub fn find_property_getter<'lua>( lua: &'lua Lua, instance: &Instance, @@ -180,6 +213,12 @@ impl InstanceRegistry { }) } + /** + Finds a property setter in the instance registry. + + Returns `None` if the property setter is not found. + */ + #[must_use] pub fn find_property_setter<'lua>( lua: &'lua Lua, instance: &Instance, @@ -202,6 +241,16 @@ impl InstanceRegistry { } } +/** + Gets the class name chain for a given class name. + + The chain starts with the given class name and ends with the root class. + + # Panics + + Panics if the class name is not valid. +*/ +#[must_use] pub fn class_name_chain(class_name: &str) -> Vec<&str> { let db = rbx_reflection_database::get(); diff --git a/src/roblox/instance/terrain.rs b/crates/lune-roblox/src/instance/terrain.rs similarity index 99% rename from src/roblox/instance/terrain.rs rename to crates/lune-roblox/src/instance/terrain.rs index fad8e3d..852f321 100644 --- a/src/roblox/instance/terrain.rs +++ b/crates/lune-roblox/src/instance/terrain.rs @@ -1,7 +1,7 @@ use mlua::prelude::*; use rbx_dom_weak::types::{MaterialColors, TerrainMaterials, Variant}; -use crate::roblox::{ +use crate::{ datatypes::types::{Color3, EnumItem}, shared::classes::{add_class_restricted_method, add_class_restricted_method_mut}, }; @@ -23,7 +23,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(methods: &mut M) CLASS_NAME, "SetMaterialColor", terrain_set_material_color, - ) + ); } fn get_or_create_material_colors(instance: &Instance) -> MaterialColors { diff --git a/src/roblox/instance/workspace.rs b/crates/lune-roblox/src/instance/workspace.rs similarity index 90% rename from src/roblox/instance/workspace.rs rename to crates/lune-roblox/src/instance/workspace.rs index 1af3f54..70f1b88 100644 --- a/src/roblox/instance/workspace.rs +++ b/crates/lune-roblox/src/instance/workspace.rs @@ -1,8 +1,6 @@ use mlua::prelude::*; -use crate::roblox::shared::classes::{ - add_class_restricted_getter, get_or_create_property_ref_instance, -}; +use crate::shared::classes::{add_class_restricted_getter, get_or_create_property_ref_instance}; use super::Instance; diff --git a/src/roblox/mod.rs b/crates/lune-roblox/src/lib.rs similarity index 81% rename from src/roblox/mod.rs rename to crates/lune-roblox/src/lib.rs index 6b0f938..878a2f2 100644 --- a/src/roblox/mod.rs +++ b/crates/lune-roblox/src/lib.rs @@ -1,6 +1,8 @@ +#![allow(clippy::cargo_common_metadata)] + use mlua::prelude::*; -use crate::lune::util::TableBuilder; +use lune_utils::TableBuilder; pub mod datatypes; pub mod document; @@ -46,6 +48,16 @@ fn create_all_exports(lua: &Lua) -> LuaResult> { ]) } +/** + Creates a table containing all the Roblox datatypes, classes, and singletons. + + Note that this is not guaranteed to contain any value unless indexed directly, + it may be optimized to use lazy initialization in the future. + + # Errors + + Errors when out of memory or when a value cannot be created. +*/ pub fn module(lua: &Lua) -> LuaResult { // FUTURE: We can probably create these lazily as users // index the main exports (this return value) table and diff --git a/src/roblox/reflection/class.rs b/crates/lune-roblox/src/reflection/class.rs similarity index 91% rename from src/roblox/reflection/class.rs rename to crates/lune-roblox/src/reflection/class.rs index 13ce84a..f5b2aac 100644 --- a/src/roblox/reflection/class.rs +++ b/crates/lune-roblox/src/reflection/class.rs @@ -7,7 +7,7 @@ use rbx_dom_weak::types::Variant as DomVariant; use rbx_reflection::{ClassDescriptor, DataType}; use super::{property::DatabaseProperty, utils::*}; -use crate::roblox::datatypes::{ +use crate::datatypes::{ conversion::DomValueToLua, types::EnumItem, userdata_impl_eq, userdata_impl_to_string, }; @@ -28,6 +28,7 @@ impl DatabaseClass { /** Get the name of this class. */ + #[must_use] pub fn get_name(&self) -> String { self.0.name.to_string() } @@ -37,6 +38,7 @@ impl DatabaseClass { May be `None` if no parent class exists. */ + #[must_use] pub fn get_superclass(&self) -> Option { let sup = self.0.superclass.as_ref()?; Some(sup.to_string()) @@ -45,6 +47,7 @@ impl DatabaseClass { /** Get all known properties for this class. */ + #[must_use] pub fn get_properties(&self) -> HashMap { self.0 .properties @@ -56,6 +59,7 @@ impl DatabaseClass { /** Get all default values for properties of this class. */ + #[must_use] pub fn get_defaults(&self) -> HashMap { self.0 .default_properties @@ -71,7 +75,12 @@ impl DatabaseClass { to players at runtime, and top-level class categories. */ pub fn get_tags_str(&self) -> Vec<&'static str> { - self.0.tags.iter().map(class_tag_to_str).collect::>() + self.0 + .tags + .iter() + .copied() + .map(class_tag_to_str) + .collect::>() } } @@ -135,14 +144,12 @@ fn make_enum_value(inner: DbClass, name: impl AsRef, value: u32) -> LuaResu let name = name.as_ref(); let enum_name = find_enum_name(inner, name).ok_or_else(|| { LuaError::RuntimeError(format!( - "Failed to get default property '{}' - No enum descriptor was found", - name + "Failed to get default property '{name}' - No enum descriptor was found", )) })?; EnumItem::from_enum_name_and_value(&enum_name, value).ok_or_else(|| { LuaError::RuntimeError(format!( - "Failed to get default property '{}' - Enum.{} does not contain numeric value {}", - name, enum_name, value + "Failed to get default property '{name}' - Enum.{enum_name} does not contain numeric value {value}", )) }) } diff --git a/src/roblox/reflection/enums.rs b/crates/lune-roblox/src/reflection/enums.rs similarity index 91% rename from src/roblox/reflection/enums.rs rename to crates/lune-roblox/src/reflection/enums.rs index 2ed29a0..62f786c 100644 --- a/src/roblox/reflection/enums.rs +++ b/crates/lune-roblox/src/reflection/enums.rs @@ -4,7 +4,7 @@ use mlua::prelude::*; use rbx_reflection::EnumDescriptor; -use crate::roblox::datatypes::{userdata_impl_eq, userdata_impl_to_string}; +use crate::datatypes::{userdata_impl_eq, userdata_impl_to_string}; type DbEnum = &'static EnumDescriptor<'static>; @@ -23,6 +23,7 @@ impl DatabaseEnum { /** Get the name of this enum. */ + #[must_use] pub fn get_name(&self) -> String { self.0.name.to_string() } @@ -31,8 +32,9 @@ impl DatabaseEnum { Get all known members of this enum. Note that this is a direct map of name -> enum values, - and does not actually use the EnumItem datatype itself. + and does not actually use the `EnumItem` datatype itself. */ + #[must_use] pub fn get_items(&self) -> HashMap { self.0 .items diff --git a/src/roblox/reflection/mod.rs b/crates/lune-roblox/src/reflection/mod.rs similarity index 89% rename from src/roblox/reflection/mod.rs rename to crates/lune-roblox/src/reflection/mod.rs index 3b59200..9bb2203 100644 --- a/src/roblox/reflection/mod.rs +++ b/crates/lune-roblox/src/reflection/mod.rs @@ -4,7 +4,7 @@ use mlua::prelude::*; use rbx_reflection::ReflectionDatabase; -use crate::roblox::datatypes::userdata_impl_eq; +use crate::datatypes::userdata_impl_eq; mod class; mod enums; @@ -30,6 +30,7 @@ impl Database { /** Creates a new database struct, referencing the bundled reflection database. */ + #[must_use] pub fn new() -> Self { Self::default() } @@ -40,6 +41,7 @@ impl Database { This will follow the format `x.y.z.w`, which most commonly looks something like `0.567.0.123456789`. */ + #[must_use] pub fn get_version(&self) -> String { let [x, y, z, w] = self.0.version; format!("{x}.{y}.{z}.{w}") @@ -48,15 +50,17 @@ impl Database { /** Retrieves a list of all currently known enum names. */ + #[must_use] pub fn get_enum_names(&self) -> Vec { - self.0.enums.keys().map(|e| e.to_string()).collect() + self.0.enums.keys().map(ToString::to_string).collect() } /** Retrieves a list of all currently known class names. */ + #[must_use] pub fn get_class_names(&self) -> Vec { - self.0.classes.keys().map(|e| e.to_string()).collect() + self.0.classes.keys().map(ToString::to_string).collect() } /** @@ -108,14 +112,17 @@ impl Database { impl LuaUserData for Database { fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("Version", |_, this| Ok(this.get_version())) + fields.add_field_method_get("Version", |_, this| Ok(this.get_version())); } fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); - methods.add_method("GetEnumNames", |_, this, _: ()| Ok(this.get_enum_names())); - methods.add_method("GetClassNames", |_, this, _: ()| Ok(this.get_class_names())); + methods.add_method("GetEnumNames", |_, this, (): ()| Ok(this.get_enum_names())); + methods.add_method( + "GetClassNames", + |_, this, (): ()| Ok(this.get_class_names()), + ); methods.add_method("GetEnum", |_, this, name: String| Ok(this.get_enum(name))); methods.add_method("GetClass", |_, this, name: String| Ok(this.get_class(name))); methods.add_method("FindEnum", |_, this, name: String| Ok(this.find_enum(name))); diff --git a/src/roblox/reflection/property.rs b/crates/lune-roblox/src/reflection/property.rs similarity index 93% rename from src/roblox/reflection/property.rs rename to crates/lune-roblox/src/reflection/property.rs index d674d3c..3e169b3 100644 --- a/src/roblox/reflection/property.rs +++ b/crates/lune-roblox/src/reflection/property.rs @@ -5,7 +5,7 @@ use mlua::prelude::*; use rbx_reflection::{ClassDescriptor, PropertyDescriptor}; use super::utils::*; -use crate::roblox::datatypes::{userdata_impl_eq, userdata_impl_to_string}; +use crate::datatypes::{userdata_impl_eq, userdata_impl_to_string}; type DbClass = &'static ClassDescriptor<'static>; type DbProp = &'static PropertyDescriptor<'static>; @@ -25,6 +25,7 @@ impl DatabaseProperty { /** Get the name of this property. */ + #[must_use] pub fn get_name(&self) -> String { self.1.name.to_string() } @@ -36,6 +37,7 @@ impl DatabaseProperty { For enums this will be a string formatted as `Enum.EnumName`. */ + #[must_use] pub fn get_datatype_name(&self) -> String { data_type_to_str(self.1.data_type.clone()) } @@ -45,8 +47,9 @@ impl DatabaseProperty { All properties are writable and readable in Lune even if scriptability is not. */ + #[must_use] pub fn get_scriptability_str(&self) -> &'static str { - scriptability_to_str(&self.1.scriptability) + scriptability_to_str(self.1.scriptability) } /** @@ -59,6 +62,7 @@ impl DatabaseProperty { self.1 .tags .iter() + .copied() .map(property_tag_to_str) .collect::>() } diff --git a/src/roblox/reflection/utils.rs b/crates/lune-roblox/src/reflection/utils.rs similarity index 86% rename from src/roblox/reflection/utils.rs rename to crates/lune-roblox/src/reflection/utils.rs index 84affd1..aa2042b 100644 --- a/src/roblox/reflection/utils.rs +++ b/crates/lune-roblox/src/reflection/utils.rs @@ -1,6 +1,6 @@ use rbx_reflection::{ClassTag, DataType, PropertyTag, Scriptability}; -use crate::roblox::datatypes::extension::DomValueExt; +use crate::datatypes::extension::DomValueExt; pub fn data_type_to_str(data_type: DataType) -> String { match data_type { @@ -17,7 +17,7 @@ pub fn data_type_to_str(data_type: DataType) -> String { NOTE: Remember to add any new strings here to typedefs too! */ -pub fn scriptability_to_str(scriptability: &Scriptability) -> &'static str { +pub fn scriptability_to_str(scriptability: Scriptability) -> &'static str { match scriptability { Scriptability::None => "None", Scriptability::Custom => "Custom", @@ -28,7 +28,7 @@ pub fn scriptability_to_str(scriptability: &Scriptability) -> &'static str { } } -pub fn property_tag_to_str(tag: &PropertyTag) -> &'static str { +pub fn property_tag_to_str(tag: PropertyTag) -> &'static str { match tag { PropertyTag::Deprecated => "Deprecated", PropertyTag::Hidden => "Hidden", @@ -41,7 +41,7 @@ pub fn property_tag_to_str(tag: &PropertyTag) -> &'static str { } } -pub fn class_tag_to_str(tag: &ClassTag) -> &'static str { +pub fn class_tag_to_str(tag: ClassTag) -> &'static str { match tag { ClassTag::Deprecated => "Deprecated", ClassTag::NotBrowsable => "NotBrowsable", diff --git a/src/roblox/shared/classes.rs b/crates/lune-roblox/src/shared/classes.rs similarity index 89% rename from src/roblox/shared/classes.rs rename to crates/lune-roblox/src/shared/classes.rs index 90af83a..e87759b 100644 --- a/src/roblox/shared/classes.rs +++ b/crates/lune-roblox/src/shared/classes.rs @@ -2,7 +2,7 @@ use mlua::prelude::*; use rbx_dom_weak::types::Variant as DomValue; -use crate::roblox::instance::Instance; +use crate::instance::Instance; use super::instance::class_is_a; @@ -20,8 +20,7 @@ pub(crate) fn add_class_restricted_getter<'lua, F: LuaUserDataFields<'lua, Insta field_getter(lua, this) } else { Err(LuaError::RuntimeError(format!( - "{} is not a valid member of {}", - field_name, class_name + "{field_name} is not a valid member of {class_name}", ))) } }); @@ -42,8 +41,7 @@ pub(crate) fn add_class_restricted_setter<'lua, F: LuaUserDataFields<'lua, Insta field_getter(lua, this, value) } else { Err(LuaError::RuntimeError(format!( - "{} is not a valid member of {}", - field_name, class_name + "{field_name} is not a valid member of {class_name}", ))) } }); @@ -64,8 +62,7 @@ pub(crate) fn add_class_restricted_method<'lua, M: LuaUserDataMethods<'lua, Inst method(lua, this, args) } else { Err(LuaError::RuntimeError(format!( - "{} is not a valid member of {}", - method_name, class_name + "{method_name} is not a valid member of {class_name}", ))) } }); @@ -92,8 +89,7 @@ pub(crate) fn add_class_restricted_method_mut< method(lua, this, args) } else { Err(LuaError::RuntimeError(format!( - "{} is not a valid member of {}", - method_name, class_name + "{method_name} is not a valid member of {class_name}", ))) } }); @@ -102,7 +98,7 @@ pub(crate) fn add_class_restricted_method_mut< /** Gets or creates the instance child with the given reference prop name and class name. - Note that the class name here must be an exact match, it is not checked using IsA. + Note that the class name here must be an exact match, it is not checked using `IsA`. The instance may be in one of several states but this function will guarantee that the property reference is correct and that the instance exists after it has been called: diff --git a/src/roblox/shared/instance.rs b/crates/lune-roblox/src/shared/instance.rs similarity index 98% rename from src/roblox/shared/instance.rs rename to crates/lune-roblox/src/shared/instance.rs index a685ffb..c8b7c57 100644 --- a/src/roblox/shared/instance.rs +++ b/crates/lune-roblox/src/shared/instance.rs @@ -60,12 +60,12 @@ pub(crate) fn find_property_info( value_type: Some(*value_type), ..Default::default() }, - _ => Default::default(), + _ => PropertyInfo::default(), }); break; } else if let Some(sup) = &class.superclass { // No property found, we should look at the superclass - class_name = Cow::Borrowed(sup) + class_name = Cow::Borrowed(sup); } else { break; } @@ -87,7 +87,7 @@ pub(crate) fn find_property_info( break; } else if let Some(sup) = &class.superclass { // No default value found, we should look at the superclass - class_name = Cow::Borrowed(sup) + class_name = Cow::Borrowed(sup); } else { break; } diff --git a/src/roblox/shared/mod.rs b/crates/lune-roblox/src/shared/mod.rs similarity index 100% rename from src/roblox/shared/mod.rs rename to crates/lune-roblox/src/shared/mod.rs diff --git a/src/roblox/shared/userdata.rs b/crates/lune-roblox/src/shared/userdata.rs similarity index 91% rename from src/roblox/shared/userdata.rs rename to crates/lune-roblox/src/shared/userdata.rs index e43fb9a..9184625 100644 --- a/src/roblox/shared/userdata.rs +++ b/crates/lune-roblox/src/shared/userdata.rs @@ -1,3 +1,5 @@ +#![allow(clippy::missing_errors_doc)] + use std::{any::type_name, cell::RefCell, fmt, ops}; use mlua::prelude::*; @@ -5,21 +7,29 @@ use mlua::prelude::*; // Utility functions type ListWriter = dyn Fn(&mut fmt::Formatter<'_>, bool, &str) -> fmt::Result; + +#[must_use] pub fn make_list_writer() -> Box { let first = RefCell::new(true); Box::new(move |f, flag, literal| { if flag { if first.take() { - write!(f, "{}", literal)?; + write!(f, "{literal}")?; } else { - write!(f, ", {}", literal)?; + write!(f, ", {literal}")?; } } Ok::<_, fmt::Error>(()) }) } -// Userdata metamethod implementations +/** + Userdata metamethod implementations + + Note that many of these return [`LuaResult`] even though they don't + return any errors - this is for consistency reasons and to make it + easier to add these blanket implementations to [`LuaUserData`] impls. +*/ pub fn userdata_impl_to_string(_: &Lua, datatype: &D, _: ()) -> LuaResult where diff --git a/crates/lune-std-datetime/Cargo.toml b/crates/lune-std-datetime/Cargo.toml new file mode 100644 index 0000000..34bef78 --- /dev/null +++ b/crates/lune-std-datetime/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "lune-std-datetime" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[lib] +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +mlua = { version = "0.9.7", features = ["luau"] } + +thiserror = "1.0" +chrono = "=0.4.34" # NOTE: 0.4.35 does not compile with chrono_lc +chrono_lc = "0.1" + +lune-utils = { version = "0.1.0", path = "../lune-utils" } diff --git a/src/lune/builtins/datetime/mod.rs b/crates/lune-std-datetime/src/date_time.rs similarity index 91% rename from src/lune/builtins/datetime/mod.rs rename to crates/lune-std-datetime/src/date_time.rs index 87a07db..fbf3910 100644 --- a/src/lune/builtins/datetime/mod.rs +++ b/crates/lune-std-datetime/src/date_time.rs @@ -6,31 +6,8 @@ use chrono::prelude::*; use chrono::DateTime as ChronoDateTime; use chrono_lc::LocaleDate; -use crate::lune::util::TableBuilder; - -mod error; -mod values; - -use error::*; -use values::*; - -pub fn create(lua: &Lua) -> LuaResult { - TableBuilder::new(lua)? - .with_function("fromIsoDate", |_, iso_date: String| { - Ok(DateTime::from_iso_date(iso_date)?) - })? - .with_function("fromLocalTime", |_, values| { - Ok(DateTime::from_local_time(&values)?) - })? - .with_function("fromUniversalTime", |_, values| { - Ok(DateTime::from_universal_time(&values)?) - })? - .with_function("fromUnixTimestamp", |_, timestamp| { - Ok(DateTime::from_unix_timestamp_float(timestamp)?) - })? - .with_function("now", |_, ()| Ok(DateTime::now()))? - .build_readonly() -} +use crate::result::{DateTimeError, DateTimeResult}; +use crate::values::DateTimeValues; const DEFAULT_FORMAT: &str = "%Y-%m-%d %H:%M:%S"; const DEFAULT_LOCALE: &str = "en"; @@ -49,6 +26,7 @@ impl DateTime { See [`chrono::DateTime::now`] for additional details. */ + #[must_use] pub fn now() -> Self { Self { inner: Utc::now() } } @@ -66,6 +44,10 @@ impl DateTime { ``` See [`chrono::DateTime::from_timestamp`] for additional details. + + # Errors + + Returns an error if the input value is out of range. */ pub fn from_unix_timestamp_float(unix_timestamp: f64) -> DateTimeResult { let whole = unix_timestamp.trunc() as i64; @@ -84,6 +66,10 @@ impl DateTime { See [`chrono::NaiveDate::from_ymd_opt`] and [`chrono::NaiveTime::from_hms_milli_opt`] for additional details and cases where this constructor may return an error. + + # Errors + + Returns an error if the date or time values are invalid. */ pub fn from_universal_time(values: &DateTimeValues) -> DateTimeResult { let date = NaiveDate::from_ymd_opt(values.year, values.month, values.day) @@ -108,6 +94,10 @@ impl DateTime { See [`chrono::NaiveDate::from_ymd_opt`] and [`chrono::NaiveTime::from_hms_milli_opt`] for additional details and cases where this constructor may return an error. + + # Errors + + Returns an error if the date or time values are invalid or ambiguous. */ pub fn from_local_time(values: &DateTimeValues) -> DateTimeResult { let date = NaiveDate::from_ymd_opt(values.year, values.month, values.day) @@ -138,6 +128,7 @@ impl DateTime { See [`chrono_lc::DateTime::formatl`] for additional details. */ + #[must_use] pub fn format_string_local(&self, format: Option<&str>, locale: Option<&str>) -> String { self.inner .with_timezone(&Local) @@ -156,6 +147,7 @@ impl DateTime { See [`chrono_lc::DateTime::formatl`] for additional details. */ + #[must_use] pub fn format_string_universal(&self, format: Option<&str>, locale: Option<&str>) -> String { self.inner .with_timezone(&Utc) @@ -171,6 +163,10 @@ impl DateTime { `1996-12-19T16:39:57-08:00`, into a new `DateTime` struct. See [`chrono::DateTime::parse_from_rfc3339`] for additional details. + + # Errors + + Returns an error if the input string is not a valid RFC 3339 date-time. */ pub fn from_iso_date(iso_date: impl AsRef) -> DateTimeResult { let inner = ChronoDateTime::parse_from_rfc3339(iso_date.as_ref())?.with_timezone(&Utc); @@ -181,6 +177,7 @@ impl DateTime { Extracts individual date & time values from this `DateTime`, using the current local time zone. */ + #[must_use] pub fn to_local_time(self) -> DateTimeValues { DateTimeValues::from(self.inner.with_timezone(&Local)) } @@ -189,6 +186,7 @@ impl DateTime { Extracts individual date & time values from this `DateTime`, using the universal (UTC) time zone. */ + #[must_use] pub fn to_universal_time(self) -> DateTimeValues { DateTimeValues::from(self.inner.with_timezone(&Utc)) } @@ -198,6 +196,7 @@ impl DateTime { See [`chrono::DateTime::to_rfc3339`] for additional details. */ + #[must_use] pub fn to_iso_date(self) -> String { self.inner.to_rfc3339() } diff --git a/crates/lune-std-datetime/src/lib.rs b/crates/lune-std-datetime/src/lib.rs new file mode 100644 index 0000000..f53ddf3 --- /dev/null +++ b/crates/lune-std-datetime/src/lib.rs @@ -0,0 +1,36 @@ +#![allow(clippy::cargo_common_metadata)] + +use mlua::prelude::*; + +use lune_utils::TableBuilder; + +mod date_time; +mod result; +mod values; + +pub use self::date_time::DateTime; + +/** + Creates the `datetime` standard library module. + + # Errors + + Errors when out of memory. +*/ +pub fn module(lua: &Lua) -> LuaResult { + TableBuilder::new(lua)? + .with_function("fromIsoDate", |_, iso_date: String| { + Ok(DateTime::from_iso_date(iso_date)?) + })? + .with_function("fromLocalTime", |_, values| { + Ok(DateTime::from_local_time(&values)?) + })? + .with_function("fromUniversalTime", |_, values| { + Ok(DateTime::from_universal_time(&values)?) + })? + .with_function("fromUnixTimestamp", |_, timestamp| { + Ok(DateTime::from_unix_timestamp_float(timestamp)?) + })? + .with_function("now", |_, ()| Ok(DateTime::now()))? + .build_readonly() +} diff --git a/src/lune/builtins/datetime/error.rs b/crates/lune-std-datetime/src/result.rs similarity index 100% rename from src/lune/builtins/datetime/error.rs rename to crates/lune-std-datetime/src/result.rs diff --git a/src/lune/builtins/datetime/values.rs b/crates/lune-std-datetime/src/values.rs similarity index 92% rename from src/lune/builtins/datetime/values.rs rename to crates/lune-std-datetime/src/values.rs index 7bca230..4193d63 100644 --- a/src/lune/builtins/datetime/values.rs +++ b/crates/lune-std-datetime/src/values.rs @@ -2,9 +2,9 @@ use mlua::prelude::*; use chrono::prelude::*; -use crate::lune::util::TableBuilder; +use lune_utils::TableBuilder; -use super::error::{DateTimeError, DateTimeResult}; +use super::result::{DateTimeError, DateTimeResult}; #[derive(Debug, Clone, Copy)] pub struct DateTimeValues { @@ -61,9 +61,9 @@ where } /** - Conversion methods between DateTimeValues and plain lua tables + Conversion methods between `DateTimeValues` and plain lua tables - Note that the IntoLua implementation here uses a read-only table, + Note that the `IntoLua` implementation here uses a read-only table, since we generally want to convert into lua when we know we have a fixed point in time, and we guarantee that it doesn't change */ @@ -118,8 +118,8 @@ impl IntoLua<'_> for DateTimeValues { } /** - Conversion methods between chrono's timezone-aware DateTime to - and from our non-timezone-aware DateTimeValues values struct + Conversion methods between chrono's timezone-aware `DateTime` to + and from our non-timezone-aware `DateTimeValues` values struct */ impl From> for DateTimeValues { diff --git a/crates/lune-std-fs/Cargo.toml b/crates/lune-std-fs/Cargo.toml new file mode 100644 index 0000000..a3bc219 --- /dev/null +++ b/crates/lune-std-fs/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "lune-std-fs" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[lib] +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +mlua = { version = "0.9.7", features = ["luau"] } + +bstr = "1.9" + +tokio = { version = "1", default-features = false, features = ["fs"] } + +lune-utils = { version = "0.1.0", path = "../lune-utils" } +lune-std-datetime = { version = "0.1.0", path = "../lune-std-datetime" } diff --git a/src/lune/builtins/fs/copy.rs b/crates/lune-std-fs/src/copy.rs similarity index 96% rename from src/lune/builtins/fs/copy.rs rename to crates/lune-std-fs/src/copy.rs index bcad01f..4fa3287 100644 --- a/src/lune/builtins/fs/copy.rs +++ b/crates/lune-std-fs/src/copy.rs @@ -13,7 +13,7 @@ pub struct CopyContents { pub files: Vec<(usize, PathBuf)>, } -async fn get_contents_at(root: PathBuf, _options: FsWriteOptions) -> LuaResult { +async fn get_contents_at(root: PathBuf, _: FsWriteOptions) -> LuaResult { let mut dirs = Vec::new(); let mut files = Vec::new(); @@ -53,11 +53,11 @@ async fn get_contents_at(root: PathBuf, _options: FsWriteOptions) -> LuaResult LuaResult { +/** + Creates the `fs` standard library module. + + # Errors + + Errors when out of memory. +*/ +pub fn module(lua: &Lua) -> LuaResult { TableBuilder::new(lua)? .with_async_function("readFile", fs_read_file)? .with_async_function("readDir", fs_read_dir)? diff --git a/src/lune/builtins/fs/metadata.rs b/crates/lune-std-fs/src/metadata.rs similarity index 98% rename from src/lune/builtins/fs/metadata.rs rename to crates/lune-std-fs/src/metadata.rs index 93bdbe6..2cf01c8 100644 --- a/src/lune/builtins/fs/metadata.rs +++ b/crates/lune-std-fs/src/metadata.rs @@ -8,7 +8,7 @@ use std::{ use mlua::prelude::*; -use crate::lune::builtins::datetime::DateTime; +use lune_std_datetime::DateTime; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FsMetadataKind { diff --git a/src/lune/builtins/fs/options.rs b/crates/lune-std-fs/src/options.rs similarity index 100% rename from src/lune/builtins/fs/options.rs rename to crates/lune-std-fs/src/options.rs diff --git a/crates/lune-std-luau/Cargo.toml b/crates/lune-std-luau/Cargo.toml new file mode 100644 index 0000000..d01584c --- /dev/null +++ b/crates/lune-std-luau/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "lune-std-luau" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[lib] +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +mlua = { version = "0.9.7", features = ["luau"] } + +lune-utils = { version = "0.1.0", path = "../lune-utils" } diff --git a/src/lune/builtins/luau/mod.rs b/crates/lune-std-luau/src/lib.rs similarity index 85% rename from src/lune/builtins/luau/mod.rs rename to crates/lune-std-luau/src/lib.rs index 89ec972..e41eed5 100644 --- a/src/lune/builtins/luau/mod.rs +++ b/crates/lune-std-luau/src/lib.rs @@ -1,13 +1,23 @@ +#![allow(clippy::cargo_common_metadata)] + use mlua::prelude::*; -use crate::lune::util::TableBuilder; +use lune_utils::TableBuilder; mod options; -use options::{LuauCompileOptions, LuauLoadOptions}; + +use self::options::{LuauCompileOptions, LuauLoadOptions}; const BYTECODE_ERROR_BYTE: u8 = 0; -pub fn create(lua: &Lua) -> LuaResult { +/** + Creates the `luau` standard library module. + + # Errors + + Errors when out of memory. +*/ +pub fn module(lua: &Lua) -> LuaResult { TableBuilder::new(lua)? .with_function("compile", compile_source)? .with_function("load", load_source)? diff --git a/src/lune/builtins/luau/options.rs b/crates/lune-std-luau/src/options.rs similarity index 95% rename from src/lune/builtins/luau/options.rs rename to crates/lune-std-luau/src/options.rs index b34df9d..a2040ec 100644 --- a/src/lune/builtins/luau/options.rs +++ b/crates/lune-std-luau/src/options.rs @@ -1,8 +1,14 @@ +#![allow(clippy::struct_field_names)] + use mlua::prelude::*; use mlua::Compiler as LuaCompiler; const DEFAULT_DEBUG_NAME: &str = "luau.load(...)"; +/** + Options for compiling Lua source code. +*/ +#[derive(Debug, Clone, Copy)] pub struct LuauCompileOptions { pub(crate) optimization_level: u8, pub(crate) coverage_level: u8, @@ -73,6 +79,10 @@ impl<'lua> FromLua<'lua> for LuauCompileOptions { } } +/** + Options for loading Lua source code. +*/ +#[derive(Debug, Clone)] pub struct LuauLoadOptions<'lua> { pub(crate) debug_name: String, pub(crate) environment: Option>, diff --git a/crates/lune-std-net/Cargo.toml b/crates/lune-std-net/Cargo.toml new file mode 100644 index 0000000..966e374 --- /dev/null +++ b/crates/lune-std-net/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "lune-std-net" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[lib] +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +mlua = { version = "0.9.7", features = ["luau"] } +mlua-luau-scheduler = "0.0.2" + +bstr = "1.9" +futures-util = "0.3" +hyper = { version = "1.1", features = ["full"] } +hyper-util = { version = "0.1", features = ["full"] } +http = "1.0" +http-body-util = { version = "0.1" } +hyper-tungstenite = { version = "0.13" } +reqwest = { version = "0.11", default-features = false, features = [ + "rustls-tls", +] } +tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] } +urlencoding = "2.1" + +tokio = { version = "1", default-features = false, features = [ + "sync", + "net", + "macros", +] } + +lune-utils = { version = "0.1.0", path = "../lune-utils" } +lune-std-serde = { version = "0.1.0", path = "../lune-std-serde" } diff --git a/src/lune/builtins/net/client.rs b/crates/lune-std-net/src/client.rs similarity index 96% rename from src/lune/builtins/net/client.rs rename to crates/lune-std-net/src/client.rs index 5eb2527..cae56bf 100644 --- a/src/lune/builtins/net/client.rs +++ b/crates/lune-std-net/src/client.rs @@ -4,10 +4,8 @@ use mlua::prelude::*; use reqwest::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_ENCODING}; -use crate::lune::{ - builtins::serde::compress_decompress::{decompress, CompressDecompressFormat}, - util::TableBuilder, -}; +use lune_std_serde::{decompress, CompressDecompressFormat}; +use lune_utils::TableBuilder; use super::{config::RequestConfig, util::header_map_to_table}; @@ -103,7 +101,7 @@ impl NetClient { .and_then(|(_, value)| value.to_str().ok()) .and_then(CompressDecompressFormat::detect_from_header_str); if let Some(format) = decompress_format { - res_bytes = decompress(format, res_bytes).await?; + res_bytes = decompress(res_bytes, format).await?; res_decompressed = true; } } diff --git a/src/lune/builtins/net/config.rs b/crates/lune-std-net/src/config.rs similarity index 99% rename from src/lune/builtins/net/config.rs rename to crates/lune-std-net/src/config.rs index 1abd121..6368d6d 100644 --- a/src/lune/builtins/net/config.rs +++ b/crates/lune-std-net/src/config.rs @@ -83,7 +83,7 @@ impl FromLua<'_> for RequestConfig { query: HashMap::new(), headers: HashMap::new(), body: None, - options: Default::default(), + options: RequestConfigOptions::default(), }) } else if let LuaValue::Table(tab) = value { // If we got a table we are able to configure the entire request diff --git a/src/lune/builtins/net/mod.rs b/crates/lune-std-net/src/lib.rs similarity index 83% rename from src/lune/builtins/net/mod.rs rename to crates/lune-std-net/src/lib.rs index 9449e6b..3f42889 100644 --- a/src/lune/builtins/net/mod.rs +++ b/crates/lune-std-net/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(unused_variables)] +#![allow(clippy::cargo_common_metadata)] use bstr::BString; use mlua::prelude::*; @@ -10,7 +10,7 @@ mod server; mod util; mod websocket; -use crate::lune::util::TableBuilder; +use lune_utils::TableBuilder; use self::{ client::{NetClient, NetClientBuilder}, @@ -20,9 +20,16 @@ use self::{ websocket::NetWebSocket, }; -use super::serde::encode_decode::{EncodeDecodeConfig, EncodeDecodeFormat}; +use lune_std_serde::{decode, encode, EncodeDecodeConfig, EncodeDecodeFormat}; -pub fn create(lua: &Lua) -> LuaResult { +/** + Creates the `net` standard library module. + + # Errors + + Errors when out of memory. +*/ +pub fn module(lua: &Lua) -> LuaResult { NetClientBuilder::new() .headers(&[("User-Agent", create_user_agent_header(lua)?)])? .build()? @@ -42,12 +49,13 @@ fn net_json_encode<'lua>( lua: &'lua Lua, (val, pretty): (LuaValue<'lua>, Option), ) -> LuaResult> { - EncodeDecodeConfig::from((EncodeDecodeFormat::Json, pretty.unwrap_or_default())) - .serialize_to_string(lua, val) + let config = EncodeDecodeConfig::from((EncodeDecodeFormat::Json, pretty.unwrap_or_default())); + encode(val, lua, config) } fn net_json_decode(lua: &Lua, json: BString) -> LuaResult { - EncodeDecodeConfig::from(EncodeDecodeFormat::Json).deserialize_from_string(lua, json) + let config = EncodeDecodeConfig::from(EncodeDecodeFormat::Json); + decode(json, lua, config) } async fn net_request(lua: &Lua, config: RequestConfig) -> LuaResult { diff --git a/src/lune/builtins/net/server/keys.rs b/crates/lune-std-net/src/server/keys.rs similarity index 100% rename from src/lune/builtins/net/server/keys.rs rename to crates/lune-std-net/src/server/keys.rs diff --git a/src/lune/builtins/net/server/mod.rs b/crates/lune-std-net/src/server/mod.rs similarity index 94% rename from src/lune/builtins/net/server/mod.rs rename to crates/lune-std-net/src/server/mod.rs index 5639bcb..7cfab9d 100644 --- a/src/lune/builtins/net/server/mod.rs +++ b/crates/lune-std-net/src/server/mod.rs @@ -10,7 +10,7 @@ use tokio::{net::TcpListener, pin}; use mlua::prelude::*; use mlua_luau_scheduler::LuaSpawnExt; -use crate::lune::util::TableBuilder; +use lune_utils::TableBuilder; use super::config::ServeConfig; @@ -81,7 +81,7 @@ pub async fn serve<'lua>( // Wait for either a new connection or a shutdown signal tokio::select! { - _ = fut_accept => {} + () = fut_accept => {} res = fut_shutdown => { // NOTE: We will only get a RecvError here if the serve handle is dropped, // this means lua has garbage collected it and the user does not want @@ -97,8 +97,8 @@ pub async fn serve<'lua>( TableBuilder::new(lua)? .with_value("ip", addr.ip().to_string())? .with_value("port", addr.port())? - .with_function("stop", move |lua, _: ()| match shutdown_tx.send(true) { - Ok(_) => Ok(()), + .with_function("stop", move |_, (): ()| match shutdown_tx.send(true) { + Ok(()) => Ok(()), Err(_) => Err(LuaError::runtime("Server already stopped")), })? .build_readonly() diff --git a/src/lune/builtins/net/server/request.rs b/crates/lune-std-net/src/server/request.rs similarity index 97% rename from src/lune/builtins/net/server/request.rs rename to crates/lune-std-net/src/server/request.rs index bab7a5d..f3de802 100644 --- a/src/lune/builtins/net/server/request.rs +++ b/crates/lune-std-net/src/server/request.rs @@ -4,7 +4,7 @@ use http::request::Parts; use mlua::prelude::*; -use crate::lune::util::TableBuilder; +use lune_utils::TableBuilder; pub(super) struct LuaRequest { pub(super) _remote_addr: SocketAddr, diff --git a/src/lune/builtins/net/server/response.rs b/crates/lune-std-net/src/server/response.rs similarity index 100% rename from src/lune/builtins/net/server/response.rs rename to crates/lune-std-net/src/server/response.rs diff --git a/src/lune/builtins/net/server/service.rs b/crates/lune-std-net/src/server/service.rs similarity index 100% rename from src/lune/builtins/net/server/service.rs rename to crates/lune-std-net/src/server/service.rs diff --git a/src/lune/builtins/net/util.rs b/crates/lune-std-net/src/util.rs similarity index 97% rename from src/lune/builtins/net/util.rs rename to crates/lune-std-net/src/util.rs index e18235e..ca79967 100644 --- a/src/lune/builtins/net/util.rs +++ b/crates/lune-std-net/src/util.rs @@ -5,7 +5,7 @@ use reqwest::header::HeaderMap; use mlua::prelude::*; -use crate::lune::util::TableBuilder; +use lune_utils::TableBuilder; pub fn create_user_agent_header(lua: &Lua) -> LuaResult { let version_global = lua @@ -28,7 +28,7 @@ pub fn header_map_to_table( remove_content_headers: bool, ) -> LuaResult { let mut res_headers: HashMap> = HashMap::new(); - for (name, value) in headers.iter() { + for (name, value) in &headers { let name = name.as_str(); let value = value.to_str().unwrap().to_owned(); if let Some(existing) = res_headers.get_mut(name) { diff --git a/src/lune/builtins/net/websocket.rs b/crates/lune-std-net/src/websocket.rs similarity index 96% rename from src/lune/builtins/net/websocket.rs rename to crates/lune-std-net/src/websocket.rs index 5dba4ec..ae2208a 100644 --- a/src/lune/builtins/net/websocket.rs +++ b/crates/lune-std-net/src/websocket.rs @@ -23,7 +23,7 @@ use hyper_tungstenite::{ WebSocketStream, }; -use crate::lune::util::TableBuilder; +use lune_utils::TableBuilder; // Wrapper implementation for compatibility and changing colon syntax to dot syntax const WEB_SOCKET_IMPL_LUA: &str = r#" @@ -155,7 +155,7 @@ where } fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_async_method("close", |lua, this, code: Option| async move { + methods.add_async_method("close", |_, this, code: Option| async move { this.close(code).await }); @@ -172,7 +172,7 @@ where }, ); - methods.add_async_method("next", |lua, this, _: ()| async move { + methods.add_async_method("next", |lua, this, (): ()| async move { let msg = this.next().await?; if let Some(WsMessage::Close(Some(frame))) = msg.as_ref() { diff --git a/crates/lune-std-process/Cargo.toml b/crates/lune-std-process/Cargo.toml new file mode 100644 index 0000000..211d875 --- /dev/null +++ b/crates/lune-std-process/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "lune-std-process" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[lib] +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +mlua = { version = "0.9.7", features = ["luau"] } +mlua-luau-scheduler = "0.0.2" + +directories = "5.0" +pin-project = "1.0" +os_str_bytes = { version = "7.0", features = ["conversions"] } + +tokio = { version = "1", default-features = false, features = [ + "sync", + "process", +] } + +lune-utils = { version = "0.1.0", path = "../lune-utils" } diff --git a/src/lune/builtins/process/mod.rs b/crates/lune-std-process/src/lib.rs similarity index 75% rename from src/lune/builtins/process/mod.rs rename to crates/lune-std-process/src/lib.rs index b64e9aa..adc4eb8 100644 --- a/src/lune/builtins/process/mod.rs +++ b/crates/lune-std-process/src/lib.rs @@ -1,36 +1,49 @@ +#![allow(clippy::cargo_common_metadata)] + use std::{ - env::{self, consts}, - path, + env::{ + self, + consts::{ARCH, OS}, + }, + path::MAIN_SEPARATOR, process::Stdio, }; use mlua::prelude::*; + +use lune_utils::TableBuilder; use mlua_luau_scheduler::{Functions, LuaSpawnExt}; use os_str_bytes::RawOsString; use tokio::io::AsyncWriteExt; -use crate::lune::util::{paths::CWD, TableBuilder}; - -mod tee_writer; - mod options; -use options::ProcessSpawnOptions; - +mod tee_writer; mod wait_for_child; -use wait_for_child::{wait_for_child, WaitForChildResult}; -pub fn create(lua: &Lua) -> LuaResult { - let cwd_str = { - let cwd_str = CWD.to_string_lossy().to_string(); - if !cwd_str.ends_with(path::MAIN_SEPARATOR) { - format!("{cwd_str}{}", path::MAIN_SEPARATOR) - } else { - cwd_str - } - }; +use self::options::ProcessSpawnOptions; +use self::wait_for_child::{wait_for_child, WaitForChildResult}; + +use lune_utils::path::get_current_dir; + +/** + Creates the `process` standard library module. + + # Errors + + Errors when out of memory. +*/ +#[allow(clippy::missing_panics_doc)] +pub fn module(lua: &Lua) -> LuaResult { + let mut cwd_str = get_current_dir() + .to_str() + .expect("cwd should be valid UTF-8") + .to_string(); + if !cwd_str.ends_with(MAIN_SEPARATOR) { + cwd_str.push(MAIN_SEPARATOR); + } // Create constants for OS & processor architecture - let os = lua.create_string(&consts::OS.to_lowercase())?; - let arch = lua.create_string(&consts::ARCH.to_lowercase())?; + let os = lua.create_string(&OS.to_lowercase())?; + let arch = lua.create_string(&ARCH.to_lowercase())?; // Create readonly args array let args_vec = lua .app_data_ref::>() @@ -94,33 +107,28 @@ fn process_env_set<'lua>( Err(LuaError::RuntimeError( "Key must not contain the NUL character".to_string(), )) - } else { - match value { - Some(value) => { - // Make sure value is valid, otherwise set_var will panic - if value.contains('\0') { - Err(LuaError::RuntimeError( - "Value must not contain the NUL character".to_string(), - )) - } else { - env::set_var(&key, &value); - Ok(()) - } - } - None => { - env::remove_var(&key); - Ok(()) - } + } else if let Some(value) = value { + // Make sure value is valid, otherwise set_var will panic + if value.contains('\0') { + Err(LuaError::RuntimeError( + "Value must not contain the NUL character".to_string(), + )) + } else { + env::set_var(&key, &value); + Ok(()) } + } else { + env::remove_var(&key); + Ok(()) } } fn process_env_iter<'lua>( lua: &'lua Lua, - (_, _): (LuaValue<'lua>, ()), + (_, ()): (LuaValue<'lua>, ()), ) -> LuaResult> { let mut vars = env::vars_os().collect::>().into_iter(); - lua.create_function_mut(move |lua, _: ()| match vars.next() { + lua.create_function_mut(move |lua, (): ()| match vars.next() { Some((key, value)) => { let raw_key = RawOsString::new(key); let raw_value = RawOsString::new(value); @@ -149,10 +157,10 @@ async fn process_spawn( An exit code may be missing if the process was terminated by some external signal, which is the only time we use this default */ - let code = res.status.code().unwrap_or(match res.stderr.is_empty() { - true => 0, - false => 1, - }); + let code = res + .status + .code() + .unwrap_or(i32::from(!res.stderr.is_empty())); // Construct and return a readonly lua table with results TableBuilder::new(lua)? @@ -174,9 +182,10 @@ async fn spawn_command( let mut child = options .into_command(program, args) - .stdin(match stdin.is_some() { - true => Stdio::piped(), - false => Stdio::null(), + .stdin(if stdin.is_some() { + Stdio::piped() + } else { + Stdio::null() }) .stdout(stdout.as_stdio()) .stderr(stderr.as_stdio()) diff --git a/src/lune/builtins/process/options/kind.rs b/crates/lune-std-process/src/options/kind.rs similarity index 98% rename from src/lune/builtins/process/options/kind.rs rename to crates/lune-std-process/src/options/kind.rs index 3e0f39c..8eff17d 100644 --- a/src/lune/builtins/process/options/kind.rs +++ b/crates/lune-std-process/src/options/kind.rs @@ -1,6 +1,5 @@ use std::{fmt, process::Stdio, str::FromStr}; -use itertools::Itertools; use mlua::prelude::*; #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] @@ -55,6 +54,7 @@ impl FromStr for ProcessSpawnOptionsStdioKind { ProcessSpawnOptionsStdioKind::all() .iter() .map(|k| format!("'{k}'")) + .collect::>() .join(", ") ))) } diff --git a/src/lune/builtins/process/options/mod.rs b/crates/lune-std-process/src/options/mod.rs similarity index 98% rename from src/lune/builtins/process/options/mod.rs rename to crates/lune-std-process/src/options/mod.rs index d37f844..8ef8be0 100644 --- a/src/lune/builtins/process/options/mod.rs +++ b/crates/lune-std-process/src/options/mod.rs @@ -56,7 +56,7 @@ impl<'lua> FromLua<'lua> for ProcessSpawnOptions { "Invalid value for option 'cwd' - failed to get home directory", ) })?; - cwd = user_dirs.home_dir().join(stripped) + cwd = user_dirs.home_dir().join(stripped); } if !cwd.exists() { return Err(LuaError::runtime( diff --git a/src/lune/builtins/process/options/stdio.rs b/crates/lune-std-process/src/options/stdio.rs similarity index 100% rename from src/lune/builtins/process/options/stdio.rs rename to crates/lune-std-process/src/options/stdio.rs diff --git a/src/lune/builtins/process/tee_writer.rs b/crates/lune-std-process/src/tee_writer.rs similarity index 100% rename from src/lune/builtins/process/tee_writer.rs rename to crates/lune-std-process/src/tee_writer.rs diff --git a/src/lune/builtins/process/wait_for_child.rs b/crates/lune-std-process/src/wait_for_child.rs similarity index 94% rename from src/lune/builtins/process/wait_for_child.rs rename to crates/lune-std-process/src/wait_for_child.rs index f126efe..4343041 100644 --- a/src/lune/builtins/process/wait_for_child.rs +++ b/crates/lune-std-process/src/wait_for_child.rs @@ -24,8 +24,7 @@ where R: AsyncRead + Unpin, { Ok(match kind { - ProcessSpawnOptionsStdioKind::None => Vec::new(), - ProcessSpawnOptionsStdioKind::Forward => Vec::new(), + ProcessSpawnOptionsStdioKind::None | ProcessSpawnOptionsStdioKind::Forward => Vec::new(), ProcessSpawnOptionsStdioKind::Default => { let mut read_from = read_from.expect("read_from must be Some when stdio kind is Default"); diff --git a/crates/lune-std-regex/Cargo.toml b/crates/lune-std-regex/Cargo.toml new file mode 100644 index 0000000..a7dc859 --- /dev/null +++ b/crates/lune-std-regex/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "lune-std-regex" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[lib] +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +mlua = { version = "0.9.7", features = ["luau"] } + +regex = "1.10" +self_cell = "1.0" + +lune-utils = { version = "0.1.0", path = "../lune-utils" } diff --git a/src/lune/builtins/regex/captures.rs b/crates/lune-std-regex/src/captures.rs similarity index 100% rename from src/lune/builtins/regex/captures.rs rename to crates/lune-std-regex/src/captures.rs diff --git a/src/lune/builtins/regex/mod.rs b/crates/lune-std-regex/src/lib.rs similarity index 56% rename from src/lune/builtins/regex/mod.rs rename to crates/lune-std-regex/src/lib.rs index bb674c2..97fb279 100644 --- a/src/lune/builtins/regex/mod.rs +++ b/crates/lune-std-regex/src/lib.rs @@ -1,8 +1,8 @@ -#![allow(clippy::module_inception)] +#![allow(clippy::cargo_common_metadata)] use mlua::prelude::*; -use crate::lune::util::TableBuilder; +use lune_utils::TableBuilder; mod captures; mod matches; @@ -10,7 +10,14 @@ mod regex; use self::regex::LuaRegex; -pub fn create(lua: &Lua) -> LuaResult { +/** + Creates the `regex` standard library module. + + # Errors + + Errors when out of memory. +*/ +pub fn module(lua: &Lua) -> LuaResult { TableBuilder::new(lua)? .with_function("new", new_regex)? .build_readonly() diff --git a/src/lune/builtins/regex/matches.rs b/crates/lune-std-regex/src/matches.rs similarity index 100% rename from src/lune/builtins/regex/matches.rs rename to crates/lune-std-regex/src/matches.rs diff --git a/src/lune/builtins/regex/regex.rs b/crates/lune-std-regex/src/regex.rs similarity index 98% rename from src/lune/builtins/regex/regex.rs rename to crates/lune-std-regex/src/regex.rs index 3325e5d..9b83544 100644 --- a/src/lune/builtins/regex/regex.rs +++ b/crates/lune-std-regex/src/regex.rs @@ -46,7 +46,7 @@ impl LuaUserData for LuaRegex { Ok(this .inner .split(&text) - .map(|s| s.to_string()) + .map(ToString::to_string) .collect::>()) }); diff --git a/crates/lune-std-roblox/Cargo.toml b/crates/lune-std-roblox/Cargo.toml new file mode 100644 index 0000000..af051c7 --- /dev/null +++ b/crates/lune-std-roblox/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "lune-std-roblox" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[lib] +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +mlua = { version = "0.9.7", features = ["luau"] } +mlua-luau-scheduler = "0.0.2" + +once_cell = "1.17" +rbx_cookie = { version = "0.1.4", default-features = false } + +lune-utils = { version = "0.1.0", path = "../lune-utils" } +lune-roblox = { version = "0.1.0", path = "../lune-roblox" } diff --git a/src/lune/builtins/roblox/mod.rs b/crates/lune-std-roblox/src/lib.rs similarity index 83% rename from src/lune/builtins/roblox/mod.rs rename to crates/lune-std-roblox/src/lib.rs index 308c148..d1f6a97 100644 --- a/src/lune/builtins/roblox/mod.rs +++ b/crates/lune-std-roblox/src/lib.rs @@ -1,23 +1,30 @@ +#![allow(clippy::cargo_common_metadata)] + use mlua::prelude::*; use mlua_luau_scheduler::LuaSpawnExt; use once_cell::sync::OnceCell; -use crate::{ - lune::util::TableBuilder, - roblox::{ - self, - document::{Document, DocumentError, DocumentFormat, DocumentKind}, - instance::{registry::InstanceRegistry, Instance}, - reflection::Database as ReflectionDatabase, - }, +use lune_roblox::{ + document::{Document, DocumentError, DocumentFormat, DocumentKind}, + instance::{registry::InstanceRegistry, Instance}, + reflection::Database as ReflectionDatabase, }; static REFLECTION_DATABASE: OnceCell = OnceCell::new(); -pub fn create(lua: &Lua) -> LuaResult { +use lune_utils::TableBuilder; + +/** + Creates the `roblox` standard library module. + + # Errors + + Errors when out of memory. +*/ +pub fn module(lua: &Lua) -> LuaResult { let mut roblox_constants = Vec::new(); - let roblox_module = roblox::module(lua)?; + let roblox_module = lune_roblox::module(lua)?; for pair in roblox_module.pairs::() { roblox_constants.push(pair?); } @@ -116,16 +123,15 @@ fn implement_property( Option, ), ) -> LuaResult<()> { - let property_setter = match property_setter { - Some(setter) => setter, - None => { - let property_name = property_name.clone(); - lua.create_function(move |_, _: LuaMultiValue| { - Err::<(), _>(LuaError::runtime(format!( - "Property '{property_name}' is read-only" - ))) - })? - } + let property_setter = if let Some(setter) = property_setter { + setter + } else { + let property_name = property_name.clone(); + lua.create_function(move |_, _: LuaMultiValue| { + Err::<(), _>(LuaError::runtime(format!( + "Property '{property_name}' is read-only" + ))) + })? }; InstanceRegistry::insert_property_getter(lua, &class_name, &property_name, property_getter) .into_lua_err()?; diff --git a/crates/lune-std-serde/Cargo.toml b/crates/lune-std-serde/Cargo.toml new file mode 100644 index 0000000..4f8faef --- /dev/null +++ b/crates/lune-std-serde/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "lune-std-serde" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[lib] +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +mlua = { version = "0.9.7", features = ["luau"] } + +async-compression = { version = "0.4", features = [ + "tokio", + "brotli", + "deflate", + "gzip", + "zlib", +] } +bstr = "1.9" +lz4 = "1.24" +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0", features = ["preserve_order"] } +serde_yaml = "0.9" +toml = { version = "0.8", features = ["preserve_order"] } + +tokio = { version = "1", default-features = false, features = [ + "rt", + "io-util", +] } + +lune-utils = { version = "0.1.0", path = "../lune-utils" } diff --git a/src/lune/builtins/serde/compress_decompress.rs b/crates/lune-std-serde/src/compress_decompress.rs similarity index 66% rename from src/lune/builtins/serde/compress_decompress.rs rename to crates/lune-std-serde/src/compress_decompress.rs index dac6ceb..3e5aaa0 100644 --- a/src/lune/builtins/serde/compress_decompress.rs +++ b/crates/lune-std-serde/src/compress_decompress.rs @@ -1,7 +1,12 @@ +use std::io::{copy as copy_std, Cursor, Read as _, Write as _}; + use mlua::prelude::*; -use lz4_flex::{compress_prepend_size, decompress_size_prepended}; -use tokio::io::{copy, BufReader}; +use lz4::{Decoder, EncoderBuilder}; +use tokio::{ + io::{copy, BufReader}, + task::spawn_blocking, +}; use async_compression::{ tokio::bufread::{ @@ -10,6 +15,9 @@ use async_compression::{ Level::Best as CompressionQuality, }; +/** + A compression and decompression format supported by Lune. +*/ #[derive(Debug, Clone, Copy)] pub enum CompressDecompressFormat { Brotli, @@ -20,6 +28,10 @@ pub enum CompressDecompressFormat { #[allow(dead_code)] impl CompressDecompressFormat { + /** + Detects a supported compression format from the given bytes. + */ + #[allow(clippy::missing_panics_doc)] pub fn detect_from_bytes(bytes: impl AsRef<[u8]>) -> Option { match bytes.as_ref() { // https://github.com/PSeitz/lz4_flex/blob/main/src/frame/header.rs#L28 @@ -55,6 +67,11 @@ impl CompressDecompressFormat { } } + /** + Detects a supported compression format from the given header string. + + The given header script should be a valid `Content-Encoding` header value. + */ pub fn detect_from_header_str(header: impl AsRef) -> Option { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding#directives match header.as_ref().to_ascii_lowercase().trim() { @@ -92,13 +109,23 @@ impl<'lua> FromLua<'lua> for CompressDecompressFormat { } } +/** + Compresses the given bytes using the specified format. + + # Errors + + Errors when the compression fails. +*/ pub async fn compress<'lua>( - format: CompressDecompressFormat, source: impl AsRef<[u8]>, + format: CompressDecompressFormat, ) -> LuaResult> { if let CompressDecompressFormat::LZ4 = format { let source = source.as_ref().to_vec(); - return Ok(blocking::unblock(move || compress_prepend_size(&source)).await); + return spawn_blocking(move || compress_lz4(source)) + .await + .into_lua_err()? + .into_lua_err(); } let mut bytes = Vec::new(); @@ -123,14 +150,22 @@ pub async fn compress<'lua>( Ok(bytes) } +/** + Decompresses the given bytes using the specified format. + + # Errors + + Errors when the decompression fails. +*/ pub async fn decompress<'lua>( - format: CompressDecompressFormat, source: impl AsRef<[u8]>, + format: CompressDecompressFormat, ) -> LuaResult> { if let CompressDecompressFormat::LZ4 = format { let source = source.as_ref().to_vec(); - return blocking::unblock(move || decompress_size_prepended(&source)) + return spawn_blocking(move || decompress_lz4(source)) .await + .into_lua_err()? .into_lua_err(); } @@ -155,3 +190,47 @@ pub async fn decompress<'lua>( Ok(bytes) } + +// TODO: Remove the compatibility layer. Prepending size is no longer +// necessary, using lz4 create instead of lz4-flex, but we must remove +// it in a major version to not unexpectedly break compatibility + +fn compress_lz4(input: Vec) -> LuaResult> { + let mut input = Cursor::new(input); + let mut output = Cursor::new(Vec::new()); + + // Prepend size for compatibility with old lz4-flex implementation + let len = input.get_ref().len() as u32; + output.write_all(len.to_le_bytes().as_ref())?; + + let mut encoder = EncoderBuilder::new() + .level(16) + .checksum(lz4::ContentChecksum::ChecksumEnabled) + .block_mode(lz4::BlockMode::Independent) + .build(output)?; + + copy_std(&mut input, &mut encoder)?; + let (output, result) = encoder.finish(); + result?; + + Ok(output.into_inner()) +} + +fn decompress_lz4(input: Vec) -> LuaResult> { + let mut input = Cursor::new(input); + + // Skip size for compatibility with old lz4-flex implementation + // Note that right now we use it for preallocating the output buffer + // and a small efficiency gain, maybe we can expose this as some kind + // of "size hint" parameter instead in the serde library in the future + let mut size = [0; 4]; + input.read_exact(&mut size)?; + + let capacity = u32::from_le_bytes(size) as usize; + let mut output = Cursor::new(Vec::with_capacity(capacity)); + + let mut decoder = Decoder::new(input)?; + copy_std(&mut decoder, &mut output)?; + + Ok(output.into_inner()) +} diff --git a/crates/lune-std-serde/src/encode_decode.rs b/crates/lune-std-serde/src/encode_decode.rs new file mode 100644 index 0000000..80e1a5f --- /dev/null +++ b/crates/lune-std-serde/src/encode_decode.rs @@ -0,0 +1,158 @@ +use mlua::prelude::*; + +use serde_json::Value as JsonValue; +use serde_yaml::Value as YamlValue; +use toml::Value as TomlValue; + +// NOTE: These are options for going from other format -> lua ("serializing" lua values) +const LUA_SERIALIZE_OPTIONS: LuaSerializeOptions = LuaSerializeOptions::new() + .set_array_metatable(false) + .serialize_none_to_null(false) + .serialize_unit_to_null(false); + +// NOTE: These are options for going from lua -> other format ("deserializing" lua values) +const LUA_DESERIALIZE_OPTIONS: LuaDeserializeOptions = LuaDeserializeOptions::new() + .sort_keys(true) + .deny_recursive_tables(false) + .deny_unsupported_types(true); + +/** + An encoding and decoding format supported by Lune. + + Encode / decode in this case is synonymous with serialize / deserialize. +*/ +#[derive(Debug, Clone, Copy)] +pub enum EncodeDecodeFormat { + Json, + Yaml, + Toml, +} + +impl<'lua> FromLua<'lua> for EncodeDecodeFormat { + fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { + if let LuaValue::String(s) = &value { + match s.to_string_lossy().to_ascii_lowercase().trim() { + "json" => Ok(Self::Json), + "yaml" => Ok(Self::Yaml), + "toml" => Ok(Self::Toml), + kind => Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "EncodeDecodeFormat", + message: Some(format!( + "Invalid format '{kind}', valid formats are: json, yaml, toml" + )), + }), + } + } else { + Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "EncodeDecodeFormat", + message: None, + }) + } + } +} + +/** + Configuration for encoding and decoding values. + + Encoding / decoding in this case is synonymous with serialize / deserialize. +*/ +#[derive(Debug, Clone, Copy)] +pub struct EncodeDecodeConfig { + pub format: EncodeDecodeFormat, + pub pretty: bool, +} + +impl From for EncodeDecodeConfig { + fn from(format: EncodeDecodeFormat) -> Self { + Self { + format, + pretty: false, + } + } +} + +impl From<(EncodeDecodeFormat, bool)> for EncodeDecodeConfig { + fn from(value: (EncodeDecodeFormat, bool)) -> Self { + Self { + format: value.0, + pretty: value.1, + } + } +} + +/** + Encodes / serializes the given value into a string, using the specified configuration. + + # Errors + + Errors when the encoding fails. +*/ +pub fn encode<'lua>( + value: LuaValue<'lua>, + lua: &'lua Lua, + config: EncodeDecodeConfig, +) -> LuaResult> { + let bytes = match config.format { + EncodeDecodeFormat::Json => { + let serialized: JsonValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?; + if config.pretty { + serde_json::to_vec_pretty(&serialized).into_lua_err()? + } else { + serde_json::to_vec(&serialized).into_lua_err()? + } + } + EncodeDecodeFormat::Yaml => { + let serialized: YamlValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?; + let mut writer = Vec::with_capacity(128); + serde_yaml::to_writer(&mut writer, &serialized).into_lua_err()?; + writer + } + EncodeDecodeFormat::Toml => { + let serialized: TomlValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?; + let s = if config.pretty { + toml::to_string_pretty(&serialized).into_lua_err()? + } else { + toml::to_string(&serialized).into_lua_err()? + }; + s.as_bytes().to_vec() + } + }; + lua.create_string(bytes) +} + +/** + Decodes / deserializes the given string into a value, using the specified configuration. + + # Errors + + Errors when the decoding fails. +*/ +pub fn decode( + bytes: impl AsRef<[u8]>, + lua: &Lua, + config: EncodeDecodeConfig, +) -> LuaResult { + let bytes = bytes.as_ref(); + match config.format { + EncodeDecodeFormat::Json => { + let value: JsonValue = serde_json::from_slice(bytes).into_lua_err()?; + lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS) + } + EncodeDecodeFormat::Yaml => { + let value: YamlValue = serde_yaml::from_slice(bytes).into_lua_err()?; + lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS) + } + EncodeDecodeFormat::Toml => { + if let Ok(s) = String::from_utf8(bytes.to_vec()) { + let value: TomlValue = toml::from_str(&s).into_lua_err()?; + lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS) + } else { + Err(LuaError::RuntimeError( + "TOML must be valid utf-8".to_string(), + )) + } + } + } +} diff --git a/crates/lune-std-serde/src/lib.rs b/crates/lune-std-serde/src/lib.rs new file mode 100644 index 0000000..4514a75 --- /dev/null +++ b/crates/lune-std-serde/src/lib.rs @@ -0,0 +1,57 @@ +#![allow(clippy::cargo_common_metadata)] + +use bstr::BString; +use mlua::prelude::*; + +use lune_utils::TableBuilder; + +mod compress_decompress; +mod encode_decode; + +pub use self::compress_decompress::{compress, decompress, CompressDecompressFormat}; +pub use self::encode_decode::{decode, encode, EncodeDecodeConfig, EncodeDecodeFormat}; + +/** + Creates the `serde` standard library module. + + # Errors + + Errors when out of memory. +*/ +pub fn module(lua: &Lua) -> LuaResult { + TableBuilder::new(lua)? + .with_function("encode", serde_encode)? + .with_function("decode", serde_decode)? + .with_async_function("compress", serde_compress)? + .with_async_function("decompress", serde_decompress)? + .build_readonly() +} + +fn serde_encode<'lua>( + lua: &'lua Lua, + (format, value, pretty): (EncodeDecodeFormat, LuaValue<'lua>, Option), +) -> LuaResult> { + let config = EncodeDecodeConfig::from((format, pretty.unwrap_or_default())); + encode(value, lua, config) +} + +fn serde_decode(lua: &Lua, (format, bs): (EncodeDecodeFormat, BString)) -> LuaResult { + let config = EncodeDecodeConfig::from(format); + decode(bs, lua, config) +} + +async fn serde_compress( + lua: &Lua, + (format, bs): (CompressDecompressFormat, BString), +) -> LuaResult { + let bytes = compress(bs, format).await?; + lua.create_string(bytes) +} + +async fn serde_decompress( + lua: &Lua, + (format, bs): (CompressDecompressFormat, BString), +) -> LuaResult { + let bytes = decompress(bs, format).await?; + lua.create_string(bytes) +} diff --git a/crates/lune-std-stdio/Cargo.toml b/crates/lune-std-stdio/Cargo.toml new file mode 100644 index 0000000..464f5d9 --- /dev/null +++ b/crates/lune-std-stdio/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "lune-std-stdio" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[lib] +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +dialoguer = "0.11" +mlua = { version = "0.9.7", features = ["luau"] } +mlua-luau-scheduler = "0.0.2" + +tokio = { version = "1", default-features = false, features = [ + "io-std", + "io-util", +] } + +lune-utils = { version = "0.1.0", path = "../lune-utils" } diff --git a/crates/lune-std-stdio/src/lib.rs b/crates/lune-std-stdio/src/lib.rs new file mode 100644 index 0000000..a4f7751 --- /dev/null +++ b/crates/lune-std-stdio/src/lib.rs @@ -0,0 +1,85 @@ +#![allow(clippy::cargo_common_metadata)] + +use lune_utils::fmt::{pretty_format_multi_value, ValueFormatConfig}; +use mlua::prelude::*; +use mlua_luau_scheduler::LuaSpawnExt; + +use tokio::io::{stderr, stdin, stdout, AsyncReadExt, AsyncWriteExt}; + +use lune_utils::TableBuilder; + +mod prompt; +mod style_and_color; + +use self::prompt::{prompt, PromptOptions, PromptResult}; +use self::style_and_color::{ColorKind, StyleKind}; + +const FORMAT_CONFIG: ValueFormatConfig = ValueFormatConfig::new() + .with_max_depth(4) + .with_colors_enabled(false); + +/** + Creates the `stdio` standard library module. + + # Errors + + Errors when out of memory. +*/ +pub fn module(lua: &Lua) -> LuaResult { + TableBuilder::new(lua)? + .with_function("color", stdio_color)? + .with_function("style", stdio_style)? + .with_function("format", stdio_format)? + .with_async_function("write", stdio_write)? + .with_async_function("ewrite", stdio_ewrite)? + .with_async_function("readToEnd", stdio_read_to_end)? + .with_async_function("prompt", stdio_prompt)? + .build_readonly() +} + +fn stdio_color(lua: &Lua, color: ColorKind) -> LuaResult { + color.ansi_escape_sequence().into_lua(lua) +} + +fn stdio_style(lua: &Lua, style: StyleKind) -> LuaResult { + style.ansi_escape_sequence().into_lua(lua) +} + +fn stdio_format(_: &Lua, args: LuaMultiValue) -> LuaResult { + Ok(pretty_format_multi_value(&args, &FORMAT_CONFIG)) +} + +async fn stdio_write(_: &Lua, s: LuaString<'_>) -> LuaResult<()> { + let mut stdout = stdout(); + stdout.write_all(s.as_bytes()).await?; + stdout.flush().await?; + Ok(()) +} + +async fn stdio_ewrite(_: &Lua, s: LuaString<'_>) -> LuaResult<()> { + let mut stderr = stderr(); + stderr.write_all(s.as_bytes()).await?; + stderr.flush().await?; + Ok(()) +} + +/* + FUTURE: Figure out how to expose some kind of "readLine" function using a buffered reader. + + This is a bit tricky since we would want to be able to use **both** readLine and readToEnd + in the same script, doing something like readLine, readLine, readToEnd from lua, and + having that capture the first two lines and then read the rest of the input. +*/ + +async fn stdio_read_to_end(lua: &Lua, (): ()) -> LuaResult { + let mut input = Vec::new(); + let mut stdin = stdin(); + stdin.read_to_end(&mut input).await?; + lua.create_string(&input) +} + +async fn stdio_prompt(lua: &Lua, options: PromptOptions) -> LuaResult { + lua.spawn_blocking(move || prompt(options)) + .await + .into_lua_err() +} diff --git a/src/lune/builtins/stdio/prompt.rs b/crates/lune-std-stdio/src/prompt.rs similarity index 60% rename from src/lune/builtins/stdio/prompt.rs rename to crates/lune-std-stdio/src/prompt.rs index 9cdd899..e1fcc02 100644 --- a/src/lune/builtins/stdio/prompt.rs +++ b/crates/lune-std-stdio/src/prompt.rs @@ -1,5 +1,6 @@ -use std::fmt; +use std::{fmt, str::FromStr}; +use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select}; use mlua::prelude::*; #[derive(Debug, Clone, Copy)] @@ -11,9 +12,7 @@ pub enum PromptKind { } impl PromptKind { - fn get_all() -> Vec { - vec![Self::Text, Self::Confirm, Self::Select, Self::MultiSelect] - } + const ALL: [PromptKind; 4] = [Self::Text, Self::Confirm, Self::Select, Self::MultiSelect]; } impl Default for PromptKind { @@ -22,6 +21,19 @@ impl Default for PromptKind { } } +impl FromStr for PromptKind { + type Err = (); + fn from_str(s: &str) -> Result { + match s.trim().to_ascii_lowercase().as_str() { + "text" => Ok(Self::Text), + "confirm" => Ok(Self::Confirm), + "select" => Ok(Self::Select), + "multiselect" => Ok(Self::MultiSelect), + _ => Err(()), + } + } +} + impl fmt::Display for PromptKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( @@ -43,45 +55,18 @@ impl<'lua> FromLua<'lua> for PromptKind { Ok(Self::default()) } else if let LuaValue::String(s) = value { let s = s.to_str()?; - /* - If the user only typed the prompt kind slightly wrong, meaning - it has some kind of space in it, a weird character, or an uppercase - character, we should try to be permissive as possible and still work - - Not everyone is using an IDE with proper Luau type definitions - installed, and Luau is still a permissive scripting language - even though it has a strict (but optional) type system - */ - let s = s - .chars() - .filter_map(|c| { - if c.is_ascii_alphabetic() { - Some(c.to_ascii_lowercase()) - } else { - None - } - }) - .collect::(); - // If the prompt kind is still invalid we will - // show the user a descriptive error message - match s.as_ref() { - "text" => Ok(Self::Text), - "confirm" => Ok(Self::Confirm), - "select" => Ok(Self::Select), - "multiselect" => Ok(Self::MultiSelect), - s => Err(LuaError::FromLuaConversionError { - from: "string", - to: "PromptKind", - message: Some(format!( - "Invalid prompt kind '{s}', valid kinds are:\n{}", - PromptKind::get_all() - .iter() - .map(ToString::to_string) - .collect::>() - .join(", ") - )), - }), - } + s.parse().map_err(|()| LuaError::FromLuaConversionError { + from: "string", + to: "PromptKind", + message: Some(format!( + "Invalid prompt kind '{s}', valid kinds are:\n{}", + PromptKind::ALL + .iter() + .map(ToString::to_string) + .collect::>() + .join(", ") + )), + }) } else { Err(LuaError::FromLuaConversionError { from: "nil", @@ -163,8 +148,8 @@ impl<'lua> FromLuaMulti<'lua> for PromptOptions { Ok(Self { kind, text, - default_bool, default_string, + default_bool, options, }) } @@ -190,3 +175,53 @@ impl<'lua> IntoLua<'lua> for PromptResult { }) } } + +pub fn prompt(options: PromptOptions) -> LuaResult { + let theme = ColorfulTheme::default(); + match options.kind { + PromptKind::Text => { + let input: String = Input::with_theme(&theme) + .allow_empty(true) + .with_prompt(options.text.unwrap_or_default()) + .with_initial_text(options.default_string.unwrap_or_default()) + .interact_text() + .into_lua_err()?; + Ok(PromptResult::String(input)) + } + PromptKind::Confirm => { + let mut prompt = Confirm::with_theme(&theme); + if let Some(b) = options.default_bool { + prompt = prompt.default(b); + }; + let result = prompt + .with_prompt(&options.text.expect("Missing text in prompt options")) + .interact() + .into_lua_err()?; + Ok(PromptResult::Boolean(result)) + } + PromptKind::Select => { + let chosen = Select::with_theme(&theme) + .with_prompt(&options.text.unwrap_or_default()) + .items(&options.options.expect("Missing options in prompt options")) + .interact_opt() + .into_lua_err()?; + Ok(match chosen { + Some(idx) => PromptResult::Index(idx + 1), + None => PromptResult::None, + }) + } + PromptKind::MultiSelect => { + let chosen = MultiSelect::with_theme(&theme) + .with_prompt(&options.text.unwrap_or_default()) + .items(&options.options.expect("Missing options in prompt options")) + .interact_opt() + .into_lua_err()?; + Ok(match chosen { + None => PromptResult::None, + Some(indices) => { + PromptResult::Indices(indices.iter().map(|idx| *idx + 1).collect()) + } + }) + } + } +} diff --git a/crates/lune-std-stdio/src/style_and_color.rs b/crates/lune-std-stdio/src/style_and_color.rs new file mode 100644 index 0000000..4080118 --- /dev/null +++ b/crates/lune-std-stdio/src/style_and_color.rs @@ -0,0 +1,195 @@ +use std::str::FromStr; + +use mlua::prelude::*; + +const ESCAPE_SEQ_RESET: &str = "\x1b[0m"; + +/** + A color kind supported by the `stdio` standard library. +*/ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ColorKind { + Reset, + Black, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White, +} + +impl ColorKind { + pub const ALL: [Self; 9] = [ + Self::Reset, + Self::Black, + Self::Red, + Self::Green, + Self::Yellow, + Self::Blue, + Self::Magenta, + Self::Cyan, + Self::White, + ]; + + /** + Returns the human-friendly name of this color kind. + */ + pub fn name(self) -> &'static str { + match self { + Self::Reset => "reset", + Self::Black => "black", + Self::Red => "red", + Self::Green => "green", + Self::Yellow => "yellow", + Self::Blue => "blue", + Self::Magenta => "magenta", + Self::Cyan => "cyan", + Self::White => "white", + } + } + + /** + Returns the ANSI escape sequence for the color kind. + */ + pub fn ansi_escape_sequence(self) -> &'static str { + match self { + Self::Reset => ESCAPE_SEQ_RESET, + Self::Black => "\x1b[30m", + Self::Red => "\x1b[31m", + Self::Green => "\x1b[32m", + Self::Yellow => "\x1b[33m", + Self::Blue => "\x1b[34m", + Self::Magenta => "\x1b[35m", + Self::Cyan => "\x1b[36m", + Self::White => "\x1b[37m", + } + } +} + +impl FromStr for ColorKind { + type Err = (); + fn from_str(s: &str) -> Result { + Ok(match s.trim().to_ascii_lowercase().as_str() { + "reset" => Self::Reset, + "black" => Self::Black, + "red" => Self::Red, + "green" => Self::Green, + "yellow" => Self::Yellow, + "blue" => Self::Blue, + // NOTE: Previous versions of Lune had this color as "purple" instead + // of "magenta", so we keep this here for backwards compatibility. + "magenta" | "purple" => Self::Magenta, + "cyan" => Self::Cyan, + "white" => Self::White, + _ => return Err(()), + }) + } +} + +impl FromLua<'_> for ColorKind { + fn from_lua(value: LuaValue, _: &Lua) -> LuaResult { + if let LuaValue::String(s) = value { + let s = s.to_str()?; + match s.parse() { + Ok(color) => Ok(color), + Err(()) => Err(LuaError::FromLuaConversionError { + from: "string", + to: "ColorKind", + message: Some(format!( + "Invalid color kind '{s}'\nValid kinds are: {}", + Self::ALL + .iter() + .map(|kind| kind.name()) + .collect::>() + .join(", ") + )), + }), + } + } else { + Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "ColorKind", + message: None, + }) + } + } +} + +/** + A style kind supported by the `stdio` standard library. +*/ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StyleKind { + Reset, + Bold, + Dim, +} + +impl StyleKind { + pub const ALL: [Self; 3] = [Self::Reset, Self::Bold, Self::Dim]; + + /** + Returns the human-friendly name for this style kind. + */ + pub fn name(self) -> &'static str { + match self { + Self::Reset => "reset", + Self::Bold => "bold", + Self::Dim => "dim", + } + } + + /** + Returns the ANSI escape sequence for this style kind. + */ + pub fn ansi_escape_sequence(self) -> &'static str { + match self { + Self::Reset => ESCAPE_SEQ_RESET, + Self::Bold => "\x1b[1m", + Self::Dim => "\x1b[2m", + } + } +} + +impl FromStr for StyleKind { + type Err = (); + fn from_str(s: &str) -> Result { + Ok(match s.trim().to_ascii_lowercase().as_str() { + "reset" => Self::Reset, + "bold" => Self::Bold, + "dim" => Self::Dim, + _ => return Err(()), + }) + } +} + +impl FromLua<'_> for StyleKind { + fn from_lua(value: LuaValue, _: &Lua) -> LuaResult { + if let LuaValue::String(s) = value { + let s = s.to_str()?; + match s.parse() { + Ok(style) => Ok(style), + Err(()) => Err(LuaError::FromLuaConversionError { + from: "string", + to: "StyleKind", + message: Some(format!( + "Invalid style kind '{s}'\nValid kinds are: {}", + Self::ALL + .iter() + .map(|kind| kind.name()) + .collect::>() + .join(", ") + )), + }), + } + } else { + Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "StyleKind", + message: None, + }) + } + } +} diff --git a/crates/lune-std-task/Cargo.toml b/crates/lune-std-task/Cargo.toml new file mode 100644 index 0000000..4df2d8a --- /dev/null +++ b/crates/lune-std-task/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "lune-std-task" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[lib] +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +mlua = { version = "0.9.7", features = ["luau"] } +mlua-luau-scheduler = "0.0.2" + +tokio = { version = "1", default-features = false, features = ["time"] } + +lune-utils = { version = "0.1.0", path = "../lune-utils" } diff --git a/src/lune/builtins/task/mod.rs b/crates/lune-std-task/src/lib.rs similarity index 81% rename from src/lune/builtins/task/mod.rs rename to crates/lune-std-task/src/lib.rs index 94a4da0..47a78d5 100644 --- a/src/lune/builtins/task/mod.rs +++ b/crates/lune-std-task/src/lib.rs @@ -1,20 +1,22 @@ +#![allow(clippy::cargo_common_metadata)] + use std::time::Duration; use mlua::prelude::*; - use mlua_luau_scheduler::Functions; -use tokio::time::{self, Instant}; -use crate::lune::util::TableBuilder; +use tokio::time::{sleep, Instant}; -const DELAY_IMPL_LUA: &str = r#" -return defer(function(...) - wait(select(1, ...)) - spawn(select(2, ...)) -end, ...) -"#; +use lune_utils::TableBuilder; -pub fn create(lua: &Lua) -> LuaResult> { +/** + Creates the `task` standard library module. + + # Errors + + Errors when out of memory, or if default Lua globals are missing. +*/ +pub fn module(lua: &Lua) -> LuaResult { let fns = Functions::new(lua)?; // Create wait & delay functions @@ -46,11 +48,18 @@ pub fn create(lua: &Lua) -> LuaResult> { .build_readonly() } +const DELAY_IMPL_LUA: &str = r" +return defer(function(...) + wait(select(1, ...)) + spawn(select(2, ...)) +end, ...) +"; + async fn wait(_: &Lua, secs: Option) -> LuaResult { let duration = Duration::from_secs_f64(secs.unwrap_or_default()); let before = Instant::now(); - time::sleep(duration).await; + sleep(duration).await; let after = Instant::now(); Ok((after - before).as_secs_f64()) diff --git a/crates/lune-std/Cargo.toml b/crates/lune-std/Cargo.toml new file mode 100644 index 0000000..3ee4c9a --- /dev/null +++ b/crates/lune-std/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "lune-std" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[lib] +path = "src/lib.rs" + +[lints] +workspace = true + +[features] +default = [ + "datetime", + "fs", + "luau", + "net", + "process", + "regex", + "roblox", + "serde", + "stdio", + "task", +] + +datetime = ["dep:lune-std-datetime"] +fs = ["dep:lune-std-fs"] +luau = ["dep:lune-std-luau"] +net = ["dep:lune-std-net"] +process = ["dep:lune-std-process"] +regex = ["dep:lune-std-regex"] +roblox = ["dep:lune-std-roblox"] +serde = ["dep:lune-std-serde"] +stdio = ["dep:lune-std-stdio"] +task = ["dep:lune-std-task"] + +[dependencies] +mlua = { version = "0.9.7", features = ["luau"] } +mlua-luau-scheduler = "0.0.2" + +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tokio = { version = "1", default-features = false, features = ["fs", "sync"] } + +lune-utils = { version = "0.1.0", path = "../lune-utils" } + +lune-std-datetime = { optional = true, version = "0.1.0", path = "../lune-std-datetime" } +lune-std-fs = { optional = true, version = "0.1.0", path = "../lune-std-fs" } +lune-std-luau = { optional = true, version = "0.1.0", path = "../lune-std-luau" } +lune-std-net = { optional = true, version = "0.1.0", path = "../lune-std-net" } +lune-std-process = { optional = true, version = "0.1.0", path = "../lune-std-process" } +lune-std-regex = { optional = true, version = "0.1.0", path = "../lune-std-regex" } +lune-std-roblox = { optional = true, version = "0.1.0", path = "../lune-std-roblox" } +lune-std-serde = { optional = true, version = "0.1.0", path = "../lune-std-serde" } +lune-std-stdio = { optional = true, version = "0.1.0", path = "../lune-std-stdio" } +lune-std-task = { optional = true, version = "0.1.0", path = "../lune-std-task" } diff --git a/crates/lune-std/src/global.rs b/crates/lune-std/src/global.rs new file mode 100644 index 0000000..1c0944f --- /dev/null +++ b/crates/lune-std/src/global.rs @@ -0,0 +1,92 @@ +use std::str::FromStr; + +use mlua::prelude::*; + +/** + A standard global provided by Lune. +*/ +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum LuneStandardGlobal { + GTable, + Print, + Require, + Version, + Warn, +} + +impl LuneStandardGlobal { + /** + All available standard globals. + */ + pub const ALL: &'static [Self] = &[ + Self::GTable, + Self::Print, + Self::Require, + Self::Version, + Self::Warn, + ]; + + /** + Gets the name of the global, such as `_G` or `require`. + */ + #[must_use] + pub fn name(&self) -> &'static str { + match self { + Self::GTable => "_G", + Self::Print => "print", + Self::Require => "require", + Self::Version => "_VERSION", + Self::Warn => "warn", + } + } + + /** + Creates the Lua value for the global. + + # Errors + + If the global could not be created. + */ + #[rustfmt::skip] + #[allow(unreachable_patterns)] + pub fn create<'lua>(&self, lua: &'lua Lua) -> LuaResult> { + let res = match self { + Self::GTable => crate::globals::g_table::create(lua), + Self::Print => crate::globals::print::create(lua), + Self::Require => crate::globals::require::create(lua), + Self::Version => crate::globals::version::create(lua), + Self::Warn => crate::globals::warn::create(lua), + }; + match res { + Ok(v) => Ok(v), + Err(e) => Err(e.context(format!( + "Failed to create standard global '{}'", + self.name() + ))), + } + } +} + +impl FromStr for LuneStandardGlobal { + type Err = String; + fn from_str(s: &str) -> Result { + let low = s.trim().to_ascii_lowercase(); + Ok(match low.as_str() { + "_g" => Self::GTable, + "print" => Self::Print, + "require" => Self::Require, + "_version" => Self::Version, + "warn" => Self::Warn, + _ => { + return Err(format!( + "Unknown standard global '{low}'\nValid globals are: {}", + Self::ALL + .iter() + .map(Self::name) + .collect::>() + .join(", ") + )) + } + }) + } +} diff --git a/crates/lune-std/src/globals/g_table.rs b/crates/lune-std/src/globals/g_table.rs new file mode 100644 index 0000000..e21fa1e --- /dev/null +++ b/crates/lune-std/src/globals/g_table.rs @@ -0,0 +1,5 @@ +use mlua::prelude::*; + +pub fn create(lua: &Lua) -> LuaResult { + lua.create_table()?.into_lua(lua) +} diff --git a/crates/lune-std/src/globals/mod.rs b/crates/lune-std/src/globals/mod.rs new file mode 100644 index 0000000..b60e9a1 --- /dev/null +++ b/crates/lune-std/src/globals/mod.rs @@ -0,0 +1,5 @@ +pub mod g_table; +pub mod print; +pub mod require; +pub mod version; +pub mod warn; diff --git a/crates/lune-std/src/globals/print.rs b/crates/lune-std/src/globals/print.rs new file mode 100644 index 0000000..3b8d5f1 --- /dev/null +++ b/crates/lune-std/src/globals/print.rs @@ -0,0 +1,19 @@ +use std::io::Write; + +use lune_utils::fmt::{pretty_format_multi_value, ValueFormatConfig}; +use mlua::prelude::*; + +const FORMAT_CONFIG: ValueFormatConfig = ValueFormatConfig::new() + .with_max_depth(4) + .with_colors_enabled(true); + +pub fn create(lua: &Lua) -> LuaResult { + let f = lua.create_function(|_, args: LuaMultiValue| { + let formatted = format!("{}\n", pretty_format_multi_value(&args, &FORMAT_CONFIG)); + let mut stdout = std::io::stdout(); + stdout.write_all(formatted.as_bytes())?; + stdout.flush()?; + Ok(()) + })?; + f.into_lua(lua) +} diff --git a/src/lune/globals/require/alias.rs b/crates/lune-std/src/globals/require/alias.rs similarity index 88% rename from src/lune/globals/require/alias.rs rename to crates/lune-std/src/globals/require/alias.rs index 09a36b0..924056b 100644 --- a/src/lune/globals/require/alias.rs +++ b/crates/lune-std/src/globals/require/alias.rs @@ -1,10 +1,8 @@ -use console::style; use mlua::prelude::*; -use crate::lune::util::{ - luaurc::LuauRc, - paths::{make_absolute_and_clean, CWD}, -}; +use lune_utils::path::{clean_path_and_make_absolute, diff_path, get_current_dir}; + +use crate::luaurc::LuauRc; use super::context::*; @@ -20,7 +18,7 @@ where { let alias = alias.to_ascii_lowercase(); - let parent = make_absolute_and_clean(source) + let parent = clean_path_and_make_absolute(source) .parent() .expect("how did a root path end up here..") .to_path_buf(); @@ -55,7 +53,7 @@ where luaurc .aliases() .iter() - .map(|(name, path)| format!(" {name} {} {path}", style(">").dim())) + .map(|(name, path)| format!(" {name} > {path}")) .collect::>() .join("\n") )) @@ -67,7 +65,7 @@ where // We now have our aliased path, our path require function just needs it // in a slightly different format with both absolute + relative to cwd let abs_path = luaurc.find_alias(&alias).unwrap().join(path); - let rel_path = pathdiff::diff_paths(&abs_path, CWD.as_path()).ok_or_else(|| { + let rel_path = diff_path(&abs_path, get_current_dir()).ok_or_else(|| { LuaError::runtime(format!("failed to find relative path for alias '{alias}'")) })?; diff --git a/src/lune/globals/require/context.rs b/crates/lune-std/src/globals/require/context.rs similarity index 86% rename from src/lune/globals/require/context.rs rename to crates/lune-std/src/globals/require/context.rs index 7f018c6..0355d27 100644 --- a/src/lune/globals/require/context.rs +++ b/crates/lune-std/src/globals/require/context.rs @@ -6,15 +6,18 @@ use std::{ use mlua::prelude::*; use mlua_luau_scheduler::LuaSchedulerExt; + use tokio::{ - fs, + fs::read, sync::{ broadcast::{self, Sender}, Mutex as AsyncMutex, }, }; -use crate::lune::{builtins::LuneBuiltin, util::paths::CWD}; +use lune_utils::path::{clean_path, clean_path_and_make_absolute}; + +use crate::library::LuneStandardLibrary; /** Context containing cached results for all `require` operations. @@ -24,9 +27,9 @@ use crate::lune::{builtins::LuneBuiltin, util::paths::CWD}; */ #[derive(Debug, Clone)] pub(super) struct RequireContext { - cache_builtins: Arc>>>, - cache_results: Arc>>>, - cache_pending: Arc>>>, + libraries: Arc>>>, + results: Arc>>>, + pending: Arc>>>, } impl RequireContext { @@ -39,9 +42,9 @@ impl RequireContext { */ pub fn new() -> Self { Self { - cache_builtins: Arc::new(AsyncMutex::new(HashMap::new())), - cache_results: Arc::new(AsyncMutex::new(HashMap::new())), - cache_pending: Arc::new(AsyncMutex::new(HashMap::new())), + libraries: Arc::new(AsyncMutex::new(HashMap::new())), + results: Arc::new(AsyncMutex::new(HashMap::new())), + pending: Arc::new(AsyncMutex::new(HashMap::new())), } } @@ -54,7 +57,6 @@ impl RequireContext { absolute path by prepending the current working directory. */ pub fn resolve_paths( - &self, source: impl AsRef, path: impl AsRef, ) -> LuaResult<(PathBuf, PathBuf)> { @@ -63,12 +65,8 @@ impl RequireContext { .ok_or_else(|| LuaError::runtime("Failed to get parent path of source"))? .join(path.as_ref()); - let rel_path = path_clean::clean(path); - let abs_path = if rel_path.is_absolute() { - rel_path.to_path_buf() - } else { - CWD.join(&rel_path) - }; + let abs_path = clean_path_and_make_absolute(&path); + let rel_path = clean_path(path); Ok((abs_path, rel_path)) } @@ -78,7 +76,7 @@ impl RequireContext { */ pub fn is_cached(&self, abs_path: impl AsRef) -> LuaResult { let is_cached = self - .cache_results + .results .try_lock() .expect("RequireContext may not be used from multiple threads") .contains_key(abs_path.as_ref()); @@ -90,7 +88,7 @@ impl RequireContext { */ pub fn is_pending(&self, abs_path: impl AsRef) -> LuaResult { let is_pending = self - .cache_pending + .pending .try_lock() .expect("RequireContext may not be used from multiple threads") .contains_key(abs_path.as_ref()); @@ -108,7 +106,7 @@ impl RequireContext { abs_path: impl AsRef, ) -> LuaResult> { let results = self - .cache_results + .results .try_lock() .expect("RequireContext may not be used from multiple threads"); @@ -138,7 +136,7 @@ impl RequireContext { ) -> LuaResult> { let mut thread_recv = { let pending = self - .cache_pending + .pending .try_lock() .expect("RequireContext may not be used from multiple threads"); let thread_id = pending @@ -163,7 +161,7 @@ impl RequireContext { // Read the file at the given path, try to parse and // load it into a new lua thread that we can schedule - let file_contents = fs::read(&abs_path).await?; + let file_contents = read(&abs_path).await?; let file_thread = lua .load(file_contents) .set_name(rel_path.to_string_lossy().to_string()); @@ -201,7 +199,7 @@ impl RequireContext { // Set this abs path as currently pending let (broadcast_tx, _) = broadcast::channel(1); - self.cache_pending + self.pending .try_lock() .expect("RequireContext may not be used from multiple threads") .insert(abs_path.to_path_buf(), broadcast_tx); @@ -221,7 +219,7 @@ impl RequireContext { // NOTE: We use the async lock and not try_lock here because // some other thread may be wanting to insert into the require // cache at the same time, and that's not an actual error case - self.cache_results + self.results .lock() .await .insert(abs_path.to_path_buf(), load_res); @@ -230,7 +228,7 @@ impl RequireContext { // broadcast a message to let any listeners know that this // path has now finished the require process and is cached let broadcast_tx = self - .cache_pending + .pending .try_lock() .expect("RequireContext may not be used from multiple threads") .remove(abs_path) @@ -241,39 +239,39 @@ impl RequireContext { } /** - Loads (requires) the builtin with the given name. + Loads (requires) the library with the given name. */ - pub fn load_builtin<'lua>( + pub fn load_library<'lua>( &self, lua: &'lua Lua, name: impl AsRef, ) -> LuaResult> { - let builtin: LuneBuiltin = match name.as_ref().parse() { + let library: LuneStandardLibrary = match name.as_ref().parse() { Err(e) => return Err(LuaError::runtime(e)), Ok(b) => b, }; let mut cache = self - .cache_builtins + .libraries .try_lock() .expect("RequireContext may not be used from multiple threads"); - if let Some(res) = cache.get(&builtin) { + if let Some(res) = cache.get(&library) { return match res { Err(e) => return Err(e.clone()), Ok(key) => { let multi_vec = lua .registry_value::>(key) - .expect("Missing builtin result in lua registry"); + .expect("Missing library result in lua registry"); Ok(LuaMultiValue::from_vec(multi_vec)) } }; }; - let result = builtin.create(lua); + let result = library.module(lua); cache.insert( - builtin, + library, match result.clone() { Err(e) => Err(e), Ok(multi) => { diff --git a/src/lune/globals/require/builtin.rs b/crates/lune-std/src/globals/require/library.rs similarity index 70% rename from src/lune/globals/require/builtin.rs rename to crates/lune-std/src/globals/require/library.rs index 42302cf..b47ea92 100644 --- a/src/lune/globals/require/builtin.rs +++ b/crates/lune-std/src/globals/require/library.rs @@ -2,7 +2,7 @@ use mlua::prelude::*; use super::context::*; -pub(super) async fn require<'lua, 'ctx>( +pub(super) fn require<'lua, 'ctx>( lua: &'lua Lua, ctx: &'ctx RequireContext, name: &str, @@ -10,5 +10,5 @@ pub(super) async fn require<'lua, 'ctx>( where 'lua: 'ctx, { - ctx.load_builtin(lua, name) + ctx.load_library(lua, name) } diff --git a/src/lune/globals/require/mod.rs b/crates/lune-std/src/globals/require/mod.rs similarity index 85% rename from src/lune/globals/require/mod.rs rename to crates/lune-std/src/globals/require/mod.rs index 1a83e58..3876e36 100644 --- a/src/lune/globals/require/mod.rs +++ b/crates/lune-std/src/globals/require/mod.rs @@ -1,19 +1,19 @@ use mlua::prelude::*; -use crate::lune::util::TableBuilder; +use lune_utils::TableBuilder; mod context; use context::RequireContext; mod alias; -mod builtin; +mod library; mod path; -const REQUIRE_IMPL: &str = r#" +const REQUIRE_IMPL: &str = r" return require(source(), ...) -"#; +"; -pub fn create(lua: &Lua) -> LuaResult> { +pub fn create(lua: &Lua) -> LuaResult { lua.set_app_data(RequireContext::new()); /* @@ -36,7 +36,7 @@ pub fn create(lua: &Lua) -> LuaResult> { */ let require_fn = lua.create_async_function(require)?; - let get_source_fn = lua.create_function(move |lua, _: ()| match lua.inspect_stack(2) { + let get_source_fn = lua.create_function(move |lua, (): ()| match lua.inspect_stack(2) { None => Err(LuaError::runtime( "Failed to get stack info for require source", )), @@ -56,7 +56,8 @@ pub fn create(lua: &Lua) -> LuaResult> { lua.load(REQUIRE_IMPL) .set_name("require") .set_environment(require_env) - .into_function() + .into_function()? + .into_lua(lua) } async fn require<'lua>( @@ -79,11 +80,8 @@ async fn require<'lua>( .app_data_ref() .expect("Failed to get RequireContext from app data"); - if let Some(builtin_name) = path - .strip_prefix("@lune/") - .map(|name| name.to_ascii_lowercase()) - { - builtin::require(lua, &context, &builtin_name).await + if let Some(builtin_name) = path.strip_prefix("@lune/").map(str::to_ascii_lowercase) { + library::require(lua, &context, &builtin_name) } else if let Some(aliased_path) = path.strip_prefix('@') { let (alias, path) = aliased_path.split_once('/').ok_or(LuaError::runtime( "Require with custom alias must contain '/' delimiter", diff --git a/src/lune/globals/require/path.rs b/crates/lune-std/src/globals/require/path.rs similarity index 97% rename from src/lune/globals/require/path.rs rename to crates/lune-std/src/globals/require/path.rs index 7e8084f..1fabebf 100644 --- a/src/lune/globals/require/path.rs +++ b/crates/lune-std/src/globals/require/path.rs @@ -14,7 +14,7 @@ pub(super) async fn require<'lua, 'ctx>( where 'lua: 'ctx, { - let (abs_path, rel_path) = ctx.resolve_paths(source, path)?; + let (abs_path, rel_path) = RequireContext::resolve_paths(source, path)?; require_abs_rel(lua, ctx, abs_path, rel_path).await } diff --git a/crates/lune-std/src/globals/version.rs b/crates/lune-std/src/globals/version.rs new file mode 100644 index 0000000..3eface4 --- /dev/null +++ b/crates/lune-std/src/globals/version.rs @@ -0,0 +1,35 @@ +use mlua::prelude::*; + +use lune_utils::get_version_string; + +struct Version(String); + +impl LuaUserData for Version {} + +pub fn create(lua: &Lua) -> LuaResult { + let v = match lua.app_data_ref::() { + Some(v) => v.0.to_string(), + None => env!("CARGO_PKG_VERSION").to_string(), + }; + let s = get_version_string(v); + lua.create_string(s)?.into_lua(lua) +} + +/** + Overrides the version string to be used by the `_VERSION` global. + + The global will be a string in the format `Lune x.y.z+luau`, + where `x.y.z` is the string passed to this function. + + The version string passed should be the version of the Lune runtime, + obtained from `env!("CARGO_PKG_VERSION")` or a similar mechanism. + + # Panics + + Panics if the version string is empty or contains invalid characters. +*/ +pub fn set_global_version(lua: &Lua, version: impl Into) { + let v = version.into(); + let _ = get_version_string(&v); // Validate version string + lua.set_app_data(Version(v)); +} diff --git a/crates/lune-std/src/globals/warn.rs b/crates/lune-std/src/globals/warn.rs new file mode 100644 index 0000000..ee42ddb --- /dev/null +++ b/crates/lune-std/src/globals/warn.rs @@ -0,0 +1,23 @@ +use std::io::Write; + +use lune_utils::fmt::{pretty_format_multi_value, Label, ValueFormatConfig}; +use mlua::prelude::*; + +const FORMAT_CONFIG: ValueFormatConfig = ValueFormatConfig::new() + .with_max_depth(4) + .with_colors_enabled(true); + +pub fn create(lua: &Lua) -> LuaResult { + let f = lua.create_function(|_, args: LuaMultiValue| { + let formatted = format!( + "{}\n{}\n", + Label::Warn, + pretty_format_multi_value(&args, &FORMAT_CONFIG) + ); + let mut stdout = std::io::stdout(); + stdout.write_all(formatted.as_bytes())?; + stdout.flush()?; + Ok(()) + })?; + f.into_lua(lua) +} diff --git a/crates/lune-std/src/lib.rs b/crates/lune-std/src/lib.rs new file mode 100644 index 0000000..a29bef0 --- /dev/null +++ b/crates/lune-std/src/lib.rs @@ -0,0 +1,29 @@ +#![allow(clippy::cargo_common_metadata)] + +use mlua::prelude::*; + +mod global; +mod globals; +mod library; +mod luaurc; + +pub use self::global::LuneStandardGlobal; +pub use self::globals::version::set_global_version; +pub use self::library::LuneStandardLibrary; + +/** + Injects all standard globals into the given Lua state / VM. + + This includes all enabled standard libraries, which can + be used from Lua with `require("@lune/library-name")`. + + # Errors + + Errors when out of memory, or if *default* Lua globals are missing. +*/ +pub fn inject_globals(lua: &Lua) -> LuaResult<()> { + for global in LuneStandardGlobal::ALL { + lua.globals().set(global.name(), global.create(lua)?)?; + } + Ok(()) +} diff --git a/crates/lune-std/src/library.rs b/crates/lune-std/src/library.rs new file mode 100644 index 0000000..9a301f5 --- /dev/null +++ b/crates/lune-std/src/library.rs @@ -0,0 +1,127 @@ +use std::str::FromStr; + +use mlua::prelude::*; + +/** + A standard library provided by Lune. +*/ +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +#[rustfmt::skip] +pub enum LuneStandardLibrary { + #[cfg(feature = "datetime")] DateTime, + #[cfg(feature = "fs")] Fs, + #[cfg(feature = "luau")] Luau, + #[cfg(feature = "net")] Net, + #[cfg(feature = "task")] Task, + #[cfg(feature = "process")] Process, + #[cfg(feature = "regex")] Regex, + #[cfg(feature = "serde")] Serde, + #[cfg(feature = "stdio")] Stdio, + #[cfg(feature = "roblox")] Roblox, +} + +impl LuneStandardLibrary { + /** + All available standard libraries. + */ + #[rustfmt::skip] + pub const ALL: &'static [Self] = &[ + #[cfg(feature = "datetime")] Self::DateTime, + #[cfg(feature = "fs")] Self::Fs, + #[cfg(feature = "luau")] Self::Luau, + #[cfg(feature = "net")] Self::Net, + #[cfg(feature = "task")] Self::Task, + #[cfg(feature = "process")] Self::Process, + #[cfg(feature = "regex")] Self::Regex, + #[cfg(feature = "serde")] Self::Serde, + #[cfg(feature = "stdio")] Self::Stdio, + #[cfg(feature = "roblox")] Self::Roblox, + ]; + + /** + Gets the name of the library, such as `datetime` or `fs`. + */ + #[must_use] + #[rustfmt::skip] + #[allow(unreachable_patterns)] + pub fn name(&self) -> &'static str { + match self { + #[cfg(feature = "datetime")] Self::DateTime => "datetime", + #[cfg(feature = "fs")] Self::Fs => "fs", + #[cfg(feature = "luau")] Self::Luau => "luau", + #[cfg(feature = "net")] Self::Net => "net", + #[cfg(feature = "task")] Self::Task => "task", + #[cfg(feature = "process")] Self::Process => "process", + #[cfg(feature = "regex")] Self::Regex => "regex", + #[cfg(feature = "serde")] Self::Serde => "serde", + #[cfg(feature = "stdio")] Self::Stdio => "stdio", + #[cfg(feature = "roblox")] Self::Roblox => "roblox", + + _ => unreachable!("no standard library enabled"), + } + } + + /** + Creates the Lua module for the library. + + # Errors + + If the library could not be created. + */ + #[rustfmt::skip] + #[allow(unreachable_patterns)] + pub fn module<'lua>(&self, lua: &'lua Lua) -> LuaResult> { + let res: LuaResult = match self { + #[cfg(feature = "datetime")] Self::DateTime => lune_std_datetime::module(lua), + #[cfg(feature = "fs")] Self::Fs => lune_std_fs::module(lua), + #[cfg(feature = "luau")] Self::Luau => lune_std_luau::module(lua), + #[cfg(feature = "net")] Self::Net => lune_std_net::module(lua), + #[cfg(feature = "task")] Self::Task => lune_std_task::module(lua), + #[cfg(feature = "process")] Self::Process => lune_std_process::module(lua), + #[cfg(feature = "regex")] Self::Regex => lune_std_regex::module(lua), + #[cfg(feature = "serde")] Self::Serde => lune_std_serde::module(lua), + #[cfg(feature = "stdio")] Self::Stdio => lune_std_stdio::module(lua), + #[cfg(feature = "roblox")] Self::Roblox => lune_std_roblox::module(lua), + + _ => unreachable!("no standard library enabled"), + }; + match res { + Ok(v) => v.into_lua_multi(lua), + Err(e) => Err(e.context(format!( + "Failed to create standard library '{}'", + self.name() + ))), + } + } +} + +impl FromStr for LuneStandardLibrary { + type Err = String; + #[rustfmt::skip] + fn from_str(s: &str) -> Result { + let low = s.trim().to_ascii_lowercase(); + Ok(match low.as_str() { + #[cfg(feature = "datetime")] "datetime" => Self::DateTime, + #[cfg(feature = "fs")] "fs" => Self::Fs, + #[cfg(feature = "luau")] "luau" => Self::Luau, + #[cfg(feature = "net")] "net" => Self::Net, + #[cfg(feature = "task")] "task" => Self::Task, + #[cfg(feature = "process")] "process" => Self::Process, + #[cfg(feature = "regex")] "regex" => Self::Regex, + #[cfg(feature = "serde")] "serde" => Self::Serde, + #[cfg(feature = "stdio")] "stdio" => Self::Stdio, + #[cfg(feature = "roblox")] "roblox" => Self::Roblox, + + _ => { + return Err(format!( + "Unknown standard library '{low}'\nValid libraries are: {}", + Self::ALL + .iter() + .map(Self::name) + .collect::>() + .join(", ") + )) + } + }) + } +} diff --git a/src/lune/util/luaurc.rs b/crates/lune-std/src/luaurc.rs similarity index 65% rename from src/lune/util/luaurc.rs rename to crates/lune-std/src/luaurc.rs index 69ac64a..0eada59 100644 --- a/src/lune/util/luaurc.rs +++ b/crates/lune-std/src/luaurc.rs @@ -1,20 +1,20 @@ use std::{ collections::HashMap, path::{Path, PathBuf, MAIN_SEPARATOR}, + sync::Arc, }; -use path_clean::PathClean; use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; -use tokio::fs; +use tokio::fs::read; -use super::paths::make_absolute_and_clean; +use lune_utils::path::{clean_path, clean_path_and_make_absolute}; const LUAURC_FILE: &str = ".luaurc"; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] -pub enum LuauLanguageMode { +enum LuauLanguageMode { NoCheck, NonStrict, Strict, @@ -22,7 +22,7 @@ pub enum LuauLanguageMode { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct LuauRcConfig { +struct LuauRcConfig { #[serde(skip_serializing_if = "Option::is_none")] language_mode: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -39,26 +39,45 @@ pub struct LuauRcConfig { aliases: Option>, } +/** + A deserialized `.luaurc` file. + + Contains utility methods for validating and searching for aliases. +*/ #[derive(Debug, Clone)] pub struct LuauRc { - dir: PathBuf, + dir: Arc, config: LuauRcConfig, } impl LuauRc { + /** + Reads a `.luaurc` file from the given directory. + + If the file does not exist, or if it is invalid, this function returns `None`. + */ pub async fn read(dir: impl AsRef) -> Option { - let dir = make_absolute_and_clean(dir); + let dir = clean_path_and_make_absolute(dir); let path = dir.join(LUAURC_FILE); - let bytes = fs::read(&path).await.ok()?; + let bytes = read(&path).await.ok()?; let config = serde_json::from_slice(&bytes).ok()?; - Some(Self { dir, config }) + Some(Self { + dir: dir.into(), + config, + }) } + /** + Reads a `.luaurc` file from the given directory, and then recursively searches + for a `.luaurc` file in the parent directories if a predicate is not satisfied. + + If no `.luaurc` file exists, or if they are invalid, this function returns `None`. + */ pub async fn read_recursive( dir: impl AsRef, mut predicate: impl FnMut(&Self) -> bool, ) -> Option { - let mut current = make_absolute_and_clean(dir); + let mut current = clean_path_and_make_absolute(dir); loop { if let Some(rc) = Self::read(¤t).await { if predicate(&rc) { @@ -73,21 +92,43 @@ impl LuauRc { } } + /** + Validates that the `.luaurc` file is correct. + + This primarily validates aliases since they are not + validated during creation of the [`LuauRc`] struct. + + # Errors + + If an alias key is invalid. + */ pub fn validate(&self) -> Result<(), String> { if let Some(aliases) = &self.config.aliases { for alias in aliases.keys() { if !is_valid_alias_key(alias) { - return Err(format!("invalid alias key: {}", alias)); + return Err(format!("invalid alias key: {alias}")); } } } Ok(()) } + /** + Gets a copy of all aliases in the `.luaurc` file. + + Will return an empty map if there are no aliases. + */ + #[must_use] pub fn aliases(&self) -> HashMap { self.config.aliases.clone().unwrap_or_default() } + /** + Finds an alias in the `.luaurc` file by name. + + If the alias does not exist, this function returns `None`. + */ + #[must_use] pub fn find_alias(&self, name: &str) -> Option { self.config.aliases.as_ref().and_then(|aliases| { aliases.iter().find_map(|(alias, path)| { @@ -96,7 +137,7 @@ impl LuauRc { .eq_ignore_ascii_case(name) && is_valid_alias_key(alias) { - Some(self.dir.join(path).clean()) + Some(clean_path(self.dir.join(path))) } else { None } diff --git a/crates/lune-utils/Cargo.toml b/crates/lune-utils/Cargo.toml new file mode 100644 index 0000000..dd39731 --- /dev/null +++ b/crates/lune-utils/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "lune-utils" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[lib] +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +mlua = { version = "0.9.7", features = ["luau", "async"] } + +tokio = { version = "1", default-features = false, features = ["fs"] } + +console = "0.15" +dunce = "1.0" +once_cell = "1.17" +path-clean = "1.0" +pathdiff = "0.2" diff --git a/crates/lune-utils/src/fmt/error/components.rs b/crates/lune-utils/src/fmt/error/components.rs new file mode 100644 index 0000000..941b8d0 --- /dev/null +++ b/crates/lune-utils/src/fmt/error/components.rs @@ -0,0 +1,152 @@ +use std::fmt; +use std::str::FromStr; +use std::sync::Arc; + +use console::style; +use mlua::prelude::*; +use once_cell::sync::Lazy; + +use super::StackTrace; + +static STYLED_STACK_BEGIN: Lazy = Lazy::new(|| { + format!( + "{}{}{}", + style("[").dim(), + style("Stack Begin").blue(), + style("]").dim() + ) +}); + +static STYLED_STACK_END: Lazy = Lazy::new(|| { + format!( + "{}{}{}", + style("[").dim(), + style("Stack End").blue(), + style("]").dim() + ) +}); + +/** + Error components parsed from a [`LuaError`]. + + Can be used to display a human-friendly error message + and stack trace, in the following Roblox-inspired format: + + ```plaintext + Error message + [Stack Begin] + Stack trace line + Stack trace line + Stack trace line + [Stack End] + ``` +*/ +#[derive(Debug, Default, Clone)] +pub struct ErrorComponents { + messages: Vec, + trace: Option, +} + +impl ErrorComponents { + /** + Returns the error messages. + */ + #[must_use] + pub fn messages(&self) -> &[String] { + &self.messages + } + + /** + Returns the stack trace, if it exists. + */ + #[must_use] + pub fn trace(&self) -> Option<&StackTrace> { + self.trace.as_ref() + } + + /** + Returns `true` if the error has a non-empty stack trace. + + Note that a trace may still *exist*, but it may be empty. + */ + #[must_use] + pub fn has_trace(&self) -> bool { + self.trace + .as_ref() + .is_some_and(|trace| !trace.lines().is_empty()) + } +} + +impl fmt::Display for ErrorComponents { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for message in self.messages() { + writeln!(f, "{message}")?; + } + if self.has_trace() { + let trace = self.trace.as_ref().unwrap(); + writeln!(f, "{}", *STYLED_STACK_BEGIN)?; + for line in trace.lines() { + writeln!(f, "\t{line}")?; + } + writeln!(f, "{}", *STYLED_STACK_END)?; + } + Ok(()) + } +} + +impl From for ErrorComponents { + fn from(error: LuaError) -> Self { + fn lua_error_message(e: &LuaError) -> String { + if let LuaError::RuntimeError(s) = e { + s.to_string() + } else { + e.to_string() + } + } + + fn lua_stack_trace(source: &str) -> Option { + // FUTURE: Preserve a parsing error here somehow? + // Maybe we can emit parsing errors using tracing? + StackTrace::from_str(source).ok() + } + + // Extract any additional "context" messages before the actual error(s) + // The Arc is necessary here because mlua wraps all inner errors in an Arc + let mut error = Arc::new(error); + let mut messages = Vec::new(); + while let LuaError::WithContext { + ref context, + ref cause, + } = *error + { + messages.push(context.to_string()); + error = cause.clone(); + } + + // We will then try to extract any stack trace + let trace = if let LuaError::CallbackError { + ref traceback, + ref cause, + } = *error + { + messages.push(lua_error_message(cause)); + lua_stack_trace(traceback) + } else if let LuaError::RuntimeError(ref s) = *error { + // NOTE: Runtime errors may include tracebacks, but they're + // joined with error messages, so we need to split them out + if let Some(pos) = s.find("stack traceback:") { + let (message, traceback) = s.split_at(pos); + messages.push(message.trim().to_string()); + lua_stack_trace(traceback) + } else { + messages.push(s.to_string()); + None + } + } else { + messages.push(lua_error_message(&error)); + None + }; + + ErrorComponents { messages, trace } + } +} diff --git a/crates/lune-utils/src/fmt/error/mod.rs b/crates/lune-utils/src/fmt/error/mod.rs new file mode 100644 index 0000000..00d0658 --- /dev/null +++ b/crates/lune-utils/src/fmt/error/mod.rs @@ -0,0 +1,8 @@ +mod components; +mod stack_trace; + +#[cfg(test)] +mod tests; + +pub use self::components::ErrorComponents; +pub use self::stack_trace::{StackTrace, StackTraceLine, StackTraceSource}; diff --git a/crates/lune-utils/src/fmt/error/stack_trace.rs b/crates/lune-utils/src/fmt/error/stack_trace.rs new file mode 100644 index 0000000..a33ec9a --- /dev/null +++ b/crates/lune-utils/src/fmt/error/stack_trace.rs @@ -0,0 +1,170 @@ +use std::fmt; +use std::str::FromStr; + +fn parse_path(s: &str) -> Option<(&str, &str)> { + let path = s.strip_prefix("[string \"")?; + let (path, after) = path.split_once("\"]:")?; + + // Remove line number after any found colon, this may + // exist if the source path is from a rust source file + let path = match path.split_once(':') { + Some((before, _)) => before, + None => path, + }; + + Some((path, after)) +} + +fn parse_function_name(s: &str) -> Option<&str> { + s.strip_prefix("in function '") + .and_then(|s| s.strip_suffix('\'')) +} + +fn parse_line_number(s: &str) -> (Option, &str) { + match s.split_once(':') { + Some((before, after)) => (before.parse::().ok(), after), + None => (None, s), + } +} + +/** + Source of a stack trace line parsed from a [`LuaError`]. +*/ +#[derive(Debug, Default, Clone, Copy)] +pub enum StackTraceSource { + /// Error originated from a C / Rust function. + C, + /// Error originated from a Lua (user) function. + #[default] + Lua, +} + +/** + Stack trace line parsed from a [`LuaError`]. +*/ +#[derive(Debug, Default, Clone)] +pub struct StackTraceLine { + source: StackTraceSource, + path: Option, + line_number: Option, + function_name: Option, +} + +impl StackTraceLine { + /** + Returns the source of the stack trace line. + */ + #[must_use] + pub fn source(&self) -> StackTraceSource { + self.source + } + + /** + Returns the path, if it exists. + */ + #[must_use] + pub fn path(&self) -> Option<&str> { + self.path.as_deref() + } + + /** + Returns the line number, if it exists. + */ + #[must_use] + pub fn line_number(&self) -> Option { + self.line_number + } + + /** + Returns the function name, if it exists. + */ + #[must_use] + pub fn function_name(&self) -> Option<&str> { + self.function_name.as_deref() + } +} + +impl FromStr for StackTraceLine { + type Err = String; + fn from_str(s: &str) -> Result { + if let Some(after) = s.strip_prefix("[C]: ") { + let function_name = parse_function_name(after).map(ToString::to_string); + + Ok(Self { + source: StackTraceSource::C, + path: None, + line_number: None, + function_name, + }) + } else if let Some((path, after)) = parse_path(s) { + let (line_number, after) = parse_line_number(after); + let function_name = parse_function_name(after).map(ToString::to_string); + + Ok(Self { + source: StackTraceSource::Lua, + path: Some(path.to_string()), + line_number, + function_name, + }) + } else { + Err(String::from("unknown format")) + } + } +} + +impl fmt::Display for StackTraceLine { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if matches!(self.source, StackTraceSource::C) { + write!(f, "Script '[C]'")?; + } else { + write!(f, "Script '{}'", self.path.as_deref().unwrap_or("[?]"))?; + if let Some(line_number) = self.line_number { + write!(f, ", Line {line_number}")?; + } + } + if let Some(function_name) = self.function_name.as_deref() { + write!(f, " - function '{function_name}'")?; + } + Ok(()) + } +} + +/** + Stack trace parsed from a [`LuaError`]. +*/ +#[derive(Debug, Default, Clone)] +pub struct StackTrace { + lines: Vec, +} + +impl StackTrace { + /** + Returns the individual stack trace lines. + */ + #[must_use] + pub fn lines(&self) -> &[StackTraceLine] { + &self.lines + } +} + +impl FromStr for StackTrace { + type Err = String; + fn from_str(s: &str) -> Result { + let (_, after) = s + .split_once("stack traceback:") + .ok_or_else(|| String::from("missing 'stack traceback:' prefix"))?; + let lines = after + .trim() + .lines() + .filter_map(|line| { + let line = line.trim(); + if line.is_empty() { + None + } else { + Some(line.parse()) + } + }) + .collect::, _>>()?; + Ok(StackTrace { lines }) + } +} diff --git a/crates/lune-utils/src/fmt/error/tests.rs b/crates/lune-utils/src/fmt/error/tests.rs new file mode 100644 index 0000000..ff5a5f3 --- /dev/null +++ b/crates/lune-utils/src/fmt/error/tests.rs @@ -0,0 +1,85 @@ +use mlua::prelude::*; + +use crate::fmt::ErrorComponents; + +fn new_lua_result() -> LuaResult<()> { + let lua = Lua::new(); + + lua.globals() + .set( + "f", + LuaFunction::wrap(|_, (): ()| { + Err::<(), _>(LuaError::runtime("oh no, a runtime error")) + }), + ) + .unwrap(); + + lua.load("f()").set_name("chunk_name").eval() +} + +// Tests for error context stack +mod context { + use super::*; + + #[test] + fn preserves_original() { + let lua_error = new_lua_result().context("additional context").unwrap_err(); + let components = ErrorComponents::from(lua_error); + + assert_eq!(components.messages()[0], "additional context"); + assert_eq!(components.messages()[1], "oh no, a runtime error"); + } + + #[test] + fn preserves_levels() { + // NOTE: The behavior in mlua is to preserve a single level of context + // and not all levels (context gets replaced on each call to `context`) + let lua_error = new_lua_result() + .context("level 1") + .context("level 2") + .context("level 3") + .unwrap_err(); + let components = ErrorComponents::from(lua_error); + + assert_eq!( + components.messages(), + &["level 3", "oh no, a runtime error"] + ); + } +} + +// Tests for error components struct: separated messages + stack trace +mod error_components { + use super::*; + + #[test] + fn message() { + let lua_error = new_lua_result().unwrap_err(); + let components = ErrorComponents::from(lua_error); + + assert_eq!(components.messages()[0], "oh no, a runtime error"); + } + + #[test] + fn stack_begin_end() { + let lua_error = new_lua_result().unwrap_err(); + let formatted = format!("{}", ErrorComponents::from(lua_error)); + + assert!(formatted.contains("Stack Begin")); + assert!(formatted.contains("Stack End")); + } + + #[test] + fn stack_lines() { + let lua_error = new_lua_result().unwrap_err(); + let components = ErrorComponents::from(lua_error); + + let mut lines = components.trace().unwrap().lines().iter(); + let line_1 = lines.next().unwrap().to_string(); + let line_2 = lines.next().unwrap().to_string(); + assert!(lines.next().is_none()); + + assert_eq!(line_1, "Script '[C]' - function 'f'"); + assert_eq!(line_2, "Script 'chunk_name', Line 1"); + } +} diff --git a/crates/lune-utils/src/fmt/label.rs b/crates/lune-utils/src/fmt/label.rs new file mode 100644 index 0000000..5e2e290 --- /dev/null +++ b/crates/lune-utils/src/fmt/label.rs @@ -0,0 +1,66 @@ +use std::fmt; + +use console::{style, Color}; + +/** + Label enum used for consistent output formatting throughout Lune. + + # Example usage + + ```rs + use lune_utils::fmt::Label; + + println!("{} This is an info message", Label::Info); + // [INFO] This is an info message + + println!("{} This is a warning message", Label::Warn); + // [WARN] This is a warning message + + println!("{} This is an error message", Label::Error); + // [ERROR] This is an error message + ``` +*/ +#[derive(Debug, Clone, Copy)] +pub enum Label { + Info, + Warn, + Error, +} + +impl Label { + /** + Returns the name of the label in all uppercase. + */ + #[must_use] + pub fn name(&self) -> &str { + match self { + Self::Info => "INFO", + Self::Warn => "WARN", + Self::Error => "ERROR", + } + } + + /** + Returns the color of the label. + */ + #[must_use] + pub fn color(&self) -> Color { + match self { + Self::Info => Color::Blue, + Self::Warn => Color::Yellow, + Self::Error => Color::Red, + } + } +} + +impl fmt::Display for Label { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}{}{}", + style("[").dim(), + style(self.name()).fg(self.color()), + style("]").dim() + ) + } +} diff --git a/crates/lune-utils/src/fmt/mod.rs b/crates/lune-utils/src/fmt/mod.rs new file mode 100644 index 0000000..0011feb --- /dev/null +++ b/crates/lune-utils/src/fmt/mod.rs @@ -0,0 +1,7 @@ +mod error; +mod label; +mod value; + +pub use self::error::{ErrorComponents, StackTrace, StackTraceLine, StackTraceSource}; +pub use self::label::Label; +pub use self::value::{pretty_format_multi_value, pretty_format_value, ValueFormatConfig}; diff --git a/crates/lune-utils/src/fmt/value/basic.rs b/crates/lune-utils/src/fmt/value/basic.rs new file mode 100644 index 0000000..cc4f9fb --- /dev/null +++ b/crates/lune-utils/src/fmt/value/basic.rs @@ -0,0 +1,74 @@ +use mlua::prelude::*; + +use super::{ + metamethods::{call_table_tostring_metamethod, call_userdata_tostring_metamethod}, + style::{COLOR_CYAN, COLOR_GREEN, COLOR_MAGENTA, COLOR_YELLOW}, +}; + +const STRING_REPLACEMENTS: &[(&str, &str)] = + &[("\"", r#"\""#), ("\t", r"\t"), ("\r", r"\r"), ("\n", r"\n")]; + +/** + Tries to return the given value as a plain string key. + + A plain string key must: + + - Start with an alphabetic character. + - Only contain alphanumeric characters and underscores. +*/ +pub(crate) fn lua_value_as_plain_string_key(value: &LuaValue) -> Option { + if let LuaValue::String(s) = value { + if let Ok(s) = s.to_str() { + let first_valid = s.chars().next().is_some_and(|c| c.is_ascii_alphabetic()); + let all_valid = s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'); + if first_valid && all_valid { + return Some(s.to_string()); + } + } + } + None +} + +/** + Formats a Lua value into a pretty string. + + This does not recursively format tables. +*/ +pub(crate) fn format_value_styled(value: &LuaValue, prefer_plain: bool) -> String { + match value { + LuaValue::Nil => COLOR_YELLOW.apply_to("nil").to_string(), + LuaValue::Boolean(true) => COLOR_YELLOW.apply_to("true").to_string(), + LuaValue::Boolean(false) => COLOR_YELLOW.apply_to("false").to_string(), + LuaValue::Number(n) => COLOR_CYAN.apply_to(n).to_string(), + LuaValue::Integer(i) => COLOR_CYAN.apply_to(i).to_string(), + LuaValue::String(s) if prefer_plain => s.to_string_lossy().to_string(), + LuaValue::String(s) => COLOR_GREEN + .apply_to({ + let mut s = s.to_string_lossy().to_string(); + for (from, to) in STRING_REPLACEMENTS { + s = s.replace(from, to); + } + format!(r#""{s}""#) + }) + .to_string(), + LuaValue::Vector(_) => COLOR_MAGENTA.apply_to("").to_string(), + LuaValue::Thread(_) => COLOR_MAGENTA.apply_to("").to_string(), + LuaValue::Function(_) => COLOR_MAGENTA.apply_to("").to_string(), + LuaValue::LightUserData(_) => COLOR_MAGENTA.apply_to("").to_string(), + LuaValue::UserData(u) => { + if let Some(s) = call_userdata_tostring_metamethod(u) { + s + } else { + COLOR_MAGENTA.apply_to("").to_string() + } + } + LuaValue::Table(t) => { + if let Some(s) = call_table_tostring_metamethod(t) { + s + } else { + COLOR_MAGENTA.apply_to("").to_string() + } + } + _ => COLOR_MAGENTA.apply_to("").to_string(), + } +} diff --git a/crates/lune-utils/src/fmt/value/config.rs b/crates/lune-utils/src/fmt/value/config.rs new file mode 100644 index 0000000..ab1e950 --- /dev/null +++ b/crates/lune-utils/src/fmt/value/config.rs @@ -0,0 +1,48 @@ +/** + Configuration for formatting values. +*/ +#[derive(Debug, Clone, Copy)] +pub struct ValueFormatConfig { + pub(super) max_depth: usize, + pub(super) colors_enabled: bool, +} + +impl ValueFormatConfig { + /** + Creates a new config with default values. + */ + #[must_use] + pub const fn new() -> Self { + Self { + max_depth: 3, + colors_enabled: false, + } + } + + /** + Sets the maximum depth to which tables will be formatted. + */ + #[must_use] + pub const fn with_max_depth(self, max_depth: usize) -> Self { + Self { max_depth, ..self } + } + + /** + Sets whether colors should be enabled. + + Colors are disabled by default. + */ + #[must_use] + pub const fn with_colors_enabled(self, colors_enabled: bool) -> Self { + Self { + colors_enabled, + ..self + } + } +} + +impl Default for ValueFormatConfig { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/lune-utils/src/fmt/value/metamethods.rs b/crates/lune-utils/src/fmt/value/metamethods.rs new file mode 100644 index 0000000..8b00b1a --- /dev/null +++ b/crates/lune-utils/src/fmt/value/metamethods.rs @@ -0,0 +1,29 @@ +use mlua::prelude::*; + +pub 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, + } +} + +pub fn call_userdata_tostring_metamethod<'a>(tab: &'a LuaAnyUserData<'a>) -> Option { + let f = match tab.get_metatable() { + Err(_) => None, + Ok(meta) => match meta.get::(LuaMetaMethod::ToString.name()) { + Ok(method) => Some(method), + Err(_) => None, + }, + }?; + match f.call::<_, String>(()) { + Ok(res) => Some(res), + Err(_) => None, + } +} diff --git a/crates/lune-utils/src/fmt/value/mod.rs b/crates/lune-utils/src/fmt/value/mod.rs new file mode 100644 index 0000000..3a3e310 --- /dev/null +++ b/crates/lune-utils/src/fmt/value/mod.rs @@ -0,0 +1,65 @@ +use std::{ + collections::HashSet, + sync::{Arc, Mutex}, +}; + +use console::{colors_enabled as get_colors_enabled, set_colors_enabled}; +use mlua::prelude::*; +use once_cell::sync::Lazy; + +mod basic; +mod config; +mod metamethods; +mod recursive; +mod style; + +use self::recursive::format_value_recursive; + +pub use self::config::ValueFormatConfig; + +// NOTE: Since the setting for colors being enabled is global, +// and these functions may be called in parallel, we use this global +// lock to make sure that we don't mess up the colors for other threads. +static COLORS_LOCK: Lazy>> = Lazy::new(|| Arc::new(Mutex::new(()))); + +/** + Formats a Lua value into a pretty string using the given config. +*/ +#[must_use] +#[allow(clippy::missing_panics_doc)] +pub fn pretty_format_value(value: &LuaValue, config: &ValueFormatConfig) -> String { + let _guard = COLORS_LOCK.lock().unwrap(); + + let were_colors_enabled = get_colors_enabled(); + set_colors_enabled(were_colors_enabled && config.colors_enabled); + + let mut visited = HashSet::new(); + let res = format_value_recursive(value, config, &mut visited, 0); + + set_colors_enabled(were_colors_enabled); + res.expect("using fmt for writing into strings should never fail") +} + +/** + Formats a Lua multi-value into a pretty string using the given config. + + Each value will be separated by a space. +*/ +#[must_use] +#[allow(clippy::missing_panics_doc)] +pub fn pretty_format_multi_value(values: &LuaMultiValue, config: &ValueFormatConfig) -> String { + let _guard = COLORS_LOCK.lock().unwrap(); + + let were_colors_enabled = get_colors_enabled(); + set_colors_enabled(were_colors_enabled && config.colors_enabled); + + let mut visited = HashSet::new(); + let res = values + .into_iter() + .map(|value| format_value_recursive(value, config, &mut visited, 0)) + .collect::, _>>(); + + set_colors_enabled(were_colors_enabled); + res.expect("using fmt for writing into strings should never fail") + .join(" ") +} diff --git a/crates/lune-utils/src/fmt/value/recursive.rs b/crates/lune-utils/src/fmt/value/recursive.rs new file mode 100644 index 0000000..7dfbef7 --- /dev/null +++ b/crates/lune-utils/src/fmt/value/recursive.rs @@ -0,0 +1,89 @@ +use std::collections::HashSet; +use std::fmt::{self, Write as _}; + +use mlua::prelude::*; + +use super::{ + basic::{format_value_styled, lua_value_as_plain_string_key}, + config::ValueFormatConfig, + style::STYLE_DIM, +}; + +const INDENT: &str = " "; + +/** + Representation of a pointer in memory to a Lua value. +*/ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct LuaValueId(usize); + +impl From<&LuaValue<'_>> for LuaValueId { + fn from(value: &LuaValue<'_>) -> Self { + Self(value.to_pointer() as usize) + } +} + +impl From<&LuaTable<'_>> for LuaValueId { + fn from(table: &LuaTable) -> Self { + Self(table.to_pointer() as usize) + } +} + +/** + Formats the given value, recursively formatting tables + up to the maximum depth specified in the config. + + NOTE: We return a result here but it's really just to make handling + of the `write!` calls easier. Writing into a string should never fail. +*/ +pub(crate) fn format_value_recursive( + value: &LuaValue, + config: &ValueFormatConfig, + visited: &mut HashSet, + depth: usize, +) -> Result { + let mut buffer = String::new(); + + if let LuaValue::Table(ref t) = value { + if depth >= config.max_depth { + write!(buffer, "{}", STYLE_DIM.apply_to("{ ... }"))?; + } else if !visited.insert(LuaValueId::from(t)) { + write!(buffer, "{}", STYLE_DIM.apply_to("{ recursive }"))?; + } else { + writeln!(buffer, "{}", STYLE_DIM.apply_to("{"))?; + + for res in t.clone().pairs::() { + let (key, value) = res.expect("conversion to LuaValue should never fail"); + let formatted = if let Some(plain_key) = lua_value_as_plain_string_key(&key) { + format!( + "{}{plain_key} {} {}{}", + INDENT.repeat(1 + depth), + STYLE_DIM.apply_to("="), + format_value_recursive(&value, config, visited, depth + 1)?, + STYLE_DIM.apply_to(","), + ) + } else { + format!( + "{}{}{}{} {} {}{}", + INDENT.repeat(1 + depth), + STYLE_DIM.apply_to("["), + format_value_recursive(&key, config, visited, depth + 1)?, + STYLE_DIM.apply_to("]"), + STYLE_DIM.apply_to("="), + format_value_recursive(&value, config, visited, depth + 1)?, + STYLE_DIM.apply_to(","), + ) + }; + buffer.push_str(&formatted); + } + + visited.remove(&LuaValueId::from(t)); + write!(buffer, "\n{}", STYLE_DIM.apply_to("}"))?; + } + } else { + let prefer_plain = depth == 0; + write!(buffer, "{}", format_value_styled(value, prefer_plain))?; + } + + Ok(buffer) +} diff --git a/crates/lune-utils/src/fmt/value/style.rs b/crates/lune-utils/src/fmt/value/style.rs new file mode 100644 index 0000000..0a4dbe4 --- /dev/null +++ b/crates/lune-utils/src/fmt/value/style.rs @@ -0,0 +1,9 @@ +use console::Style; +use once_cell::sync::Lazy; + +pub static COLOR_GREEN: Lazy