add gs
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Settings>
|
||||
<ContentFolder>content</ContentFolder>
|
||||
<BaseUrl>http://www.vortexi.cc</BaseUrl>
|
||||
</Settings>
|
||||
@@ -0,0 +1,41 @@
|
||||
stds.roblox = {
|
||||
globals = {
|
||||
"game"
|
||||
},
|
||||
read_globals = {
|
||||
-- Roblox globals
|
||||
"script",
|
||||
|
||||
-- Extra functions
|
||||
"tick", "warn", "spawn",
|
||||
"wait", "settings", "typeof", "delay",
|
||||
|
||||
-- Types
|
||||
"Vector2", "Vector3",
|
||||
"Color3",
|
||||
"UDim", "UDim2",
|
||||
"Rect",
|
||||
"CFrame",
|
||||
"Enum",
|
||||
"Instance",
|
||||
}
|
||||
}
|
||||
|
||||
stds.testez = {
|
||||
read_globals = {
|
||||
"describe",
|
||||
"it", "itFOCUS", "itSKIP",
|
||||
"FOCUS", "SKIP", "HACK_NO_XPCALL",
|
||||
"expect",
|
||||
}
|
||||
}
|
||||
|
||||
ignore = {
|
||||
"212", -- unused arguments
|
||||
}
|
||||
|
||||
std = "lua51+roblox"
|
||||
|
||||
files["**/*.spec.lua"] = {
|
||||
std = "+testez",
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"language": {
|
||||
"mode": "noinfer"
|
||||
},
|
||||
"lint": {
|
||||
"MultiLineStatement": "disabled",
|
||||
"DeprecatedGlobal": "disabled",
|
||||
"LocalShadow": "disabled",
|
||||
"LocalUnused": "disabled",
|
||||
"FunctionUnused": "fatal",
|
||||
"ImportUnused": "disabled",
|
||||
"SameLineStatement": "fatal",
|
||||
"ImplicitReturn": "disabled",
|
||||
"UnknownGlobal": "fatal",
|
||||
"GlobalUsedAsLocal": "fatal"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
--[[
|
||||
A centralized hub for basic metrics reporting.
|
||||
This class is designed to provide a baseline exposure to the reporters.
|
||||
|
||||
Context specific Analytics.lua objects should be created in sub-projects,
|
||||
to report specific actions like chat interactions, or game page interactions.
|
||||
|
||||
Analytics.lua and the reporters here in Common should serve to cover the
|
||||
most common interactions.
|
||||
]]
|
||||
|
||||
local AnalyticsService = game:GetService("RbxAnalyticsService")
|
||||
local Reporters = script.Parent.AnalyticsReporters
|
||||
|
||||
local DiagReporter = require(Reporters.Diag)
|
||||
local EventStreamReporter = require(Reporters.EventStream)
|
||||
local GoogleAnalyticsReporter = require(Reporters.GoogleAnalytics)
|
||||
local InfluxDbReporter = require(Reporters.Influx)
|
||||
|
||||
|
||||
local Analytics = {}
|
||||
Analytics.__index = Analytics
|
||||
|
||||
-- reportingService : (Service, optional) an object that exposes the same functions as AnalyticsService
|
||||
function Analytics.new(reportingService)
|
||||
if not reportingService then
|
||||
reportingService = AnalyticsService
|
||||
end
|
||||
|
||||
-- All public reporting functions are exposed by the objects defined in the properties
|
||||
local self = {}
|
||||
self.Diag = DiagReporter.new(reportingService)
|
||||
self.EventStream = EventStreamReporter.new(reportingService)
|
||||
self.GoogleAnalytics = GoogleAnalyticsReporter.new(reportingService)
|
||||
self.InfluxDb = InfluxDbReporter.new(reportingService)
|
||||
|
||||
setmetatable(self, Analytics)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Analytics.mock()
|
||||
-- create a reporting service that does not fire any requests out to the world
|
||||
local fakeReportingService = {}
|
||||
function fakeReportingService.ReportCounter() end
|
||||
function fakeReportingService.ReportInfluxSeries() end
|
||||
function fakeReportingService.ReportStats() end
|
||||
function fakeReportingService.SetRBXEvent() end
|
||||
function fakeReportingService.SetRBXEventStream() end
|
||||
function fakeReportingService.TrackEvent() end
|
||||
function fakeReportingService.UpdateHeartbeatObject() end
|
||||
|
||||
return Analytics.new(fakeReportingService)
|
||||
end
|
||||
|
||||
return Analytics
|
||||
@@ -0,0 +1,69 @@
|
||||
return function()
|
||||
local Analytics = require(script.Parent.Analytics)
|
||||
|
||||
describe("new()", function()
|
||||
it("should properly construct a new object", function()
|
||||
local na = Analytics.new()
|
||||
expect(na).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should accept a custom reporting service", function()
|
||||
local fakeService = {}
|
||||
local na = Analytics.new(fakeService)
|
||||
expect(na).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should have a reporter specifically for Diag", function()
|
||||
local na = Analytics.new()
|
||||
expect(na.Diag).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should have a reporter specifically for RBXEventStream", function()
|
||||
local na = Analytics.new()
|
||||
expect(na.EventStream).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should have a reporter specifically for Google Analytics", function()
|
||||
local na = Analytics.new()
|
||||
expect(na.GoogleAnalytics).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should have a reporter specifically for Influx", function()
|
||||
local na = Analytics.new()
|
||||
expect(na.InfluxDb).to.be.ok()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("mock()", function()
|
||||
it("should properly construct a new object", function()
|
||||
local ma = Analytics.mock()
|
||||
expect(ma).to.be.ok()
|
||||
expect(ma.Diag).to.be.ok()
|
||||
expect(ma.EventStream).to.be.ok()
|
||||
expect(ma.GoogleAnalytics).to.be.ok()
|
||||
expect(ma.InfluxDb).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should succeed for all function calls in Diag", function()
|
||||
local ma = Analytics.mock()
|
||||
ma.Diag:reportCounter("fakeCounter", 1)
|
||||
ma.Diag:reportStats("fakeCategory", 1)
|
||||
end)
|
||||
|
||||
it("should succeed for all function call in EventStream", function()
|
||||
local ma = Analytics.mock()
|
||||
ma.EventStream:setRBXEvent("fakeContext", "fakeEventName")
|
||||
ma.EventStream:setRBXEventStream("fakeContext", "fakeEventName")
|
||||
end)
|
||||
|
||||
it("should succeed for all function call in GoogleAnalytics", function()
|
||||
local ma = Analytics.mock()
|
||||
ma.GoogleAnalytics:trackEvent("fakeCategory", "fakeAction", "fakeLabel")
|
||||
end)
|
||||
|
||||
it("should succeed for all function call in Influx", function()
|
||||
local ma = Analytics.mock()
|
||||
ma.InfluxDb:reportSeries("fakeSeries", {}, 1)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,64 @@
|
||||
--[[
|
||||
Specialized analytics reporter for ephemeral counters.
|
||||
Useful for tracking at-a-glance health of a feature.
|
||||
]]
|
||||
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
|
||||
local Diag = {}
|
||||
Diag.__index = Diag
|
||||
|
||||
-- reportingService - (object) any object that defines the same functions for Diag as AnalyticsService
|
||||
function Diag.new(reportingService)
|
||||
local rsType = type(reportingService)
|
||||
assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService")
|
||||
|
||||
local self = {
|
||||
_reporter = reportingService,
|
||||
_isEnabled = true,
|
||||
}
|
||||
setmetatable(self, Diag)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-- isEnabled : (boolean)
|
||||
function Diag:setEnabled(isEnabled)
|
||||
assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean")
|
||||
self._isEnabled = isEnabled
|
||||
end
|
||||
|
||||
-- counterName : (string) the name of the ephemeral counter to increment
|
||||
-- amount : (int) the value to increment the counter by
|
||||
function Diag:reportCounter(counterName, amount)
|
||||
assert(type(counterName) == "string", "Expected counterName to be a string")
|
||||
assert(type(amount) == "number", "Expected amount to be a number")
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
-- use special naming convention for Xbox counters
|
||||
-- the call to GetPlatform is wrapped in a pcall() because the Testing Service
|
||||
-- executes the scripts in the wrong authorization level
|
||||
local platformID = Enum.Platform.None
|
||||
pcall(function()
|
||||
platformID = UserInputService:GetPlatform()
|
||||
end)
|
||||
if platformID == Enum.Platform.XBoxOne then
|
||||
counterName = "Xbox-" .. tostring(counterName)
|
||||
end
|
||||
|
||||
-- the AnalyticsService should automatically handle batch reporting
|
||||
self._reporter:ReportCounter(counterName, amount)
|
||||
end
|
||||
|
||||
|
||||
-- category : (string) the name of the statistics buffer which to append the data
|
||||
-- value : (number) any type of numeric value
|
||||
function Diag:reportStats(category, value)
|
||||
assert(type(category) == "string", "Expected category to be a string")
|
||||
assert(type(value) == "number", "Expected value to be a number")
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
self._reporter:ReportStats(category, value)
|
||||
end
|
||||
|
||||
return Diag
|
||||
@@ -0,0 +1,176 @@
|
||||
return function()
|
||||
local Diag = require(script.Parent.Diag)
|
||||
|
||||
local testCounterName = "testCounter"
|
||||
local testCounterAmount = 1
|
||||
local testCategoryName = "testCategory"
|
||||
local testCategoryValue = 98
|
||||
|
||||
local badTestCounterName = 5
|
||||
local badTestCounterAmount = "hello"
|
||||
local badTestCategoryName = {}
|
||||
local badTestCategoryValue = {}
|
||||
|
||||
local DebugReportingService = {}
|
||||
function DebugReportingService:ReportCounter(counterName, amount)
|
||||
assert(counterName == testCounterName, "Unexpected value for counterName: " .. counterName)
|
||||
assert(amount == testCounterAmount, "Unexpected value for amount: " .. amount)
|
||||
end
|
||||
function DebugReportingService:ReportStats(categoryName, value)
|
||||
assert(categoryName == testCategoryName, "Unexpected value for category: " .. categoryName)
|
||||
if value then
|
||||
assert(value == testCategoryValue, "Unexpected value for value: " .. value)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe("new()", function()
|
||||
it("should construct with a Reporting Service", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(diag).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should throw an error to be constructed without a Reporting Service", function()
|
||||
expect(function()
|
||||
Diag.new(nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("setEnabled()", function()
|
||||
it("should succeed with valid input", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
diag:setEnabled(false)
|
||||
diag:setEnabled(true)
|
||||
end)
|
||||
it("should disable the reporter", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
diag:setEnabled(false)
|
||||
expect(function()
|
||||
diag:reportCounter(testCounterName, testCounterAmount)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("reportCounter()", function()
|
||||
it("should work when appropriately enabled / disabled", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
|
||||
expect(function()
|
||||
diag:setEnabled(false)
|
||||
diag:reportCounter(testCounterName, testCounterAmount)
|
||||
end).to.throw()
|
||||
|
||||
diag:setEnabled(true)
|
||||
diag:reportCounter(testCounterName, testCounterAmount)
|
||||
end)
|
||||
|
||||
it("should succeed with valid input", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
diag:reportCounter(testCounterName, testCounterAmount)
|
||||
end)
|
||||
|
||||
it("should throw an error with invalid input for the counter name", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportCounter(badTestCounterName, testCounterAmount)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error with invalid input for the amount", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportCounter(testCounterName, badTestCounterAmount)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error with completely invalid input", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportCounter(badTestCounterName, badTestCounterAmount)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a counter name", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportCounter(nil, testCounterAmount)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing an amount", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportCounter(testCounterName, nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing any input", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportCounter(nil, nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("reportStats()", function()
|
||||
it("should work when appropriately enabled / disabled", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
|
||||
expect(function()
|
||||
diag:setEnabled(false)
|
||||
diag:reportStats(testCategoryName, testCategoryValue)
|
||||
end).to.throw()
|
||||
|
||||
diag:setEnabled(true)
|
||||
diag:reportStats(testCategoryName, testCategoryValue)
|
||||
end)
|
||||
|
||||
it("should succeed with valid input", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
diag:reportStats(testCategoryName, testCategoryValue)
|
||||
end)
|
||||
|
||||
it("should throw an error with invalid input for the category name", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportStats(badTestCategoryName, testCategoryValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error with invalid input for the value", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportStats(testCategoryName, badTestCategoryValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error with completely invalid input", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportStats(badTestCategoryName, badTestCategoryValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a category name", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportStats(nil, testCategoryValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a value", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportStats(testCategoryName, nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing any input", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportStats(nil, nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,154 @@
|
||||
--[[
|
||||
Specialized reporter for RBX Event Ingest data.
|
||||
Useful for tracking explicit user interactions with screens and guis.
|
||||
]]
|
||||
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
|
||||
local function getPlatformTarget()
|
||||
local platformTarget = "unknownLua"
|
||||
local platformEnum = Enum.Platform.None
|
||||
|
||||
-- the call to GetPlatform is wrapped in a pcall() because the Testing Service
|
||||
-- executes the scripts in the wrong authorization level
|
||||
pcall(function()
|
||||
platformEnum = UserInputService:GetPlatform()
|
||||
end)
|
||||
|
||||
-- bucket the platform based on consumer platform
|
||||
local isDesktopClient = (platformEnum == Enum.Platform.Windows) or (platformEnum == Enum.Platform.OSX)
|
||||
|
||||
local isMobileClient = (platformEnum == Enum.Platform.IOS) or (platformEnum == Enum.Platform.Android)
|
||||
isMobileClient = isMobileClient or (platformEnum == Enum.Platform.UWP)
|
||||
|
||||
local isConsole = (platformEnum == Enum.Platform.XBox360) or (platformEnum == Enum.Platform.XBoxOne)
|
||||
isConsole = isConsole or (platformEnum == Enum.Platform.PS3) or (platformEnum == Enum.Platform.PS4)
|
||||
isConsole = isConsole or (platformEnum == Enum.Platform.WiiU)
|
||||
|
||||
-- assign a target based on the form factor
|
||||
if isDesktopClient then
|
||||
platformTarget = "client"
|
||||
elseif isMobileClient then
|
||||
platformTarget = "mobile"
|
||||
elseif isConsole then
|
||||
platformTarget = "console"
|
||||
else
|
||||
-- if we don't have a name for the form factor, report it here so that we can eventually track it down
|
||||
platformTarget = platformTarget .. tostring(platformEnum)
|
||||
end
|
||||
|
||||
|
||||
return platformTarget
|
||||
end
|
||||
|
||||
|
||||
local EventStream = {}
|
||||
EventStream.__index = EventStream
|
||||
|
||||
-- reportingService - (object) any object that defines the same functions for Event Stream as AnalyticsService
|
||||
function EventStream.new(reportingService)
|
||||
local rsType = type(reportingService)
|
||||
assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService")
|
||||
|
||||
local self = {
|
||||
_reporter = reportingService,
|
||||
_isEnabled = true,
|
||||
}
|
||||
setmetatable(self, EventStream)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-- isEnabled : (boolean)
|
||||
function EventStream:setEnabled(isEnabled)
|
||||
assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean")
|
||||
self._isEnabled = isEnabled
|
||||
end
|
||||
|
||||
-- eventContext : (string) the location or context in which the event is occurring.
|
||||
-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example.
|
||||
-- additionalArgs : (optional, map<string, Value>) table for additional information to appear in the event stream.
|
||||
function EventStream:setRBXEvent(eventContext, eventName, additionalArgs)
|
||||
local target = getPlatformTarget()
|
||||
additionalArgs = additionalArgs or {}
|
||||
|
||||
assert(type(eventContext) == "string", "Expected eventContext to be a string")
|
||||
assert(type(eventName) == "string", "Expected eventName to be a string")
|
||||
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
-- This function fires reports to the server right away
|
||||
self._reporter:SetRBXEvent(target, eventContext, eventName, additionalArgs)
|
||||
end
|
||||
|
||||
|
||||
-- eventContext : (string) the location or context in which the event is occurring.
|
||||
-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example.
|
||||
-- additionalArgs : (optional, map<string, Value>) map for extra keys to appear in the event stream.
|
||||
function EventStream:setRBXEventStream(eventContext, eventName, additionalArgs)
|
||||
local target = getPlatformTarget()
|
||||
additionalArgs = additionalArgs or {}
|
||||
|
||||
assert(type(eventContext) == "string", "Expected eventContext to be a string")
|
||||
assert(type(eventName) == "string", "Expected eventName to be a string")
|
||||
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
-- this function sends reports to the server in batches, not real-time
|
||||
self._reporter:SetRBXEventStream(target, eventContext, eventName, additionalArgs)
|
||||
end
|
||||
|
||||
|
||||
function EventStream:releaseRBXEventStream()
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
self._reporter:ReleaseRBXEventStream(getPlatformTarget())
|
||||
end
|
||||
|
||||
|
||||
-- eventContext : (string) the location or context in which the event is occurring.
|
||||
-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example.
|
||||
-- additionalArgs : (optional, map<string, Value>) map for extra keys to appear in the event stream.
|
||||
function EventStream:sendEventDeferred(eventContext, eventName, additionalArgs)
|
||||
local target = getPlatformTarget()
|
||||
additionalArgs = additionalArgs or {}
|
||||
|
||||
assert(type(eventContext) == "string", "Expected eventContext to be a string")
|
||||
assert(type(eventName) == "string", "Expected eventName to be a string")
|
||||
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
-- this function sends reports to the server in batches, not real-time
|
||||
self._reporter:SendEventDeferred(target, eventContext, eventName, additionalArgs)
|
||||
end
|
||||
|
||||
|
||||
-- eventContext : (string) the location or context in which the event is occurring.
|
||||
-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example.
|
||||
-- additionalArgs : (optional, map<string, Value>) map for extra keys to appear in the event stream.
|
||||
function EventStream:sendEventImmediately(eventContext, eventName, additionalArgs)
|
||||
local target = getPlatformTarget()
|
||||
additionalArgs = additionalArgs or {}
|
||||
|
||||
assert(type(eventContext) == "string", "Expected eventContext to be a string")
|
||||
assert(type(eventName) == "string", "Expected eventName to be a string")
|
||||
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
-- this function sends reports to the server in batches, not real-time
|
||||
self._reporter:SendEventImmediately(target, eventContext, eventName, additionalArgs)
|
||||
end
|
||||
|
||||
|
||||
-- additionalArgs : (optional, map<string, string>) table for extra keys to appear in the event stream.
|
||||
function EventStream:updateHeartbeatObject(additionalArgs)
|
||||
additionalArgs = additionalArgs or {}
|
||||
|
||||
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
self._reporter:UpdateHeartbeatObject(additionalArgs)
|
||||
end
|
||||
|
||||
|
||||
return EventStream
|
||||
+393
@@ -0,0 +1,393 @@
|
||||
return function()
|
||||
|
||||
local EventStream = require(script.Parent.EventStream)
|
||||
|
||||
local testArgs = {
|
||||
testKey = "testValue"
|
||||
}
|
||||
local testContext = "testContext"
|
||||
local testEvent = "testEventName"
|
||||
local badTestArgs = "hello"
|
||||
local badTestContext = {}
|
||||
local badTestEvent = {}
|
||||
|
||||
|
||||
local function isTableEqual(table1, table2)
|
||||
if table1 == table2 then
|
||||
return true
|
||||
end
|
||||
|
||||
if type(table1) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
if type(table2) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
for key, _ in pairs(table1) do
|
||||
if table1[key] ~= table2[key] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
for key, _ in pairs(table2) do
|
||||
if table2[key] ~= table1[key] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function createDebugReportingService()
|
||||
local function validateInputs(eventTarget, eventContext, eventName, additionalArgs)
|
||||
assert(eventTarget, "no value found for eventTarget")
|
||||
assert(eventContext == testContext, "unexpected value for eventContext : " .. eventContext)
|
||||
assert(eventName == testEvent, "unexpected value for eventName : " .. eventName)
|
||||
if additionalArgs and not isTableEqual(additionalArgs, {}) then
|
||||
assert(isTableEqual(additionalArgs, testArgs), "unexpected value for additionalArgs")
|
||||
end
|
||||
end
|
||||
|
||||
local DebugReportingService = {}
|
||||
function DebugReportingService:SetRBXEvent(eventTarget, eventContext, eventName, additionalArgs)
|
||||
validateInputs(eventTarget, eventContext, eventName, additionalArgs)
|
||||
end
|
||||
function DebugReportingService:SetRBXEventStream(eventTarget, eventContext, eventName, additionalArgs)
|
||||
validateInputs(eventTarget, eventContext, eventName, additionalArgs)
|
||||
end
|
||||
function DebugReportingService:SendEventImmediately(eventTarget, eventContext, eventName, additionalArgs)
|
||||
validateInputs(eventTarget, eventContext, eventName, additionalArgs)
|
||||
end
|
||||
function DebugReportingService:SendEventDeferred(eventTarget, eventContext, eventName, additionalArgs)
|
||||
validateInputs(eventTarget, eventContext, eventName, additionalArgs)
|
||||
end
|
||||
function DebugReportingService:ReleaseRBXEventStream(eventTarget)
|
||||
assert(eventTarget, "no value found for eventTarget")
|
||||
end
|
||||
function DebugReportingService:UpdateHeartbeatObject(additionalArgs)
|
||||
if additionalArgs and not isTableEqual(additionalArgs, {}) then
|
||||
assert(isTableEqual(additionalArgs, testArgs), "unexpected value for additionalArgs")
|
||||
end
|
||||
end
|
||||
|
||||
return DebugReportingService
|
||||
end
|
||||
|
||||
|
||||
describe("new()", function()
|
||||
it("should construct with a Reporting Service", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(es).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should not allow construction without a Reporting Service", function()
|
||||
expect(function()
|
||||
EventStream.new(nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("setEnabled()", function()
|
||||
it("should succeed with valid input", function()
|
||||
local reporter = EventStream.new(createDebugReportingService())
|
||||
reporter:setEnabled(false)
|
||||
reporter:setEnabled(true)
|
||||
end)
|
||||
it("should disable the reporter", function()
|
||||
local reporter = EventStream.new(createDebugReportingService())
|
||||
reporter:setEnabled(false)
|
||||
expect(function()
|
||||
reporter:updateHeartbeatObject()
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("setRBXEvent()", function()
|
||||
it("should succeed with valid input", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:setRBXEvent(testContext, testEvent, testArgs)
|
||||
end)
|
||||
|
||||
it("should work when appropriately enabled / disabled", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
|
||||
expect(function()
|
||||
es:setEnabled(false)
|
||||
es:setRBXEvent(testContext, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
|
||||
es:setEnabled(true)
|
||||
es:setRBXEvent(testContext, testEvent, testArgs)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a context", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEvent(nil, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing an event name", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEvent(testContext, nil, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should succeed even if there aren't any additional args", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:setRBXEvent(testContext, testEvent, nil)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for a context", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEvent(badTestContext, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for a event", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEvent(testContext, badTestEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for additionalArgs", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEvent(testContext, testEvent, badTestArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("setRBXEventStream()", function()
|
||||
it("should succeed with valid input", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:setRBXEventStream(testContext, testEvent, testArgs)
|
||||
end)
|
||||
|
||||
it("should work when appropriately enabled / disabled", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
|
||||
expect(function()
|
||||
es:setEnabled(false)
|
||||
es:setRBXEventStream(testContext, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
|
||||
es:setEnabled(true)
|
||||
es:setRBXEventStream(testContext, testEvent, testArgs)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a context", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEventStream(nil, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing an event name", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEventStream(testContext, nil, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should succeed even if there aren't any additional args", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:setRBXEventStream(testContext, testEvent, nil)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for a context", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEventStream(badTestContext, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for a event", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEventStream(testContext, badTestEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for additionalArgs", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEventStream(testContext, testEvent, badTestArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("sendEventImmediately()", function()
|
||||
it("should succeed with valid input", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:sendEventImmediately(testContext, testEvent, testArgs)
|
||||
end)
|
||||
|
||||
it("should call without a fuss when enabled and throw when disabled", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
|
||||
expect(function()
|
||||
es:setEnabled(false)
|
||||
es:sendEventImmediately(testContext, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
|
||||
es:setEnabled(true)
|
||||
es:sendEventImmediately(testContext, testEvent, testArgs)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a context", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventImmediately(nil, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing an event name", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventImmediately(testContext, nil, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should succeed even if there aren't any additional args", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:sendEventImmediately(testContext, testEvent, nil)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for a context", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventImmediately(badTestContext, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for a event", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventImmediately(testContext, badTestEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for additionalArgs", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventImmediately(testContext, testEvent, badTestArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("sendEventDeferred()", function()
|
||||
it("should succeed with valid input", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:sendEventDeferred(testContext, testEvent, testArgs)
|
||||
end)
|
||||
|
||||
it("should call without a fuss when enabled and throw when disabled", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
|
||||
expect(function()
|
||||
es:setEnabled(false)
|
||||
es:sendEventDeferred(testContext, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
|
||||
es:setEnabled(true)
|
||||
es:sendEventDeferred(testContext, testEvent, testArgs)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a context", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventDeferred(nil, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing an event name", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventDeferred(testContext, nil, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should succeed even if there aren't any additional args", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:sendEventDeferred(testContext, testEvent, nil)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for a context", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventDeferred(badTestContext, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for a event", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventDeferred(testContext, badTestEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for additionalArgs", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventDeferred(testContext, testEvent, badTestArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("releaseRBXEventStream()", function()
|
||||
it("should succeed with valid input", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:releaseRBXEventStream()
|
||||
end)
|
||||
|
||||
it("should throw when disabled and succeed when enabled", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
|
||||
expect(function()
|
||||
es:setEnabled(false)
|
||||
es:releaseRBXEventStream()
|
||||
end).to.throw()
|
||||
|
||||
es:setEnabled(true)
|
||||
es:releaseRBXEventStream()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("updateHeartbeatObject()", function()
|
||||
it("should work when appropriately enabled / disabled", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
|
||||
expect(function()
|
||||
es:setEnabled(false)
|
||||
es:updateHeartbeatObject(testArgs)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
es:setEnabled(true)
|
||||
es:updateHeartbeatObject(testArgs)
|
||||
end).never.to.throw()
|
||||
end)
|
||||
|
||||
it("should succeed with valid input", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:updateHeartbeatObject(testArgs)
|
||||
end)
|
||||
|
||||
it("should succeed even if there aren't any additional args", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:updateHeartbeatObject(nil)
|
||||
end)
|
||||
|
||||
it("should throw an error with invalid input", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:updateHeartbeatObject(badTestArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
--[[
|
||||
Specialized reporter for sending data to GA.
|
||||
Useful for creating a breadcrumb trail of user interactions.
|
||||
|
||||
Events in GA are aggregated and organized in order by category, action, label.
|
||||
]]
|
||||
|
||||
local GoogleAnalytics = {}
|
||||
GoogleAnalytics.__index = GoogleAnalytics
|
||||
|
||||
-- reportingService : (table or userdata) any object that defines the same functions for GA as AnalyticsService
|
||||
function GoogleAnalytics.new(reportingService)
|
||||
local rsType = type(reportingService)
|
||||
assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService")
|
||||
|
||||
local self = {
|
||||
_reporter = reportingService,
|
||||
_isEnabled = true,
|
||||
}
|
||||
setmetatable(self, GoogleAnalytics)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-- isEnabled : (boolean)
|
||||
function GoogleAnalytics:setEnabled(isEnabled)
|
||||
assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean")
|
||||
self._isEnabled = isEnabled
|
||||
end
|
||||
|
||||
-- category : (string) the most generic category by which to organize data, ex) LuaApp, Errors, GameSettings, etc.
|
||||
-- action : (string) a specific event to record, ex) ButtonPressed, GameExit
|
||||
-- label : (string, optional) a detail to differentiate one action over another, ex) LoginButton, Exit Code 0
|
||||
-- value : (integer, optional) the number of times this event has occurred
|
||||
function GoogleAnalytics:trackEvent(category, action, label, value)
|
||||
assert(type(category) == "string", "Expected category to be a string")
|
||||
assert(type(action) == "string", "Expected action to be a string")
|
||||
if label then
|
||||
assert(type(label) == "string", "Expected label to be a string")
|
||||
end
|
||||
if value then
|
||||
assert(type(value) == "number", "Expected value to be a number")
|
||||
assert(value >= 0, "Expected value must not be a negative value")
|
||||
end
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
self._reporter:TrackEvent(category, action, label, value)
|
||||
end
|
||||
|
||||
|
||||
return GoogleAnalytics
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
return function()
|
||||
local GoogleAnalytics = require(script.Parent.GoogleAnalytics)
|
||||
|
||||
local testCategory = "testCategory"
|
||||
local testAction = "testAction"
|
||||
local testLabel = "testLabel"
|
||||
local testValue = 6
|
||||
local badTestCategory = 13141
|
||||
local badTestAction = {}
|
||||
local badTestLabel = {}
|
||||
local badTestValue = "heyo"
|
||||
|
||||
|
||||
local DebugReportingService = {}
|
||||
function DebugReportingService:TrackEvent(category, action, label, value)
|
||||
if category ~= testCategory then
|
||||
error("unexpected value for category: " .. category)
|
||||
end
|
||||
if action ~= testAction then
|
||||
error("unexpected value for action: " .. action)
|
||||
end
|
||||
if label then
|
||||
if label ~= testLabel then
|
||||
error("unexpected value for label: " .. label)
|
||||
end
|
||||
end
|
||||
if value then
|
||||
if value ~= testValue then
|
||||
error("unexpected value for value: " .. value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe("new()", function()
|
||||
it("should construct with a Reporting Service and Logging Service", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
expect(ga).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should fail to be constructed without a Reporting Service", function()
|
||||
expect(function()
|
||||
GoogleAnalytics.new(nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("setEnabled()", function()
|
||||
it("should succeed with valid input", function()
|
||||
local reporter = GoogleAnalytics.new(DebugReportingService)
|
||||
reporter:setEnabled(false)
|
||||
reporter:setEnabled(true)
|
||||
end)
|
||||
it("should disable the reporter", function()
|
||||
local reporter = GoogleAnalytics.new(DebugReportingService)
|
||||
reporter:setEnabled(false)
|
||||
expect(function()
|
||||
reporter:trackEvent(testCategory, testAction, testLabel, testValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("trackEvent()", function()
|
||||
it("should work when appropriately enabled / disabled", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
|
||||
expect(function()
|
||||
ga:setEnabled(false)
|
||||
ga:trackEvent(testCategory, testAction, testLabel)
|
||||
end).to.throw()
|
||||
|
||||
ga:setEnabled(true)
|
||||
ga:trackEvent(testCategory, testAction, testLabel)
|
||||
end)
|
||||
|
||||
it("should succeed with valid input", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
ga:trackEvent(testCategory, testAction, testLabel, testValue)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a category", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
expect(function()
|
||||
ga:trackEvent(nil, testAction, testLabel, testValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a testAction", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
expect(function()
|
||||
ga:trackEvent(testCategory, nil, testLabel, testValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should not throw an error if it is missing a label", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
ga:trackEvent(testCategory, testAction, nil, testValue)
|
||||
end)
|
||||
|
||||
it("should not throw an error if it is missing a value", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
ga:trackEvent(testCategory, testAction, testLabel)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given invalid input for category", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
expect(function()
|
||||
ga:trackEvent(badTestCategory, testAction, testLabel, testValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given invalid input for action", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
expect(function()
|
||||
ga:trackEvent(testCategory, badTestAction, testLabel, testValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given invalid input for label", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
expect(function()
|
||||
ga:trackEvent(testCategory, testAction, badTestLabel, testValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given invalid input for value", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
expect(function()
|
||||
ga:trackEvent(testCategory, testAction, testLabel, badTestValue)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
ga:trackEvent(testCategory, testAction, testLabel, -1)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
--[[
|
||||
Specialized reporter for sending data to InfluxDb.
|
||||
Useful for very detailed information about specific errors.
|
||||
|
||||
Due to how Influx sends data, it is disallowed on XBox.
|
||||
~Kyler Mulherin (9/12/2017)
|
||||
]]
|
||||
|
||||
local Influx = {}
|
||||
Influx.__index = Influx
|
||||
|
||||
-- reportingService - (object) any object that defines the same functions for Influx as AnalyticsService
|
||||
function Influx.new(reportingService)
|
||||
local rsType = type(reportingService)
|
||||
assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService")
|
||||
|
||||
local self = {
|
||||
_reporter = reportingService,
|
||||
_isEnabled = true,
|
||||
}
|
||||
setmetatable(self, Influx)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-- isEnabled : (boolean)
|
||||
function Influx:setEnabled(isEnabled)
|
||||
assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean")
|
||||
self._isEnabled = isEnabled
|
||||
end
|
||||
|
||||
-- seriesName : (string) the name of the series as it will appear in InfluxDb
|
||||
-- additionalArgs : (map<string, string>) extra key/values to appear in each series
|
||||
-- throttlingPercent : (int) the chance to actually report this series
|
||||
function Influx:reportSeries(seriesName, additionalArgs, throttlingPercent)
|
||||
additionalArgs = additionalArgs or {}
|
||||
|
||||
assert(type(seriesName) == "string", "Expected seriesName to be a string")
|
||||
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
|
||||
assert(type(throttlingPercent) == "number", "Expected throttlingPercent to be a number")
|
||||
assert(throttlingPercent >= 0 and throttlingPercent <= 10000, "throttlingPercent must be between 0 - 10,000")
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
self._reporter:ReportInfluxSeries(seriesName, additionalArgs, throttlingPercent)
|
||||
end
|
||||
|
||||
return Influx
|
||||
@@ -0,0 +1,165 @@
|
||||
return function()
|
||||
local Influx = require(script.Parent.Influx)
|
||||
|
||||
local testSeriesName = "testSeries"
|
||||
local testArgs = {
|
||||
testKey = "testValue"
|
||||
}
|
||||
local testThrottlingPercentage = 1000
|
||||
|
||||
local badTestSeriesName = 114
|
||||
local badTestArgs = "someString"
|
||||
local badThrottlingPercentage1 = -15
|
||||
local badThrottlingPercentage2 = 150000
|
||||
local badThrottlingPercentage3 = "a lot"
|
||||
|
||||
|
||||
local function isTableEqual(table1, table2)
|
||||
if table1 == table2 then
|
||||
return true
|
||||
end
|
||||
|
||||
if type(table2) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
for key, _ in pairs(table1) do
|
||||
if table1[key] ~= table2[key] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
for key, _ in pairs(table2) do
|
||||
if table2[key] ~= table1[key] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
local DebugReportingService = {}
|
||||
function DebugReportingService:ReportInfluxSeries(seriesName, additionalArgs, throttlingPercentage)
|
||||
if seriesName ~= seriesName then
|
||||
error("Unexpected value for seriesName: " .. seriesName)
|
||||
end
|
||||
if throttlingPercentage ~= testThrottlingPercentage then
|
||||
error("Unexpected value for throttlingPercentage: " .. throttlingPercentage)
|
||||
end
|
||||
if isTableEqual(additionalArgs, {}) == false then
|
||||
if isTableEqual(additionalArgs, testArgs) == false then
|
||||
error("Unexpected value for additionalArgs")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe("new()", function()
|
||||
it("should construct with a Reporting Service", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
expect(influx).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should fail to be constructed without a Reporting Service", function()
|
||||
expect(function()
|
||||
Influx.new(nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("setEnabled()", function()
|
||||
it("should succeed with valid input", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
influx:setEnabled(false)
|
||||
influx:setEnabled(true)
|
||||
end)
|
||||
it("should disable the reporter", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
influx:setEnabled(false)
|
||||
expect(function()
|
||||
influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("reportSeries()", function()
|
||||
it("should work when appropriately enabled / disabled", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
|
||||
expect(function()
|
||||
influx:setEnabled(false)
|
||||
influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage)
|
||||
end).to.throw()
|
||||
|
||||
|
||||
influx:setEnabled(true)
|
||||
influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage)
|
||||
end)
|
||||
|
||||
it("should succeed with valid input", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage)
|
||||
end)
|
||||
|
||||
it("should succeed even if it is missing any additionalArgs", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
influx:reportSeries(testSeriesName, nil, testThrottlingPercentage)
|
||||
end)
|
||||
|
||||
it("should throw an error with invalid input for the seriesName", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
expect(function()
|
||||
influx:reportSeries(badTestSeriesName, testArgs, testThrottlingPercentage)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error with invalid input for the throttlingPercentage - out of range - below zero", function()
|
||||
expect(function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage1)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error with invalid input for the throttlingPercentage - out of range - above cap", function()
|
||||
expect(function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage2)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error with invalid input for the throttlingPercentage - bad type", function()
|
||||
expect(function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage3)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error with completely invalid input", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
expect(function()
|
||||
influx:reportSeries(badTestSeriesName, badTestArgs, badThrottlingPercentage1)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a seriesName", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
expect(function()
|
||||
influx:reportSeries(nil, testArgs, testThrottlingPercentage)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a throttlingPercentage", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
expect(function()
|
||||
influx:reportSeries(testSeriesName, testArgs, nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing any input", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
expect(function()
|
||||
influx:reportSeries(nil, nil, nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"lint": {
|
||||
"LocalUnused": "fatal",
|
||||
"ImportUnused": "fatal",
|
||||
"DeprecatedGlobal": "fatal"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"language": {
|
||||
"mode": "nonstrict"
|
||||
},
|
||||
"lint": {
|
||||
"LocalShadow": "fatal",
|
||||
"ImplicitReturn": "fatal"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
-----------------------------------------------------------------------------
|
||||
--- ---
|
||||
--- Under Migration to CorePackages ---
|
||||
--- ---
|
||||
--- Please put your changes in Analytics. ---
|
||||
-----------------------------------------------------------------------------
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
return require(CorePackages.Analytics.AnalyticsReporters.Diag)
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"language": {
|
||||
"mode": "nonstrict"
|
||||
},
|
||||
"lint": {
|
||||
"ImplicitReturn": "fatal"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
--[[
|
||||
A helper function to define a Rodux action creator with an associated name.
|
||||
|
||||
Normally when creating a Rodux action, you can just create a function:
|
||||
|
||||
return function(value)
|
||||
return {
|
||||
type = "MyAction",
|
||||
value = value,
|
||||
}
|
||||
end
|
||||
|
||||
And then when you check for it in your reducer, you either use a constant,
|
||||
or type out the string name:
|
||||
|
||||
if action.type == "MyAction" then
|
||||
-- change some state
|
||||
end
|
||||
|
||||
Typos here are a remarkably common bug. We also have the issue that there's
|
||||
no link between reducers and the actions that they respond to!
|
||||
|
||||
`Action` (this helper) provides a utility that makes this a bit cleaner.
|
||||
|
||||
Instead, define your Rodux action like this:
|
||||
|
||||
return Action("MyAction", function(value)
|
||||
return {
|
||||
value = value,
|
||||
}
|
||||
end)
|
||||
|
||||
We no longer need to add the `type` field manually.
|
||||
|
||||
Additionally, the returned action creator now has a 'name' property that can
|
||||
be checked by your reducer:
|
||||
|
||||
local MyAction = require(Reducers.MyAction)
|
||||
|
||||
...
|
||||
|
||||
if action.type == MyAction.name then
|
||||
-- change some state!
|
||||
end
|
||||
|
||||
Now we have a clear link between our reducers and the actions they use, and
|
||||
if we ever typo a name, we'll get a warning in LuaCheck as well as an error
|
||||
at runtime!
|
||||
]]
|
||||
|
||||
return function(name, fn)
|
||||
assert(type(name) == "string", "A name must be provided to create an Action")
|
||||
assert(type(fn) == "function", "A function must be provided to create an Action")
|
||||
|
||||
return setmetatable({
|
||||
name = name,
|
||||
}, {
|
||||
__call = function(self, ...)
|
||||
local result = fn(...)
|
||||
|
||||
assert(type(result) == "table", "An action must return a table")
|
||||
|
||||
result.type = name
|
||||
|
||||
return result
|
||||
end
|
||||
})
|
||||
end
|
||||
@@ -0,0 +1,85 @@
|
||||
return function()
|
||||
local Action = require(script.Parent.Action)
|
||||
|
||||
it("should return a table", function()
|
||||
local action = Action("foo", function()
|
||||
return {}
|
||||
end)
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should set the name of the action", function()
|
||||
local action = Action("foo", function()
|
||||
return {}
|
||||
end)
|
||||
|
||||
expect(action.name).to.equal("foo")
|
||||
end)
|
||||
|
||||
it("should be able to be called as a function", function()
|
||||
local action = Action("foo", function()
|
||||
return {}
|
||||
end)
|
||||
|
||||
expect(action).never.to.throw()
|
||||
end)
|
||||
|
||||
it("should return a table when called as a function", function()
|
||||
local action = Action("foo", function()
|
||||
return {}
|
||||
end)
|
||||
|
||||
expect(action()).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should set the type of the action", function()
|
||||
local action = Action("foo", function()
|
||||
return {}
|
||||
end)
|
||||
|
||||
expect(action().type).to.equal("foo")
|
||||
end)
|
||||
|
||||
it("should set values", function()
|
||||
local action = Action("foo", function(value)
|
||||
return {
|
||||
value = value
|
||||
}
|
||||
end)
|
||||
|
||||
expect(action(100).value).to.equal(100)
|
||||
end)
|
||||
|
||||
it("should throw when passed a function", function()
|
||||
local action = Action("foo", function()
|
||||
return function() end
|
||||
end)
|
||||
|
||||
expect(action).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw with a invalid name", function()
|
||||
expect(function()
|
||||
Action(nil, function()
|
||||
return {}
|
||||
end)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
Action(100, function()
|
||||
return {}
|
||||
end)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw when passed a invalid function", function()
|
||||
expect(function()
|
||||
Action("foo", nil)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
Action("foo", {})
|
||||
end).to.throw()
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
local Color = {}
|
||||
|
||||
function Color.RgbFromHex(hexColor)
|
||||
assert(hexColor >= 0 and hexColor <= 0xffffff, "RgbFromHex: Out of range")
|
||||
|
||||
local b = hexColor % 256
|
||||
hexColor = (hexColor - b) / 256
|
||||
local g = hexColor % 256
|
||||
hexColor = (hexColor - g) / 256
|
||||
local r = hexColor
|
||||
|
||||
return r, g, b
|
||||
end
|
||||
|
||||
function Color.Color3FromHex(hexColor)
|
||||
return Color3.fromRGB(Color.RgbFromHex(hexColor))
|
||||
end
|
||||
|
||||
return Color
|
||||
@@ -0,0 +1,33 @@
|
||||
return function()
|
||||
local Color = require(script.Parent.Color)
|
||||
|
||||
describe("RgbFromHex", function()
|
||||
it("should convert a hex color to rgb correctly", function()
|
||||
local r, g, b = Color.RgbFromHex(0x232527)
|
||||
expect(r).to.equal(35)
|
||||
expect(g).to.equal(37)
|
||||
expect(b).to.equal(39)
|
||||
|
||||
r, g, b = Color.RgbFromHex(0x0)
|
||||
expect(r).to.equal(0)
|
||||
expect(g).to.equal(0)
|
||||
expect(b).to.equal(0)
|
||||
|
||||
r, g, b = Color.RgbFromHex(0xffffff)
|
||||
expect(r).to.equal(255)
|
||||
expect(g).to.equal(255)
|
||||
expect(b).to.equal(255)
|
||||
end)
|
||||
|
||||
|
||||
it("should assert if given a hex color out of range", function()
|
||||
expect(function()
|
||||
Color.RgbFromHex(-1)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
Color.RgbFromHex(0x1000000)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,131 @@
|
||||
--[[
|
||||
Provides an implementation of functional programming primitives.
|
||||
]]
|
||||
|
||||
local Functional = {}
|
||||
|
||||
--[[
|
||||
Create a copy of a list with only values for which `callback` returns true
|
||||
]]
|
||||
function Functional.Filter(list, callback)
|
||||
local new = {}
|
||||
|
||||
for key = 1, #list do
|
||||
local value = list[key]
|
||||
if callback(value, key) then
|
||||
table.insert(new, value)
|
||||
end
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
Create a copy of a list where each value is transformed by `callback`
|
||||
]]
|
||||
function Functional.Map(list, callback)
|
||||
local new = {}
|
||||
|
||||
for key = 1, #list do
|
||||
new[key] = callback(list[key], key)
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
Identical to Map, except that the result will be reversed.
|
||||
]]
|
||||
function Functional.MapReverse(list, callback)
|
||||
local new = {}
|
||||
|
||||
for key = #list, 1, -1 do
|
||||
new[key] = callback(list[key], key)
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
Create a copy of a list doing a combination filter and map.
|
||||
|
||||
If callback returns nil for any item, it is considered filtered from the
|
||||
list. Any other value is considered the result of the 'map' operation.
|
||||
]]
|
||||
function Functional.FilterMap(list, callback)
|
||||
local new = {}
|
||||
|
||||
for key = 1, #list do
|
||||
local value = list[key]
|
||||
local result = callback(value, key)
|
||||
|
||||
if result ~= nil then
|
||||
table.insert(new, result)
|
||||
end
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
Performs a left-fold of the list with the given initial value and callback.
|
||||
]]
|
||||
function Functional.Fold(list, initial, callback)
|
||||
local accum = initial
|
||||
|
||||
for key = 1, #list do
|
||||
accum = callback(accum, list[key], key)
|
||||
end
|
||||
|
||||
return accum
|
||||
end
|
||||
|
||||
--[[
|
||||
Performs a fold over the entries in the given dictionary.
|
||||
]]
|
||||
function Functional.FoldDictionary(dictionary, initial, callback)
|
||||
local accum = initial
|
||||
|
||||
for key, value in pairs(dictionary) do
|
||||
accum = callback(accum, key, value)
|
||||
end
|
||||
|
||||
return accum
|
||||
end
|
||||
|
||||
--[[
|
||||
Returns a list that contains at most `count` values from the given list.
|
||||
]]
|
||||
function Functional.Take(list, count, startingIndex)
|
||||
startingIndex = startingIndex or 1
|
||||
|
||||
local maxIndex = count + (startingIndex - 1)
|
||||
if maxIndex > #list then
|
||||
maxIndex = #list
|
||||
end
|
||||
|
||||
local new = {}
|
||||
|
||||
for i = startingIndex, maxIndex do
|
||||
local value = list[i]
|
||||
local newIndex = i - (startingIndex - 1)
|
||||
new[newIndex] = value
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
If the list contains the sought-after element, return its index, or nil otherwise.
|
||||
]]
|
||||
function Functional.Find(list, value)
|
||||
for index, element in ipairs(list) do
|
||||
if element == value then
|
||||
return index
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
return Functional
|
||||
@@ -0,0 +1,218 @@
|
||||
return function()
|
||||
local Functional = require(script.Parent.Functional)
|
||||
|
||||
local function identity(...)
|
||||
return ...
|
||||
end
|
||||
|
||||
local function add(a, b)
|
||||
return a + b
|
||||
end
|
||||
|
||||
describe("Filter", function()
|
||||
it("should copy lists correctly", function()
|
||||
local listA = {1, 2, 3}
|
||||
local listB = Functional.Filter(listA, function()
|
||||
return true
|
||||
end)
|
||||
|
||||
expect(listB).never.to.equal(listA)
|
||||
|
||||
for i = 1, #listB do
|
||||
expect(listB[i]).to.equal(listA[i])
|
||||
end
|
||||
end)
|
||||
|
||||
it("should correctly use the filter predicate", function()
|
||||
local listA = {1, 2, 3, 4, 5}
|
||||
local listB = Functional.Filter(listA, function(value, key)
|
||||
expect(value).to.equal(key)
|
||||
|
||||
return value % 2 == 0
|
||||
end)
|
||||
|
||||
expect(listB[1]).to.equal(2)
|
||||
expect(listB[2]).to.equal(4)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Map", function()
|
||||
it("should copy lists correctly using the identity function", function()
|
||||
local listA = {1, 2, 3}
|
||||
local listB = Functional.Map(listA, identity)
|
||||
|
||||
expect(listB).never.to.equal(listA)
|
||||
|
||||
for i = 1, #listB do
|
||||
expect(listB[i]).to.equal(listA[i])
|
||||
end
|
||||
end)
|
||||
|
||||
it("should correctly use the map predicate", function()
|
||||
local listA = {1, 2, 3}
|
||||
local listB = Functional.Map(listA, function(value, key)
|
||||
expect(value).to.equal(key)
|
||||
|
||||
return value * 2
|
||||
end)
|
||||
|
||||
for i = 1, #listB do
|
||||
expect(listB[i]).to.equal(listA[i] * 2)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("MapReverse", function()
|
||||
it("should copy lists correctly using the identity function", function()
|
||||
local listA = {1, 2, 3}
|
||||
local listB = Functional.MapReverse(listA, identity)
|
||||
|
||||
expect(listB).never.to.equal(listA)
|
||||
|
||||
for i = 1, #listB do
|
||||
expect(listB[i]).to.equal(listA[i])
|
||||
end
|
||||
end)
|
||||
|
||||
it("should correctly use the map predicate", function()
|
||||
local listA = {1, 2, 3}
|
||||
local listB = Functional.MapReverse(listA, function(value, key)
|
||||
expect(value).to.equal(key)
|
||||
|
||||
return value * 2
|
||||
end)
|
||||
|
||||
for i = 1, #listB do
|
||||
expect(listB[i]).to.equal(listA[i] * 2)
|
||||
end
|
||||
end)
|
||||
|
||||
it("should iterate backwards", function()
|
||||
local list = {1, 2, 3}
|
||||
local nextKey = 3
|
||||
|
||||
Functional.MapReverse(list, function(value, key)
|
||||
expect(value).to.equal(nextKey)
|
||||
expect(key).to.equal(nextKey)
|
||||
|
||||
nextKey = nextKey - 1
|
||||
end)
|
||||
|
||||
expect(nextKey).to.equal(0)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("FilterMap", function()
|
||||
it("should copy truthy lists using the identity function", function()
|
||||
local listA = {1, 2, 3}
|
||||
local listB = Functional.FilterMap(listA, identity)
|
||||
|
||||
expect(listB).never.to.equal(listA)
|
||||
|
||||
for i = 1, #listB do
|
||||
expect(listB[i]).to.equal(listA[i])
|
||||
end
|
||||
end)
|
||||
|
||||
it("should correctly use the filter-map predicate", function()
|
||||
local listA = {1, 2, 3, 4, 5}
|
||||
|
||||
-- Create a list containing only the odd numbers, and double those numbers
|
||||
local listB = Functional.FilterMap(listA, function(value, key)
|
||||
expect(value).to.equal(key)
|
||||
|
||||
if value % 2 == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
return value * 2
|
||||
end)
|
||||
|
||||
expect(listB[1]).to.equal(2)
|
||||
expect(listB[2]).to.equal(6)
|
||||
expect(listB[3]).to.equal(10)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Fold", function()
|
||||
it("should left-fold lists", function()
|
||||
local list = {1, 2, 3, 4, 5}
|
||||
|
||||
local sum = Functional.Fold(list, 0, add)
|
||||
|
||||
expect(sum).to.equal(15)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Take", function()
|
||||
it("should take values from a list", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = Functional.Take(a, 2)
|
||||
|
||||
expect(#b).to.equal(2)
|
||||
expect(b[1]).to.equal(1)
|
||||
expect(b[2]).to.equal(2)
|
||||
end)
|
||||
|
||||
it("should not take past the end of a list", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = Functional.Take(a, 4)
|
||||
|
||||
expect(#b).to.equal(3)
|
||||
expect(b[1]).to.equal(1)
|
||||
expect(b[2]).to.equal(2)
|
||||
expect(b[3]).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should copy all values when taking past the end of a list", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = Functional.Take(a, 4)
|
||||
|
||||
expect(#b).to.equal(#a)
|
||||
expect(a[1]).to.equal(b[1])
|
||||
expect(a[2]).to.equal(b[2])
|
||||
expect(a[3]).to.equal(b[3])
|
||||
end)
|
||||
|
||||
it("should take values from a starting index when provided", function()
|
||||
local a = {1, 2, 3, 4}
|
||||
local b = Functional.Take(a, 2, 2)
|
||||
|
||||
expect(#b).to.equal(2)
|
||||
expect(b[1]).to.equal(2)
|
||||
expect(b[2]).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should not take past the end of a list when the starting index is provided", function()
|
||||
local a = {1, 2, 3, 4}
|
||||
local b = Functional.Take(a, 3, 3)
|
||||
|
||||
expect(#b).to.equal(2)
|
||||
expect(b[1]).to.equal(3)
|
||||
expect(b[2]).to.equal(4)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Find", function()
|
||||
it("should return index of matched item", function()
|
||||
local a = {"foo", "bar", "garply"}
|
||||
local b = Functional.Find(a, "bar")
|
||||
|
||||
expect(b).to.equal(2)
|
||||
end)
|
||||
|
||||
it("should find the first example in the case of duplicates", function()
|
||||
local a = {"foo", "bar", "garply", "bar"}
|
||||
local b = Functional.Find(a, "bar")
|
||||
|
||||
expect(b).to.equal(2)
|
||||
end)
|
||||
|
||||
it("should return nil if item is not found", function()
|
||||
local a = {"foo", "bar", "garply"}
|
||||
local b = Functional.Find(a, "fleebledegoop")
|
||||
|
||||
expect(b).to.equal(nil)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,141 @@
|
||||
--[[
|
||||
Provides functions for manipulating immutable data structures.
|
||||
]]
|
||||
|
||||
local Immutable = {}
|
||||
|
||||
--[[
|
||||
Merges dictionary-like tables together.
|
||||
]]
|
||||
function Immutable.JoinDictionaries(...)
|
||||
local result = {}
|
||||
|
||||
for i = 1, select("#", ...) do
|
||||
local dictionary = select(i, ...)
|
||||
for key, value in pairs(dictionary) do
|
||||
result[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--[[
|
||||
Joins any number of lists together into a new list
|
||||
]]
|
||||
function Immutable.JoinLists(...)
|
||||
local new = {}
|
||||
|
||||
for listKey = 1, select("#", ...) do
|
||||
local list = select(listKey, ...)
|
||||
local len = #new
|
||||
|
||||
for itemKey = 1, #list do
|
||||
new[len + itemKey] = list[itemKey]
|
||||
end
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
Creates a new copy of the dictionary and sets a value inside it.
|
||||
]]
|
||||
function Immutable.Set(dictionary, key, value)
|
||||
local new = {}
|
||||
|
||||
for key, value in pairs(dictionary) do
|
||||
new[key] = value
|
||||
end
|
||||
|
||||
new[key] = value
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
Creates a new copy of the list with the given elements appended to it.
|
||||
]]
|
||||
function Immutable.Append(list, ...)
|
||||
local new = {}
|
||||
local len = #list
|
||||
|
||||
for key = 1, len do
|
||||
new[key] = list[key]
|
||||
end
|
||||
|
||||
for i = 1, select("#", ...) do
|
||||
new[len + i] = select(i, ...)
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
Remove elements from a dictionary
|
||||
]]
|
||||
function Immutable.RemoveFromDictionary(dictionary, ...)
|
||||
local result = {}
|
||||
|
||||
for key, value in pairs(dictionary) do
|
||||
local found = false
|
||||
for listKey = 1, select("#", ...) do
|
||||
if key == select(listKey, ...) then
|
||||
found = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not found then
|
||||
result[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--[[
|
||||
Remove the given key from the list.
|
||||
]]
|
||||
function Immutable.RemoveFromList(list, removeIndex)
|
||||
local new = {}
|
||||
|
||||
for i = 1, #list do
|
||||
if i ~= removeIndex then
|
||||
table.insert(new, list[i])
|
||||
end
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
Remove the range from the list starting from the index.
|
||||
]]
|
||||
function Immutable.RemoveRangeFromList(list, index, count)
|
||||
local new = {}
|
||||
|
||||
for i = 1, #list do
|
||||
if i < index or i >= index + count then
|
||||
table.insert(new, list[i])
|
||||
end
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
Creates a new list that has no occurrences of the given value.
|
||||
]]
|
||||
function Immutable.RemoveValueFromList(list, removeValue)
|
||||
local new = {}
|
||||
|
||||
for i = 1, #list do
|
||||
if list[i] ~= removeValue then
|
||||
table.insert(new, list[i])
|
||||
end
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
return Immutable
|
||||
@@ -0,0 +1,284 @@
|
||||
return function()
|
||||
local Immutable = require(script.Parent.Immutable)
|
||||
|
||||
describe("JoinDictionaries", function()
|
||||
it("should preserve immutability", function()
|
||||
local a = {}
|
||||
local b = {}
|
||||
|
||||
local c = Immutable.JoinDictionaries(a, b)
|
||||
|
||||
expect(c).never.to.equal(a)
|
||||
expect(c).never.to.equal(b)
|
||||
end)
|
||||
|
||||
it("should treat list-like values like dictionary values", function()
|
||||
local a = {
|
||||
[1] = 1,
|
||||
[2] = 2,
|
||||
[3] = 3
|
||||
}
|
||||
|
||||
local b = {
|
||||
[1] = 11,
|
||||
[2] = 22
|
||||
}
|
||||
|
||||
local c = Immutable.JoinDictionaries(a, b)
|
||||
|
||||
expect(c[1]).to.equal(b[1])
|
||||
expect(c[2]).to.equal(b[2])
|
||||
expect(c[3]).to.equal(a[3])
|
||||
end)
|
||||
|
||||
it("should merge dictionary values correctly", function()
|
||||
local a = {
|
||||
hello = "world",
|
||||
foo = "bar"
|
||||
}
|
||||
|
||||
local b = {
|
||||
foo = "baz",
|
||||
tux = "penguin"
|
||||
}
|
||||
|
||||
local c = Immutable.JoinDictionaries(a, b)
|
||||
|
||||
expect(c.hello).to.equal(a.hello)
|
||||
expect(c.foo).to.equal(b.foo)
|
||||
expect(c.tux).to.equal(b.tux)
|
||||
end)
|
||||
|
||||
it("should merge multiple dictionaries", function()
|
||||
local a = {
|
||||
foo = "yes"
|
||||
}
|
||||
|
||||
local b = {
|
||||
bar = "yup"
|
||||
}
|
||||
|
||||
local c = {
|
||||
baz = "sure"
|
||||
}
|
||||
|
||||
local d = Immutable.JoinDictionaries(a, b, c)
|
||||
|
||||
expect(d.foo).to.equal(a.foo)
|
||||
expect(d.bar).to.equal(b.bar)
|
||||
expect(d.baz).to.equal(c.baz)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("JoinLists", function()
|
||||
it("should preserve immutability", function()
|
||||
local a = {}
|
||||
local b = {}
|
||||
|
||||
local c = Immutable.JoinLists(a, b)
|
||||
|
||||
expect(c).never.to.equal(a)
|
||||
expect(c).never.to.equal(b)
|
||||
end)
|
||||
|
||||
it("should treat list-like values correctly", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = {4, 5, 6}
|
||||
|
||||
local c = Immutable.JoinLists(a, b)
|
||||
|
||||
expect(#c).to.equal(6)
|
||||
|
||||
for i = 1, #c do
|
||||
expect(c[i]).to.equal(i)
|
||||
end
|
||||
end)
|
||||
|
||||
it("should merge multiple lists", function()
|
||||
local a = {1, 2}
|
||||
local b = {3, 4}
|
||||
local c = {5, 6}
|
||||
|
||||
local d = Immutable.JoinLists(a, b, c)
|
||||
|
||||
expect(#d).to.equal(6)
|
||||
|
||||
for i = 1, #d do
|
||||
expect(d[i]).to.equal(i)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Set", function()
|
||||
it("should preserve immutability", function()
|
||||
local a = {}
|
||||
|
||||
local b = Immutable.Set(a, "foo", "bar")
|
||||
|
||||
expect(b).never.to.equal(a)
|
||||
end)
|
||||
|
||||
it("should treat numeric keys normally", function()
|
||||
local a = {1, 2, 3}
|
||||
|
||||
local b = Immutable.Set(a, 2, 4)
|
||||
|
||||
expect(b[1]).to.equal(1)
|
||||
expect(b[2]).to.equal(4)
|
||||
expect(b[3]).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should overwrite dictionary-like keys", function()
|
||||
local a = {
|
||||
foo = "bar",
|
||||
baz = "qux"
|
||||
}
|
||||
|
||||
local b = Immutable.Set(a, "foo", "hello there")
|
||||
|
||||
expect(b.foo).to.equal("hello there")
|
||||
expect(b.baz).to.equal(a.baz)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Append", function()
|
||||
it("should preserve immutability", function()
|
||||
local a = {}
|
||||
|
||||
local b = Immutable.Append(a, "another happy landing")
|
||||
|
||||
expect(b).never.to.equal(a)
|
||||
end)
|
||||
|
||||
it("should append values", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = Immutable.Append(a, 4, 5)
|
||||
|
||||
expect(#b).to.equal(5)
|
||||
|
||||
for i = 1, #b do
|
||||
expect(b[i]).to.equal(i)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("RemoveFromDictionary", function()
|
||||
it("should preserve immutability", function()
|
||||
local a = { foo = "bar" }
|
||||
|
||||
local b = Immutable.RemoveFromDictionary(a, "foo")
|
||||
|
||||
expect(b).to.never.equal(a)
|
||||
end)
|
||||
|
||||
it("should remove fields from the dictionary", function()
|
||||
local a = {
|
||||
foo = "bar",
|
||||
baz = "qux",
|
||||
boof = "garply",
|
||||
}
|
||||
|
||||
local b = Immutable.RemoveFromDictionary(a, "foo", "boof")
|
||||
|
||||
expect(b.foo).to.never.be.ok()
|
||||
expect(b.baz).to.equal("qux")
|
||||
expect(b.boof).to.never.be.ok()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("RemoveFromList", function()
|
||||
it("should preserve immutability", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = Immutable.RemoveFromList(a, 2)
|
||||
|
||||
expect(b).never.to.equal(a)
|
||||
end)
|
||||
|
||||
it("should remove elements from the list", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = Immutable.RemoveFromList(a, 2)
|
||||
|
||||
expect(b[1]).to.equal(1)
|
||||
expect(b[2]).to.equal(3)
|
||||
expect(b[3]).never.to.be.ok()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("RemoveRangeFromList", function()
|
||||
it("should preserve immutability", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = Immutable.RemoveRangeFromList(a, 2, 1)
|
||||
|
||||
expect(b).never.to.equal(a)
|
||||
end)
|
||||
|
||||
it("should remove elements properly from the list 1", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = Immutable.RemoveRangeFromList(a, 2, 1)
|
||||
|
||||
expect(b[1]).to.equal(1)
|
||||
expect(b[2]).to.equal(3)
|
||||
expect(b[3]).never.to.be.ok()
|
||||
end)
|
||||
|
||||
it("should remove elements properly from the list 2", function()
|
||||
local a = {1, 2, 3, 4, 5, 6}
|
||||
local b = Immutable.RemoveRangeFromList(a, 1, 4)
|
||||
|
||||
expect(b[1]).to.equal(5)
|
||||
expect(b[2]).to.equal(6)
|
||||
expect(b[3]).never.to.be.ok()
|
||||
end)
|
||||
|
||||
it("should remove elements properly from the list 3", function()
|
||||
local a = {1, 2, 3, 4, 5, 6}
|
||||
local b = Immutable.RemoveRangeFromList(a, 2, 4)
|
||||
|
||||
expect(b[1]).to.equal(1)
|
||||
expect(b[2]).to.equal(6)
|
||||
expect(b[3]).never.to.be.ok()
|
||||
end)
|
||||
|
||||
it("should remove elements properly from the list 4", function()
|
||||
local a = {1, 2, 3, 4, 5, 6, 7}
|
||||
local b = Immutable.RemoveRangeFromList(a, 4, 4)
|
||||
|
||||
expect(b[1]).to.equal(1)
|
||||
expect(b[2]).to.equal(2)
|
||||
expect(b[3]).to.equal(3)
|
||||
expect(b[4]).never.to.be.ok()
|
||||
end)
|
||||
|
||||
it("should not remove any elements when count is 0 or less", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = Immutable.RemoveRangeFromList(a, 2, 0)
|
||||
|
||||
expect(b[1]).to.equal(1)
|
||||
expect(b[2]).to.equal(2)
|
||||
expect(b[3]).to.equal(3)
|
||||
|
||||
local c = Immutable.RemoveRangeFromList(a, 2, -1)
|
||||
expect(c[1]).to.equal(1)
|
||||
expect(c[2]).to.equal(2)
|
||||
expect(c[3]).to.equal(3)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("RemoveValueFromList", function()
|
||||
it("should preserve immutability", function()
|
||||
local a = {1, 1, 1}
|
||||
local b = Immutable.RemoveValueFromList(a, 1)
|
||||
|
||||
expect(b).never.to.equal(a)
|
||||
end)
|
||||
|
||||
it("should remove all elements from the list", function()
|
||||
local a = {1, 2, 2, 3}
|
||||
local b = Immutable.RemoveValueFromList(a, 2)
|
||||
|
||||
expect(b[1]).to.equal(1)
|
||||
expect(b[2]).to.equal(3)
|
||||
expect(b[3]).never.to.be.ok()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,75 @@
|
||||
--[[
|
||||
A limited, simple implementation of a Signal.
|
||||
|
||||
Handlers are fired in order, and (dis)connections are properly handled when
|
||||
executing an event.
|
||||
|
||||
Signal uses Immutable to avoid invalidating the 'Fire' loop iteration.
|
||||
]]
|
||||
|
||||
local Immutable = require(script.Parent.Immutable)
|
||||
|
||||
local Signal = {}
|
||||
|
||||
Signal.__index = Signal
|
||||
|
||||
function Signal.new()
|
||||
local self = {
|
||||
_listeners = {}
|
||||
}
|
||||
|
||||
setmetatable(self, Signal)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Signal:connect(callback)
|
||||
local listener = {
|
||||
callback = callback,
|
||||
isConnected = true,
|
||||
}
|
||||
self._listeners = Immutable.Append(self._listeners, listener)
|
||||
|
||||
local function disconnect()
|
||||
listener.isConnected = false
|
||||
self._listeners = Immutable.RemoveValueFromList(self._listeners, listener)
|
||||
end
|
||||
|
||||
return {
|
||||
Disconnect = function()
|
||||
warn(string.format(
|
||||
"Connection:Disconnect() has been deprecated, use Connection:disconnect()\n%s]",
|
||||
debug.traceback()
|
||||
))
|
||||
disconnect()
|
||||
end,
|
||||
disconnect = disconnect,
|
||||
}
|
||||
end
|
||||
|
||||
function Signal:fire(...)
|
||||
for _, listener in ipairs(self._listeners) do
|
||||
if listener.isConnected then
|
||||
listener.callback(...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Signal:Connect(...)
|
||||
warn(string.format(
|
||||
"Signal:Connect() has been deprecated, use Signal:connect()\n%s]",
|
||||
debug.traceback()
|
||||
))
|
||||
return self:connect(...)
|
||||
end
|
||||
|
||||
function Signal:Fire(...)
|
||||
warn(string.format(
|
||||
"Signal:Fire() has been deprecated, use Signal:fire()\n%s]",
|
||||
debug.traceback()
|
||||
))
|
||||
self:fire(...)
|
||||
end
|
||||
|
||||
|
||||
return Signal
|
||||
@@ -0,0 +1,114 @@
|
||||
return function()
|
||||
local Signal = require(script.Parent.Signal)
|
||||
|
||||
it("should construct from nothing", function()
|
||||
local signal = Signal.new()
|
||||
|
||||
expect(signal).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should fire connected callbacks", function()
|
||||
local callCount = 0
|
||||
local value1 = "Hello World"
|
||||
local value2 = 7
|
||||
|
||||
local callback = function(arg1, arg2)
|
||||
expect(arg1).to.equal(value1)
|
||||
expect(arg2).to.equal(value2)
|
||||
callCount = callCount + 1
|
||||
end
|
||||
|
||||
local signal = Signal.new()
|
||||
|
||||
local connection = signal:connect(callback)
|
||||
signal:fire(value1, value2)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
|
||||
connection:disconnect()
|
||||
signal:fire(value1, value2)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should disconnect handlers", function()
|
||||
local callback = function()
|
||||
error("Callback was called after disconnect!")
|
||||
end
|
||||
|
||||
local signal = Signal.new()
|
||||
|
||||
local connection = signal:connect(callback)
|
||||
connection:disconnect()
|
||||
|
||||
signal:fire()
|
||||
end)
|
||||
|
||||
it("should fire handlers in order", function()
|
||||
local signal = Signal.new()
|
||||
local x = 0
|
||||
local y = 0
|
||||
|
||||
local callback1 = function()
|
||||
expect(x).to.equal(0)
|
||||
expect(y).to.equal(0)
|
||||
x = x + 1
|
||||
end
|
||||
|
||||
local callback2 = function()
|
||||
expect(x).to.equal(1)
|
||||
expect(y).to.equal(0)
|
||||
y = y + 1
|
||||
end
|
||||
|
||||
signal:connect(callback1)
|
||||
signal:connect(callback2)
|
||||
signal:fire()
|
||||
|
||||
expect(x).to.equal(1)
|
||||
expect(y).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should continue firing despite mid-event disconnection", function()
|
||||
local signal = Signal.new()
|
||||
local countA = 0
|
||||
local countB = 0
|
||||
|
||||
local connectionA
|
||||
connectionA = signal:connect(function()
|
||||
connectionA:disconnect()
|
||||
countA = countA + 1
|
||||
end)
|
||||
|
||||
signal:connect(function()
|
||||
countB = countB + 1
|
||||
end)
|
||||
|
||||
signal:fire()
|
||||
|
||||
expect(countA).to.equal(1)
|
||||
expect(countB).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should skip listeners that were disconnected during event evaluation", function()
|
||||
local signal = Signal.new()
|
||||
local countA = 0
|
||||
local countB = 0
|
||||
|
||||
local connectionB
|
||||
|
||||
signal:connect(function()
|
||||
countA = countA + 1
|
||||
connectionB:disconnect()
|
||||
end)
|
||||
|
||||
connectionB = signal:connect(function()
|
||||
countB = countB + 1
|
||||
end)
|
||||
|
||||
signal:fire()
|
||||
|
||||
expect(countA).to.equal(1)
|
||||
expect(countB).to.equal(0)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,113 @@
|
||||
local EngineFeatureTextBoundsRoundUp = game:GetEngineFeature("TextBoundsRoundUp")
|
||||
|
||||
local TextService = game:GetService("TextService")
|
||||
|
||||
local Text = {}
|
||||
|
||||
-- FYI: Any number greater than 2^30 will make TextService:GetTextSize give invalid results
|
||||
local MAX_BOUND = 10000
|
||||
|
||||
-- Remove with EngineFeatureTextBoundsRoundUp
|
||||
Text._TEMP_PATCHED_PADDING = Vector2.new(0, 0)
|
||||
|
||||
if not EngineFeatureTextBoundsRoundUp then
|
||||
Text._TEMP_PATCHED_PADDING = Vector2.new(2, 2)
|
||||
end
|
||||
|
||||
-- Wrapper function for GetTextSize
|
||||
function Text.GetTextBounds(text, font, fontSize, bounds)
|
||||
return TextService:GetTextSize(text, fontSize, font, bounds) + Text._TEMP_PATCHED_PADDING
|
||||
end
|
||||
|
||||
function Text.GetTextWidth(text, font, fontSize)
|
||||
return Text.GetTextBounds(text, font, fontSize, Vector2.new(MAX_BOUND, MAX_BOUND)).X
|
||||
end
|
||||
|
||||
function Text.GetTextHeight(text, font, fontSize, widthCap)
|
||||
return Text.GetTextBounds(text, font, fontSize, Vector2.new(widthCap, MAX_BOUND)).Y
|
||||
end
|
||||
|
||||
-- TODO(CLIPLAYEREX-391): Kill these truncate functions once we have official support for text truncation
|
||||
function Text.Truncate(text, font, fontSize, widthInPixels, overflowMarker)
|
||||
overflowMarker = overflowMarker or ""
|
||||
|
||||
if Text.GetTextWidth(text, font, fontSize) > widthInPixels then
|
||||
-- A binary search may be more efficient
|
||||
local lastText = ""
|
||||
for _, stopIndex in utf8.graphemes(text) do
|
||||
local newText = string.sub(text, 1, stopIndex) .. overflowMarker
|
||||
if Text.GetTextWidth(newText, font, fontSize) > widthInPixels then
|
||||
return lastText
|
||||
end
|
||||
lastText = newText
|
||||
end
|
||||
else -- No truncation needed
|
||||
return text
|
||||
end
|
||||
|
||||
return ""
|
||||
end
|
||||
|
||||
function Text.TruncateTextLabel(textLabel, overflowMarker)
|
||||
textLabel.Text = Text.Truncate(textLabel.Text, textLabel.Font,
|
||||
textLabel.TextSize, textLabel.AbsoluteSize.X, overflowMarker)
|
||||
end
|
||||
|
||||
-- Remove whitespace from the beginning and end of the string
|
||||
function Text.Trim(str)
|
||||
if type(str) ~= "string" then
|
||||
error(string.format("Text.Trim called on non-string type %s.", type(str)), 2)
|
||||
end
|
||||
return (str:gsub("^%s*(.-)%s*$", "%1"))
|
||||
end
|
||||
|
||||
-- Remove whitespace from the end of the string
|
||||
function Text.RightTrim(str)
|
||||
if type(str) ~= "string" then
|
||||
error(string.format("Text.RightTrim called on non-string type %s.", type(str)), 2)
|
||||
end
|
||||
return (str:gsub("%s+$", ""))
|
||||
end
|
||||
|
||||
-- Remove whitespace from the beginning of the string
|
||||
function Text.LeftTrim(str)
|
||||
if type(str) ~= "string" then
|
||||
error(string.format("Text.LeftTrim called on non-string type %s.", type(str)), 2)
|
||||
end
|
||||
return (str:gsub("^%s+", ""))
|
||||
end
|
||||
|
||||
-- Replace multiple whitespace with one; remove leading and trailing whitespace
|
||||
function Text.SpaceNormalize(str)
|
||||
if type(str) ~= "string" then
|
||||
error(string.format("Text.SpaceNormalize called on non-string type %s.", type(str)), 2)
|
||||
end
|
||||
return (str:gsub("%s+", " "):gsub("^%s+" , ""):gsub("%s+$" , ""))
|
||||
end
|
||||
|
||||
-- Splits a string by the provided pattern into a table. The pattern is interpreted as plain text.
|
||||
function Text.Split(str, pattern)
|
||||
if type(str) ~= "string" then
|
||||
error(string.format("Text.Split called on non-string type %s.", type(str)), 2)
|
||||
elseif type(pattern) ~= "string" then
|
||||
error(string.format("Text.Split called with a pattern that is non-string type %s.", type(pattern)), 2)
|
||||
elseif pattern == "" then
|
||||
error("Text.Split called with an empty pattern.", 2)
|
||||
end
|
||||
|
||||
local result = {}
|
||||
local currentPosition = 1
|
||||
|
||||
while true do
|
||||
local patternStart, patternEnd = string.find(str, pattern, currentPosition, true)
|
||||
if not patternStart or not patternEnd then break end
|
||||
table.insert(result, string.sub(str, currentPosition, patternStart - 1))
|
||||
currentPosition = patternEnd + 1
|
||||
end
|
||||
|
||||
table.insert(result, string.sub(str, currentPosition, string.len(str)))
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
return Text
|
||||
@@ -0,0 +1,410 @@
|
||||
return function()
|
||||
local Text = require(script.Parent.Text)
|
||||
|
||||
describe("GetTextBounds", function()
|
||||
it("should return a bounds of padding width and font-size height when the string is empty", function()
|
||||
local bounds = Text.GetTextBounds("", Enum.Font.SourceSans, 18, Vector2.new(1000, 1000))
|
||||
expect(bounds.X).to.equal(Text._TEMP_PATCHED_PADDING.x)
|
||||
expect(bounds.Y).to.equal(18 + Text._TEMP_PATCHED_PADDING.y)
|
||||
end)
|
||||
it("should return the height and width of a string as one line with large bounds", function()
|
||||
local bounds = Text.GetTextBounds("One Two Three", Enum.Font.SourceSans, 18, Vector2.new(1000, 1000))
|
||||
expect(bounds.Y).to.equal(18 + Text._TEMP_PATCHED_PADDING.y)
|
||||
end)
|
||||
|
||||
it("should return the height of the string as multiple lines with short bounds", function()
|
||||
local bounds = Text.GetTextBounds("One Two Three Four", Enum.Font.SourceSans, 18, Vector2.new(32, 1000))
|
||||
expect(bounds.Y > 18).to.equal(true)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("GetTextHeight", function()
|
||||
it("should return height equal to font size when string is empty", function()
|
||||
local height = Text.GetTextHeight("", Enum.Font.SourceSans, 18, 0)
|
||||
expect(height).to.equal(18 + Text._TEMP_PATCHED_PADDING.y)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("GetTextWidth", function()
|
||||
it("should return width equal to 1 when string is empty", function()
|
||||
local width = Text.GetTextWidth("", Enum.Font.SourceSans, 18)
|
||||
expect(width).to.equal(Text._TEMP_PATCHED_PADDING.x)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Truncate", function()
|
||||
it("should return empty string", function()
|
||||
local emptyQuery = Text.Truncate("", Enum.Font.SourceSans, 18, 0, "...")
|
||||
expect(emptyQuery).to.be.a("string")
|
||||
expect(emptyQuery).to.equal("")
|
||||
end)
|
||||
|
||||
it("should return empty string for not empty box", function()
|
||||
local emptyQuery = Text.Truncate("", Enum.Font.SourceSans, 18, 50, "...")
|
||||
expect(emptyQuery).to.be.a("string")
|
||||
expect(emptyQuery).to.equal("")
|
||||
end)
|
||||
|
||||
it("should truncate with ...", function()
|
||||
local reallyLongQuery = Text.Truncate(
|
||||
"One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve", Enum.Font.SourceSans, 18, 100, "...")
|
||||
expect(reallyLongQuery).to.equal("One Two Thre...")
|
||||
end)
|
||||
|
||||
it("should truncate without a ...", function()
|
||||
local reallyLongQueryNoOverflowMarker = Text.Truncate(
|
||||
"One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve", Enum.Font.SourceSans, 18, 100)
|
||||
expect(reallyLongQueryNoOverflowMarker).to.equal("One Two Three ")
|
||||
end)
|
||||
|
||||
it("should not truncate", function()
|
||||
local shouldFitQuery = Text.Truncate("One Two", Enum.Font.SourceSans, 18, 100)
|
||||
expect(shouldFitQuery).to.equal("One Two")
|
||||
end)
|
||||
|
||||
it("should not truncate, off by one check", function()
|
||||
local oneCharQuery = Text.Truncate("O", Enum.Font.SourceSans, 18, 100)
|
||||
expect(oneCharQuery).to.equal("O")
|
||||
end)
|
||||
|
||||
it("should truncate, off by one check", function()
|
||||
local oneCharNoRoomQuery = Text.Truncate("O", Enum.Font.SourceSans, 18, 0)
|
||||
expect(oneCharNoRoomQuery).to.equal("")
|
||||
end)
|
||||
|
||||
it("should perform a negative width check", function()
|
||||
local shouldFitQuery = Text.Truncate("One Two", Enum.Font.SourceSans, 18, -100, "...")
|
||||
expect(shouldFitQuery).to.equal("")
|
||||
end)
|
||||
|
||||
itFIXME("should truncate long graphemes properly", function()
|
||||
-- 11-byte rainbow flag grapheme
|
||||
-- Flag, zero-space-joiner, rainbow
|
||||
local rainbowFlag = utf8.char(127987) .. utf8.char(8205) .. utf8.char(127752)
|
||||
local oneFlagWithinLimit = Text.Truncate(
|
||||
rainbowFlag, Enum.Font.SourceSans, 18, 100, "...")
|
||||
expect(oneFlagWithinLimit).to.equal(rainbowFlag)
|
||||
|
||||
local twoRainbowFlags = rainbowFlag .. rainbowFlag
|
||||
local twoFlagsAreFine = Text.Truncate(
|
||||
twoRainbowFlags, Enum.Font.SourceSans, 18, 100, "...")
|
||||
expect(twoFlagsAreFine).to.equal(twoRainbowFlags)
|
||||
|
||||
local fourRainbowFlags = twoRainbowFlags .. twoRainbowFlags
|
||||
local fourFlagsIsTooLong = Text.Truncate(
|
||||
fourRainbowFlags, Enum.Font.SourceSans, 18, 100, "...")
|
||||
expect(fourFlagsIsTooLong).to.equal(twoRainbowFlags .. "...") -- With --fflags==true fails because of truncation
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("TruncateTextLabel", function()
|
||||
it("should use text label attributes to truncate text", function()
|
||||
local screenGui = Instance.new("ScreenGui")
|
||||
local textLabel = Instance.new("TextLabel")
|
||||
textLabel.Size = UDim2.new(0, 100, 0, 32)
|
||||
textLabel.Text = "One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve"
|
||||
textLabel.Font = Enum.Font.SourceSans
|
||||
textLabel.TextSize = 18
|
||||
textLabel.Parent = screenGui
|
||||
Text.TruncateTextLabel(textLabel)
|
||||
|
||||
expect(textLabel.Text).to.equal("One Two Three ")
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
describe("TrimString", function()
|
||||
it("Should trim the string properly 1", function()
|
||||
local trimmedInput = Text.Trim("")
|
||||
local expected = ""
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly 2", function()
|
||||
local trimmedInput = Text.Trim(" ")
|
||||
local expected = ""
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly 3", function()
|
||||
local trimmedInput = Text.Trim("ab")
|
||||
local expected = "ab"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly 4", function()
|
||||
local trimmedInput = Text.Trim(" ab ")
|
||||
local expected = "ab"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly 5", function()
|
||||
local trimmedInput = Text.Trim(" a b ")
|
||||
local expected = "a b"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly 6", function()
|
||||
local trimmedInput = Text.Trim("\r\n\t\f a\r\n\t\f ")
|
||||
local expected = "a"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string with unicode characters properly", function()
|
||||
local trimmedInput = Text.Trim("😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓")
|
||||
local expected = "😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly 7", function()
|
||||
local trimmedInput = Text.Trim(" 😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓 ")
|
||||
local expected = "😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly 8", function()
|
||||
local trimmedInput = Text.Trim("\n 😤👩🏼🏫😭ぼ😀 \nで😹🤕あ👩🏻🎓 \n")
|
||||
local expected = "😤👩🏼🏫😭ぼ😀 \nで😹🤕あ👩🏻🎓"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
describe("RightTrimString", function()
|
||||
it("Should right trim the string properly 1", function()
|
||||
local trimmedInput = Text.RightTrim("")
|
||||
local expected = ""
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly 2", function()
|
||||
local trimmedInput = Text.RightTrim(" ")
|
||||
local expected = ""
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly 3", function()
|
||||
local trimmedInput = Text.RightTrim("ab")
|
||||
local expected = "ab"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly 4", function()
|
||||
local trimmedInput = Text.RightTrim(" ab ")
|
||||
local expected = " ab"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly 5", function()
|
||||
local trimmedInput = Text.RightTrim(" a b ")
|
||||
local expected = " a b"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly 6", function()
|
||||
local trimmedInput = Text.RightTrim("\r\n\t\f a\r\n\t\f ")
|
||||
local expected = "\r\n\t\f a"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string with unicode characters properly", function()
|
||||
local trimmedInput = Text.RightTrim("😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓")
|
||||
local expected = "😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly 7", function()
|
||||
local trimmedInput = Text.RightTrim(" 😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓 ")
|
||||
local expected = " 😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly 8", function()
|
||||
local trimmedInput = Text.RightTrim("\n 😤👩🏼🏫😭ぼ😀 \nで😹🤕あ👩🏻🎓 \n")
|
||||
local expected = "\n 😤👩🏼🏫😭ぼ😀 \nで😹🤕あ👩🏻🎓"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
describe("LeftTrimString", function()
|
||||
it("Should left trim the string properly 1", function()
|
||||
local trimmedInput = Text.LeftTrim("")
|
||||
local expected = ""
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string properly 2", function()
|
||||
local trimmedInput = Text.LeftTrim(" ")
|
||||
local expected = ""
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string properly 3", function()
|
||||
local trimmedInput = Text.LeftTrim("ab")
|
||||
local expected = "ab"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string properly 4", function()
|
||||
local trimmedInput = Text.LeftTrim(" ab ")
|
||||
local expected = "ab "
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string properly 5", function()
|
||||
local trimmedInput = Text.LeftTrim(" a b ")
|
||||
local expected = "a b "
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string properly 6", function()
|
||||
local trimmedInput = Text.LeftTrim("\r\n\t\f a\r\n\t\f ")
|
||||
local expected = "a\r\n\t\f "
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string with unicode characters properly", function()
|
||||
local trimmedInput = Text.LeftTrim("😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓")
|
||||
local expected = "😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string properly 7", function()
|
||||
local trimmedInput = Text.LeftTrim(" 😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓 ")
|
||||
local expected = "😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓 "
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string properly", function()
|
||||
local trimmedInput = Text.LeftTrim("\n 😤👩🏼🏫😭ぼ😀 \nで😹🤕あ👩🏻🎓 \n")
|
||||
local expected = "😤👩🏼🏫😭ぼ😀 \nで😹🤕あ👩🏻🎓 \n"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
describe("SpaceNormalize", function()
|
||||
it("should remove multiple spaces between words", function()
|
||||
local a = "This is not a normal sentence."
|
||||
|
||||
expect(Text.SpaceNormalize(a)).to.equal("This is not a normal sentence.")
|
||||
end)
|
||||
|
||||
it("should remove leading and trailing whitespace", function()
|
||||
local a = " SpaceTabSpaceTab "
|
||||
|
||||
expect(Text.SpaceNormalize(a)).to.equal("SpaceTabSpaceTab")
|
||||
end)
|
||||
|
||||
it("should not change a string with no whitespace", function()
|
||||
local a = "There'sNo%Whit.e\\space--InThis."
|
||||
|
||||
expect(Text.SpaceNormalize(a)).to.equal(a)
|
||||
end)
|
||||
|
||||
it("should remove all whitespace in a string that is nothing but whitespace", function()
|
||||
local a = " "
|
||||
|
||||
expect(Text.SpaceNormalize(a)).to.equal("")
|
||||
end)
|
||||
|
||||
it("should handle the case where the string is empty", function()
|
||||
local a = ""
|
||||
|
||||
expect(Text.SpaceNormalize(a)).to.equal(a)
|
||||
end)
|
||||
|
||||
it("should throw an error if called an a non-string type", function()
|
||||
local a = { first = 1, second = 2 }
|
||||
|
||||
expect(function()
|
||||
Text.SpaceNormalize(a)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
describe("Split", function()
|
||||
local function tableEquals(tb1, tb2)
|
||||
local tables = { tb1, tb2 }
|
||||
|
||||
for _,tb in ipairs(tables) do
|
||||
for key in pairs(tb) do
|
||||
if tb1[key] ~= tb2[key] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
it("should return the correct table for your standard use case", function()
|
||||
local a = "this,is,comma,separated"
|
||||
local pattern = ","
|
||||
local expectedResult = {
|
||||
[1] = "this",
|
||||
[2] = "is",
|
||||
[3] = "comma",
|
||||
[4] = "separated",
|
||||
}
|
||||
|
||||
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should not remove whitespace", function()
|
||||
local a = " SpaceTab , , Space"
|
||||
local pattern = ","
|
||||
local expectedResult = {
|
||||
[1] = " SpaceTab ",
|
||||
[2] = " ",
|
||||
[3] = " Space",
|
||||
}
|
||||
|
||||
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should treat regular expressions as plain text", function()
|
||||
local a = "Notyour^%s+normalstring.Thisisasecondsentence."
|
||||
local b = "."
|
||||
local c = "^%s+"
|
||||
local d = "%A"
|
||||
|
||||
local expectedB = {
|
||||
[1] = "Notyour^%s+normalstring",
|
||||
[2] = "Thisisasecondsentence",
|
||||
[3] = "",
|
||||
}
|
||||
local expectedC = {
|
||||
[1] = "Notyour",
|
||||
[2] = "normalstring.Thisisasecondsentence."
|
||||
}
|
||||
local expectedD = {
|
||||
[1] = "Notyour^%s+normalstring.Thisisasecondsentence."
|
||||
}
|
||||
|
||||
expect(tableEquals(Text.Split(a, b), expectedB)).to.equal(true)
|
||||
expect(tableEquals(Text.Split(a, c), expectedC)).to.equal(true)
|
||||
expect(tableEquals(Text.Split(a, d), expectedD)).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should work when pattern is not in string", function()
|
||||
local a = "The pattern you are looking for does not exist."
|
||||
local pattern = ","
|
||||
local expectedResult = {
|
||||
[1] = "The pattern you are looking for does not exist.",
|
||||
}
|
||||
|
||||
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should work when called on an empty string", function()
|
||||
local a = ""
|
||||
local pattern = ","
|
||||
local expectedResult = {
|
||||
[1] = "",
|
||||
}
|
||||
|
||||
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should throw an error if called on an empty pattern", function()
|
||||
local a = "The pattern definitely doesn't exist here."
|
||||
local pattern = ""
|
||||
|
||||
expect(function()
|
||||
Text.Split(a, pattern)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if called an a non-string type", function()
|
||||
local a = { first = 1, second = 2 }
|
||||
local b = "an actual string"
|
||||
|
||||
expect(function()
|
||||
Text.Split(a, b)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
Text.Split(b, a)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,68 @@
|
||||
--[[
|
||||
|
||||
memoize creates a function as a wrapper that caches the last outputs of a function.
|
||||
|
||||
This is useful if you know that the function should return the same output every
|
||||
|
||||
time it is run with the same inputs. The function should only return an output, and
|
||||
|
||||
not have any side effects. These side effects are not cached.
|
||||
|
||||
|
||||
|
||||
Without memoize's caching, even though the function ouputs the same values, the
|
||||
|
||||
memory locations of the values are different; tables made in the function, even if
|
||||
|
||||
they have the same values, won't be the same tables.
|
||||
|
||||
|
||||
|
||||
memoize only caches the last set of inputs and ouputs. This means that it is only
|
||||
|
||||
helpful when the function is likely to be called with the same inputs multiple
|
||||
|
||||
times in a row. This is the case with most Roact use cases.
|
||||
|
||||
|
||||
|
||||
Note that memoize only does a ** shallow check on table inputs ** . This means
|
||||
|
||||
that if the same table is input but the elements of the table are different then
|
||||
|
||||
it will be assumed that the table has not changed.
|
||||
|
||||
|
||||
|
||||
In addition to all the previous warnings, memoize strips trailing nils. This means
|
||||
|
||||
that if foo is a memoized function and we call foo(), then foo(nil) will return a
|
||||
|
||||
cached value. This is opposed to how print handles input. print() only outputs a
|
||||
|
||||
new line, but print(nil) outputs "nil". This is because varargs can detect the
|
||||
|
||||
number of arguments passed in. So, be careful when using memoize with varargs.
|
||||
|
||||
Trailing nils will be stripped.
|
||||
|
||||
|
||||
|
||||
The wrapper can take any number of inputs and give any number of outputs.
|
||||
|
||||
Leading and interspersed nils are handled gracefully. Trailing nils on the input
|
||||
|
||||
are stripped.
|
||||
|
||||
]]
|
||||
|
||||
local function captureSize(...)
|
||||
|
||||
return {...}, select("#", ...)
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
local function memoize(func)
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
return function()
|
||||
local memoize = require(script.Parent.memoize)
|
||||
|
||||
describe("memoize", function()
|
||||
it("should handle arity 0", function()
|
||||
local callCount = 0
|
||||
local identity = memoize(function(a, b)
|
||||
callCount = callCount + 1
|
||||
return a, b
|
||||
end)
|
||||
|
||||
expect(identity()).to.equal(nil)
|
||||
expect(identity(nil)).to.equal(nil)
|
||||
expect(identity(nil, nil)).to.equal(nil)
|
||||
expect(callCount).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should handle arity 1", function()
|
||||
local callCount = 0
|
||||
local identity = memoize(function(a)
|
||||
callCount = callCount + 1
|
||||
return a
|
||||
end)
|
||||
|
||||
expect(identity(5)).to.equal(5)
|
||||
expect(identity(5)).to.equal(5)
|
||||
expect(callCount).to.equal(1)
|
||||
|
||||
expect(identity(6)).to.equal(6)
|
||||
expect(callCount).to.equal(2)
|
||||
|
||||
expect(identity(5)).to.equal(5)
|
||||
expect(callCount).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should handle arity 2", function()
|
||||
local callCount = 0
|
||||
local identity = memoize(function(a, b)
|
||||
callCount = callCount + 1
|
||||
return a, b
|
||||
end)
|
||||
|
||||
local a, b
|
||||
|
||||
a, b = identity(5, 6)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(6)
|
||||
|
||||
a, b = identity(5, 6)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(6)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
|
||||
a, b = identity(6, 5)
|
||||
expect(a).to.equal(6)
|
||||
expect(b).to.equal(5)
|
||||
|
||||
expect(callCount).to.equal(2)
|
||||
|
||||
a, b = identity(5, 6)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(6)
|
||||
|
||||
expect(callCount).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should handle mixed arity", function()
|
||||
local callCount = 0
|
||||
local identity = memoize(function(a, b)
|
||||
callCount = callCount + 1
|
||||
return a, b
|
||||
end)
|
||||
|
||||
local a, b
|
||||
|
||||
a, b = identity(5, 6)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(6)
|
||||
|
||||
a, b = identity(5, 6)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(6)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
|
||||
a, b = identity(5)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(nil)
|
||||
|
||||
a, b = identity(5)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(nil)
|
||||
|
||||
expect(callCount).to.equal(2)
|
||||
|
||||
a, b = identity()
|
||||
expect(a).to.equal(nil)
|
||||
expect(b).to.equal(nil)
|
||||
|
||||
a, b = identity()
|
||||
expect(a).to.equal(nil)
|
||||
expect(b).to.equal(nil)
|
||||
|
||||
expect(callCount).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should handle trailing nils", function()
|
||||
local callCount = 0
|
||||
local identity = memoize(function(a, b)
|
||||
callCount = callCount + 1
|
||||
return a, b
|
||||
end)
|
||||
|
||||
local a, b
|
||||
|
||||
a, b = identity(5, nil)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(nil)
|
||||
|
||||
a, b = identity(5)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(nil)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
|
||||
a, b = identity(7)
|
||||
expect(a).to.equal(7)
|
||||
expect(b).to.equal(nil)
|
||||
|
||||
expect(callCount).to.equal(2)
|
||||
|
||||
a, b = identity(5)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(nil)
|
||||
|
||||
expect(callCount).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should handle leading nils", function()
|
||||
local callCount = 0
|
||||
local identity = memoize(function(a, b)
|
||||
callCount = callCount + 1
|
||||
return a, b
|
||||
end)
|
||||
|
||||
local a, b
|
||||
|
||||
a, b = identity(nil, 7)
|
||||
expect(a).to.equal(nil)
|
||||
expect(b).to.equal(7)
|
||||
|
||||
a, b = identity(nil, 7)
|
||||
expect(a).to.equal(nil)
|
||||
expect(b).to.equal(7)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
|
||||
a, b = identity(7)
|
||||
expect(a).to.equal(7)
|
||||
expect(b).to.equal(nil)
|
||||
|
||||
expect(callCount).to.equal(2)
|
||||
|
||||
a, b = identity(nil, 7)
|
||||
expect(a).to.equal(nil)
|
||||
expect(b).to.equal(7)
|
||||
|
||||
expect(callCount).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should handle interspersed nils", function()
|
||||
local callCount = 0
|
||||
local identity = memoize(function(a, b, c, d)
|
||||
callCount = callCount + 1
|
||||
return a, b, c, d
|
||||
end)
|
||||
|
||||
local a, b, c, d
|
||||
|
||||
a, b, c, d = identity(7, nil, 7, nil)
|
||||
expect(a).to.equal(7)
|
||||
expect(b).to.equal(nil)
|
||||
expect(c).to.equal(7)
|
||||
expect(d).to.equal(nil)
|
||||
|
||||
-- Trailing nils can affect how interspersed nils are handled
|
||||
a, b, c, d = identity(7, nil, 7)
|
||||
expect(a).to.equal(7)
|
||||
expect(b).to.equal(nil)
|
||||
expect(c).to.equal(7)
|
||||
expect(d).to.equal(nil)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
|
||||
a, b, c, d = identity(7, nil, nil, nil)
|
||||
expect(a).to.equal(7)
|
||||
expect(b).to.equal(nil)
|
||||
expect(c).to.equal(nil)
|
||||
expect(d).to.equal(nil)
|
||||
|
||||
expect(callCount).to.equal(2)
|
||||
|
||||
a, b, c, d = identity(7, nil, 7, nil)
|
||||
expect(a).to.equal(7)
|
||||
expect(b).to.equal(nil)
|
||||
expect(c).to.equal(7)
|
||||
expect(d).to.equal(nil)
|
||||
|
||||
expect(callCount).to.equal(3)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"lint": {
|
||||
"LocalShadow": "fatal"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"language": {
|
||||
"mode": "nonstrict"
|
||||
},
|
||||
"lint": {
|
||||
"ImplicitReturn": "fatal"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
return Action(script.Name, function(user)
|
||||
return {
|
||||
user = user
|
||||
}
|
||||
end)
|
||||
@@ -0,0 +1,8 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
return Action(script.Name, function(users)
|
||||
return {
|
||||
users = users
|
||||
}
|
||||
end)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
return Action(script.Name, function(userId)
|
||||
return {
|
||||
userId = userId
|
||||
}
|
||||
end)
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
return Action(script.Name, function(userId, response)
|
||||
return {
|
||||
userId = userId,
|
||||
response = response
|
||||
}
|
||||
end)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
return Action(script.Name, function(userId)
|
||||
return {
|
||||
userId = userId
|
||||
}
|
||||
end)
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
return Action(script.Name, function(userId, displayName)
|
||||
return {
|
||||
userId = tostring(userId),
|
||||
displayName = displayName,
|
||||
}
|
||||
end)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
return Action(script.Name, function(placesInfos)
|
||||
return {
|
||||
placesInfos = placesInfos,
|
||||
}
|
||||
end)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
return Action(script.Name, function(countryCode)
|
||||
return {
|
||||
countryCode = countryCode,
|
||||
}
|
||||
end)
|
||||
@@ -0,0 +1,8 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
return Action(script.Name, function(userId)
|
||||
return {
|
||||
userId = userId,
|
||||
}
|
||||
end)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
return Action(script.Name, function(deviceOrientation)
|
||||
return {
|
||||
deviceOrientation = deviceOrientation,
|
||||
}
|
||||
end)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Common = CorePackages.AppTempCommon.Common
|
||||
|
||||
local Action = require(Common.Action)
|
||||
|
||||
return Action(script.Name, function(count)
|
||||
return {
|
||||
count = count,
|
||||
}
|
||||
end)
|
||||
@@ -0,0 +1,14 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
local ArgCheck = require(CorePackages.ArgCheck)
|
||||
|
||||
--[[
|
||||
Each entry in the table is a type of GameIcon with the universe id as key
|
||||
]]
|
||||
return Action(script.Name, function(iconsTable)
|
||||
ArgCheck.isType(iconsTable, "table", "iconsTable")
|
||||
|
||||
return {
|
||||
gameIcons = iconsTable
|
||||
}
|
||||
end)
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
return function()
|
||||
local SetGameIcons = require(script.Parent.SetGameIcons)
|
||||
|
||||
it("should assert if given a non-table for thumbnailsTable", function()
|
||||
SetGameIcons({})
|
||||
|
||||
expect(function()
|
||||
SetGameIcons("string")
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
SetGameIcons(0)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
SetGameIcons(nil)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
SetGameIcons(false)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
SetGameIcons(function() end)
|
||||
end).to.throw()
|
||||
end)
|
||||
end
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
--[[
|
||||
Passes a table that looks like this : { "universeId" : {json}, ... }
|
||||
|
||||
{
|
||||
"26034470" : {
|
||||
universeId : "26034470",
|
||||
placeId : "70542190",
|
||||
url : https://t5.rbxcdn.com/ed422c6fbb22280971cfb289f40ac814,
|
||||
final : true
|
||||
}, {...}, ...
|
||||
}
|
||||
|
||||
]]
|
||||
|
||||
--TODO MOBLUAPP-778 Refactor improper Setter Actions.
|
||||
return Action(script.Name, function(thumbnailsTable)
|
||||
assert(type(thumbnailsTable) == "table",
|
||||
string.format("SetGameThumbnails action expects thumbnailsTable to be a table, was %s", type(thumbnailsTable)))
|
||||
|
||||
return {
|
||||
thumbnails = thumbnailsTable
|
||||
}
|
||||
end)
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Common = CorePackages.AppTempCommon.Common
|
||||
|
||||
local Action = require(Common.Action)
|
||||
|
||||
return Action(script.Name, function(userId, universeId)
|
||||
return {
|
||||
userId = userId,
|
||||
universeId = universeId,
|
||||
}
|
||||
end)
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
return Action(script.Name, function(userId, isFriend)
|
||||
return {
|
||||
userId = userId,
|
||||
isFriend = isFriend,
|
||||
}
|
||||
end)
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Common = CorePackages.AppTempCommon.Common
|
||||
|
||||
local Action = require(Common.Action)
|
||||
|
||||
return Action(script.Name, function(userId, membershipType)
|
||||
return {
|
||||
userId = userId,
|
||||
membershipType = membershipType,
|
||||
}
|
||||
end)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
return Action(script.Name, function(userId, presence, lastLocation)
|
||||
return {
|
||||
userId = tostring(userId),
|
||||
presence = presence,
|
||||
lastLocation = lastLocation,
|
||||
}
|
||||
end)
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Common = CorePackages.AppTempCommon.Common
|
||||
|
||||
local Action = require(Common.Action)
|
||||
|
||||
return Action(script.Name, function(userId, image, thumbnailType, thumbnailSize)
|
||||
return {
|
||||
userId = userId,
|
||||
image = image,
|
||||
thumbnailType = thumbnailType,
|
||||
thumbnailSize = thumbnailSize,
|
||||
}
|
||||
end)
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
return Action(script.Name, function(key, status)
|
||||
return {
|
||||
key = key,
|
||||
status = status
|
||||
}
|
||||
end)
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
return function()
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local UpdateFetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Actions.UpdateFetchingStatus)
|
||||
|
||||
describe("Action UpdateFetchingStatus", function()
|
||||
it("should return correct action name", function()
|
||||
expect(UpdateFetchingStatus.name).to.equal("UpdateFetchingStatus")
|
||||
end)
|
||||
|
||||
it("should return correct action type name", function()
|
||||
local action = UpdateFetchingStatus()
|
||||
expect(action.type).to.equal(UpdateFetchingStatus.name)
|
||||
end)
|
||||
|
||||
it("should return a table with the correct key and status", function()
|
||||
local action = UpdateFetchingStatus("key", "status")
|
||||
expect(action.key).to.equal("key")
|
||||
expect(action.status).to.equal("status")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"language": {
|
||||
"mode": "nonstrict"
|
||||
},
|
||||
"lint": {
|
||||
"ImplicitReturn": "fatal"
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
local Workspace = game:GetService("Workspace")
|
||||
local RunService = game:GetService('RunService')
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Roact = require(CorePackages.Roact)
|
||||
|
||||
local BAR_SLICE_CENTER = Rect.new(1, 0, 2, 3)
|
||||
local BAR_MAX_SIZE = 15
|
||||
local BAR_MAX_AMPLITUDE = 40
|
||||
local BAR_DIAMETER = 4
|
||||
local BAR_PERIOD = 1.25
|
||||
|
||||
local LoadingBar = Roact.Component:extend("LoadingBar")
|
||||
|
||||
function LoadingBar:init()
|
||||
self.barRef = Roact.createRef()
|
||||
end
|
||||
|
||||
function LoadingBar:render()
|
||||
local zIndex = self.props.ZIndex
|
||||
|
||||
return Roact.createElement("ImageLabel", {
|
||||
Image = "rbxasset://textures/ui/LuaApp/9-slice/gr-loading-indicator.png",
|
||||
ScaleType = "Slice",
|
||||
SliceCenter = BAR_SLICE_CENTER,
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
ZIndex = zIndex,
|
||||
[Roact.Ref] = self.barRef,
|
||||
})
|
||||
end
|
||||
|
||||
function LoadingBar:didMount()
|
||||
self.connection = RunService.RenderStepped:Connect(function()
|
||||
local t = Workspace.DistributedGameTime
|
||||
local instance = self.barRef.current
|
||||
local period = 2.0 * math.pi / BAR_PERIOD
|
||||
|
||||
local width = (BAR_MAX_SIZE/2) * (1 - math.cos(2*t*period))
|
||||
instance.Size = UDim2.new(0, BAR_DIAMETER + width, 0, BAR_DIAMETER)
|
||||
|
||||
local x = BAR_MAX_AMPLITUDE * math.cos(t*period)
|
||||
instance.Position = UDim2.new(0.5, x - width/2 - BAR_DIAMETER/2, 0.5, 0)
|
||||
end)
|
||||
end
|
||||
|
||||
function LoadingBar:willUnmount()
|
||||
self.connection:Disconnect()
|
||||
end
|
||||
|
||||
return LoadingBar
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
return function()
|
||||
local LoadingBar = require(script.Parent.LoadingBar)
|
||||
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Roact = require(CorePackages.Roact)
|
||||
|
||||
it("should create and destroy without errors", function()
|
||||
local element = Roact.createElement(LoadingBar, {
|
||||
Position = UDim2.new(0.5, 0, 0.5, 5),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"language": {
|
||||
"mode": "nonstrict"
|
||||
},
|
||||
"lint": {
|
||||
"ImplicitReturn": "fatal"
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
return {
|
||||
AvatarThumbnail = "AvatarThumbnail",
|
||||
HeadShot = "HeadShot",
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
local RetrievalStatus = {}
|
||||
|
||||
local EnumValues =
|
||||
{
|
||||
NotStarted = "NotStarted",
|
||||
Fetching = "Fetching",
|
||||
Done = "Done",
|
||||
Failed = "Failed",
|
||||
}
|
||||
|
||||
setmetatable(RetrievalStatus,
|
||||
{
|
||||
__newindex = function(t, key, index)
|
||||
end,
|
||||
__index = function(t, index)
|
||||
assert(EnumValues[index] ~= nil, ("RetrievalStatus Enum has no value: " .. tostring(index)))
|
||||
return EnumValues[index]
|
||||
end
|
||||
})
|
||||
|
||||
return RetrievalStatus
|
||||
@@ -0,0 +1,10 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local User = require(CorePackages.AppTempCommon.LuaApp.Models.User)
|
||||
|
||||
return {
|
||||
[0] = User.PresenceType.OFFLINE,
|
||||
[1] = User.PresenceType.ONLINE,
|
||||
[2] = User.PresenceType.IN_GAME,
|
||||
[3] = User.PresenceType.IN_STUDIO,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"language": {
|
||||
"mode": "nonstrict"
|
||||
},
|
||||
"lint": {
|
||||
"ImplicitReturn": "fatal"
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local ThrottleUserId = require(CorePackages.AppTempCommon.LuaApp.Utils.ThrottleUserId)
|
||||
|
||||
local FIntAvatarEditorNewCatalogButton = settings():GetFVariable("AvatarEditorNewCatalogButton2")
|
||||
|
||||
return function(userId)
|
||||
if tonumber(userId) then
|
||||
local throttleNumber = tonumber(FIntAvatarEditorNewCatalogButton)
|
||||
local id = tonumber(userId)
|
||||
return ThrottleUserId(throttleNumber, id)
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
-- TODO: Delete this file when deleting the flag: LuaAppConvertUniverseIdToStringV364
|
||||
local FFlagLuaAppConvertUniverseIdToString = settings():GetFFlag("LuaAppConvertUniverseIdToStringV364")
|
||||
|
||||
return function(universeId)
|
||||
-- When the flag is on, we've converted the universe id to string at the place we received it
|
||||
if FFlagLuaAppConvertUniverseIdToString then
|
||||
return universeId
|
||||
else
|
||||
return tostring(universeId)
|
||||
end
|
||||
end
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local ThrottleUserId = require(CorePackages.AppTempCommon.LuaApp.Utils.ThrottleUserId)
|
||||
|
||||
local FIntEnableFriendFooterOnHomePage = settings():GetFVariable("EnableFriendFooterOnHomePageV369")
|
||||
|
||||
-- Don't call this function globally because we cannot get the userId
|
||||
-- Reason: The LocalPlayer wouldn't be ready if we called it globally.
|
||||
return function()
|
||||
local throttleNumber = tonumber(FIntEnableFriendFooterOnHomePage)
|
||||
local userId = Players.LocalPlayer.UserId
|
||||
|
||||
return ThrottleUserId(throttleNumber, userId)
|
||||
end
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
return function()
|
||||
return settings():GetFFlag("UseDateTimeType3")
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"language": {
|
||||
"mode": "nonstrict"
|
||||
},
|
||||
"lint": {
|
||||
"ImplicitReturn": "fatal"
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
|
||||
|
||||
return function(requestImpl, conversationId, universeId, decorators)
|
||||
assert(requestImpl, "requestImpl is required")
|
||||
assert(conversationId, "conversationId is required")
|
||||
assert(universeId, "universeId is required")
|
||||
|
||||
local payload = HttpService:JSONEncode({
|
||||
conversationId = conversationId,
|
||||
universeId = universeId,
|
||||
decorators = decorators
|
||||
})
|
||||
local url = string.format("%s/send-game-link-message", Url.CHAT_URL)
|
||||
|
||||
return requestImpl(url, "POST", { postBody = payload })
|
||||
end
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
|
||||
|
||||
return function(requestImpl, conversationId, messageText, decorators)
|
||||
local payload = HttpService:JSONEncode({
|
||||
conversationId = conversationId,
|
||||
message = messageText,
|
||||
decorators = decorators
|
||||
})
|
||||
|
||||
local url = string.format("%s/send-message", Url.CHAT_URL)
|
||||
|
||||
return requestImpl(url, "POST", { postBody = payload })
|
||||
end
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
|
||||
|
||||
return function(requestImpl, userId, clientId)
|
||||
local payload = HttpService:JSONEncode({
|
||||
participantuserId = userId
|
||||
})
|
||||
|
||||
local url = string.format("%s/start-one-to-one-conversation", Url.CHAT_URL)
|
||||
|
||||
return requestImpl(url, "POST", { postBody = payload })
|
||||
end
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
|
||||
|
||||
--[[
|
||||
Docs: https://thumbnails.roblox.com/docs#!/Games/get_v1_games_icons
|
||||
This resolves to
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"targetId": 0,
|
||||
"state": "Error",
|
||||
"imageUrl": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
]]
|
||||
return function (requestImpl, universeIds, size)
|
||||
local qs = Url:makeQueryString({
|
||||
universeIds = table.concat(universeIds, ","),
|
||||
format = "png",
|
||||
size = size,
|
||||
})
|
||||
local url = string.format("%sv1/games/icons?%s", Url.THUMBNAILS_URL, qs)
|
||||
return requestImpl(url, "GET")
|
||||
end
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
--[[
|
||||
*** DEPRECATED ***
|
||||
TODO: removed this file after new thumbnail API is being in use without any flags
|
||||
RELATED: GAMEDISC-27 GAMEDISC-126 FIntLuaAppPercentRollOutNewThumbnailsApiV3
|
||||
]]
|
||||
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
|
||||
|
||||
--[[
|
||||
This endpoint returns a promise that resolves to:
|
||||
[
|
||||
{
|
||||
"final": true,
|
||||
"url": "string",
|
||||
"retryToken": "string",
|
||||
"universeId": 0,
|
||||
"placeId": 0
|
||||
}, {...}, ...
|
||||
]
|
||||
]]
|
||||
|
||||
-- requestImpl - (function<promise<HttpResponse>>(url, requestMethod, options))
|
||||
-- imageTokens - (array<long>) the placeIds of the places you want to get thumbnails for
|
||||
-- height - (int) the height of the asset to render
|
||||
-- width - (int) the width of the asset to render
|
||||
return function(requestImpl, imageTokens, height, width)
|
||||
local args = {}
|
||||
|
||||
if height then
|
||||
table.insert(args, string.format("height=%d", height))
|
||||
end
|
||||
|
||||
if width then
|
||||
table.insert(args, string.format("width=%d", width))
|
||||
end
|
||||
|
||||
-- append all of the thumbnail tokens
|
||||
local totalTokens = 0
|
||||
for _, value in pairs(imageTokens) do
|
||||
totalTokens = totalTokens + 1
|
||||
table.insert(args, string.format("imageTokens=%s", value))
|
||||
end
|
||||
if totalTokens == 0 then
|
||||
error("cannot fetch thumbnails without tokens")
|
||||
end
|
||||
|
||||
-- construct the url
|
||||
local url = string.format("%sv1/games/game-thumbnails?%s", Url.GAME_URL, table.concat(args, "&"))
|
||||
|
||||
-- return a promise of the result listed above
|
||||
return requestImpl(url, "GET")
|
||||
end
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
|
||||
|
||||
--[[
|
||||
This endpoint returns games' information with a batches of place ids
|
||||
Doc: https://games.roblox.com/docs#!/Games/get_v1_games_multiget_place_details
|
||||
{
|
||||
"placeId": 0,
|
||||
"name": "string",
|
||||
"description": "string",
|
||||
"url": "string",
|
||||
"builder": "string",
|
||||
"builderId": 0,
|
||||
"isPlayable": true,
|
||||
"reasonProhibited": "string",
|
||||
"universeId": 0,
|
||||
"universeRootPlaceId": 0,
|
||||
"price": 0,
|
||||
"imageToken": "string"
|
||||
}
|
||||
]]--
|
||||
|
||||
return function(requestImpl, placeIds)
|
||||
local argTable = {
|
||||
placeIds = placeIds,
|
||||
}
|
||||
|
||||
local args = Url:makeQueryString(argTable)
|
||||
local url = string.format("%s/v1/games/multiget-place-details?%s", Url.GAME_URL, args)
|
||||
|
||||
return requestImpl(url, "GET")
|
||||
end
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
|
||||
|
||||
return function(requestImpl, placeIds)
|
||||
local argTable = {
|
||||
placeIds = placeIds,
|
||||
}
|
||||
|
||||
-- construct the url
|
||||
local args = Url:makeQueryString(argTable)
|
||||
local url = string.format("%s/v1/games/multiget-place-details?%s",
|
||||
Url.GAME_URL, args
|
||||
)
|
||||
|
||||
return requestImpl(url, "GET")
|
||||
end
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
|
||||
|
||||
--[[
|
||||
Documentation of endpoint:
|
||||
https://thumbnails.roblox.com/docs#!/Avatar/get_v1_users_avatar
|
||||
|
||||
input:
|
||||
userIds
|
||||
thumbnailSize
|
||||
output:
|
||||
[
|
||||
{
|
||||
"targetId": number,
|
||||
"state": string,
|
||||
"imageUrl": string,
|
||||
},
|
||||
]
|
||||
]]
|
||||
|
||||
local MAX_USER_IDS = 100
|
||||
|
||||
return function (networkImpl, userIds, thumbnailSize)
|
||||
assert(type(userIds) == "table", "ThumbnailsGetAvatar expects userIds to be a table")
|
||||
|
||||
if #userIds == 0 or #userIds > MAX_USER_IDS then
|
||||
error(string.format("ThumbnailsGetAvatar request expects userIds count between 1-%d", MAX_USER_IDS))
|
||||
end
|
||||
|
||||
local queryString = Url:makeQueryString({
|
||||
userIds = table.concat(userIds, ","),
|
||||
size = thumbnailSize,
|
||||
format = "png",
|
||||
})
|
||||
|
||||
local url = string.format("%sv1/users/avatar?%s", Url.THUMBNAILS_URL, queryString)
|
||||
|
||||
return networkImpl(url, "GET")
|
||||
end
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
|
||||
|
||||
--[[
|
||||
Documentation of endpoint:
|
||||
https://thumbnails.roblox.com/docs#!/Avatar/get_v1_users_avatar_headshot
|
||||
|
||||
input:
|
||||
userIds
|
||||
thumbnailSize
|
||||
output:
|
||||
[
|
||||
{
|
||||
"targetId": number,
|
||||
"state": string,
|
||||
"imageUrl": string,
|
||||
},
|
||||
]
|
||||
]]
|
||||
|
||||
local MAX_USER_IDS = 100
|
||||
|
||||
return function (networkImpl, userIds, thumbnailSize)
|
||||
assert(type(userIds) == "table", "ThumbnailsGetAvatarHeadshot expects userIds to be a table")
|
||||
|
||||
if #userIds == 0 or #userIds > MAX_USER_IDS then
|
||||
error(string.format("ThumbnailsGetAvatarHeadshot request expects userIds count between 1-%d", MAX_USER_IDS))
|
||||
end
|
||||
|
||||
local queryString = Url:makeQueryString({
|
||||
userIds = table.concat(userIds, ","),
|
||||
size = thumbnailSize,
|
||||
format = "png",
|
||||
})
|
||||
|
||||
local url = string.format("%sv1/users/avatar-headshot?%s", Url.THUMBNAILS_URL, queryString)
|
||||
|
||||
return networkImpl(url, "GET")
|
||||
end
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
|
||||
|
||||
local isNewFriendsEndpointsEnabled = require(CorePackages.AppTempCommon.LuaChat.Flags.isNewFriendsEndpointsEnabled)
|
||||
|
||||
--[[
|
||||
This endpoint returns a promise that resolves to:
|
||||
|
||||
[
|
||||
{
|
||||
"success:" true,
|
||||
"count": "0"
|
||||
},
|
||||
]
|
||||
]]--
|
||||
|
||||
-- requestImpl - (function<promise<HttpResponse>>(url, requestMethod, options))
|
||||
return function(requestImpl)
|
||||
|
||||
local url = string.format("%s/user/get-friendship-count?%s",
|
||||
Url.API_URL, tostring(Players.LocalPlayer.UserId)
|
||||
)
|
||||
|
||||
if isNewFriendsEndpointsEnabled() then
|
||||
url = string.format("%s/my/friends/count", Url.FRIEND_URL)
|
||||
end
|
||||
|
||||
return requestImpl(url, "GET")
|
||||
end
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
|
||||
|
||||
return function(requestImpl, userId)
|
||||
local url = string.format("%s/users/%s/friends",
|
||||
Url.FRIEND_URL, userId
|
||||
)
|
||||
|
||||
return requestImpl(url, "GET")
|
||||
end
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
|
||||
|
||||
-- Endpoint documented here:
|
||||
-- https://presence.roblox.com/docs
|
||||
|
||||
return function(requestImpl, userIds)
|
||||
local userIdsToNumber = {}
|
||||
for _, id in pairs(userIds) do
|
||||
local idToNumber = tonumber(id)
|
||||
if idToNumber then
|
||||
table.insert(userIdsToNumber, idToNumber)
|
||||
end
|
||||
end
|
||||
|
||||
local payload = HttpService:JSONEncode({
|
||||
userIds = userIdsToNumber,
|
||||
})
|
||||
|
||||
local url = string.format("%s/presence/users", Url.PRESENCE_URL)
|
||||
|
||||
return requestImpl(url, "POST", { postBody = payload })
|
||||
end
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
|
||||
|
||||
local THUMBNAIL_TYPE_BY_NAME = {
|
||||
AvatarThumbnail = Enum.ThumbnailType.AvatarThumbnail,
|
||||
HeadShot = Enum.ThumbnailType.HeadShot,
|
||||
}
|
||||
|
||||
local THUMBNAIL_SIZE_BY_NAME = {
|
||||
Size48x48 = Enum.ThumbnailSize.Size48x48,
|
||||
Size60x60 = Enum.ThumbnailSize.Size60x60,
|
||||
Size100x100 = Enum.ThumbnailSize.Size100x100,
|
||||
Size150x150 = Enum.ThumbnailSize.Size150x150,
|
||||
Size352x352 = Enum.ThumbnailSize.Size352x352
|
||||
}
|
||||
|
||||
return function(userId, thumbnailType, thumbnailSize)
|
||||
return Promise.new(function(resolve, reject)
|
||||
--Async methods will yield the thread
|
||||
spawn(function()
|
||||
local result = {success = false}
|
||||
local success, message = pcall(function()
|
||||
local image, isFinal = Players:GetUserThumbnailAsync(
|
||||
tonumber(userId), THUMBNAIL_TYPE_BY_NAME[thumbnailType], THUMBNAIL_SIZE_BY_NAME[thumbnailSize]
|
||||
)
|
||||
|
||||
result = {
|
||||
success = true,
|
||||
id = userId,
|
||||
thumbnailType = thumbnailType,
|
||||
thumbnailSize = thumbnailSize,
|
||||
|
||||
image = isFinal and image or nil,
|
||||
isFinal = isFinal,
|
||||
}
|
||||
end)
|
||||
|
||||
if success then
|
||||
resolve(result)
|
||||
else
|
||||
result.message = message
|
||||
reject(result)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,181 @@
|
||||
--[[
|
||||
Url Constructor
|
||||
|
||||
Provides a single location for base urls.
|
||||
|
||||
]]--
|
||||
local ContentProvider = game:GetService("ContentProvider")
|
||||
|
||||
local FFlagLuaFixEconomyCreatorStatsUrl = game:DefineFastFlag("LuaFixEconomyCreatorStatsUrl", false)
|
||||
|
||||
-- helper functions
|
||||
local function parseBaseUrlInformation()
|
||||
-- get the current base url from the current configuration
|
||||
local baseUrl = ContentProvider.BaseUrl
|
||||
|
||||
-- keep a copy of the base url (https://www.roblox.com/)
|
||||
-- append a trailing slash if there isn't one
|
||||
if baseUrl:sub(#baseUrl) ~= "/" then
|
||||
baseUrl = baseUrl .. "/"
|
||||
end
|
||||
|
||||
-- parse out scheme (http, https)
|
||||
local _, schemeEnd = baseUrl:find("://")
|
||||
|
||||
-- parse out the prefix (www, kyle, ying, etc.)
|
||||
local prefixIndex, prefixEnd = baseUrl:find("%.", schemeEnd + 1)
|
||||
local basePrefix = baseUrl:sub(schemeEnd + 1, prefixIndex - 1)
|
||||
|
||||
-- parse out the domain (roblox.com/, sitetest1.robloxlabs.com/, etc.)
|
||||
local baseDomain = baseUrl:sub(prefixEnd + 1)
|
||||
|
||||
return baseUrl, basePrefix, baseDomain
|
||||
end
|
||||
local function preventTableModification(aTable, key, value)
|
||||
error("Attempt to modify read-only table")
|
||||
end
|
||||
local function createReadOnlyTable(aTable)
|
||||
return setmetatable({}, {
|
||||
__index = aTable,
|
||||
__newindex = preventTableModification,
|
||||
__metatable = false
|
||||
});
|
||||
end
|
||||
|
||||
|
||||
-- url construction building blocks
|
||||
local _baseUrl, _basePrefix, _baseDomain = parseBaseUrlInformation()
|
||||
|
||||
-- construct urls once
|
||||
local _baseApiUrl = string.format("https://api.%s", _baseDomain)
|
||||
local _baseApisUrl = string.format("https://apis.%s", _baseDomain)
|
||||
local _baseAuthUrl = string.format("https://auth.%s", _baseDomain)
|
||||
local _baseAccountSettingsUrl = string.format("https://accountsettings.%s", _baseDomain)
|
||||
local _baseAvatarUrl = string.format("https://avatar.%s", _baseDomain)
|
||||
local _baseCatalogUrl = string.format("https://catalog.%s", _baseDomain)
|
||||
local _baseInventoryUrl = string.format("https://inventory.%s", _baseDomain)
|
||||
local _baseChatUrl = string.format("https://chat.%sv2", _baseDomain)
|
||||
local _baseFriendUrl = string.format("https://friends.%sv1", _baseDomain)
|
||||
local _baseGameAssetUrl = string.format("https://assetgame.%s", _baseDomain)
|
||||
local _baseGamesUrl = string.format("https://games.%s", _baseDomain)
|
||||
local _baseGroupsUrl = string.format("https://groups.%s", _baseDomain)
|
||||
local _baseNotificationUrl = string.format("https://notifications.%s", _baseDomain)
|
||||
local _basePresenceUrl = string.format("https://presence.%sv1", _baseDomain)
|
||||
local _baseRealtimeUrl = string.format("https://realtime.%s", _baseDomain)
|
||||
local _baseWebUrl = string.format("https://web.%s", _baseDomain)
|
||||
local _baseWwwUrl = string.format("https://www.%s", _baseDomain)
|
||||
local _baseAdsUrl = string.format("https://ads.%s", _baseDomain)
|
||||
local _baseFollowingsUrl = string.format("https://followings.%s", _baseDomain)
|
||||
local _baseEconomyUrl = string.format("https://economy.%s", _baseDomain)
|
||||
local _baseThumbnailsUrl = string.format("https://thumbnails.%s", _baseDomain)
|
||||
local _baseAccountSettings = string.format("https://accountsettings.%s", _baseDomain)
|
||||
local _basePremiumFeatures = string.format("https://premiumfeatures.%s", _baseDomain)
|
||||
local _baseLocale = string.format("https://locale.%s", _baseDomain)
|
||||
local _baseBadgesUrl = string.format("https://badges.%s", _baseDomain)
|
||||
local _baseMetricsUrl = string.format("https://metrics.%sv1", _baseDomain)
|
||||
local _baseApisRcsUrl = string.format("https://apis.rcs.%s", _baseDomain)
|
||||
local _baseDiscussionsUrl = string.format("https://discussions.%s", _baseDomain)
|
||||
local _baseContactsUrl = string.format("https://contacts.%s", _baseDomain)
|
||||
local _baseSearchUrl = string.format("https://search.%s", _baseDomain)
|
||||
local _baseStaticUrl = string.format("https://static.%s", _baseDomain)
|
||||
local _baseGameSearchUITreatments = string.format("https://gamesearchuitreatments.api.%s", _baseDomain)
|
||||
local _baseEconomyCreatorStats = FFlagLuaFixEconomyCreatorStatsUrl
|
||||
and string.format("https://economycreatorstats.%s", _baseDomain)
|
||||
or string.format("https://economycreatorstats.api.%s", _baseDomain)
|
||||
local _baseUrlSecure = string.gsub(_baseUrl, "http://", "https://")
|
||||
|
||||
-- public api
|
||||
local Url = {
|
||||
DOMAIN = _baseDomain,
|
||||
PREFIX = _basePrefix,
|
||||
BASE_URL = _baseUrl,
|
||||
BASE_URL_SECURE = _baseUrlSecure,
|
||||
API_URL = _baseApiUrl,
|
||||
APIS_URL = _baseApisUrl,
|
||||
AUTH_URL = _baseAuthUrl,
|
||||
ACCOUNT_SETTINGS_URL = _baseAccountSettingsUrl,
|
||||
AVATAR_URL = _baseAvatarUrl,
|
||||
CATALOG_URL = _baseCatalogUrl,
|
||||
INVENTORY_URL = _baseInventoryUrl,
|
||||
GAME_URL = _baseGamesUrl,
|
||||
GAME_ASSET_URL = _baseGameAssetUrl,
|
||||
GROUPS_URL = _baseGroupsUrl,
|
||||
CHAT_URL = _baseChatUrl,
|
||||
FRIEND_URL = _baseFriendUrl,
|
||||
PRESENCE_URL = _basePresenceUrl,
|
||||
NOTIFICATION_URL = _baseNotificationUrl,
|
||||
REALTIME_URL = _baseRealtimeUrl,
|
||||
WEB_URL = _baseWebUrl,
|
||||
WWW_URL = _baseWwwUrl,
|
||||
ADS_URL = _baseAdsUrl,
|
||||
SEARCH_URL = _baseSearchUrl,
|
||||
GAME_SEARCH_UI_TREATMENTS = _baseGameSearchUITreatments,
|
||||
FOLLOWINGS_URL = _baseFollowingsUrl,
|
||||
ECONOMY_URL = _baseEconomyUrl,
|
||||
THUMBNAILS_URL = _baseThumbnailsUrl,
|
||||
BADGES_URL = _baseBadgesUrl,
|
||||
ACCOUNT_SETTINGS = _baseAccountSettings,
|
||||
PREMIUM_FEATURES = _basePremiumFeatures,
|
||||
LOCALE = _baseLocale,
|
||||
METRICS_URL = _baseMetricsUrl,
|
||||
APIS_RCS_URL = _baseApisRcsUrl,
|
||||
DISCUSSIONS_URL = _baseDiscussionsUrl,
|
||||
CONTACTS_URL = _baseContactsUrl,
|
||||
STATIC_URL = _baseStaticUrl,
|
||||
BLOG_URL = "https://blog.roblox.com/",
|
||||
CORP_URL = "https://corp.roblox.com/",
|
||||
ECNOMY_CREATOR_STATS = _baseEconomyCreatorStats,
|
||||
}
|
||||
|
||||
function Url:getUserProfileUrl(userId)
|
||||
return string.format("%susers/%s/profile", self.BASE_URL, userId)
|
||||
end
|
||||
|
||||
function Url:getUserFriendsUrl(userId)
|
||||
return string.format("%susers/%s/friends", self.BASE_URL, userId)
|
||||
end
|
||||
|
||||
function Url:getUserInventoryUrl(userId)
|
||||
return string.format("%susers/%s/inventory", self.BASE_URL, userId)
|
||||
end
|
||||
|
||||
function Url:getPlaceDefaultThumbnailUrl(placeId, width, height)
|
||||
return string.format(
|
||||
"%sThumbs/Asset.ashx?width=%d&height=%d&assetId=%s&ignorePlaceMediaItems=true",
|
||||
self.BASE_URL,
|
||||
width,
|
||||
height,
|
||||
tostring(placeId))
|
||||
end
|
||||
|
||||
function Url:isVanitySite()
|
||||
return self.PREFIX ~= "www"
|
||||
end
|
||||
|
||||
-- data - (table<string, string>) a table of key/value pairs to format
|
||||
function Url:makeQueryString(data)
|
||||
--NOTE - This function can be used to create a query string of parameters
|
||||
-- at the end of url query, or create a application/form-url-encoded post body string
|
||||
local params = {}
|
||||
|
||||
-- NOTE - Arrays are handled, but generally data is expected to be flat.
|
||||
for key, value in pairs(data) do
|
||||
if value ~= nil then --for optional params
|
||||
if type(value) == "table" then
|
||||
for i = 1, #value do
|
||||
table.insert(params, key .. "=" .. value[i])
|
||||
end
|
||||
else
|
||||
table.insert(params, key .. "=" .. tostring(value))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return table.concat(params, "&")
|
||||
end
|
||||
|
||||
|
||||
-- prevent anyone from modifying this table:
|
||||
Url = createReadOnlyTable(Url)
|
||||
|
||||
return Url
|
||||
@@ -0,0 +1,12 @@
|
||||
return function()
|
||||
|
||||
local ContentProvider = game:GetService("ContentProvider")
|
||||
local Url = require(script.Parent.Url)
|
||||
|
||||
it("The base url has not been changed for debugging", function()
|
||||
local baseUrl = ContentProvider.BaseUrl
|
||||
|
||||
expect(baseUrl).to.equal(Url.BASE_URL)
|
||||
end)
|
||||
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
--[[
|
||||
A function to return a fake ID, used for testing.
|
||||
|
||||
We turn all IDs into strings as we typically use them as keys in the state.
|
||||
It's better to use a string than a number, because a number would indicate
|
||||
an array index.
|
||||
|
||||
Roblox APIs expect to be given integers for IDs however, so just tonumber()
|
||||
the ID in this case.
|
||||
]]
|
||||
|
||||
local lastId = 0
|
||||
|
||||
return function()
|
||||
lastId = lastId + 1
|
||||
return tostring(lastId)
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"language": {
|
||||
"mode": "nonstrict"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
--[[
|
||||
{
|
||||
universeId : string,
|
||||
state : string,
|
||||
url : string,
|
||||
}
|
||||
]]
|
||||
|
||||
local Thumbnail = {}
|
||||
|
||||
function Thumbnail.new()
|
||||
local self = {}
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Thumbnail.fromThumbnailData(thumbnailData, size)
|
||||
local self = Thumbnail.new()
|
||||
|
||||
self.universeId = tostring(thumbnailData.targetId)
|
||||
self.state = thumbnailData.state
|
||||
self.url = thumbnailData.imageUrl
|
||||
self.size = size
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Thumbnail.isCompleteThumbnailData(thumbnailData)
|
||||
return type(thumbnailData) == "table"
|
||||
and type(thumbnailData.targetId) == "number"
|
||||
and type(thumbnailData.state) == "string"
|
||||
and (type(thumbnailData.imageUrl) == "string" or thumbnailData.imageUrl == nil)
|
||||
end
|
||||
|
||||
function Thumbnail.checkStateIsFinal(thumbnailState)
|
||||
return thumbnailState ~= "Pending"
|
||||
end
|
||||
|
||||
return Thumbnail
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
return function()
|
||||
local Thumbnail = require(script.Parent.Thumbnail)
|
||||
|
||||
it("should set fields without errors", function()
|
||||
local testData =
|
||||
{
|
||||
targetId = 123456,
|
||||
state = "Completed",
|
||||
imageUrl = "a url",
|
||||
}
|
||||
|
||||
local thumbnail = Thumbnail.fromThumbnailData(testData)
|
||||
|
||||
expect(thumbnail).to.be.a("table")
|
||||
expect(thumbnail.universeId).to.equal("123456")
|
||||
expect(thumbnail.state).to.equal("Completed")
|
||||
expect(thumbnail.url).to.equal("a url")
|
||||
end)
|
||||
|
||||
end
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
local ThumbnailRequest = {}
|
||||
|
||||
function ThumbnailRequest.new()
|
||||
local self = {}
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function ThumbnailRequest.fromData(thumbnailType, thumbnailSize)
|
||||
local self = ThumbnailRequest.new()
|
||||
|
||||
self.thumbnailType = thumbnailType
|
||||
self.thumbnailSize = thumbnailSize
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return ThumbnailRequest
|
||||
@@ -0,0 +1,134 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local MockId = require(CorePackages.AppTempCommon.LuaApp.MockId)
|
||||
|
||||
local User = {}
|
||||
|
||||
User.PresenceType = {
|
||||
OFFLINE = "OFFLINE",
|
||||
ONLINE = "ONLINE",
|
||||
IN_GAME = "IN_GAME",
|
||||
IN_STUDIO = "IN_STUDIO",
|
||||
}
|
||||
|
||||
function User.new()
|
||||
local self = {}
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function User.mock()
|
||||
local self = User.new()
|
||||
|
||||
self.id = MockId()
|
||||
|
||||
self.isFetching = false
|
||||
self.isFriend = false
|
||||
self.lastLocation = nil
|
||||
self.name = "USER NAME"
|
||||
self.universeId = nil
|
||||
self.placeId = nil
|
||||
self.rootPlaceId = nil
|
||||
self.gameInstanceId = nil
|
||||
self.presence = User.PresenceType.OFFLINE
|
||||
self.membership = nil
|
||||
self.thumbnails = nil
|
||||
self.lastOnline = nil
|
||||
self.displayName = "DN+" .. self.name
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-- Note: Going forward, leverage User.fromDataTable() instead.
|
||||
-- It accepts a more flexible parameter than User.fromData() and constructs the same User model
|
||||
function User.fromData(id, name, isFriend)
|
||||
local self = User.new()
|
||||
|
||||
self.id = tostring(id)
|
||||
|
||||
self.isFetching = false
|
||||
self.isFriend = isFriend
|
||||
self.lastLocation = nil
|
||||
self.name = name
|
||||
self.universeId = nil
|
||||
self.placeId = nil
|
||||
self.rootPlaceId = nil
|
||||
self.gameInstanceId = nil
|
||||
|
||||
self.presence = (Players.LocalPlayer and self.id == tostring(Players.LocalPlayer.UserId))
|
||||
and User.PresenceType.ONLINE or nil
|
||||
self.thumbnails = nil
|
||||
self.lastOnline = nil
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function User.fromDataTable(data)
|
||||
local self = User.new()
|
||||
|
||||
self.id = tostring(data.id)
|
||||
self.isFriend = data.isFriend
|
||||
self.presence = (Players.LocalPlayer
|
||||
and self.id == tostring(Players.LocalPlayer.UserId)) and User.PresenceType.ONLINE or nil
|
||||
self.isFetching = false
|
||||
self.lastLocation = nil
|
||||
self.name = data.name
|
||||
self.displayName = data.displayName or data.name
|
||||
self.universeId = nil
|
||||
self.placeId = nil
|
||||
self.rootPlaceId = nil
|
||||
self.gameInstanceId = nil
|
||||
self.thumbnails = nil
|
||||
self.lastOnline = nil
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function User.compare(user1, user2)
|
||||
assert(not(user1 == nil and user2 == nil))
|
||||
assert(user1 == nil or typeof(user1) == "table")
|
||||
assert(user2 == nil or typeof(user2) == "table")
|
||||
|
||||
-- Return false if any of the provided input is nil(empty).
|
||||
if not user1 or not user2 then
|
||||
return false
|
||||
end
|
||||
|
||||
for field, valueInUser2 in pairs(user2) do
|
||||
if user1[field] ~= valueInUser2 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
for field, valueInUser1 in pairs(user1) do
|
||||
if user2[field] ~= valueInUser1 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function User.userPresenceToText(localization, user)
|
||||
local presence = user.presence
|
||||
local lastLocation = user.lastLocation
|
||||
|
||||
if not presence then
|
||||
return ''
|
||||
end
|
||||
|
||||
if presence == User.PresenceType.OFFLINE then
|
||||
return localization:Format("Common.Presence.Label.Offline")
|
||||
elseif presence == User.PresenceType.ONLINE then
|
||||
return localization:Format("Common.Presence.Label.Online")
|
||||
elseif (presence == User.PresenceType.IN_GAME) or (presence == User.PresenceType.IN_STUDIO) then
|
||||
if lastLocation ~= nil then
|
||||
return lastLocation
|
||||
else
|
||||
return localization:Format("Common.Presence.Label.Online")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return User
|
||||
@@ -0,0 +1,98 @@
|
||||
return function()
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
|
||||
local User = require(CorePackages.AppTempCommon.LuaApp.Models.User)
|
||||
|
||||
it("should detect if provided users are identical", function()
|
||||
local clone1 = User.fromData(1, "Andy", true)
|
||||
local clone2 = Immutable.Set(clone1, "isFriend", true)
|
||||
|
||||
local result = User.compare(clone1, clone2)
|
||||
expect(result).to.equal(true)
|
||||
|
||||
result = User.compare(clone2, clone1)
|
||||
expect(result).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should detect when there is one or more fields with different values", function()
|
||||
local andy = User.fromData(1, "Andy", true)
|
||||
local ollie = Immutable.Set(andy, "name", "Ollie")
|
||||
|
||||
local result = User.compare(andy, ollie)
|
||||
expect(result).to.equal(false)
|
||||
|
||||
result = User.compare(ollie, andy)
|
||||
expect(result).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should detect descrepancy when one user model contains more fields than the other", function()
|
||||
local andy = User.fromData(1, "Andy", true)
|
||||
local secretlyNotAndy = Immutable.Set(andy, "someDifferentField", "I'm Ollie!")
|
||||
|
||||
local result = User.compare(andy, secretlyNotAndy)
|
||||
expect(result).to.equal(false)
|
||||
|
||||
result = User.compare(secretlyNotAndy, andy)
|
||||
expect(result).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should throw if invalid input is provided", function()
|
||||
local aString = "I'm not a table."
|
||||
local teddy = User.fromData(1, "Teddy", true)
|
||||
|
||||
expect(function() User.compare(nil, nil) end).to.throw()
|
||||
expect(function() User.compare(aString, nil) end).to.throw()
|
||||
expect(function() User.compare(nil, aString) end).to.throw()
|
||||
expect(function() User.compare(aString, aString) end).to.throw()
|
||||
expect(function() User.compare(teddy, aString) end).to.throw()
|
||||
expect(function() User.compare(aString, teddy) end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return false if any one of the input is empty or nil)", function()
|
||||
local emptyTable = {}
|
||||
local teddy = User.fromData(1, "Teddy", true)
|
||||
|
||||
local result = User.compare(teddy, nil)
|
||||
expect(result).to.equal(false)
|
||||
|
||||
result = User.compare(nil, teddy)
|
||||
expect(result).to.equal(false)
|
||||
|
||||
result = User.compare(teddy, emptyTable)
|
||||
expect(result).to.equal(false)
|
||||
|
||||
result = User.compare(emptyTable, teddy)
|
||||
expect(result).to.equal(false)
|
||||
end)
|
||||
|
||||
describe("fromDataTable", function()
|
||||
it("should properly set user data", function()
|
||||
local data = {
|
||||
id = 1,
|
||||
name = "FooBar",
|
||||
displayName = "FooBar+DN",
|
||||
isFriend = false,
|
||||
}
|
||||
local user = User.fromDataTable(data)
|
||||
|
||||
expect(user.id).to.equal("1")
|
||||
expect(user.name).to.equal("FooBar")
|
||||
expect(user.displayName).to.equal("FooBar+DN")
|
||||
expect(user.isFriend).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should still set user data without a displayName property", function()
|
||||
local data = {
|
||||
id = 1,
|
||||
name = "FooBar",
|
||||
isFriend = false,
|
||||
}
|
||||
local user = User.fromDataTable(data)
|
||||
|
||||
expect(user.id).to.equal("1")
|
||||
expect(user.name).to.equal("FooBar")
|
||||
expect(user.displayName).to.equal("FooBar")
|
||||
expect(user.isFriend).to.equal(false)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
local percentReporting = tonumber(settings():GetFVariable("PercentReportingNetworkProfileAfterStartup"))
|
||||
|
||||
local FEATURE_NAME = "NetworkProfileDuringStartup"
|
||||
local QUEUED_MEASURE_NAME = "Queued"
|
||||
local NAME_LOOKUP_MEASURE_NAME = "NameLookup"
|
||||
local CONNECT_MEASURE_NAME = "Connect"
|
||||
local SSL_HANDSHAKE_MEASURE_NAME = "SSLHandshake"
|
||||
local MAKE_REQUEST_MEASURE_NAME = "MakeRequest"
|
||||
local RECEIVE_RESPONSE_MEASURE_NAME = "ReceiveResponse"
|
||||
|
||||
local NetworkProfiler = {}
|
||||
NetworkProfiler.__index = NetworkProfiler
|
||||
|
||||
NetworkProfiler.aggregate = {
|
||||
queued = 0.0,
|
||||
nameLookup = 0.0,
|
||||
connect = 0.0,
|
||||
sslHandshake = 0.0,
|
||||
makeRequest = 0.0,
|
||||
receiveResponse = 0.0,
|
||||
}
|
||||
|
||||
function NetworkProfiler:track(timeProfile)
|
||||
self.aggregate.queued = self.aggregate.queued + timeProfile.queued
|
||||
if timeProfile.nameLookup >= 0 then
|
||||
self.aggregate.nameLookup = self.aggregate.nameLookup + timeProfile.nameLookup
|
||||
end
|
||||
if timeProfile.connect >= 0 then
|
||||
self.aggregate.connect = self.aggregate.connect + timeProfile.connect
|
||||
end
|
||||
if timeProfile.sslHandshake >= 0 then
|
||||
self.aggregate.sslHandshake = self.aggregate.sslHandshake + timeProfile.sslHandshake
|
||||
end
|
||||
if timeProfile.makeRequest >= 0 then
|
||||
self.aggregate.makeRequest = self.aggregate.makeRequest + timeProfile.makeRequest
|
||||
end
|
||||
if timeProfile.receiveResponse >= 0 then
|
||||
self.aggregate.receiveResponse = self.aggregate.receiveResponse + timeProfile.receiveResponse
|
||||
end
|
||||
end
|
||||
|
||||
function NetworkProfiler:report(reportToDiag)
|
||||
reportToDiag(FEATURE_NAME, QUEUED_MEASURE_NAME, self.aggregate.queued, percentReporting)
|
||||
reportToDiag(FEATURE_NAME, NAME_LOOKUP_MEASURE_NAME, self.aggregate.nameLookup, percentReporting)
|
||||
reportToDiag(FEATURE_NAME, CONNECT_MEASURE_NAME, self.aggregate.connect, percentReporting)
|
||||
reportToDiag(FEATURE_NAME, SSL_HANDSHAKE_MEASURE_NAME, self.aggregate.sslHandshake, percentReporting)
|
||||
reportToDiag(FEATURE_NAME, MAKE_REQUEST_MEASURE_NAME, self.aggregate.makeRequest, percentReporting)
|
||||
reportToDiag(FEATURE_NAME, RECEIVE_RESPONSE_MEASURE_NAME, self.aggregate.receiveResponse, percentReporting)
|
||||
end
|
||||
|
||||
return NetworkProfiler
|
||||
@@ -0,0 +1,8 @@
|
||||
-----------------------------------------------------------------------------
|
||||
--- ---
|
||||
--- Under Migration to CorePackages ---
|
||||
--- ---
|
||||
-----------------------------------------------------------------------------
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
return require(CorePackages.Promise)
|
||||
@@ -0,0 +1,91 @@
|
||||
--[[
|
||||
Provides utility functions for Promises
|
||||
]]
|
||||
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Result = require(CorePackages.AppTempCommon.LuaApp.Result)
|
||||
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
|
||||
|
||||
local PromiseUtilities = {}
|
||||
|
||||
--[[
|
||||
Accept a table of promises;
|
||||
promises = {
|
||||
[1] = Promise.resolve(),
|
||||
["Home"] = Promise.reject(),
|
||||
...
|
||||
}
|
||||
Returns a new promise that:
|
||||
* is resolved when all input promises are finished.
|
||||
returns the results of each individual promises in a list of Results
|
||||
results = {
|
||||
[1] = Result1,
|
||||
["Home"] = Result2,
|
||||
...
|
||||
}
|
||||
* is never rejected.
|
||||
]]
|
||||
function PromiseUtilities.Batch(promises)
|
||||
assert(type(promises) == "table", "PromiseUtilities expects a list of Promises!")
|
||||
|
||||
local numberOfPromises = 0
|
||||
|
||||
for _, promise in pairs(promises) do
|
||||
assert(Promise.is(promise), "PromiseUtilities expects a list of Promises!")
|
||||
numberOfPromises = numberOfPromises + 1
|
||||
end
|
||||
|
||||
return Promise.new(function(resolve, reject)
|
||||
local totalCompleted = 0
|
||||
local results = {}
|
||||
|
||||
local function promiseCompleted(key, success, value)
|
||||
results[key] = Result.new(success, value)
|
||||
totalCompleted = totalCompleted + 1
|
||||
|
||||
if totalCompleted == numberOfPromises then
|
||||
resolve(results)
|
||||
end
|
||||
end
|
||||
|
||||
if next(promises) == nil then
|
||||
resolve(results)
|
||||
end
|
||||
|
||||
for key, promise in pairs(promises) do
|
||||
promise:andThen(
|
||||
function(result, ...)
|
||||
if select("#", ...) > 0 then
|
||||
warn("Promises in PromiseUtilities.Batch should not return tuple")
|
||||
end
|
||||
promiseCompleted(key, true, result)
|
||||
end,
|
||||
function(reason)
|
||||
promiseCompleted(key, false, reason)
|
||||
end
|
||||
)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function PromiseUtilities.CountResults(batchPromiseResults)
|
||||
local totalCount = 0
|
||||
local failureCount = 0
|
||||
|
||||
for _, result in pairs(batchPromiseResults) do
|
||||
local success, _ = result:unwrap()
|
||||
if not success then
|
||||
failureCount = failureCount + 1
|
||||
end
|
||||
totalCount = totalCount + 1
|
||||
end
|
||||
|
||||
return {
|
||||
successCount = totalCount - failureCount,
|
||||
failureCount = failureCount,
|
||||
totalCount = totalCount,
|
||||
}
|
||||
end
|
||||
|
||||
return PromiseUtilities
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
return function()
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local PromiseUtilities = require(CorePackages.AppTempCommon.LuaApp.PromiseUtilities)
|
||||
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
|
||||
local Result = require(CorePackages.AppTempCommon.LuaApp.Result)
|
||||
local TableUtilities = require(CorePackages.AppTempCommon.LuaApp.TableUtilities)
|
||||
|
||||
describe("PromiseUtilities.Batch", function()
|
||||
it("should assert if input is not a list of Promises", function()
|
||||
expect(function()
|
||||
PromiseUtilities.Batch()
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
PromiseUtilities.Batch(Promise.resolve(), Promise.resolve())
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
PromiseUtilities.Batch({
|
||||
Promise.resolve(),
|
||||
"something else"
|
||||
})
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should invoke the given resolve callback when all promises are finished", function()
|
||||
local promises = {
|
||||
[1] = Promise.resolve(),
|
||||
["Home"] = Promise.resolve()
|
||||
}
|
||||
local callCount = 0
|
||||
|
||||
local batchedPromise = PromiseUtilities.Batch(promises):andThen(
|
||||
function()
|
||||
callCount = callCount + 1
|
||||
end
|
||||
)
|
||||
|
||||
expect(batchedPromise).to.be.ok()
|
||||
expect(callCount).to.equal(1)
|
||||
expect(batchedPromise._status).to.equal(Promise.Status.Resolved)
|
||||
end)
|
||||
|
||||
it("should not invoke any callbacks when one of the promises are not finished", function()
|
||||
local promises = {
|
||||
[1] = Promise.resolve(),
|
||||
["Home"] = Promise.new(function() end)
|
||||
}
|
||||
local callCount = 0
|
||||
|
||||
local batchedPromise = PromiseUtilities.Batch(promises):andThen(
|
||||
function()
|
||||
callCount = callCount + 1
|
||||
end,
|
||||
|
||||
function()
|
||||
callCount = callCount + 1
|
||||
end
|
||||
)
|
||||
|
||||
expect(batchedPromise).to.be.ok()
|
||||
expect(callCount).to.equal(0)
|
||||
expect(batchedPromise._status).to.equal(Promise.Status.Started)
|
||||
end)
|
||||
|
||||
it("should return the correct results of each individual promise", function()
|
||||
local promises = {
|
||||
[1] = Promise.resolve(5),
|
||||
["Home"] = Promise.reject("failed")
|
||||
}
|
||||
local promiseResults = nil
|
||||
|
||||
local batchedPromise = PromiseUtilities.Batch(promises):andThen(
|
||||
function(results)
|
||||
promiseResults = results
|
||||
end
|
||||
)
|
||||
|
||||
expect(batchedPromise).to.be.ok()
|
||||
expect(batchedPromise._status).to.equal(Promise.Status.Resolved)
|
||||
expect(TableUtilities.FieldCount(promiseResults)).to.equal(2)
|
||||
|
||||
expect(Result.is(promiseResults[1])).to.equal(true)
|
||||
local success1, value1 = promiseResults[1]:unwrap()
|
||||
expect(success1).to.equal(true)
|
||||
expect(value1).to.equal(5)
|
||||
local isMatchCalled1 = false
|
||||
promiseResults[1]:match(function(result)
|
||||
expect(result).to.equal(5)
|
||||
isMatchCalled1 = true
|
||||
end,
|
||||
function()
|
||||
error("should not be called")
|
||||
end)
|
||||
expect(isMatchCalled1).to.equal(true)
|
||||
|
||||
|
||||
expect(Result.is(promiseResults["Home"])).to.equal(true)
|
||||
local success2, value2 = promiseResults["Home"]:unwrap()
|
||||
expect(success2).to.equal(false)
|
||||
expect(value2).to.equal("failed")
|
||||
local isMatchCalled2 = false
|
||||
promiseResults["Home"]:match(function()
|
||||
error("should not be called")
|
||||
end,
|
||||
function(err)
|
||||
expect(err).to.equal("failed")
|
||||
isMatchCalled2 = true
|
||||
end)
|
||||
expect(isMatchCalled2).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should return the correct results of each individual promise that resolved later", function()
|
||||
local resolveLater
|
||||
local rejectLater
|
||||
|
||||
local promises = {
|
||||
[1] = Promise.new(function(resolve)
|
||||
resolveLater = resolve
|
||||
end),
|
||||
["Home"] = Promise.new(function(_, reject)
|
||||
rejectLater = reject
|
||||
end)
|
||||
}
|
||||
local promiseResults = nil
|
||||
|
||||
local batchedPromise = PromiseUtilities.Batch(promises):andThen(
|
||||
function(results)
|
||||
promiseResults = results
|
||||
end
|
||||
)
|
||||
|
||||
resolveLater(5)
|
||||
rejectLater("failed")
|
||||
|
||||
expect(batchedPromise).to.be.ok()
|
||||
expect(batchedPromise._status).to.equal(Promise.Status.Resolved)
|
||||
expect(TableUtilities.FieldCount(promiseResults)).to.equal(2)
|
||||
|
||||
expect(Result.is(promiseResults[1])).to.equal(true)
|
||||
local success1, value1 = promiseResults[1]:unwrap()
|
||||
expect(success1).to.equal(true)
|
||||
expect(value1).to.equal(5)
|
||||
|
||||
expect(Result.is(promiseResults["Home"])).to.equal(true)
|
||||
local success2, value2 = promiseResults["Home"]:unwrap()
|
||||
expect(success2).to.equal(false)
|
||||
expect(value2).to.equal("failed")
|
||||
end)
|
||||
|
||||
it("should resolve if given an empty list of promises", function()
|
||||
local emptyPromises = {}
|
||||
local callCount = 0
|
||||
|
||||
local batchedPromise = PromiseUtilities.Batch(emptyPromises):andThen(
|
||||
function(results)
|
||||
callCount = callCount + 1
|
||||
end
|
||||
)
|
||||
|
||||
expect(batchedPromise).to.be.ok()
|
||||
expect(callCount).to.equal(1)
|
||||
expect(batchedPromise._status).to.equal(Promise.Status.Resolved)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("PromiseUtilities.CountResults", function()
|
||||
it("should count the results correctly", function()
|
||||
local emptyResults = {}
|
||||
|
||||
local countResult = PromiseUtilities.CountResults(emptyResults)
|
||||
|
||||
expect(countResult).to.be.ok()
|
||||
expect(countResult.successCount).to.equal(0)
|
||||
expect(countResult.failureCount).to.equal(0)
|
||||
expect(countResult.totalCount).to.equal(0)
|
||||
|
||||
local promiseResults = { Result.success(0), Result.success(0), Result.error(1) }
|
||||
|
||||
countResult = PromiseUtilities.CountResults(promiseResults)
|
||||
|
||||
expect(countResult).to.be.ok()
|
||||
expect(countResult.successCount).to.equal(2)
|
||||
expect(countResult.failureCount).to.equal(1)
|
||||
expect(countResult.totalCount).to.equal(3)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"language": {
|
||||
"mode": "nonstrict"
|
||||
},
|
||||
"lint": {
|
||||
"ImplicitReturn": "fatal"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Rodux = require(CorePackages.Rodux)
|
||||
|
||||
local ReceivedUserCountryCode = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedUserCountryCode)
|
||||
|
||||
local DEFAULT_STATE = ""
|
||||
return Rodux.createReducer(DEFAULT_STATE, {
|
||||
[ReceivedUserCountryCode.name] = function(state, action)
|
||||
return action.countryCode
|
||||
end,
|
||||
})
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
return function()
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local ReceivedUserCountryCode = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedUserCountryCode)
|
||||
local CountryCodeReducer = require(CorePackages.AppTempCommon.LuaApp.Reducers.CountryCode)
|
||||
|
||||
describe("CountryCode", function()
|
||||
it("should be and empty string by default", function()
|
||||
local state = CountryCodeReducer(nil, {})
|
||||
|
||||
expect(state).to.equal("")
|
||||
end)
|
||||
|
||||
it("should not be modified by other actions", function()
|
||||
local oldState = CountryCodeReducer(nil, {})
|
||||
local newState = CountryCodeReducer(oldState, { type = "not a real action" })
|
||||
|
||||
expect(newState).to.equal(oldState)
|
||||
end)
|
||||
|
||||
it("should be changed using ReceivedUserCountryCode", function()
|
||||
local state = CountryCodeReducer(nil, {})
|
||||
|
||||
state = CountryCodeReducer(state, ReceivedUserCountryCode("US"))
|
||||
expect(state).to.equal("US")
|
||||
|
||||
state = CountryCodeReducer(state, ReceivedUserCountryCode(""))
|
||||
expect(state).to.equal("")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local UpdateFetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Actions.UpdateFetchingStatus)
|
||||
local Cryo = require(CorePackages.Cryo)
|
||||
|
||||
return function(state, action)
|
||||
state = state or {}
|
||||
|
||||
if action.type == UpdateFetchingStatus.name then
|
||||
local key = action.key
|
||||
local status = action.status
|
||||
local value
|
||||
if status ~= nil then
|
||||
value = status
|
||||
else
|
||||
value = Cryo.None
|
||||
end
|
||||
|
||||
state = Cryo.Dictionary.join(
|
||||
state,
|
||||
{
|
||||
[key] = value,
|
||||
}
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
return function()
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local UpdateFetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Actions.UpdateFetchingStatus)
|
||||
local FetchingStatusReducer = require(CorePackages.AppTempCommon.LuaApp.Reducers.FetchingStatus)
|
||||
local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus)
|
||||
local TableUtilities = require(CorePackages.AppTempCommon.LuaApp.TableUtilities)
|
||||
|
||||
local KEY_1 = "key_1"
|
||||
local KEY_2 = "key_2"
|
||||
|
||||
describe("FetchingStatus", function()
|
||||
it("should be empty by default", function()
|
||||
local state = FetchingStatusReducer(nil, {})
|
||||
|
||||
expect(TableUtilities.FieldCount(state)).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should not be modified by other actions", function()
|
||||
local oldState = FetchingStatusReducer(nil, {})
|
||||
local newState = FetchingStatusReducer(oldState, { type = "not a real action" })
|
||||
|
||||
expect(newState).to.equal(oldState)
|
||||
end)
|
||||
|
||||
it("should be changed using UpdateFetchingStatus", function()
|
||||
local state = FetchingStatusReducer(nil, {})
|
||||
|
||||
state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, RetrievalStatus.Fetching))
|
||||
expect(state[KEY_1]).to.equal(RetrievalStatus.Fetching)
|
||||
|
||||
state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, RetrievalStatus.Failed))
|
||||
expect(state[KEY_1]).to.equal(RetrievalStatus.Failed)
|
||||
end)
|
||||
|
||||
it("should store different values for different keys", function()
|
||||
local state = FetchingStatusReducer(nil, {})
|
||||
|
||||
state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, RetrievalStatus.Failed))
|
||||
state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_2, RetrievalStatus.Done))
|
||||
|
||||
expect(state[KEY_1]).to.equal(RetrievalStatus.Failed)
|
||||
expect(state[KEY_2]).to.equal(RetrievalStatus.Done)
|
||||
end)
|
||||
|
||||
it("should clear values for nil keys", function()
|
||||
local state = { [KEY_1] = RetrievalStatus.Fetching }
|
||||
|
||||
state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, nil))
|
||||
expect(state[KEY_1]).to.equal(nil)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,43 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
|
||||
local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus)
|
||||
|
||||
local FetchUserFriendsStarted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsStarted)
|
||||
local FetchUserFriendsFailed = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsFailed)
|
||||
local FetchUserFriendsCompleted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsCompleted)
|
||||
|
||||
local function setFieldPerUser(state, fieldName, userId, value)
|
||||
local field = state[fieldName] or {}
|
||||
return Immutable.JoinDictionaries(state, {
|
||||
[fieldName] = Immutable.JoinDictionaries(field, {
|
||||
[userId] = value
|
||||
})
|
||||
})
|
||||
end
|
||||
|
||||
local function setRetrievalStatus(state, userId, status)
|
||||
return setFieldPerUser(state, "retrievalStatus", userId, status)
|
||||
end
|
||||
|
||||
local function setRetrievalFailureResponse(state, userId, response)
|
||||
return setFieldPerUser(state, "retrievalFailureResponse", userId, response)
|
||||
end
|
||||
|
||||
return function(state, action)
|
||||
state = state or {
|
||||
retrievalStatus = {},
|
||||
retrievalFailureResponse = {},
|
||||
}
|
||||
|
||||
if action.type == FetchUserFriendsStarted.name then
|
||||
state = setRetrievalStatus(state, action.userId, RetrievalStatus.Fetching)
|
||||
elseif action.type == FetchUserFriendsFailed.name then
|
||||
state = setRetrievalStatus(state, action.userId, RetrievalStatus.Failed)
|
||||
state = setRetrievalFailureResponse(state, action.userId, action.response)
|
||||
elseif action.type == FetchUserFriendsCompleted.name then
|
||||
state = setRetrievalStatus(state, action.userId, RetrievalStatus.Done)
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
|
||||
local ReceivedPlacesInfos = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedPlacesInfos)
|
||||
|
||||
local LuaAppFlags = CorePackages.AppTempCommon.LuaApp.Flags
|
||||
local convertUniverseIdToString = require(LuaAppFlags.ConvertUniverseIdToString)
|
||||
|
||||
return function(state, action)
|
||||
state = state or {}
|
||||
|
||||
if action.type == ReceivedPlacesInfos.name then
|
||||
for _, placeInfo in pairs(action.placesInfos) do
|
||||
local universeId = convertUniverseIdToString(placeInfo.universeId)
|
||||
|
||||
state = Immutable.Set(state, universeId, placeInfo)
|
||||
end
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user