scansion/stanzacmp.lua

Sun, 30 Dec 2018 09:43:36 +0000

author
Matthew Wild <mwild1@gmail.com>
date
Sun, 30 Dec 2018 09:43:36 +0000
changeset 164
14500a149b31
parent 153
f83ea6e5c3d8
child 170
db73c4c317ce
permissions
-rw-r--r--

client: Ignore timeout timer if we received a stanza

-- 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 stanzas_strict_match(stanza1, stanza2)
	if stanza1.name ~= stanza2.name or stanza1.attr.xmlns ~= stanza2.attr.xmlns then
		return false;
	end
	
	for k, v in pairs(stanza1.attr) do
		if not k:match("^scansion:") and v ~= "{scansion:any}" and stanza2.attr[k] ~= v then
			return false;
		end
	end
	
	for k, v in pairs(stanza2.attr) do
		if stanza1.attr[k] ~= "{scansion:any}" and stanza1.attr[k] ~= v then
			return false;
		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);
			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)
	if wants_strict(stanza1, stanza1.attr.xmlns == nil and "no" or "yes") then
		return stanzas_strict_match(stanza1, stanza2);
	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
		if not k:match("^scansion:") and v ~= "{scansion:any}" and stanza2.attr[k] ~= v then
			return false;
		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);
						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