Last active
October 7, 2015 18:37
-
-
Save mejedi/a61c4e2a0140bf1f0401 to your computer and use it in GitHub Desktop.
RFC: Cfg module
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
cfg = require('config').new() | |
cfg.reg('foo', 42) | |
cfg.reg('bar', 'example.org') | |
cfg.reg('bazz') | |
cfg.load{} | |
print('Adding listener (monitors foo and bar)') | |
cfg.add_listener(function (cfg_data) | |
print('<...> foo: ' .. tostring(cfg_data.foo) .. ', bar: ' .. tostring(cfg_data.bar)) | |
end) | |
print('Updating foo') | |
cfg.update{foo = 0} | |
print('Touching foo, value not changed') | |
cfg.update{foo = 0} | |
print('Updating bar') | |
cfg.update{bar = 'example.org:8080'} | |
print('Touching bar, value not changed') | |
cfg.update{bar = 'example.org:8080'} | |
print('Updating unrelated key') | |
cfg.update{bazz = 123456} | |
print('Loading default config') | |
cfg.load{} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Adding listener (monitors foo and bar) | |
<...> foo: 42, bar: example.org | |
Updating foo | |
<...> foo: 0, bar: example.org | |
Touching foo, value not changed | |
Updating bar | |
<...> foo: 0, bar: example.org:8080 | |
Touching bar, value not changed | |
Updating unrelated key | |
Loading default config | |
<...> foo: 42, bar: example.org |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
cfg = require('config').new() | |
cfg.reg('listen', nil, 'string, number') | |
cfg.reg('work_dir', '.', 'string') | |
cfg.reg('sophia.memory_limit', 0, 'number') | |
print('Adding listener. It runs now for the first time') | |
cfg.add_listener(function (cfg_data) | |
print('<...> listen: ' .. tostring(cfg_data.listen)) | |
end) | |
print('Loading default config') | |
cfg.load() | |
print('Current sophia.memory_limit: ' .. tostring(cfg.data.sophia.memory_limit)) | |
print('Current work_dir: ' .. tostring(cfg.get('work_dir'))) | |
print('Updating work dir (/tmp)') | |
cfg.update{work_dir = '/tmp'} | |
print('Updated work_dir: ' .. tostring(cfg.get('work_dir'))) | |
print('Notice: no message about listen changing') | |
print('Now updating listen (example.org). This will trigger the listener') | |
cfg.update{listen = 'example.org'} | |
print('Setting listen to the same value (example.org)') | |
cfg.update{listen = 'example.org'} | |
print('Notice: no message about listen changing') | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Adding listener. It runs now for the first time | |
<...> listen: nil | |
Loading default config | |
Current sophia.memory_limit: 0 | |
Current work_dir: . | |
Updating work dir (/tmp) | |
Updated work_dir: /tmp | |
Notice: no message about listen changing | |
Now updating listen (example.org). This will trigger the listener | |
<...> listen: example.org | |
Setting listen to the same value (example.org) | |
Notice: no message about listen changing |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local config_module = {} | |
-- '<opt_tag>a.b.c.d' -> '<opt_tag>', 'a.b.c.d' | |
local function extract_tag(path) | |
local tag = nil | |
path = string.gsub(path, '^(<[^>]+>)', | |
function (match) tag = match; return '' end) | |
return tag, path | |
end | |
-- create_schema_node() -> | |
-- true, node if a new node was created | |
-- false, node if returning existing node | |
local function create_schema_node(schema, path) | |
local node = nil | |
local did_create = false | |
local ns = schema | |
local part | |
for part in string.gmatch(path, '[^.]+') do | |
if ns then | |
node = ns[part] | |
else | |
-- create 'nested' in parent | |
ns = {} | |
node.nested = ns | |
node = nil | |
end | |
if node then | |
ns = node.nested | |
else | |
did_create = true | |
node = {} | |
ns[part] = node | |
ns = nil -- defer 'nested' creation | |
end | |
end | |
return did_create, node | |
end | |
-- equality check (builtin == performs shallow compare) | |
local function deep_equal(a, b) | |
if type(a) ~= 'table' then | |
return a == b | |
elseif type(b) ~= 'table' then | |
return false | |
else | |
local k, v, _ | |
for k, v in pairs(a) do | |
if not deep_equal(v, b[k]) then | |
return false | |
end | |
end | |
for k, _ in pairs(b) do | |
if not a[k] then | |
return false | |
end | |
end | |
return true | |
end | |
end | |
-- Validates new_data according to the schema; | |
-- augments the missing keys from old_data and defaults; | |
-- complains if a frozen node changes; | |
-- if a changed node has listeners adds them to scheduled_listeners (set); | |
-- returns | |
-- true, result if the produced result is different from ref_data | |
-- false, result otherwise | |
local function render_updated_data(node, frozen_tags, | |
new_data, old_data, ref_data, | |
scheduled_listeners) | |
local did_change = false | |
local result = old_data or node.default | |
if not new_data and result then | |
-- fixme: this won't augment old_data with default keys, | |
-- can happen if new keys with defaults were registered | |
-- since last load | |
if result ~= ref_data then | |
did_change = not deep_equal(result, ref_data) | |
end | |
else | |
local nested = node.nested | |
if not nested or new_data and type(new_data) ~= 'table' then | |
-- unable to perform a merge reasonably | |
result = new_data | |
did_change = (new_data ~= ref_data and | |
not deep_equal(new_data, ref_data)) | |
else | |
-- check keys and merge | |
result = {} | |
new_data = new_data or {} | |
local safe_old_data = ( | |
type(old_data) == 'table' and old_data or {}) | |
local safe_ref_data = ( | |
type(ref_data) == 'table' and ref_data or {}) | |
local k, nested_node, _ | |
for k, nested_node in pairs(nested) do | |
local nested_did_change | |
nested_did_change, result[k] = render_updated_data( | |
nested_node, frozen_tags, | |
new_data[k], safe_old_data[k], safe_ref_data[k], | |
scheduled_listeners) | |
did_change = did_change or nested_did_change | |
end | |
for k, _ in pairs(new_data) do | |
if not nested[k] then | |
error('Validation failed: Unknown key: ' .. k) | |
end | |
end | |
end | |
end | |
local custom_filter = node.custom_filter | |
if custom_filter then | |
local custom_filter_res = custom_filter(result, old_data) | |
if custom_filter_res and custom_filter_res ~= result then | |
-- arbitrary changes could've happened | |
did_change = not deep_equal(result, old_data) | |
end | |
end | |
local valid_types = node.valid_types | |
if valid_types and result and not valid_types[type(result)] then | |
error('Validation failed: Wrong type') | |
end | |
if did_change then | |
if (node.frozen or frozen_tags[node.tag]) then | |
error('Validation failed: Attempted to change a frozen key') | |
end | |
local listeners = node.listeners | |
if listeners then | |
local listener, _ | |
for listener, _ in pairs(listeners) do | |
scheduled_listeners[listener] = true | |
end | |
end | |
end | |
return did_change, result | |
end | |
-- Invokes a listener; | |
-- builds a set of nodes accessed during execution (subscriptions); | |
-- updates the schema removing the listener from nodes that are no longer relevant | |
-- and installing it in relevant ones; | |
-- returns the updated subscriptions set. | |
local function invoke_listener(listener, schema_root, data, cur_subscriptions) | |
local upd_subscriptions = {} | |
local function wrap(schema_node, data) | |
local m = {} | |
function m.__index(_, key) | |
local v = data[key] | |
local ns = schema_node.nested | |
local next_schema_node = ns and ns[key] | |
if type(v) == 'table' and next_schema_node then | |
return wrap(next_schema_node, v) | |
end | |
upd_subscriptions[next_schema_node or schema_node] = true | |
return v | |
end | |
function m.__tostring(_) | |
upd_subscriptions[schema_node] = true | |
return tostring(data) | |
end | |
function m.__pairs(_) | |
upd_subscriptions[schema_node] = true | |
return pairs(data) | |
end | |
function m.__ipairs(_) | |
upd_subscriptions[schema_node] = true | |
return ipairs(data) | |
end | |
function m.__len() | |
upd_subscriptions[schema_node] = true | |
return #data | |
end | |
-- todo: other metamethods? | |
local o = {} | |
setmetatable(o, m) | |
return o | |
end | |
pcall(listener, type(data) == 'table' and wrap(schema_root, data) or data) | |
local node, _ | |
for node, _ in pairs(cur_subscriptions) do | |
if not upd_subscriptions[node] then | |
-- remove listener | |
local listeners = node.listeners | |
if listeners then | |
listeners[listener] = nil | |
end | |
end | |
end | |
for node, _ in pairs(upd_subscriptions) do | |
if not cur_subscriptions[node] then | |
-- install listener | |
local listeners = node.listeners | |
if not listeners then | |
listeners = {} | |
node.listeners = listeners | |
end | |
listeners[listener] = true | |
end | |
end | |
return upd_subscriptions | |
end | |
function config_module.new() | |
local cfg = {} | |
local data = {} | |
local schema = {} | |
local schema_root = {nested = schema} | |
local frozen_tags = {} | |
-- listener -> list of affecting schema nodes | |
local subscriptions = {} | |
cfg.data = data | |
function cfg._schema() | |
return schema | |
end | |
function cfg.get(name) | |
return data[name] | |
end | |
function cfg.reg(name, default, valid_types, custom_filter) | |
local tag, path = extract_tag(name) | |
if path == '' then | |
error('Bad name: ' .. name) | |
end | |
local did_create, node = create_schema_node(schema, path) | |
if not did_create and node.is_explicit then | |
error('Name already defined: ' .. name) | |
end | |
node.is_explicit = true | |
if tag then | |
node.tag = tag | |
end | |
if default then | |
node.default = default | |
end | |
if custom_filter then | |
node.custom_filter = custom_filter | |
end | |
if valid_types then | |
local types = {} | |
local type_name | |
valid_types = string.gsub(valid_types, ' ', '') | |
for type_name in string.gmatch(valid_types, '[^,]+') do | |
types[type_name] = true | |
end | |
if #types and not types.any then | |
node.valid_types = types | |
end | |
end | |
end | |
local function do_load_or_update(new_data, old_data) | |
local scheduled_listeners = {} | |
local _, result = render_updated_data( | |
schema_root, frozen_tags, new_data, old_data, data, | |
scheduled_listeners) | |
data = result | |
cfg.data = data | |
local listener, _ | |
for listener, _ in pairs(scheduled_listeners) do | |
subscriptions[listener] = invoke_listener( | |
listener, schema_root, data, subscriptions[listener]) | |
end | |
end | |
function cfg.load_file(path) | |
error('not implemented') | |
end | |
function cfg.load(lua_table) | |
do_load_or_update(lua_table, nil) | |
end | |
function cfg.update(lua_table) | |
do_load_or_update(lua_table, data) | |
end | |
function cfg.freeze(name) | |
local tag, path = extract_tag(name) | |
if tag then | |
if path ~= '' then | |
error('Both a tag and a name provided: ' .. name) | |
end | |
frozen_tags[tag] = true | |
end | |
if path == '' then | |
error('Expecting a tag or a name') | |
end | |
local _, node = create_schema_node(schema, path) | |
node.frozen = true | |
end | |
function cfg.add_listener(listener) | |
local exists = subscriptions[listener] | |
if exists then | |
return | |
end | |
subscriptions[listener] = invoke_listener( | |
listener, schema_root, data, {}) | |
end | |
return cfg | |
end | |
return config_module | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment