mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-23 22:25:26 +00:00
Build out real formatting machinery
This commit is contained in:
@@ -1,34 +1,155 @@
|
|||||||
|
--[[
|
||||||
|
This library describes a formatting mechanism akin to Rust's std::fmt.
|
||||||
|
|
||||||
|
It has a couple building blocks:
|
||||||
|
|
||||||
|
* A new syntax for formatting strings, taken verbatim from Rust. It'd also
|
||||||
|
be possible to use printf-style formatting specifiers to integrate with
|
||||||
|
the existing string.format utility.
|
||||||
|
|
||||||
|
* An equivalent to Rust's `Display` trait. We're mapping the semantics of
|
||||||
|
tostring and the __tostring metamethod onto this trait. A lot of types
|
||||||
|
should already have __tostring implementations, too!
|
||||||
|
|
||||||
|
* An equivalent to Rust's `Debug` trait. This library Lua-ifies that idea by
|
||||||
|
inventing a new metamethod, `__fmtDebug`. We pass along the "extended
|
||||||
|
form" attribute which is the equivalent of the "alternate mode" in Rust's
|
||||||
|
Debug trait since it's the author's opinion that treating it as a
|
||||||
|
verbosity flag is semantically accurate.
|
||||||
|
]]
|
||||||
|
|
||||||
|
--[[
|
||||||
|
The default implementation of __fmtDebug for tables when the extended option
|
||||||
|
is not set.
|
||||||
|
]]
|
||||||
|
local function defaultTableDebug(buffer, input)
|
||||||
|
buffer:writeRaw("{")
|
||||||
|
|
||||||
|
for key, value in pairs(input) do
|
||||||
|
buffer:write("[{:?}] = {:?}", key, value)
|
||||||
|
|
||||||
|
if next(input, key) ~= nil then
|
||||||
|
buffer:writeRaw(", ")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
buffer:writeRaw("}")
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
The default implementation of __fmtDebug for tables with the extended option
|
||||||
|
set.
|
||||||
|
]]
|
||||||
|
local function defaultTableDebugExtended(buffer, input)
|
||||||
|
-- Special case for empty tables.
|
||||||
|
if next(buffer) == nil then
|
||||||
|
buffer:writeRaw("{}")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
buffer:writeLineRaw("{")
|
||||||
|
buffer:indent()
|
||||||
|
|
||||||
|
for key, value in pairs(input) do
|
||||||
|
buffer:writeLine("[{:?}] = {:#?},", key, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
buffer:unindent()
|
||||||
|
buffer:writeRaw("}")
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
The default debug representation for all types.
|
||||||
|
]]
|
||||||
|
local function debugImpl(buffer, value, extendedForm)
|
||||||
|
local valueType = typeof(value)
|
||||||
|
|
||||||
|
if valueType == "string" then
|
||||||
|
local formatted = string.format("%q", value)
|
||||||
|
buffer:writeRaw(formatted)
|
||||||
|
elseif valueType == "table" then
|
||||||
|
local valueMeta = getmetatable(value)
|
||||||
|
|
||||||
|
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
|
||||||
|
-- This type implement's the metamethod we made up to line up with
|
||||||
|
-- Rust's 'Debug' trait.
|
||||||
|
|
||||||
|
valueMeta.__fmtDebug(value, buffer, extendedForm)
|
||||||
|
else
|
||||||
|
if extendedForm then
|
||||||
|
defaultTableDebugExtended(buffer, value)
|
||||||
|
else
|
||||||
|
defaultTableDebug(buffer, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
buffer:writeRaw(tostring(value))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Defines and implements the library's template syntax.
|
||||||
|
]]
|
||||||
local function writeFmt(buffer, template, ...)
|
local function writeFmt(buffer, template, ...)
|
||||||
local currentArg = 0
|
local currentArg = 0
|
||||||
local i = 1
|
local i = 1
|
||||||
local len = #template
|
local len = #template
|
||||||
|
|
||||||
while i < len do
|
while i <= len do
|
||||||
local openBrace = template:find("{", i)
|
local openBrace = template:find("{", i)
|
||||||
|
|
||||||
if openBrace == nil then
|
if openBrace == nil then
|
||||||
|
-- There are no remaining open braces in this string, so we can
|
||||||
|
-- write the rest of the string to the buffer.
|
||||||
|
|
||||||
buffer:writeRaw(template:sub(i))
|
buffer:writeRaw(template:sub(i))
|
||||||
break
|
break
|
||||||
else
|
else
|
||||||
if openBrace - i > 0 then
|
-- We found an open brace! This could be:
|
||||||
buffer:writeRaw(template:sub(i, openBrace - 1))
|
-- - A literal '{', written as '{{'
|
||||||
end
|
-- - The beginning of an interpolation, like '{}'
|
||||||
|
-- - An error, if there's no matching '}'
|
||||||
|
|
||||||
local charAfterBrace = template:sub(openBrace + 1, openBrace + 1)
|
local charAfterBrace = template:sub(openBrace + 1, openBrace + 1)
|
||||||
if charAfterBrace == "{" then
|
if charAfterBrace == "{" then
|
||||||
|
-- This is a literal brace, so we'll write everything up to this
|
||||||
|
-- point (including the first brace), and then skip over the
|
||||||
|
-- second brace.
|
||||||
|
|
||||||
buffer:writeRaw(template:sub(i, openBrace))
|
buffer:writeRaw(template:sub(i, openBrace))
|
||||||
i = openBrace + 2
|
i = openBrace + 2
|
||||||
else
|
else
|
||||||
|
-- This SHOULD be an interpolation. We'll find our matching
|
||||||
|
-- brace and treat the contents as the formatting specifier.
|
||||||
|
|
||||||
|
-- If there were any unwritten characters before this
|
||||||
|
-- interpolation, write them to the buffer.
|
||||||
|
if openBrace - i > 0 then
|
||||||
|
buffer:writeRaw(template:sub(i, openBrace - 1))
|
||||||
|
end
|
||||||
|
|
||||||
local closeBrace = template:find("}", openBrace + 1)
|
local closeBrace = template:find("}", openBrace + 1)
|
||||||
assert(closeBrace ~= nil, "Unclosed formatting specifier. Use '{{' to write an open brace.")
|
assert(closeBrace ~= nil, "Unclosed formatting specifier. Use '{{' to write an open brace.")
|
||||||
|
|
||||||
local formatSpecifier = template:sub(openBrace + 1, closeBrace - 1)
|
local formatSpecifier = template:sub(openBrace + 1, closeBrace - 1)
|
||||||
|
|
||||||
currentArg = currentArg + 1
|
currentArg = currentArg + 1
|
||||||
|
local arg = select(currentArg, ...)
|
||||||
|
|
||||||
if formatSpecifier == "" then
|
if formatSpecifier == "" then
|
||||||
local arg = select(currentArg, ...)
|
-- This should use the equivalent of Rust's 'Display', ie
|
||||||
|
-- tostring and the __tostring metamethod.
|
||||||
|
|
||||||
buffer:writeRaw(tostring(arg))
|
buffer:writeRaw(tostring(arg))
|
||||||
|
elseif formatSpecifier == ":?" then
|
||||||
|
-- This should use the equivalent of Rust's 'Debug',
|
||||||
|
-- invented for this library as __fmtDebug.
|
||||||
|
|
||||||
|
debugImpl(buffer, arg, false)
|
||||||
|
elseif formatSpecifier == ":#?" then
|
||||||
|
-- This should use the equivlant of Rust's 'Debug' with the
|
||||||
|
-- 'alternate' (ie expanded) flag set.
|
||||||
|
|
||||||
|
debugImpl(buffer, arg, true)
|
||||||
else
|
else
|
||||||
error("unsupported format specifier " .. formatSpecifier, 2)
|
error("unsupported format specifier " .. formatSpecifier, 2)
|
||||||
end
|
end
|
||||||
@@ -39,18 +160,20 @@ local function writeFmt(buffer, template, ...)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function writeLineFmt(buffer, template, ...)
|
|
||||||
writeFmt(buffer, template, ...)
|
|
||||||
table.insert(buffer, "\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function debugOutputBuffer()
|
local function debugOutputBuffer()
|
||||||
local buffer = {}
|
local buffer = {}
|
||||||
|
local startOfLine = true
|
||||||
local indentLevel = 0
|
local indentLevel = 0
|
||||||
local indentation = ""
|
local indentation = ""
|
||||||
|
|
||||||
function buffer:writeLine(template, ...)
|
function buffer:writeLine(template, ...)
|
||||||
return writeLineFmt(self, template, ...)
|
writeFmt(self, template, ...)
|
||||||
|
self:nextLine()
|
||||||
|
end
|
||||||
|
|
||||||
|
function buffer:writeLineRaw(value)
|
||||||
|
self:writeRaw(value)
|
||||||
|
self:nextLine()
|
||||||
end
|
end
|
||||||
|
|
||||||
function buffer:write(template, ...)
|
function buffer:write(template, ...)
|
||||||
@@ -58,26 +181,20 @@ local function debugOutputBuffer()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function buffer:writeRaw(value)
|
function buffer:writeRaw(value)
|
||||||
if #indentation > 0 then
|
if #value > 0 then
|
||||||
value = value:gsub("\n", "\n" .. indentation)
|
if startOfLine and #indentation > 0 then
|
||||||
end
|
startOfLine = false
|
||||||
|
table.insert(self, indentation)
|
||||||
|
end
|
||||||
|
|
||||||
table.insert(self, value)
|
table.insert(self, value)
|
||||||
|
startOfLine = false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function buffer:writeLineRaw(piece)
|
function buffer:nextLine()
|
||||||
if #indentation > 0 then
|
|
||||||
self:writeRaw(indentation)
|
|
||||||
end
|
|
||||||
|
|
||||||
self:writeRaw(piece)
|
|
||||||
table.insert(self, "\n")
|
table.insert(self, "\n")
|
||||||
end
|
startOfLine = true
|
||||||
|
|
||||||
function buffer:push(template, ...)
|
|
||||||
local value = string.format(template, ...)
|
|
||||||
|
|
||||||
self:writeLineRaw(value)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function buffer:indent()
|
function buffer:indent()
|
||||||
@@ -97,27 +214,13 @@ local function debugOutputBuffer()
|
|||||||
return buffer
|
return buffer
|
||||||
end
|
end
|
||||||
|
|
||||||
local function debugInner(value)
|
local function fmt(template, ...)
|
||||||
local valueType = typeof(value)
|
local buffer = debugOutputBuffer()
|
||||||
|
writeFmt(buffer, template, ...)
|
||||||
if valueType == "string" then
|
return buffer:finish()
|
||||||
return string.format("%q", value)
|
|
||||||
elseif valueType == "number" then
|
|
||||||
return tostring(value)
|
|
||||||
elseif valueType == "table" then
|
|
||||||
local debugImpl = getmetatable(value).__fmtDebug
|
|
||||||
|
|
||||||
if debugImpl ~= nil then
|
|
||||||
return debugImpl()
|
|
||||||
else
|
|
||||||
-- TODO: Nicer default debug implementation?
|
|
||||||
return tostring(value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
debugOutputBuffer = debugOutputBuffer,
|
debugOutputBuffer = debugOutputBuffer,
|
||||||
writeFmt = writeFmt,
|
fmt = fmt,
|
||||||
writeLineFmt = writeLineFmt,
|
|
||||||
}
|
}
|
||||||
@@ -11,12 +11,10 @@ local Status = strict("Session.Status", {
|
|||||||
Disconnected = "Disconnected",
|
Disconnected = "Disconnected",
|
||||||
})
|
})
|
||||||
|
|
||||||
local function DEBUG_showPatch(patch)
|
local function fmtPatch(patch)
|
||||||
local HttpService = game:GetService("HttpService")
|
|
||||||
|
|
||||||
local output = Fmt.debugOutputBuffer()
|
local output = Fmt.debugOutputBuffer()
|
||||||
|
|
||||||
output:push("Patch {")
|
output:writeLine("Patch {{")
|
||||||
output:indent()
|
output:indent()
|
||||||
|
|
||||||
for removed in ipairs(patch.removed) do
|
for removed in ipairs(patch.removed) do
|
||||||
@@ -24,23 +22,15 @@ local function DEBUG_showPatch(patch)
|
|||||||
end
|
end
|
||||||
|
|
||||||
for id, added in pairs(patch.added) do
|
for id, added in pairs(patch.added) do
|
||||||
output:push("Add ID %s {", id)
|
output:writeLine("Add ID {} {:#?}", id, added)
|
||||||
output:indent()
|
|
||||||
output:push("%s", HttpService:JSONEncode(added))
|
|
||||||
output:unindent()
|
|
||||||
output:push("}")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, updated in ipairs(patch.updated) do
|
for _, updated in ipairs(patch.updated) do
|
||||||
output:push("Update ID %s {", updated.id)
|
output:writeLine("Update ID {} {:#?}", updated.id, updated)
|
||||||
output:indent()
|
|
||||||
output:push("%s", HttpService:JSONEncode(updated))
|
|
||||||
output:unindent()
|
|
||||||
output:push("}")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
output:unindent()
|
output:unindent()
|
||||||
output:push("}")
|
output:writeLine("}")
|
||||||
|
|
||||||
return output:finish()
|
return output:finish()
|
||||||
end
|
end
|
||||||
@@ -114,7 +104,7 @@ function ServeSession:__initialSync(rootInstanceId)
|
|||||||
game
|
game
|
||||||
)
|
)
|
||||||
|
|
||||||
Log.trace("Computed hydration patch: %s", DEBUG_showPatch(hydratePatch))
|
Log.trace("Computed hydration patch: %s", fmtPatch(hydratePatch))
|
||||||
|
|
||||||
-- TODO: Prompt user to notify them of this patch, since it's
|
-- TODO: Prompt user to notify them of this patch, since it's
|
||||||
-- effectively a conflict between the Rojo server and the client.
|
-- effectively a conflict between the Rojo server and the client.
|
||||||
|
|||||||
Reference in New Issue
Block a user