Skip to content

Commit b01ad54

Browse files
committed
config/schema: allow to index 'any' type in :get()
`<schema object>:get()` now can access a field inside an `any` type if it is a `table` or `nil`/`box.NULL`. `config:get()` now can access fields inside `app.cfg.<key>` and `roles_cfg.<key>`. Fixes tarantool#10205 NO_DOC=The `<schema object>:get()` update is included into tarantool/doc#4279. The `config:get()` reference on the website doesn't mention the constraint, so it doesn't need an update.
1 parent 7db4de7 commit b01ad54

File tree

4 files changed

+157
-15
lines changed

4 files changed

+157
-15
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
## feature/config
2+
3+
* `<schema object>:get()` now supports indexing values inside an `any` type
4+
(gh-10205).
5+
* `config:get()` now supports indexing values inside `app.cfg.<key>` and
6+
`roles_cfg.<key>` (gh-10205).

src/box/lua/config/utils/schema.lua

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,36 @@ local function get_usage()
643643
'path: nil/string/table)', 0)
644644
end
645645

646+
-- Access a given nested field without checks against a schema.
647+
local function get_schemaless(data, ctx)
648+
local cur = data
649+
650+
while #ctx.journey > 0 do
651+
local requested_field = table.remove(ctx.journey, 1)
652+
assert(requested_field ~= nil)
653+
654+
-- Indexing box.NULL/nil should return nil. There is no
655+
-- reason to follow the path down, because the path is not
656+
-- checked against a schema inside an 'any' scalar and no
657+
-- errors are possible.
658+
if cur == nil then
659+
return nil
660+
end
661+
662+
-- A primitive type can't be indexed.
663+
if type(cur) ~= 'table' then
664+
walkthrough_error(ctx, 'Attempt to index a non-table value ' ..
665+
'(%s) by field %q', type(cur), requested_field)
666+
end
667+
668+
-- Step down.
669+
walkthrough_enter(ctx, requested_field)
670+
cur = cur[requested_field]
671+
end
672+
673+
return cur
674+
end
675+
646676
local function get_impl(schema, data, ctx)
647677
-- The journey is finished. Return what is under the feet.
648678
if #ctx.journey == 0 then
@@ -655,7 +685,10 @@ local function get_impl(schema, data, ctx)
655685
local requested_field = ctx.journey[1]
656686
assert(requested_field ~= nil)
657687

658-
if is_scalar(schema) then
688+
if is_scalar(schema) and schema.type == 'any' then
689+
return get_schemaless(data, ctx)
690+
elseif is_scalar(schema) then
691+
assert(schema.type ~= 'any')
659692
walkthrough_error(ctx, 'Attempt to index a scalar value of type %s ' ..
660693
'by field %q', schema.type, requested_field)
661694
elseif schema.type == 'record' then
@@ -705,7 +738,8 @@ end
705738
-- ('foo.bar' works like foo?.bar in TypeScript).
706739
--
707740
-- The function checks the path against the schema: it doesn't
708-
-- allow to use a non-existing field or index a scalar value.
741+
-- allow to use a non-existing field or index a scalar value
742+
-- (except 'any').
709743
--
710744
-- The path is either array-like table or a string in the dot
711745
-- notation.
@@ -720,9 +754,10 @@ end
720754
-- Nuances:
721755
--
722756
-- * Array indexing is not supported yet.
723-
-- * A scalar of the 'any' type can't be indexed, even when it is
724-
-- a table. It is OK to acquire the whole value of the 'any'
725-
-- type.
757+
-- * A scalar of the 'any' type can be indexed if it is a table
758+
-- or nil/box.NULL. In this case a tail of the path that is
759+
-- inside the 'any' type is not checked against a schema.
760+
-- Indexing nil/box.NULL always returns nil.
726761
function methods.get(self, data, path)
727762
local schema = rawget(self, 'schema')
728763

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
local t = require('luatest')
2+
local cbuilder = require('test.config-luatest.cbuilder')
3+
local cluster = require('test.config-luatest.cluster')
4+
5+
local g = t.group()
6+
7+
g.before_all(cluster.init)
8+
g.after_each(cluster.drop)
9+
g.after_all(cluster.clean)
10+
11+
-- gh-10205: verify that options inside app.cfg.<key> and
12+
-- roles_cfg.<key> can be accessed using config:get().
13+
g.test_basic = function(g)
14+
-- Minimal config.
15+
local mycfg = {foo = {bar = {baz = 42, n = box.NULL}}}
16+
local config = cbuilder.new()
17+
:add_instance('i-001', {})
18+
:set_global_option('app.cfg', mycfg)
19+
:set_global_option('roles_cfg', mycfg)
20+
:config()
21+
22+
-- Minimal cluster.
23+
local cluster = cluster.new(g, config)
24+
cluster:start()
25+
26+
-- Test cases.
27+
cluster['i-001']:exec(function()
28+
local config = require('config')
29+
30+
-- A usual option defined in a strict part of the config.
31+
t.assert_equals(config:get('memtx.memory'), 256 * 1024 * 1024)
32+
33+
-- An option inside app.cfg.<key> or roles_cfg.<key>.
34+
t.assert_equals(config:get('app.cfg.foo.bar.baz'), 42)
35+
t.assert_equals(config:get('roles_cfg.foo.bar.baz'), 42)
36+
37+
-- A missed option inside app.cfg.<key> or
38+
-- roles_cfg.<key>.
39+
t.assert_type(config:get('app.cfg.x.y.z'), 'nil')
40+
t.assert_type(config:get('roles_cfg.x.y.z'), 'nil')
41+
42+
-- A box.NULL (null is YAML) option inside app.cfg.<key>
43+
-- or roles_cfg.<key>.
44+
t.assert_type(config:get('app.cfg.foo.bar.n'), 'cdata')
45+
t.assert_type(config:get('roles_cfg.foo.bar.n'), 'cdata')
46+
47+
-- Indexing a box.NULL option inside app.cfg.<key> or
48+
-- roles_cfg.<key>.
49+
t.assert_type(config:get('app.cfg.foo.bar.n.i'), 'nil')
50+
t.assert_type(config:get('roles_cfg.foo.bar.n.i'), 'nil')
51+
52+
-- Attempt to index a primitive value (except nil/box.NULL)
53+
-- inside app.cfg.<key>.
54+
local exp_err_msg = '[instance_config] app.cfg.foo.bar.baz: ' ..
55+
'Attempt to index a non-table value (number) by field "fiz"'
56+
t.assert_error_msg_equals(exp_err_msg, function()
57+
config:get('app.cfg.foo.bar.baz.fiz')
58+
end)
59+
60+
-- The same inside roles_cfg.<key>.
61+
local exp_err_msg = '[instance_config] roles_cfg.foo.bar.baz: ' ..
62+
'Attempt to index a non-table value (number) by field "fiz"'
63+
t.assert_error_msg_equals(exp_err_msg, function()
64+
config:get('roles_cfg.foo.bar.baz.fiz')
65+
end)
66+
end)
67+
end

