Let the framework own sets and lists instead of the features

Feature.to_set made every feature turn its config lists into sets on
each lookup, which is work the framework should do once. Sets are now a
field type of their own. Feature.set{...} declares one, the controller
sends a list, and _apply converts it when the override is applied, on
load and on update alike. Features index the config directly, and
to_set and its cache are gone.

Lists are explicit too. Feature.list{...} declares one, where before
any table of strings, and any empty table, counted as a list. Untagged
tables with string keys are nested sections, and other tables can not
be overridden. Feature.optional uses the same marker as list and set.

On the web side a string list can no longer name an input component,
so the cast that made one look like an object field is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bbassieandClaude Opus 5.5 committed 2026-10-01 19:40:33 +00:00
1 parent 628e4df514
commit f8ca285ff6
9 files changed
+128 -90

No files matched your search

+18 -14
View File
@@ -34,7 +34,7 @@ Suite.test("update_config() changes the config in place", function(env)
end)
Suite.test("update_config() raises on_config_updated for each changed value", function(env)
local feature = env.Feature.register("test", { flag = true, names = { "a" }, section = { count = 1 } })
local feature = env.Feature.register("test", { flag = true, names = env.Feature.set{ "a" }, section = { count = 1 } })
feature:update_config(false, { names = { "a" }, ["section.count"] = 2 })
Suite.eq(#env.events, 2, "unchanged values raise nothing")
Suite.eq(env.Feature.events.on_config_updated, assert(env.events[1]).name, "the event is on_config_updated")
@@ -97,21 +97,33 @@ Suite.test("register() addresses nested values by path", function(env)
local feature = env.Feature.register("test", { section = { flag = true, count = 1 }, offset = offset })
feature:update_config(true, { ["section.flag"] = false, offset = { "x" } })
Suite.eq(feature.config.section, { flag = false, count = 1 }, "the nested value changes in place")
Suite.eq(feature.config.offset, offset, "lists of other types can not be overridden")
Suite.eq(feature.config.offset, offset, "tables which are not sections can not be overridden")
feature:update_config(true, {})
Suite.eq(feature.config.section.flag, true, "and goes back to its default")
end)
Suite.test("register() allows lists of strings to be overridden", function(env)
local feature = env.Feature.register("test", { names = { "a", "b" }, empty = {} })
feature:update_config(true, { names = { "c" }, empty = { "d" } })
Suite.test("list() declares a list of strings which can be overridden", function(env)
local feature = env.Feature.register("test", { names = env.Feature.list{ "a", "b" }, empty = env.Feature.list{}, plain = { "x" } })
Suite.eq(feature.config.names, { "a", "b" }, "the default is the list")
feature:update_config(true, { names = { "c" }, empty = { "d" }, plain = { "y" } })
Suite.eq(feature.config.names, { "c" }, "a list is replaced")
Suite.eq(feature.config.empty, { "d" }, "an empty default is a list")
Suite.eq(feature.config.empty, { "d" }, "an empty list can be overridden")
Suite.eq(feature.config.plain, { "x" }, "a plain table can not be overridden")
feature:update_config(true, { names = { 1 } })
Suite.eq(feature.config.names, { "a", "b" }, "a list with other types is invalid")
end)
Suite.test("set() declares a set of strings which is overridden with a list", function(env)
local feature = env.Feature.register("test", { names = env.Feature.set{ "a" }, empty = env.Feature.set{} })
Suite.eq(feature.config.names, { a = true }, "the default is a set")
Suite.eq(feature.config.empty, {}, "and can be empty")
feature:update_config(true, { names = { "b", "c" } })
Suite.eq(feature.config.names, { b = true, c = true }, "the list is converted to a set")
env.on_load{ test = { enabled = true, names = { "d" } } }
Suite.eq(feature.config.names, { d = true }, "also when the overrides are loaded")
end)
Suite.test("optional() declares a value without a default", function(env)
local feature = env.Feature.register("test", { role_id = env.Feature.optional("number") })
Suite.eq(feature.config.role_id, nil, "nil until overridden")
@@ -121,14 +133,6 @@ Suite.test("optional() declares a value without a default", function(env)
Suite.eq(feature.config.role_id, nil, "and back to nil")
end)
Suite.test("to_set() is cached until the list is replaced", function(env)
local feature = env.Feature.register("test", { names = { "a" } })
local set = env.Feature.to_set(feature.config.names)
Suite.eq(set, { a = true }, "a set of the items")
Suite.eq(env.Feature.to_set(feature.config.names) == set, true, "the same set while the list is unchanged")
env.on_load{ test = { enabled = true, names = { "b" } } }
Suite.eq(env.Feature.to_set(feature.config.names), { b = true }, "a new set after load replaces the list")
end)
Suite.test("config files register their feature", function(env)
local feature = env.load_config("popups")