The readability definitely could be improved, the terminology more precise, and a more defensive approach when it comes to trusting what types the parameters are going to be.
Out of prior boredom and intrigue for what you had accomplished, I figured that I’d write a version of this “parser” (which I’ve changed the name to in my snippet as “codec” as I thought it was more appropriate).
I also expanded the codec to encode and decode Roblox datatypes since the ease of use was simple to implement and the use-case for this codec is not restricted to DataStores and/or being serialized to JSON.
Note that this is not a prescriptive solution and by no means the “best” way to do this; this should act as a point of reference for future endeavours in designing function signatures and the overall stylization of your code.
This source is heavily annotated; if you still have questions, feel free to ask!
-- A module that contains utilities in encoding and decoding `Configuration`
-- instances.
local ConfigurationCodec = {};
-- A table that maps a type as returned by `typeof` to the associated
-- `ClassName` of the instance container under a `Configuration` instance.
local TYPE_TO_CLASS_NAME_MAP = {
boolean = "BoolValue",
number = "NumberValue",
string = "StringValue",
table = "Configuration",
BrickColor = "BrickColorValue",
CFrame = "CFrameValue",
Color3 = "Color3Value",
Ray = "RayValue",
Vector3 = "Vector3Value",
Instance = "ObjectValue",
};
-- `assertEncodedConfiguration` returns `true` if the given `tableValue` is a
-- valid return value of `encode`--a table with only string indexes and
-- supported value types as mapped by `TYPE_TO_CLASS_NAME_MAP`.
local function assertEncodedConfiguration(tableValue)
for key, value in pairs(tableValue) do
local keyType = typeof(key);
if keyType ~= "string" then
-- NOTE: Level is set to 3 so the call stack results in where any exported
-- module-level function is called. The standard level of 2 only is
-- effective for functions directly called by the end user consuming this
-- module.
error(
("Can not use %q as a key; only string values allowed."):format(keyType),
3
);
end
local valueType = typeof(value);
local isTypeSupported = TYPE_TO_CLASS_NAME_MAP[valueType] ~= nil;
if not isTypeSupported then
error(("Can not deserialize a %q value."):format(valueType), 3);
end
end
return tableValue;
end
-- `ConfigurationCodec.encode` takes a `Configuration` instance and encodes it
-- to a Lua table to be consumed or can later be decoded through the use of
-- `ConfigurationCodec.decode`. The table is guaranteed only to have string
-- indexes and supported value types as mapped by `TYPE_TO_CLASS_NAME_MAP`. This
-- function also has an optional `strict` parameter that expects a `boolean` if
-- any value. If `strict` has any value but `nil`, `encode` will throw an error if a child
-- of `configuration` is of an invalid class.
function ConfigurationCodec.encode(configuration, strict)
if strict == nil then
strict = true;
end
assert(typeof(configuration) == "Instance" and configuration:IsA("Configuration"));
assert(typeof(strict) == "boolean");
local dictionary = {};
for _, child in ipairs(configuration:GetChildren()) do
local serializedValue;
if child:IsA("Configuration") then
serializedValue = ConfigurationCodec.encode(child, strict);
elseif child:IsA("ValueBase") then
serializedValue = child.Value;
elseif strict then
error(("Can not serialize a %q instance"):format(child.ClassName), 2);
end
dictionary[child.Name] = serializedValue;
end
return dictionary;
end
-- `ConfigurationCodec.decode` takes an encoded `Configuration` as returned by
-- `ConfigurationCodec.encode` and decodes it into a `Configuration` instance.
function ConfigurationCodec.decode(dictionary)
local serializedConfiguration = assertEncodedConfiguration(dictionary);
local configuration = Instance.new("Configuration");
for key, value in pairs(serializedConfiguration) do
local className = TYPE_TO_CLASS_NAME_MAP[typeof(value)];
local instance = Instance.new(className);
if instance:IsA("ValueBase") then
instance.Value = value;
else
instance = ConfigurationCodec.decode(value);
end
instance.Name = key;
instance.Parent = configuration;
end
return configuration;
end
-- 'twould be nice if we shared! Don't want to forget this line:
return ConfigurationCodec;
To ensure this module worked, I ran the following script and it performed as intended:
local ConfigurationCodec = require(script.Parent.ConfigurationCodec);
local data = {
Ryo = 100,
Rank = "E",
LoreName = {
FirstName = "Tetsuto",
LastName = "Jikken",
},
Quests = {},
};
local configuration = ConfigurationCodec.decode(data);
configuration.Parent = workspace;
-- Appears in explorer as intended!
local echoData = ConfigurationCodec.encode(configuration);
print(echoData.LoreName.FirstName);
-- Prints "Tetsuto" as intended!