Add typedefs and tests for custom instance properties and methods

This commit is contained in:
Filip Tibell 2023-10-08 23:05:19 -05:00
parent 9fe3b02d71
commit 1aa6aef679
No known key found for this signature in database
6 changed files with 255 additions and 0 deletions

View file

@ -8,6 +8,26 @@ 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 `implementProperty` and `implementMethod` to the `roblox` built-in library to fill in missing functionality that Lune does not aim to implement itself.
Example usage:
```lua
local roblox = require("@lune/roblox")
local part = roblox.Instance.new("Part")
roblox.implementMethod("BasePart", "TestMethod", function(_, ...)
print("Tried to call TestMethod with", ...)
end)
part:TestMethod("Hello", "world!")
```
## `0.7.8` - October 5th, 2023
### Added

View file

@ -164,6 +164,10 @@ create_tests! {
roblox_instance_classes_workspace: "roblox/instance/classes/Workspace",
roblox_instance_classes_terrain: "roblox/instance/classes/Terrain",
roblox_instance_custom_async: "roblox/instance/custom/async",
roblox_instance_custom_methods: "roblox/instance/custom/methods",
roblox_instance_custom_properties: "roblox/instance/custom/properties",
roblox_instance_methods_clear_all_children: "roblox/instance/methods/ClearAllChildren",
roblox_instance_methods_clone: "roblox/instance/methods/Clone",
roblox_instance_methods_destroy: "roblox/instance/methods/Destroy",

View file

@ -0,0 +1,12 @@
local roblox = require("@lune/roblox")
local game = roblox.Instance.new("DataModel")
local http = game:GetService("HttpService") :: any
roblox.implementMethod("HttpService", "GetAsync", function()
-- TODO: Fill in method body
end)
-- TODO: Fill in rest of test cases here
http:GetAsync()

View file

@ -0,0 +1,48 @@
local roblox = require("@lune/roblox")
local inst = roblox.Instance.new("Instance") :: any
local part = roblox.Instance.new("Part") :: any
-- Basic sanity checks for callbacks
local success = pcall(function()
inst:Wat()
end)
assert(not success, "Nonexistent methods should error")
roblox.implementMethod("Instance", "Wat", function() end)
local success2 = pcall(function()
inst:Wat()
end)
assert(success2, "Nonexistent methods should error, unless implemented")
-- Instance should be passed to callback
roblox.implementMethod("Instance", "PassingInstanceTest", function(instance)
assert(instance == inst, "Invalid instance was passed to callback")
end)
roblox.implementMethod("Part", "PassingPartTest", function(instance)
assert(instance == part, "Invalid instance was passed to callback")
end)
inst:PassingInstanceTest()
part:PassingPartTest()
-- Any number of args passed & returned should work
roblox.implementMethod("Instance", "Echo", function(_, ...)
return ...
end)
local one, two, three = inst:Echo("one", "two", "three")
assert(one == "one", "implementMethod callback should return proper values")
assert(two == "two", "implementMethod callback should return proper values")
assert(three == "three", "implementMethod callback should return proper values")
-- Methods implemented by Lune should take precedence
roblox.implementMethod("Instance", "FindFirstChild", function()
error("unreachable")
end)
inst:FindFirstChild("Test")
part:FindFirstChild("Test")

View file

@ -0,0 +1,64 @@
local roblox = require("@lune/roblox")
local inst = roblox.Instance.new("Instance") :: any
local part = roblox.Instance.new("Part") :: any
-- Basic sanity checks for callbacks
local success = pcall(function()
local _ = inst.Wat
end)
assert(not success, "Nonexistent properties should error")
roblox.implementProperty("Instance", "Wat", function()
return nil
end)
local success2 = pcall(function()
local _ = inst.Wat
end)
assert(success2, "Nonexistent properties should error, unless implemented")
-- Instance should be passed to callback
roblox.implementProperty("Instance", "PassingInstanceTest", function(instance)
assert(instance == inst, "Invalid instance was passed to callback")
return nil
end)
roblox.implementProperty("Part", "PassingPartTest", function(instance)
assert(instance == part, "Invalid instance was passed to callback")
return nil
end)
local _ = inst.PassingInstanceTest
local _ = part.PassingPartTest
-- Any number of args passed & returned should work
local counters = {}
roblox.implementProperty("Instance", "Counter", function(instance)
-- FIXME: Instances do not make for unique table keys for some reason ...
local value = counters[tostring(instance)] or 0
value += 1
counters[tostring(instance)] = value
return value
end, function(instance, value)
counters[tostring(instance)] = value
end)
assert(inst.Counter == 1, "implementProperty callback should return proper values")
assert(inst.Counter == 2, "implementProperty callback should return proper values")
assert(inst.Counter == 3, "implementProperty callback should return proper values")
inst.Counter = 10
assert(inst.Counter == 11, "implementProperty callback should set proper values")
assert(inst.Counter == 12, "implementProperty callback should return proper values")
assert(inst.Counter == 13, "implementProperty callback should return proper values")
-- Properties implemented by Lune should take precedence
roblox.implementProperty("Instance", "Parent", function()
error("unreachable")
end)
local _ = inst.Parent
local _ = part.Parent

View file

@ -391,6 +391,113 @@ function roblox.getReflectionDatabase(): Database
return nil :: any
end
--[=[
@within Roblox
Implements a property for all instances of the given `className`.
This takes into account class hierarchies, so implementing a property
for the `BasePart` class will also implement it for `Part` and others,
unless a more specific implementation is added to the `Part` class directly.
### Behavior
The given `getter` callback will be called each time the property is
indexed, with the instance as its one and only argument. The `setter`
callback, if given, will be called each time the property should be set,
with the instance as the first argument and the property value as second.
### Example usage
```lua
local roblox = require("@lune/roblox")
local part = roblox.Instance.new("Part")
local propertyValues = {}
roblox.implementProperty(
"BasePart",
"CoolProp",
function(instance)
if propertyValues[instance] == nil then
propertyValues[instance] = 0
end
propertyValues[instance] += 1
return propertyValues[instance]
end,
function(instance, value)
propertyValues[instance] = value
end
)
print(part.CoolProp) --> 1
print(part.CoolProp) --> 2
print(part.CoolProp) --> 3
part.CoolProp = 10
print(part.CoolProp) --> 11
print(part.CoolProp) --> 12
print(part.CoolProp) --> 13
```
@param className The class to implement the property for.
@param propertyName The name of the property to implement.
@param getter The function which will be called to get the property value when indexed.
@param setter The function which will be called to set the property value when indexed. Defaults to a function that will error with a message saying the property is read-only.
]=]
function roblox.implementProperty<T>(
className: string,
propertyName: string,
getter: (instance: Instance) -> T,
setter: ((instance: Instance, value: T) -> ())?
)
return nil :: any
end
--[=[
@within Roblox
Implements a method for all instances of the given `className`.
This takes into account class hierarchies, so implementing a method
for the `BasePart` class will also implement it for `Part` and others,
unless a more specific implementation is added to the `Part` class directly.
### Behavior
The given `callback` will be called every time the method is called,
and will receive the instance it was called on as its first argument.
The remaining arguments will be what the caller passed to the method, and
all values returned from the callback will then be returned to the caller.
### Example usage
```lua
local roblox = require("@lune/roblox")
local part = roblox.Instance.new("Part")
roblox.implementMethod("BasePart", "TestMethod", function(instance, ...)
print("Called TestMethod on instance", instance, "with", ...)
end)
part:TestMethod("Hello", "world!")
--> Called TestMethod on instance Part with Hello, world!
```
@param className The class to implement the method for.
@param methodName The name of the method to implement.
@param callback The function which will be called when the method is called.
]=]
function roblox.implementMethod(
className: string,
methodName: string,
callback: (instance: Instance, ...any) -> ...any
)
return nil :: any
end
-- TODO: Make typedefs for all of the datatypes as well...
roblox.Instance = (nil :: any) :: {
new: ((className: "DataModel") -> DataModel) & ((className: string) -> Instance),