Skip to content

Commit 6dc0405

Browse files
committed
Introduce new Lua helper fuctions for working with Lua arrays
New functions luaX_is_empty_table(), luaX_is_array() and luaX_for_each() take some of the boilerplate out of working with Lua arrays.
1 parent efb0072 commit 6dc0405

File tree

6 files changed

+222
-46
lines changed

6 files changed

+222
-46
lines changed

src/flex-lua-index.cpp

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,18 @@ static void check_and_add_columns(flex_table_t const &table,
3232
std::vector<std::string> *columns,
3333
lua_State *lua_state)
3434
{
35-
lua_pushnil(lua_state);
36-
while (lua_next(lua_state, -2) != 0) {
37-
if (!lua_isnumber(lua_state, -2)) {
38-
throw std::runtime_error{
39-
"The 'column' field must contain a string or an array."};
40-
}
35+
if (!luaX_is_array(lua_state)) {
36+
throw std::runtime_error{
37+
"The 'column' field must contain a string or an array."};
38+
}
39+
40+
luaX_for_each(lua_state, [&]() {
4141
if (!lua_isstring(lua_state, -1)) {
4242
throw std::runtime_error{
4343
"The entries in the 'column' array must be strings."};
4444
}
4545
check_and_add_column(table, columns, lua_tostring(lua_state, -1));
46-
lua_pop(lua_state, 1); // table
47-
}
46+
});
4847
}
4948

5049
void flex_lua_setup_index(lua_State *lua_state, flex_table_t *table)

src/lua-utils.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,34 @@ int luaX_pcall(lua_State *lua_state, int narg, int nres)
202202
}
203203

204204
#endif
205+
206+
bool luaX_is_empty_table(lua_State *lua_state)
207+
{
208+
assert(lua_istable(lua_state, -1));
209+
lua_pushnil(lua_state);
210+
if (lua_next(lua_state, -2) == 0) {
211+
return true;
212+
}
213+
lua_pop(lua_state, 2);
214+
return false;
215+
}
216+
217+
bool luaX_is_array(lua_State *lua_state)
218+
{
219+
// Checking that a Lua table is an array is surprisingly difficult.
220+
// This code is based on:
221+
// https://web.archive.org/web/20140227143701/http://ericjmritz.name/2014/02/26/lua-is_array/
222+
assert(lua_istable(lua_state, -1));
223+
int i = 0;
224+
lua_pushnil(lua_state);
225+
while (lua_next(lua_state, -2) != 0) {
226+
++i;
227+
lua_rawgeti(lua_state, -3, i);
228+
if (lua_isnil(lua_state, -1)) {
229+
lua_pop(lua_state, 3);
230+
return false;
231+
}
232+
lua_pop(lua_state, 2);
233+
}
234+
return true;
235+
}

src/lua-utils.hpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ extern "C"
1818
#include <lua.h>
1919
}
2020

21+
#include <cassert>
2122
#include <cstdint>
2223
#include <utility>
2324

@@ -64,4 +65,47 @@ bool luaX_get_table_bool(lua_State *lua_state, char const *key, int table_index,
6465

6566
int luaX_pcall(lua_State *lua_state, int narg, int nres);
6667

68+
/**
69+
* Returns true if the value on top of the stack is an empty Lua table.
70+
*
71+
* \pre Value on top of the Lua stack must be a Lua table.
72+
* \post Stack is unchanged.
73+
*/
74+
bool luaX_is_empty_table(lua_State *lua_state);
75+
76+
/**
77+
* Check that the value on the top of the Lua stack is a simple array.
78+
* This means that all keys must be consecutive integers starting from 1.
79+
*
80+
* \returns True if this is an array (also for Lua tables without any items)
81+
*
82+
* \pre Value on top of the Lua stack must be a Lua table.
83+
* \post Stack is unchanged.
84+
*/
85+
bool luaX_is_array(lua_State *lua_state);
86+
87+
/**
88+
* Call a function for each item in a Lua array table. The item value will
89+
* be on the top of the stack inside that function.
90+
*
91+
* \pre Value on top of the Lua stack must be a Lua array table.
92+
* \pre The function must leave the Lua stack in the same condition it found
93+
* it in.
94+
* \post Stack is unchanged.
95+
*/
96+
template <typename FUNC>
97+
void luaX_for_each(lua_State *lua_state, FUNC &&func)
98+
{
99+
assert(lua_istable(lua_state, -1));
100+
lua_pushnil(lua_state);
101+
while (lua_next(lua_state, -2) != 0) {
102+
#ifndef NDEBUG
103+
int const top = lua_gettop(lua_state);
104+
#endif
105+
std::forward<FUNC>(func)();
106+
assert(top == lua_gettop(lua_state));
107+
lua_pop(lua_state, 1);
108+
}
109+
}
110+
67111
#endif // OSM2PGSQL_LUA_UTILS_HPP

src/output-flex.cpp

Lines changed: 31 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -577,13 +577,11 @@ void output_flex_t::setup_flex_table_columns(flex_table_t *table)
577577
table->name());
578578
}
579579