test/config-luatest/schema_test.lua

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,6 +1102,49 @@ g.test_get_nested_any_in_record = function()
11021102
t.assert_equals(s:get(data, 'foo.bar'), scalar_data)
11031103
end
11041104

1105+
-- Verify that :get() allows to get a nested value from a scalar
1106+
-- of the any type.
1107+
g.test_get_nested_in_any = function()
1108+
local s = schema.new('myschema', schema.scalar({type = 'any'}))
1109+
1110+
-- Existing data.
1111+
local data = {foo = {bar = 'baz'}}
1112+
t.assert_equals(s:get(data, 'foo'), {bar = 'baz'})
1113+
t.assert_equals(s:get(data, 'foo.bar'), 'baz')
1114+
1115+
-- Non-existing data. Verify that :get() works in the optional
1116+
-- chaining way.
1117+
local data = {}
1118+
t.assert_type(s:get(data, 'foo'), 'nil')
1119+
t.assert_type(s:get(data, 'foo.bar'), 'nil')
1120+
1121+
-- The same, but the non-existing field is box.NULL.
1122+
--
1123+
-- Indexing of nil/box.NULL gives nil.
1124+
local data = {foo = box.NULL}
1125+
t.assert_type(s:get(data, 'foo.bar'), 'nil')
1126+
1127+
-- The same, but nil/box.NULL is on the 'any' scalar level,
1128+
-- not inside.
1129+
local data = nil
1130+
t.assert_type(s:get(data, 'foo'), 'nil')
1131+
local data = box.NULL
1132+
t.assert_type(s:get(data, 'foo'), 'nil')
1133+
1134+
-- If the path points to box.NULL, it is returned as is (not
1135+
-- as nil).
1136+
local data = {foo = box.NULL}
1137+
t.assert_type(s:get(data, 'foo'), 'cdata')
1138+
1139+
-- Attempt to index a primitive value (except nil/box.NULL).
1140+
local exp_err_msg = '[myschema] foo.bar: Attempt to index a non-table ' ..
1141+
'value (number) by field "baz"'
1142+
t.assert_error_msg_equals(exp_err_msg, function()
1143+
local data = {foo = {bar = 42}}
1144+
s:get(data, 'foo.bar.baz')
1145+
end)
1146+
end
1147+
11051148
-- Index a map.
11061149
g.test_get_nested_in_map = function()
11071150
local s = schema.new('myschema', schema.map({
@@ -1211,23 +1254,14 @@ end
12111254

12121255
-- Attempt to index a scalar value.
12131256
g.test_get_index_scalar = function()
1214-
-- Indexing a scalar is forbidden.
1257+
-- Indexing a scalar is forbidden (except 'any').
12151258
local s = schema.new('myschema', schema.scalar({type = 'string'}))
12161259
local exp_err_msg = '[myschema] Attempt to index a scalar value of type ' ..
12171260
'string by field "foo"'
12181261
t.assert_error_msg_equals(exp_err_msg, function()
12191262
local data = 5
12201263
s:get(data, 'foo')
12211264
end)
1222-
1223-
-- The scalar of the 'any' type is the same in this regard.
1224-
local s = schema.new('myschema', schema.scalar({type = 'any'}))
1225-
local exp_err_msg = '[myschema] Attempt to index a scalar value of type ' ..
1226-
'any by field "foo"'
1227-
t.assert_error_msg_equals(exp_err_msg, function()
1228-
local data = {foo = {bar = 'baz'}}
1229-
s:get(data, 'foo')
1230-
end)
12311265
end
12321266

12331267
-- }}} <schema object>:get()

0 commit comments

Comments
 (0)