Compare commits

...

111 commits
v0.8.6 ... main

Author SHA1 Message Date
Filip Tibell
df56cd58e7
Github does not like requests from github to github, remove it from https test 2025-05-02 22:33:28 +02:00
Filip Tibell
66e3b58cd7
Url and uri are not the same 2025-05-02 21:57:13 +02:00
Filip Tibell
fb33d1812d
Remove old unused app data 2025-05-02 12:31:58 +02:00
Filip Tibell
0ddaaaefb5
Update changelog 2025-05-02 12:29:46 +02:00
Filip Tibell
2e5b3bb5eb
Fix panicking during require because of long lived require context borrow 2025-05-02 12:27:20 +02:00
Filip Tibell
6645631c46
Properly store process args and env as part of runtime initialization instead of in std-process 2025-05-02 12:18:53 +02:00
Filip Tibell
120048ae95
Update changelog 2025-05-01 21:12:54 +02:00
Filip Tibell
2d8e58b028
Revamp handling of process args and env with fully featured newtypes 2025-05-01 21:08:58 +02:00
Filip Tibell
b1fc60023d
Add release date to changelog 2025-04-30 15:40:39 +02:00
Filip Tibell
1429450a64
Version 0.9.2 2025-04-30 15:40:08 +02:00
Filip Tibell
d2a89f41c8
Fixed https support in net client + update changelog 2025-04-30 15:28:29 +02:00
Filip Tibell
9c9b90d70d
Final optimizations and dependency cleanup for mlua-luau-scheduler 2025-04-30 14:54:10 +02:00
Filip Tibell
d425d2568a
Final optimizations for future and thread queues 2025-04-30 14:47:16 +02:00
Filip Tibell
4c2bbcf425
Optimize thread queue storage for spawned and deferred threads 2025-04-30 14:21:48 +02:00
Filip Tibell
461ca24c33
Implement optimized event listener for thread queues 2025-04-30 14:10:35 +02:00
Filip Tibell
7fd390dead
Organize queue-related files a bit better 2025-04-30 13:53:43 +02:00
Filip Tibell
c35eaa7899
Organize thread-related files a bit better 2025-04-30 13:39:35 +02:00
Filip Tibell
b57fa6fad3
Optimize tracking of thread results in mlua-luau-scheduler 2025-04-30 13:35:42 +02:00
Filip Tibell
3e80a0a1c4
Reduce overhead of lua result tracking 2025-04-29 23:31:10 +02:00
Filip Tibell
ac8c809a20
Implement zero-copy hyper body type that wraps over lua values 2025-04-29 23:00:03 +02:00
Filip Tibell
4079842a33
Re enable crate publishing in release workflow 2025-04-29 16:00:59 +02:00
Filip Tibell
464c431697
Version 0.9.1 2025-04-29 15:59:49 +02:00
Filip Tibell
39f6319bdb
Bye bye tokio 2025-04-29 15:38:04 +02:00
Filip Tibell
62910f02ab
Rewrite the net standard library with smol ecosystem of crates (#310) 2025-04-29 15:06:16 +02:00
Sasial
1f43ff89f7
RuntimeReturnValues should derive Debug (#309) 2025-04-26 22:51:17 +02:00
Filip Tibell
e234eab813
Make pretext in latest changelog entry clearer 2025-04-25 16:43:12 +02:00
Filip Tibell
fc12e15a9a
Add missing error-send feature to mlua in lune-std-serde 2025-04-25 16:29:00 +02:00
Filip Tibell
c3f483b3dd
Fix category in mlua-luau-scheduler 2025-04-25 16:26:13 +02:00
Filip Tibell
98e68629c9
Add release date to changelog 2025-04-25 16:23:56 +02:00
Filip Tibell
0954730646
Try actually writing the stdin to stdout in powershell 2025-04-25 15:53:07 +02:00
Filip Tibell
27409f1bf4
Try to use dotnet console class instead for stdin test 2025-04-25 15:39:35 +02:00
Filip Tibell
c13728fce4
Okay lets try poweshell again 2025-04-25 15:02:12 +02:00
Filip Tibell
7cbeafc5b3
Enforce bash in exec stdin test instead of trying to use powershell 2025-04-25 13:53:41 +02:00
Filip Tibell
5a23cf04fa
Attempt to fix process exec test on windows 2025-04-25 13:42:15 +02:00
Filip Tibell
f6d2afb003
Improve process exec stdin test 2025-04-25 13:03:22 +02:00
Filip Tibell
b74568eea0
Bump all crate versions 2025-04-25 12:49:58 +02:00
Filip Tibell
387080cf45
Make the final breaking change with net json and update changelog 2025-04-25 12:43:40 +02:00
Filip Tibell
1119f0d46b
Remove legacy stdin option in favor of full stdio options on process exec + adjust tests accordingly 2025-04-24 22:14:03 +02:00
Filip Tibell
551120fba1
Pass rest args plainly to process.args when using lune run 2025-04-24 22:01:00 +02:00
Filip Tibell
88494fedcb
Migrate lune-std to use async-fs and friends 2025-04-24 21:52:07 +02:00
Filip Tibell
0a3b57697d
Migrate lune-std-fs to use async-fs instead of tokio 2025-04-24 21:47:40 +02:00
Filip Tibell
43f11cd9f5
Disable exact compression tests and write about why 2025-04-24 21:43:41 +02:00
Filip Tibell
c163c6f157
Fix __tostring metamethods sometimes not being respected properly 2025-04-24 21:31:03 +02:00
Filip Tibell
4ce2d3a80f
Make runtime return values into a non exhaustive struct to prevent future breakage 2025-04-24 21:15:46 +02:00
Filip Tibell
74375ff708
Implement self alias for module requires 2025-04-24 21:02:16 +02:00
Filip Tibell
a673f80c95
Start working on getting require semantics up to date 2025-04-24 20:38:10 +02:00
Filip Tibell
acd0f126e5
Make sure all test files use proper requires 2025-04-24 20:32:55 +02:00
Filip Tibell
0d653450de
Update dependencies across all crates 2025-04-24 19:58:05 +02:00
Filip Tibell
b147e83d3c
Bundle each typedefs file with its respective lune-std crate to fix publishing and cargo-binstall issues 2025-04-24 19:49:20 +02:00
Filip Tibell
58a65afc2c
Write tracing output to stderr instead of stdout 2025-04-24 19:22:56 +02:00
Filip Tibell
b1d60d904c
Run stylua 2025-04-24 18:54:19 +02:00
Filip Tibell
6b45f44d1f
Implement readLine in lune-std-stdio 2025-04-24 18:53:17 +02:00
Filip Tibell
c4374a0e18
Complete migration of lune-std-process to use async-process instead of tokio 2025-04-24 16:28:34 +02:00
Filip Tibell
8059026251
Start migrating lune-std-process to use async-process instead of tokio 2025-04-24 13:16:50 +02:00
Filip Tibell
2a701e919c
Migrate lune-std-serde to async-io instead of tokio 2025-04-23 23:28:33 +02:00
Filip Tibell
aeaebf4290
Migrate lune-std-stdio to async-io instead of tokio 2025-04-23 23:22:35 +02:00
Filip Tibell
54115430f5
Migrate lune-std-task to use async-io instead of tokio for better scheduler integration + remove temp test file 2025-04-23 22:17:21 +02:00
Filip Tibell
524a5c0777
Remove unused tokio dependency from lune-utils 2025-04-23 19:29:08 +02:00
Filip Tibell
c091b05f6c
Guarantee that wait/sleep functions always yield in both task library and scheduler examples 2025-04-23 19:18:08 +02:00
Filip Tibell
8ffbb328f3
Enforce some more consistency with new datetime changes + add deprecation warning to iso methods 2025-04-23 16:01:12 +02:00
Filip Tibell
d6f4cb289e
Remove self_cell dependency from main crate (regex still needs it) 2025-04-23 15:27:28 +02:00
Filip Tibell
6252bc218e
Migrate to mlua 0.10 2025-04-23 15:20:53 +02:00
Filip Tibell
ad8822c1e7
Remove re-export of lune roblox crate in main crate 2025-04-23 13:21:25 +02:00
Filip Tibell
83fbcbb255
Remove once_cell dependency 2025-04-23 13:20:56 +02:00
Filip Tibell
02fb2dc662
Fix all new clippy lints 2025-04-23 13:19:19 +02:00
Ryan
dd7f6d613b
Add support for RFC 2822 in DateTime (#285) 2025-04-23 13:09:11 +02:00
Micah
27e3efca97
Implement Enum attributes (#306) 2025-04-03 22:14:24 +02:00
Micah
8bd1a9b77d
Implement support for Roblox's Content type (#305) 2025-04-03 20:40:08 +02:00
Micah
bb8c4bce82
Update rbx-dom dependencies (#304) 2025-04-02 23:10:56 +02:00
Filip Tibell
6902ecaa7c
Fix various new clippy lints 2025-03-24 19:44:14 +01:00
dai
dc08b91314
Fix deadlock in stdio.format calls in tostring metamethod (#288) 2025-03-24 19:34:51 +01:00
Micah
822dd19393
Add functions for getting Roblox Studio locations to roblox library (#284) 2025-03-24 19:29:22 +01:00
6cd0234a5f
Allow toggling JIT in the CLI (#265) 2025-03-24 19:26:02 +01:00
Micah
19e7f57284
Loosen Lune version string requirements (#294) 2025-03-24 19:24:36 +01:00
Qwreey
5d1401cdf6
Add process.endianness constant (#267) 2024-11-05 13:10:05 +01:00
Sasial
91af86cca2
IsA, ClassName & Parent should work if an instance is already destroyed (#271) 2024-11-05 13:02:15 +01:00
Filip Tibell
c935149c1e
Update dependencies 2024-10-17 11:43:51 +02:00
Filip Tibell
e5bda57665
Document new breaking changes in changelog 2024-10-17 11:43:13 +02:00
Filip Tibell
ef294f207c
Fix websocket example files 2024-10-17 11:27:32 +02:00
Filip Tibell
f89d02a60d
Use 4 spaces for error formatting indentation 2024-10-17 11:26:01 +02:00
Filip Tibell
d090cd2420
Remove redundant stack trace information in error formatter 2024-10-17 11:23:20 +02:00
Filip Tibell
99c17795c1
Update rokit action version and tool versions 2024-10-17 09:26:13 +02:00
Filip Tibell
138221b93e
Update websocket tests and types to use new calling convention 2024-10-16 22:00:33 +02:00
Filip Tibell
8abfc21181
Use standard method calling conventions for websockets 2024-10-16 21:55:53 +02:00
309c461e11
Implement a non-blocking child process interface (#211) 2024-10-16 21:48:12 +02:00
Filip Tibell
93fa14d832
Revert some unnecessary stylistic changes 2024-10-16 21:41:16 +02:00
df4fb9be91
Make Runtime::run Return Lua Values (#178) 2024-10-16 21:35:23 +02:00
eaac9ff53a
Migrate to Rokit as toolchain manager (#238) 2024-10-16 21:06:14 +02:00
Eli
0d2f5539b6
Add Moonwave comments for DateTime properties. (#248) 2024-10-16 21:03:58 +02:00
howmanysmall
0f4cac29aa
Fix Regex types (#250) 2024-10-16 21:03:00 +02:00
Filip Tibell
010cd36375
Version 0.8.9 2024-10-07 19:34:55 +02:00
Filip Tibell
c17da72815
Update dependencies 2024-10-07 19:33:59 +02:00
Filip Tibell
ff83c401b8
Version 0.8.8 2024-08-22 21:30:36 +02:00
Kenneth Loeffler
a007fa94a6
Update all rbx-dom dependencies to their latest versions (#245) 2024-08-22 21:24:32 +02:00
Filip Tibell
1d4d1635eb
Temporarily disable publish to crates.io in workflow 2024-08-10 13:36:19 +02:00
Filip Tibell
56f08a88aa
Fix new clippy lints 2024-08-10 13:34:13 +02:00
Filip Tibell
833d0e244b
Add version and date to changelog 2024-08-10 13:25:55 +02:00
Filip Tibell
3e09807638
Bump all crate versions 2024-08-10 13:07:56 +02:00
Filip Tibell
ea7013322f
Fix type and tostring metamethods not always being respected during table formatting 2024-08-10 13:01:13 +02:00
Filip Tibell
98b31b9f67
Update tooling 2024-08-10 12:48:51 +02:00
Filip Tibell
8364a8e4de
We no longer use selene 2024-08-10 12:48:27 +02:00
Filip Tibell
473ad80e8f
Update dependencies 2024-08-10 12:47:36 +02:00
Filip Tibell
180d20ce4a
Update changelog with missing PRs 2024-08-10 12:28:26 +02:00
Filip Tibell
b585234b08
Clarify some comments and expose more instance functions in lune-roblox 2024-07-21 23:50:55 +02:00
ZachCurtis
5379c79488
Add missing vector methods (#228) 2024-07-21 23:35:58 +02:00
Nick Winans
8aefe88104
Add compression level option to serde.compress (#224) 2024-07-06 22:38:35 +02:00
Maxwell Ruben
cb552af660
Fix readDir with trailing forward-slash on Windows (#220) 2024-07-06 22:34:12 +02:00
Filip Tibell
95c2ca0965
Fix mixed indentation in regex documentation comment 2024-06-23 14:56:31 +02:00
Filip Tibell
5167a71e6f
Improve documentation comments for serde library 2024-06-23 14:53:32 +02:00
Filip Tibell
eac34d2e7e
Re-enable crates.io publish step in release workflow 2024-06-23 14:40:33 +02:00
Filip Tibell
ff80981282
Add missing changelog entry 2024-06-23 14:39:58 +02:00
286 changed files with 8792 additions and 4996 deletions

View file

@ -2,15 +2,16 @@ name: CI
on:
push:
pull_request:
workflow_dispatch:
defaults:
run:
shell: bash
jobs:
env:
CARGO_TERM_COLOR: always
jobs:
fmt:
name: Check formatting
runs-on: ubuntu-latest
@ -23,11 +24,8 @@ jobs:
with:
components: rustfmt
- name: Install Just
uses: extractions/setup-just@v2
- name: Install Tooling
uses: ok-nick/setup-aftman@v0.4.2
uses: CompeyDev/setup-rokit@v0.1.2
- name: Check Formatting
run: just fmt-check
@ -40,11 +38,8 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Just
uses: extractions/setup-just@v2
- name: Install Tooling
uses: ok-nick/setup-aftman@v0.4.2
uses: CompeyDev/setup-rokit@v0.1.2
- name: Analyze
run: just analyze
@ -85,23 +80,26 @@ jobs:
components: clippy
targets: ${{ matrix.cargo-target }}
- name: Install binstall
uses: cargo-bins/cargo-binstall@main
- name: Install nextest
run: cargo binstall cargo-nextest
- name: Build
run: |
cargo build \
--workspace \
cargo build --workspace \
--locked --all-features \
--target ${{ matrix.cargo-target }}
- name: Lint
run: |
cargo clippy \
--workspace \
cargo clippy --workspace \
--locked --all-features \
--target ${{ matrix.cargo-target }}
- name: Test
run: |
cargo test \
--lib --workspace \
cargo nextest run --no-fail-fast \
--locked --all-features \
--target ${{ matrix.cargo-target }}

View file

@ -27,23 +27,23 @@ jobs:
file: crates/lune/Cargo.toml
field: package.version
# dry-run:
# name: Dry-run
# needs: ["init"]
# runs-on: ubuntu-latest
# steps:
# - name: Checkout repository
# uses: actions/checkout@v4
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: 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 }}
- 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"]
@ -139,20 +139,20 @@ jobs:
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
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: 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 }}
- name: Publish crates
uses: katyo/publish-crates@v2
with:
dry-run: false
check-repo: true
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}

5
.gitignore vendored
View file

@ -21,7 +21,12 @@ lune.yml
luneDocs.json
luneTypes.d.luau
# Dirs generated by runtime or build scripts
/types
# Files generated by runtime or build scripts
scripts/brick_color.rs
scripts/font_enum_map.rs
scripts/physical_properties_enum_map.rs

View file

@ -45,7 +45,7 @@ test-bin *ARGS:
fmt:
#!/usr/bin/env bash
set -euo pipefail
stylua .lune scripts tests types \
stylua .lune crates scripts tests \
--glob "tests/**/*.luau" \
--glob "!tests/roblox/rbx-test-files/**"
cargo fmt
@ -55,7 +55,7 @@ fmt:
fmt-check:
#!/usr/bin/env bash
set -euo pipefail
stylua .lune scripts tests types \
stylua .lune crates scripts tests \
--glob "tests/**/*.luau" \
--glob "!tests/roblox/rbx-test-files/**"
cargo fmt --check
@ -65,10 +65,11 @@ fmt-check:
analyze:
#!/usr/bin/env bash
set -euo pipefail
lune run scripts/analyze_copy_typedefs
luau-lsp analyze \
--settings=".vscode/settings.json" \
--ignore="tests/roblox/rbx-test-files/**" \
.lune scripts tests types
.lune crates scripts tests
# Zips up the built binary into a single zip file
[no-exit-message]

View file

@ -1,6 +1,7 @@
local fs = require("@lune/fs")
local net = require("@lune/net")
local process = require("@lune/process")
local serde = require("@lune/serde")
local stdio = require("@lune/stdio")
local task = require("@lune/task")
@ -129,7 +130,7 @@ end
]]
print("Sending 4 pings to google 🌏")
local result = process.spawn("ping", {
local result = process.exec("ping", {
"google.com",
"-c 4",
})
@ -145,10 +146,8 @@ local result = process.spawn("ping", {
if result.ok then
assert(#result.stdout > 0, "Result output was empty")
local min, avg, max, stddev = string.match(
result.stdout,
"min/avg/max/stddev = ([%d%.]+)/([%d%.]+)/([%d%.]+)/([%d%.]+) ms"
)
local min, avg, max, stddev =
string.match(result.stdout, "min/avg/max/stddev = ([%d%.]+)/([%d%.]+)/([%d%.]+)/([%d%.]+) ms")
print(string.format("Minimum ping time: %.3fms", assert(tonumber(min))))
print(string.format("Maximum ping time: %.3fms", assert(tonumber(max))))
print(string.format("Average ping time: %.3fms", assert(tonumber(avg))))
@ -172,7 +171,7 @@ local apiResult = net.request({
headers = {
["Content-Type"] = "application/json",
} :: { [string]: string },
body = net.jsonEncode({
body = serde.encode("json", {
title = "foo",
body = "bar",
}),
@ -192,7 +191,7 @@ type ApiResponse = {
userId: number,
}
local apiResponse: ApiResponse = net.jsonDecode(apiResult.body)
local apiResponse: ApiResponse = serde.decode("json", apiResult.body)
assert(apiResponse.title == "foo", "Invalid json response")
assert(apiResponse.body == "bar", "Invalid json response")
print("Got valid JSON response with changes applied")

View file

@ -3,7 +3,6 @@
local net = require("@lune/net")
local process = require("@lune/process")
local task = require("@lune/task")
local PORT = if process.env.PORT ~= nil and #process.env.PORT > 0
then assert(tonumber(process.env.PORT), "Failed to parse port from env")
@ -11,6 +10,10 @@ local PORT = if process.env.PORT ~= nil and #process.env.PORT > 0
-- Create our responder functions
local function root(_request: net.ServeRequest): string
return `Hello from Lune server!`
end
local function pong(request: net.ServeRequest): string
return `Pong!\n{request.path}\n{request.body}`
end
@ -29,10 +32,12 @@ local function notFound(_request: net.ServeRequest): net.ServeResponse
}
end
-- Run the server on port 8080
-- Run the server on the port forever
local handle = net.serve(PORT, function(request)
if string.sub(request.path, 1, 5) == "/ping" then
net.serve(PORT, function(request)
if request.path == "/" then
return root(request)
elseif string.sub(request.path, 1, 5) == "/ping" then
return pong(request)
elseif string.sub(request.path, 1, 7) == "/teapot" then
return teapot(request)
@ -42,12 +47,4 @@ local handle = net.serve(PORT, function(request)
end)
print(`Listening on port {PORT} 🚀`)
-- Exit our example after a small delay, if you copy this
-- example just remove this part to keep the server running
task.delay(2, function()
print("Shutting down...")
task.wait(1)
handle.stop()
end)
print("Press Ctrl+C to stop")

View file

@ -28,8 +28,8 @@ end)
for _ = 1, 5 do
local start = os.clock()
socket.send(tostring(1))
local response = socket.next()
socket:send(tostring(1))
local response = socket:next()
local elapsed = os.clock() - start
print(`Got response '{response}' in {elapsed * 1_000} milliseconds`)
task.wait(1 - elapsed)
@ -38,7 +38,7 @@ end
-- Everything went well, and we are done with the socket, so we can close it
print("Closing web socket...")
socket.close()
socket:close()
task.cancel(forceExit)
print("Done! 🌙")

View file

@ -15,9 +15,9 @@ local handle = net.serve(PORT, {
handleWebSocket = function(socket)
print("Got new web socket connection!")
repeat
local message = socket.next()
local message = socket:next()
if message ~= nil then
socket.send("Echo - " .. message)
socket:send("Echo - " .. message)
end
until message == nil
print("Web socket disconnected.")

View file

@ -8,6 +8,176 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Added support for non-UTF8 strings in arguments to `process.exec` and `process.spawn`
### Changed
- Improved cross-platform compatibility and correctness for values in `process.args` and `process.env`, especially on Windows
### Fixed
- Fixed various crashes during require that had the error `cannot mutably borrow app data container`
## `0.9.2` - April 30th, 2025
### Changed
- Improved performance of `net.request` and `net.serve` when handling large request bodies
- Improved performance and memory usage of `task.spawn`, `task.defer`, and `task.delay`
### Fixed
- Fixed accidental breakage of `net.request` in version `0.9.1`
## `0.9.1` - April 29th, 2025
### Added
- Added support for automatic decompression of HTTP requests in `net.serve` ([#310])
### Fixed
- Fixed `net.serve` no longer serving requests if the returned `ServeHandle` is discarded ([#310])
- Fixed `net.serve` having various performance issues ([#310])
- Fixed Lune still running after cancelling a task such as `task.delay(5, ...)` and all tasks having completed
[#310]: https://github.com/lune-org/lune/pull/310
## `0.9.0` - April 25th, 2025
The next major version of Lune has finally been released!
This release has been a long time coming, and many breaking changes have been made.
If you are an existing Lune user upgrading to this version, you will **most likely** be affected.
The full list of breaking changes can be found on below.
### Breaking changes & additions
- The behavior of `require` has changed, according to the latest Luau RFCs and specifications.
For the full details, feel free to read documentation [here](https://github.com/luau-lang/rfcs), otherwise, the most notable changes here are:
- Paths passed to require must start with either `./`, `../` or `@` - require statements such as `require("foo")` **will now error** and must be changed to `require("./foo")`.
- The behavior of require from within `init.luau` and `init.lua` files has changed - previously `require("./foo")` would resolve
to the file or directory `foo` _as a **sibling** of the init file_, but will now resolve to the file or directory `foo` _which is a sibling of the **parent directory** of the init file_.
To require files inside of the same directory as the init file, the new `@self` alias must be used - like `require("@self/foo")`.
- The main `lune run` subcommand will no longer sink flags passed to it - `lune run --` will now *literally* pass the string `--` as the first
value in `process.args`, and `--` is no longer necessary to be able to pass flag arguments such as `--foo` and `-b` properly to your Lune programs.
- Two new process spawning functions - `process.create` and `process.exec` - replace the previous `process.spawn` API. ([#211])
To migrate from `process.spawn`, use the new `process.exec` API which retains the same behavior as the old function, with slight changes in how the `stdin` option is passed.
The new `process.create` function is a non-blocking process creation API and can be used to interactively
read and write to standard input and output streams of the child process.
```lua
local child = process.create("program", {
"first-argument",
"second-argument"
})
-- Writing to stdin
child.stdin:write("Hello from Lune!")
-- Reading partial data from stdout
local data = child.stdout:read()
print(data)
-- Reading the full stdout
local full = child.stdout:readToEnd()
print(full)
```
- Removed `net.jsonEncode` and `net.jsonDecode` - please use the equivalent `serde.encode("json", ...)` and `serde.decode("json", ...)` instead
- WebSocket methods in `net.socket` and `net.serve` now use standard Lua method calling convention and colon syntax.
This means `socket.send(...)` is now `socket:send(...)`, `socket.close(...)` is now `socket:close(...)`, and so on.
- Various changes have been made to the Lune Rust crates:
- `Runtime::run` now returns a more useful value instead of an `ExitCode` ([#178])
- All Lune standard library crates now export a `typedefs` function that returns the source code for the respective standard library module type definitions
- All Lune crates now depend on `mlua` version `0.10` or above
- Most Lune crates have been migrated to the `smol` and `async-*` ecosystem instead of `tokio`, with a full migration expected soon (this will not break public types)
- The `roblox` crate re-export has been removed from the main `lune` crate - please depend on `lune-roblox` crate directly instead
### Added
- Added functions for getting Roblox Studio locations to the `roblox` standard library ([#284])
- Added support for the `Content` datatype in the `roblox` standard library ([#305])
- Added support for `EnumItem` instance attributes in the `roblox` standard library ([#306])
- Added support for RFC 2822 dates in the `datetime` standard library using `fromRfc2822` ([#285]) - the `fromIsoDate`
function has also been deprecated (not removed yet) and `fromRfc3339` should instead be preferred for any new work.
- Added a `readLine` function to the `stdio` standard library for reading line-by-line from stdin.
- Added a way to disable JIT by setting the `LUNE_LUAU_JIT` environment variable to `false` before running Lune.
- Added `process.endianness` constant ([#267])
### Changed
- Documentation comments for several standard library properties have been improved ([#248], [#250])
- Error messages no longer contain redundant or duplicate stack trace information
- Updated to Luau version `0.663`
- Updated to rbx-dom database version `0.670`
### Fixed
- Fixed deadlock in `stdio.format` calls in `__tostring` metamethods ([#288])
- Fixed `task.wait` and `task.delay` not being guaranteed to yield when duration is set to zero or very small values
- Fixed `__tostring` metamethods sometimes not being respected in `print` and `stdio.format` calls
[#178]: https://github.com/lune-org/lune/pull/178
[#211]: https://github.com/lune-org/lune/pull/211
[#248]: https://github.com/lune-org/lune/pull/248
[#250]: https://github.com/lune-org/lune/pull/250
[#265]: https://github.com/lune-org/lune/pull/265
[#267]: https://github.com/lune-org/lune/pull/267
[#284]: https://github.com/lune-org/lune/pull/284
[#285]: https://github.com/lune-org/lune/pull/285
[#288]: https://github.com/lune-org/lune/pull/288
[#305]: https://github.com/lune-org/lune/pull/305
[#306]: https://github.com/lune-org/lune/pull/306
## `0.8.9` - October 7th, 2024
### Changed
- Updated to Luau version `0.640`
## `0.8.8` - August 22nd, 2024
### Fixed
- Fixed errors when deserializing `Lighting.AttributesSerialize` by updating `rbx-dom` dependencies ([#245])
[#245]: https://github.com/lune-org/lune/pull/245
## `0.8.7` - August 10th, 2024
### Added
- Added a compression level option to `serde.compress` ([#224])
- Added missing vector methods to the `roblox` library ([#228])
### Changed
- Updated to Luau version `0.635`
- Updated to rbx-dom database version `0.634`
### Fixed
- Fixed `fs.readDir` with trailing forward-slash on Windows ([#220])
- Fixed `__type` and `__tostring` metamethods not always being respected when formatting tables
[#220]: https://github.com/lune-org/lune/pull/220
[#224]: https://github.com/lune-org/lune/pull/224
[#228]: https://github.com/lune-org/lune/pull/228
## `0.8.6` - June 23rd, 2024
### Added
@ -37,6 +207,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Check the documentation for the `luau` standard library for more information.
- Implemented support for floor division operator / `__idiv` for the `Vector2` and `Vector3` types in the `roblox` standard library ([#196])
- Fixed the `_VERSION` global containing an incorrect Lune version string.
### Changed

2717
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,4 +0,0 @@
[tools]
luau-lsp = "JohnnyMorganz/luau-lsp@1.29.1"
selene = "Kampfkarren/selene@0.27.1"
stylua = "JohnnyMorganz/StyLua@0.20.0"

View file

@ -1,6 +1,6 @@
[package]
name = "lune-roblox"
version = "0.1.2"
version = "0.2.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
@ -13,17 +13,16 @@ path = "src/lib.rs"
workspace = true
[dependencies]
mlua = { version = "0.9.7", features = ["luau"] }
mlua = { version = "0.10.3", features = ["luau"] }
glam = "0.27"
rand = "0.8"
thiserror = "1.0"
once_cell = "1.17"
glam = "0.30"
rand = "0.9"
thiserror = "2.0"
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"
rbx_binary = "1.0"
rbx_dom_weak = "3.0"
rbx_reflection = "5.0"
rbx_reflection_database = "1.0"
rbx_xml = "1.0"
lune-utils = { version = "0.1.2", path = "../lune-utils" }
lune-utils = { version = "0.2.2", path = "../lune-utils" }

View file

@ -47,6 +47,7 @@ pub fn ensure_valid_attribute_value(value: &DomValue) -> LuaResult<()> {
| DomType::CFrame
| DomType::Color3
| DomType::ColorSequence
| DomType::EnumItem
| DomType::Float32
| DomType::Float64
| DomType::Font

View file

@ -6,7 +6,7 @@ use crate::{datatypes::extension::DomValueExt, instance::Instance};
use super::*;
pub(crate) trait LuaToDomValue<'lua> {
pub(crate) trait LuaToDomValue {
/**
Converts a lua value into a weak dom value.
@ -15,16 +15,16 @@ pub(crate) trait LuaToDomValue<'lua> {
*/
fn lua_to_dom_value(
&self,
lua: &'lua Lua,
lua: &Lua,
variant_type: Option<DomType>,
) -> DomConversionResult<DomValue>;
}
pub(crate) trait DomValueToLua<'lua>: Sized {
pub(crate) trait DomValueToLua: Sized {
/**
Converts a weak dom value into a lua value.
*/
fn dom_value_to_lua(lua: &'lua Lua, variant: &DomValue) -> DomConversionResult<Self>;
fn dom_value_to_lua(lua: &Lua, variant: &DomValue) -> DomConversionResult<Self>;
}
/*
@ -37,8 +37,8 @@ pub(crate) trait DomValueToLua<'lua>: Sized {
*/
impl<'lua> DomValueToLua<'lua> for LuaValue<'lua> {
fn dom_value_to_lua(lua: &'lua Lua, variant: &DomValue) -> DomConversionResult<Self> {
impl DomValueToLua for LuaValue {
fn dom_value_to_lua(lua: &Lua, variant: &DomValue) -> DomConversionResult<Self> {
use rbx_dom_weak::types as dom;
match LuaAnyUserData::dom_value_to_lua(lua, variant) {
@ -51,7 +51,7 @@ impl<'lua> DomValueToLua<'lua> for LuaValue<'lua> {
DomValue::Float32(n) => Ok(LuaValue::Number(*n as f64)),
DomValue::String(s) => Ok(LuaValue::String(lua.create_string(s)?)),
DomValue::BinaryString(s) => Ok(LuaValue::String(lua.create_string(s)?)),
DomValue::Content(s) => Ok(LuaValue::String(
DomValue::ContentId(s) => Ok(LuaValue::String(
lua.create_string(AsRef::<str>::as_ref(s))?,
)),
@ -76,10 +76,10 @@ impl<'lua> DomValueToLua<'lua> for LuaValue<'lua> {
}
}
impl<'lua> LuaToDomValue<'lua> for LuaValue<'lua> {
impl LuaToDomValue for LuaValue {
fn lua_to_dom_value(
&self,
lua: &'lua Lua,
lua: &Lua,
variant_type: Option<DomType>,
) -> DomConversionResult<DomValue> {
use rbx_dom_weak::types as dom;
@ -102,10 +102,10 @@ impl<'lua> LuaToDomValue<'lua> for LuaValue<'lua> {
Ok(DomValue::String(s.to_str()?.to_string()))
}
(LuaValue::String(s), DomType::BinaryString) => {
Ok(DomValue::BinaryString(s.as_ref().into()))
Ok(DomValue::BinaryString(s.as_bytes().to_vec().into()))
}
(LuaValue::String(s), DomType::Content) => {
Ok(DomValue::Content(s.to_str()?.to_string().into()))
(LuaValue::String(s), DomType::ContentId) => {
Ok(DomValue::ContentId(s.to_str()?.to_string().into()))
}
// NOTE: Some values are either optional or default and we
@ -186,9 +186,9 @@ macro_rules! userdata_to_dom {
};
}
impl<'lua> DomValueToLua<'lua> for LuaAnyUserData<'lua> {
impl DomValueToLua for LuaAnyUserData {
#[rustfmt::skip]
fn dom_value_to_lua(lua: &'lua Lua, variant: &DomValue) -> DomConversionResult<Self> {
fn dom_value_to_lua(lua: &Lua, variant: &DomValue) -> DomConversionResult<Self> {
use super::types::*;
use rbx_dom_weak::types as dom;
@ -200,6 +200,8 @@ impl<'lua> DomValueToLua<'lua> for LuaAnyUserData<'lua> {
DomValue::Color3(value) => dom_to_userdata!(lua, value => Color3),
DomValue::Color3uint8(value) => dom_to_userdata!(lua, value => Color3),
DomValue::ColorSequence(value) => dom_to_userdata!(lua, value => ColorSequence),
DomValue::Content(value) => dom_to_userdata!(lua, value => Content),
DomValue::EnumItem(value) => dom_to_userdata!(lua, value => EnumItem),
DomValue::Faces(value) => dom_to_userdata!(lua, value => Faces),
DomValue::Font(value) => dom_to_userdata!(lua, value => Font),
DomValue::NumberRange(value) => dom_to_userdata!(lua, value => NumberRange),
@ -233,13 +235,9 @@ impl<'lua> DomValueToLua<'lua> for LuaAnyUserData<'lua> {
}
}
impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> {
impl LuaToDomValue for LuaAnyUserData {
#[rustfmt::skip]
fn lua_to_dom_value(
&self,
_: &'lua Lua,
variant_type: Option<DomType>,
) -> DomConversionResult<DomValue> {
fn lua_to_dom_value(&self, _: &Lua, variant_type: Option<DomType>) -> DomConversionResult<DomValue> {
use super::types::*;
use rbx_dom_weak::types as dom;
@ -256,7 +254,8 @@ impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> {
DomType::Color3 => userdata_to_dom!(self as Color3 => dom::Color3),
DomType::Color3uint8 => userdata_to_dom!(self as Color3 => dom::Color3uint8),
DomType::ColorSequence => userdata_to_dom!(self as ColorSequence => dom::ColorSequence),
DomType::Enum => userdata_to_dom!(self as EnumItem => dom::Enum),
DomType::Content => userdata_to_dom!(self as Content => dom::Content),
DomType::EnumItem => userdata_to_dom!(self as EnumItem => dom::EnumItem),
DomType::Faces => userdata_to_dom!(self as Faces => dom::Faces),
DomType::Font => userdata_to_dom!(self as Font => dom::Font),
DomType::NumberRange => userdata_to_dom!(self as NumberRange => dom::NumberRange),
@ -276,13 +275,13 @@ impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> {
// NOTE: The none and default variants of these types are handled in
// LuaToDomValue for the LuaValue type instead, allowing for nil/default
DomType::OptionalCFrame => {
return match self.borrow::<CFrame>() {
match self.borrow::<CFrame>() {
Err(_) => unreachable!("Invalid use of conversion method, should be using LuaValue"),
Ok(value) => Ok(DomValue::OptionalCFrame(Some(dom::CFrame::from(*value)))),
}
}
DomType::PhysicalProperties => {
return match self.borrow::<PhysicalProperties>() {
match self.borrow::<PhysicalProperties>() {
Err(_) => unreachable!("Invalid use of conversion method, should be using LuaValue"),
Ok(value) => {
let props = dom::CustomPhysicalProperties::from(*value);
@ -314,7 +313,7 @@ impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> {
value if value.is::<CFrame>() => userdata_to_dom!(value as CFrame => dom::CFrame),
value if value.is::<Color3>() => userdata_to_dom!(value as Color3 => dom::Color3),
value if value.is::<ColorSequence>() => userdata_to_dom!(value as ColorSequence => dom::ColorSequence),
value if value.is::<Enum>() => userdata_to_dom!(value as EnumItem => dom::Enum),
value if value.is::<EnumItem>() => userdata_to_dom!(value as EnumItem => dom::EnumItem),
value if value.is::<Faces>() => userdata_to_dom!(value as Faces => dom::Faces),
value if value.is::<Font>() => userdata_to_dom!(value as Font => dom::Font),
value if value.is::<Instance>() => userdata_to_dom!(value as Instance => dom::Ref),

View file

@ -19,7 +19,9 @@ impl DomValueExt for DomType {
Color3uint8 => "Color3uint8",
ColorSequence => "ColorSequence",
Content => "Content",
ContentId => "ContentId",
Enum => "Enum",
EnumItem => "EnumItem",
Faces => "Faces",
Float32 => "Float32",
Float64 => "Float64",

View file

@ -21,11 +21,11 @@ pub struct Axes {
pub(crate) z: bool,
}
impl LuaExportsTable<'_> for Axes {
impl LuaExportsTable for Axes {
const EXPORT_NAME: &'static str = "Axes";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
let axes_new = |_, args: LuaMultiValue| {
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let axes_new = |_: &Lua, args: LuaMultiValue| {
let mut x = false;
let mut y = false;
let mut z = false;
@ -76,7 +76,7 @@ impl LuaExportsTable<'_> for Axes {
}
impl LuaUserData for Axes {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("X", |_, this| Ok(this.x));
fields.add_field_method_get("Y", |_, this| Ok(this.y));
fields.add_field_method_get("Z", |_, this| Ok(this.z));
@ -88,7 +88,7 @@ impl LuaUserData for Axes {
fields.add_field_method_get("Back", |_, this| Ok(this.z));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}

View file

@ -1,7 +1,7 @@
use core::fmt;
use mlua::prelude::*;
use rand::seq::SliceRandom;
use rand::prelude::*;
use rbx_dom_weak::types::BrickColor as DomBrickColor;
use lune_utils::TableBuilder;
@ -24,16 +24,16 @@ pub struct BrickColor {
pub(crate) rgb: (u8, u8, u8),
}
impl LuaExportsTable<'_> for BrickColor {
impl LuaExportsTable for BrickColor {
const EXPORT_NAME: &'static str = "BrickColor";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
type ArgsNumber = u16;
type ArgsName = String;
type ArgsRgb = (u8, u8, u8);
type ArgsColor3<'lua> = LuaUserDataRef<'lua, Color3>;
type ArgsColor3 = LuaUserDataRef<Color3>;
let brick_color_new = |lua, args: LuaMultiValue| {
let brick_color_new = |lua: &Lua, args: LuaMultiValue| {
if let Ok(number) = ArgsNumber::from_lua_multi(args.clone(), lua) {
Ok(color_from_number(number))
} else if let Ok(name) = ArgsName::from_lua_multi(args.clone(), lua) {
@ -50,7 +50,7 @@ impl LuaExportsTable<'_> for BrickColor {
}
};
let brick_color_palette = |_, index: u16| {
let brick_color_palette = |_: &Lua, index: u16| {
if index == 0 {
Err(LuaError::RuntimeError("Invalid index".to_string()))
} else if let Some(number) = BRICK_COLOR_PALETTE.get((index - 1) as usize) {
@ -60,8 +60,8 @@ impl LuaExportsTable<'_> for BrickColor {
}
};
let brick_color_random = |_, ()| {
let number = BRICK_COLOR_PALETTE.choose(&mut rand::thread_rng());
let brick_color_random = |_: &Lua, ()| {
let number = BRICK_COLOR_PALETTE.choose(&mut rand::rng());
Ok(color_from_number(*number.unwrap()))
};
@ -71,7 +71,7 @@ impl LuaExportsTable<'_> for BrickColor {
.with_function("random", brick_color_random)?;
for (name, number) in BRICK_COLOR_CONSTRUCTORS {
let f = |_, ()| Ok(color_from_number(*number));
let f = |_: &Lua, ()| Ok(color_from_number(*number));
builder = builder.with_function(*name, f)?;
}
@ -80,7 +80,7 @@ impl LuaExportsTable<'_> for BrickColor {
}
impl LuaUserData for BrickColor {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Number", |_, this| Ok(this.number));
fields.add_field_method_get("Name", |_, this| Ok(this.name));
fields.add_field_method_get("R", |_, this| Ok(this.rgb.0 as f32 / 255f32));
@ -92,7 +92,7 @@ impl LuaUserData for BrickColor {
fields.add_field_method_get("Color", |_, this| Ok(Color3::from(*this)));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}

View file

@ -43,27 +43,28 @@ impl CFrame {
}
}
impl LuaExportsTable<'_> for CFrame {
impl LuaExportsTable for CFrame {
const EXPORT_NAME: &'static str = "CFrame";
#[allow(clippy::too_many_lines)]
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
let cframe_angles = |_, (rx, ry, rz): (f32, f32, f32)| {
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let cframe_angles = |_: &Lua, (rx, ry, rz): (f32, f32, f32)| {
Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz)))
};
let cframe_from_axis_angle =
|_, (v, r): (LuaUserDataRef<Vector3>, f32)| Ok(CFrame(Mat4::from_axis_angle(v.0, r)));
let cframe_from_axis_angle = |_: &Lua, (v, r): (LuaUserDataRef<Vector3>, f32)| {
Ok(CFrame(Mat4::from_axis_angle(v.0, r)))
};
let cframe_from_euler_angles_xyz = |_, (rx, ry, rz): (f32, f32, f32)| {
let cframe_from_euler_angles_xyz = |_: &Lua, (rx, ry, rz): (f32, f32, f32)| {
Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz)))
};
let cframe_from_euler_angles_yxz = |_, (rx, ry, rz): (f32, f32, f32)| {
let cframe_from_euler_angles_yxz = |_: &Lua, (rx, ry, rz): (f32, f32, f32)| {
Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz)))
};
let cframe_from_matrix = |_,
let cframe_from_matrix = |_: &Lua,
(pos, rx, ry, rz): (
LuaUserDataRef<Vector3>,
LuaUserDataRef<Vector3>,
@ -79,11 +80,11 @@ impl LuaExportsTable<'_> for CFrame {
)))
};
let cframe_from_orientation = |_, (rx, ry, rz): (f32, f32, f32)| {
let cframe_from_orientation = |_: &Lua, (rx, ry, rz): (f32, f32, f32)| {
Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz)))
};
let cframe_look_at = |_,
let cframe_look_at = |_: &Lua,
(from, to, up): (
LuaUserDataRef<Vector3>,
LuaUserDataRef<Vector3>,
@ -97,18 +98,18 @@ impl LuaExportsTable<'_> for CFrame {
};
// Dynamic args constructor
type ArgsPos<'lua> = LuaUserDataRef<'lua, Vector3>;
type ArgsLook<'lua> = (
LuaUserDataRef<'lua, Vector3>,
LuaUserDataRef<'lua, Vector3>,
Option<LuaUserDataRef<'lua, Vector3>>,
type ArgsPos = LuaUserDataRef<Vector3>;
type ArgsLook = (
LuaUserDataRef<Vector3>,
LuaUserDataRef<Vector3>,
Option<LuaUserDataRef<Vector3>>,
);
type ArgsPosXYZ = (f32, f32, f32);
type ArgsPosXYZQuat = (f32, f32, f32, f32, f32, f32, f32);
type ArgsMatrix = (f32, f32, f32, f32, f32, f32, f32, f32, f32, f32, f32, f32);
let cframe_new = |lua, args: LuaMultiValue| match args.len() {
let cframe_new = |lua: &Lua, args: LuaMultiValue| match args.len() {
0 => Ok(CFrame(Mat4::IDENTITY)),
1 => match ArgsPos::from_lua_multi(args, lua) {
@ -174,7 +175,7 @@ impl LuaExportsTable<'_> for CFrame {
}
impl LuaUserData for CFrame {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Position", |_, this| Ok(Vector3(this.position())));
fields.add_field_method_get("Rotation", |_, this| {
Ok(CFrame(Mat4::from_cols(
@ -200,7 +201,7 @@ impl LuaUserData for CFrame {
}
#[allow(clippy::too_many_lines)]
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
// Methods
methods.add_method("Inverse", |_, this, ()| Ok(this.inverse()));
methods.add_method(
@ -323,10 +324,10 @@ impl LuaUserData for CFrame {
} else if let Ok(vec) = ud.borrow::<Vector3>() {
return lua.create_userdata(*this * *vec);
}
};
}
Err(LuaError::FromLuaConversionError {
from: rhs.type_name(),
to: "userdata",
to: "userdata".to_string(),
message: Some(format!(
"Expected CFrame or Vector3, got {}",
rhs.type_name()

View file

@ -28,11 +28,11 @@ pub struct Color3 {
pub(crate) b: f32,
}
impl LuaExportsTable<'_> for Color3 {
impl LuaExportsTable for Color3 {
const EXPORT_NAME: &'static str = "Color3";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
let color3_from_rgb = |_, (r, g, b): (Option<u8>, Option<u8>, Option<u8>)| {
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let color3_from_rgb = |_: &Lua, (r, g, b): (Option<u8>, Option<u8>, Option<u8>)| {
Ok(Color3 {
r: (r.unwrap_or_default() as f32) / 255f32,
g: (g.unwrap_or_default() as f32) / 255f32,
@ -40,7 +40,7 @@ impl LuaExportsTable<'_> for Color3 {
})
};
let color3_from_hsv = |_, (h, s, v): (f32, f32, f32)| {
let color3_from_hsv = |_: &Lua, (h, s, v): (f32, f32, f32)| {
// https://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
let i = (h * 6.0).floor();
let f = h * 6.0 - i;
@ -61,7 +61,7 @@ impl LuaExportsTable<'_> for Color3 {
Ok(Color3 { r, g, b })
};
let color3_from_hex = |_, hex: String| {
let color3_from_hex = |_: &Lua, hex: String| {
let trimmed = hex.trim_start_matches('#').to_ascii_uppercase();
let chars = if trimmed.len() == 3 {
(
@ -94,7 +94,7 @@ impl LuaExportsTable<'_> for Color3 {
}
};
let color3_new = |_, (r, g, b): (Option<f32>, Option<f32>, Option<f32>)| {
let color3_new = |_: &Lua, (r, g, b): (Option<f32>, Option<f32>, Option<f32>)| {
Ok(Color3 {
r: r.unwrap_or_default(),
g: g.unwrap_or_default(),
@ -111,14 +111,14 @@ impl LuaExportsTable<'_> for Color3 {
}
}
impl<'lua> FromLua<'lua> for Color3 {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
impl FromLua for Color3 {
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
if let LuaValue::UserData(ud) = value {
Ok(*ud.borrow::<Color3>()?)
} else {
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "Color3",
to: "Color3".to_string(),
message: None,
})
}
@ -126,13 +126,13 @@ impl<'lua> FromLua<'lua> for Color3 {
}
impl LuaUserData for Color3 {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("R", |_, this| Ok(this.r));
fields.add_field_method_get("G", |_, this| Ok(this.g));
fields.add_field_method_get("B", |_, this| Ok(this.b));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
// Methods
methods.add_method(
"Lerp",

View file

@ -21,15 +21,15 @@ pub struct ColorSequence {
pub(crate) keypoints: Vec<ColorSequenceKeypoint>,
}
impl LuaExportsTable<'_> for ColorSequence {
impl LuaExportsTable for ColorSequence {
const EXPORT_NAME: &'static str = "ColorSequence";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
type ArgsColor<'lua> = LuaUserDataRef<'lua, Color3>;
type ArgsColors<'lua> = (LuaUserDataRef<'lua, Color3>, LuaUserDataRef<'lua, Color3>);
type ArgsKeypoints<'lua> = Vec<LuaUserDataRef<'lua, ColorSequenceKeypoint>>;
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
type ArgsColor = LuaUserDataRef<Color3>;
type ArgsColors = (LuaUserDataRef<Color3>, LuaUserDataRef<Color3>);
type ArgsKeypoints = Vec<LuaUserDataRef<ColorSequenceKeypoint>>;
let color_sequence_new = |lua, args: LuaMultiValue| {
let color_sequence_new = |lua: &Lua, args: LuaMultiValue| {
if let Ok(color) = ArgsColor::from_lua_multi(args.clone(), lua) {
Ok(ColorSequence {
keypoints: vec![
@ -75,11 +75,11 @@ impl LuaExportsTable<'_> for ColorSequence {
}
impl LuaUserData for ColorSequence {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Keypoints", |_, this| Ok(this.keypoints.clone()));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}

View file

@ -20,16 +20,17 @@ pub struct ColorSequenceKeypoint {
pub(crate) color: Color3,
}
impl LuaExportsTable<'_> for ColorSequenceKeypoint {
impl LuaExportsTable for ColorSequenceKeypoint {
const EXPORT_NAME: &'static str = "ColorSequenceKeypoint";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
let color_sequence_keypoint_new = |_, (time, color): (f32, LuaUserDataRef<Color3>)| {
Ok(ColorSequenceKeypoint {
time,
color: *color,
})
};
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let color_sequence_keypoint_new =
|_: &Lua, (time, color): (f32, LuaUserDataRef<Color3>)| {
Ok(ColorSequenceKeypoint {
time,
color: *color,
})
};
TableBuilder::new(lua)?
.with_function("new", color_sequence_keypoint_new)?
@ -38,12 +39,12 @@ impl LuaExportsTable<'_> for ColorSequenceKeypoint {
}
impl LuaUserData for ColorSequenceKeypoint {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Time", |_, this| Ok(this.time));
fields.add_field_method_get("Value", |_, this| Ok(this.color));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}

View file

@ -0,0 +1,120 @@
use core::fmt;
use mlua::prelude::*;
use rbx_dom_weak::types::{Content as DomContent, ContentType};
use lune_utils::TableBuilder;
use crate::{exports::LuaExportsTable, instance::Instance};
use super::{super::*, EnumItem};
/**
An implementation of the [Content](https://create.roblox.com/docs/reference/engine/datatypes/Content) Roblox datatype.
This implements all documented properties, methods & constructors of the Content type as of April 2025.
*/
#[derive(Debug, Clone, PartialEq)]
pub struct Content(ContentType);
impl LuaExportsTable for Content {
const EXPORT_NAME: &'static str = "Content";
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let from_uri = |_: &Lua, uri: String| Ok(Self(ContentType::Uri(uri)));
let from_object = |_: &Lua, obj: LuaUserDataRef<Instance>| {
let database = rbx_reflection_database::get();
let instance_descriptor = database
.classes
.get("Instance")
.expect("the reflection database should always have Instance in it");
let param_descriptor = database.classes.get(obj.get_class_name()).expect(
"you should not be able to construct an Instance that is not known to Lune",
);
if database.has_superclass(param_descriptor, instance_descriptor) {
Err(LuaError::runtime("the provided object is a descendant class of 'Instance', expected one that was only an 'Object'"))
} else {
Ok(Content(ContentType::Object(obj.dom_ref)))
}
};
TableBuilder::new(lua)?
.with_value("none", Content(ContentType::None))?
.with_function("fromUri", from_uri)?
.with_function("fromObject", from_object)?
.build_readonly()
}
}
impl LuaUserData for Content {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("SourceType", |_, this| {
let variant_name = match &this.0 {
ContentType::None => "None",
ContentType::Uri(_) => "Uri",
ContentType::Object(_) => "Object",
other => {
return Err(LuaError::runtime(format!(
"cannot get SourceType: unknown ContentType variant '{other:?}'"
)))
}
};
Ok(EnumItem::from_enum_name_and_name(
"ContentSourceType",
variant_name,
))
});
fields.add_field_method_get("Uri", |_, this| {
if let ContentType::Uri(uri) = &this.0 {
Ok(Some(uri.to_owned()))
} else {
Ok(None)
}
});
fields.add_field_method_get("Object", |_, this| {
if let ContentType::Object(referent) = &this.0 {
Ok(Instance::new_opt(*referent))
} else {
Ok(None)
}
});
}
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}
}
impl fmt::Display for Content {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Regardless of the actual content of the Content, Roblox just emits
// `Content` when casting it to a string. We do not do that.
write!(f, "Content(")?;
match &self.0 {
ContentType::None => write!(f, "None")?,
ContentType::Uri(uri) => write!(f, "Uri={uri}")?,
ContentType::Object(_) => write!(f, "Object")?,
other => write!(f, "UnknownType({other:?})")?,
}
write!(f, ")")
}
}
impl From<DomContent> for Content {
fn from(value: DomContent) -> Self {
Self(value.value().clone())
}
}
impl From<Content> for DomContent {
fn from(value: Content) -> Self {
match value.0 {
ContentType::None => Self::none(),
ContentType::Uri(uri) => Self::from_uri(uri),
ContentType::Object(referent) => Self::from_referent(referent),
other => unimplemented!("unknown variant of ContentType: {other:?}"),
}
}
}

View file

@ -23,7 +23,7 @@ impl Enum {
}
impl LuaUserData for Enum {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
// Methods
methods.add_method("GetEnumItems", |_, this, ()| {
Ok(this

View file

@ -1,7 +1,7 @@
use core::fmt;
use mlua::prelude::*;
use rbx_dom_weak::types::Enum as DomEnum;
use rbx_dom_weak::types::EnumItem as DomEnumItem;
use super::{super::*, Enum};
@ -62,26 +62,26 @@ impl EnumItem {
}
impl LuaUserData for EnumItem {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Name", |_, this| Ok(this.name.clone()));
fields.add_field_method_get("Value", |_, this| Ok(this.value));
fields.add_field_method_get("EnumType", |_, this| Ok(this.parent.clone()));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}
}
impl<'lua> FromLua<'lua> for EnumItem {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
impl FromLua for EnumItem {
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
if let LuaValue::UserData(ud) = value {
Ok(ud.borrow::<EnumItem>()?.to_owned())
} else {
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "EnumItem",
to: "EnumItem".to_string(),
message: None,
})
}
@ -100,8 +100,18 @@ impl PartialEq for EnumItem {
}
}
impl From<EnumItem> for DomEnum {
impl From<EnumItem> for DomEnumItem {
fn from(v: EnumItem) -> Self {
DomEnum::from_u32(v.value)
DomEnumItem {
ty: v.parent.desc.name.to_string(),
value: v.value,
}
}
}
impl From<DomEnumItem> for EnumItem {
fn from(value: DomEnumItem) -> Self {
EnumItem::from_enum_name_and_value(value.ty, value.value)
.expect("cannot convert rbx_type::EnumItem with unknown type into EnumItem")
}
}

View file

@ -13,7 +13,7 @@ use super::{super::*, Enum};
pub struct Enums;
impl LuaUserData for Enums {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
// Methods
methods.add_method("GetEnums", |_, _, ()| {
let db = rbx_reflection_database::get();

View file

@ -26,11 +26,11 @@ pub struct Faces {
pub(crate) front: bool,
}
impl LuaExportsTable<'_> for Faces {
impl LuaExportsTable for Faces {
const EXPORT_NAME: &'static str = "Faces";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
let faces_new = |_, args: LuaMultiValue| {
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let faces_new = |_: &Lua, args: LuaMultiValue| {
let mut right = false;
let mut top = false;
let mut back = false;
@ -87,7 +87,7 @@ impl LuaExportsTable<'_> for Faces {
}
impl LuaUserData for Faces {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Right", |_, this| Ok(this.right));
fields.add_field_method_get("Top", |_, this| Ok(this.top));
fields.add_field_method_get("Back", |_, this| Ok(this.back));
@ -96,7 +96,7 @@ impl LuaUserData for Faces {
fields.add_field_method_get("Front", |_, this| Ok(this.front));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}

View file

@ -40,11 +40,11 @@ impl Font {
}
}
impl LuaExportsTable<'_> for Font {
impl LuaExportsTable for Font {
const EXPORT_NAME: &'static str = "Font";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
let font_from_enum = |_, value: LuaUserDataRef<EnumItem>| {
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let font_from_enum = |_: &Lua, value: LuaUserDataRef<EnumItem>| {
if value.parent.desc.name == "Font" {
match Font::from_enum_item(&value) {
Some(props) => Ok(props),
@ -62,7 +62,7 @@ impl LuaExportsTable<'_> for Font {
};
let font_from_name =
|_, (file, weight, style): (String, Option<FontWeight>, Option<FontStyle>)| {
|_: &Lua, (file, weight, style): (String, Option<FontWeight>, Option<FontStyle>)| {
Ok(Font {
family: format!("rbxasset://fonts/families/{file}.json"),
weight: weight.unwrap_or_default(),
@ -72,7 +72,7 @@ impl LuaExportsTable<'_> for Font {
};
let font_from_id =
|_, (id, weight, style): (i32, Option<FontWeight>, Option<FontStyle>)| {
|_: &Lua, (id, weight, style): (i32, Option<FontWeight>, Option<FontStyle>)| {
Ok(Font {
family: format!("rbxassetid://{id}"),
weight: weight.unwrap_or_default(),
@ -82,7 +82,7 @@ impl LuaExportsTable<'_> for Font {
};
let font_new =
|_, (family, weight, style): (String, Option<FontWeight>, Option<FontStyle>)| {
|_: &Lua, (family, weight, style): (String, Option<FontWeight>, Option<FontStyle>)| {
Ok(Font {
family,
weight: weight.unwrap_or_default(),
@ -101,7 +101,7 @@ impl LuaExportsTable<'_> for Font {
}
impl LuaUserData for Font {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
// Getters
fields.add_field_method_get("Family", |_, this| Ok(this.family.clone()));
fields.add_field_method_get("Weight", |_, this| Ok(this.weight));
@ -126,7 +126,7 @@ impl LuaUserData for Font {
});
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}
@ -282,8 +282,8 @@ impl std::fmt::Display for FontWeight {
}
}
impl<'lua> FromLua<'lua> for FontWeight {
fn from_lua(lua_value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
impl FromLua for FontWeight {
fn from_lua(lua_value: LuaValue, _: &Lua) -> LuaResult<Self> {
let mut message = None;
if let LuaValue::UserData(ud) = &lua_value {
let value = ud.borrow::<EnumItem>()?;
@ -304,18 +304,18 @@ impl<'lua> FromLua<'lua> for FontWeight {
}
Err(LuaError::FromLuaConversionError {
from: lua_value.type_name(),
to: "Enum.FontWeight",
to: "Enum.FontWeight".to_string(),
message,
})
}
}
impl<'lua> IntoLua<'lua> for FontWeight {
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
impl IntoLua for FontWeight {
fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> {
match EnumItem::from_enum_name_and_name("FontWeight", self.to_string()) {
Some(enum_item) => Ok(LuaValue::UserData(lua.create_userdata(enum_item)?)),
None => Err(LuaError::ToLuaConversionError {
from: "FontWeight",
from: "FontWeight".to_string(),
to: "EnumItem",
message: Some(format!("Found unknown Enum.FontWeight value '{self}'")),
}),
@ -376,8 +376,8 @@ impl std::fmt::Display for FontStyle {
}
}
impl<'lua> FromLua<'lua> for FontStyle {
fn from_lua(lua_value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
impl FromLua for FontStyle {
fn from_lua(lua_value: LuaValue, _: &Lua) -> LuaResult<Self> {
let mut message = None;
if let LuaValue::UserData(ud) = &lua_value {
let value = ud.borrow::<EnumItem>()?;
@ -398,18 +398,18 @@ impl<'lua> FromLua<'lua> for FontStyle {
}
Err(LuaError::FromLuaConversionError {
from: lua_value.type_name(),
to: "Enum.FontStyle",
to: "Enum.FontStyle".to_string(),
message,
})
}
}
impl<'lua> IntoLua<'lua> for FontStyle {
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
impl IntoLua for FontStyle {
fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> {
match EnumItem::from_enum_name_and_name("FontStyle", self.to_string()) {
Some(enum_item) => Ok(LuaValue::UserData(lua.create_userdata(enum_item)?)),
None => Err(LuaError::ToLuaConversionError {
from: "FontStyle",
from: "FontStyle".to_string(),
to: "EnumItem",
message: Some(format!("Found unknown Enum.FontStyle value '{self}'")),
}),

View file

@ -4,6 +4,7 @@ mod cframe;
mod color3;
mod color_sequence;
mod color_sequence_keypoint;
mod content;
mod r#enum;
mod r#enum_item;
mod r#enums;
@ -30,6 +31,7 @@ pub use cframe::CFrame;
pub use color3::Color3;
pub use color_sequence::ColorSequence;
pub use color_sequence_keypoint::ColorSequenceKeypoint;
pub use content::Content;
pub use faces::Faces;
pub use font::Font;
pub use number_range::NumberRange;

View file

@ -20,11 +20,11 @@ pub struct NumberRange {
pub(crate) max: f32,
}
impl LuaExportsTable<'_> for NumberRange {
impl LuaExportsTable for NumberRange {
const EXPORT_NAME: &'static str = "NumberRange";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
let number_range_new = |_, (min, max): (f32, Option<f32>)| {
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let number_range_new = |_: &Lua, (min, max): (f32, Option<f32>)| {
Ok(match max {
Some(max) => NumberRange {
min: min.min(max),
@ -41,12 +41,12 @@ impl LuaExportsTable<'_> for NumberRange {
}
impl LuaUserData for NumberRange {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Min", |_, this| Ok(this.min));
fields.add_field_method_get("Max", |_, this| Ok(this.max));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}

View file

@ -21,15 +21,15 @@ pub struct NumberSequence {
pub(crate) keypoints: Vec<NumberSequenceKeypoint>,
}
impl LuaExportsTable<'_> for NumberSequence {
impl LuaExportsTable for NumberSequence {
const EXPORT_NAME: &'static str = "NumberSequence";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
type ArgsColor = f32;
type ArgsColors = (f32, f32);
type ArgsKeypoints<'lua> = Vec<LuaUserDataRef<'lua, NumberSequenceKeypoint>>;
type ArgsKeypoints = Vec<LuaUserDataRef<NumberSequenceKeypoint>>;
let number_sequence_new = |lua, args: LuaMultiValue| {
let number_sequence_new = |lua: &Lua, args: LuaMultiValue| {
if let Ok(value) = ArgsColor::from_lua_multi(args.clone(), lua) {
Ok(NumberSequence {
keypoints: vec![
@ -79,11 +79,11 @@ impl LuaExportsTable<'_> for NumberSequence {
}
impl LuaUserData for NumberSequence {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Keypoints", |_, this| Ok(this.keypoints.clone()));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}

View file

@ -21,17 +21,18 @@ pub struct NumberSequenceKeypoint {
pub(crate) envelope: f32,
}
impl LuaExportsTable<'_> for NumberSequenceKeypoint {
impl LuaExportsTable for NumberSequenceKeypoint {
const EXPORT_NAME: &'static str = "NumberSequenceKeypoint";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
let number_sequence_keypoint_new = |_, (time, value, envelope): (f32, f32, Option<f32>)| {
Ok(NumberSequenceKeypoint {
time,
value,
envelope: envelope.unwrap_or_default(),
})
};
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let number_sequence_keypoint_new =
|_: &Lua, (time, value, envelope): (f32, f32, Option<f32>)| {
Ok(NumberSequenceKeypoint {
time,
value,
envelope: envelope.unwrap_or_default(),
})
};
TableBuilder::new(lua)?
.with_function("new", number_sequence_keypoint_new)?
@ -40,13 +41,13 @@ impl LuaExportsTable<'_> for NumberSequenceKeypoint {
}
impl LuaUserData for NumberSequenceKeypoint {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Time", |_, this| Ok(this.time));
fields.add_field_method_get("Value", |_, this| Ok(this.value));
fields.add_field_method_get("Envelope", |_, this| Ok(this.envelope));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}

View file

@ -38,14 +38,14 @@ impl PhysicalProperties {
}
}
impl LuaExportsTable<'_> for PhysicalProperties {
impl LuaExportsTable for PhysicalProperties {
const EXPORT_NAME: &'static str = "PhysicalProperties";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
type ArgsMaterial<'lua> = LuaUserDataRef<'lua, EnumItem>;
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
type ArgsMaterial = LuaUserDataRef<EnumItem>;
type ArgsNumbers = (f32, f32, f32, Option<f32>, Option<f32>);
let physical_properties_new = |lua, args: LuaMultiValue| {
let physical_properties_new = |lua: &Lua, args: LuaMultiValue| {
if let Ok(value) = ArgsMaterial::from_lua_multi(args.clone(), lua) {
if value.parent.desc.name == "Material" {
match PhysicalProperties::from_material(&value) {
@ -86,7 +86,7 @@ impl LuaExportsTable<'_> for PhysicalProperties {
}
impl LuaUserData for PhysicalProperties {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Density", |_, this| Ok(this.density));
fields.add_field_method_get("Friction", |_, this| Ok(this.friction));
fields.add_field_method_get("FrictionWeight", |_, this| Ok(this.friction_weight));
@ -94,7 +94,7 @@ impl LuaUserData for PhysicalProperties {
fields.add_field_method_get("ElasticityWeight", |_, this| Ok(this.elasticity_weight));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}

View file

@ -32,12 +32,12 @@ impl Ray {
}
}
impl LuaExportsTable<'_> for Ray {
impl LuaExportsTable for Ray {
const EXPORT_NAME: &'static str = "Ray";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let ray_new =
|_, (origin, direction): (LuaUserDataRef<Vector3>, LuaUserDataRef<Vector3>)| {
|_: &Lua, (origin, direction): (LuaUserDataRef<Vector3>, LuaUserDataRef<Vector3>)| {
Ok(Ray {
origin: origin.0,
direction: direction.0,
@ -51,7 +51,7 @@ impl LuaExportsTable<'_> for Ray {
}
impl LuaUserData for Ray {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Origin", |_, this| Ok(Vector3(this.origin)));
fields.add_field_method_get("Direction", |_, this| Ok(Vector3(this.direction)));
fields.add_field_method_get("Unit", |_, this| {
@ -62,7 +62,7 @@ impl LuaUserData for Ray {
});
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
// Methods
methods.add_method("ClosestPoint", |_, this, to: LuaUserDataRef<Vector3>| {
Ok(Vector3(this.closest_point(to.0)))

View file

@ -32,17 +32,17 @@ impl Rect {
}
}
impl LuaExportsTable<'_> for Rect {
impl LuaExportsTable for Rect {
const EXPORT_NAME: &'static str = "Rect";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
type ArgsVector2s<'lua> = (
Option<LuaUserDataRef<'lua, Vector2>>,
Option<LuaUserDataRef<'lua, Vector2>>,
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
type ArgsVector2s = (
Option<LuaUserDataRef<Vector2>>,
Option<LuaUserDataRef<Vector2>>,
);
type ArgsNums = (Option<f32>, Option<f32>, Option<f32>, Option<f32>);
let rect_new = |lua, args: LuaMultiValue| {
let rect_new = |lua: &Lua, args: LuaMultiValue| {
if let Ok((min, max)) = ArgsVector2s::from_lua_multi(args.clone(), lua) {
Ok(Rect::new(
min.map(|m| *m).unwrap_or_default().0,
@ -67,14 +67,14 @@ impl LuaExportsTable<'_> for Rect {
}
impl LuaUserData for Rect {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Min", |_, this| Ok(Vector2(this.min)));
fields.add_field_method_get("Max", |_, this| Ok(Vector2(this.max)));
fields.add_field_method_get("Width", |_, this| Ok(this.max.x - this.min.x));
fields.add_field_method_get("Height", |_, this| Ok(this.max.y - this.min.y));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<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_meta_method(LuaMetaMethod::Unm, userdata_impl_unm);

View file

@ -22,16 +22,17 @@ pub struct Region3 {
pub(crate) max: Vec3,
}
impl LuaExportsTable<'_> for Region3 {
impl LuaExportsTable for Region3 {
const EXPORT_NAME: &'static str = "Region3";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
let region3_new = |_, (min, max): (LuaUserDataRef<Vector3>, LuaUserDataRef<Vector3>)| {
Ok(Region3 {
min: min.0,
max: max.0,
})
};
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let region3_new =
|_: &Lua, (min, max): (LuaUserDataRef<Vector3>, LuaUserDataRef<Vector3>)| {
Ok(Region3 {
min: min.0,
max: max.0,
})
};
TableBuilder::new(lua)?
.with_function("new", region3_new)?
@ -40,14 +41,14 @@ impl LuaExportsTable<'_> for Region3 {
}
impl LuaUserData for Region3 {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("CFrame", |_, this| {
Ok(CFrame(Mat4::from_translation(this.min.lerp(this.max, 0.5))))
});
fields.add_field_method_get("Size", |_, this| Ok(Vector3(this.max - this.min)));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
// Methods
methods.add_method("ExpandToGrid", |_, this, resolution: f32| {
Ok(Region3 {

View file

@ -22,12 +22,12 @@ pub struct Region3int16 {
pub(crate) max: IVec3,
}
impl LuaExportsTable<'_> for Region3int16 {
impl LuaExportsTable for Region3int16 {
const EXPORT_NAME: &'static str = "Region3int16";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let region3int16_new =
|_, (min, max): (LuaUserDataRef<Vector3int16>, LuaUserDataRef<Vector3int16>)| {
|_: &Lua, (min, max): (LuaUserDataRef<Vector3int16>, LuaUserDataRef<Vector3int16>)| {
Ok(Region3int16 {
min: min.0,
max: max.0,
@ -41,12 +41,12 @@ impl LuaExportsTable<'_> for Region3int16 {
}
impl LuaUserData for Region3int16 {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Min", |_, this| Ok(Vector3int16(this.min)));
fields.add_field_method_get("Max", |_, this| Ok(Vector3int16(this.max)));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}

View file

@ -27,11 +27,11 @@ impl UDim {
}
}
impl LuaExportsTable<'_> for UDim {
impl LuaExportsTable for UDim {
const EXPORT_NAME: &'static str = "UDim";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
let udim_new = |_, (scale, offset): (Option<f32>, Option<i32>)| {
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let udim_new = |_: &Lua, (scale, offset): (Option<f32>, Option<i32>)| {
Ok(UDim {
scale: scale.unwrap_or_default(),
offset: offset.unwrap_or_default(),
@ -45,12 +45,12 @@ impl LuaExportsTable<'_> for UDim {
}
impl LuaUserData for UDim {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Scale", |_, this| Ok(this.scale));
fields.add_field_method_get("Offset", |_, this| Ok(this.offset));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<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_meta_method(LuaMetaMethod::Unm, userdata_impl_unm);

View file

@ -24,30 +24,27 @@ pub struct UDim2 {
pub(crate) y: UDim,
}
impl LuaExportsTable<'_> for UDim2 {
impl LuaExportsTable for UDim2 {
const EXPORT_NAME: &'static str = "UDim2";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
let udim2_from_offset = |_, (x, y): (Option<i32>, Option<i32>)| {
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let udim2_from_offset = |_: &Lua, (x, y): (Option<i32>, Option<i32>)| {
Ok(UDim2 {
x: UDim::new(0f32, x.unwrap_or_default()),
y: UDim::new(0f32, y.unwrap_or_default()),
})
};
let udim2_from_scale = |_, (x, y): (Option<f32>, Option<f32>)| {
let udim2_from_scale = |_: &Lua, (x, y): (Option<f32>, Option<f32>)| {
Ok(UDim2 {
x: UDim::new(x.unwrap_or_default(), 0),
y: UDim::new(y.unwrap_or_default(), 0),
})
};
type ArgsUDims<'lua> = (
Option<LuaUserDataRef<'lua, UDim>>,
Option<LuaUserDataRef<'lua, UDim>>,
);
type ArgsUDims = (Option<LuaUserDataRef<UDim>>, Option<LuaUserDataRef<UDim>>);
type ArgsNums = (Option<f32>, Option<i32>, Option<f32>, Option<i32>);
let udim2_new = |lua, args: LuaMultiValue| {
let udim2_new = |lua: &Lua, args: LuaMultiValue| {
if let Ok((x, y)) = ArgsUDims::from_lua_multi(args.clone(), lua) {
Ok(UDim2 {
x: x.map(|x| *x).unwrap_or_default(),
@ -75,14 +72,14 @@ impl LuaExportsTable<'_> for UDim2 {
}
impl LuaUserData for UDim2 {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("X", |_, this| Ok(this.x));
fields.add_field_method_get("Y", |_, this| Ok(this.y));
fields.add_field_method_get("Width", |_, this| Ok(this.x));
fields.add_field_method_get("Height", |_, this| Ok(this.y));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
// Methods
methods.add_method(
"Lerp",

View file

@ -21,11 +21,11 @@ use super::super::*;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Vector2(pub Vec2);
impl LuaExportsTable<'_> for Vector2 {
impl LuaExportsTable for Vector2 {
const EXPORT_NAME: &'static str = "Vector2";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
let vector2_new = |_, (x, y): (Option<f32>, Option<f32>)| {
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let vector2_new = |_: &Lua, (x, y): (Option<f32>, Option<f32>)| {
Ok(Vector2(Vec2 {
x: x.unwrap_or_default(),
y: y.unwrap_or_default(),
@ -43,15 +43,18 @@ impl LuaExportsTable<'_> for Vector2 {
}
impl LuaUserData for Vector2 {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Magnitude", |_, this| Ok(this.0.length()));
fields.add_field_method_get("Unit", |_, this| Ok(Vector2(this.0.normalize())));
fields.add_field_method_get("X", |_, this| Ok(this.0.x));
fields.add_field_method_get("Y", |_, this| Ok(this.0.y));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
// Methods
methods.add_method("Angle", |_, this, rhs: LuaUserDataRef<Vector2>| {
Ok(this.0.angle_to(rhs.0))
});
methods.add_method("Cross", |_, this, rhs: LuaUserDataRef<Vector2>| {
let this_v3 = Vec3::new(this.0.x, this.0.y, 0f32);
let rhs_v3 = Vec3::new(rhs.0.x, rhs.0.y, 0f32);
@ -60,6 +63,14 @@ impl LuaUserData for Vector2 {
methods.add_method("Dot", |_, this, rhs: LuaUserDataRef<Vector2>| {
Ok(this.0.dot(rhs.0))
});
methods.add_method(
"FuzzyEq",
|_, this, (rhs, epsilon): (LuaUserDataRef<Vector2>, f32)| {
let eq_x = (rhs.0.x - this.0.x).abs() <= epsilon;
let eq_y = (rhs.0.y - this.0.y).abs() <= epsilon;
Ok(eq_x && eq_y)
},
);
methods.add_method(
"Lerp",
|_, this, (rhs, alpha): (LuaUserDataRef<Vector2>, f32)| {
@ -72,6 +83,10 @@ impl LuaUserData for Vector2 {
methods.add_method("Min", |_, this, rhs: LuaUserDataRef<Vector2>| {
Ok(Vector2(this.0.min(rhs.0)))
});
methods.add_method("Abs", |_, this, ()| Ok(Vector2(this.0.abs())));
methods.add_method("Ceil", |_, this, ()| Ok(Vector2(this.0.ceil())));
methods.add_method("Floor", |_, this, ()| Ok(Vector2(this.0.floor())));
methods.add_method("Sign", |_, this, ()| Ok(Vector2(this.0.signum())));
// Metamethods
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);

View file

@ -21,11 +21,11 @@ use super::super::*;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Vector2int16(pub IVec2);
impl LuaExportsTable<'_> for Vector2int16 {
impl LuaExportsTable for Vector2int16 {
const EXPORT_NAME: &'static str = "Vector2int16";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
let vector2int16_new = |_, (x, y): (Option<i16>, Option<i16>)| {
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let vector2int16_new = |_: &Lua, (x, y): (Option<i16>, Option<i16>)| {
Ok(Vector2int16(IVec2 {
x: x.unwrap_or_default() as i32,
y: y.unwrap_or_default() as i32,
@ -39,12 +39,12 @@ impl LuaExportsTable<'_> for Vector2int16 {
}
impl LuaUserData for Vector2int16 {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("X", |_, this| Ok(this.0.x));
fields.add_field_method_get("Y", |_, this| Ok(this.0.y));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<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_meta_method(LuaMetaMethod::Unm, userdata_impl_unm);

View file

@ -24,11 +24,11 @@ use super::{super::*, EnumItem};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Vector3(pub Vec3);
impl LuaExportsTable<'_> for Vector3 {
impl LuaExportsTable for Vector3 {
const EXPORT_NAME: &'static str = "Vector3";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
let vector3_from_axis = |_, normal_id: LuaUserDataRef<EnumItem>| {
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let vector3_from_axis = |_: &Lua, normal_id: LuaUserDataRef<EnumItem>| {
if normal_id.parent.desc.name == "Axis" {
Ok(match normal_id.name.as_str() {
"X" => Vector3(Vec3::X),
@ -48,7 +48,7 @@ impl LuaExportsTable<'_> for Vector3 {
}
};
let vector3_from_normal_id = |_, normal_id: LuaUserDataRef<EnumItem>| {
let vector3_from_normal_id = |_: &Lua, normal_id: LuaUserDataRef<EnumItem>| {
if normal_id.parent.desc.name == "NormalId" {
Ok(match normal_id.name.as_str() {
"Left" => Vector3(Vec3::X),
@ -71,7 +71,7 @@ impl LuaExportsTable<'_> for Vector3 {
}
};
let vector3_new = |_, (x, y, z): (Option<f32>, Option<f32>, Option<f32>)| {
let vector3_new = |_: &Lua, (x, y, z): (Option<f32>, Option<f32>, Option<f32>)| {
Ok(Vector3(Vec3 {
x: x.unwrap_or_default(),
y: y.unwrap_or_default(),
@ -93,7 +93,7 @@ impl LuaExportsTable<'_> for Vector3 {
}
impl LuaUserData for Vector3 {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Magnitude", |_, this| Ok(this.0.length()));
fields.add_field_method_get("Unit", |_, this| Ok(Vector3(this.0.normalize())));
fields.add_field_method_get("X", |_, this| Ok(this.0.x));
@ -101,7 +101,7 @@ impl LuaUserData for Vector3 {
fields.add_field_method_get("Z", |_, this| Ok(this.0.z));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
// Methods
methods.add_method("Angle", |_, this, rhs: LuaUserDataRef<Vector3>| {
Ok(this.0.angle_between(rhs.0))
@ -133,6 +133,10 @@ impl LuaUserData for Vector3 {
methods.add_method("Min", |_, this, rhs: LuaUserDataRef<Vector3>| {
Ok(Vector3(this.0.min(rhs.0)))
});
methods.add_method("Abs", |_, this, ()| Ok(Vector3(this.0.abs())));
methods.add_method("Ceil", |_, this, ()| Ok(Vector3(this.0.ceil())));
methods.add_method("Floor", |_, this, ()| Ok(Vector3(this.0.floor())));
methods.add_method("Sign", |_, this, ()| Ok(Vector3(this.0.signum())));
// Metamethods
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);

View file

@ -21,11 +21,11 @@ use super::super::*;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Vector3int16(pub IVec3);
impl LuaExportsTable<'_> for Vector3int16 {
impl LuaExportsTable for Vector3int16 {
const EXPORT_NAME: &'static str = "Vector3int16";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
let vector3int16_new = |_, (x, y, z): (Option<i16>, Option<i16>, Option<i16>)| {
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let vector3int16_new = |_: &Lua, (x, y, z): (Option<i16>, Option<i16>, Option<i16>)| {
Ok(Vector3int16(IVec3 {
x: x.unwrap_or_default() as i32,
y: y.unwrap_or_default() as i32,
@ -40,13 +40,13 @@ impl LuaExportsTable<'_> for Vector3int16 {
}
impl LuaUserData for Vector3int16 {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("X", |_, this| Ok(this.0.x));
fields.add_field_method_get("Y", |_, this| Ok(this.0.y));
fields.add_field_method_get("Z", |_, this| Ok(this.0.z));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<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_meta_method(LuaMetaMethod::Unm, userdata_impl_unm);

View file

@ -65,7 +65,7 @@ impl DocumentKind {
for child_ref in dom.root().children() {
if let Some(child_inst) = dom.get_by_ref(*child_ref) {
has_top_level_child = true;
if class_is_a_service(&child_inst.class).unwrap_or(false) {
if class_is_a_service(child_inst.class).unwrap_or(false) {
has_top_level_service = true;
break;
}

View file

@ -1,6 +1,6 @@
use rbx_dom_weak::{
types::{Ref as DomRef, VariantType as DomType},
Instance as DomInstance, WeakDom,
ustr, Instance as DomInstance, WeakDom,
};
use crate::shared::instance::class_is_a;
@ -18,8 +18,8 @@ pub fn postprocess_dom_for_model(dom: &mut WeakDom) {
remove_matching_prop(inst, DomType::UniqueId, "HistoryId");
// Similar story with ScriptGuid - this is used
// in the studio-only cloud script drafts feature
if class_is_a(&inst.class, "LuaSourceContainer").unwrap_or(false) {
inst.properties.remove("ScriptGuid");
if class_is_a(inst.class, "LuaSourceContainer").unwrap_or(false) {
inst.properties.remove(&ustr("ScriptGuid"));
}
});
}
@ -41,7 +41,8 @@ where
}
fn remove_matching_prop(inst: &mut DomInstance, ty: DomType, name: &'static str) {
if inst.properties.get(name).map_or(false, |u| u.ty() == ty) {
let name = &ustr(name);
if inst.properties.get(name).is_some_and(|u| u.ty() == ty) {
inst.properties.remove(name);
}
}

View file

@ -18,10 +18,10 @@ use mlua::prelude::*;
}
}
impl LuaExportsTable<'_> for MyType {
impl LuaExportsTable for MyType {
const EXPORT_NAME: &'static str = "MyType";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let my_type_new = |lua, n: Option<usize>| {
Self::new(n.unwrap_or_default())
};
@ -37,10 +37,10 @@ use mlua::prelude::*;
}
```
*/
pub trait LuaExportsTable<'lua> {
pub trait LuaExportsTable {
const EXPORT_NAME: &'static str;
fn create_exports_table(lua: &'lua Lua) -> LuaResult<LuaTable<'lua>>;
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable>;
}
/**
@ -57,12 +57,12 @@ pub trait LuaExportsTable<'lua> {
let (name2, table2) = export::<Type2>(lua)?;
```
*/
pub fn export<'lua, T>(lua: &'lua Lua) -> LuaResult<(&'static str, LuaValue<'lua>)>
pub fn export<T>(lua: Lua) -> LuaResult<(&'static str, LuaValue)>
where
T: LuaExportsTable<'lua>,
T: LuaExportsTable,
{
Ok((
T::EXPORT_NAME,
<T as LuaExportsTable>::create_exports_table(lua)?.into_lua(lua)?,
<T as LuaExportsTable>::create_exports_table(lua.clone())?.into_lua(&lua)?,
))
}

View file

@ -20,7 +20,7 @@ use crate::{
use super::{data_model, registry::InstanceRegistry, Instance};
#[allow(clippy::too_many_lines)]
pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
pub fn add_methods<M: LuaUserDataMethods<Instance>>(m: &mut M) {
m.add_meta_method(LuaMetaMethod::ToString, |lua, this, ()| {
ensure_not_destroyed(this)?;
userdata_impl_to_string(lua, this, ())
@ -71,7 +71,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
"FindFirstAncestorWhichIsA",
|lua, this, class_name: String| {
ensure_not_destroyed(this)?;
this.find_ancestor(|child| class_is_a(&child.class, &class_name).unwrap_or(false))
this.find_ancestor(|child| class_is_a(child.class, &class_name).unwrap_or(false))
.into_lua(lua)
},
);
@ -104,7 +104,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|lua, this, (class_name, recursive): (String, Option<bool>)| {
ensure_not_destroyed(this)?;
let predicate =
|child: &DomInstance| class_is_a(&child.class, &class_name).unwrap_or(false);
|child: &DomInstance| class_is_a(child.class, &class_name).unwrap_or(false);
if matches!(recursive, Some(true)) {
this.find_descendant(predicate).into_lua(lua)
} else {
@ -113,8 +113,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
},
);
m.add_method("IsA", |_, this, class_name: String| {
ensure_not_destroyed(this)?;
Ok(class_is_a(&this.class_name, class_name).unwrap_or(false))
Ok(class_is_a(this.class_name, class_name).unwrap_or(false))
});
m.add_method(
"IsAncestorOf",
@ -212,25 +211,22 @@ fn ensure_not_destroyed(inst: &Instance) -> LuaResult<()> {
3. Get a current child of the instance
4. No valid property or instance found, throw error
*/
fn instance_property_get<'lua>(
lua: &'lua Lua,
this: &Instance,
prop_name: String,
) -> LuaResult<LuaValue<'lua>> {
ensure_not_destroyed(this)?;
fn instance_property_get(lua: &Lua, this: &Instance, prop_name: String) -> LuaResult<LuaValue> {
match prop_name.as_str() {
"ClassName" => return this.get_class_name().into_lua(lua),
"Name" => {
return this.get_name().into_lua(lua);
}
"Parent" => {
return this.get_parent().into_lua(lua);
}
_ => {}
}
if let Some(info) = find_property_info(&this.class_name, &prop_name) {
ensure_not_destroyed(this)?;
if prop_name.as_str() == "Name" {
return this.get_name().into_lua(lua);
}
if let Some(info) = find_property_info(this.class_name, &prop_name) {
if let Some(prop) = this.get_property(&prop_name) {
if let DomValue::Enum(enum_value) = prop {
let enum_name = info.enum_name.ok_or_else(|| {
@ -275,7 +271,7 @@ fn instance_property_get<'lua>(
} else if let Some(inst) = this.find_child(|inst| inst.name == prop_name) {
Ok(LuaValue::UserData(lua.create_userdata(inst)?))
} else if let Some(getter) = InstanceRegistry::find_property_getter(lua, this, &prop_name) {
getter.call(this.clone())
getter.call(*this)
} else if let Some(method) = InstanceRegistry::find_method(lua, this, &prop_name) {
Ok(LuaValue::Function(method))
} else {
@ -295,10 +291,10 @@ fn instance_property_get<'lua>(
2a. Set a strict enum from a given EnumItem OR
2b. Set a normal property from a given value
*/
fn instance_property_set<'lua>(
lua: &'lua Lua,
fn instance_property_set(
lua: &Lua,
this: &mut Instance,
(prop_name, prop_value): (String, LuaValue<'lua>),
(prop_name, prop_value): (String, LuaValue),
) -> LuaResult<()> {
ensure_not_destroyed(this)?;
@ -319,19 +315,19 @@ fn instance_property_set<'lua>(
"Failed to set Parent - DataModel can not be reparented".to_string(),
));
}
type Parent<'lua> = Option<LuaUserDataRef<'lua, Instance>>;
type Parent = Option<LuaUserDataRef<Instance>>;
let parent = Parent::from_lua(prop_value, lua)?;
this.set_parent(parent.map(|p| p.clone()));
this.set_parent(parent.map(|p| *p));
return Ok(());
}
_ => {}
}
if let Some(info) = find_property_info(&this.class_name, &prop_name) {
if let Some(info) = find_property_info(this.class_name, &prop_name) {
if let Some(enum_name) = info.enum_name {
match LuaUserDataRef::<EnumItem>::from_lua(prop_value, lua) {
Ok(given_enum) if given_enum.parent.desc.name == enum_name => {
this.set_property(prop_name, DomValue::Enum((*given_enum).clone().into()));
this.set_property(prop_name, DomValue::EnumItem((*given_enum).clone().into()));
Ok(())
}
Ok(given_enum) => Err(LuaError::RuntimeError(format!(
@ -354,7 +350,7 @@ fn instance_property_set<'lua>(
)))
}
} else if let Some(setter) = InstanceRegistry::find_property_setter(lua, this, &prop_name) {
setter.call((this.clone(), prop_value))
setter.call((*this, prop_value))
} else {
Err(LuaError::RuntimeError(format!(
"{prop_name} is not a valid member of {this}",

View file

@ -12,11 +12,11 @@ use super::Instance;
pub const CLASS_NAME: &str = "DataModel";
pub fn add_fields<'lua, F: LuaUserDataFields<'lua, Instance>>(f: &mut F) {
pub fn add_fields<F: LuaUserDataFields<Instance>>(f: &mut F) {
add_class_restricted_getter(f, CLASS_NAME, "Workspace", data_model_get_workspace);
}
pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
pub fn add_methods<M: LuaUserDataMethods<Instance>>(m: &mut M) {
add_class_restricted_method(m, CLASS_NAME, "GetService", data_model_get_service);
add_class_restricted_method(m, CLASS_NAME, "FindService", data_model_find_service);
}
@ -26,7 +26,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
### See Also
* [`Terrain`](https://create.roblox.com/docs/reference/engine/classes/Workspace#Terrain)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
fn data_model_get_workspace(_: &Lua, this: &Instance) -> LuaResult<Instance> {
get_or_create_property_ref_instance(this, "Workspace", "Workspace")
@ -37,7 +37,7 @@ fn data_model_get_workspace(_: &Lua, this: &Instance) -> LuaResult<Instance> {
### See Also
* [`GetService`](https://create.roblox.com/docs/reference/engine/classes/ServiceProvider#GetService)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
fn data_model_get_service(_: &Lua, this: &Instance, service_name: String) -> LuaResult<Instance> {
if matches!(class_is_a_service(&service_name), None | Some(false)) {
@ -48,7 +48,7 @@ fn data_model_get_service(_: &Lua, this: &Instance, service_name: String) -> Lua
Ok(service)
} else {
let service = Instance::new_orphaned(service_name);
service.set_parent(Some(this.clone()));
service.set_parent(Some(*this));
Ok(service)
}
}
@ -58,7 +58,7 @@ fn data_model_get_service(_: &Lua, this: &Instance, service_name: String) -> Lua
### See Also
* [`FindService`](https://create.roblox.com/docs/reference/engine/classes/ServiceProvider#FindService)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
fn data_model_find_service(
_: &Lua,

View file

@ -4,14 +4,13 @@ use std::{
collections::{BTreeMap, VecDeque},
fmt,
hash::{Hash, Hasher},
sync::Mutex,
sync::{LazyLock, Mutex},
};
use mlua::prelude::*;
use once_cell::sync::Lazy;
use rbx_dom_weak::{
types::{Attributes as DomAttributes, Ref as DomRef, Variant as DomValue},
Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, WeakDom,
ustr, Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, Ustr, WeakDom,
};
use lune_utils::TableBuilder;
@ -31,13 +30,13 @@ pub mod registry;
const PROPERTY_NAME_ATTRIBUTES: &str = "Attributes";
const PROPERTY_NAME_TAGS: &str = "Tags";
static INTERNAL_DOM: Lazy<Mutex<WeakDom>> =
Lazy::new(|| Mutex::new(WeakDom::new(DomInstanceBuilder::new("ROOT"))));
static INTERNAL_DOM: LazyLock<Mutex<WeakDom>> =
LazyLock::new(|| Mutex::new(WeakDom::new(DomInstanceBuilder::new("ROOT"))));
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy)]
pub struct Instance {
pub(crate) dom_ref: DomRef,
pub(crate) class_name: String,
pub(crate) class_name: Ustr,
}
impl Instance {
@ -45,38 +44,26 @@ impl Instance {
Creates a new `Instance` from an existing dom object ref.
Panics if the instance does not exist in the internal dom,
or if the given dom object ref points to the dom root.
or if the given dom object ref points to the internal dom root.
**WARNING:** Creating a new instance requires locking the internal dom,
any existing lock must first be released to prevent any deadlocking.
*/
pub(crate) fn new(dom_ref: DomRef) -> Self {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let instance = dom
.get_by_ref(dom_ref)
.expect("Failed to find instance in document");
assert!(
!(instance.referent() == dom.root_ref()),
"Instances can not be created from dom roots"
);
Self {
dom_ref,
class_name: instance.class.clone(),
}
#[must_use]
pub fn new(dom_ref: DomRef) -> Self {
Self::new_opt(dom_ref).expect("Failed to find instance in document")
}
/**
Creates a new `Instance` from a dom object ref, if the instance exists.
Panics if the given dom object ref points to the dom root.
Panics if the given dom object ref points to the internal dom root.
**WARNING:** Creating a new instance requires locking the internal dom,
any existing lock must first be released to prevent any deadlocking.
*/
pub(crate) fn new_opt(dom_ref: DomRef) -> Option<Self> {
#[must_use]
pub fn new_opt(dom_ref: DomRef) -> Option<Self> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
if let Some(instance) = dom.get_by_ref(dom_ref) {
@ -87,7 +74,7 @@ impl Instance {
Some(Self {
dom_ref,
class_name: instance.class.clone(),
class_name: instance.class,
})
} else {
None
@ -97,24 +84,25 @@ impl Instance {
/**
Creates a new orphaned `Instance` with a given class name.
An orphaned instance is an instance at the root of a weak dom.
An orphaned instance is an instance at the root of Lune's internal weak dom.
**WARNING:** Creating a new instance requires locking the internal dom,
any existing lock must first be released to prevent any deadlocking.
*/
pub(crate) fn new_orphaned(class_name: impl AsRef<str>) -> Self {
#[must_use]
pub fn new_orphaned(class_name: impl AsRef<str>) -> Self {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let class_name = class_name.as_ref();
let instance = DomInstanceBuilder::new(class_name.to_string());
let instance = DomInstanceBuilder::new(class_name);
let dom_root = dom.root_ref();
let dom_ref = dom.insert(dom_root, instance);
Self {
dom_ref,
class_name: class_name.to_string(),
class_name: ustr(class_name),
}
}
@ -122,10 +110,11 @@ impl Instance {
Creates a new orphaned `Instance` by transferring
it from an external weak dom to the internal one.
An orphaned instance is an instance at the root of a weak dom.
An orphaned instance is an instance at the root of Lune's internal weak dom.
Panics if the given dom ref is the root dom ref of the external weak dom.
*/
#[must_use]
pub fn from_external_dom(external_dom: &mut WeakDom, external_dom_ref: DomRef) -> Self {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let dom_root = dom.root_ref();
@ -151,6 +140,12 @@ impl Instance {
cloned
}
/**
Clones multiple instances to an external weak dom.
This will place the instances as children of the
root of the weak dom, and return their referents.
*/
pub fn clone_multiple_into_external_dom(
referents: &[DomRef],
external_dom: &mut WeakDom,
@ -174,7 +169,7 @@ impl Instance {
### See Also
* [`Clone`](https://create.roblox.com/docs/reference/engine/classes/Instance#Clone)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
#[must_use]
pub fn clone_instance(&self) -> Self {
@ -198,7 +193,7 @@ impl Instance {
### See Also
* [`Destroy`](https://create.roblox.com/docs/reference/engine/classes/Instance#Destroy)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn destroy(&mut self) -> bool {
if self.is_destroyed() {
@ -225,7 +220,7 @@ impl Instance {
### See Also
* [`Instance::Destroy`] for more info about what happens when an instance gets destroyed
* [`ClearAllChildren`](https://create.roblox.com/docs/reference/engine/classes/Instance#ClearAllChildren)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn clear_all_children(&mut self) {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -245,10 +240,10 @@ impl Instance {
### See Also
* [`IsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#IsA)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn is_a(&self, class_name: impl AsRef<str>) -> bool {
class_is_a(&self.class_name, class_name).unwrap_or(false)
class_is_a(self.class_name, class_name).unwrap_or(false)
}
/**
@ -258,7 +253,7 @@ impl Instance {
### See Also
* [`ClassName`](https://create.roblox.com/docs/reference/engine/classes/Instance#ClassName)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
#[must_use]
pub fn get_class_name(&self) -> &str {
@ -270,7 +265,7 @@ impl Instance {
### See Also
* [`Name`](https://create.roblox.com/docs/reference/engine/classes/Instance#Name)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn get_name(&self) -> String {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -286,7 +281,7 @@ impl Instance {
### See Also
* [`Name`](https://create.roblox.com/docs/reference/engine/classes/Instance#Name)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn set_name(&self, name: impl Into<String>) {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -301,15 +296,12 @@ impl Instance {
### See Also
* [`Parent`](https://create.roblox.com/docs/reference/engine/classes/Instance#Parent)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn get_parent(&self) -> Option<Instance> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let parent_ref = dom
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document")
.parent();
let parent_ref = dom.get_by_ref(self.dom_ref)?.parent();
if parent_ref == dom.root_ref() {
None
@ -324,11 +316,11 @@ impl Instance {
If the provided parent is [`None`] the instance will become orphaned.
An orphaned instance is an instance at the root of a weak dom.
An orphaned instance is an instance at the root of Lune's internal weak dom.
### See Also
* [`Parent`](https://create.roblox.com/docs/reference/engine/classes/Instance#Parent)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn set_parent(&self, parent: Option<Instance>) {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -348,7 +340,7 @@ impl Instance {
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document")
.properties
.get(name.as_ref())
.get(&ustr(name.as_ref()))
.cloned()
}
@ -365,7 +357,7 @@ impl Instance {
.get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document")
.properties
.insert(name.as_ref().to_string(), value);
.insert(ustr(name.as_ref()), value);
}
/**
@ -373,7 +365,7 @@ impl Instance {
### See Also
* [`GetAttribute`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttribute)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn get_attribute(&self, name: impl AsRef<str>) -> Option<DomValue> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -381,7 +373,7 @@ impl Instance {
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Attributes(attributes)) =
inst.properties.get(PROPERTY_NAME_ATTRIBUTES)
inst.properties.get(&ustr(PROPERTY_NAME_ATTRIBUTES))
{
attributes.get(name.as_ref()).cloned()
} else {
@ -394,7 +386,7 @@ impl Instance {
### See Also
* [`GetAttributes`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttributes)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn get_attributes(&self) -> BTreeMap<String, DomValue> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -402,7 +394,7 @@ impl Instance {
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Attributes(attributes)) =
inst.properties.get(PROPERTY_NAME_ATTRIBUTES)
inst.properties.get(&ustr(PROPERTY_NAME_ATTRIBUTES))
{
attributes.clone().into_iter().collect()
} else {
@ -415,7 +407,7 @@ impl Instance {
### See Also
* [`SetAttribute`](https://create.roblox.com/docs/reference/engine/classes/Instance#SetAttribute)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn set_attribute(&self, name: impl AsRef<str>, value: DomValue) {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -429,14 +421,14 @@ impl Instance {
value => value,
};
if let Some(DomValue::Attributes(attributes)) =
inst.properties.get_mut(PROPERTY_NAME_ATTRIBUTES)
inst.properties.get_mut(&ustr(PROPERTY_NAME_ATTRIBUTES))
{
attributes.insert(name.as_ref().to_string(), value);
} else {
let mut attributes = DomAttributes::new();
attributes.insert(name.as_ref().to_string(), value);
inst.properties.insert(
PROPERTY_NAME_ATTRIBUTES.to_string(),
ustr(PROPERTY_NAME_ATTRIBUTES),
DomValue::Attributes(attributes),
);
}
@ -456,11 +448,11 @@ impl Instance {
.get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Attributes(attributes)) =
inst.properties.get_mut(PROPERTY_NAME_ATTRIBUTES)
inst.properties.get_mut(&ustr(PROPERTY_NAME_ATTRIBUTES))
{
attributes.remove(name.as_ref());
if attributes.is_empty() {
inst.properties.remove(PROPERTY_NAME_ATTRIBUTES);
inst.properties.remove(&ustr(PROPERTY_NAME_ATTRIBUTES));
}
}
}
@ -470,18 +462,18 @@ impl Instance {
### See Also
* [`AddTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#AddTag)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn add_tag(&self, name: impl AsRef<str>) {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let inst = dom
.get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(PROPERTY_NAME_TAGS) {
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(&ustr(PROPERTY_NAME_TAGS)) {
tags.push(name.as_ref());
} else {
inst.properties.insert(
PROPERTY_NAME_TAGS.to_string(),
ustr(PROPERTY_NAME_TAGS),
DomValue::Tags(vec![name.as_ref().to_string()].into()),
);
}
@ -492,14 +484,14 @@ impl Instance {
### See Also
* [`GetTags`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#GetTags)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn get_tags(&self) -> Vec<String> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let inst = dom
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Tags(tags)) = inst.properties.get(PROPERTY_NAME_TAGS) {
if let Some(DomValue::Tags(tags)) = inst.properties.get(&ustr(PROPERTY_NAME_TAGS)) {
tags.iter().map(ToString::to_string).collect()
} else {
Vec::new()
@ -511,14 +503,14 @@ impl Instance {
### See Also
* [`HasTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#HasTag)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn has_tag(&self, name: impl AsRef<str>) -> bool {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let inst = dom
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Tags(tags)) = inst.properties.get(PROPERTY_NAME_TAGS) {
if let Some(DomValue::Tags(tags)) = inst.properties.get(&ustr(PROPERTY_NAME_TAGS)) {
let name = name.as_ref();
tags.iter().any(|tag| tag == name)
} else {
@ -531,21 +523,19 @@ impl Instance {
### See Also
* [`RemoveTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#RemoveTag)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn remove_tag(&self, name: impl AsRef<str>) {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let inst = dom
.get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(PROPERTY_NAME_TAGS) {
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(&ustr(PROPERTY_NAME_TAGS)) {
let name = name.as_ref();
let mut new_tags = tags.iter().map(ToString::to_string).collect::<Vec<_>>();
new_tags.retain(|tag| tag != name);
inst.properties.insert(
PROPERTY_NAME_TAGS.to_string(),
DomValue::Tags(new_tags.into()),
);
inst.properties
.insert(ustr(PROPERTY_NAME_TAGS), DomValue::Tags(new_tags.into()));
}
}
@ -557,7 +547,7 @@ impl Instance {
### See Also
* [`GetChildren`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetChildren)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn get_children(&self) -> Vec<Instance> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -580,7 +570,7 @@ impl Instance {
### See Also
* [`GetDescendants`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetDescendants)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn get_descendants(&self) -> Vec<Instance> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -614,7 +604,7 @@ impl Instance {
### See Also
* [`GetFullName`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetFullName)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn get_full_name(&self) -> String {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -703,8 +693,7 @@ impl Instance {
predicate callback and a breadth-first search.
### See Also
* [`FindFirstDescendant`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstDescendant)
on the Roblox Developer Hub
* [`FindFirstDescendant`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstDescendant) on the Roblox Developer Hub
*/
pub fn find_descendant<F>(&self, predicate: F) -> Option<Instance>
where
@ -734,11 +723,11 @@ impl Instance {
}
}
impl LuaExportsTable<'_> for Instance {
impl LuaExportsTable for Instance {
const EXPORT_NAME: &'static str = "Instance";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
let instance_new = |lua, class_name: String| {
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
let instance_new = |lua: &Lua, class_name: String| {
if class_exists(&class_name) {
Instance::new_orphaned(class_name).into_lua(lua)
} else {
@ -767,12 +756,12 @@ impl LuaExportsTable<'_> for Instance {
instance registry, and register properties + methods from the lua side
*/
impl LuaUserData for Instance {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
data_model::add_fields(fields);
workspace::add_fields(fields);
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
base::add_methods(methods);
data_model::add_methods(methods);
terrain::add_methods(methods);

View file

@ -58,11 +58,11 @@ impl InstanceRegistry {
- If the method already exists in the registry.
*/
pub fn insert_method<'lua>(
lua: &'lua Lua,
pub fn insert_method(
lua: &Lua,
class_name: &str,
method_name: &str,
method: LuaFunction<'lua>,
method: LuaFunction,
) -> Result<(), InstanceRegistryError> {
let registry = Self::get_or_create(lua);
@ -94,11 +94,11 @@ impl InstanceRegistry {
- If the property already exists in the registry.
*/
pub fn insert_property_getter<'lua>(
lua: &'lua Lua,
pub fn insert_property_getter(
lua: &Lua,
class_name: &str,
property_name: &str,
property_getter: LuaFunction<'lua>,
property_getter: LuaFunction,
) -> Result<(), InstanceRegistryError> {
let registry = Self::get_or_create(lua);
@ -130,11 +130,11 @@ impl InstanceRegistry {
- If the property already exists in the registry.
*/
pub fn insert_property_setter<'lua>(
lua: &'lua Lua,
pub fn insert_property_setter(
lua: &Lua,
class_name: &str,
property_name: &str,
property_setter: LuaFunction<'lua>,
property_setter: LuaFunction,
) -> Result<(), InstanceRegistryError> {
let registry = Self::get_or_create(lua);
@ -165,11 +165,7 @@ impl InstanceRegistry {
Returns `None` if the method is not found.
*/
#[must_use]
pub fn find_method<'lua>(
lua: &'lua Lua,
instance: &Instance,
method_name: &str,
) -> Option<LuaFunction<'lua>> {
pub fn find_method(lua: &Lua, instance: &Instance, method_name: &str) -> Option<LuaFunction> {
let registry = Self::get_or_create(lua);
let methods = registry
.methods
@ -192,11 +188,11 @@ impl InstanceRegistry {
Returns `None` if the property getter is not found.
*/
#[must_use]
pub fn find_property_getter<'lua>(
lua: &'lua Lua,
pub fn find_property_getter(
lua: &Lua,
instance: &Instance,
property_name: &str,
) -> Option<LuaFunction<'lua>> {
) -> Option<LuaFunction> {
let registry = Self::get_or_create(lua);
let getters = registry
.getters
@ -219,11 +215,11 @@ impl InstanceRegistry {
Returns `None` if the property setter is not found.
*/
#[must_use]
pub fn find_property_setter<'lua>(
lua: &'lua Lua,
pub fn find_property_setter(
lua: &Lua,
instance: &Instance,
property_name: &str,
) -> Option<LuaFunction<'lua>> {
) -> Option<LuaFunction> {
let registry = Self::get_or_create(lua);
let setters = registry
.setters

View file

@ -10,7 +10,7 @@ use super::Instance;
pub const CLASS_NAME: &str = "Terrain";
pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(methods: &mut M) {
pub fn add_methods<M: LuaUserDataMethods<Instance>>(methods: &mut M) {
add_class_restricted_method(
methods,
CLASS_NAME,
@ -27,9 +27,8 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(methods: &mut M)
}
fn get_or_create_material_colors(instance: &Instance) -> MaterialColors {
if let Some(Variant::MaterialColors(material_colors)) = instance.get_property("MaterialColors")
{
material_colors
if let Some(Variant::MaterialColors(inner)) = instance.get_property("MaterialColors") {
inner
} else {
MaterialColors::default()
}
@ -40,7 +39,7 @@ fn get_or_create_material_colors(instance: &Instance) -> MaterialColors {
### See Also
* [`GetMaterialColor`](https://create.roblox.com/docs/reference/engine/classes/Terrain#GetMaterialColor)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
fn terrain_get_material_color(_: &Lua, this: &Instance, material: EnumItem) -> LuaResult<Color3> {
let material_colors = get_or_create_material_colors(this);
@ -65,7 +64,7 @@ fn terrain_get_material_color(_: &Lua, this: &Instance, material: EnumItem) -> L
### See Also
* [`SetMaterialColor`](https://create.roblox.com/docs/reference/engine/classes/Terrain#SetMaterialColor)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
fn terrain_set_material_color(
_: &Lua,

View file

@ -6,7 +6,7 @@ use super::Instance;
pub const CLASS_NAME: &str = "Workspace";
pub fn add_fields<'lua, F: LuaUserDataFields<'lua, Instance>>(f: &mut F) {
pub fn add_fields<F: LuaUserDataFields<Instance>>(f: &mut F) {
add_class_restricted_getter(f, CLASS_NAME, "Terrain", workspace_get_terrain);
add_class_restricted_getter(f, CLASS_NAME, "CurrentCamera", workspace_get_camera);
}
@ -16,7 +16,7 @@ pub fn add_fields<'lua, F: LuaUserDataFields<'lua, Instance>>(f: &mut F) {
### See Also
* [`Terrain`](https://create.roblox.com/docs/reference/engine/classes/Workspace#Terrain)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
fn workspace_get_terrain(_: &Lua, this: &Instance) -> LuaResult<Instance> {
get_or_create_property_ref_instance(this, "Terrain", "Terrain")
@ -27,7 +27,7 @@ fn workspace_get_terrain(_: &Lua, this: &Instance) -> LuaResult<Instance> {
### See Also
* [`CurrentCamera`](https://create.roblox.com/docs/reference/engine/classes/Workspace#CurrentCamera)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
fn workspace_get_camera(_: &Lua, this: &Instance) -> LuaResult<Instance> {
get_or_create_property_ref_instance(this, "CurrentCamera", "Camera")

View file

@ -14,37 +14,38 @@ pub(crate) mod shared;
use exports::export;
fn create_all_exports(lua: &Lua) -> LuaResult<Vec<(&'static str, LuaValue)>> {
fn create_all_exports(lua: Lua) -> LuaResult<Vec<(&'static str, LuaValue)>> {
use datatypes::types::*;
use instance::Instance;
Ok(vec![
// Datatypes
export::<Axes>(lua)?,
export::<BrickColor>(lua)?,
export::<CFrame>(lua)?,
export::<Color3>(lua)?,
export::<ColorSequence>(lua)?,
export::<ColorSequenceKeypoint>(lua)?,
export::<Faces>(lua)?,
export::<Font>(lua)?,
export::<NumberRange>(lua)?,
export::<NumberSequence>(lua)?,
export::<NumberSequenceKeypoint>(lua)?,
export::<PhysicalProperties>(lua)?,
export::<Ray>(lua)?,
export::<Rect>(lua)?,
export::<UDim>(lua)?,
export::<UDim2>(lua)?,
export::<Region3>(lua)?,
export::<Region3int16>(lua)?,
export::<Vector2>(lua)?,
export::<Vector2int16>(lua)?,
export::<Vector3>(lua)?,
export::<Vector3int16>(lua)?,
export::<Axes>(lua.clone())?,
export::<BrickColor>(lua.clone())?,
export::<CFrame>(lua.clone())?,
export::<Color3>(lua.clone())?,
export::<ColorSequence>(lua.clone())?,
export::<ColorSequenceKeypoint>(lua.clone())?,
export::<Content>(lua.clone())?,
export::<Faces>(lua.clone())?,
export::<Font>(lua.clone())?,
export::<NumberRange>(lua.clone())?,
export::<NumberSequence>(lua.clone())?,
export::<NumberSequenceKeypoint>(lua.clone())?,
export::<PhysicalProperties>(lua.clone())?,
export::<Ray>(lua.clone())?,
export::<Rect>(lua.clone())?,
export::<UDim>(lua.clone())?,
export::<UDim2>(lua.clone())?,
export::<Region3>(lua.clone())?,
export::<Region3int16>(lua.clone())?,
export::<Vector2>(lua.clone())?,
export::<Vector2int16>(lua.clone())?,
export::<Vector3>(lua.clone())?,
export::<Vector3int16>(lua.clone())?,
// Classes
export::<Instance>(lua)?,
export::<Instance>(lua.clone())?,
// Singletons
("Enum", Enums.into_lua(lua)?),
("Enum", Enums.into_lua(&lua)?),
])
}
@ -58,13 +59,13 @@ fn create_all_exports(lua: &Lua) -> LuaResult<Vec<(&'static str, LuaValue)>> {
Errors when out of memory or when a value cannot be created.
*/
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
pub fn module(lua: Lua) -> LuaResult<LuaTable> {
// FUTURE: We can probably create these lazily as users
// index the main exports (this return value) table and
// save some memory and startup time. The full exports
// table is quite big and probably won't get any smaller
// since we impl all roblox constructors for each datatype.
let exports = create_all_exports(lua)?;
let exports = create_all_exports(lua.clone())?;
TableBuilder::new(lua)?
.with_values(exports)?
.build_readonly()

View file

@ -85,7 +85,7 @@ impl DatabaseClass {
}
impl LuaUserData for DatabaseClass {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Name", |_, this| Ok(this.get_name()));
fields.add_field_method_get("Superclass", |_, this| Ok(this.get_superclass()));
fields.add_field_method_get("Properties", |_, this| Ok(this.get_properties()));
@ -108,7 +108,7 @@ impl LuaUserData for DatabaseClass {
fields.add_field_method_get("Tags", |_, this| Ok(this.get_tags_str()));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}

View file

@ -45,12 +45,12 @@ impl DatabaseEnum {
}
impl LuaUserData for DatabaseEnum {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Name", |_, this| Ok(this.get_name()));
fields.add_field_method_get("Items", |_, this| Ok(this.get_items()));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}

View file

@ -111,11 +111,11 @@ impl Database {
}
impl LuaUserData for Database {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Version", |_, this| Ok(this.get_version()));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<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()));

View file

@ -69,14 +69,14 @@ impl DatabaseProperty {
}
impl LuaUserData for DatabaseProperty {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("Name", |_, this| Ok(this.get_name()));
fields.add_field_method_get("Datatype", |_, this| Ok(this.get_datatype_name()));
fields.add_field_method_get("Scriptability", |_, this| Ok(this.get_scriptability_str()));
fields.add_field_method_get("Tags", |_, this| Ok(this.get_tags_str()));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}

View file

@ -6,14 +6,14 @@ use crate::instance::Instance;
use super::instance::class_is_a;
pub(crate) fn add_class_restricted_getter<'lua, F: LuaUserDataFields<'lua, Instance>, R, G>(
pub(crate) fn add_class_restricted_getter<F: LuaUserDataFields<Instance>, R, G>(
fields: &mut F,
class_name: &'static str,
field_name: &'static str,
field_getter: G,
) where
R: IntoLua<'lua>,
G: 'static + Fn(&'lua Lua, &Instance) -> LuaResult<R>,
R: IntoLua,
G: 'static + Fn(&Lua, &Instance) -> LuaResult<R>,
{
fields.add_field_method_get(field_name, move |lua, this| {
if class_is_a(this.get_class_name(), class_name).unwrap_or(false) {
@ -27,14 +27,14 @@ pub(crate) fn add_class_restricted_getter<'lua, F: LuaUserDataFields<'lua, Insta
}
#[allow(dead_code)]
pub(crate) fn add_class_restricted_setter<'lua, F: LuaUserDataFields<'lua, Instance>, A, G>(
pub(crate) fn add_class_restricted_setter<F: LuaUserDataFields<Instance>, A, G>(
fields: &mut F,
class_name: &'static str,
field_name: &'static str,
field_getter: G,
) where
A: FromLua<'lua>,
G: 'static + Fn(&'lua Lua, &Instance, A) -> LuaResult<()>,
A: FromLua,
G: 'static + Fn(&Lua, &Instance, A) -> LuaResult<()>,
{
fields.add_field_method_set(field_name, move |lua, this, value| {
if class_is_a(this.get_class_name(), class_name).unwrap_or(false) {
@ -47,15 +47,15 @@ pub(crate) fn add_class_restricted_setter<'lua, F: LuaUserDataFields<'lua, Insta
});
}
pub(crate) fn add_class_restricted_method<'lua, M: LuaUserDataMethods<'lua, Instance>, A, R, F>(
pub(crate) fn add_class_restricted_method<M: LuaUserDataMethods<Instance>, A, R, F>(
methods: &mut M,
class_name: &'static str,
method_name: &'static str,
method: F,
) where
A: FromLuaMulti<'lua>,
R: IntoLuaMulti<'lua>,
F: 'static + Fn(&'lua Lua, &Instance, A) -> LuaResult<R>,
A: FromLuaMulti,
R: IntoLuaMulti,
F: 'static + Fn(&Lua, &Instance, A) -> LuaResult<R>,
{
methods.add_method(method_name, move |lua, this, args| {
if class_is_a(this.get_class_name(), class_name).unwrap_or(false) {
@ -68,21 +68,15 @@ pub(crate) fn add_class_restricted_method<'lua, M: LuaUserDataMethods<'lua, Inst
});
}
pub(crate) fn add_class_restricted_method_mut<
'lua,
M: LuaUserDataMethods<'lua, Instance>,
A,
R,
F,
>(
pub(crate) fn add_class_restricted_method_mut<M: LuaUserDataMethods<Instance>, A, R, F>(
methods: &mut M,
class_name: &'static str,
method_name: &'static str,
method: F,
) where
A: FromLuaMulti<'lua>,
R: IntoLuaMulti<'lua>,
F: 'static + Fn(&'lua Lua, &mut Instance, A) -> LuaResult<R>,
A: FromLuaMulti,
R: IntoLuaMulti,
F: 'static + Fn(&Lua, &mut Instance, A) -> LuaResult<R>,
{
methods.add_method_mut(method_name, move |lua, this, args| {
if class_is_a(this.get_class_name(), class_name).unwrap_or(false) {
@ -122,7 +116,7 @@ pub(crate) fn get_or_create_property_ref_instance(
Ok(inst)
} else {
let inst = Instance::new_orphaned(class_name);
inst.set_parent(Some(this.clone()));
inst.set_parent(Some(*this));
this.set_property(prop_name, DomValue::Ref(inst.dom_ref));
Ok(inst)
}

View file

@ -23,7 +23,7 @@ pub fn make_list_writer() -> Box<ListWriter> {
})
}
/**
/*
Userdata metamethod implementations
Note that many of these return [`LuaResult`] even though they don't
@ -87,10 +87,10 @@ where
}
}
_ => {}
};
}
Err(LuaError::FromLuaConversionError {
from: rhs.type_name(),
to: type_name::<D>(),
to: type_name::<D>().to_string(),
message: Some(format!(
"Expected {} or number, got {}",
type_name::<D>(),
@ -112,10 +112,10 @@ where
}
}
_ => {}
};
}
Err(LuaError::FromLuaConversionError {
from: rhs.type_name(),
to: type_name::<D>(),
to: type_name::<D>().to_string(),
message: Some(format!(
"Expected {} or number, got {}",
type_name::<D>(),
@ -137,10 +137,10 @@ where
}
}
_ => {}
};
}
Err(LuaError::FromLuaConversionError {
from: rhs.type_name(),
to: type_name::<D>(),
to: type_name::<D>().to_string(),
message: Some(format!(
"Expected {} or number, got {}",
type_name::<D>(),
@ -168,10 +168,10 @@ where
}
}
_ => {}
};
}
Err(LuaError::FromLuaConversionError {
from: rhs.type_name(),
to: type_name::<D>(),
to: type_name::<D>().to_string(),
message: Some(format!(
"Expected {} or number, got {}",
type_name::<D>(),
@ -193,10 +193,10 @@ where
}
}
_ => {}
};
}
Err(LuaError::FromLuaConversionError {
from: rhs.type_name(),
to: type_name::<D>(),
to: type_name::<D>().to_string(),
message: Some(format!(
"Expected {} or number, got {}",
type_name::<D>(),

View file

@ -1,6 +1,6 @@
[package]
name = "lune-std-datetime"
version = "0.1.2"
version = "0.2.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
@ -13,10 +13,10 @@ path = "src/lib.rs"
workspace = true
[dependencies]
mlua = { version = "0.9.7", features = ["luau"] }
mlua = { version = "0.10.3", features = ["luau"] }
thiserror = "1.0"
thiserror = "2.0"
chrono = "0.4.38"
chrono_lc = "0.1.6"
lune-utils = { version = "0.1.2", path = "../lune-utils" }
lune-utils = { version = "0.2.2", path = "../lune-utils" }

View file

@ -159,7 +159,7 @@ impl DateTime {
}
/**
Parses a time string in the ISO 8601 format, such as
Parses a time string in the RFC 3339 format, such as
`1996-12-19T16:39:57-08:00`, into a new `DateTime` struct.
See [`chrono::DateTime::parse_from_rfc3339`] for additional details.
@ -168,8 +168,23 @@ impl DateTime {
Returns an error if the input string is not a valid RFC 3339 date-time.
*/
pub fn from_iso_date(iso_date: impl AsRef<str>) -> DateTimeResult<Self> {
let inner = ChronoDateTime::parse_from_rfc3339(iso_date.as_ref())?.with_timezone(&Utc);
pub fn from_rfc_3339(date: impl AsRef<str>) -> DateTimeResult<Self> {
let inner = ChronoDateTime::parse_from_rfc3339(date.as_ref())?.with_timezone(&Utc);
Ok(Self { inner })
}
/**
Parses a time string in the RFC 2822 format, such as
`Tue, 1 Jul 2003 10:52:37 +0200`, into a new `DateTime` struct.
See [`chrono::DateTime::parse_from_rfc2822`] for additional details.
# Errors
Returns an error if the input string is not a valid RFC 2822 date-time.
*/
pub fn from_rfc_2822(date: impl AsRef<str>) -> DateTimeResult<Self> {
let inner = ChronoDateTime::parse_from_rfc2822(date.as_ref())?.with_timezone(&Utc);
Ok(Self { inner })
}
@ -192,25 +207,35 @@ impl DateTime {
}
/**
Formats a time string in the ISO 8601 format, such as `1996-12-19T16:39:57-08:00`.
Formats a time string in the RFC 3339 format, such as `1996-12-19T16:39:57-08:00`.
See [`chrono::DateTime::to_rfc3339`] for additional details.
*/
#[must_use]
pub fn to_iso_date(self) -> String {
pub fn to_rfc_3339(self) -> String {
self.inner.to_rfc3339()
}
/**
Formats a time string in the RFC 2822 format, such as `Tue, 1 Jul 2003 10:52:37 +0200`.
See [`chrono::DateTime::to_rfc2822`] for additional details.
*/
#[must_use]
pub fn to_rfc_2822(self) -> String {
self.inner.to_rfc2822()
}
}
impl LuaUserData for DateTime {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("unixTimestamp", |_, this| Ok(this.inner.timestamp()));
fields.add_field_method_get("unixTimestampMillis", |_, this| {
Ok(this.inner.timestamp_millis())
});
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
// Metamethods to compare DateTime as instants in time
methods.add_meta_method(
LuaMetaMethod::Eq,
@ -229,7 +254,9 @@ impl LuaUserData for DateTime {
},
);
// Normal methods
methods.add_method("toIsoDate", |_, this, ()| Ok(this.to_iso_date()));
methods.add_method("toIsoDate", |_, this, ()| Ok(this.to_rfc_3339())); // FUTURE: Remove this rfc3339 alias method
methods.add_method("toRfc3339", |_, this, ()| Ok(this.to_rfc_3339()));
methods.add_method("toRfc2822", |_, this, ()| Ok(this.to_rfc_2822()));
methods.add_method(
"formatUniversalTime",
|_, this, (format, locale): (Option<String>, Option<String>)| {

View file

@ -10,6 +10,16 @@ mod values;
pub use self::date_time::DateTime;
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
/**
Returns a string containing type definitions for the `datetime` standard library.
*/
#[must_use]
pub fn typedefs() -> String {
TYPEDEFS.to_string()
}
/**
Creates the `datetime` standard library module.
@ -17,10 +27,16 @@ pub use self::date_time::DateTime;
Errors when out of memory.
*/
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
pub fn module(lua: Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)?
.with_function("fromIsoDate", |_, iso_date: String| {
Ok(DateTime::from_iso_date(iso_date)?)
.with_function("fromIsoDate", |_, date: String| {
Ok(DateTime::from_rfc_3339(date)?) // FUTURE: Remove this rfc3339 alias method
})?
.with_function("fromRfc3339", |_, date: String| {
Ok(DateTime::from_rfc_3339(date)?)
})?
.with_function("fromRfc2822", |_, date: String| {
Ok(DateTime::from_rfc_2822(date)?)
})?
.with_function("fromLocalTime", |_, values| {
Ok(DateTime::from_local_time(&values)?)

View file

@ -60,7 +60,7 @@ where
}
}
/**
/*
Conversion methods between `DateTimeValues` and plain lua tables
Note that the `IntoLua` implementation here uses a read-only table,
@ -68,15 +68,15 @@ where
a fixed point in time, and we guarantee that it doesn't change
*/
impl FromLua<'_> for DateTimeValues {
impl FromLua for DateTimeValues {
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
if !value.is_table() {
return Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "DateTimeValues",
to: "DateTimeValues".to_string(),
message: Some("value must be a table".to_string()),
});
};
}
let value = value.as_table().unwrap();
let values = Self {
@ -93,16 +93,16 @@ impl FromLua<'_> for DateTimeValues {
Ok(dt) => Ok(dt),
Err(e) => Err(LuaError::FromLuaConversionError {
from: "table",
to: "DateTimeValues",
to: "DateTimeValues".to_string(),
message: Some(e.to_string()),
}),
}
}
}
impl IntoLua<'_> for DateTimeValues {
impl IntoLua for DateTimeValues {
fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> {
let tab = TableBuilder::new(lua)?
let tab = TableBuilder::new(lua.clone())?
.with_value("year", self.year)?
.with_values(vec![
("month", self.month),
@ -117,7 +117,7 @@ impl IntoLua<'_> for DateTimeValues {
}
}
/**
/*
Conversion methods between chrono's timezone-aware `DateTime` to
and from our non-timezone-aware `DateTimeValues` values struct
*/

View file

@ -87,10 +87,19 @@ export type DateTimeValueArguments = DateTimeValues & OptionalMillisecond
]=]
export type DateTimeValueReturns = DateTimeValues & Millisecond
--[=[
@prop unixTimestamp number
@within DateTime
Number of seconds passed since the UNIX epoch.
]=]
--[=[
@prop unixTimestampMillis number
@within DateTime
Number of milliseconds passed since the UNIX epoch.
]=]
local DateTime = {
--- Number of seconds passed since the UNIX epoch.
unixTimestamp = (nil :: any) :: number,
--- Number of milliseconds passed since the UNIX epoch.
unixTimestampMillis = (nil :: any) :: number,
}
@ -154,11 +163,7 @@ end
@param locale -- The locale the time should be formatted in
@return string -- The formatting string
]=]
function DateTime.formatUniversalTime(
self: DateTime,
formatString: string?,
locale: Locale?
): string
function DateTime.formatUniversalTime(self: DateTime, formatString: string?, locale: Locale?): string
return nil :: any
end
@ -166,6 +171,8 @@ end
@within DateTime
@tag Method
**DEPRECATED**: Use `DateTime.toRfc3339` instead.
Formats this `DateTime` as an ISO 8601 date-time string.
Some examples of ISO 8601 date-time strings are:
@ -180,6 +187,42 @@ function DateTime.toIsoDate(self: DateTime): string
return nil :: any
end
--[=[
@within DateTime
@tag Method
Formats this `DateTime` as an RFC 2822 date-time string.
Some examples of RFC 2822 date-time strings are:
- `Fri, 21 Nov 1997 09:55:06 -0600`
- `Tue, 1 Jul 2003 10:52:37 +0200`
- `Mon, 23 Dec 2024 01:58:48 GMT`
@return string -- The RFC 2822 formatted string
]=]
function DateTime.toRfc2822(self: DateTime): string
return nil :: any
end
--[=[
@within DateTime
@tag Method
Formats this `DateTime` as an RFC 3339 date-time string.
Some examples of RFC 3339 date-time strings are:
- `2020-02-22T18:12:08Z`
- `2000-01-31T12:34:56+05:00`
- `1970-01-01T00:00:00.055Z`
@return string -- The RFC 3339 formatted string
]=]
function DateTime.toRfc3339(self: DateTime): string
return nil :: any
end
--[=[
@within DateTime
@tag Method
@ -243,8 +286,11 @@ export type DateTime = typeof(DateTime)
-- Creates a DateTime for the current exact moment in time
local now = DateTime.now()
-- Formats the current moment in time as an ISO 8601 string
print(now:toIsoDate())
-- Formats the current moment in time as an RFC 3339 string
print(now:toRfc3339())
-- Formats the current moment in time as an RFC 2822 string
print(now:toRfc2822())
-- Formats the current moment in time, using the local
-- time, the French locale, and the specified time string
@ -385,6 +431,8 @@ end
@within DateTime
@tag Constructor
**DEPRECATED**: Use `DateTime.fromRfc3339` instead.
Creates a new `DateTime` from an ISO 8601 date-time string.
### Errors
@ -405,4 +453,52 @@ function dateTime.fromIsoDate(isoDate: string): DateTime
return nil :: any
end
--[=[
@within DateTime
@tag Constructor
Creates a new `DateTime` from an RFC 3339 date-time string.
### Errors
This constructor is fallible and may throw an error if the given
string does not strictly follow the RFC 3339 date-time string format.
Some examples of valid RFC 3339 date-time strings are:
- `2020-02-22T18:12:08Z`
- `2000-01-31T12:34:56+05:00`
- `1970-01-01T00:00:00.055Z`
@param rfc3339Date -- An RFC 3339 formatted string
@return DateTime -- The new DateTime object
]=]
function dateTime.fromRfc3339(rfc3339Date: string): DateTime
return nil :: any
end
--[=[
@within DateTime
@tag Constructor
Creates a new `DateTime` from an RFC 2822 date-time string.
### Errors
This constructor is fallible and may throw an error if the given
string does not strictly follow the RFC 2822 date-time string format.
Some examples of valid RFC 2822 date-time strings are:
- `Fri, 21 Nov 1997 09:55:06 -0600`
- `Tue, 1 Jul 2003 10:52:37 +0200`
- `Mon, 23 Dec 2024 01:58:48 GMT`
@param rfc2822Date -- An RFC 2822 formatted string
@return DateTime -- The new DateTime object
]=]
function dateTime.fromRfc2822(rfc2822Date: string): DateTime
return nil :: any
end
return dateTime

View file

@ -1,6 +1,6 @@
[package]
name = "lune-std-fs"
version = "0.1.1"
version = "0.2.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
@ -13,11 +13,11 @@ path = "src/lib.rs"
workspace = true
[dependencies]
mlua = { version = "0.9.7", features = ["luau"] }
mlua = { version = "0.10.3", features = ["luau"] }
async-fs = "2.1"
bstr = "1.9"
futures-lite = "2.6"
tokio = { version = "1", default-features = false, features = ["fs"] }
lune-utils = { version = "0.1.2", path = "../lune-utils" }
lune-std-datetime = { version = "0.1.1", path = "../lune-std-datetime" }
lune-utils = { version = "0.2.2", path = "../lune-utils" }
lune-std-datetime = { version = "0.2.2", path = "../lune-std-datetime" }

View file

@ -2,8 +2,9 @@ use std::collections::VecDeque;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use async_fs as fs;
use futures_lite::prelude::*;
use mlua::prelude::*;
use tokio::fs;
use super::options::FsWriteOptions;
@ -24,8 +25,8 @@ async fn get_contents_at(root: PathBuf, _: FsWriteOptions) -> LuaResult<CopyCont
})?;
// Push initial children of the root path into the queue
let mut entries = fs::read_dir(&normalized_root).await?;
while let Some(entry) = entries.next_entry().await? {
let mut reader = fs::read_dir(&normalized_root).await?;
while let Some(entry) = reader.try_next().await? {
queue.push_back((1, entry.path()));
}
@ -42,7 +43,7 @@ async fn get_contents_at(root: PathBuf, _: FsWriteOptions) -> LuaResult<CopyCont
} else if meta.is_dir() {
// FUTURE: Add an option in FsWriteOptions for max depth and limit it here
let mut entries = fs::read_dir(&current_path).await?;
while let Some(entry) = entries.next_entry().await? {
while let Some(entry) = entries.try_next().await? {
queue.push_back((current_depth + 1, entry.path()));
}
dirs.push((current_depth, current_path));

64
crates/lune-std-fs/src/lib.rs Normal file → Executable file
View file

@ -1,11 +1,12 @@
#![allow(clippy::cargo_common_metadata)]
use std::io::ErrorKind as IoErrorKind;
use std::path::{PathBuf, MAIN_SEPARATOR};
use std::path::PathBuf;
use async_fs as fs;
use bstr::{BString, ByteSlice};
use futures_lite::prelude::*;
use mlua::prelude::*;
use tokio::fs;
use lune_utils::TableBuilder;
@ -17,6 +18,16 @@ use self::copy::copy;
use self::metadata::FsMetadata;
use self::options::FsWriteOptions;
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
/**
Returns a string containing type definitions for the `fs` standard library.
*/
#[must_use]
pub fn typedefs() -> String {
TYPEDEFS.to_string()
}
/**
Creates the `fs` standard library module.
@ -24,7 +35,7 @@ use self::options::FsWriteOptions;
Errors when out of memory.
*/
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
pub fn module(lua: Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)?
.with_async_function("readFile", fs_read_file)?
.with_async_function("readDir", fs_read_dir)?
@ -40,58 +51,45 @@ pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
.build_readonly()
}
async fn fs_read_file(lua: &Lua, path: String) -> LuaResult<LuaString> {
async fn fs_read_file(lua: Lua, path: String) -> LuaResult<LuaString> {
let bytes = fs::read(&path).await.into_lua_err()?;
lua.create_string(bytes)
}
async fn fs_read_dir(_: &Lua, path: String) -> LuaResult<Vec<String>> {
async fn fs_read_dir(_: Lua, path: String) -> LuaResult<Vec<String>> {
let mut dir_strings = Vec::new();
let mut dir = fs::read_dir(&path).await.into_lua_err()?;
while let Some(dir_entry) = dir.next_entry().await.into_lua_err()? {
if let Some(dir_path_str) = dir_entry.path().to_str() {
dir_strings.push(dir_path_str.to_owned());
while let Some(dir_entry) = dir.try_next().await.into_lua_err()? {
if let Some(dir_name_str) = dir_entry.file_name().to_str() {
dir_strings.push(dir_name_str.to_owned());
} else {
return Err(LuaError::RuntimeError(format!(
"File path could not be converted into a string: '{}'",
dir_entry.path().display()
"File name could not be converted into a string: '{}'",
dir_entry.file_name().to_string_lossy()
)));
}
}
let mut dir_string_prefix = path;
if !dir_string_prefix.ends_with(MAIN_SEPARATOR) {
dir_string_prefix.push(MAIN_SEPARATOR);
}
let dir_strings_no_prefix = dir_strings
.iter()
.map(|inner_path| {
inner_path
.trim()
.trim_start_matches(&dir_string_prefix)
.to_owned()
})
.collect::<Vec<_>>();
Ok(dir_strings_no_prefix)
Ok(dir_strings)
}
async fn fs_write_file(_: &Lua, (path, contents): (String, BString)) -> LuaResult<()> {
async fn fs_write_file(_: Lua, (path, contents): (String, BString)) -> LuaResult<()> {
fs::write(&path, contents.as_bytes()).await.into_lua_err()
}
async fn fs_write_dir(_: &Lua, path: String) -> LuaResult<()> {
async fn fs_write_dir(_: Lua, path: String) -> LuaResult<()> {
fs::create_dir_all(&path).await.into_lua_err()
}
async fn fs_remove_file(_: &Lua, path: String) -> LuaResult<()> {
async fn fs_remove_file(_: Lua, path: String) -> LuaResult<()> {
fs::remove_file(&path).await.into_lua_err()
}
async fn fs_remove_dir(_: &Lua, path: String) -> LuaResult<()> {
async fn fs_remove_dir(_: Lua, path: String) -> LuaResult<()> {
fs::remove_dir_all(&path).await.into_lua_err()
}
async fn fs_metadata(_: &Lua, path: String) -> LuaResult<FsMetadata> {
async fn fs_metadata(_: Lua, path: String) -> LuaResult<FsMetadata> {
match fs::metadata(path).await {
Err(e) if e.kind() == IoErrorKind::NotFound => Ok(FsMetadata::not_found()),
Ok(meta) => Ok(FsMetadata::from(meta)),
@ -99,7 +97,7 @@ async fn fs_metadata(_: &Lua, path: String) -> LuaResult<FsMetadata> {
}
}
async fn fs_is_file(_: &Lua, path: String) -> LuaResult<bool> {
async fn fs_is_file(_: Lua, path: String) -> LuaResult<bool> {
match fs::metadata(path).await {
Err(e) if e.kind() == IoErrorKind::NotFound => Ok(false),
Ok(meta) => Ok(meta.is_file()),
@ -107,7 +105,7 @@ async fn fs_is_file(_: &Lua, path: String) -> LuaResult<bool> {
}
}
async fn fs_is_dir(_: &Lua, path: String) -> LuaResult<bool> {
async fn fs_is_dir(_: Lua, path: String) -> LuaResult<bool> {
match fs::metadata(path).await {
Err(e) if e.kind() == IoErrorKind::NotFound => Ok(false),
Ok(meta) => Ok(meta.is_dir()),
@ -115,7 +113,7 @@ async fn fs_is_dir(_: &Lua, path: String) -> LuaResult<bool> {
}
}
async fn fs_move(_: &Lua, (from, to, options): (String, String, FsWriteOptions)) -> LuaResult<()> {
async fn fs_move(_: Lua, (from, to, options): (String, String, FsWriteOptions)) -> LuaResult<()> {
let path_from = PathBuf::from(from);
if !path_from.exists() {
return Err(LuaError::RuntimeError(format!(
@ -134,6 +132,6 @@ async fn fs_move(_: &Lua, (from, to, options): (String, String, FsWriteOptions))
Ok(())
}
async fn fs_copy(_: &Lua, (from, to, options): (String, String, FsWriteOptions)) -> LuaResult<()> {
async fn fs_copy(_: Lua, (from, to, options): (String, String, FsWriteOptions)) -> LuaResult<()> {
copy(from, to, options).await
}

View file

@ -61,8 +61,8 @@ impl From<StdFileType> for FsMetadataKind {
}
}
impl<'lua> IntoLua<'lua> for FsMetadataKind {
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
impl IntoLua for FsMetadataKind {
fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> {
if self == Self::None {
Ok(LuaValue::Nil)
} else {
@ -84,8 +84,8 @@ impl From<StdPermissions> for FsPermissions {
}
}
impl<'lua> IntoLua<'lua> for FsPermissions {
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
impl IntoLua for FsPermissions {
fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> {
let tab = lua.create_table_with_capacity(0, 1)?;
tab.set("readOnly", self.read_only)?;
tab.set_readonly(true);
@ -116,8 +116,8 @@ impl FsMetadata {
}
}
impl<'lua> IntoLua<'lua> for FsMetadata {
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
impl IntoLua for FsMetadata {
fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> {
let tab = lua.create_table_with_capacity(0, 6)?;
tab.set("kind", self.kind)?;
tab.set("exists", self.exists)?;

View file

@ -5,8 +5,8 @@ pub struct FsWriteOptions {
pub(crate) overwrite: bool,
}
impl<'lua> FromLua<'lua> for FsWriteOptions {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
impl FromLua for FsWriteOptions {
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
Ok(match value {
LuaValue::Nil => Self { overwrite: false },
LuaValue::Boolean(b) => Self { overwrite: b },
@ -19,7 +19,7 @@ impl<'lua> FromLua<'lua> for FsWriteOptions {
_ => {
return Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "FsWriteOptions",
to: "FsWriteOptions".to_string(),
message: Some(format!(
"Invalid write options - expected boolean or table, got {}",
value.type_name()

View file

@ -1,4 +1,6 @@
local DateTime = require("./datetime")
--!nocheck
local DateTime = require("@lune/datetime")
type DateTime = DateTime.DateTime
export type MetadataKind = "file" | "dir" | "symlink"

View file

@ -1,6 +1,6 @@
[package]
name = "lune-std-luau"
version = "0.1.1"
version = "0.2.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
@ -13,6 +13,6 @@ path = "src/lib.rs"
workspace = true
[dependencies]
mlua = { version = "0.9.7", features = ["luau", "luau-jit"] }
mlua = { version = "0.10.3", features = ["luau", "luau-jit"] }
lune-utils = { version = "0.1.2", path = "../lune-utils" }
lune-utils = { version = "0.2.2", path = "../lune-utils" }

View file

@ -2,13 +2,21 @@
use mlua::prelude::*;
use lune_utils::TableBuilder;
use lune_utils::{jit::JitEnablement, TableBuilder};
mod options;
use self::options::{LuauCompileOptions, LuauLoadOptions};
const BYTECODE_ERROR_BYTE: u8 = 0;
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
/**
Returns a string containing type definitions for the `luau` standard library.
*/
#[must_use]
pub fn typedefs() -> String {
TYPEDEFS.to_string()
}
/**
Creates the `luau` standard library module.
@ -17,33 +25,30 @@ const BYTECODE_ERROR_BYTE: u8 = 0;
Errors when out of memory.
*/
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
pub fn module(lua: Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)?
.with_function("compile", compile_source)?
.with_function("load", load_source)?
.build_readonly()
}
fn compile_source<'lua>(
lua: &'lua Lua,
(source, options): (LuaString<'lua>, LuauCompileOptions),
) -> LuaResult<LuaString<'lua>> {
let bytecode = options.into_compiler().compile(source);
match bytecode.first() {
Some(&BYTECODE_ERROR_BYTE) => Err(LuaError::RuntimeError(
String::from_utf8_lossy(&bytecode).into_owned(),
)),
Some(_) => lua.create_string(bytecode),
None => panic!("Compiling resulted in empty bytecode"),
}
fn compile_source(
lua: &Lua,
(source, options): (LuaString, LuauCompileOptions),
) -> LuaResult<LuaString> {
options
.into_compiler()
.compile(source.as_bytes())
.and_then(|s| lua.create_string(s))
}
fn load_source<'lua>(
lua: &'lua Lua,
(source, options): (LuaString<'lua>, LuauLoadOptions),
) -> LuaResult<LuaFunction<'lua>> {
let mut chunk = lua.load(source.as_bytes()).set_name(options.debug_name);
fn load_source(
lua: &Lua,
(source, options): (LuaString, LuauLoadOptions),
) -> LuaResult<LuaFunction> {
let mut chunk = lua
.load(source.as_bytes().to_vec())
.set_name(options.debug_name);
let env_changed = options.environment.is_some();
if let Some(custom_environment) = options.environment {
@ -56,10 +61,10 @@ fn load_source<'lua>(
environment.set(key, value)?;
}
if let Some(global_metatable) = lua.globals().get_metatable() {
if let Some(global_metatable) = lua.globals().metatable() {
environment.set_metatable(Some(global_metatable));
}
} else if let Some(custom_metatable) = custom_environment.get_metatable() {
} else if let Some(custom_metatable) = custom_environment.metatable() {
// Since we don't need to set the global metatable,
// we can just set a custom metatable if it exists
environment.set_metatable(Some(custom_metatable));
@ -78,7 +83,13 @@ fn load_source<'lua>(
// changed, otherwise disable JIT since it'll fall back anyways
lua.enable_jit(options.codegen_enabled && !env_changed);
let function = chunk.into_function()?;
lua.enable_jit(true);
lua.enable_jit(
lua.app_data_ref::<JitEnablement>()
.ok_or(LuaError::runtime(
"Failed to get current JitStatus ref from AppData",
))?
.enabled(),
);
Ok(function)
}

View file

@ -36,8 +36,8 @@ impl Default for LuauCompileOptions {
}
}
impl<'lua> FromLua<'lua> for LuauCompileOptions {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
impl FromLua for LuauCompileOptions {
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
Ok(match value {
LuaValue::Nil => Self::default(),
LuaValue::Table(t) => {
@ -68,7 +68,7 @@ impl<'lua> FromLua<'lua> for LuauCompileOptions {
_ => {
return Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "CompileOptions",
to: "CompileOptions".to_string(),
message: Some(format!(
"Invalid compile options - expected table, got {}",
value.type_name()
@ -79,14 +79,14 @@ impl<'lua> FromLua<'lua> for LuauCompileOptions {
}
}
pub struct LuauLoadOptions<'lua> {
pub struct LuauLoadOptions {
pub(crate) debug_name: String,
pub(crate) environment: Option<LuaTable<'lua>>,
pub(crate) environment: Option<LuaTable>,
pub(crate) inject_globals: bool,
pub(crate) codegen_enabled: bool,
}
impl Default for LuauLoadOptions<'_> {
impl Default for LuauLoadOptions {
fn default() -> Self {
Self {
debug_name: DEFAULT_DEBUG_NAME.to_string(),
@ -97,8 +97,8 @@ impl Default for LuauLoadOptions<'_> {
}
}
impl<'lua> FromLua<'lua> for LuauLoadOptions<'lua> {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
impl FromLua for LuauLoadOptions {
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
Ok(match value {
LuaValue::Nil => Self::default(),
LuaValue::Table(t) => {
@ -131,7 +131,7 @@ impl<'lua> FromLua<'lua> for LuauLoadOptions<'lua> {
_ => {
return Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "LoadOptions",
to: "LoadOptions".to_string(),
message: Some(format!(
"Invalid load options - expected string or table, got {}",
value.type_name()

View file

@ -1,6 +1,6 @@
[package]
name = "lune-std-net"
version = "0.1.1"
version = "0.2.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
@ -13,27 +13,30 @@ path = "src/lib.rs"
workspace = true
[dependencies]
mlua = { version = "0.9.7", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
mlua = { version = "0.10.3", features = ["luau"] }
mlua-luau-scheduler = { version = "0.1.2", path = "../mlua-luau-scheduler" }
async-channel = "2.3"
async-executor = "1.13"
async-io = "2.4"
async-lock = "3.4"
async-net = "2.0"
async-tungstenite = "0.29"
blocking = "1.6"
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"] }
form_urlencoded = "1.2"
futures = { version = "0.3", default-features = false, features = ["std"] }
futures-lite = "2.6"
futures-rustls = "0.26"
http-body-util = "0.1"
hyper = { version = "1.6", default-features = false, features = ["http1", "client", "server"] }
pin-project-lite = "0.2"
rustls = { version = "0.23", default-features = false, features = ["std", "tls12", "ring"] }
rustls-pki-types = "1.11"
url = "2.5"
urlencoding = "2.1"
webpki = "0.22"
webpki-roots = "0.26"
tokio = { version = "1", default-features = false, features = [
"sync",
"net",
"macros",
] }
lune-utils = { version = "0.1.2", path = "../lune-utils" }
lune-std-serde = { version = "0.1.1", path = "../lune-std-serde" }
lune-utils = { version = "0.2.2", path = "../lune-utils" }
lune-std-serde = { version = "0.2.2", path = "../lune-std-serde" }

View file

@ -0,0 +1,59 @@
use hyper::body::{Buf, Bytes};
use super::inner::ReadableBodyInner;
/**
The cursor keeping track of inner data and its position for a readable body.
*/
#[derive(Debug, Clone)]
pub struct ReadableBodyCursor {
inner: ReadableBodyInner,
start: usize,
}
impl ReadableBodyCursor {
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn as_slice(&self) -> &[u8] {
&self.inner.as_slice()[self.start..]
}
pub fn advance(&mut self, cnt: usize) {
self.start += cnt;
if self.start > self.inner.len() {
self.start = self.inner.len();
}
}
pub fn into_bytes(self) -> Bytes {
self.inner.into_bytes()
}
}
impl Buf for ReadableBodyCursor {
fn remaining(&self) -> usize {
self.len().saturating_sub(self.start)
}
fn chunk(&self) -> &[u8] {
self.as_slice()
}
fn advance(&mut self, cnt: usize) {
self.advance(cnt);
}
}
impl<T> From<T> for ReadableBodyCursor
where
T: Into<ReadableBodyInner>,
{
fn from(value: T) -> Self {
Self {
inner: value.into(),
start: 0,
}
}
}

View file

@ -0,0 +1,35 @@
use http_body_util::BodyExt;
use hyper::{
body::{Bytes, Incoming},
header::CONTENT_ENCODING,
HeaderMap,
};
use mlua::prelude::*;
use lune_std_serde::{decompress, CompressDecompressFormat};
pub async fn handle_incoming_body(
headers: &HeaderMap,
body: Incoming,
should_decompress: bool,
) -> LuaResult<(Bytes, bool)> {
let mut body = body.collect().await.into_lua_err()?.to_bytes();
let was_decompressed = if should_decompress {
let decompress_format = headers
.get(CONTENT_ENCODING)
.and_then(|value| value.to_str().ok())
.and_then(CompressDecompressFormat::detect_from_header_str);
if let Some(format) = decompress_format {
body = Bytes::from(decompress(body, format).await?);
true
} else {
false
}
} else {
false
};
Ok((body, was_decompressed))
}

View file

@ -0,0 +1,110 @@
use hyper::body::{Buf as _, Bytes};
use mlua::{prelude::*, Buffer as LuaBuffer};
/**
The inner data for a readable body.
*/
#[derive(Debug, Clone)]
pub enum ReadableBodyInner {
Bytes(Bytes),
String(String),
LuaString(LuaString),
LuaBuffer(LuaBuffer),
}
impl ReadableBodyInner {
pub fn len(&self) -> usize {
match self {
Self::Bytes(b) => b.len(),
Self::String(s) => s.len(),
Self::LuaString(s) => s.as_bytes().len(),
Self::LuaBuffer(b) => b.len(),
}
}
pub fn as_slice(&self) -> &[u8] {
/*
SAFETY: Reading lua strings and lua buffers as raw slices is safe while we can
guarantee that the inner Lua value + main lua struct has not yet been dropped
1. Buffers are fixed-size and guaranteed to never resize
2. We do not expose any method for writing to the body, only reading
3. We guarantee that net.request and net.serve futures are only driven forward
while we also know that the Lua + scheduler pair have not yet been dropped
4. Any writes from within lua to a buffer, are considered user error,
and are not unsafe, since the only possible outcome with the above
guarantees is invalid / mangled contents in request / response bodies
*/
match self {
Self::Bytes(b) => b.chunk(),
Self::String(s) => s.as_bytes(),
Self::LuaString(s) => unsafe {
// BorrowedBytes would not let us return a plain slice here,
// which is what the Buf implementation below needs - we need to
// do a little hack here to re-create the slice without a lifetime
let b = s.as_bytes();
let ptr = b.as_ptr();
let len = b.len();
std::slice::from_raw_parts(ptr, len)
},
Self::LuaBuffer(b) => unsafe {
// Similar to above, we need to get the raw slice for the buffer,
// which is a bit trickier here because Buffer has a read + write
// interface instead of using slices for some unknown reason
let v = LuaValue::Buffer(b.clone());
let ptr = v.to_pointer().cast::<u8>();
let len = b.len();
std::slice::from_raw_parts(ptr, len)
},
}
}
pub fn into_bytes(self) -> Bytes {
match self {
Self::Bytes(b) => b,
Self::String(s) => Bytes::from(s),
Self::LuaString(s) => Bytes::from(s.as_bytes().to_vec()),
Self::LuaBuffer(b) => Bytes::from(b.to_vec()),
}
}
}
impl From<&'static str> for ReadableBodyInner {
fn from(value: &'static str) -> Self {
Self::Bytes(Bytes::from(value))
}
}
impl From<Vec<u8>> for ReadableBodyInner {
fn from(value: Vec<u8>) -> Self {
Self::Bytes(Bytes::from(value))
}
}
impl From<Bytes> for ReadableBodyInner {
fn from(value: Bytes) -> Self {
Self::Bytes(value)
}
}
impl From<String> for ReadableBodyInner {
fn from(value: String) -> Self {
Self::String(value)
}
}
impl From<LuaString> for ReadableBodyInner {
fn from(value: LuaString) -> Self {
Self::LuaString(value)
}
}
impl From<LuaBuffer> for ReadableBodyInner {
fn from(value: LuaBuffer) -> Self {
Self::LuaBuffer(value)
}
}

View file

@ -0,0 +1,11 @@
#![allow(unused_imports)]
mod cursor;
mod incoming;
mod inner;
mod readable;
pub use self::cursor::ReadableBodyCursor;
pub use self::incoming::handle_incoming_body;
pub use self::inner::ReadableBodyInner;
pub use self::readable::ReadableBody;

View file

@ -0,0 +1,105 @@
use std::convert::Infallible;
use std::pin::Pin;
use std::task::{Context, Poll};
use hyper::body::{Body, Bytes, Frame, SizeHint};
use mlua::prelude::*;
use super::cursor::ReadableBodyCursor;
/**
Zero-copy wrapper for a readable body.
Provides methods to read bytes that can be safely used if, and only
if, the respective Lua struct for the body has not yet been dropped.
If the body was created from a `Vec<u8>`, `Bytes`, or a `String`, reading
bytes is always safe and does not go through any additional indirections.
*/
#[derive(Debug, Clone)]
pub struct ReadableBody {
cursor: Option<ReadableBodyCursor>,
}
impl ReadableBody {
pub const fn empty() -> Self {
Self { cursor: None }
}
pub fn as_slice(&self) -> &[u8] {
match self.cursor.as_ref() {
Some(cursor) => cursor.as_slice(),
None => &[],
}
}
pub fn into_bytes(self) -> Bytes {
match self.cursor {
Some(cursor) => cursor.into_bytes(),
None => Bytes::new(),
}
}
}
impl Body for ReadableBody {
type Data = ReadableBodyCursor;
type Error = Infallible;
fn poll_frame(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
Poll::Ready(self.cursor.take().map(|d| Ok(Frame::data(d))))
}
fn is_end_stream(&self) -> bool {
self.cursor.is_none()
}
fn size_hint(&self) -> SizeHint {
self.cursor.as_ref().map_or_else(
|| SizeHint::with_exact(0),
|c| SizeHint::with_exact(c.len() as u64),
)
}
}
impl<T> From<T> for ReadableBody
where
T: Into<ReadableBodyCursor>,
{
fn from(value: T) -> Self {
Self {
cursor: Some(value.into()),
}
}
}
impl<T> From<Option<T>> for ReadableBody
where
T: Into<ReadableBodyCursor>,
{
fn from(value: Option<T>) -> Self {
Self {
cursor: value.map(Into::into),
}
}
}
impl FromLua for ReadableBody {
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
match value {
LuaValue::Nil => Ok(Self::empty()),
LuaValue::String(str) => Ok(Self::from(str)),
LuaValue::Buffer(buf) => Ok(Self::from(buf)),
v => Err(LuaError::FromLuaConversionError {
from: v.type_name(),
to: "Body".to_string(),
message: Some(format!(
"Invalid body - expected string or buffer, got {}",
v.type_name()
)),
}),
}
}
}

View file

@ -1,163 +0,0 @@
use std::str::FromStr;
use mlua::prelude::*;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_ENCODING};
use lune_std_serde::{decompress, CompressDecompressFormat};
use lune_utils::TableBuilder;
use super::{config::RequestConfig, util::header_map_to_table};
const REGISTRY_KEY: &str = "NetClient";
pub struct NetClientBuilder {
builder: reqwest::ClientBuilder,
}
impl NetClientBuilder {
pub fn new() -> NetClientBuilder {
Self {
builder: reqwest::ClientBuilder::new(),
}
}
pub fn headers<K, V>(mut self, headers: &[(K, V)]) -> LuaResult<Self>
where
K: AsRef<str>,
V: AsRef<[u8]>,
{
let mut map = HeaderMap::new();
for (key, val) in headers {
let hkey = HeaderName::from_str(key.as_ref()).into_lua_err()?;
let hval = HeaderValue::from_bytes(val.as_ref()).into_lua_err()?;
map.insert(hkey, hval);
}
self.builder = self.builder.default_headers(map);
Ok(self)
}
pub fn build(self) -> LuaResult<NetClient> {
let client = self.builder.build().into_lua_err()?;
Ok(NetClient { inner: client })
}
}
#[derive(Debug, Clone)]
pub struct NetClient {
inner: reqwest::Client,
}
impl NetClient {
pub fn from_registry(lua: &Lua) -> Self {
lua.named_registry_value(REGISTRY_KEY)
.expect("Failed to get NetClient from lua registry")
}
pub fn into_registry(self, lua: &Lua) {
lua.set_named_registry_value(REGISTRY_KEY, self)
.expect("Failed to store NetClient in lua registry");
}
pub async fn request(&self, config: RequestConfig) -> LuaResult<NetClientResponse> {
// Create and send the request
let mut request = self.inner.request(config.method, config.url);
for (query, values) in config.query {
request = request.query(
&values
.iter()
.map(|v| (query.as_str(), v))
.collect::<Vec<_>>(),
);
}
for (header, values) in config.headers {
for value in values {
request = request.header(header.as_str(), value);
}
}
let res = request
.body(config.body.unwrap_or_default())
.send()
.await
.into_lua_err()?;
// Extract status, headers
let res_status = res.status().as_u16();
let res_status_text = res.status().canonical_reason();
let res_headers = res.headers().clone();
// Read response bytes
let mut res_bytes = res.bytes().await.into_lua_err()?.to_vec();
let mut res_decompressed = false;
// Check for extra options, decompression
if config.options.decompress {
let decompress_format = res_headers
.iter()
.find(|(name, _)| {
name.as_str()
.eq_ignore_ascii_case(CONTENT_ENCODING.as_str())
})
.and_then(|(_, value)| value.to_str().ok())
.and_then(CompressDecompressFormat::detect_from_header_str);
if let Some(format) = decompress_format {
res_bytes = decompress(res_bytes, format).await?;
res_decompressed = true;
}
}
Ok(NetClientResponse {
ok: (200..300).contains(&res_status),
status_code: res_status,
status_message: res_status_text.unwrap_or_default().to_string(),
headers: res_headers,
body: res_bytes,
body_decompressed: res_decompressed,
})
}
}
impl LuaUserData for NetClient {}
impl FromLua<'_> for NetClient {
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
if let LuaValue::UserData(ud) = value {
if let Ok(ctx) = ud.borrow::<NetClient>() {
return Ok(ctx.clone());
}
}
unreachable!("NetClient should only be used from registry")
}
}
impl From<&Lua> for NetClient {
fn from(value: &Lua) -> Self {
value
.named_registry_value(REGISTRY_KEY)
.expect("Missing require context in lua registry")
}
}
pub struct NetClientResponse {
ok: bool,
status_code: u16,
status_message: String,
headers: HeaderMap,
body: Vec<u8>,
body_decompressed: bool,
}
impl NetClientResponse {
pub fn into_lua_table(self, lua: &Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)?
.with_value("ok", self.ok)?
.with_value("statusCode", self.status_code)?
.with_value("statusMessage", self.status_message)?
.with_value(
"headers",
header_map_to_table(lua, self.headers, self.body_decompressed)?,
)?
.with_value("body", lua.create_string(&self.body)?)?
.build_readonly()
}
}

View file

@ -0,0 +1,95 @@
use std::{
io,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use async_net::TcpStream;
use futures_lite::prelude::*;
use futures_rustls::{TlsConnector, TlsStream};
use rustls_pki_types::ServerName;
use url::Url;
use crate::client::rustls::CLIENT_CONFIG;
#[derive(Debug)]
pub enum HttpStream {
Plain(TcpStream),
Tls(TlsStream<TcpStream>),
}
impl HttpStream {
pub async fn connect(url: Url) -> Result<Self, io::Error> {
let Some(host) = url.host() else {
return Err(make_err("unknown or missing host"));
};
let Some(port) = url.port_or_known_default() else {
return Err(make_err("unknown or missing port"));
};
let use_tls = match url.scheme() {
"http" => false,
"https" => true,
s => return Err(make_err(format!("unsupported scheme: {s}"))),
};
let host = host.to_string();
let stream = TcpStream::connect((host.clone(), port)).await?;
let stream = if use_tls {
let servname = ServerName::try_from(host).map_err(make_err)?.to_owned();
let connector = TlsConnector::from(Arc::clone(&CLIENT_CONFIG));
let stream = connector.connect(servname, stream).await?;
Self::Tls(TlsStream::Client(stream))
} else {
Self::Plain(stream)
};
Ok(stream)
}
}
impl AsyncRead for HttpStream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
match &mut *self {
HttpStream::Plain(stream) => Pin::new(stream).poll_read(cx, buf),
HttpStream::Tls(stream) => Pin::new(stream).poll_read(cx, buf),
}
}
}
impl AsyncWrite for HttpStream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
match &mut *self {
HttpStream::Plain(stream) => Pin::new(stream).poll_write(cx, buf),
HttpStream::Tls(stream) => Pin::new(stream).poll_write(cx, buf),
}
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
match &mut *self {
HttpStream::Plain(stream) => Pin::new(stream).poll_close(cx),
HttpStream::Tls(stream) => Pin::new(stream).poll_close(cx),
}
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
match &mut *self {
HttpStream::Plain(stream) => Pin::new(stream).poll_flush(cx),
HttpStream::Tls(stream) => Pin::new(stream).poll_flush(cx),
}
}
}
fn make_err(e: impl ToString) -> io::Error {
io::Error::new(io::ErrorKind::Other, e.to_string())
}

View file

@ -0,0 +1,125 @@
use http_body_util::Full;
use hyper::{
body::Incoming,
client::conn::http1::handshake,
header::{HeaderValue, ACCEPT, CONTENT_LENGTH, HOST, LOCATION, USER_AGENT},
Method, Request as HyperRequest, Response as HyperResponse, Uri,
};
use mlua::prelude::*;
use url::Url;
use crate::{
body::ReadableBody,
client::{http_stream::HttpStream, ws_stream::WsStream},
shared::{
headers::create_user_agent_header,
hyper::{HyperExecutor, HyperIo},
request::Request,
response::Response,
websocket::Websocket,
},
};
pub mod http_stream;
pub mod rustls;
pub mod ws_stream;
const MAX_REDIRECTS: usize = 10;
/**
Connects to a websocket at the given URL.
*/
pub async fn connect_websocket(url: Url) -> LuaResult<Websocket<WsStream>> {
let stream = WsStream::connect(url).await?;
Ok(Websocket::from(stream))
}
/**
Sends the request and returns the final response.
This will follow any redirects returned by the server,
modifying the request method and body as necessary.
*/
pub async fn send_request(mut request: Request, lua: Lua) -> LuaResult<Response> {
let url = request
.inner
.uri()
.to_string()
.parse::<Url>()
.into_lua_err()?;
// Some headers are required by most if not
// all servers, make sure those are present...
if !request.headers().contains_key(HOST.as_str()) {
if let Some(host) = url.host_str() {
let host = HeaderValue::from_str(host).into_lua_err()?;
request.inner.headers_mut().insert(HOST, host);
}
}
if !request.headers().contains_key(USER_AGENT.as_str()) {
let ua = create_user_agent_header(&lua)?;
let ua = HeaderValue::from_str(&ua).into_lua_err()?;
request.inner.headers_mut().insert(USER_AGENT, ua);
}
if !request.headers().contains_key(CONTENT_LENGTH.as_str()) && request.method() != Method::GET {
let len = request.body().len().to_string();
let len = HeaderValue::from_str(&len).into_lua_err()?;
request.inner.headers_mut().insert(CONTENT_LENGTH, len);
}
if !request.headers().contains_key(ACCEPT.as_str()) {
let accept = HeaderValue::from_static("*/*");
request.inner.headers_mut().insert(ACCEPT, accept);
}
// ... we can now safely continue and send the request
loop {
let stream = HttpStream::connect(url.clone()).await?;
let (mut sender, conn) = handshake(HyperIo::from(stream)).await.into_lua_err()?;
HyperExecutor::execute(lua.clone(), conn);
let (parts, body) = request.clone_inner().into_parts();
let data = HyperRequest::from_parts(parts, Full::new(body.into_bytes()));
let incoming = sender.send_request(data).await.into_lua_err()?;
if let Some((new_method, new_uri)) =
check_redirect(request.inner.method().clone(), &incoming)
{
if request.redirects.is_some_and(|r| r >= MAX_REDIRECTS) {
return Err(LuaError::external("Too many redirects"));
}
if new_method == Method::GET {
*request.inner.body_mut() = ReadableBody::empty();
}
*request.inner.method_mut() = new_method;
*request.inner.uri_mut() = new_uri;
*request.redirects.get_or_insert_default() += 1;
continue;
}
break Response::from_incoming(incoming, request.decompress).await;
}
}
fn check_redirect(method: Method, response: &HyperResponse<Incoming>) -> Option<(Method, Uri)> {
if !response.status().is_redirection() {
return None;
}
let location = response.headers().get(LOCATION)?;
let location = location.to_str().ok()?;
let location = location.parse().ok()?;
let method = match response.status().as_u16() {
301..=303 => Method::GET,
_ => method,
};
Some((method, location))
}

View file

@ -0,0 +1,26 @@
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, LazyLock,
};
use rustls::{crypto::ring, ClientConfig};
static PROVIDER_INITIALIZED: AtomicBool = AtomicBool::new(false);
pub fn initialize_provider() {
if !PROVIDER_INITIALIZED.load(Ordering::Relaxed) {
PROVIDER_INITIALIZED.store(true, Ordering::Relaxed);
// Only errors if already installed, which is fine
ring::default_provider().install_default().ok();
}
}
pub static CLIENT_CONFIG: LazyLock<Arc<ClientConfig>> = LazyLock::new(|| {
initialize_provider();
rustls::ClientConfig::builder()
.with_root_certificates(rustls::RootCertStore {
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
})
.with_no_client_auth()
.into()
});

View file

@ -0,0 +1,114 @@
use std::{
io,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use async_net::TcpStream;
use async_tungstenite::{
tungstenite::{Error as TungsteniteError, Message, Result as TungsteniteResult},
WebSocketStream as TungsteniteStream,
};
use futures::Sink;
use futures_lite::prelude::*;
use futures_rustls::{TlsConnector, TlsStream};
use rustls_pki_types::ServerName;
use url::Url;
use crate::client::rustls::CLIENT_CONFIG;
#[derive(Debug)]
pub enum WsStream {
Plain(TungsteniteStream<TcpStream>),
Tls(TungsteniteStream<TlsStream<TcpStream>>),
}
impl WsStream {
pub async fn connect(url: Url) -> Result<Self, io::Error> {
let Some(host) = url.host() else {
return Err(make_err("unknown or missing host"));
};
let Some(port) = url.port_or_known_default() else {
return Err(make_err("unknown or missing port"));
};
let use_tls = match url.scheme() {
"ws" => false,
"wss" => true,
s => return Err(make_err(format!("unsupported scheme: {s}"))),
};
let host = host.to_string();
let stream = TcpStream::connect((host.clone(), port)).await?;
let stream = if use_tls {
let servname = ServerName::try_from(host).map_err(make_err)?.to_owned();
let connector = TlsConnector::from(Arc::clone(&CLIENT_CONFIG));
let stream = connector.connect(servname, stream).await?;
let stream = TlsStream::Client(stream);
let stream = async_tungstenite::client_async(url.to_string(), stream)
.await
.map_err(make_err)?
.0;
Self::Tls(stream)
} else {
let stream = async_tungstenite::client_async(url.to_string(), stream)
.await
.map_err(make_err)?
.0;
Self::Plain(stream)
};
Ok(stream)
}
}
impl Sink<Message> for WsStream {
type Error = TungsteniteError;
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match &mut *self {
WsStream::Plain(s) => Pin::new(s).poll_ready(cx),
WsStream::Tls(s) => Pin::new(s).poll_ready(cx),
}
}
fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> {
match &mut *self {
WsStream::Plain(s) => Pin::new(s).start_send(item),
WsStream::Tls(s) => Pin::new(s).start_send(item),
}
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match &mut *self {
WsStream::Plain(s) => Pin::new(s).poll_flush(cx),
WsStream::Tls(s) => Pin::new(s).poll_flush(cx),
}
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match &mut *self {
WsStream::Plain(s) => Pin::new(s).poll_close(cx),
WsStream::Tls(s) => Pin::new(s).poll_close(cx),
}
}
}
impl Stream for WsStream {
type Item = TungsteniteResult<Message>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match &mut *self {
WsStream::Plain(s) => Pin::new(s).poll_next(cx),
WsStream::Tls(s) => Pin::new(s).poll_next(cx),
}
}
}
fn make_err(e: impl ToString) -> io::Error {
io::Error::new(io::ErrorKind::Other, e.to_string())
}

View file

@ -1,231 +0,0 @@
use std::{
collections::HashMap,
net::{IpAddr, Ipv4Addr},
};
use bstr::{BString, ByteSlice};
use mlua::prelude::*;
use reqwest::Method;
use super::util::table_to_hash_map;
const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
const WEB_SOCKET_UPDGRADE_REQUEST_HANDLER: &str = r#"
return {
status = 426,
body = "Upgrade Required",
headers = {
Upgrade = "websocket",
},
}
"#;
// Net request config
#[derive(Debug, Clone)]
pub struct RequestConfigOptions {
pub decompress: bool,
}
impl Default for RequestConfigOptions {
fn default() -> Self {
Self { decompress: true }
}
}
impl<'lua> FromLua<'lua> for RequestConfigOptions {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
if let LuaValue::Nil = value {
// Nil means default options
Ok(Self::default())
} else if let LuaValue::Table(tab) = value {
// Table means custom options
let decompress = match tab.get::<_, Option<bool>>("decompress") {
Ok(decomp) => Ok(decomp.unwrap_or(true)),
Err(_) => Err(LuaError::RuntimeError(
"Invalid option value for 'decompress' in request config options".to_string(),
)),
}?;
Ok(Self { decompress })
} else {
// Anything else is invalid
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "RequestConfigOptions",
message: Some(format!(
"Invalid request config options - expected table or nil, got {}",
value.type_name()
)),
})
}
}
}
#[derive(Debug, Clone)]
pub struct RequestConfig {
pub url: String,
pub method: Method,
pub query: HashMap<String, Vec<String>>,
pub headers: HashMap<String, Vec<String>>,
pub body: Option<Vec<u8>>,
pub options: RequestConfigOptions,
}
impl FromLua<'_> for RequestConfig {
fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
// If we just got a string we assume its a GET request to a given url
if let LuaValue::String(s) = value {
Ok(Self {
url: s.to_string_lossy().to_string(),
method: Method::GET,
query: HashMap::new(),
headers: HashMap::new(),
body: None,
options: RequestConfigOptions::default(),
})
} else if let LuaValue::Table(tab) = value {
// If we got a table we are able to configure the entire request
// Extract url
let url = match tab.get::<_, LuaString>("url") {
Ok(config_url) => Ok(config_url.to_string_lossy().to_string()),
Err(_) => Err(LuaError::runtime("Missing 'url' in request config")),
}?;
// Extract method
let method = match tab.get::<_, LuaString>("method") {
Ok(config_method) => config_method.to_string_lossy().trim().to_ascii_uppercase(),
Err(_) => "GET".to_string(),
};
// Extract query
let query = match tab.get::<_, LuaTable>("query") {
Ok(tab) => table_to_hash_map(tab, "query")?,
Err(_) => HashMap::new(),
};
// Extract headers
let headers = match tab.get::<_, LuaTable>("headers") {
Ok(tab) => table_to_hash_map(tab, "headers")?,
Err(_) => HashMap::new(),
};
// Extract body
let body = match tab.get::<_, BString>("body") {
Ok(config_body) => Some(config_body.as_bytes().to_owned()),
Err(_) => None,
};
// Convert method string into proper enum
let method = method.trim().to_ascii_uppercase();
let method = match method.as_ref() {
"GET" => Ok(Method::GET),
"POST" => Ok(Method::POST),
"PUT" => Ok(Method::PUT),
"DELETE" => Ok(Method::DELETE),
"HEAD" => Ok(Method::HEAD),
"OPTIONS" => Ok(Method::OPTIONS),
"PATCH" => Ok(Method::PATCH),
_ => Err(LuaError::RuntimeError(format!(
"Invalid request config method '{}'",
&method
))),
}?;
// Parse any extra options given
let options = match tab.get::<_, LuaValue>("options") {
Ok(opts) => RequestConfigOptions::from_lua(opts, lua)?,
Err(_) => RequestConfigOptions::default(),
};
// All good, validated and we got what we need
Ok(Self {
url,
method,
query,
headers,
body,
options,
})
} else {
// Anything else is invalid
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "RequestConfig",
message: Some(format!(
"Invalid request config - expected string or table, got {}",
value.type_name()
)),
})
}
}
}
// Net serve config
#[derive(Debug)]
pub struct ServeConfig<'a> {
pub address: IpAddr,
pub handle_request: LuaFunction<'a>,
pub handle_web_socket: Option<LuaFunction<'a>>,
}
impl<'lua> FromLua<'lua> for ServeConfig<'lua> {
fn from_lua(value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
if let LuaValue::Function(f) = &value {
// Single function = request handler, rest is default
Ok(ServeConfig {
handle_request: f.clone(),
handle_web_socket: None,
address: DEFAULT_IP_ADDRESS,
})
} else if let LuaValue::Table(t) = &value {
// Table means custom options
let address: Option<LuaString> = t.get("address")?;
let handle_request: Option<LuaFunction> = t.get("handleRequest")?;
let handle_web_socket: Option<LuaFunction> = t.get("handleWebSocket")?;
if handle_request.is_some() || handle_web_socket.is_some() {
let address: IpAddr = match &address {
Some(addr) => {
let addr_str = addr.to_str()?;
addr_str
.trim_start_matches("http://")
.trim_start_matches("https://")
.parse()
.map_err(|_e| LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ServeConfig",
message: Some(format!(
"IP address format is incorrect - \
expected an IP in the form 'http://0.0.0.0' or '0.0.0.0', \
got '{addr_str}'"
)),
})?
}
None => DEFAULT_IP_ADDRESS,
};
Ok(Self {
address,
handle_request: handle_request.unwrap_or_else(|| {
lua.load(WEB_SOCKET_UPDGRADE_REQUEST_HANDLER)
.into_function()
.expect("Failed to create default http responder function")
}),
handle_web_socket,
})
} else {
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ServeConfig",
message: Some(String::from(
"Invalid serve config - expected table with 'handleRequest' or 'handleWebSocket' function",
)),
})
}
} else {
// Anything else is invalid
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ServeConfig",
message: None,
})
}
}
}

View file

@ -1,26 +1,29 @@
#![allow(clippy::cargo_common_metadata)]
use bstr::BString;
use mlua::prelude::*;
use mlua_luau_scheduler::LuaSpawnExt;
mod client;
mod config;
mod server;
mod util;
mod websocket;
use lune_utils::TableBuilder;
use mlua::prelude::*;
pub(crate) mod body;
pub(crate) mod client;
pub(crate) mod server;
pub(crate) mod shared;
pub(crate) mod url;
use self::{
client::{NetClient, NetClientBuilder},
config::{RequestConfig, ServeConfig},
server::serve,
util::create_user_agent_header,
websocket::NetWebSocket,
client::ws_stream::WsStream,
server::config::ServeConfig,
shared::{request::Request, response::Response, websocket::Websocket},
};
use lune_std_serde::{decode, encode, EncodeDecodeConfig, EncodeDecodeFormat};
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
/**
Returns a string containing type definitions for the `net` standard library.
*/
#[must_use]
pub fn typedefs() -> String {
TYPEDEFS.to_string()
}
/**
Creates the `net` standard library module.
@ -29,14 +32,10 @@ use lune_std_serde::{decode, encode, EncodeDecodeConfig, EncodeDecodeFormat};
Errors when out of memory.
*/
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
NetClientBuilder::new()
.headers(&[("User-Agent", create_user_agent_header(lua)?)])?
.build()?
.into_registry(lua);
pub fn module(lua: Lua) -> LuaResult<LuaTable> {
// No initial rustls setup is necessary, the respective
// functions lazily initialize anything there as needed
TableBuilder::new(lua)?
.with_function("jsonEncode", net_json_encode)?
.with_function("jsonDecode", net_json_decode)?
.with_async_function("request", net_request)?
.with_async_function("socket", net_socket)?
.with_async_function("serve", net_serve)?
@ -45,58 +44,35 @@ pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
.build_readonly()
}
fn net_json_encode<'lua>(
lua: &'lua Lua,
(val, pretty): (LuaValue<'lua>, Option<bool>),
) -> LuaResult<LuaString<'lua>> {
let config = EncodeDecodeConfig::from((EncodeDecodeFormat::Json, pretty.unwrap_or_default()));
encode(val, lua, config)
async fn net_request(lua: Lua, req: Request) -> LuaResult<Response> {
self::client::send_request(req, lua).await
}
fn net_json_decode(lua: &Lua, json: BString) -> LuaResult<LuaValue> {
let config = EncodeDecodeConfig::from(EncodeDecodeFormat::Json);
decode(json, lua, config)
async fn net_socket(_: Lua, url: String) -> LuaResult<Websocket<WsStream>> {
let url = url.parse().into_lua_err()?;
self::client::connect_websocket(url).await
}
async fn net_request(lua: &Lua, config: RequestConfig) -> LuaResult<LuaTable> {
let client = NetClient::from_registry(lua);
// NOTE: We spawn the request as a background task to free up resources in lua
let res = lua.spawn(async move { client.request(config).await });
res.await?.into_lua_table(lua)
async fn net_serve(lua: Lua, (port, config): (u16, ServeConfig)) -> LuaResult<LuaTable> {
self::server::serve(lua.clone(), port, config)
.await?
.into_lua_table(lua)
}
async fn net_socket(lua: &Lua, url: String) -> LuaResult<LuaTable> {
let (ws, _) = tokio_tungstenite::connect_async(url).await.into_lua_err()?;
NetWebSocket::new(ws).into_lua_table(lua)
fn net_url_encode(
lua: &Lua,
(lua_string, as_binary): (LuaString, Option<bool>),
) -> LuaResult<LuaString> {
let as_binary = as_binary.unwrap_or_default();
let bytes = self::url::encode(lua_string, as_binary)?;
lua.create_string(bytes)
}
async fn net_serve<'lua>(
lua: &'lua Lua,
(port, config): (u16, ServeConfig<'lua>),
) -> LuaResult<LuaTable<'lua>> {
serve(lua, port, config).await
}
fn net_url_encode<'lua>(
lua: &'lua Lua,
(lua_string, as_binary): (LuaString<'lua>, Option<bool>),
) -> LuaResult<LuaValue<'lua>> {
if matches!(as_binary, Some(true)) {
urlencoding::encode_binary(lua_string.as_bytes()).into_lua(lua)
} else {
urlencoding::encode(lua_string.to_str()?).into_lua(lua)
}
}
fn net_url_decode<'lua>(
lua: &'lua Lua,
(lua_string, as_binary): (LuaString<'lua>, Option<bool>),
) -> LuaResult<LuaValue<'lua>> {
if matches!(as_binary, Some(true)) {
urlencoding::decode_binary(lua_string.as_bytes()).into_lua(lua)
} else {
urlencoding::decode(lua_string.to_str()?)
.map_err(|e| LuaError::RuntimeError(format!("Encountered invalid encoding - {e}")))?
.into_lua(lua)
}
fn net_url_decode(
lua: &Lua,
(lua_string, as_binary): (LuaString, Option<bool>),
) -> LuaResult<LuaString> {
let as_binary = as_binary.unwrap_or_default();
let bytes = self::url::decode(lua_string, as_binary)?;
lua.create_string(bytes)
}

View file

@ -0,0 +1,87 @@
use std::net::{IpAddr, Ipv4Addr};
use mlua::prelude::*;
const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
const WEB_SOCKET_UPDGRADE_REQUEST_HANDLER: &str = r#"
return {
status = 426,
body = "Upgrade Required",
headers = {
Upgrade = "websocket",
},
}
"#;
#[derive(Debug, Clone)]
pub struct ServeConfig {
pub address: IpAddr,
pub handle_request: LuaFunction,
pub handle_web_socket: Option<LuaFunction>,
}
impl FromLua for ServeConfig {
fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
if let LuaValue::Function(f) = &value {
// Single function = request handler, rest is default
Ok(ServeConfig {
handle_request: f.clone(),
handle_web_socket: None,
address: DEFAULT_IP_ADDRESS,
})
} else if let LuaValue::Table(t) = &value {
// Table means custom options
let address: Option<LuaString> = t.get("address")?;
let handle_request: Option<LuaFunction> = t.get("handleRequest")?;
let handle_web_socket: Option<LuaFunction> = t.get("handleWebSocket")?;
if handle_request.is_some() || handle_web_socket.is_some() {
let address: IpAddr = match &address {
Some(addr) => {
let addr_str = addr.to_str()?;
addr_str
.trim_start_matches("http://")
.trim_start_matches("https://")
.parse()
.map_err(|_e| LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ServeConfig".to_string(),
message: Some(format!(
"IP address format is incorrect - \
expected an IP in the form 'http://0.0.0.0' or '0.0.0.0', \
got '{addr_str}'"
)),
})?
}
None => DEFAULT_IP_ADDRESS,
};
Ok(Self {
address,
handle_request: handle_request.unwrap_or_else(|| {
lua.load(WEB_SOCKET_UPDGRADE_REQUEST_HANDLER)
.into_function()
.expect("Failed to create default http responder function")
}),
handle_web_socket,
})
} else {
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ServeConfig".to_string(),
message: Some(String::from(
"Invalid serve config - expected table with 'handleRequest' or 'handleWebSocket' function",
)),
})
}
} else {
// Anything else is invalid
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ServeConfig".to_string(),
message: None,
})
}
}
}

View file

@ -0,0 +1,72 @@
use std::{
net::SocketAddr,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use async_channel::{unbounded, Receiver, Sender};
use lune_utils::TableBuilder;
use mlua::prelude::*;
#[derive(Debug, Clone)]
pub struct ServeHandle {
addr: SocketAddr,
shutdown: Arc<AtomicBool>,
sender: Sender<()>,
}
impl ServeHandle {
pub fn new(addr: SocketAddr) -> (Self, Receiver<()>) {
let (sender, receiver) = unbounded();
let this = Self {
addr,
shutdown: Arc::new(AtomicBool::new(false)),
sender,
};
(this, receiver)
}
// TODO: Remove this in the next major release to use colon/self
// based call syntax and userdata implementation below instead
pub fn into_lua_table(self, lua: Lua) -> LuaResult<LuaTable> {
let shutdown = self.shutdown.clone();
let sender = self.sender.clone();
TableBuilder::new(lua)?
.with_value("ip", self.addr.ip().to_string())?
.with_value("port", self.addr.port())?
.with_function("stop", move |_, ()| {
if shutdown.load(Ordering::SeqCst) {
Err(LuaError::runtime("Server already stopped"))
} else {
shutdown.store(true, Ordering::SeqCst);
sender.try_send(()).ok();
sender.close();
Ok(())
}
})?
.build()
}
}
impl LuaUserData for ServeHandle {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("ip", |_, this| Ok(this.addr.ip().to_string()));
fields.add_field_method_get("port", |_, this| Ok(this.addr.port()));
}
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_method("stop", |_, this, ()| {
if this.shutdown.load(Ordering::SeqCst) {
Err(LuaError::runtime("Server already stopped"))
} else {
this.shutdown.store(true, Ordering::SeqCst);
this.sender.try_send(()).ok();
this.sender.close();
Ok(())
}
});
}
}

View file

@ -1,61 +0,0 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use mlua::prelude::*;
#[derive(Debug, Clone, Copy)]
pub(super) struct SvcKeys {
key_request: &'static str,
key_websocket: Option<&'static str>,
}
impl SvcKeys {
pub(super) fn new<'lua>(
lua: &'lua Lua,
handle_request: LuaFunction<'lua>,
handle_websocket: Option<LuaFunction<'lua>>,
) -> LuaResult<Self> {
static SERVE_COUNTER: AtomicUsize = AtomicUsize::new(0);
let count = SERVE_COUNTER.fetch_add(1, Ordering::Relaxed);
// NOTE: We leak strings here, but this is an acceptable tradeoff since programs
// generally only start one or a couple of servers and they are usually never dropped.
// Leaking here lets us keep this struct Copy and access the request handler callbacks
// very performantly, significantly reducing the per-request overhead of the server.
let key_request: &'static str =
Box::leak(format!("__net_serve_request_{count}").into_boxed_str());
let key_websocket: Option<&'static str> = if handle_websocket.is_some() {
Some(Box::leak(
format!("__net_serve_websocket_{count}").into_boxed_str(),
))
} else {
None
};
lua.set_named_registry_value(key_request, handle_request)?;
if let Some(key) = key_websocket {
lua.set_named_registry_value(key, handle_websocket.unwrap())?;
}
Ok(Self {
key_request,
key_websocket,
})
}
pub(super) fn has_websocket_handler(&self) -> bool {
self.key_websocket.is_some()
}
pub(super) fn request_handler<'lua>(&self, lua: &'lua Lua) -> LuaResult<LuaFunction<'lua>> {
lua.named_registry_value(self.key_request)
}
pub(super) fn websocket_handler<'lua>(
&self,
lua: &'lua Lua,
) -> LuaResult<Option<LuaFunction<'lua>>> {
self.key_websocket
.map(|key| lua.named_registry_value(key))
.transpose()
}
}

View file

@ -1,105 +1,121 @@
use std::{
net::SocketAddr,
rc::{Rc, Weak},
};
use std::{cell::Cell, net::SocketAddr, rc::Rc};
use hyper::server::conn::http1;
use hyper_util::rt::TokioIo;
use tokio::{net::TcpListener, pin};
use async_net::TcpListener;
use futures_lite::pin;
use hyper::server::conn::http1::Builder as Http1Builder;
use mlua::prelude::*;
use mlua_luau_scheduler::LuaSpawnExt;
use lune_utils::TableBuilder;
use crate::{
server::{config::ServeConfig, handle::ServeHandle, service::Service},
shared::{
futures::{either, Either},
hyper::{HyperIo, HyperTimer},
},
};
use super::config::ServeConfig;
pub mod config;
pub mod handle;
pub mod service;
pub mod upgrade;
mod keys;
mod request;
mod response;
mod service;
/**
Starts an HTTP server using the given port and configuration.
use keys::SvcKeys;
use service::Svc;
pub async fn serve<'lua>(
lua: &'lua Lua,
port: u16,
config: ServeConfig<'lua>,
) -> LuaResult<LuaTable<'lua>> {
let addr: SocketAddr = (config.address, port).into();
let listener = TcpListener::bind(addr).await?;
let (lua_svc, lua_inner) = {
let rc = lua
.app_data_ref::<Weak<Lua>>()
.expect("Missing weak lua ref")
.upgrade()
.expect("Lua was dropped unexpectedly");
(Rc::clone(&rc), rc)
Returns a `ServeHandle` that can be used to gracefully stop the server.
*/
pub async fn serve(lua: Lua, port: u16, config: ServeConfig) -> LuaResult<ServeHandle> {
let address = SocketAddr::from((config.address, port));
let service = Service {
lua: lua.clone(),
address,
config,
};
let keys = SvcKeys::new(lua, config.handle_request, config.handle_web_socket)?;
let svc = Svc {
lua: lua_svc,
addr,
keys,
};
let listener = TcpListener::bind(address).await?;
let (handle, shutdown_rx) = ServeHandle::new(address);
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
lua.spawn_local(async move {
let mut shutdown_rx_outer = shutdown_rx.clone();
loop {
// Create futures for accepting new connections and shutting down
let fut_shutdown = shutdown_rx_outer.changed();
let fut_accept = async {
let stream = match listener.accept().await {
Err(_) => return,
Ok((s, _)) => s,
lua.spawn_local({
let lua = lua.clone();
async move {
let handle_dropped = Rc::new(Cell::new(false));
loop {
// 1. Keep accepting new connections until we should shutdown
let (conn, addr) = if handle_dropped.get() {
// 1a. Handle has been dropped, and we don't need to listen for shutdown
match listener.accept().await {
Ok(acc) => acc,
Err(_err) => {
// TODO: Propagate error somehow
continue;
}
}
} else {
// 1b. Handle is possibly active, we must listen for shutdown
match either(shutdown_rx.recv(), listener.accept()).await {
Either::Left(Ok(())) => break,
Either::Left(Err(_)) => {
// NOTE #1: We will only get a RecvError if the serve handle is dropped,
// this means lua has garbage collected it and the user does not want
// to manually stop the server using the serve handle. Run forever.
handle_dropped.set(true);
continue;
}
Either::Right(Ok(acc)) => acc,
Either::Right(Err(_err)) => {
// TODO: Propagate error somehow
continue;
}
}
};
let io = TokioIo::new(stream);
let svc = svc.clone();
let mut shutdown_rx_inner = shutdown_rx.clone();
// 2. For each connection, spawn a new task to handle it
lua.spawn_local({
let rx = shutdown_rx.clone();
let io = HyperIo::from(conn);
lua_inner.spawn_local(async move {
let conn = http1::Builder::new()
.keep_alive(true) // Web sockets need this
.serve_connection(io, svc)
.with_upgrades();
// NOTE: Because we need to use keep_alive for websockets, we need to
// also manually poll this future and handle the shutdown signal here
pin!(conn);
tokio::select! {
_ = conn.as_mut() => {}
_ = shutdown_rx_inner.changed() => {
conn.as_mut().graceful_shutdown();
let mut svc = service.clone();
svc.address = addr;
let handle_dropped = Rc::clone(&handle_dropped);
async move {
let conn = Http1Builder::new()
.writev(false)
.timer(HyperTimer)
.keep_alive(true)
.serve_connection(io, svc)
.with_upgrades();
if handle_dropped.get() {
if let Err(_err) = conn.await {
// TODO: Propagate error somehow
}
} else {
// NOTE #2: Because we use keep_alive for websockets above, we need to
// also manually poll this future and handle the graceful shutdown,
// otherwise the already accepted connection will linger and run
// even if the stop method has been called on the serve handle
pin!(conn);
match either(rx.recv(), conn.as_mut()).await {
Either::Left(Ok(())) => conn.as_mut().graceful_shutdown(),
Either::Left(Err(_)) => {
// Same as note #1
handle_dropped.set(true);
if let Err(_err) = conn.await {
// TODO: Propagate error somehow
}
}
Either::Right(Ok(())) => {}
Either::Right(Err(_err)) => {
// TODO: Propagate error somehow
}
}
}
}
});
};
// Wait for either a new connection or a shutdown signal
tokio::select! {
() = 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
// to manually stop the server using the serve handle. Run forever.
if res.is_ok() {
break;
}
}
}
}
});
TableBuilder::new(lua)?
.with_value("ip", addr.ip().to_string())?
.with_value("port", addr.port())?
.with_function("stop", move |_, (): ()| match shutdown_tx.send(true) {
Ok(()) => Ok(()),
Err(_) => Err(LuaError::runtime("Server already stopped")),
})?
.build_readonly()
Ok(handle)
}

View file

@ -1,54 +0,0 @@
use std::{collections::HashMap, net::SocketAddr};
use http::request::Parts;
use mlua::prelude::*;
use lune_utils::TableBuilder;
pub(super) struct LuaRequest {
pub(super) _remote_addr: SocketAddr,
pub(super) head: Parts,
pub(super) body: Vec<u8>,
}
impl LuaRequest {
pub fn into_lua_table(self, lua: &Lua) -> LuaResult<LuaTable> {
let method = self.head.method.as_str().to_string();
let path = self.head.uri.path().to_string();
let body = lua.create_string(&self.body)?;
let query: HashMap<LuaString, LuaString> = self
.head
.uri
.query()
.unwrap_or_default()
.split('&')
.filter_map(|q| q.split_once('='))
.map(|(k, v)| {
let k = lua.create_string(k)?;
let v = lua.create_string(v)?;
Ok((k, v))
})
.collect::<LuaResult<_>>()?;
let headers: HashMap<LuaString, LuaString> = self
.head
.headers
.iter()
.map(|(k, v)| {
let k = lua.create_string(k.as_str())?;
let v = lua.create_string(v.as_bytes())?;
Ok((k, v))
})
.collect::<LuaResult<_>>()?;
TableBuilder::new(lua)?
.with_value("method", method)?
.with_value("path", path)?
.with_value("query", query)?
.with_value("headers", headers)?
.with_value("body", body)?
.build()
}
}

View file

@ -1,89 +0,0 @@
use std::str::FromStr;
use bstr::{BString, ByteSlice};
use http_body_util::Full;
use hyper::{
body::Bytes,
header::{HeaderName, HeaderValue},
HeaderMap, Response,
};
use mlua::prelude::*;
#[derive(Debug, Clone, Copy)]
pub(super) enum LuaResponseKind {
PlainText,
Table,
}
pub(super) struct LuaResponse {
pub(super) kind: LuaResponseKind,
pub(super) status: u16,
pub(super) headers: HeaderMap,
pub(super) body: Option<Vec<u8>>,
}
impl LuaResponse {
pub(super) fn into_response(self) -> LuaResult<Response<Full<Bytes>>> {
Ok(match self.kind {
LuaResponseKind::PlainText => Response::builder()
.status(200)
.header("Content-Type", "text/plain")
.body(Full::new(Bytes::from(self.body.unwrap())))
.into_lua_err()?,
LuaResponseKind::Table => {
let mut response = Response::builder()
.status(self.status)
.body(Full::new(Bytes::from(self.body.unwrap_or_default())))
.into_lua_err()?;
response.headers_mut().extend(self.headers);
response
}
})
}
}
impl FromLua<'_> for LuaResponse {
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
match value {
// Plain strings from the handler are plaintext responses
LuaValue::String(s) => Ok(Self {
kind: LuaResponseKind::PlainText,
status: 200,
headers: HeaderMap::new(),
body: Some(s.as_bytes().to_vec()),
}),
// Tables are more detailed responses with potential status, headers, body
LuaValue::Table(t) => {
let status: Option<u16> = t.get("status")?;
let headers: Option<LuaTable> = t.get("headers")?;
let body: Option<BString> = t.get("body")?;
let mut headers_map = HeaderMap::new();
if let Some(headers) = headers {
for pair in headers.pairs::<String, LuaString>() {
let (h, v) = pair?;
let name = HeaderName::from_str(&h).into_lua_err()?;
let value = HeaderValue::from_bytes(v.as_bytes()).into_lua_err()?;
headers_map.insert(name, value);
}
}
let body_bytes = body.map(|s| s.as_bytes().to_vec());
Ok(Self {
kind: LuaResponseKind::Table,
status: status.unwrap_or(200),
headers: headers_map,
body: body_bytes,
})
}
// Anything else is an error
value => Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "NetServeResponse",
message: None,
}),
}
}
}

View file

@ -1,82 +1,116 @@
use std::{future::Future, net::SocketAddr, pin::Pin, rc::Rc};
use std::{future::Future, net::SocketAddr, pin::Pin};
use http_body_util::{BodyExt, Full};
use async_tungstenite::{tungstenite::protocol::Role, WebSocketStream};
use hyper::{
body::{Bytes, Incoming},
service::Service,
Request, Response,
body::Incoming, service::Service as HyperService, Request as HyperRequest,
Response as HyperResponse, StatusCode,
};
use hyper_tungstenite::{is_upgrade_request, upgrade};
use mlua::prelude::*;
use mlua_luau_scheduler::{LuaSchedulerExt, LuaSpawnExt};
use super::{
super::websocket::NetWebSocket, keys::SvcKeys, request::LuaRequest, response::LuaResponse,
use crate::{
body::ReadableBody,
server::{
config::ServeConfig,
upgrade::{is_upgrade_request, make_upgrade_response},
},
shared::{hyper::HyperIo, request::Request, response::Response, websocket::Websocket},
};
#[derive(Debug, Clone)]
pub(super) struct Svc {
pub(super) lua: Rc<Lua>,
pub(super) addr: SocketAddr,
pub(super) keys: SvcKeys,
pub(super) struct Service {
pub(super) lua: Lua,
pub(super) address: SocketAddr, // NOTE: This must be the remote address of the connected client
pub(super) config: ServeConfig,
}
impl Service<Request<Incoming>> for Svc {
type Response = Response<Full<Bytes>>;
impl HyperService<HyperRequest<Incoming>> for Service {
type Response = HyperResponse<ReadableBody>;
type Error = LuaError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn call(&self, req: Request<Incoming>) -> Self::Future {
let lua = self.lua.clone();
let addr = self.addr;
let keys = self.keys;
fn call(&self, req: HyperRequest<Incoming>) -> Self::Future {
if is_upgrade_request(&req) {
if let Some(handler) = self.config.handle_web_socket.clone() {
let lua = self.lua.clone();
return Box::pin(async move {
let response = match make_upgrade_response(&req) {
Ok(res) => res,
Err(err) => {
return Ok(HyperResponse::builder()
.status(StatusCode::BAD_REQUEST)
.body(ReadableBody::from(err.to_string()))
.unwrap())
}
};
if keys.has_websocket_handler() && is_upgrade_request(&req) {
Box::pin(async move {
let (res, sock) = upgrade(req, None).into_lua_err()?;
lua.spawn_local({
let lua = lua.clone();
async move {
if let Err(_err) = handle_websocket(lua, handler, req).await {
// TODO: Propagate the error somehow?
}
}
});
let lua_inner = lua.clone();
lua.spawn_local(async move {
let sock = sock.await.unwrap();
let lua_sock = NetWebSocket::new(sock);
let lua_tab = lua_sock.into_lua_table(&lua_inner).unwrap();
let handler_websocket: LuaFunction =
keys.websocket_handler(&lua_inner).unwrap().unwrap();
lua_inner
.push_thread_back(handler_websocket, lua_tab)
.unwrap();
Ok(response)
});
Ok(res)
})
} else {
let (head, body) = req.into_parts();
Box::pin(async move {
let handler_request: LuaFunction = keys.request_handler(&lua).unwrap();
let body = body.collect().await.into_lua_err()?;
let body = body.to_bytes().to_vec();
let lua_req = LuaRequest {
_remote_addr: addr,
head,
body,
};
let lua_req_table = lua_req.into_lua_table(&lua)?;
let thread_id = lua.push_thread_back(handler_request, lua_req_table)?;
lua.track_thread(thread_id);
lua.wait_for_thread(thread_id).await;
let thread_res = lua
.get_thread_result(thread_id)
.expect("Missing handler thread result")?;
LuaResponse::from_lua_multi(thread_res, &lua)?.into_response()
})
}
}
let lua = self.lua.clone();
let address = self.address;
let handler = self.config.handle_request.clone();
Box::pin(async move {
match handle_request(lua, handler, req, address).await {
Ok(response) => Ok(response),
Err(_err) => {
// TODO: Propagate the error somehow?
Ok(HyperResponse::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(ReadableBody::from("Lune: Internal server error"))
.unwrap())
}
}
})
}
}
async fn handle_request(
lua: Lua,
handler: LuaFunction,
request: HyperRequest<Incoming>,
address: SocketAddr,
) -> LuaResult<HyperResponse<ReadableBody>> {
let request = Request::from_incoming(request, true)
.await?
.with_address(address);
let thread_id = lua.push_thread_back(handler, request)?;
lua.track_thread(thread_id);
lua.wait_for_thread(thread_id).await;
let thread_res = lua
.get_thread_result(thread_id)
.expect("Missing handler thread result")?;
let response = Response::from_lua_multi(thread_res, &lua)?;
Ok(response.into_inner())
}
async fn handle_websocket(
lua: Lua,
handler: LuaFunction,
request: HyperRequest<Incoming>,
) -> LuaResult<()> {
let upgraded = hyper::upgrade::on(request).await.into_lua_err()?;
let stream =
WebSocketStream::from_raw_socket(HyperIo::from(upgraded), Role::Server, None).await;
let websocket = Websocket::from(stream);
lua.push_thread_back(handler, websocket)?;
Ok(())
}

View file

@ -0,0 +1,56 @@
use async_tungstenite::tungstenite::{error::ProtocolError, handshake::derive_accept_key};
use hyper::{
body::Incoming,
header::{HeaderName, CONNECTION, UPGRADE},
HeaderMap, Request as HyperRequest, Response as HyperResponse, StatusCode,
};
use crate::body::ReadableBody;
const SEC_WEBSOCKET_VERSION: HeaderName = HeaderName::from_static("sec-websocket-version");
const SEC_WEBSOCKET_KEY: HeaderName = HeaderName::from_static("sec-websocket-key");
const SEC_WEBSOCKET_ACCEPT: HeaderName = HeaderName::from_static("sec-websocket-accept");
pub fn is_upgrade_request(request: &HyperRequest<Incoming>) -> bool {
fn check_header_contains(headers: &HeaderMap, header_name: HeaderName, value: &str) -> bool {
headers.get(header_name).is_some_and(|header| {
header.to_str().map_or_else(
|_| false,
|header_str| {
header_str
.split(',')
.any(|part| part.trim().eq_ignore_ascii_case(value))
},
)
})
}
check_header_contains(request.headers(), CONNECTION, "Upgrade")
&& check_header_contains(request.headers(), UPGRADE, "websocket")
}
pub fn make_upgrade_response(
request: &HyperRequest<Incoming>,
) -> Result<HyperResponse<ReadableBody>, ProtocolError> {
let key = request
.headers()
.get(SEC_WEBSOCKET_KEY)
.ok_or(ProtocolError::MissingSecWebSocketKey)?;
if request
.headers()
.get(SEC_WEBSOCKET_VERSION)
.is_none_or(|v| v.as_bytes() != b"13")
{
return Err(ProtocolError::MissingSecWebSocketVersionHeader);
}
Ok(HyperResponse::builder()
.status(StatusCode::SWITCHING_PROTOCOLS)
.header(CONNECTION, "upgrade")
.header(UPGRADE, "websocket")
.header(SEC_WEBSOCKET_ACCEPT, derive_accept_key(key.as_bytes()))
.body(ReadableBody::from("switching to websocket protocol"))
.unwrap())
}

View file

@ -0,0 +1,19 @@
use futures_lite::prelude::*;
pub use http_body_util::Either;
/**
Combines the left and right futures into a single future
that resolves to either the left or right output.
This combinator is biased - if both futures resolve at
the same time, the left future's output is returned.
*/
pub fn either<L: Future, R: Future>(
left: L,
right: R,
) -> impl Future<Output = Either<L::Output, R::Output>> {
let fut_left = async move { Either::Left(left.await) };
let fut_right = async move { Either::Right(right.await) };
fut_left.or(fut_right)
}

View file

@ -0,0 +1,88 @@
use std::collections::HashMap;
use hyper::{
header::{CONTENT_ENCODING, CONTENT_LENGTH},
HeaderMap,
};
use lune_utils::TableBuilder;
use mlua::prelude::*;
pub fn create_user_agent_header(lua: &Lua) -> LuaResult<String> {
let version_global = lua
.globals()
.get::<LuaString>("_VERSION")
.expect("Missing _VERSION global");
let version_global_str = version_global
.to_str()
.context("Invalid utf8 found in _VERSION global")?;
let (package_name, full_version) = version_global_str.split_once(' ').unwrap();
Ok(format!("{}/{}", package_name.to_lowercase(), full_version))
}
pub fn header_map_to_table(
lua: &Lua,
headers: HeaderMap,
remove_content_headers: bool,
) -> LuaResult<LuaTable> {
let mut string_map = HashMap::<String, Vec<String>>::new();
for (name, value) in headers {
if let Some(name) = name {
if let Ok(value) = value.to_str() {
string_map
.entry(name.to_string())
.or_default()
.push(value.to_owned());
}
}
}
hash_map_to_table(lua, string_map, remove_content_headers)
}
pub fn hash_map_to_table(
lua: &Lua,
map: impl IntoIterator<Item = (String, Vec<String>)>,
remove_content_headers: bool,
) -> LuaResult<LuaTable> {
let mut string_map = HashMap::<String, Vec<String>>::new();
for (name, values) in map {
let name = name.as_str();
if remove_content_headers {
let content_encoding_header_str = CONTENT_ENCODING.as_str();
let content_length_header_str = CONTENT_LENGTH.as_str();
if name == content_encoding_header_str || name == content_length_header_str {
continue;
}
}
for value in values {
let value = value.as_str();
string_map
.entry(name.to_owned())
.or_default()
.push(value.to_owned());
}
}
let mut builder = TableBuilder::new(lua.clone())?;
for (name, mut values) in string_map {
if values.len() == 1 {
let value = values.pop().unwrap().into_lua(lua)?;
builder = builder.with_value(name, value)?;
} else {
let values = TableBuilder::new(lua.clone())?
.with_sequential_values(values)?
.build_readonly()?
.into_lua(lua)?;
builder = builder.with_value(name, values)?;
}
}
builder.build_readonly()
}

View file

@ -0,0 +1,198 @@
use std::{
future::Future,
io,
pin::Pin,
slice,
task::{Context, Poll},
time::{Duration, Instant},
};
use async_io::Timer;
use futures_lite::{prelude::*, ready};
use hyper::rt::{self, Executor, ReadBuf, ReadBufCursor};
use mlua::prelude::*;
use mlua_luau_scheduler::LuaSpawnExt;
// Hyper executor that spawns futures onto our Lua scheduler
#[derive(Debug, Clone)]
pub struct HyperExecutor {
lua: Lua,
}
#[allow(dead_code)]
impl HyperExecutor {
pub fn execute<Fut>(lua: Lua, fut: Fut)
where
Fut: Future + Send + 'static,
Fut::Output: Send + 'static,
{
let exec = if let Some(exec) = lua.app_data_ref::<Self>() {
exec
} else {
lua.set_app_data(Self { lua: lua.clone() });
lua.app_data_ref::<Self>().unwrap()
};
exec.execute(fut);
}
}
impl<Fut: Future + Send + 'static> rt::Executor<Fut> for HyperExecutor
where
Fut::Output: Send + 'static,
{
fn execute(&self, fut: Fut) {
self.lua.spawn(fut).detach();
}
}
// Hyper timer & sleep future wrapper for async-io
#[derive(Debug)]
pub struct HyperTimer;
impl rt::Timer for HyperTimer {
fn sleep(&self, duration: Duration) -> Pin<Box<dyn rt::Sleep>> {
Box::pin(HyperSleep::from(Timer::after(duration)))
}
fn sleep_until(&self, at: Instant) -> Pin<Box<dyn rt::Sleep>> {
Box::pin(HyperSleep::from(Timer::at(at)))
}
fn reset(&self, sleep: &mut Pin<Box<dyn rt::Sleep>>, new_deadline: Instant) {
if let Some(mut sleep) = sleep.as_mut().downcast_mut_pin::<HyperSleep>() {
sleep.inner.set_at(new_deadline);
} else {
*sleep = Box::pin(HyperSleep::from(Timer::at(new_deadline)));
}
}
}
#[derive(Debug)]
pub struct HyperSleep {
inner: Timer,
}
impl From<Timer> for HyperSleep {
fn from(inner: Timer) -> Self {
Self { inner }
}
}
impl Future for HyperSleep {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
match Pin::new(&mut self.inner).poll(cx) {
Poll::Ready(_) => Poll::Ready(()),
Poll::Pending => Poll::Pending,
}
}
}
impl rt::Sleep for HyperSleep {}
// Hyper I/O wrapper for bidirectional compatibility
// between hyper & futures-lite async read/write traits
pin_project_lite::pin_project! {
#[derive(Debug)]
pub struct HyperIo<T> {
#[pin]
inner: T
}
}
impl<T> From<T> for HyperIo<T> {
fn from(inner: T) -> Self {
Self { inner }
}
}
impl<T> HyperIo<T> {
pub fn pin_mut(self: Pin<&mut Self>) -> Pin<&mut T> {
self.project().inner
}
}
// Compat for futures-lite -> hyper runtime
impl<T: AsyncRead> rt::Read for HyperIo<T> {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
mut buf: ReadBufCursor<'_>,
) -> Poll<io::Result<()>> {
// Fill the read buffer with initialized data
let read_slice = unsafe {
let buffer = buf.as_mut();
buffer.as_mut_ptr().write_bytes(0, buffer.len());
slice::from_raw_parts_mut(buffer.as_mut_ptr().cast::<u8>(), buffer.len())
};
// Read bytes from the underlying source
let n = match self.pin_mut().poll_read(cx, read_slice) {
Poll::Ready(Ok(n)) => n,
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
};
unsafe {
buf.advance(n);
}
Poll::Ready(Ok(()))
}
}
impl<T: AsyncWrite> rt::Write for HyperIo<T> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
self.pin_mut().poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self.pin_mut().poll_flush(cx)
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self.pin_mut().poll_close(cx)
}
}
// Compat for hyper runtime -> futures-lite
impl<T: rt::Read> AsyncRead for HyperIo<T> {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
let mut buf = ReadBuf::new(buf);
ready!(self.pin_mut().poll_read(cx, buf.unfilled()))?;
Poll::Ready(Ok(buf.filled().len()))
}
}
impl<T: rt::Write> AsyncWrite for HyperIo<T> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
self.pin_mut().poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
self.pin_mut().poll_flush(cx)
}
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self.pin_mut().poll_shutdown(cx)
}
}

View file

@ -0,0 +1,41 @@
use hyper::{
header::{HeaderName, HeaderValue},
HeaderMap, Method,
};
use mlua::prelude::*;
pub fn lua_value_to_method(value: &LuaValue) -> LuaResult<Method> {
match value {
LuaValue::Nil => Ok(Method::GET),
LuaValue::String(str) => {
let bytes = str.as_bytes().trim_ascii().to_ascii_uppercase();
Method::from_bytes(&bytes).into_lua_err()
}
LuaValue::Buffer(buf) => {
let bytes = buf.to_vec().trim_ascii().to_ascii_uppercase();
Method::from_bytes(&bytes).into_lua_err()
}
v => Err(LuaError::FromLuaConversionError {
from: v.type_name(),
to: "Method".to_string(),
message: Some(format!(
"Invalid method - expected string or buffer, got {}",
v.type_name()
)),
}),
}
}
pub fn lua_table_to_header_map(table: &LuaTable) -> LuaResult<HeaderMap> {
let mut headers = HeaderMap::new();
for pair in table.pairs::<LuaString, LuaString>() {
let (key, val) = pair?;
let key = HeaderName::from_bytes(&key.as_bytes()).into_lua_err()?;
let val = HeaderValue::from_bytes(&val.as_bytes()).into_lua_err()?;
headers.insert(key, val);
}
Ok(headers)
}

View file

@ -0,0 +1,7 @@
pub mod futures;
pub mod headers;
pub mod hyper;
pub mod lua;
pub mod request;
pub mod response;
pub mod websocket;

View file

@ -0,0 +1,256 @@
use std::{collections::HashMap, net::SocketAddr};
use url::Url;
use hyper::{body::Incoming, HeaderMap, Method, Request as HyperRequest};
use mlua::prelude::*;
use crate::{
body::{handle_incoming_body, ReadableBody},
shared::{
headers::{hash_map_to_table, header_map_to_table},
lua::{lua_table_to_header_map, lua_value_to_method},
},
};
#[derive(Debug, Clone)]
pub struct RequestOptions {
pub decompress: bool,
}
impl Default for RequestOptions {
fn default() -> Self {
Self { decompress: true }
}
}
impl FromLua for RequestOptions {
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
if let LuaValue::Nil = value {
// Nil means default options
Ok(Self::default())
} else if let LuaValue::Table(tab) = value {
// Table means custom options
let decompress = match tab.get::<Option<bool>>("decompress") {
Ok(decomp) => Ok(decomp.unwrap_or(true)),
Err(_) => Err(LuaError::RuntimeError(
"Invalid option value for 'decompress' in request options".to_string(),
)),
}?;
Ok(Self { decompress })
} else {
// Anything else is invalid
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "RequestOptions".to_string(),
message: Some(format!(
"Invalid request options - expected table or nil, got {}",
value.type_name()
)),
})
}
}
}
#[derive(Debug, Clone)]
pub struct Request {
pub(crate) inner: HyperRequest<ReadableBody>,
pub(crate) address: Option<SocketAddr>,
pub(crate) redirects: Option<usize>,
pub(crate) decompress: bool,
}
impl Request {
/**
Creates a new request from a raw incoming request.
*/
pub async fn from_incoming(
incoming: HyperRequest<Incoming>,
decompress: bool,
) -> LuaResult<Self> {
let (parts, body) = incoming.into_parts();
let (body, decompress) = handle_incoming_body(&parts.headers, body, decompress).await?;
Ok(Self {
inner: HyperRequest::from_parts(parts, ReadableBody::from(body)),
address: None,
redirects: None,
decompress,
})
}
/**
Attaches a socket address to the request.
This will make the `ip` and `port` fields available on the request.
*/
pub fn with_address(mut self, address: SocketAddr) -> Self {
self.address = Some(address);
self
}
/**
Returns the method of the request.
*/
pub fn method(&self) -> Method {
self.inner.method().clone()
}
/**
Returns the path of the request.
*/
pub fn path(&self) -> &str {
self.inner.uri().path()
}
/**
Returns the query parameters of the request.
*/
pub fn query(&self) -> HashMap<String, Vec<String>> {
let uri = self.inner.uri();
let mut result = HashMap::<String, Vec<String>>::new();
if let Some(query) = uri.query() {
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
result
.entry(key.to_string())
.or_default()
.push(value.to_string());
}
}
result
}
/**
Returns the headers of the request.
*/
pub fn headers(&self) -> &HeaderMap {
self.inner.headers()
}
/**
Returns the body of the request.
*/
pub fn body(&self) -> &[u8] {
self.inner.body().as_slice()
}
/**
Clones the inner `hyper` request.
*/
#[allow(dead_code)]
pub fn clone_inner(&self) -> HyperRequest<ReadableBody> {
self.inner.clone()
}
/**
Takes the inner `hyper` request by ownership.
*/
#[allow(dead_code)]
pub fn into_inner(self) -> HyperRequest<ReadableBody> {
self.inner
}
}
impl FromLua for Request {
fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
if let LuaValue::String(s) = value {
// If we just got a string we assume
// its a GET request to a given url
let uri = s.to_str()?;
let uri = uri.parse().into_lua_err()?;
let mut request = HyperRequest::new(ReadableBody::empty());
*request.uri_mut() = uri;
Ok(Self {
inner: request,
address: None,
redirects: None,
decompress: RequestOptions::default().decompress,
})
} else if let LuaValue::Table(tab) = value {
// If we got a table we are able to configure the
// entire request, maybe with extra options too
let options = match tab.get::<LuaValue>("options") {
Ok(opts) => RequestOptions::from_lua(opts, lua)?,
Err(_) => RequestOptions::default(),
};
// Extract url (required) + optional structured query params
let url = tab.get::<LuaString>("url")?;
let mut url = url.to_str()?.parse::<Url>().into_lua_err()?;
if let Some(t) = tab.get::<Option<LuaTable>>("query")? {
let mut query = url.query_pairs_mut();
for pair in t.pairs::<LuaString, LuaString>() {
let (key, value) = pair?;
let key = key.to_str()?;
let value = value.to_str()?;
query.append_pair(&key, &value);
}
}
// Extract method
let method = tab.get::<LuaValue>("method")?;
let method = lua_value_to_method(&method)?;
// Extract headers
let headers = tab.get::<Option<LuaTable>>("headers")?;
let headers = headers
.map(|t| lua_table_to_header_map(&t))
.transpose()?
.unwrap_or_default();
// Extract body
let body = tab.get::<ReadableBody>("body")?;
// Build the full request
let mut request = HyperRequest::new(body);
request.headers_mut().extend(headers);
*request.uri_mut() = url.to_string().parse().unwrap();
*request.method_mut() = method;
// All good, validated and we got what we need
Ok(Self {
inner: request,
address: None,
redirects: None,
decompress: options.decompress,
})
} else {
// Anything else is invalid
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "Request".to_string(),
message: Some(format!(
"Invalid request - expected string or table, got {}",
value.type_name()
)),
})
}
}
}
impl LuaUserData for Request {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("ip", |_, this| {
Ok(this.address.map(|address| address.ip().to_string()))
});
fields.add_field_method_get("port", |_, this| {
Ok(this.address.map(|address| address.port()))
});
fields.add_field_method_get("method", |_, this| Ok(this.method().to_string()));
fields.add_field_method_get("path", |_, this| Ok(this.path().to_string()));
fields.add_field_method_get("query", |lua, this| {
hash_map_to_table(lua, this.query(), false)
});
fields.add_field_method_get("headers", |lua, this| {
header_map_to_table(lua, this.headers().clone(), this.decompress)
});
fields.add_field_method_get("body", |lua, this| lua.create_string(this.body()));
}
}

Some files were not shown because too many files have changed in this diff Show more