Skip to content

Commit

Permalink
Add typedefs and tests for custom instance properties and methods
Browse files Browse the repository at this point in the history
  • Loading branch information
filiptibell committed Oct 9, 2023
1 parent 9fe3b02 commit 1aa6aef
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 0 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions tests/roblox/instance/custom/async.luau
Original file line number Diff line number Diff line change
@@ -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()
48 changes: 48 additions & 0 deletions tests/roblox/instance/custom/methods.luau
Original file line number Diff line number Diff line change
@@ -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")
64 changes: 64 additions & 0 deletions tests/roblox/instance/custom/properties.luau
Original file line number Diff line number Diff line change
@@ -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
107 changes: 107 additions & 0 deletions types/roblox.luau
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down

0 comments on commit 1aa6aef

Please sign in to comment.