My Instance "parser" script, what can I change about it?

Here’s the code for my Instance → Dictionary script. It can also go back into Instance form and vice-versa.

CODE
local Parser = {}

local function parse(instance, tbl)
	
	for _, v in pairs(instance:GetChildren()) do
		if v:IsA("Configuration") then
			tbl[v.Name] = {}
			parse(v, tbl[v.Name])
		elseif v:IsA("ValueBase") then
			tbl[v.Name] = v.Value
		end
	end
	
end

function Parser.ParseData(instance, tbl)
	local serialize = {}
	
	parse(instance, serialize)
	
	for k, v in pairs(serialize) do
		print(k,v)
	end
	
	return serialize
end

function Parser.UnparseData(tbl, parent)
	for k, v in pairs(tbl) do
		if type(v) == "table" then
			local value = Instance.new("Configuration")
			value.Name = k
			value.Parent = parent
			Parser.UnparseData(v, value)
		elseif type(v) == "number" then
			local value = Instance.new("NumberValue")
			value.Name = k
			value.Value = v
			value.Parent = parent
		elseif type(v) == "string" then
			local value = Instance.new("StringValue")
			value.Name = k
			value.Value = v
			value.Parent = parent
		elseif type(v) == "boolean" then
			local value = Instance.new("BoolValue")
			value.Name = k
			value.Value = v
			value.Parent = parent
		end
	end
end

return Parser

In some implementation with a DataStore, I can do stuff like this.

local defaultData = {
	["Ryo"] = 100,
	["Rank"] = "E",
	["LoreName"] = {
		["FirstName"] = "Tetsuto",
		["LastName"] = "Jikken"
	},
	["Quests"] = {}
}

Then I can “un-parse” this data and turn it into an configuration of values.

image

It also saves any new values added which is handy for stuff like quests.

As shown in this video, the saving of new instances is very handy.

Any feedback on anything that can be changed?

1 Like

You could probably simplify the UnparseData function.

function Parser.UnparseData(tbl, parent)
	for k, v in pairs(tbl) do
		local t = type(v)
		local targets = {
			["table"] = "Configuration";
			["number"] = "NumberValue";
			["string"] = "StringValue";
			["boolean"] = "BoolValue";
		}
		
		local target = targets[t]
		if (target) then
			local value = Instance.new(target)
			value.Name = k
			value.Parent = parent
			if (target == "Configuration") then
				Parser.UnparseData(v, value)
			else
				value.Value = v
			end
		end
		
	end
end

Not really sure if that would be a cleaner solution but it is a different way of doing it.

I split it up.

local function getValueType(val)
	
	local types = {
		["table"] = "Configuration",
		["number"] = "NumberValue",
		["string"] = "StringValue",
		["boolean"] = "BoolValue"
	}
	
	for k, v in pairs(types) do
		if val == k then
			return v
		end
	end
	
end

function Parser.UnparseData(tbl, parent)
	for k, v in pairs(tbl) do
		local valueType = getValueType(type(v))
		
		local value = Instance.new(valueType)
		value.Name = k
		value.Parent = parent
		
		if valueType == "Configuration" then
			Parser.UnparseData(v, value)
		else
			value.Value = v
		end
		
	end
end

Makes it look pretty clean.

1 Like

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!