Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement attribute support into New function. #199

Merged
merged 17 commits into from
Feb 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/Instances/Attribute.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
--!strict

--[[
A special key for property tables, which allows users to apply custom
attributes to instances
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local logError = require(Package.Logging.logError)
local xtypeof = require(Package.Utility.xtypeof)
local Observer = require(Package.State.Observer)

local function setAttribute(instance: Instance, attribute: string, value: any)
instance:SetAttribute(attribute, value)
end

local function bindAttribute(instance: Instance, attribute: string, value: any, cleanupTasks: {PubTypes.Task})
if xtypeof(value) == "State" then
local didDefer = false
local function update()
if not didDefer then
didDefer = true
task.defer(function()
didDefer = false
setAttribute(instance, attribute, value:get(false))
end)
end
end
setAttribute(instance, attribute, value:get(false))
table.insert(cleanupTasks, Observer(value :: any):onChange(update))
else
setAttribute(instance, attribute, value)
end
end

local function Attribute(attributeName: string): PubTypes.SpecialKey
local AttributeKey = {}
AttributeKey.type = "SpecialKey"
AttributeKey.kind = "Attribute"
AttributeKey.stage = "self"

if attributeName == nil then
logError("attributeNameNil")
end

function AttributeKey:apply(attributeValue: any, applyTo: Instance, cleanupTasks: {PubTypes.Task})
bindAttribute(applyTo, attributeName, attributeValue, cleanupTasks)
end
return AttributeKey
end

return Attribute
40 changes: 40 additions & 0 deletions src/Instances/AttributeChange.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
--!strict

--[[
A special key for property tables, which allows users to connect to
an attribute change on an instance.
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local logError = require(Package.Logging.logError)
local xtypeof = require(Package.Utility.xtypeof)

local function AttributeChange(attributeName: string): PubTypes.SpecialKey
local attributeKey = {}
attributeKey.type = "SpecialKey"
attributeKey.kind = "AttributeChange"
attributeKey.stage = "observer"

if attributeName == nil then
logError("attributeNameNil")
end

function attributeKey:apply(callback: any, applyTo: Instance, cleanupTasks: {PubTypes.Task})
if typeof(callback) ~= "function" then
logError("invalidAttributeChangeHandler", nil, attributeName)
end
local ok, event = pcall(applyTo.GetAttributeChangedSignal, applyTo, attributeName)
if not ok then
logError("cannotConnectAttributeChange", nil, applyTo.ClassName, attributeName)
else
callback((applyTo :: any):GetAttribute(attributeName))
table.insert(cleanupTasks, event:Connect(function()
callback((applyTo :: any):GetAttribute(attributeName))
end))
end
end
return attributeKey
end

return AttributeChange
43 changes: 43 additions & 0 deletions src/Instances/AttributeOut.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
--!strict

--[[
A special key for property tables, which allows users to save instance attributes
into state objects
]]

local Package = script.Parent.Parent
local PubTypes = require(Package.PubTypes)
local logError = require(Package.Logging.logError)
local xtypeof = require(Package.Utility.xtypeof)

local function AttributeOut(attributeName: string): PubTypes.SpecialKey
local attributeOutKey = {}
attributeOutKey.type = "SpecialKey"
attributeOutKey.kind = "AttributeOut"
attributeOutKey.stage = "observer"

function attributeOutKey:apply(stateObject: PubTypes.StateObject, applyTo: Instance, cleanupTasks: { PubTypes.Task })
if xtypeof(stateObject) ~= "State" or stateObject.kind ~= "Value" then
logError("invalidAttributeOutType")
end
if attributeName == nil then
logError("attributeNameNil")
end
local ok, event = pcall(applyTo.GetAttributeChangedSignal, applyTo, attributeName)
if not ok then
logError("invalidOutAttributeName", applyTo.ClassName, attributeName)
else
stateObject:set((applyTo :: any):GetAttribute(attributeName))
table.insert(cleanupTasks, event:Connect(function()
stateObject:set((applyTo :: any):GetAttribute(attributeName))
end))
table.insert(cleanupTasks, function()
stateObject:set(nil)
end)
end
end

return attributeOutKey
end

return AttributeOut
5 changes: 5 additions & 0 deletions src/Logging/messages.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
]]

return {
attributeNameNil = "Attribute name cannot be nil",
cannotAssignProperty = "The class type '%s' has no assignable property '%s'.",
cannotConnectChange = "The %s class doesn't have a property called '%s'.",
cannotConnectAttributeChange = "The %s class doesn't have an attribute called '%s'.",
cannotConnectEvent = "The %s class doesn't have an event called '%s'.",
cannotCreateClass = "Can't create a new instance of class '%s'.",
computedCallbackError = "Computed callback error: ERROR_MESSAGE",
Expand All @@ -26,11 +28,14 @@ return {
forValuesProcessorError = "ForValues callback error: ERROR_MESSAGE",
forValuesDestructorError = "ForValues destructor error: ERROR_MESSAGE",
invalidChangeHandler = "The change handler for the '%s' property must be a function.",
invalidAttributeChangeHandler = "The change handler for the '%s' attribute must be a function.",
invalidEventHandler = "The handler for the '%s' event must be a function.",
invalidPropertyType = "'%s.%s' expected a '%s' type, but got a '%s' type.",
invalidRefType = "Instance refs must be Value objects.",
invalidOutType = "[Out] properties must be given Value objects.",
invalidAttributeOutType = "[AttributeOut] properties must be given Value objects.",
invalidOutProperty = "The %s class doesn't have a property called '%s'.",
invalidOutAttributeName = "The %s class doesn't have an attribute called '%s'.",
invalidSpringDamping = "The damping ratio for a spring must be >= 0. (damping was %.2f)",
invalidSpringSpeed = "The speed of a spring must be >= 0. (speed was %.2f)",
mistypedSpringDamping = "The damping ratio for a spring must be a number. (got a %s)",
Expand Down
6 changes: 6 additions & 0 deletions src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ type Fusion = {
Out: PubTypes.SpecialKey,
OnEvent: (eventName: string) -> PubTypes.SpecialKey,
OnChange: (propertyName: string) -> PubTypes.SpecialKey,
Attribute: (attributeName: string) -> PubTypes.SpecialKey,
AttributeChange: (attributeName: string) -> PubTypes.SpecialKey,
AttributeOut: (attributeName: string) -> PubTypes.SpecialKey,

Value: <T>(initialValue: T) -> Value<T>,
Computed: <T>(callback: () -> T, destructor: (T) -> ()?) -> Computed<T>,
Expand All @@ -56,6 +59,9 @@ return restrictRead("Fusion", {
Children = require(script.Instances.Children),
OnEvent = require(script.Instances.OnEvent),
OnChange = require(script.Instances.OnChange),
Attribute = require(script.Instances.Attribute),
AttributeChange = require(script.Instances.AttributeChange),
AttributeOut = require(script.Instances.AttributeOut),

Value = require(script.State.Value),
Computed = require(script.State.Computed),
Expand Down
61 changes: 61 additions & 0 deletions test/Instances/Attribute.spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
local Package = game:GetService("ReplicatedStorage").Fusion

local New = require(Package.Instances.New)
local Attribute = require(Package.Instances.Attribute)
local Value = require(Package.State.Value)

return function()
it("should create attributes (constant)", function()
local child = New "Folder" {
[Attribute "Foo"] = "Bar"
}
expect(child:GetAttribute("Foo")).to.equal("Bar")
end)

it("should create attributes (state)", function()
local attributeValue = Value("Bar")
local child = New "Folder" {
[Attribute "Foo"] = attributeValue
}
expect(child:GetAttribute("Foo")).to.equal("Bar")
end)

it("should update attributes when state objects are updated", function()
local attributeValue = Value("Bar")
local child = New "Folder" {
[Attribute "Foo"] = attributeValue
}
expect(child:GetAttribute("Foo")).to.equal("Bar")
attributeValue:set("Baz")
task.wait()
expect(child:GetAttribute("Foo")).to.equal("Baz")
end)

it("should error when given nil names (constant)", function()
expect(function()
local child = New "Folder" {
[Attribute(nil)] = "foo"
}
end).to.throw("attributeNameNil")
end)

it("should error when given nil names (state)", function()
expect(function()
local attributeValue = Value("foo")
local child = New "Folder" {
[Attribute(nil)] = attributeValue
}
end).to.throw("attributeNameNil")
end)

it("should defer attribute changes", function()
local value = Value("Bar")
local child = New "Folder" {
[Attribute "Foo"] = value
}
value:set("Baz")
expect(child:GetAttribute("Foo")).to.equal("Bar")
task.wait()
expect(child:GetAttribute("Foo")).to.equal("Baz")
end)
end
42 changes: 42 additions & 0 deletions test/Instances/AttributeChange.spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
local Package = game:GetService("ReplicatedStorage").Fusion
local New = require(Package.Instances.New)
local Attribute = require(Package.Instances.Attribute)
local AttributeChange = require(Package.Instances.AttributeChange)
local Value = require(Package.State.Value)

return function()
it("should connect attribute change handlers", function()
local changeCount = 0
local child = New "Folder" {
[Attribute "Foo"] = "Bar",
[AttributeChange "Foo"] = function()
changeCount += 1
end
}

child:SetAttribute("Foo", "Baz")
task.wait()
expect(changeCount).never.to.equal(0)
end)

it("should pass the updated value as an argument", function()
local updatedValue = ""
local child = New "Folder" {
[AttributeChange "Foo"] = function(newValue)
updatedValue = newValue
end
}

child:SetAttribute("Foo", "Baz")
task.wait()
expect(updatedValue).to.equal("Baz")
end)

it("should error when given an invalid handler", function()
expect(function()
local child = New "Folder" {
[AttributeChange "Foo"] = 0
}
end).to.throw("invalidAttributeChangeHandler")
end)
end
48 changes: 48 additions & 0 deletions test/Instances/AttributeOut.spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
local Package = game:GetService("ReplicatedStorage").Fusion
local New = require(Package.Instances.New)
local Attribute = require(Package.Instances.Attribute)
local AttributeOut = require(Package.Instances.AttributeOut)
local Value = require(Package.State.Value)

return function()
it("should update when attributes are changed externally", function()
local attributeValue = Value()
local child = New "Folder" {
[AttributeOut "Foo"] = attributeValue
}

expect(attributeValue:get()).to.equal(nil)
child:SetAttribute("Foo", "Bar")
task.wait()
expect(attributeValue:get()).to.equal("Bar")
end)

it("should update when state objects linked update", function()
local attributeValue = Value("Foo")
local attributeOutValue = Value()
local child = New "Folder" {
[Attribute "Foo"] = attributeValue,
[AttributeOut "Foo"] = attributeOutValue
}
expect(attributeOutValue:get()).to.equal("Foo")
attributeValue:set("Bar")
task.wait()
expect(attributeOutValue:get()).to.equal("Bar")
end)

it("should work with two-way connections", function()
local attributeValue = Value("Bar")
local child = New "Folder" {
[Attribute "Foo"] = attributeValue,
[AttributeOut "Foo"] = attributeValue
}

expect(attributeValue:get()).to.equal("Bar")
attributeValue:set("Baz")
task.wait()
expect(child:GetAttribute("Foo")).to.equal("Baz")
child:SetAttribute("Foo", "Biff")
task.wait()
expect(attributeValue:get()).to.equal("Biff")
end)
end
3 changes: 3 additions & 0 deletions test/init.spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ return function()
Children = "table",
OnEvent = "function",
OnChange = "function",
Attribute = "function",
AttributeChange = "function",
AttributeOut = "function",

Value = "function",
Computed = "function",
Expand Down