Skip to content

Commit 5e585eb

Browse files
committed
fable: Add optional to_json support for sol::object
This requires you to include fable/utility/sol.hpp and provide the sol2 library yourself.
1 parent 04f5b1c commit 5e585eb

File tree

4 files changed

+206
-0
lines changed

4 files changed

+206
-0
lines changed

fable/CMakeLists.txt

+3
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ include(CTest)
6767
if(BUILD_TESTING)
6868
find_package(GTest REQUIRED)
6969
find_package(Boost COMPONENTS headers filesystem REQUIRED)
70+
find_package(sol2 REQUIRED)
7071
include(GoogleTest)
7172

7273
add_executable(test-fable
@@ -90,6 +91,7 @@ if(BUILD_TESTING)
9091
src/fable/schema_test.cpp
9192
src/fable/utility/chrono_test.cpp
9293
src/fable/utility/string_test.cpp
94+
src/fable/utility/sol_test.cpp
9395
)
9496
set_target_properties(test-fable PROPERTIES
9597
CXX_STANDARD 17
@@ -100,6 +102,7 @@ if(BUILD_TESTING)
100102
GTest::gtest_main
101103
Boost::headers
102104
Boost::filesystem
105+
sol2::sol2
103106
fable
104107
)
105108
gtest_add_tests(TARGET test-fable)

fable/conanfile.py

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def requirements(self):
5252
def build_requirements(self):
5353
self.test_requires("gtest/1.13.0")
5454
self.test_requires("boost/[>=1.65.1]")
55+
self.test_requires("sol2/3.3.0")
5556

5657
def layout(self):
5758
cmake.cmake_layout(self)

