local M = {}
local VARY_TAG = {}
local function shallow_copy(t)
local out = {}
for k, v in pairs(t) do
out[k] = v
end
return out
end
function M.vary(dim_name)
if type(dim_name) ~= "string" then
error(string.format("vary: name must be string, got %s", type(dim_name)), 2)
end
return function(entries)
if type(entries) ~= "table" or #entries < 1 then
error(string.format("vary '%s': at least 1 entry required", dim_name), 2)
end
local copied = {}
for i, entry in ipairs(entries) do
local c = shallow_copy(entry)
if not c.name then
c.name = string.format("%s_%d", dim_name, i)
end
copied[#copied + 1] = c
end
return {
_tag = VARY_TAG,
dimension = dim_name,
entries = copied,
}
end
end
local function is_vary(v)
return type(v) == "table" and v._tag == VARY_TAG
end
local function merge(base, entry)
local out = shallow_copy(base)
for k, v in pairs(entry) do
if k ~= "name" then
out[k] = v
end
end
return out
end
local function cross_product(dimensions)
local combos = { { name_parts = {}, merged = {} } }
for _, dim in ipairs(dimensions) do
local next_combos = {}
for _, combo in ipairs(combos) do
for _, entry in ipairs(dim.entries) do
local new_parts = shallow_copy(combo.name_parts)
new_parts[#new_parts + 1] = entry.name
local new_merged = merge(combo.merged, entry)
next_combos[#next_combos + 1] = {
name_parts = new_parts,
merged = new_merged,
}
end
end
combos = next_combos
end
return combos
end
local function zip_product(dimensions)
local min_len = math.huge
for _, dim in ipairs(dimensions) do
if #dim.entries < min_len then
min_len = #dim.entries
end
end
local combos = {}
for i = 1, min_len do
local name_parts = {}
local merged = {}
for _, dim in ipairs(dimensions) do
local entry = dim.entries[i]
name_parts[#name_parts + 1] = entry.name
merged = merge(merged, entry)
end
combos[#combos + 1] = {
name_parts = name_parts,
merged = merged,
}
end
return combos
end
function M.generate(spec)
if type(spec) ~= "table" then
error("variants.generate: spec must be a table", 2)
end
local base = spec.base or {}
local mode = spec.mode or "cross"
if mode ~= "cross" and mode ~= "zip" then
error(string.format("variants.generate: mode must be 'cross' or 'zip', got '%s'", mode), 2)
end
local dimensions = {}
for _, v in ipairs(spec) do
if is_vary(v) then
dimensions[#dimensions + 1] = v
end
end
if #dimensions == 0 then
error("variants.generate: at least one vary dimension required", 2)
end
local combos
if mode == "cross" then
combos = cross_product(dimensions)
else
combos = zip_product(dimensions)
end
local results = {}
for _, combo in ipairs(combos) do
local variant = merge(base, combo.merged)
variant.name = table.concat(combo.name_parts, "_")
results[#results + 1] = variant
end
return results
end
return M