add gs
This commit is contained in:
@@ -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
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
--[[
|
||||
Package link auto-generated by Rotriever
|
||||
]]
|
||||
local PackageIndex = script.Parent.Parent
|
||||
|
||||
local package = PackageIndex["roblox_string-utilities"]["string-utilities"]
|
||||
|
||||
if package.ClassName == "ModuleScript" then
|
||||
return require(package)
|
||||
end
|
||||
|
||||
return package
|
||||
@@ -0,0 +1,9 @@
|
||||
# Generated by Rotriever. Format subject to change in future releases.
|
||||
name = "roblox/url-builder"
|
||||
version = "1.0.4"
|
||||
commit = "cc593441e3efee555f0d6789c5df45138947909f"
|
||||
source = "url+https://github.com/roblox/url-builder"
|
||||
dependencies = [
|
||||
"Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo",
|
||||
"StringUtilities roblox/string-utilities 1.0.0 url+https://github.com/roblox/string-utilities",
|
||||
]
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
|
||||
local ContentProvider = game:GetService("ContentProvider")
|
||||
|
||||
--- base configuration -----------------------------
|
||||
local baseUrl = ContentProvider.BaseUrl
|
||||
baseUrl = string.gsub(baseUrl, ".*://", "")
|
||||
baseUrl = string.gsub(baseUrl, "/.*", "")
|
||||
baseUrl = string.gsub(baseUrl, "^www%.", "")
|
||||
|
||||
local UrlBase = {}
|
||||
|
||||
--[[
|
||||
builds the base URL for an API
|
||||
|
||||
name (string): base name of the API, eg: "games"
|
||||
params (optional table): a configuration table with any of the following:
|
||||
* proto (optional string): the URL protocol, eg "http", "ftp", defaults to "https"
|
||||
* version (optional int, string): an optional version, eg "1" will append "/v1" to the URL
|
||||
* path (optional string): a subpath to append to the URL, no leading slash
|
||||
* secure (optional boolean): equivalent to proto = "https"
|
||||
all strings can be empty (including name), output will adapt accordingly
|
||||
|
||||
alternatively, version can be provided as second argument eg ("games", 1)
|
||||
]]
|
||||
function UrlBase.new(name, params)
|
||||
assert(type(name) == "string", "UrlBase.new: `name` should be a string")
|
||||
if params == nil then
|
||||
params = {}
|
||||
end
|
||||
if type(params) == "number" or type(params) == "string" then
|
||||
params = {version = params}
|
||||
end
|
||||
assert(type(params) == "table", "UrlBase.new: `params` should be a table")
|
||||
local proto = params.proto
|
||||
local version = params.version
|
||||
local path = params.path
|
||||
if proto == nil then
|
||||
if params.secure == false then
|
||||
proto = "http"
|
||||
else
|
||||
proto = "https"
|
||||
end
|
||||
end
|
||||
local urlbase = proto
|
||||
if #proto > 0 then
|
||||
urlbase = urlbase .. "://"
|
||||
end
|
||||
urlbase = urlbase .. name
|
||||
if #name > 0 then
|
||||
urlbase = urlbase .. "."
|
||||
end
|
||||
urlbase = urlbase .. baseUrl
|
||||
if version ~= nil and #tostring(version) > 0 then
|
||||
urlbase = urlbase .. "/v" .. tostring(version)
|
||||
end
|
||||
if path ~= nil and #path > 0 then
|
||||
urlbase = urlbase .. "/" .. path
|
||||
end
|
||||
return urlbase
|
||||
end
|
||||
|
||||
local isQQ = string.sub(baseUrl, -6) == "qq.com"
|
||||
|
||||
-- from Url.lua
|
||||
UrlBase.API = UrlBase.new("api")
|
||||
UrlBase.APIS = UrlBase.new("apis")
|
||||
UrlBase.AUTH = UrlBase.new("auth")
|
||||
UrlBase.CHAT = UrlBase.new("chat")
|
||||
UrlBase.FRIENDS = UrlBase.new("friends", 1)
|
||||
UrlBase.ASSETGAME = UrlBase.new("assetgame")
|
||||
UrlBase.GAMES = UrlBase.new("games", 1)
|
||||
UrlBase.NOTIFICATION = UrlBase.new("notification", 2)
|
||||
UrlBase.PRESENCE = UrlBase.new("presence", 1)
|
||||
UrlBase.REALTIME = UrlBase.new("realtime")
|
||||
UrlBase.WEB = UrlBase.new("web")
|
||||
UrlBase.WWW = UrlBase.new("www")
|
||||
UrlBase.ASD = UrlBase.new("ads", 1)
|
||||
UrlBase.FOLLOWINGS = UrlBase.new("followings", 1)
|
||||
UrlBase.PREMIUM = UrlBase.new("premiumfeatures", 1)
|
||||
UrlBase.BLOG = "https://blog.roblox.com"
|
||||
UrlBase.CORP = isQQ and "https://roblox.qq.com" or "https://corp.roblox.com"
|
||||
-- from Http.lua
|
||||
UrlBase.ACCOUNTSETTINGS = UrlBase.new("accountsettings")
|
||||
UrlBase.BADGES = UrlBase.new("badges", 1)
|
||||
UrlBase.INVENTORY = UrlBase.new("inventory", 1)
|
||||
UrlBase.CATALOG = UrlBase.new("catalog", 1)
|
||||
-- from AEWebApi.lua
|
||||
UrlBase.AVATAR = UrlBase.new("avatar", 1)
|
||||
|
||||
UrlBase.MOBILENAV = "robloxmobile://navigation"
|
||||
UrlBase.APPSFLYER = "https://ro.blox.com"
|
||||
|
||||
return UrlBase
|
||||
+390
@@ -0,0 +1,390 @@
|
||||
local Packages = script.Parent.Parent
|
||||
|
||||
local Cryo = require(Packages.Cryo)
|
||||
local StringUtilities = require(Packages.StringUtilities)
|
||||
local StringTrim = StringUtilities.StringTrim
|
||||
local StringSplit = StringUtilities.StringSplit
|
||||
local encodeURIComponent = require(script.Parent.encodeURIComponent)
|
||||
local UrlBase = require(script.Parent.UrlBase)
|
||||
|
||||
local GameUrlPatterns = require(script.Parent.UrlPatterns.GameUrlPatterns)
|
||||
local UserUrlPatterns = require(script.Parent.UrlPatterns.UserUrlPatterns)
|
||||
local StaticUrlPatterns = require(script.Parent.UrlPatterns.StaticUrlPatterns)
|
||||
local CatalogUrlPatterns = require(script.Parent.UrlPatterns.CatalogUrlPatterns)
|
||||
|
||||
local UrlBuilder = {}
|
||||
|
||||
--[[
|
||||
UTILITY FUNCTIONS
|
||||
]]
|
||||
|
||||
-- splits by separator, trims whitspaces from each part and filters out empty ones
|
||||
local function splitAndTrim(input, separator, limit)
|
||||
local list = StringSplit(input, separator, limit)
|
||||
list = Cryo.List.map(list, function(item)
|
||||
return StringTrim(item)
|
||||
end)
|
||||
list = Cryo.List.filter(list, function(item)
|
||||
return #item > 0
|
||||
end)
|
||||
return list
|
||||
end
|
||||
|
||||
--[[
|
||||
PATTERN VALIDATION
|
||||
]]
|
||||
|
||||
-- a value should be a string, number, or a table of strings and numbers only
|
||||
local function validateValueType(value)
|
||||
local valueType = type(value)
|
||||
if valueType == "string" then
|
||||
return true
|
||||
elseif valueType == "number" then
|
||||
return true
|
||||
elseif valueType == "table" then
|
||||
for _, item in ipairs(value) do
|
||||
local itemType = type(item)
|
||||
if itemType ~= "string" and itemType ~= "number" then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- validate that item is either a literal (no check needed), or a valid placeholder
|
||||
local function assertPlaceholder(element)
|
||||
if string.sub(element, 1, 1) == "{" then
|
||||
assert(string.sub(element, -1, -1) == "}", "invalid pattern: placeholder items should end with `}`")
|
||||
end
|
||||
end
|
||||
|
||||
-- validate a pattern's path or query element
|
||||
local function assertElementIsValid(element, inQuery)
|
||||
assert(type(element) == "table", "invalid pattern: elements should all be tables")
|
||||
if inQuery then
|
||||
assert(type(element.name) == "string", "invalid pattern: element name should be a string")
|
||||
assert(#element.name > 0, "invalid pattern: element name should not be empty")
|
||||
end
|
||||
assert(
|
||||
type(element.value) == "string" or type(element.value) == "number",
|
||||
"invalid pattern: element value should be a string or number"
|
||||
)
|
||||
if type(element.value) == "string" then
|
||||
assertPlaceholder(element.value)
|
||||
end
|
||||
if element.optional ~= nil then
|
||||
assert(type(element.optional) == "boolean", "invalid pattern: element optional should be a boolean")
|
||||
end
|
||||
if element.default ~= nil then
|
||||
assert(
|
||||
validateValueType(element.default),
|
||||
"invalid pattern: element default should be a string, number, or a table of strings and numbers only"
|
||||
)
|
||||
end
|
||||
if element.collect ~= nil then
|
||||
assert(
|
||||
element.collect == "multi" or element.collect == "csv",
|
||||
"invalid pattern: element optional should be one of `multi`, `csv`"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
-- validate a full pattern object, could use a validator library in the future
|
||||
local function assertPatternIsValid(pattern)
|
||||
assert(type(pattern.base) == "string", "invalid pattern: base should be a string")
|
||||
assert(#pattern.base > 0, "invalid pattern: base should not be empty")
|
||||
assert(type(pattern.path) == "table", "invalid pattern: path should be a table")
|
||||
for _, element in ipairs(pattern.path) do
|
||||
assertElementIsValid(element, false)
|
||||
end
|
||||
if pattern.query ~= nil then
|
||||
assert(type(pattern.query) == "table", "invalid pattern: query should be a table")
|
||||
end
|
||||
if type(pattern.query) == "table" then
|
||||
for _, element in ipairs(pattern.query) do
|
||||
assertElementIsValid(element, true)
|
||||
end
|
||||
end
|
||||
if pattern.hash ~= nil then
|
||||
assert(type(pattern.hash) == "string", "invalid pattern: hash should be a string")
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
STRING PATTERNS
|
||||
]]
|
||||
|
||||
-- converts a "/" or "&" delimited string into a list or path/query elements
|
||||
local function buildElementsFromString(elements, inQuery)
|
||||
local separator = inQuery and "&" or "/"
|
||||
local elements = splitAndTrim(elements, separator)
|
||||
elements = Cryo.List.map(elements, function(element)
|
||||
local elementName = nil
|
||||
local elementValue = StringTrim(element)
|
||||
local elementOptional = nil
|
||||
local elementDefault = nil
|
||||
local elementCollect = nil
|
||||
if inQuery then
|
||||
local queryitems = StringSplit(elementValue, "=", 2)
|
||||
elementName = StringTrim(queryitems[1])
|
||||
elementValue = queryitems[2]
|
||||
if string.sub(elementName, 1, 1) == "{" then
|
||||
elementName = StringTrim(elementName, "{}")
|
||||
elementName = StringSplit(elementName, "|", 2)[1]
|
||||
if elementValue == nil then
|
||||
elementValue = queryitems[1]
|
||||
end
|
||||
elementName = StringTrim(elementName)
|
||||
end
|
||||
elementCollect = "multi"
|
||||
end
|
||||
if elementValue ~= nil and string.find(elementValue, "^{.*}$") then
|
||||
elementValue = StringTrim(elementValue, "{}")
|
||||
local valueitems = StringSplit(elementValue, "|", 2)
|
||||
elementValue = StringTrim(valueitems[1])
|
||||
if #valueitems > 1 then
|
||||
elementOptional = true
|
||||
if #(valueitems[2]) > 1 then
|
||||
elementDefault = valueitems[2]
|
||||
end
|
||||
end
|
||||
elementValue = "{" .. elementValue .. "}"
|
||||
end
|
||||
return {
|
||||
name = elementName,
|
||||
value = elementValue,
|
||||
optional = elementOptional,
|
||||
default = elementDefault,
|
||||
collect = elementCollect,
|
||||
}
|
||||
end)
|
||||
return elements
|
||||
end
|
||||
|
||||
local function buildQueryStringFromTable(elements)
|
||||
local stringpattern = {}
|
||||
for name, value in pairs(elements) do
|
||||
table.insert(stringpattern, name .. "=" .. value)
|
||||
end
|
||||
return table.concat(stringpattern, "&")
|
||||
end
|
||||
|
||||
-- expands a pattern, replacing string parts with element tables
|
||||
local function simplifyPattern(pattern)
|
||||
local patternbase = pattern.base
|
||||
local patternpath = pattern.path
|
||||
local patternquery = pattern.query
|
||||
local patternhash = pattern.hash
|
||||
if patternbase ~= nil and UrlBase[string.upper(patternbase)] ~= nil then
|
||||
patternbase = UrlBase[string.upper(patternbase)]
|
||||
end
|
||||
if type(patternpath) == "string" then
|
||||
patternpath = buildElementsFromString(patternpath, false)
|
||||
end
|
||||
if type(patternquery) == "table" and patternquery[1] == nil then
|
||||
patternquery = buildQueryStringFromTable(patternquery)
|
||||
end
|
||||
if type(patternquery) == "string" then
|
||||
patternquery = buildElementsFromString(patternquery, true)
|
||||
end
|
||||
return {
|
||||
base = patternbase,
|
||||
path = patternpath,
|
||||
query = patternquery,
|
||||
hash = patternhash,
|
||||
}
|
||||
end
|
||||
|
||||
--[[
|
||||
PATTERN RESOLUTION
|
||||
]]
|
||||
|
||||
local function concatValues(elementValues, inQuery)
|
||||
-- suppress all empty values, avoids double slashes and empty query params
|
||||
elementValues = Cryo.List.filter(elementValues, function(value)
|
||||
if inQuery then
|
||||
return #splitAndTrim(value, "=") > 1
|
||||
end
|
||||
return #value > 0
|
||||
end)
|
||||
local separator = inQuery and "&" or "/"
|
||||
return table.concat(elementValues, separator)
|
||||
end
|
||||
|
||||
-- resolves the element to a string, from the given input table
|
||||
local function resolveElement(element, input, inQuery)
|
||||
if input == nil then
|
||||
input = {}
|
||||
end
|
||||
local elementValues
|
||||
if type(element.value) == "string" and string.sub(element.value, 1, 1) == "{" then
|
||||
elementValues = input[string.sub(element.value, 2, -2)]
|
||||
if elementValues == nil then
|
||||
elementValues = element.default
|
||||
end
|
||||
if not element.optional then
|
||||
assert(elementValues ~= nil, "UrlBuilder: missing parameter: `" .. element.value .. "`")
|
||||
end
|
||||
if elementValues == nil then
|
||||
-- at this point optional == true, so we remove the element from output
|
||||
elementValues = {}
|
||||
end
|
||||
assert(
|
||||
validateValueType(elementValues),
|
||||
"UrlBuilder: invalid parameter: `" .. element.value .. "`, " ..
|
||||
"should be a string, number, or a table of strings and numbers only"
|
||||
)
|
||||
else
|
||||
elementValues = element.value
|
||||
end
|
||||
if type(elementValues) ~= "table" then
|
||||
elementValues = {elementValues}
|
||||
end
|
||||
elementValues = Cryo.List.map(elementValues, function(value)
|
||||
return encodeURIComponent(tostring(value))
|
||||
end)
|
||||
if inQuery then
|
||||
-- we are resolving a query parameter
|
||||
if element.collect == "csv" then
|
||||
elementValues = {table.concat(elementValues, "%2C")} -- ","
|
||||
end
|
||||
elementValues = Cryo.List.map(elementValues, function(value)
|
||||
return encodeURIComponent(element.name) .. "=" .. value
|
||||
end)
|
||||
end
|
||||
return concatValues(elementValues, inQuery)
|
||||
end
|
||||
|
||||
local function resolveElementList(elementList, input, inQuery)
|
||||
local elementValues = Cryo.List.map(elementList or {}, function(element)
|
||||
return resolveElement(element, input, inQuery)
|
||||
end)
|
||||
return concatValues(elementValues, inQuery)
|
||||
end
|
||||
|
||||
--[[
|
||||
PATTERN CONSTRUCTION
|
||||
]]
|
||||
|
||||
--[[
|
||||
creates a new URL builder function for the given pattern
|
||||
the function can then be called with an input (table) to generate a URL
|
||||
|
||||
pattern (table): pattern specification with the following:
|
||||
* base (string): the domain and base path for the URL, eg "http://static.roblox.com"
|
||||
* the UrlBase module exposes a list of available APIs, or can be used to properly build a new base
|
||||
* path (string or table): list of path elements, one of
|
||||
* (string): "/" delimited string, each element can be a literal or a "{}" enclosed placeholder
|
||||
* (table): list of full path elements, see details below
|
||||
* query (optional table): list of querystring elements, one of
|
||||
* (string) : query string, with optional placeholders, eg "imageId={images}&size=140"
|
||||
* (table): dictionary of {name = value, ...} pairs, eg {imageId = "{images}", size = 140}
|
||||
* (table): list of full query elements, see details below
|
||||
* hash (optional string): will be appended AS IS (no placeholders or UrlEncode), separated by "#"
|
||||
|
||||
path and query elements are tables with the following (some only apply to query elements):
|
||||
* name (string): *query only* the name of the query parameter
|
||||
* value (string or number): value of the element, can be a literal or a "{}" enclosed placeholder
|
||||
* optional (optional boolean): marks the element as optional, if placeholder value can't be found
|
||||
* default (optional string): value if placeholder can't be found, implies "optional = true"
|
||||
* collect (optional string): *query only* how to resolve table values, one of
|
||||
* "multi": param and value will be repeated, eg "p=v1&p=v2&p=v3", this is the default
|
||||
* "csv": one param, values will be concatenated, eg "p=v1,v2,v3"
|
||||
]]
|
||||
function UrlBuilder.new(pattern)
|
||||
pattern = simplifyPattern(pattern)
|
||||
assertPatternIsValid(pattern)
|
||||
return function(input, expected)
|
||||
local url = StringTrim(pattern.base, "/", {right = true})
|
||||
local path = resolveElementList(pattern.path, input, false)
|
||||
if #path > 0 then
|
||||
url = url .. "/" .. path
|
||||
end
|
||||
-- append slash if URL only consists of "proto://domain"
|
||||
if string.match(url, "[^/]/[^/]") == nil then
|
||||
url = url .. "/"
|
||||
end
|
||||
local query = resolveElementList(pattern.query, input, true)
|
||||
if #query > 0 then
|
||||
url = url .. "?" .. query
|
||||
end
|
||||
if pattern.hash and #pattern.hash > 0 then
|
||||
url = url .. "#" .. pattern.hash
|
||||
end
|
||||
-- testing, for development use only
|
||||
if expected then
|
||||
if url ~= expected then
|
||||
warn("UrlBuilder: unexpected output for pattern:")
|
||||
warn("UrlBuilder: expected `" .. expected .. "`")
|
||||
warn("UrlBuilder: actual `" .. url .. "`")
|
||||
end
|
||||
return expected
|
||||
end
|
||||
return url
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
creates a new URL builder function from a string pattern
|
||||
|
||||
pattern format: "base:path/to/{endpoint}?param1={value1}&{param2}"
|
||||
]]
|
||||
function UrlBuilder.fromString(pattern)
|
||||
local patternitems = StringSplit(pattern, ":", 2)
|
||||
if #patternitems < 2 then
|
||||
patternitems = {"", patternitems[1]}
|
||||
end
|
||||
local patternbase = patternitems[1]
|
||||
patternitems = StringSplit(patternitems[2], "%#", 2)
|
||||
local patternhash = patternitems[2] or ""
|
||||
patternitems = StringSplit(patternitems[1], "%?", 2)
|
||||
local patternpath = patternitems[1] or ""
|
||||
local patternquery = patternitems[2] or ""
|
||||
-- in case ":" was the protocol delimiter of a full url (eg http://domain/...)
|
||||
if string.sub(patternpath, 1, 2) == "//" then
|
||||
patternitems = StringSplit(string.sub(patternpath, 3), "/", 2)
|
||||
patternbase = patternbase .. "://" .. patternitems[1]
|
||||
patternpath = patternitems[2] or "/"
|
||||
end
|
||||
return UrlBuilder.new({
|
||||
base = StringTrim(patternbase),
|
||||
path = StringTrim(patternpath),
|
||||
query = StringTrim(patternquery),
|
||||
hash = patternhash,
|
||||
})
|
||||
end
|
||||
|
||||
--[[
|
||||
CONVENIENCE SHORTHANDS
|
||||
]]
|
||||
|
||||
function UrlBuilder.addQueryString(url, query)
|
||||
local pattern = StringTrim(url)
|
||||
local queryindex = string.find(pattern, "%?")
|
||||
if queryindex == nil then
|
||||
pattern = pattern .. "?"
|
||||
elseif queryindex < #pattern then
|
||||
pattern = pattern .. "&"
|
||||
end
|
||||
local queryitems = Cryo.Dictionary.keys(query)
|
||||
queryitems = Cryo.List.map(queryitems, function(param)
|
||||
return "{" .. param .. "}"
|
||||
end)
|
||||
queryitems = table.concat(queryitems, "&")
|
||||
pattern = pattern .. queryitems
|
||||
return UrlBuilder.fromString(pattern)(query)
|
||||
end
|
||||
|
||||
--[[
|
||||
PATTERN REGISTRATION
|
||||
]]
|
||||
|
||||
UrlBuilder.game = GameUrlPatterns(UrlBuilder)
|
||||
UrlBuilder.user = UserUrlPatterns(UrlBuilder)
|
||||
UrlBuilder.catalog = CatalogUrlPatterns(UrlBuilder)
|
||||
UrlBuilder.static = StaticUrlPatterns(UrlBuilder)
|
||||
|
||||
return UrlBuilder
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
|
||||
return function()
|
||||
local UrlBuilder = require(script.Parent.UrlBuilder)
|
||||
|
||||
describe("simple", function()
|
||||
local pattern = UrlBuilder.fromString("api:game/{universeId}/thumbnail?size={pxWidth|150}")
|
||||
|
||||
it("should generate proper url", function()
|
||||
local url = pattern({
|
||||
universeId = "1356984689",
|
||||
pxWidth = 720,
|
||||
})
|
||||
expect(url).to.equal("https://api.roblox.com/game/1356984689/thumbnail?size=720")
|
||||
end)
|
||||
|
||||
it("table values duplicate url parts", function()
|
||||
local url = pattern({
|
||||
universeId = {"1356984689", "8745654337"},
|
||||
pxWidth = {720, 320},
|
||||
})
|
||||
expect(url).to.equal("https://api.roblox.com/game/1356984689/8745654337/thumbnail?size=720&size=320")
|
||||
end)
|
||||
|
||||
it("empty table should remove url part", function()
|
||||
local url = pattern({
|
||||
universeId = "1356984689",
|
||||
pxWidth = {},
|
||||
})
|
||||
expect(url).to.equal("https://api.roblox.com/game/1356984689/thumbnail")
|
||||
end)
|
||||
|
||||
it("missing values should throw", function()
|
||||
local function getUrl()
|
||||
pattern({pxWidth = 720})
|
||||
end
|
||||
expect(getUrl).to.throw()
|
||||
end)
|
||||
|
||||
it("missing optional values should not throw", function()
|
||||
local url = pattern({universeId = "1356984689"})
|
||||
expect(url).to.equal("https://api.roblox.com/game/1356984689/thumbnail?size=150")
|
||||
end)
|
||||
|
||||
end)
|
||||
|
||||
describe("complex", function()
|
||||
local pattern = UrlBuilder.fromString(" https://roblox.com/ test/{special}/path / // {empty}/{multiple}/ x ? {num}¶m1=static&dupl={multiple}&")
|
||||
|
||||
it("complex pattern/values with nultiple edge cases", function()
|
||||
local url = pattern({
|
||||
special = "$pec!al",
|
||||
num = 568,
|
||||
multiple = {"ab/", "c d"},
|
||||
empty = "",
|
||||
})
|
||||
expect(url).to.equal("https://roblox.com/test/%24pec!al/path/ab%2F/c%20d/x?num=568¶m1=static&dupl=ab%2F&dupl=c%20d")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("invalid patterns", function()
|
||||
|
||||
it("should throw on malformed placeholder", function()
|
||||
local function invalidPattern()
|
||||
UrlBuilder.fromString("api:game/{universeI}d/thumbnail?size={pxWidth|150}")
|
||||
end
|
||||
expect(invalidPattern).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw on missing base", function()
|
||||
local function invalidPattern()
|
||||
UrlBuilder.fromString("/game/{universeId}/thumbnail?size={pxWidth|150}")
|
||||
end
|
||||
expect(invalidPattern).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw on missing parameter name", function()
|
||||
local function invalidPattern()
|
||||
UrlBuilder.fromString("api:game/{universeId}/thumbnail?={pxWidth|150}")
|
||||
end
|
||||
expect(invalidPattern).to.throw()
|
||||
end)
|
||||
|
||||
end)
|
||||
|
||||
end
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"lint": {
|
||||
"LocalShadow": "fatal",
|
||||
"LocalUnused": "fatal",
|
||||
"ImportUnused": "fatal"
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
--!nocheck
|
||||
|
||||
return function(UrlBuilder)
|
||||
|
||||
local CatalogUrlPatterns = {}
|
||||
|
||||
CatalogUrlPatterns.info = {
|
||||
webbundle = UrlBuilder.fromString("www:bundles/{assetId}"),
|
||||
webasset = UrlBuilder.fromString("www:catalog/{assetId}"),
|
||||
webpage = function(params)
|
||||
if params.assetType == "Bundle" then
|
||||
return CatalogUrlPatterns.info.webbundle(params)
|
||||
elseif params.assetType == "Asset" then
|
||||
return CatalogUrlPatterns.info.webasset(params)
|
||||
else
|
||||
warn(string.format("%s - unknown assetType of %s", tostring(script.name), tostring(params.assetType)))
|
||||
return nil
|
||||
end
|
||||
end,
|
||||
appsflyer = function(params)
|
||||
return UrlBuilder.fromString("appsflyer:Ebh5?pid=share&is_retargeting=true&af_dp={mobileUrl}&af_web_dp={webUrl}")({
|
||||
mobileUrl = UrlBuilder.fromString("mobilenav:item_details?itemId={assetId}&itemType={assetType}")(params),
|
||||
webUrl = CatalogUrlPatterns.info.webpage(params),
|
||||
})
|
||||
end,
|
||||
}
|
||||
|
||||
return CatalogUrlPatterns
|
||||
end
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
return function()
|
||||
local UrlBuilder = require(script.Parent.Parent.UrlBuilder)
|
||||
|
||||
describe("appsflyer link", function()
|
||||
it("should generate proper bundle url", function()
|
||||
local url = UrlBuilder.catalog.info.appsflyer({
|
||||
assetId = "1356984689",
|
||||
assetType = "Bundle",
|
||||
})
|
||||
expect(url).to.equal("https://ro.blox.com/Ebh5?pid=share&is_retargeting=true" ..
|
||||
"&af_dp=robloxmobile%3A%2F%2Fnavigation%2Fitem_details%3FitemId%3D1356984689%26itemType%3DBundle" ..
|
||||
"&af_web_dp=https%3A%2F%2Fwww.roblox.com%2Fbundles%2F1356984689")
|
||||
end)
|
||||
|
||||
it("should generate proper asset url", function()
|
||||
local url = UrlBuilder.catalog.info.appsflyer({
|
||||
assetId = "1356984689",
|
||||
assetType = "Asset",
|
||||
})
|
||||
expect(url).to.equal("https://ro.blox.com/Ebh5?pid=share&is_retargeting=true" ..
|
||||
"&af_dp=robloxmobile%3A%2F%2Fnavigation%2Fitem_details%3FitemId%3D1356984689%26itemType%3DAsset" ..
|
||||
"&af_web_dp=https%3A%2F%2Fwww.roblox.com%2Fcatalog%2F1356984689")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
--!nocheck
|
||||
|
||||
return function(UrlBuilder)
|
||||
|
||||
local GameUrlPatterns = {}
|
||||
|
||||
--- from GameInfoList.lua
|
||||
GameUrlPatterns.info = {
|
||||
webpage = UrlBuilder.fromString("www:games/{placeId}"),
|
||||
store = UrlBuilder.fromString("www:games/store-section/{universeId}"),
|
||||
badges = UrlBuilder.fromString("www:games/badges-section/{universeId}"),
|
||||
servers = UrlBuilder.fromString("www:games/servers-section/{universeId}"),
|
||||
serversPreopenCreateVip = UrlBuilder.fromString("www:games/servers-section-preopen-create-vip/{universeId}"),
|
||||
group = UrlBuilder.fromString("www:groups/{creatorId}"),
|
||||
user = UrlBuilder.fromString("www:users/{creatorId}/profile"),
|
||||
-- {creatorType=Group|User, creatorId}
|
||||
creator = function(params)
|
||||
if params.creatorType == "Group" then
|
||||
return GameUrlPatterns.info.group(params)
|
||||
elseif params.creatorType == "User" then
|
||||
return GameUrlPatterns.info.user(params)
|
||||
end
|
||||
warn(string.format("%s - unknown creatorType of %s", tostring(script.name), tostring(params.creatorType)))
|
||||
return nil
|
||||
end,
|
||||
appsflyer = function(params)
|
||||
return UrlBuilder.fromString("appsflyer:Ebh5?pid=share&is_retargeting=true&af_dp={mobileUrl}&af_web_dp={webUrl}")({
|
||||
mobileUrl = UrlBuilder.fromString("mobilenav:game_details?gameId={universeId}")(params),
|
||||
webUrl = GameUrlPatterns.info.webpage(params),
|
||||
})
|
||||
end,
|
||||
}
|
||||
|
||||
--- from Http/Requests/*
|
||||
GameUrlPatterns.details = UrlBuilder.fromString("games:games?{universeIds}")
|
||||
GameUrlPatterns.playability = UrlBuilder.fromString("games:games/multiget-playability-status?{universeIds}")
|
||||
GameUrlPatterns.media = UrlBuilder.fromString("games:games/{universeId}/media")
|
||||
GameUrlPatterns.favorite = UrlBuilder.fromString("games:games/{universeId}/favorites")
|
||||
GameUrlPatterns.social = UrlBuilder.fromString("games:games/{universeId}/social-links/list")
|
||||
GameUrlPatterns.recommended = UrlBuilder.fromString("games:games/recommendations/game/{universeId}?{paginationKey|}&{maxRows|6}")
|
||||
GameUrlPatterns.thumbnail = UrlBuilder.fromString("games:games/game-thumbnails?{height|150}&{width|150}&{imageTokens}")
|
||||
GameUrlPatterns.vote = {
|
||||
-- votes for all users
|
||||
all = UrlBuilder.fromString("games:games/{universeId}/votes"),
|
||||
-- current user vote status
|
||||
get = UrlBuilder.fromString("games:games/{universeId}/votes/user"),
|
||||
set = UrlBuilder.fromString("games:games/{universeId}/user-votes"),
|
||||
}
|
||||
GameUrlPatterns.follow = {
|
||||
get = UrlBuilder.fromString("followings:users/{userId}/universes/{universeId}/status"),
|
||||
set = UrlBuilder.fromString("followings:users/{userId}/universes/{universeId}"),
|
||||
}
|
||||
GameUrlPatterns.report = UrlBuilder.fromString("www:abusereport/asset?id={placeId}")
|
||||
|
||||
GameUrlPatterns.place = UrlBuilder.fromString("games:games/multiget-place-details?{placeIds}")
|
||||
|
||||
return GameUrlPatterns
|
||||
|
||||
end
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
return function()
|
||||
local UrlBuilder = require(script.Parent.Parent.UrlBuilder)
|
||||
|
||||
describe("appsflyer link", function()
|
||||
it("should generate proper url", function()
|
||||
local url = UrlBuilder.game.info.appsflyer({
|
||||
universeId = "1356984689",
|
||||
placeId = "123456789",
|
||||
})
|
||||
expect(url).to.equal("https://ro.blox.com/Ebh5?pid=share&is_retargeting=true" ..
|
||||
"&af_dp=robloxmobile%3A%2F%2Fnavigation%2Fgame_details%3FgameId%3D1356984689" ..
|
||||
"&af_web_dp=https%3A%2F%2Fwww.roblox.com%2Fgames%2F123456789")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
return function(UrlBuilder)
|
||||
local function isQQ()
|
||||
return string.find(UrlBuilder.fromString("corp:")(), "qq.com")
|
||||
end
|
||||
|
||||
return {
|
||||
catalog = UrlBuilder.fromString("www:catalog"),
|
||||
buildersClub = UrlBuilder.fromString("www:mobile-app-upgrades/native-ios/bc"),
|
||||
trades = UrlBuilder.fromString("www:trades"),
|
||||
profile = UrlBuilder.fromString("www:users/profile"),
|
||||
friends = UrlBuilder.fromString("www:users/friends"),
|
||||
groups = UrlBuilder.fromString("www:my/groups"),
|
||||
inventory = UrlBuilder.fromString("www:users/inventory"),
|
||||
messages = UrlBuilder.fromString("www:my/messages"),
|
||||
feed = UrlBuilder.fromString("www:feeds/inapp"),
|
||||
develop = UrlBuilder.fromString("www:develop/landing"),
|
||||
blog = UrlBuilder.fromString("blog:"),
|
||||
help = UrlBuilder.fromString(isQQ() and "corp:faq" or "www:help"),
|
||||
email = {
|
||||
getSetEmail = UrlBuilder.fromString("accountSettings:v1/email"),
|
||||
sendVerificationEmail = UrlBuilder.fromString("accountSettings:v1/email/verify")
|
||||
},
|
||||
about = {
|
||||
us = UrlBuilder.fromString("corp:"),
|
||||
careers = UrlBuilder.fromString(isQQ() and "corp:careers.html" or "corp:careers"),
|
||||
parents = UrlBuilder.fromString("corp:parents"),
|
||||
terms = function(params)
|
||||
if isQQ() and params.useGameQQUrls then
|
||||
return UrlBuilder.fromString("https://game.qq.com/contract.shtml")()
|
||||
else
|
||||
return UrlBuilder.fromString("www:info/terms")()
|
||||
end
|
||||
end,
|
||||
privacy = function(params)
|
||||
if isQQ() and params.useGameQQUrls then
|
||||
return UrlBuilder.fromString("https://game.qq.com/privacy_guide.shtml")()
|
||||
else
|
||||
return UrlBuilder.fromString("www:info/privacy")()
|
||||
end
|
||||
end,
|
||||
},
|
||||
settings = {
|
||||
account = UrlBuilder.fromString("www:my/account#!/info"),
|
||||
security = UrlBuilder.fromString("www:my/account#!/security"),
|
||||
privacy = UrlBuilder.fromString("www:my/account#!/privacy"),
|
||||
billing = UrlBuilder.fromString("www:my/account#!/billing"),
|
||||
notifications = UrlBuilder.fromString("www:my/account#!/notifications"),
|
||||
},
|
||||
tencent = {
|
||||
childrenPrivacyGuide = UrlBuilder.fromString("https://game.qq.com/privacy_guide_children.shtml"),
|
||||
luobuRiderTerms = UrlBuilder.fromString("https://roblox.qq.com/web201904/newsdetail.html?newsid=12429812"),
|
||||
reputationInfo = UrlBuilder.fromString("https://gamecredit.qq.com/static/games/index.htm"),
|
||||
},
|
||||
}
|
||||
end
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
|
||||
return function(UrlBuilder)
|
||||
return {
|
||||
profile = UrlBuilder.fromString("www:users/{userId}/profile"),
|
||||
group = UrlBuilder.fromString("www:groups/{groupId}/{groupName|}#!/about"),
|
||||
friends = UrlBuilder.fromString("www:users/{userId}/friends"),
|
||||
inventory = UrlBuilder.fromString("www:users/{userId}/inventory"),
|
||||
search = UrlBuilder.fromString("www:search/users?{keyword}"),
|
||||
|
||||
report = function(params)
|
||||
-- Web is fixing a bug that requires actionName and redirectUrl for this page to work
|
||||
-- once fixed, this pattern function can be replaced with
|
||||
-- UrlBuilder.fromString("www:abusereport/embedded/chat?id={userId}&{conversationId}"),
|
||||
return UrlBuilder.fromString("www:abusereport/embedded/chat?id={userId}&{actionName}&{conversationId}&{redirecturl}")({
|
||||
userId = params.userId,
|
||||
conversationId = params.conversationId,
|
||||
actionName = "chat",
|
||||
redirecturl = UrlBuilder.fromString("www:home")(),
|
||||
})
|
||||
end
|
||||
}
|
||||
end
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
local function formatCharacter(character)
|
||||
return string.format("%%%02X", character:byte(1,1))
|
||||
end
|
||||
|
||||
return function(stringToEncode)
|
||||
return stringToEncode:gsub("[^%w_%-%!%.%~%*%'%(%)]", formatCharacter)
|
||||
end
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
return function()
|
||||
local encodeURIComponent = require(script.Parent.encodeURIComponent)
|
||||
|
||||
it('should not filter alphanumerics', function()
|
||||
local str = 'abcXYZ1230'
|
||||
expect(encodeURIComponent(str)).to.equal(str)
|
||||
end)
|
||||
|
||||
it('should not filter allowed special characters', function()
|
||||
local str = 'abcABC123-_.!~*\'()'
|
||||
expect(encodeURIComponent(str)).to.equal(str)
|
||||
end)
|
||||
|
||||
it('should filter other non-alphanumeric characters', function()
|
||||
local str = 'hello world&result=true'
|
||||
expect(encodeURIComponent(str)).to.equal('hello%20world%26result%3Dtrue')
|
||||
end)
|
||||
end
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
return {
|
||||
encodeURIComponent = require(script.encodeURIComponent),
|
||||
UrlBase = require(script.UrlBase),
|
||||
UrlBuilder = require(script.UrlBuilder),
|
||||
}
|
||||
Reference in New Issue
Block a user