fable/include/fable/utility/sol.hpp

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright 2023 Robert Bosch GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*/
18+
/**
19+
* \file fable/utility/sol.hpp
20+
*
21+
* This file contains specializations of `nlohmann::adl_serializer` for sol2
22+
* types.
23+
*
24+
* In order to provide serialization for third-party types, we need to either
25+
* use their namespace or provide a specialization in that of nlohmann. It is
26+
* illegal to define anything in the std namespace, so we are left no choice in
27+
* this regard.
28+
*
29+
* See: https://github.com/nlohmann/json
30+
*/
31+
32+
#pragma once
33+
34+
#include <nlohmann/json.hpp>
35+
36+
#include <sol/object.hpp> // for object
37+
#include <sol/optional.hpp> // for optional
38+
#include <sol/table.hpp> // for table
39+
40+
namespace nlohmann {
41+
42+
/**
43+
* Unfortunately, the way that sol is implemented, when json j = lua["key"],
44+
* lua["key"].operator json&() is called, which leads to an error if lua["key"]
45+
* is anything other than the usertype json.
46+
*
47+
* For this reason, it's required to wrap any table access with something
48+
* that turns it into a sol::object:
49+
*
50+
* json j1 = lua["key"].template get<sol::object>();
51+
* json j2 = sol::object(lua["key"]);
52+
*/
53+
template <>
54+
struct adl_serializer<sol::object> {
55+
static void to_json_array(json& j, const sol::table& tbl) {
56+
if (j.type() != json::value_t::array) {
57+
j = json::array();
58+
}
59+
auto kv_args = json::object();
60+
for (auto& kv : tbl) {
61+
if (kv.first.get_type() != sol::type::number) {
62+
auto key = kv.first.as<std::string>();
63+
to_json(kv_args[key], kv.second.as<sol::object>());
64+
continue;
65+
}
66+
j.emplace_back(kv.second.as<sol::object>());
67+
}
68+
if (!kv_args.empty()) {
69+
j.emplace_back(std::move(kv_args));
70+
}
71+
}
72+
73+
static void to_json_object(json& j, const sol::table& tbl) {
74+
if (j.type() != json::value_t::object) {
75+
j = json::object();
76+
}
77+
for (auto& kv : tbl) {
78+
auto key = kv.first.as<std::string>();
79+
to_json(j[key], kv.second.as<sol::object>());
80+
}
81+
}
82+
83+
static void to_json(json& j, const sol::table& tbl) {
84+
if (tbl.pairs().begin() == tbl.pairs().end()) {
85+
// We don't know whether this is an empty array or an empty object,
86+
// but it's probably an array since this makes more sense to have.
87+
if (j.type() == json::value_t::null) {
88+
j = json::array();
89+
}
90+
return;
91+
}
92+
93+
// Lua only accepts table keys that are integers or strings,
94+
// but they can be mixed; we use the first element to guess
95+
// whether its an array or an object.
96+
auto first = (*tbl.pairs().begin()).first;
97+
bool looks_like_array = (first.get_type() == sol::type::number);
98+
if (looks_like_array) {
99+
to_json_array(j, tbl);
100+
} else {
101+
to_json_object(j, tbl);
102+
}
103+
}
104+
105+
static void to_json(json& j, const sol::object& obj) {
106+
switch (obj.get_type()) {
107+
case sol::type::table: {
108+
to_json(j, obj.as<sol::table>());
109+
break;
110+
}
111+
case sol::type::string: {
112+
j = obj.as<std::string>();
113+
break;
114+
}
115+
case sol::type::boolean: {
116+
j = obj.as<bool>();
117+
break;
118+
}
119+
case sol::type::number: {
120+
// If the number in Lua has any significant decimals, even if they are zero,
121+
// it is not an integer and this optional will be falsy.
122+
if (auto num = obj.as<sol::optional<int64_t>>(); num) {
123+
j = *num;
124+
} else {
125+
j = obj.as<double>();
126+
}
127+
break;
128+
}
129+
case sol::type::nil:
130+
case sol::type::none: {
131+
j = nullptr;
132+
break;
133+
}
134+
case sol::type::poly:
135+
// throw std::out_of_range("cannot serialize lua poly type to JSON");
136+
j = "<poly>";
137+
break;
138+
case sol::type::function:
139+
// throw std::out_of_range("cannot serialize lua function to JSON");
140+
j = "<function>";
141+
break;
142+
case sol::type::thread:
143+
// throw std::out_of_range("cannot serialize lua thread to JSON");
144+
j = "<thread>";
145+
break;
146+
case sol::type::userdata:
147+
case sol::type::lightuserdata:
148+
j = "<userdata>";
149+
break;
150+
}
151+
}
152+
};
153+
154+
} // namespace nlohmann

fable/src/fable/utility/sol_test.cpp

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2023 Robert Bosch GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*/
18+
19+
#include <chrono>
20+
21+
#include <gtest/gtest.h>
22+
#include <sol/sol.hpp>
23+
24+
#include <fable/utility/sol.hpp>
25+
#include <fable/utility/gtest.hpp>
26+
#include <fable/json.hpp>
27+
28+
using namespace fable;
29+
30+
TEST(fable_utility_sol, to_json) {
31+
auto lua = sol::state();
32+
lua.open_libraries(sol::lib::base);
33+
34+
auto assert_xeq = [&](const char* lua_script, const char* expect) {
35+
lua.script(lua_script);
36+
assert_eq(sol::object(lua["x"]), expect);
37+
};
38+
39+
assert_xeq("x = true", "true");
40+
assert_xeq("x = 1", "1");
41+
assert_xeq("x = 2.5", "2.5");
42+
assert_xeq("x = { false, true }", "[ false, true ]");
43+
assert_xeq("x = 'hello world'", "\"hello world\"");
44+
assert_xeq("x = { name = 'ok', value = 42 }", R"({ "name": "ok", "value": 42 })");
45+
assert_xeq("x = { 1, 2, 3, extra = true }", R"([ 1, 2, 3, { "extra": true } ])");
46+
assert_xeq("x = { extra = true, 1, 2, 3 }", R"([ 1, 2, 3, { "extra": true } ])");
47+
assert_xeq("x = {}", "[]");
48+
}

0 commit comments

Comments
 (0)