This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_cryo"]["cryo"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["freeze"]["freeze"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["lua-promise"]["lua-promise"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_rodux"]["rodux"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
# Generated by Rotriever. Format subject to change in future releases.
name = "rodux-networking"
version = "1.0.0"
commit = "ea19cfe3c71e5747fa3ab4235beed4fc21d63e4d"
source = "git+https://github.rbx.com/roblox/rodux-networking#v1.0.1"
dependencies = [
"Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo",
"Freeze freeze ef89f9d0 git+https://github.rbx.com/roblox/freeze#master",
"Promise lua-promise bbb9e162 git+https://github.rbx.com/roblox/lua-promise#master",
"Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux",
"tutils tutils 0be577db git+https://github.rbx.com/roblox/tutils#master",
]
@@ -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,4 @@
local root = script.Parent
local Packages = root.Parent
return require(Packages.Cryo)
@@ -0,0 +1,4 @@
local root = script.Parent
local Packages = root.Parent
return require(Packages.Freeze)
@@ -0,0 +1,6 @@
local root = script.Parent
local makeRequestApi = require(root.makeRequestApi)
return function(options)
return makeRequestApi(options, "GET")
end
@@ -0,0 +1,24 @@
return function()
local root = script.Parent
local mockNetworkImpl = function()
return nil
end
local GET = require(root.GET)({
keyPath = "hello.world",
networkImpl = mockNetworkImpl,
})
describe("WHEN invoked", function()
local endpoint = GET(script, function(requestBuilder)
return requestBuilder("example.com")
end)
it("SHOULD return an object have all expected fields", function()
expect(endpoint.API).to.be.ok()
expect(endpoint.getStatus).to.be.ok()
expect(endpoint.Succeeded).to.be.ok()
expect(endpoint.Failed).to.be.ok()
end)
end)
end
@@ -0,0 +1,20 @@
local EnumNetworkStatus = {}
local EnumValues = {
NotStarted = "NotStarted",
Fetching = "Fetching",
Done = "Done",
Failed = "Failed",
}
setmetatable(EnumNetworkStatus,
{
__newindex = function(t, key, index)
end,
__index = function(t, index)
assert(EnumValues[index] ~= nil, ("EnumNetworkStatus has no value: " .. tostring(index)))
return EnumValues[index]
end
})
return EnumNetworkStatus
@@ -0,0 +1,4 @@
local root = script.Parent.Parent
local Packages = root.Parent
return require(Packages.Freeze)
@@ -0,0 +1,4 @@
local root = script.Parent.Parent
local Packages = root.Parent
return require(Packages.Promise)
@@ -0,0 +1,4 @@
local root = script.Parent.Parent
local Packages = root.Parent
return require(Packages.Rodux)
@@ -0,0 +1,3 @@
return function(options)
return "networkStatus:" .. tostring(options.keyPath)
end
@@ -0,0 +1,17 @@
return function()
local buildActionName = require(script.Parent.buildActionName)
it("SHOULD concat networkStatus: to keyPath", function()
local result = buildActionName({
keyPath = "Hello",
})
expect(result).to.equal("networkStatus:Hello")
end)
it("SHOULD concat networkStatus: to keyPath with depth", function()
local result = buildActionName({
keyPath = "Hello.World",
})
expect(result).to.equal("networkStatus:Hello.World")
end)
end
@@ -0,0 +1,12 @@
return function(tab, keyPath)
local currentNode = tab
for _, key in ipairs(keyPath:split(".")) do
if not currentNode[key] then
return nil
end
currentNode = currentNode[key]
end
return currentNode
end
@@ -0,0 +1,54 @@
return function()
local getDeepValue = require(script.Parent.getDeepValue)
describe("GIVEN an empty array", function()
local tab = {}
it("SHOULD return nil", function()
expect(getDeepValue(tab, "")).to.equal(nil)
expect(getDeepValue(tab, "hello.world")).to.equal(nil)
end)
end)
describe("GIVEN a dictionary with hello.world", function()
local tab = {
hello = {
world = 100,
},
}
describe("GIVEN an empty string as the second argument", function()
it("SHOULD return nil", function()
expect(getDeepValue(tab, "")).to.equal(nil)
end)
end)
describe("GIVEN `goodbye.world` as the second argument", function()
it("SHOULD return nil", function()
expect(getDeepValue(tab, "goodbye.world")).to.equal(nil)
end)
end)
describe("GIVEN `hello.there` as the second argument", function()
it("SHOULD return nil", function()
expect(getDeepValue(tab, "hello.there")).to.equal(nil)
end)
end)
describe("GIVEN `hello.there` as the second argument", function()
it("SHOULD return nil", function()
expect(getDeepValue(tab, "hello.there")).to.equal(nil)
end)
end)
describe("GIVEN `hello` as the second argument", function()
it("SHOULD return the hello table", function()
expect(getDeepValue(tab, "hello")).to.equal(tab.hello)
end)
end)
describe("GIVEN `hello.world` as the second argument", function()
it("SHOULD return the 100 (the value mapped to hello.world)", function()
expect(getDeepValue(tab, "hello.world")).to.equal(100)
end)
end)
end)
end
@@ -0,0 +1,20 @@
local root = script.Parent
local getDeepValue = require(root.getDeepValue)
local EnumNetworkStatus = require(root.EnumNetworkStatus)
return function(options)
local keyPath = options.keyPath
return function(state, fetchStatusKey)
assert(typeof(state) == "table")
assert(typeof(fetchStatusKey) == "string")
assert(#fetchStatusKey > 0)
local reducerValue = getDeepValue(state, keyPath)
assert(reducerValue, string.format(
"Reducer not found for keyPath: %s. Did you forget to call `installReducer`?",
keyPath
))
return reducerValue:get(fetchStatusKey) or EnumNetworkStatus.NotStarted
end
end
@@ -0,0 +1,35 @@
local Freeze = require(script.Parent.Freeze)
return function()
local getStatus = require(script.Parent.getStatus)({
keyPath = "testingKeyPathStatus",
})
local EnumNetworkStatus = require(script.Parent.EnumNetworkStatus)
local TEST_KEY_1 = "item_key"
it("should return NotStarted for missing key", function()
local state = { testingKeyPathStatus = Freeze.UnorderedMap.new({}) }
local status = getStatus(state, TEST_KEY_1)
expect(status).to.equal(EnumNetworkStatus.NotStarted)
end)
it("should return matching status for state in store", function()
local statusesToTest = {
EnumNetworkStatus.NotStarted,
EnumNetworkStatus.Fetching,
EnumNetworkStatus.Done,
EnumNetworkStatus.Failed
}
for _, testStatus in ipairs(statusesToTest) do
local state = {
testingKeyPathStatus = Freeze.UnorderedMap.new({
[TEST_KEY_1] = testStatus
})
}
expect(getStatus(state, TEST_KEY_1)).to.equal(testStatus)
end
end)
end
@@ -0,0 +1,11 @@
return function(options)
return {
getStatus = require(script.getStatus)(options),
setStatus = require(script.setStatus)(options),
installReducer = require(script.installReducer)(options),
Enum = {
Status = require(script.EnumNetworkStatus),
},
}
end
@@ -0,0 +1,23 @@
local Rodux = require(script.Parent.Rodux)
local Freeze = require(script.Parent.Freeze)
local buildActionName = require(script.Parent.buildActionName)
return function(options)
return function()
return Rodux.createReducer(Freeze.UnorderedMap.new({}), {
[buildActionName(options)] = function(state, action)
local updatedStatus = {}
if #action.ids == 0 then
updatedStatus[action.keymapper()] = action.status
else
for _, id in ipairs(action.ids) do
local mappedId = action.keymapper(id)
updatedStatus[mappedId] = action.status
end
end
return state:batchSet(updatedStatus)
end,
})
end
end
@@ -0,0 +1,107 @@
return function()
local options = {
keyPath = "networkStatus"
}
local installReducer = require(script.Parent.installReducer)(options)
local buildActionName = require(script.Parent.buildActionName)
local getStatus = require(script.Parent.getStatus)(options)
local reducer = installReducer()
describe("GIVEN an action with no id", function()
local initialAction = {
type = buildActionName(options),
ids = {},
keymapper = function() return "key" end,
status = "test",
}
it("SHOULD return a new UnorderedMap with the key mapped properly", function()
local result = reducer(nil, initialAction)
expect(result).to.be.ok()
expect(result:get("key")).to.equal("test")
end)
describe("GIVEN another action that rewrites the previous key", function()
local overwriteAction = {
type = buildActionName(options),
ids = {},
keymapper = function() return "key" end,
status = "next-best-thing",
}
it("SHOULD update the key accordingly", function()
local result = reducer(reducer(nil, initialAction), overwriteAction)
expect(result).to.be.ok()
expect(result:get("key")).to.equal("next-best-thing")
end)
end)
it("SHOULD be retrievable with getStatus", function()
local state = {
networkStatus = reducer(nil, initialAction),
}
local result = getStatus(state, "key")
expect(result).to.equal("test")
end)
end)
describe("GIVEN an action with one id", function()
local initialAction = {
type = buildActionName(options),
ids = { "123" },
keymapper = function(id) return "key:" .. id end,
status = "test",
}
it("SHOULD return a new UnorderedMap with the key mapped properly", function()
local result = reducer(nil, initialAction)
expect(result).to.be.ok()
expect(result:get("key:123")).to.equal("test")
end)
describe("GIVEN another action that rewrites the previous key", function()
local overwriteAction = {
type = buildActionName(options),
ids = { "123" },
keymapper = function(id) return "key:" .. id end,
status = "next-best-thing",
}
it("SHOULD update the key accordingly", function()
local result = reducer(reducer(nil, initialAction), overwriteAction)
expect(result).to.be.ok()
expect(result:get("key:123")).to.equal("next-best-thing")
end)
end)
it("SHOULD be retrievable with getStatus", function()
local state = {
networkStatus = reducer(nil, initialAction),
}
local result = getStatus(state, "key:123")
expect(result).to.equal("test")
end)
end)
describe("GIVEN an action with multiple ids", function()
local batchAction = {
type = buildActionName(options),
ids = { "123", "456", "789" },
keymapper = function(id) return "key:" .. id end,
status = "the-same-status",
}
it("SHOULD update all keys to the same status", function()
local result = reducer(nil, batchAction)
expect(result).to.be.ok()
expect(result:get("key:123")).to.equal("the-same-status")
expect(result:get("key:456")).to.equal("the-same-status")
expect(result:get("key:789")).to.equal("the-same-status")
end)
end)
end
@@ -0,0 +1,41 @@
local EnumNetworkStatus = require(script.Parent.EnumNetworkStatus)
local buildActionName = require(script.Parent.buildActionName)
return function(options)
local getStatus = require(script.Parent.getStatus)(options)
local function actionCreator(ids, keymapper, status)
return {
ids = ids,
keymapper = keymapper,
status = status,
type = buildActionName(options),
}
end
local function filter(state, ids, keymapper)
local filteredIds = {}
for _, id in ipairs(ids) do
local status = getStatus(state, keymapper(id))
if status ~= EnumNetworkStatus.Fetching then
table.insert(filteredIds, id)
end
end
return filteredIds
end
return function(store, ids, keymapper, promiseFunction)
local filteredIds = filter(store:getState(), ids, keymapper)
store:dispatch(actionCreator(filteredIds, keymapper, EnumNetworkStatus.Fetching))
return promiseFunction(store, filteredIds):andThen(function(result)
store:dispatch(actionCreator(filteredIds, keymapper, EnumNetworkStatus.Done))
return result
end,
function(errorString)
store:dispatch(actionCreator(filteredIds, keymapper, EnumNetworkStatus.Failed))
error(errorString)
end)
end
end
@@ -0,0 +1,85 @@
return function()
local Freeze = require(script.Parent.Freeze)
local EnumNetworkStatus = require(script.Parent.EnumNetworkStatus)
local Promise = require(script.Parent.Promise)
local setStatus = require(script.Parent.setStatus)({
keyPath = "testingKeyPath",
})
local mockStore = require(script.Parent.Parent.mockStore)
describe("GIVEN a store", function()
local actionHistory = {}
local roduxStore = mockStore.config({
dispatch = function(self, action)
if type(action) == "function" then
action(self)
else
table.insert(actionHistory, action)
end
end,
})
describe("GIVEN multiple ids with a valid keymapper and promise function", function()
local ids = { "123", "456", "789" }
local keymapper = function(id)
return "key" .. tostring(id)
end
describe("WHEN there are no ongoing requests", function()
it("SHOULD not filter any ids", function()
roduxStore:setState({
testingKeyPath = Freeze.UnorderedMap.new({}),
})
local promiseFunction = function(store, filteredIds)
expect(store).to.be.ok()
expect(#filteredIds).to.equal(#ids)
return Promise.resolve(filteredIds)
end
setStatus(roduxStore, ids, keymapper, promiseFunction)
end)
it("SHOULD only dispatch relevant actions", function()
expect(#actionHistory).to.equal(2)
local secondToLastAction = actionHistory[#actionHistory - 1]
expect(secondToLastAction).to.be.ok()
expect(type(secondToLastAction)).to.equal("table")
expect(secondToLastAction.type).to.equal("networkStatus:testingKeyPath")
expect(secondToLastAction.status).to.equal(EnumNetworkStatus.Fetching)
local lastAction = actionHistory[#actionHistory]
expect(lastAction).to.be.ok()
expect(type(lastAction)).to.equal("table")
expect(lastAction.type).to.equal("networkStatus:testingKeyPath")
expect(lastAction.status).to.equal(EnumNetworkStatus.Done)
end)
end)
describe("WHEN there is already an ongoing request", function()
it("SHOULD filter the ids that are currently ongoing", function()
actionHistory = {}
roduxStore:setState({
testingKeyPath = Freeze.UnorderedMap.new({
[keymapper("123")] = EnumNetworkStatus.Fetching,
[keymapper("456")] = EnumNetworkStatus.Fetching,
[keymapper("789")] = EnumNetworkStatus.Fetching,
}),
})
local promiseFunction = function(store, filteredIds)
expect(store).to.be.ok()
expect(#filteredIds).to.equal(0)
return Promise.resolve(filteredIds)
end
setStatus(roduxStore, ids, keymapper, promiseFunction)
end)
it("SHOULD still fire any actions if there are no ids", function()
expect(#actionHistory).to.equal(2)
end)
end)
end)
end)
end
@@ -0,0 +1,6 @@
local root = script.Parent
local makeRequestApi = require(root.makeRequestApi)
return function(options)
return makeRequestApi(options, "POST")
end
@@ -0,0 +1,24 @@
return function()
local root = script.Parent
local mockNetworkImpl = function()
return nil
end
local POST = require(root.POST)({
keyPath = "hello.world",
networkImpl = mockNetworkImpl,
})
describe("WHEN invoked", function()
local endpoint = POST(script, function(requestBuilder)
return requestBuilder("example.com")
end)
it("SHOULD return an object have all expected fields", function()
expect(endpoint.API).to.be.ok()
expect(endpoint.getStatus).to.be.ok()
expect(endpoint.Succeeded).to.be.ok()
expect(endpoint.Failed).to.be.ok()
end)
end)
end
@@ -0,0 +1,4 @@
local root = script.Parent
local Packages = root.Parent
return require(Packages.Promise)
@@ -0,0 +1,4 @@
local root = script.Parent.Parent
local Packages = root.Parent
return require(Packages.Cryo)
@@ -0,0 +1,139 @@
local root = script.Parent
local Cryo = require(root.Cryo)
local UriIds = {}
UriIds.__index = UriIds
function UriIds:new(ids, delimiter)
return setmetatable({
ids = UriIds.makeArray(ids),
delimiter = delimiter
}, self)
end
function UriIds:setIds(ids)
self.ids = UriIds.makeArray(ids)
end
function UriIds.makeArray(ids)
if type(ids) == "table" then
return ids
end
return {ids}
end
function UriIds:__tostring()
return table.concat(self.ids, self.delimiter)
end
local RequestBuilder = {}
RequestBuilder.__index = RequestBuilder
function RequestBuilder:new(baseUrl)
return setmetatable({
baseUrl = baseUrl,
keyMapper = nil,
args = {},
pathElements = {},
configurableIds = nil,
namedIds = {},
idsDelimiter = ";",
options = {},
}, self)
end
function RequestBuilder:path(path)
table.insert(self.pathElements, path)
return self
end
function RequestBuilder:id(ids, key)
if not key and #self.pathElements < 1 then
warn("Cannot name id or ids because there is no leading path segment and no name is provided")
end
local name = key or self.pathElements[#self.pathElements]
self.namedIds[name] = ids
self.configurableIds = UriIds:new(ids, self.idsDelimiter)
table.insert(self.pathElements, self.configurableIds)
return self
end
function RequestBuilder:queryArgWithIds(argName, ids)
self.namedIds[argName] = ids
self.configurableIds = UriIds:new(ids, self.idsDelimiter)
self:queryArgs({
[argName] = self.configurableIds
})
return self
end
function RequestBuilder:queryArgs(args)
self.args = Cryo.Dictionary.join(self.args, args)
return self
end
function RequestBuilder:body(dictionary)
self.options.postBody = dictionary
return self
end
function RequestBuilder:makeKeyMapper()
return function(someId)
return self:makeUrl(someId)
end
end
function RequestBuilder:makeUri(ids)
local fullPath = ""
for _, element in ipairs(self.pathElements) do
fullPath = fullPath .. "/" .. tostring(element)
end
return fullPath
end
function RequestBuilder:makeQueryArgs(ids)
self:_plugInConfigurableIds(ids)
local argsString = ""
for k,v in pairs(self.args) do
local arg = tostring(k) .. "=" .. tostring(v)
if argsString:len() > 1 then
argsString = argsString .. "&" .. arg
else
argsString = arg
end
end
if argsString:len() > 1 then
return "?" .. argsString
end
return ""
end
function RequestBuilder:makeUrl(ids)
self:_plugInConfigurableIds(ids)
local fullUrl = self.baseUrl .. self:makeUri(ids) .. self:makeQueryArgs(ids)
return fullUrl
end
function RequestBuilder:makeOptions()
return self.options
end
function RequestBuilder:_plugInConfigurableIds(ids)
if ids ~= nil and self.configurableIds then
self.configurableIds:setIds(ids)
end
end
function RequestBuilder:getIds()
if self.configurableIds and self.configurableIds.ids then
return self.configurableIds.ids
end
return {}
end
function RequestBuilder:getNamedIds()
return self.namedIds
end
return RequestBuilder
@@ -0,0 +1,177 @@
return function()
local root = script.Parent
local RequestBuilder = require(root.RequestBuilder)
local tutils = require(root.tutils)
local baseUrl = "https://example.com"
describe("RequestBuilder basics", function()
it("builder functions should return self", function()
local builder = RequestBuilder:new()
expect(builder).to.equal(builder:path("test"))
expect(builder).to.equal(builder:id("test"))
expect(builder).to.equal(builder:queryArgs({ "test" }))
expect(builder).to.equal(builder:body({ "test" }))
end)
it("should be constructible from base URL", function()
local builder = RequestBuilder:new(baseUrl)
expect(builder).to.be.ok()
expect(builder:makeUrl()).to.equal(baseUrl)
end)
it("should be constructible from successive path calls", function()
local builder = RequestBuilder:new(baseUrl)
builder:path("some"):path("element")
expect(builder:makeUrl()).to.equal(baseUrl .. "/some/element")
expect(tutils.shallowEqual(builder:getIds(), {})).to.equal(true)
end)
it("should allow building query args", function()
local builder = RequestBuilder:new(baseUrl)
builder:queryArgs({
arg = "value"
})
expect(builder:makeUrl()).to.equal(baseUrl .. "?arg=value")
expect(tutils.shallowEqual(builder:getIds(), {})).to.equal(true)
end)
it("should allow building multiple query args", function()
local builder = RequestBuilder:new(baseUrl)
builder:queryArgs({
arg = "value"
})
builder:queryArgs({
arg2 = "value2"
})
expect(builder:makeUrl()).to.equal(baseUrl .. "?arg2=value2&arg=value")
expect(tutils.shallowEqual(builder:getIds(), {})).to.equal(true)
end)
end)
describe("RequestBuilder path ids", function()
it("should be constructible from path and single id", function()
local builder = RequestBuilder:new(baseUrl)
builder:path("some/element"):id(123)
local expectedUrl = baseUrl .. "/some/element/123"
expect(builder:makeUrl()).to.equal(expectedUrl)
expect(builder:makeKeyMapper()).to.be.ok()
expect(builder:makeKeyMapper()()).to.equal(expectedUrl)
expect(tutils.shallowEqual(builder:getIds(), {123})).to.equal(true)
end)
it("should be constructible from path and ids array", function()
local builder = RequestBuilder:new(baseUrl)
builder:path("some/element"):id({123, 321})
local staticUrlPart = baseUrl .. "/some/element/"
expect(builder:makeUrl()).to.equal(staticUrlPart .. "123;321")
expect(builder:makeKeyMapper()(123)).to.equal(staticUrlPart .. "123")
expect(builder:makeKeyMapper()(321)).to.equal(staticUrlPart .. "321")
end)
it("should allow swapping ids in makeUrl call", function()
local builder = RequestBuilder:new(baseUrl)
builder:path("some/element"):id({})
expect(builder:makeUrl({567, 789})).to.equal(baseUrl .. "/some/element/567;789")
end)
it("should allow swapping ids in makeUrl call, but only for last ids group", function()
local builder = RequestBuilder:new(baseUrl)
builder:path("some/element"):id(123):path("other"):id({})
local staticUrlPart = baseUrl .. "/some/element/123/other"
expect(builder:makeUrl(567)).to.equal(staticUrlPart .. "/567")
expect(builder:makeUrl({567})).to.equal(staticUrlPart .. "/567")
expect(builder:makeUrl({567, 789})).to.equal(staticUrlPart .. "/567;789")
expect(builder:makeKeyMapper()(567)).to.equal(staticUrlPart .. "/567")
expect(builder:makeKeyMapper()(789)).to.equal(staticUrlPart .. "/789")
end)
it("should map previous path segment to id", function()
local builder = RequestBuilder:new(baseUrl)
builder:path("pathexample"):id(444)
local namedIds = builder:getNamedIds()
expect(namedIds["pathexample"]).to.be.ok()
expect(namedIds["pathexample"]).to.equal(444)
end)
it("should map previous path segment to id with multiple path segments and ids", function()
local builder = RequestBuilder:new(baseUrl)
builder:path("firstpathexample"):id(444):path("anotherpathexample"):path("yetanotherpathexample"):id(555)
local namedIds = builder:getNamedIds()
expect(namedIds["firstpathexample"]).to.be.ok()
expect(namedIds["anotherpathexample"]).to.never.be.ok()
expect(namedIds["yetanotherpathexample"]).to.be.ok()
expect(namedIds["firstpathexample"]).to.equal(444)
expect(namedIds["yetanotherpathexample"]).to.equal(555)
end)
it("should map previous path segment to id with multiple path segments and ids, with multiple ids given to function", function()
local builder = RequestBuilder:new(baseUrl)
builder:path("firstpathexample"):id({ 333, 444 }):path("anotherpathexample"):path("yetanotherpathexample"):id(555)
local namedIds = builder:getNamedIds()
expect(namedIds["firstpathexample"]).to.be.ok()
expect(namedIds["anotherpathexample"]).to.never.be.ok()
expect(namedIds["yetanotherpathexample"]).to.be.ok()
expect(tutils.deepEqual(namedIds["firstpathexample"], { 333, 444 })).to.equal(true)
expect(namedIds["yetanotherpathexample"]).to.equal(555)
end)
it("should allow one to override the default key", function()
local builder = RequestBuilder:new(baseUrl)
local KEY1 = "KEY1"
builder:path("firstpathexample"):id({ 333, 444 }, KEY1):path("anotherpathexample"):path("yetanotherpathexample"):id(555)
local namedIds = builder:getNamedIds()
expect(namedIds["firstpathexample"]).to.never.be.ok()
expect(namedIds["anotherpathexample"]).to.never.be.ok()
expect(namedIds["yetanotherpathexample"]).to.be.ok()
expect(tutils.deepEqual(namedIds[KEY1], { 333, 444 })).to.equal(true)
expect(namedIds["yetanotherpathexample"]).to.equal(555)
end)
end)
describe("RequestBuilder query args and ids", function()
it("should allow swapping query argument ids in makeUrl call", function()
local builder = RequestBuilder:new(baseUrl)
builder:queryArgWithIds("arg", {123})
expect(builder:makeUrl()).to.equal(baseUrl .. "?arg=123")
end)
it("should allow swapping multiple query argument ids in makeUrl call", function()
local builder = RequestBuilder:new(baseUrl)
builder:queryArgWithIds("arg", {})
expect(builder:makeUrl(123)).to.equal(baseUrl .. "?arg=123")
expect(builder:makeUrl({345, 456})).to.equal(baseUrl .. "?arg=345;456")
expect(builder:makeKeyMapper()(567)).to.equal(baseUrl .. "?arg=567")
expect(builder:makeKeyMapper()({567, 321})).to.equal(baseUrl .. "?arg=567;321")
end)
it("should allow swapping query argument ids in makeUrl call and not affect path ids", function()
local builder = RequestBuilder:new(baseUrl)
builder:path("some/element"):id(999):queryArgWithIds("arg", {})
expect(builder:makeUrl(123)).to.equal(baseUrl .. "/some/element/999?arg=123")
expect(builder:makeUrl({345, 456})).to.equal(baseUrl .. "/some/element/999?arg=345;456")
end)
end)
describe("RequestBuilder body", function()
it("should replace postBody", function()
local myBody = { hi = "there" }
local builder = RequestBuilder:new(baseUrl):body(myBody)
expect(builder:makeOptions()).to.be.ok()
expect(builder:makeOptions().postBody.hi).to.equal("there")
end)
end)
end
@@ -0,0 +1 @@
return require(script.RequestBuilder)
@@ -0,0 +1,4 @@
local root = script.Parent.Parent
local Packages = root.Parent
return require(Packages.tutils)
@@ -0,0 +1,37 @@
local GET = require(script.Parent.GET)
local POST = require(script.Parent.POST)
local RoduxNetworking = {}
RoduxNetworking.__index = RoduxNetworking
function RoduxNetworking.new(options)
assert(options, "Expected options to be passed into RoduxNetworking")
assert(options.keyPath, "Expected options.keyPath to be passed into RoduxNetworking")
assert(options.networkImpl, "Expected options.networkImpl to be passed into RoduxNetworking")
local self = {
options = options,
}
return setmetatable(self, RoduxNetworking)
end
function RoduxNetworking:GET(moduleScript, constructBuilderFunction)
assert(moduleScript, "RoduxNetworking:GET expects moduleScript argument")
assert(moduleScript, "RoduxNetworking:GET expects constructBuilderFunction argument")
return GET(self.options)(moduleScript, constructBuilderFunction)
end
function RoduxNetworking:POST(...)
return POST(self.options)(...)
end
function RoduxNetworking:getNetworkImpl()
return self.options.networkImpl
end
function RoduxNetworking:setNetworkImpl(networkImpl)
self.options.networkImpl = networkImpl
end
return RoduxNetworking
@@ -0,0 +1,50 @@
return function()
local RoduxNetworking = require(script.Parent.RoduxNetworking)
local Promise = require(script.Parent.Promise)
local noOpt = function()
return Promise.resolve()
end
local moduleScript = {
Name = "moduleScript",
}
local builderConstructorFunction = function()
end
describe("GIVEN an options configuration", function()
local options = {
keyPath = "",
networkImpl = noOpt
}
it("SHOULD return a unique object when .new is called", function()
local instance1 = RoduxNetworking.new(options)
local instance2 = RoduxNetworking.new(options)
expect(instance1).to.be.ok()
expect(instance2).to.be.ok()
expect(instance1).to.never.equal(instance2)
end)
describe("API", function()
local instance = RoduxNetworking.new(options)
it("SHOULD return an object when GET is called", function()
local result = instance:GET(moduleScript, builderConstructorFunction)
expect(result).to.be.ok()
end)
it("SHOULD return an object when POST is called", function()
local result = instance:POST(moduleScript, builderConstructorFunction)
expect(result).to.be.ok()
end)
describe("WHEN setNetworkImpl is called", function()
local newNetworkImpl = function()
return Promise.reject()
end
instance:setNetworkImpl(newNetworkImpl)
it("SHOULD return the given value when getNetworkImpl is called", function()
expect(instance:getNetworkImpl()).to.equal(newNetworkImpl)
end)
end)
end)
end)
end
@@ -0,0 +1,4 @@
local root = script.Parent
local Packages = root.Parent
return require(Packages.UrlBuilder)
@@ -0,0 +1,29 @@
local RoduxNetworking = require(script.RoduxNetworking)
local NetworkStatus = require(script.NetworkStatus)
return {
config = function(options)
local roduxNetworkingInstance = RoduxNetworking.new(options)
local networkStatusInstance = NetworkStatus(options)
return {
GET = function(...)
return roduxNetworkingInstance:GET(...)
end,
POST = function(...)
return roduxNetworkingInstance:POST(...)
end,
getNetworkImpl = function()
return roduxNetworkingInstance:getNetworkImpl()
end,
setNetworkImpl = function(...)
roduxNetworkingInstance:setNetworkImpl(...)
end,
installReducer = networkStatusInstance.installReducer,
Enum = {
NetworkStatus = networkStatusInstance.Enum.Status,
},
}
end,
}
@@ -0,0 +1,21 @@
return function()
local init = require(script.Parent)
local noOpt = function()
end
describe("GIVEN an options configuration", function()
local instance = init.config({
keyPath = "hello.world",
networkImpl = noOpt,
})
it("SHOULD have all expected fields", function()
expect(instance.GET).to.be.ok()
expect(instance.POST).to.be.ok()
expect(instance.Enum).to.be.ok()
expect(instance.installReducer).to.be.ok()
expect(instance.getNetworkImpl).to.be.ok()
expect(instance.setNetworkImpl).to.be.ok()
end)
end)
end
@@ -0,0 +1,21 @@
local root = script.Parent
local Action = require(root.Action)
return function(networkRequestScript)
return {
Succeeded = Action(networkRequestScript.Name .. "_Succeeded", function(ids, responseBody, namedIds)
return {
ids = ids,
responseBody = responseBody,
namedIds = namedIds,
}
end),
Failed = Action(networkRequestScript.Name .. "_Failed", function(ids, error, namedIds)
return {
ids = ids,
error = error,
namedIds = namedIds,
}
end),
}
end
@@ -0,0 +1,13 @@
return function()
local makeActionCreator = require(script.Parent.makeActionCreator)
describe("GIVEN a script", function()
local myScript = Instance.new("ModuleScript")
local result = makeActionCreator(myScript)
it("SHOULD return an object with a success field and failed field", function()
expect(result.Succeeded).to.be.ok()
expect(result.Failed).to.be.ok()
end)
end)
end
@@ -0,0 +1,51 @@
local root = script.Parent
local makeActionCreator = require(root.makeActionCreator)
local RequestBuilder = require(root.RequestBuilder)
local NetworkStatus = require(root.NetworkStatus)
return function(options, methodType)
local keyPath = options.keyPath
local myNetworkStatus = NetworkStatus({
keyPath = keyPath,
})
return function(moduleScript, constructBuilderFunction)
local self = makeActionCreator(moduleScript)
self.API = function(...)
local userRequestBuilder = constructBuilderFunction(function(...)
return RequestBuilder:new(...)
end, ...)
return function(store)
return myNetworkStatus.setStatus(store, userRequestBuilder:getIds(), userRequestBuilder:makeKeyMapper(), function(store, filteredIds)
local networkImpl = options.networkImpl
return networkImpl(userRequestBuilder:makeUrl(filteredIds), methodType, userRequestBuilder:makeOptions()):andThen(
function(payload)
store:dispatch(self.Succeeded(filteredIds, payload.responseBody, userRequestBuilder:getNamedIds()))
return payload
end,
function(errorString)
store:dispatch(self.Failed(filteredIds, error, userRequestBuilder:getNamedIds()))
-- Throw again so we can catch it outside of library
error(errorString)
end
)
end)
end
end
self.getStatus = (methodType == "GET") and function(state, key)
local userRequestBuilder = constructBuilderFunction(function(...)
return RequestBuilder:new(...)
end, key)
local keymapper = userRequestBuilder:makeKeyMapper()
local mappedKey = keymapper(key)
return myNetworkStatus.getStatus(state, mappedKey)
end
return self
end
end
@@ -0,0 +1,121 @@
return function()
local root = script.Parent
local Freeze = require(root.Freeze)
local EnumNetworkStatus = require(root.NetworkStatus.EnumNetworkStatus)
local Promise = require(root.Promise)
local mockNetworkImpl = function(url, method, options)
return Promise.resolve({
responseBody = "benj",
})
end
local mockStore = require(root.mockStore)
local GET = require(root.makeRequestApi)({
keyPath = "hello.world",
networkImpl = mockNetworkImpl,
}, "GET")
describe("GIVEN a store", function()
local actionHistory = {}
local roduxStore = mockStore.config({
dispatch = function(self, action)
if type(action) == "function" then
action(self)
else
table.insert(actionHistory, action)
end
end,
state = {
hello = {
world = Freeze.UnorderedMap.new({}),
}
}
})
describe("GIVEN a module script and builderFunction w/out parameters", function()
local hasBuilderFunctionRun = false
local receivedChannelArgument = nil
local mockGETChannels = GET(script, function(requestBuilder, channel)
hasBuilderFunctionRun = true
receivedChannelArgument = channel
return requestBuilder("example.com"):path("v1"):path("channels"):id(channel):path("messages")
end)
describe("GIVEN no parameters for a url", function()
it("SHOULD return a valid thunk", function()
local thunk = mockGETChannels.API("weather-channel")
expect(type(thunk)).to.equal("function")
end)
describe("WHEN thunk is dispatched", function()
it("SHOULD invoke builder function given to GET constructor", function()
roduxStore:dispatch(mockGETChannels.API("announcements"))
expect(hasBuilderFunctionRun).to.equal(true)
expect(receivedChannelArgument).to.equal("announcements")
end)
it("SHOULD dispatch network status actions and payload action", function()
actionHistory = {}
roduxStore:dispatch(mockGETChannels.API("announcements"))
local action1 = actionHistory[1]
expect(action1).to.be.ok()
expect(action1.type).to.equal("networkStatus:hello.world")
expect(action1.status).to.equal(EnumNetworkStatus.Fetching)
end)
end)
end)
describe("Action Creators", function()
it("SHOULD have a Succeeded action creator", function()
expect(mockGETChannels.Succeeded).to.be.ok()
local action = mockGETChannels.Succeeded({}, {}, {})
expect(action).to.be.ok()
expect(action.ids).to.be.ok()
expect(action.responseBody).to.be.ok()
expect(action.namedIds).to.be.ok()
end)
it("SHOULD have a Failed action creator", function()
expect(mockGETChannels.Failed).to.be.ok()
local action = mockGETChannels.Failed({}, {}, {})
expect(action).to.be.ok()
expect(action.ids).to.be.ok()
expect(action.error).to.be.ok()
expect(action.namedIds).to.be.ok()
end)
end)
describe("getStatus", function()
it("SHOULD return Done for successful network responses", function()
local store = mockStore.config({
state = {
hello = {
world = Freeze.UnorderedMap.new({
-- key mapper would usually generate this
["example.com/v1/channels/faq/messages"] = "testingStatus",
}),
}
}
})
local status = mockGETChannels.getStatus(store:getState(), "faq")
expect(status).to.equal("testingStatus")
end)
it("SHOULD throw for non-GET request types", function()
local POST = require(root.makeRequestApi)({
keyPath = "hello.world",
networkImpl = mockNetworkImpl,
}, "POST")
expect(function()
POST.getStatus({}, "testing")
end).to.throw()
end)
end)
end)
end)
end
@@ -0,0 +1,84 @@
return function()
local root = script.Parent
local Freeze = require(root.Freeze)
local EnumNetworkStatus = require(root.NetworkStatus.EnumNetworkStatus)
local Promise = require(root.Promise)
local mockNetworkImpl = function(url, method, options)
return Promise.reject("networkError")
end
local mockStore = require(root.mockStore)
local GET = require(root.makeRequestApi)({
methodType = "GET",
keyPath = "hello.world",
networkImpl = mockNetworkImpl,
})
local function noOpt()
end
describe("GIVEN a store", function()
local actionHistory = {}
local roduxStore = mockStore.config({
dispatch = function(self, action)
if type(action) == "function" then
return action(self)
else
table.insert(actionHistory, action)
end
end,
state = {
hello = {
world = Freeze.UnorderedMap.new({}),
}
}
})
describe("GIVEN a module script and builderFunction w/out parameters", function()
local hasBuilderFunctionRun = false
local receivedChannelArgument = nil
local mockGETChannels = GET(script, function(requestBuilder, channel)
hasBuilderFunctionRun = true
receivedChannelArgument = channel
return requestBuilder("example.com"):path("v1"):path("channels"):id(channel):path("messages")
end)
describe("WHEN thunk is dispatched", function()
it("SHOULD invoke builder function given to GET constructor", function()
roduxStore:dispatch(mockGETChannels.API("announcements")):catch(noOpt)
expect(hasBuilderFunctionRun).to.equal(true)
expect(receivedChannelArgument).to.equal("announcements")
end)
it("SHOULD dispatch network status actions and payload action", function()
actionHistory = {}
roduxStore:dispatch(mockGETChannels.API("announcements")):catch(noOpt)
local action1 = actionHistory[1]
expect(action1).to.be.ok()
expect(action1.type).to.equal("networkStatus:hello.world")
expect(action1.status).to.equal(EnumNetworkStatus.Fetching)
end)
it("SHOULD dispatch a failed network status action", function()
actionHistory = {}
roduxStore:dispatch(mockGETChannels.API("announcements")):catch(noOpt)
local action2 = actionHistory[2]
expect(action2).to.be.ok()
local isFound = string.find(action2.type, "Failed")
expect(isFound).to.be.ok()
end)
it("SHOULD allow the promise to be caught", function()
local itWasCaught = false
roduxStore:dispatch(mockGETChannels.API("announcements")):catch(function()
itWasCaught = true
end)
assert(itWasCaught, "Promise was not able to be caught when network fails")
end)
end)
end)
end)
end
@@ -0,0 +1,23 @@
return {
config = function(options)
options = options or {}
local state = options.state or {}
return {
dispatch = options.dispatch or function(self, thunk)
if type(thunk) == "function" then
thunk(self)
end
end,
getState = function()
return state
end,
setState = function(_, newState)
state = newState
end,
}
end,
}
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["tutils"]["tutils"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package