diff --git a/CMakeLists.txt b/CMakeLists.txt index 8272382e3..69f9e307c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,7 +72,11 @@ if(${NO_EVENTFD}) endif() # Check packages +find_package(CURL 7.29 REQUIRED) +find_package(fmt REQUIRED) +find_package(OpenSSL 1.0.0 REQUIRED) find_package(PkgConfig REQUIRED) +find_package(spdlog REQUIRED) find_package(Threads REQUIRED) find_package(OpenMP) find_package(IBVerbs) @@ -96,7 +100,6 @@ if(NOT PASTE) message(SEND_ERROR "GNU paste is missing. Please install coreutils") endif() - set(ENV{PKG_CONFIG_PATH} "$ENV{PKG_CONFIG_PATH}:/usr/local/lib/pkgconfig:/usr/local/lib64/pkgconfig:/usr/local/share/pkgconfig:/usr/lib64/pkgconfig") pkg_check_modules(JANSSON IMPORTED_TARGET REQUIRED jansson>=2.13) @@ -171,9 +174,10 @@ else() endif() # Build options -cmake_dependent_option(WITHOUT_GPL "Build VILLASnode without any GPL code" OFF "" ON) +option(LOG_COLOR_DISABLE "Disable any colored log output" OFF) +option(WITHOUT_GPL "Build VILLASnode without any GPL code" OFF) +option(WITH_DEFAULTS "Defaults for non required build options" ON) cmake_dependent_option(WITH_GHC_FS "Build using ghc::filesystem, a drop in replacement for std::filesystem" ON "STDCXX_FS_NOT_FOUND" OFF) -cmake_dependent_option(WITH_DEFAULTS "Defaults for non required build options" ON "" OFF) cmake_dependent_option(WITH_API "Build with remote control API" "${WITH_DEFAULTS}" "" OFF) cmake_dependent_option(WITH_CLIENTS "Build client applications" "${WITH_DEFAULTS}" "TOPLEVEL_PROJECT" OFF) cmake_dependent_option(WITH_CONFIG "Build with support for libconfig configuration syntax" "${WITH_DEFAULTS}" "LIBCONFIG_FOUND" OFF) diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index f794a6950..5f1b17d73 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -4,33 +4,11 @@ # SPDX-FileCopyrightText: 2014-2023 Institute for Automation of Complex Power Systems, RWTH Aachen University # SPDX-License-Identifier: Apache-2.0 -include(FindPkgConfig) -include(FeatureSummary) -include(GNUInstallDirs) - -# Check packages -find_package(OpenSSL 1.0.0 REQUIRED) -find_package(CURL 7.29 REQUIRED) -find_package(spdlog 1.6.0 REQUIRED) -find_package(fmt 6.0.0 REQUIRED) - -pkg_check_modules(JANSSON IMPORTED_TARGET REQUIRED jansson>=2.7) -pkg_check_modules(LIBCONFIG IMPORTED_TARGET libconfig>=1.4.9) -pkg_check_modules(UUID IMPORTED_TARGET REQUIRED uuid>=2.23) - -if(fmt_VERSION VERSION_LESS "9.0.0") - message("Using legacy ostream formatting") - set(FMT_LEGACY_OSTREAM_FORMATTER 1) -endif() - add_subdirectory(lib) if(WITH_TESTS) add_subdirectory(tests) endif() -# Disable any colored log output -option(LOG_COLOR_DISABLE "Disable any colored log output" OFF) - configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/include/villas/config.hpp.in ${CMAKE_CURRENT_BINARY_DIR}/include/villas/config.hpp diff --git a/common/include/villas/jansson.hpp b/common/include/villas/jansson.hpp new file mode 100644 index 000000000..ce08905c2 --- /dev/null +++ b/common/include/villas/jansson.hpp @@ -0,0 +1,510 @@ +#pragma once + +#include +#include +#include + +#include + +#include + +namespace villas { + +// smart pointer for ::json_t values. +// +// this class mirrors the interface of std::shared_ptr using the internal reference count of ::json_t +class JanssonPtr { +public: + JanssonPtr() : inner(nullptr) {} + explicit JanssonPtr(::json_t *json) : inner(json) {} + + JanssonPtr(JanssonPtr const &other) : inner(json_incref(other.inner)) {} + JanssonPtr(JanssonPtr &&other) : inner(std::exchange(other.inner, nullptr)) {} + ~JanssonPtr() { json_decref(inner); } + + JanssonPtr &operator=(JanssonPtr const &other) { + json_decref(inner); + inner = json_incref(other.inner); + return *this; + } + + JanssonPtr &operator=(JanssonPtr &&other) { + json_decref(inner); + inner = std::exchange(other.inner, nullptr); + return *this; + } + + ::json_t *release() { return std::exchange(inner, nullptr); } + + void reset() { + json_decref(inner); + inner = nullptr; + } + + void reset(::json_t *json) { + json_decref(inner); + inner = json_incref(json); + } + + void swap(JanssonPtr &other) { std::swap(inner, other.inner); } + + operator bool() { return inner != nullptr; } + ::json_t *get() const { return inner; } + ::json_t *operator->() const { return inner; } + + friend void swap(JanssonPtr &lhs, JanssonPtr &rhs) { lhs.swap(rhs); } + friend auto operator<=>(JanssonPtr const &, JanssonPtr const &) = default; + +private: + ::json_t *inner; +}; + +// type trait helper for applying variadic default promotions +template class va_default_promote { + static auto impl() { + using U = std::decay_t; + if constexpr (std::is_enum_v) { + if constexpr (std::is_convertible_v>) { + // unscoped enumerations are converted to their underlying type and then promoted using integer promotions + return std::type_identity< + decltype(+std::declval>())>(); + } else { + // scoped enumeration handling is implementation defined just pass them without promotions + return std::type_identity(); + } + } else if constexpr (std::is_same_v) { + // float values are promoted to double + return std::type_identity(); + } else if constexpr (std::is_integral_v) { + // integral values are promoted using integer promotions + return std::type_identity())>(); + } else { + // default case without any promotions + return std::type_identity(); + } + } + +public: + using type = decltype(impl())::type; +}; + +template using va_default_promote_t = va_default_promote::type; + +// helper type for validating format strings +enum class JanssonFormatArg { + KEY, + STRING, + SIZE, + INT, + JSON_INT = + std::is_same_v + ? std::underlying_type_t(JanssonFormatArg::INT) + : std::underlying_type_t(JanssonFormatArg::INT) + 1, + DOUBLE, + JSON, +}; + +// make a JanssonFormatArg from a type using template specializations +template consteval JanssonFormatArg makeJanssonFormatArg() { + if (std::is_same_v) + return JanssonFormatArg::KEY; + + if (std::is_same_v) + return JanssonFormatArg::STRING; + + if (std::is_same_v) + return JanssonFormatArg::SIZE; + + if (std::is_same_v or std::is_same_v) + return JanssonFormatArg::INT; + + if (std::is_same_v or + std::is_same_v>) + return JanssonFormatArg::JSON_INT; + + if (std::is_same_v) + return JanssonFormatArg::DOUBLE; + + if (std::is_same_v) + return JanssonFormatArg::JSON; + + throw "Unsupported argument type"; +} + +template +constexpr static JanssonFormatArg janssonFormatArg = makeJanssonFormatArg(); + +// helper type for validating format strings with nested structures +enum class JanssonNestedStructure { + OBJECT, + ARRAY, +}; + +// compile time validator for json_unpack-style format strings +template class JanssonUnpackFormatString { +public: + consteval JanssonUnpackFormatString(char const *fmt) : fmt(fmt) { +#ifdef __cpp_lib_constexpr_vector + validate(fmt); +#endif + } + + constexpr char const *c_str() const { return fmt; } + +private: + consteval static void validate(char const *fmt) { + constexpr auto ignored = std::string_view(" \t\n,:"); + + auto const args = + std::initializer_list{janssonFormatArg>...}; + auto const string = std::string_view(fmt); + auto const findNextToken = [&](std::size_t pos) { + if (pos == std::string_view::npos) + return std::string_view::npos; + + return string.find_first_not_of(ignored, pos); + }; + + std::size_t token = 0; + auto arg = args.begin(); + auto nested = std::vector{}; + + while ((token = findNextToken(token)) != std::string_view::npos) { + if (not nested.empty() and + nested.back() == JanssonNestedStructure::OBJECT) { + if (string[token] == '}') { + nested.pop_back(); + token++; + continue; + } + + if (string[token++] != 's') + throw "Invalid specifier for object key"; + + if (*arg++ != JanssonFormatArg::KEY) + throw "Expected 'const char *' argument for object key"; + + if ((token = findNextToken(token)) == std::string_view::npos) + break; + + if (string[token] == '?') + token++; + + if ((token = findNextToken(token)) == std::string_view::npos) + break; + } + + switch (string[token++]) { + case 's': { + if (*arg++ != JanssonFormatArg::STRING) + throw "Expected 'const char **' argument for 's' specifier"; + + if ((token = findNextToken(token)) == std::string_view::npos) + break; + + if (string[token] == '%') { + if (*arg++ != JanssonFormatArg::SIZE) + throw "Expected 'size_t *' argument for 's%' specifier"; + token++; + } + } break; + + case 'n': + break; + + case 'b': { + if (*arg++ != JanssonFormatArg::INT) + throw "Expected 'int *' argument for 'b' specifier"; + } break; + + case 'i': { + if (*arg++ != JanssonFormatArg::INT) + throw "Expected 'int *' argument for 'i' specifier"; + } break; + + case 'I': { + if (*arg++ != JanssonFormatArg::JSON_INT) + throw "Expected 'json_int_t *' argument for 'I' specifier"; + } break; + + case 'f': { + if (*arg++ != JanssonFormatArg::DOUBLE) + throw "Expected 'double *' argument for 'f' specifier"; + } break; + + case 'F': { + if (*arg++ != JanssonFormatArg::DOUBLE) + throw "Expected 'double *' argument for 'F' specifier"; + } break; + + case 'o': { + if (*arg++ != JanssonFormatArg::JSON) + throw "Expected 'json_t *' argument for 'o' specifier"; + } break; + + case 'O': { + if (*arg++ != JanssonFormatArg::JSON) + throw "Expected 'json_t *' argument for 'O' specifier"; + } break; + + case '[': { + nested.push_back(JanssonNestedStructure::ARRAY); + } break; + + case ']': { + if (nested.empty() or nested.back() != JanssonNestedStructure::ARRAY) + throw "Unexpected ]"; + nested.pop_back(); + } break; + + case '{': { + nested.push_back(JanssonNestedStructure::OBJECT); + } break; + + case '}': + throw "Unexpected }"; + + case '!': + break; + + case '*': + break; + + default: + throw "Unknown format specifier in format string"; + } + } + + if (not nested.empty()) + throw "Unclosed structure in format string"; + + if (arg != args.end()) + throw "Unused trailing arguments"; + } + + char const *fmt; +}; + +template +void janssonUnpack(::json_t *json, + JanssonUnpackFormatString...> fmt, + Args... args) { + ::json_error_t err; + if (auto ret = ::json_unpack_ex(json, &err, 0, fmt.c_str(), args...); + ret == -1) + throw RuntimeError("Could not unpack json value: {}", err.text); +} + +// compile time validator for json_pack-style format strings +template class JanssonPackFormatString { +public: + consteval JanssonPackFormatString(char const *fmt) : fmt(fmt) { +#ifdef __cpp_lib_constexpr_vector + validate(fmt); +#endif + } + + constexpr char const *c_str() const { return fmt; } + +private: + consteval static void validate(char const *fmt) { + constexpr auto ignored = std::string_view(" \t\n,:"); + + auto const args = std::initializer_list{janssonFormatArg...}; + auto const string = std::string_view(fmt); + auto const findNextToken = [&](std::size_t pos) { + if (pos == std::string_view::npos) + return std::string_view::npos; + + return string.find_first_not_of(ignored, pos); + }; + + std::size_t token = 0; + auto arg = args.begin(); + auto nested = std::vector{}; + while ((token = findNextToken(token)) != std::string_view::npos) { + if (not nested.empty() and + nested.back() == JanssonNestedStructure::OBJECT) { + if (string[token] == '}') { + nested.pop_back(); + token++; + continue; + } + + if (string[token++] != 's') + throw "Invalid specifier for object key"; + + if (*arg++ != JanssonFormatArg::STRING) + throw "Expected 'const char *' argument for object key"; + + if ((token = findNextToken(token)) == std::string_view::npos) + break; + + if (string[token] == '#') { + if (*arg++ != JanssonFormatArg::INT) + throw "Expected 'int' argument for 's#' specifier"; + token++; + } else if (string[token] == '%') { + if (*arg++ != JanssonFormatArg::SIZE) + throw "Expected 'size_t' argument for 's%' specifier"; + token++; + } + + while ((token = findNextToken(token)) != std::string_view::npos) { + if (string[token] != '+') + break; + + if (*arg++ != JanssonFormatArg::STRING) + throw "Expected 'const char *' argument for '+' specifier"; + + if ((token = findNextToken(++token)) == std::string_view::npos) + break; + + if (string[token] == '#') { + if (*arg++ != JanssonFormatArg::INT) + throw "Expected 'int' argument for '+#' specifier"; + token++; + } else if (string[token] == '%') { + if (*arg++ != JanssonFormatArg::SIZE) + throw "Expected 'size_t' argument for 's%' specifier"; + token++; + } + } + + if (token == std::string_view::npos) + throw "Expected closing }"; + } + + switch (string[token++]) { + case 's': { + if (*arg++ != JanssonFormatArg::STRING) + throw "Expected 'const char *' argument for 's' specifier"; + + if ((token = findNextToken(token)) == std::string_view::npos) + break; + + auto optionalValue = false; + if (string[token] == '?' or string[token] == '*') { + optionalValue = true; + token++; + } else if (string[token] == '#') { + if (*arg++ != JanssonFormatArg::INT) + throw "Expected 'int' argument for 's#' specifier"; + token++; + } else if (string[token] == '%') { + if (*arg++ != JanssonFormatArg::SIZE) + throw "Expected 'size_t' argument for 's%' specifier"; + token++; + } + + if (optionalValue) + break; + + while ((token = findNextToken(token)) != std::string_view::npos) { + if (string[token] != '+') + break; + + if (*arg++ != JanssonFormatArg::STRING) + throw "Expected 'const char *' argument for '+' specifier"; + + if ((token = findNextToken(++token)) == std::string_view::npos) + break; + + if (string[token] == '#') { + if (*arg++ != JanssonFormatArg::INT) + throw "Expected 'int' argument for '+#' specifier"; + token++; + } else if (string[token] == '%') { + if (*arg++ != JanssonFormatArg::SIZE) + throw "Expected 'size_t' argument for 's%' specifier"; + token++; + } + } + } break; + + case '+': + throw "Unexpected '+' specifier"; + + case 'n': + break; + + case 'b': { + if (*arg++ != JanssonFormatArg::INT) + throw "Expected 'int' argument for 'b' specifier"; + } break; + + case 'i': { + if (*arg++ != JanssonFormatArg::INT) + throw "Expected 'int' argument for 'i' specifier"; + } break; + + case 'I': { + if (*arg++ != JanssonFormatArg::JSON_INT) + throw "Expected 'json_int_t' argument for 'I' specifier"; + } break; + + case 'f': { + if (*arg++ != JanssonFormatArg::DOUBLE) + throw "Expected 'double' argument for 'f' specifier"; + } break; + + case 'o': { + if (*arg++ != JanssonFormatArg::JSON) + throw "Expected 'json_t *' argument for 'o' specifier"; + + if (string[token] == '?' or string[token] == '*') + token++; + } break; + + case 'O': { + if (*arg++ != JanssonFormatArg::JSON) + throw "Expected 'json_t *' argument for 'O' specifier"; + + if (string[token] == '?' or string[token] == '*') + token++; + } break; + + case '[': { + nested.push_back(JanssonNestedStructure::ARRAY); + } break; + + case ']': { + if (nested.empty() or nested.back() != JanssonNestedStructure::ARRAY) + throw "Unexpected ]"; + nested.pop_back(); + } break; + + case '{': { + nested.push_back(JanssonNestedStructure::OBJECT); + } break; + + case '}': + throw "Unexpected }"; + + default: + throw "Unknown format specifier in format string"; + } + } + + if (not nested.empty()) + throw "Unclosed structure in format string"; + + if (arg != args.end()) + throw "Unused trailing arguments"; + } + + char const *fmt; +}; + +template +JanssonPtr +janssonPack(JanssonPackFormatString...> fmt, + Args... args) { + auto json = ::json_pack(fmt.c_str(), args...); + if (json == nullptr) + throw RuntimeError("Could not pack json value"); + + return JanssonPtr(json); +} + +} // namespace villas diff --git a/common/lib/hist.cpp b/common/lib/hist.cpp index de1a2278e..dd2f7f0ab 100644 --- a/common/lib/hist.cpp +++ b/common/lib/hist.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -203,15 +204,24 @@ char *Hist::dump() const { json_t *Hist::toJson() const { json_t *json_buckets, *json_hist; - json_hist = json_pack("{ s: f, s: f, s: i }", "low", low, "high", high, - "total", total); + json_hist = janssonPack("{ s:f, s:f, s:I }", // + "low", low, // + "high", high, // + "total", static_cast(total)) + .release(); if (total > 0) { - json_object_update(json_hist, - json_pack("{ s: i, s: i, s: f, s: f, s: f, s: f, s: f }", - "higher", higher, "lower", lower, "highest", - highest, "lowest", lowest, "mean", getMean(), - "variance", getVar(), "stddev", getStddev())); + json_object_update_new( + json_hist, + janssonPack("{ s:I, s:I, s:f, s:f, s:f, s:f, s:f }", // + "higher", static_cast(higher), // + "lower", static_cast(lower), // + "highest", highest, // + "lowest", lowest, // + "mean", getMean(), // + "variance", getVar(), // + "stddev", getStddev()) + .release()); } if (total - lower - higher > 0) { @@ -220,7 +230,7 @@ json_t *Hist::toJson() const { for (auto elm : data) json_array_append(json_buckets, json_integer(elm)); - json_object_set(json_hist, "buckets", json_buckets); + json_object_set_new(json_hist, "buckets", json_buckets); } return json_hist; diff --git a/common/lib/log.cpp b/common/lib/log.cpp index e9c653517..cddc7b589 100644 --- a/common/lib/log.cpp +++ b/common/lib/log.cpp @@ -6,7 +6,6 @@ */ #include -#include #include #include @@ -14,6 +13,7 @@ #include #include +#include #include #include @@ -100,17 +100,15 @@ void Log::parse(json_t *json) { const char *path = nullptr; const char *pattern = nullptr; - int ret, syslog = 0; - - json_error_t err; + int syslog = 0; json_t *json_expressions = nullptr; - ret = json_unpack_ex(json, &err, JSON_STRICT, - "{ s?: s, s?: s, s?: o, s?: b, s?: s }", "level", &level, - "file", &path, "expressions", &json_expressions, - "syslog", &syslog, "pattern", &pattern); - if (ret) - throw ConfigError(json, err, "node-config-logging"); + janssonUnpack(json, "{ s?: s, s?: s, s?: o, s?: b, s?: s }", // + "level", &level, // + "file", &path, // + "expressions", &json_expressions, // + "syslog", &syslog, // + "pattern", &pattern); if (level) setLevel(level); @@ -179,22 +177,16 @@ Log::Level Log::getLevel() const { return level; } std::string Log::getLevelName() const { auto sv = spdlog::level::to_string_view(level); - return std::string(sv.data()); + return std::string(sv.begin(), sv.end()); } Log::Expression::Expression(json_t *json) { - int ret; - const char *nme; const char *lvl; - json_error_t err; - - ret = json_unpack_ex(json, &err, JSON_STRICT, "{ s: s, s: s }", "name", &nme, - "level", &lvl); - if (ret) - throw ConfigError(json, err, "node-config-logging-expressions"); - + janssonUnpack(json, "{ s: s, s: s }", // + "name", &nme, // + "level", &lvl); level = spdlog::level::from_str(lvl); name = nme; } diff --git a/common/tests/unit/buffer.cpp b/common/tests/unit/buffer.cpp index f6043c0d8..f4451724a 100644 --- a/common/tests/unit/buffer.cpp +++ b/common/tests/unit/buffer.cpp @@ -9,9 +9,9 @@ #include #include -#include #include +#include using namespace villas; @@ -95,7 +95,7 @@ Test(buffer, multiple) { std::srand(std::time(nullptr)); for (int i = 0; i < N; i++) { - k[i] = json_pack("{ s: i }", "id", std::rand()); + k[i] = janssonPack("{ s:i }", "id", std::rand()).release(); cr_assert_not_null(k[i]); ret = buf.encode(k[i]); diff --git a/lib/node_direction.cpp b/lib/node_direction.cpp index c766eaae1..b064bcfc3 100644 --- a/lib/node_direction.cpp +++ b/lib/node_direction.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -24,19 +25,17 @@ NodeDirection::NodeDirection(enum NodeDirection::Direction dir, Node *n) int NodeDirection::parse(json_t *json) { int ret; - - json_error_t err; json_t *json_hooks = nullptr; json_t *json_signals = nullptr; config = json; - ret = json_unpack_ex(json, &err, 0, "{ s?: o, s?: o, s?: i, s?: b, s?: b }", - "hooks", &json_hooks, "signals", &json_signals, - "vectorize", &vectorize, "builtin", &builtin, "enabled", - &enabled); - if (ret) - throw ConfigError(json, err, "node-config-node-in"); + janssonUnpack(json, "{ s?o, s?o, s?i, s?b, s?b }", // + "hooks", &json_hooks, // + "signals", &json_signals, // + "vectorize", &vectorize, // + "builtin", &builtin, // + "enabled", &enabled); if (node->getFactory()->getFlags() & (int)NodeFactory::Flags::PROVIDES_SIGNALS) { @@ -53,10 +52,7 @@ int NodeDirection::parse(json_t *json) { json_t *json_name, *json_signal = json_signals; int count; - ret = json_unpack_ex(json_signal, &err, 0, "{ s: i }", "count", &count); - if (ret) - throw ConfigError(json_signals, "node-config-node-signals", - "Invalid signal definition"); + janssonUnpack(json_signal, "{ s:i }", "count", &count); json_signals = json_array(); for (int i = 0; i < count; i++) { diff --git a/lib/nodes/webrtc.cpp b/lib/nodes/webrtc.cpp index 1ae565473..85a75fba1 100644 --- a/lib/nodes/webrtc.cpp +++ b/lib/nodes/webrtc.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -57,17 +58,22 @@ int WebRTCNode::parse(json_t *json) { const char *pr = nullptr; int ord = -1; int &rexmit = dci.reliability.rexmit.emplace(0); - json_t *json_ice = nullptr; + json_t *json_servers = nullptr; + int tcp = -1; json_t *json_format = nullptr; - json_error_t err; - ret = json_unpack_ex( - json, &err, 0, "{ s: s, s?: s, s?: s, s?: i, s?: i, s?: b, s?: o }", - "session", &sess, "peer", &pr, "server", &svr, "wait_seconds", - &wait_seconds, "max_retransmits", &rexmit, "ordered", &ord, "ice", - &json_ice, "format", &json_format); - if (ret) - throw ConfigError(json, err, "node-config-node-webrtc"); + janssonUnpack(json, + "{ s:s, s?s, s?s, s?i, s?i, s?b, s?{ s?o, s?b }, s?o }", // + "session", &sess, // + "peer", &pr, // + "server", &svr, // + "wait_seconds", &wait_seconds, // + "max_retransmits", &rexmit, // + "ordered", &ord, // + "ice", // + /* ice */ "servers", &json_servers, // + /* ice */ "tcp", &tcp, // + "format", &json_format); session = sess; @@ -80,39 +86,28 @@ int WebRTCNode::parse(json_t *json) { if (ord) dci.reliability.unordered = !ord; - if (json_ice) { - json_t *json_servers = nullptr; - - int tcp = -1; - - ret = json_unpack_ex(json_ice, &err, 0, "{ s?: o, s?: b }", "servers", - &json_servers, "tcp", &tcp); - if (ret) - throw ConfigError(json, err, "node-config-node-webrtc-ice"); + if (json_servers) { + rtcConf.iceServers.clear(); - if (json_servers) { - rtcConf.iceServers.clear(); + if (!json_is_array(json_servers)) + throw ConfigError( + json_servers, "node-config-node-webrtc-ice-servers", + "ICE Servers must be a an array of server configurations."); - if (!json_is_array(json_servers)) - throw ConfigError( - json_servers, "node-config-node-webrtc-ice-servers", - "ICE Servers must be a an array of server configurations."); + size_t i; + json_t *json_server; + json_array_foreach (json_servers, i, json_server) { + if (!json_is_string(json_server)) + throw ConfigError(json_server, "node-config-node-webrtc-ice-server", + "ICE servers must be provided as STUN/TURN url."); - size_t i; - json_t *json_server; - json_array_foreach (json_servers, i, json_server) { - if (!json_is_string(json_server)) - throw ConfigError(json_server, "node-config-node-webrtc-ice-server", - "ICE servers must be provided as STUN/TURN url."); + std::string uri = json_string_value(json_server); - std::string uri = json_string_value(json_server); - - rtcConf.iceServers.emplace_back(uri); - } - - if (tcp > 0) - rtcConf.enableIceTcp = tcp > 0; + rtcConf.iceServers.emplace_back(uri); } + + if (tcp > 0) + rtcConf.enableIceTcp = tcp > 0; } auto *fmt = json_format ? FormatFactory::make(json_format)