580+
if (!luaX_is_array(lua_state())) {
581+
throw std::runtime_error{"The 'columns' field must contain an array."};
582+
}
580583
std::size_t num_columns = 0;
581-
lua_pushnil(lua_state());
582-
while (lua_next(lua_state(), -2) != 0) {
583-
if (!lua_isnumber(lua_state(), -2)) {
584-
throw std::runtime_error{
585-
"The 'columns' field must contain an array."};
586-
}
584+
luaX_for_each(lua_state(), [&]() {
587585
if (!lua_istable(lua_state(), -1)) {
588586
throw std::runtime_error{
589587
"The entries in the 'columns' array must be tables."};
@@ -611,16 +609,15 @@ void output_flex_t::setup_flex_table_columns(flex_table_t *table)
611609
column.type() == table_column_type::area) {
612610
column.set_projection(lua_tostring(lua_state(), -1));
613611
} else {
614-
throw std::runtime_error{
615-
"Projection can only be set on geometry and area columns."};
612+
throw std::runtime_error{"Projection can only be set on "
613+
"geometry and area columns."};
616614
}
617615
}
618616

619-
// stack has: projection, create_only, not_null, sql_type, column,
620-
// type, table
621-
lua_pop(lua_state(), 7);
617+
// stack has: projection, create_only, not_null, sql_type, column, type
618+
lua_pop(lua_state(), 6);
622619
++num_columns;
623-
}
620+
});
624621

625622
if (num_columns == 0 && !table->has_id_column()) {
626623
throw fmt_error("No columns defined for table '{}'.", table->name());
@@ -655,21 +652,18 @@ void output_flex_t::setup_indexes(flex_table_t *table)
655652
table->name());
656653
}
657654

658-
lua_pushnil(lua_state());
659-
while (lua_next(lua_state(), -2) != 0) {
660-
if (!lua_isnumber(lua_state(), -2)) {
661-
throw std::runtime_error{
662-
"The 'indexes' field must contain an array."};
663-
}
655+
if (!luaX_is_array(lua_state())) {
656+
throw std::runtime_error{"The 'indexes' field must contain an array."};
657+
}
658+
659+
luaX_for_each(lua_state(), [&]() {
664660
if (!lua_istable(lua_state(), -1)) {
665661
throw std::runtime_error{
666662
"The entries in the 'indexes' array must be Lua tables."};
667663
}
668664

669665
flex_lua_setup_index(lua_state(), table);
670-
671-
lua_pop(lua_state(), 1);
672-
}
666+
});
673667

674668
lua_pop(lua_state(), 1); // "indexes"
675669
}
@@ -1179,25 +1173,24 @@ void output_flex_t::select_relation_members()
11791173
}
11801174

11811175
// Iterate over the 'ways' table to get all ids...
1182-
lua_pushnil(lua_state());
1183-
while (lua_next(lua_state(), -2) != 0) {
1184-
if (!lua_isnumber(lua_state(), -2)) {
1185-
throw std::runtime_error{
1186-
"Table returned from select_relation_members() contains 'ways' "
1187-
"field, but it isn't an array table."};
1188-
}
1176+
if (!luaX_is_array(lua_state())) {
1177+
throw std::runtime_error{
1178+
"Table returned from select_relation_members() contains 'ways' "
1179+
"field, but it isn't an array table."};
1180+
}
11891181

1190-
osmid_t const id = lua_tointeger(lua_state(), -1);
1191-
if (id == 0) {
1192-
throw std::runtime_error{
1193-
"Table returned from select_relation_members() contains 'ways' "
1194-
"field, which must contain an array of non-zero integer way "
1195-
"ids."};
1196-
}
1182+
luaX_for_each(
1183+
lua_state(), [&]() {
1184+
osmid_t const id = lua_tointeger(lua_state(), -1);
1185+
if (id == 0) {
1186+
throw std::runtime_error{
1187+
"Table returned from select_relation_members() contains "
1188+
"'ways' field, which must contain an array of non-zero "
1189+
"integer way ids."};
1190+
}
11971191

1198-
m_stage2_way_ids->set(id);
1199-
lua_pop(lua_state(), 1); // value pushed by lua_next()
1200-
}
1192+
m_stage2_way_ids->set(id);
1193+
});
12011194

12021195
lua_pop(lua_state(), 2); // return value (a table), ways field (a table)
12031196
}

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ set_test(test-wkb LABELS NoDB)
8585
# these tests require LUA support
8686
if (WITH_LUA)
8787
set_test(test-flex-indexes)
88+
set_test(test-lua-utils)
8889
set_test(test-output-flex)
8990
set_test(test-output-flex-multi-input)
9091
set_test(test-output-flex-nodes)

