scansion/stanzacmp.lua

Thu, 23 Mar 2023 18:28:20 +0000

author
Matthew Wild <mwild1@gmail.com>
date
Thu, 23 Mar 2023 18:28:20 +0000
changeset 181
3a9b9c98304a
parent 170
db73c4c317ce
permissions
-rw-r--r--

Add support for component connections

-- This is my attempt at a utility library to compare two XMPP stanzas
-- It is not testing for exact equivalency, but through some vague rules that felt right.
-- For example, the second stanza passed to stanzas_match() is allowed to have unexpected
-- elements and attributes at the top level. Beyond this, they must match exactly, except
-- for whitespace differences only.
--
-- There are probably bugs, and it can probably be smarter, but I don't want to spend too
-- much time on it right now.

local function trim(s)
	return (s:gsub("^%s+", ""):gsub("%s+$", ""));
end

local function wants_strict(tag, default)
	local opt = tag.attr["scansion:strict"] or default or "yes";
	if opt == "no" or opt == "false" or opt == "0" then
		return false;
	elseif opt == "yes" or opt == "true" or opt == "1" then
		return true;
	end
	error("Unexpected scansion:strict value: "..opt);
end

local function is_wildcard(k, v)
	if v == "{scansion:any}" then
		return "attr:"..k;
	end
	return (v:match("^{scansion:capture:([^}]+)}$"));
end

-- stanza1 == expected, stanza2 == variable
-- captures is an optional table to store captures (captures["foo"] == {scansion:capture:foo})
local function stanzas_strict_match(stanza1, stanza2, captures)
	if stanza1.name ~= stanza2.name or stanza1.attr.xmlns ~= stanza2.attr.xmlns then
		return false;
	end
	
	for k, v in pairs(stanza1.attr) do
		local wildcard = is_wildcard(k, v);
		if not k:match("^scansion:") and not wildcard and stanza2.attr[k] ~= v then
			return false;
		end
		if wildcard and captures then
			captures[wildcard] = stanza2.attr[k];
		end
	end
	
	for k, v in pairs(stanza2.attr) do
		local wildcard = is_wildcard(k, stanza1.attr[k]);
		if not wildcard and stanza1.attr[k] ~= v then
			return false;
		end
		if wildcard and captures then
			captures[wildcard] = v;
		end
	end
	
	if #stanza1.tags ~= #stanza2.tags then
		return false;
	end
	
	local stanza2_pos = 1;
	for _, child in ipairs(stanza1) do
		if type(child) == "table" or child:match("%S") then
			local match;
			local child2 = stanza2[stanza2_pos];
			while child2 and not(type(child2) == "table" or child2:match("%S")) do
				stanza2_pos = stanza2_pos + 1;
				child2 = stanza2[stanza2_pos];
			end
			if type(child) ~= type(child2) then
				return false;
			end
			if type(child) == "table" and child2.name == child.name and child2.attr.xmlns == child.attr.xmlns then
				-- Strict deep match
				match = stanzas_strict_match(child, child2, captures);
			elseif type(child) == "string" then -- Text nodes, must be equal, ignoring leading/trailing whitespace
				match = trim(child) == trim(child2);
			end
			if not match then
				return false;
			end
			stanza2_pos = stanza2_pos + 1;
		end
	end
	return true;
end

-- Everything in stanza1 should be present in stanza2

local function stanzas_match(stanza1, stanza2, captures)
	if wants_strict(stanza1, stanza1.attr.xmlns == nil and "no" or "yes") then
		return stanzas_strict_match(stanza1, stanza2, captures);
	end
	if stanza1.name ~= stanza2.name or stanza1.attr.xmlns ~= stanza2.attr.xmlns then
		return false;
	end
	
	for k, v in pairs(stanza1.attr) do
		local wildcard = is_wildcard(k, v);
		if not k:match("^scansion:") and not wildcard and stanza2.attr[k] ~= v then
			return false;
		end
		if wildcard and captures then
			captures[wildcard] = stanza2.attr[k];
		end
	end
	
	local matched_children = {};
	for _, child in ipairs(stanza1) do
		if type(child) == "table" or child:match("%S") then
			local match;
			-- Iterate through remaining nodes in stanza2, looking for a match
			local stanza2_pos = 1;
			while stanza2_pos <= #stanza2 do
				if not matched_children[stanza2_pos] then
					local child2 = stanza2[stanza2_pos];
					stanza2_pos = stanza2_pos + 1;

					if type(child2) == type(child) then
						if type(child) == "table" and child2.name == child.name and child2.attr.xmlns == child.attr.xmlns then
							match = stanzas_match(child, child2, captures);
						elseif type(child) == "string" then -- Text nodes, must be equal, ignoring leading/trailing whitespace
							match = trim(child) == trim(child2);
						end
						if match then
							break;
						end
					end
				end
			end
			if not match then
				--print(_, "No match for ", child:pretty_print())
				return false;
			end
		end
	end
	return true;
end


return {
	stanzas_match = stanzas_match;
	stanzas_strict_match = stanzas_strict_match;
};

mercurial