tests/test-lua-utils.cpp

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* SPDX-License-Identifier: GPL-2.0-or-later
3+
*
4+
* This file is part of osm2pgsql (https://osm2pgsql.org/).
5+
*
6+
* Copyright (C) 2006-2022 by the osm2pgsql developer community.
7+
* For a full list of authors see the git log.
8+
*/
9+
10+
#include <catch.hpp>
11+
12+
#include "lua-utils.hpp"
13+
14+
extern "C"
15+
{
16+
#include <lauxlib.h>
17+
}
18+
19+
// Run the Lua code in "code" and then execute the function "func".
20+
template <typename FUNC>
21+
void test_lua(lua_State *lua_state, char const *code, FUNC&& func) {
22+
REQUIRE(lua_gettop(lua_state) == 0);
23+
REQUIRE(luaL_dostring(lua_state, code) == 0);
24+
REQUIRE(lua_gettop(lua_state) == 1);
25+
std::forward<FUNC>(func)();
26+
REQUIRE(lua_gettop(lua_state) == 1);
27+
lua_pop(lua_state, 1); // result from executing the Lua code
28+
REQUIRE(lua_gettop(lua_state) == 0);
29+
}
30+
31+
TEST_CASE("check luaX_is_empty_table", "[NoDB]")
32+
{
33+
std::shared_ptr<lua_State> lua_state{
34+
luaL_newstate(), [](lua_State *state) { lua_close(state); }};
35+
36+
test_lua(lua_state.get(), "return {}", [&](){
37+
REQUIRE(luaX_is_empty_table(lua_state.get()));
38+
});
39+
40+
test_lua(lua_state.get(), "return { 1 }", [&](){
41+
REQUIRE_FALSE(luaX_is_empty_table(lua_state.get()));
42+
});
43+
44+
test_lua(lua_state.get(), "return { a = 'b' }", [&](){
45+
REQUIRE_FALSE(luaX_is_empty_table(lua_state.get()));
46+
});
47+
}
48+
49+
TEST_CASE("check luaX_is_array", "[NoDB]")
50+
{
51+
std::shared_ptr<lua_State> lua_state{
52+
luaL_newstate(), [](lua_State *state) { lua_close(state); }};
53+
54+
test_lua(lua_state.get(), "return { 1, 2, 3 }", [&](){
55+
REQUIRE(luaX_is_array(lua_state.get()));
56+
});
57+
58+
test_lua(lua_state.get(), "return { }", [&](){
59+
REQUIRE(luaX_is_array(lua_state.get()));
60+
});
61+
62+
test_lua(lua_state.get(), "return { 1 }", [&](){
63+
REQUIRE(luaX_is_array(lua_state.get()));
64+
});
65+
66+
test_lua(lua_state.get(), "return { [1] = 1, [2] = 2 }", [&](){
67+
REQUIRE(luaX_is_array(lua_state.get()));
68+
});
69+
70+
test_lua(lua_state.get(), "return { 1, nil, 3 }", [&](){
71+
REQUIRE_FALSE(luaX_is_array(lua_state.get()));
72+
});
73+
74+
test_lua(lua_state.get(), "return { a = 'foo' }", [&](){
75+
REQUIRE_FALSE(luaX_is_array(lua_state.get()));
76+
});
77+
78+
test_lua(lua_state.get(), "return { [1] = 'foo', ['bar'] = 2 }", [&](){
79+
REQUIRE_FALSE(luaX_is_array(lua_state.get()));
80+
});
81+
}
82+
83+
TEST_CASE("luaX_for_each should call function n times", "[NoDB]")
84+
{
85+
std::shared_ptr<lua_State> lua_state{
86+
luaL_newstate(), [](lua_State *state) { lua_close(state); }};
87+
88+
test_lua(lua_state.get(), "return { 3, 4, 5 }", [&](){
89+
int sum = 0;
90+
luaX_for_each(lua_state.get(), [&]() {
91+
sum += lua_tonumber(lua_state.get(), -1);
92+
});
93+
REQUIRE(sum == 12);
94+
});
95+
}
96+
97+
TEST_CASE("luaX_for_each should not call the function for empty arrays",
98+
"[NoDB]")
99+
{
100+
std::shared_ptr<lua_State> lua_state{
101+
luaL_newstate(), [](lua_State *state) { lua_close(state); }};
102+
103+
bool called = false;
104+
test_lua(lua_state.get(), "return {}", [&]() {
105+
luaX_for_each(lua_state.get(), [&]() { called = true; });
106+
});
107+
REQUIRE_FALSE(called);
108+
}

0 commit comments

Comments
 (0)