diff --git a/src/stan/callbacks/json_writer.hpp b/src/stan/callbacks/json_writer.hpp index b13f6fb14b3..46a4415518c 100644 --- a/src/stan/callbacks/json_writer.hpp +++ b/src/stan/callbacks/json_writer.hpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include #include @@ -51,115 +51,23 @@ class json_writer final : public structured_writer { *output_ << "\n"; } - /** - * Process a string to escape special characters. - * Valid json strings cannot contain any of the special characters - * `'\\', '"', '/', '\b', '\f', '\n', '\r', '\t', '\v', '\a', '\0'`. - * In order to print these characters, they must be escaped. - * @param value The string to process. - * @return The processed string. - */ - std::string process_string(const std::string& value) { - static constexpr std::array chars_to_escape - = {'\\', '"', '/', '\b', '\f', '\n', '\r', '\t', '\v', '\a', '\0'}; - static constexpr std::array chars_to_replace - = {"\\\\", "\\\"", "\\/", "\\b", "\\f", "\\n", - "\\r", "\\t", "\\v", "\\a", "\\0"}; - // Replacing every value leads to 2x the size - std::string new_value(value.size() * 2, 'x'); - std::size_t pos = 0; - std::size_t count = 0; - std::size_t prev_pos = 0; - while ((pos = value.find_first_of(chars_to_escape.data(), pos, 10)) - != std::string::npos) { - for (int i = prev_pos; i < pos; ++i) { - new_value[i + count] = value[i]; - } - int idx - = strchr(chars_to_escape.data(), value[pos]) - chars_to_escape.data(); - new_value[pos + count] = chars_to_replace[idx][0]; - new_value[pos + count + 1] = chars_to_replace[idx][1]; - pos += 1; - count++; - prev_pos = pos; - } - for (int i = prev_pos; i < value.size(); ++i) { - new_value[i + count] = value[i]; - } - // Shrink any unused space - new_value.resize(value.size() + count); - return new_value; - } - /** * Writes key plus colon for key-value pair. * * @param[in] key member name. */ void write_key(const std::string& key) { - *output_ << std::string(record_depth_ * 2, ' ') << "\"" - << process_string(key) << "\" : "; + stan::json::write_key(output_, key, record_depth_); } template - void write_int_like(const std::string& key, T value) { + void write_value(const std::string& key, const T& value) { if (output_ == nullptr) { return; } write_sep(); write_key(key); - *output_ << value; - } - - /** - * Writes a single value. Corrects capitalization for inf and nans. - * - * @param[in] v value - */ - void write_value(double v) { - if (unlikely(std::isinf(v))) { - if (v > 0) { - *output_ << "Inf"; - } else { - *output_ << "-Inf"; - } - } else if (unlikely(std::isnan(v))) { - *output_ << "NaN"; - } else { - *output_ << v; - } - } - - /** - * Writes a single complex value. - * - * @param[in] v value - */ - void write_complex_value(std::complex v) { - *output_ << "["; - write_value(v.real()); - *output_ << ", "; - write_value(v.imag()); - *output_ << "]"; - } - - /** - * Writes the set of comma separated values in an Eigen (row) vector. - * - * @param[in] v Values in an `Eigen::Vector` - */ - template - void write_eigen_vector(const Eigen::DenseBase& v) { - *output_ << "[ "; - if (v.size() > 0) { - size_t last = v.size() - 1; - for (Eigen::Index i = 0; i < last; ++i) { - write_value(v[i]); - *output_ << ", "; - } - write_value(v[last]); - } - *output_ << " ]"; + stan::json::write_json_value(output_, value, record_depth_); } public: @@ -251,13 +159,7 @@ class json_writer final : public structured_writer { * @param value string to write. */ void write(const std::string& key, const std::string& value) { - if (output_ == nullptr) { - return; - } - std::string processsed_string = process_string(value); - write_sep(); - write_key(key); - *output_ << "\"" << processsed_string << "\""; + write_value(key, value); } /** @@ -266,13 +168,7 @@ class json_writer final : public structured_writer { * @param value pointer to chars to write. */ void write(const std::string& key, const char* value) { - if (output_ == nullptr) { - return; - } - std::string processsed_string = process_string(value); - write_sep(); - write_key(key); - *output_ << "\"" << processsed_string << "\""; + write_value(key, value); } /** @@ -294,15 +190,18 @@ class json_writer final : public structured_writer { * @param key Name of the value pair * @param value int to write. */ - void write(const std::string& key, int value) { write_int_like(key, value); } + void write(const std::string& key, int value) { write_value(key, value); } - /** - * Write a key-value pair where the value is an `std::size_t`. - * @param key Name of the value pair - * @param value `std::size_t` to write. - */ - void write(const std::string& key, std::size_t value) { - write_int_like(key, value); + /// Write a key-value pair with an unsigned long value. + void write(const std::string& key, + unsigned long value) { // NOLINT(runtime/int) + write_value(key, value); + } + + /// Write a key-value pair with an unsigned long long value. + void write(const std::string& key, + unsigned long long value) { // NOLINT(runtime/int) + write_value(key, value); } /** @@ -313,7 +212,7 @@ class json_writer final : public structured_writer { void write(const std::string& key, long long int value // NOLINT(runtime/int) ) { - write_int_like(key, value); + write_value(key, value); } /** @@ -322,7 +221,7 @@ class json_writer final : public structured_writer { * @param value `unsigned int` to write. */ void write(const std::string& key, unsigned int value) { - write_int_like(key, value); + write_value(key, value); } /** @@ -330,14 +229,7 @@ class json_writer final : public structured_writer { * @param key Name of the value pair * @param value double to write. */ - void write(const std::string& key, double value) { - if (output_ == nullptr) { - return; - } - write_sep(); - write_key(key); - write_value(value); - } + void write(const std::string& key, double value) { write_value(key, value); } /** * Write a key-value pair where the value is a complex value. @@ -345,12 +237,7 @@ class json_writer final : public structured_writer { * @param value complex value to write. */ void write(const std::string& key, const std::complex& value) { - if (output_ == nullptr) { - return; - } - write_sep(); - write_key(key); - write_complex_value(value); + write_value(key, value); } /** @@ -359,22 +246,7 @@ class json_writer final : public structured_writer { * @param values vector to write. */ void write(const std::string& key, const std::vector& values) { - if (output_ == nullptr) { - return; - } - write_sep(); - write_key(key); - - *output_ << "[ "; - if (values.size() > 0) { - auto last = values.end(); - --last; - for (auto it = values.begin(); it != last; ++it) { - *output_ << process_string(*it) << ", "; - } - *output_ << process_string(values.back()); - } - *output_ << " ]"; + write_value(key, values); } /** @@ -383,23 +255,7 @@ class json_writer final : public structured_writer { * @param values vector to write. */ void write(const std::string& key, const std::vector& values) { - if (output_ == nullptr) { - return; - } - write_sep(); - write_key(key); - - *output_ << "[ "; - if (values.size() > 0) { - auto last = values.end(); - --last; - for (auto it = values.begin(); it != last; ++it) { - write_value(*it); - *output_ << ", "; - } - write_value(values.back()); - } - *output_ << " ]"; + write_value(key, values); } /** @@ -408,22 +264,7 @@ class json_writer final : public structured_writer { * @param values vector to write. */ void write(const std::string& key, const std::vector& values) { - if (output_ == nullptr) { - return; - } - write_sep(); - write_key(key); - - *output_ << "[ "; - if (values.size() > 0) { - auto last = values.end(); - --last; - for (auto it = values.begin(); it != last; ++it) { - *output_ << *it << ", "; - } - *output_ << values.back(); - } - *output_ << " ]"; + write_value(key, values); } /** @@ -433,23 +274,7 @@ class json_writer final : public structured_writer { */ void write(const std::string& key, const std::vector>& values) { - if (output_ == nullptr) { - return; - } - write_sep(); - write_key(key); - - *output_ << "[ "; - if (values.size() > 0) { - auto last = values.end(); - --last; - for (auto it = values.begin(); it != last; ++it) { - write_complex_value(*it); - *output_ << ", "; - } - write_complex_value(values.back()); - } - *output_ << " ]"; + write_value(key, values); } /** @@ -458,12 +283,7 @@ class json_writer final : public structured_writer { * @param vec Eigen Vector to write. */ void write(const std::string& key, const Eigen::VectorXd& vec) { - if (output_ == nullptr) { - return; - } - write_sep(); - write_key(key); - write_eigen_vector(vec); + write_value(key, vec); } /** @@ -472,12 +292,7 @@ class json_writer final : public structured_writer { * @param vec Eigen Vector to write. */ void write(const std::string& key, const Eigen::RowVectorXd& vec) { - if (output_ == nullptr) { - return; - } - write_sep(); - write_key(key); - write_eigen_vector(vec); + write_value(key, vec); } /** @@ -486,21 +301,7 @@ class json_writer final : public structured_writer { * @param mat Eigen Matrix to write. */ void write(const std::string& key, const Eigen::MatrixXd& mat) { - if (output_ == nullptr) { - return; - } - write_sep(); - write_key(key); - *output_ << "[ "; - if (mat.rows() > 0) { - Eigen::Index last = mat.rows() - 1; - for (Eigen::Index i = 0; i < last; ++i) { - write_eigen_vector(mat.row(i)); - *output_ << ", "; - } - write_eigen_vector(mat.row(last)); - } - *output_ << " ]"; + write_value(key, mat); } }; diff --git a/src/stan/callbacks/structured_writer.hpp b/src/stan/callbacks/structured_writer.hpp index 5af3b54202b..0e97d560746 100644 --- a/src/stan/callbacks/structured_writer.hpp +++ b/src/stan/callbacks/structured_writer.hpp @@ -1,7 +1,6 @@ #ifndef STAN_CALLBACKS_STRUCTURED_WRITER_HPP #define STAN_CALLBACKS_STRUCTURED_WRITER_HPP -#include #include #include #include @@ -65,12 +64,15 @@ class structured_writer { */ virtual void write(const std::string& key, int value) {} - /** - * Write a key-value pair where the value is an `uint64_t`. - * @param key Name of the value pair - * @param value `uint64_t` to write. - */ - virtual void write(const std::string& key, uint64_t value) {} + /// Write a key-value pair with an unsigned long value. + virtual void write(const std::string& key, + unsigned long value // NOLINT(runtime/int) + ) {} + + /// Write a key-value pair with an unsigned long long value. + virtual void write(const std::string& key, + unsigned long long value // NOLINT(runtime/int) + ) {} /** * Write a key-value pair where the value is an `long long int`. @@ -86,7 +88,7 @@ class structured_writer { * @param key Name of the value pair * @param value `unsigned int` to write. */ - virtual void write(const std::string& key, uint32_t value) {} + virtual void write(const std::string& key, unsigned int value) {} /** * Write a key-value pair where the value is a double. diff --git a/src/stan/io/json/write_json_value.hpp b/src/stan/io/json/write_json_value.hpp new file mode 100644 index 00000000000..4857af70553 --- /dev/null +++ b/src/stan/io/json/write_json_value.hpp @@ -0,0 +1,198 @@ +#ifndef STAN_IO_JSON_WRITE_JSON_VALUE_HPP +#define STAN_IO_JSON_WRITE_JSON_VALUE_HPP + +#include +#include +#include + +#include +#include +#include + +namespace stan { +namespace json { + +namespace internal { +/** + * Process a string to escape special characters. + * Valid json strings cannot contain any of the special characters + * `'\\', '"', '/', '\b', '\f', '\n', '\r', '\t', '\v', '\a', '\0'`. + * In order to print these characters, they must be escaped. + * @param value The string to process. + * @return The processed string. + */ +inline std::string process_string(const std::string& value) { + static constexpr std::array chars_to_escape + = {'\\', '"', '/', '\b', '\f', '\n', '\r', '\t', '\v', '\a', '\0'}; + static constexpr std::array chars_to_replace + = {"\\\\", "\\\"", "\\/", "\\b", "\\f", "\\n", + "\\r", "\\t", "\\v", "\\a", "\\0"}; + // Replacing every value leads to 2x the size + std::string new_value(value.size() * 2, 'x'); + std::size_t pos = 0; + std::size_t count = 0; + std::size_t prev_pos = 0; + while ((pos = value.find_first_of(chars_to_escape.data(), pos, 10)) + != std::string::npos) { + for (int i = prev_pos; i < pos; ++i) { + new_value[i + count] = value[i]; + } + int idx + = strchr(chars_to_escape.data(), value[pos]) - chars_to_escape.data(); + new_value[pos + count] = chars_to_replace[idx][0]; + new_value[pos + count + 1] = chars_to_replace[idx][1]; + pos += 1; + count++; + prev_pos = pos; + } + for (int i = prev_pos; i < value.size(); ++i) { + new_value[i + count] = value[i]; + } + // Shrink any unused space + new_value.resize(value.size() + count); + return new_value; +} + +} // namespace internal + +/** + * Writes key plus colon for key-value pair. + */ +template +void write_key(Stream&& output, const std::string& key, int record_depth = 0) { + *output << std::string(record_depth * 2, ' ') << "\"" + << internal::process_string(key) << "\" : "; +} + +/** + * Write a floating point value to JSON. NaN and infinite values are + * rendered with capitalization. + */ +template * = nullptr> +void write_json_value(Stream&& output, T value, int /*record_depth*/ = 0) { + if (unlikely(std::isinf(value))) { + if (value > 0) { + *output << "Inf"; + } else { + *output << "-Inf"; + } + } else if (unlikely(std::isnan(value))) { + *output << "NaN"; + } else { + *output << value; + } +} + +/** + * Write an integer to JSON. + */ +template * = nullptr> +void write_json_value(Stream&& output, T value, int /*record_depth*/ = 0) { + *output << value; +} + +/** + * Write a complex number to JSON. We render this as a 2-element list. + */ +template * = nullptr> +void write_json_value(Stream&& output, T&& value, int /*record_depth*/ = 0) { + *output << "["; + write_json_value(output, value.real()); + *output << ", "; + write_json_value(output, value.imag()); + *output << "]"; +} + +/** + * Write an Eigen (Row)Vector to JSON. + */ +template * = nullptr> +void write_json_value(Stream&& output, T&& value, int /*record_depth*/ = 0) { + *output << "[ "; + if (value.size() > 0) { + size_t last = value.size() - 1; + for (Eigen::Index i = 0; i < last; ++i) { + write_json_value(output, value[i]); + *output << ", "; + } + write_json_value(output, value[last]); + } + *output << " ]"; +} + +/** + * Write an Eigen Matrix to JSON. + */ +template * = nullptr> +void write_json_value(Stream&& output, T&& value, int /*record_depth*/ = 0) { + *output << "[ "; + if (value.rows() > 0) { + Eigen::Index last = value.rows() - 1; + for (Eigen::Index i = 0; i < last; ++i) { + write_json_value(output, value.row(i)); + *output << ", "; + } + write_json_value(output, value.row(last)); + } + *output << " ]"; +} + +/** + * Write a string to JSON. + */ +template +void write_json_value(Stream&& output, const std::string& value, + int /*record_depth*/ = 0) { + *output << "\"" << internal::process_string(value) << "\""; +} + +template * = nullptr> +void write_json_value(Stream&& output, const T& value, int record_depth = 0); + +/** + * Write a vector of values to JSON. + */ +template +void write_json_value(Stream&& output, const std::vector& values, + int record_depth = 0) { + *output << "[ "; + if (values.size() > 0) { + auto last = values.end(); + --last; + for (auto it = values.begin(); it != last; ++it) { + write_json_value(output, *it, record_depth); + *output << ", "; + } + write_json_value(output, values.back(), record_depth); + } + *output << " ]"; +} + +/** + * Write a tuple of values to JSON. Each element of the tuple gets an integer + * key, starting at "1". + */ +template *> +void write_json_value(Stream&& output, const T& value, int record_depth) { + *output << '{'; + size_t i = 0; + stan::math::for_each( + [&i, record_depth, output](auto&& elt) { + if (i > 0) { + *output << ", "; + } + i++; + write_key(output, std::to_string(i), record_depth); + write_json_value(output, elt, record_depth + 1); + }, + value); + *output << '}'; +} + +} // namespace json +} // namespace stan +#endif diff --git a/src/stan/model/model_header.hpp b/src/stan/model/model_header.hpp index 5a7e8b1e99f..1d3b481f9e1 100644 --- a/src/stan/model/model_header.hpp +++ b/src/stan/model/model_header.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include diff --git a/src/test/unit/callbacks/json_writer_test.cpp b/src/test/unit/callbacks/json_writer_test.cpp index af589637b8d..9c223d947d7 100644 --- a/src/test/unit/callbacks/json_writer_test.cpp +++ b/src/test/unit/callbacks/json_writer_test.cpp @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include struct deleter_noop { @@ -38,6 +40,22 @@ TEST_F(StanInterfaceCallbacksJsonWriter, begin_end_record) { EXPECT_EQ("{}", out); } +TEST_F(StanInterfaceCallbacksJsonWriter, write_unsigned_through_base) { + stan::callbacks::structured_writer& base = writer; + base.begin_record(); + base.write("uint", 42U); + base.write("ulong", 4294967295UL); + base.write("ullong", 18446744073709551615ULL); + base.write("size", std::size_t{5}); + base.write("uint64", std::numeric_limits::max()); + base.end_record(); + EXPECT_EQ( + "{\"uint\":42,\"ulong\":4294967295," + "\"ullong\":18446744073709551615,\"size\":5," + "\"uint64\":18446744073709551615}", + output_sans_whitespace(ss)); +} + TEST_F(StanInterfaceCallbacksJsonWriter, begin_end_named_record) { writer.begin_record(); writer.begin_record("name"); @@ -193,7 +211,7 @@ TEST_F(StanInterfaceCallbacksJsonWriter, write_string_vector) { writer.write("key", x); auto out = output_sans_whitespace(ss); - EXPECT_EQ("\"key\":[0,1,2,3,4]", out); + EXPECT_EQ("\"key\":[\"0\",\"1\",\"2\",\"3\",\"4\"]", out); } TEST_F(StanInterfaceCallbacksJsonWriter, write_weird_string_vector) { @@ -202,9 +220,13 @@ TEST_F(StanInterfaceCallbacksJsonWriter, write_weird_string_vector) { for (int n = 0; n < N; ++n) x.push_back(std::to_string(n) + "\""); + writer.begin_record(); writer.write("key", x); + writer.end_record(); auto out = output_sans_whitespace(ss); - EXPECT_EQ("\"key\":[0\\\",1\\\",2\\\",3\\\",4\\\"]", out); + EXPECT_EQ("{\"key\":[\"0\\\"\",\"1\\\"\",\"2\\\"\",\"3\\\"\",\"4\\\"\"]}", + out); + ASSERT_TRUE(stan::test::is_valid_JSON(out)); } TEST_F(StanInterfaceCallbacksJsonWriter, write_null) { diff --git a/src/test/unit/io/json/write_json_value_test.cpp b/src/test/unit/io/json/write_json_value_test.cpp new file mode 100644 index 00000000000..ce752a0ee63 --- /dev/null +++ b/src/test/unit/io/json/write_json_value_test.cpp @@ -0,0 +1,100 @@ +#include +#include +#include +#include + +class StanIOWriteJsonValue : public ::testing::Test { + public: + StanIOWriteJsonValue() : ss() {} + + void SetUp() { + ss.str(std::string()); + ss.clear(); + } + + void TearDown() { ASSERT_TRUE(stan::test::is_valid_JSON(ss.str())); } + + std::stringstream ss; +}; + +TEST_F(StanIOWriteJsonValue, write_basic_int) { + stan::json::write_json_value(&ss, 1234); + EXPECT_EQ("1234", ss.str()); +} + +TEST_F(StanIOWriteJsonValue, write_basic_double) { + stan::json::write_json_value(&ss, 12.34); + EXPECT_EQ("12.34", ss.str()); +} + +TEST_F(StanIOWriteJsonValue, write_basic_complex) { + stan::json::write_json_value(&ss, std::complex{1.2, 3.4}); + EXPECT_EQ("[1.2, 3.4]", ss.str()); +} + +TEST_F(StanIOWriteJsonValue, write_string) { + stan::json::write_json_value(&ss, "f00\nb\"a'r"); + EXPECT_EQ("\"f00\\nb\\\"a'r\"", ss.str()); +} + +TEST_F(StanIOWriteJsonValue, write_vector) { + stan::json::write_json_value( + &ss, std::vector{std::vector{1, 2}, std::vector{3, 4}}); + EXPECT_EQ("[ [ 1, 2 ], [ 3, 4 ] ]", ss.str()); +} + +TEST_F(StanIOWriteJsonValue, write_special_double_vectors) { + stan::json::write_json_value( + &ss, std::vector{-0.0, std::numeric_limits::quiet_NaN(), + std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}); + EXPECT_EQ("[ -0, NaN, Inf, -Inf ]", ss.str()); +} + +TEST_F(StanIOWriteJsonValue, write_nasty_complex) { + stan::json::write_json_value( + &ss, std::complex{std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}); + EXPECT_EQ("[Inf, -Inf]", ss.str()); +} + +TEST_F(StanIOWriteJsonValue, write_eigen_vector) { + stan::json::write_json_value(&ss, Eigen::VectorXd{{1.0, 2.0, 3.0, 4.0}}); + EXPECT_EQ("[ 1, 2, 3, 4 ]", ss.str()); +} + +TEST_F(StanIOWriteJsonValue, write_eigen_row_vector) { + stan::json::write_json_value(&ss, Eigen::RowVectorXd{{1.0, 2.0, 3.0, 4.0}}); + EXPECT_EQ("[ 1, 2, 3, 4 ]", ss.str()); +} + +TEST_F(StanIOWriteJsonValue, write_eigen_matrix) { + stan::json::write_json_value( + &ss, Eigen::MatrixXd{{{1.0, 2.0, 3.0}, {4.0, 5.0, 6.0}}}); + EXPECT_EQ("[ [ 1, 2, 3 ], [ 4, 5, 6 ] ]", ss.str()); +} + +TEST_F(StanIOWriteJsonValue, write_basic_tuple) { + stan::json::write_json_value(&ss, std::tuple{1, 2.0}); + EXPECT_EQ("{\"1\" : 1, \"2\" : 2}", ss.str()); +} + +TEST_F(StanIOWriteJsonValue, write_vector_tuple) { + stan::json::write_json_value( + &ss, std::vector{std::tuple{1, 2.0}, std::tuple{3, 4.0}}); + EXPECT_EQ("[ {\"1\" : 1, \"2\" : 2}, {\"1\" : 3, \"2\" : 4} ]", ss.str()); +} + +TEST_F(StanIOWriteJsonValue, write_nasty_tuple) { + auto value = std::make_tuple( + 3.14, std::complex{1.0, 2.0}, std::tuple{1, .30}, std::vector{4.0, 5.0}, + std::vector{std::make_tuple(1, 2.0), std::make_tuple(3, 4.0)}, + Eigen::MatrixXd::Zero(2, 3)); + + stan::json::write_json_value(&ss, value); + EXPECT_EQ( + "{\"1\" : 3.14, \"2\" : [1, 2], \"3\" : { \"1\" : 1, \"2\" : 0.3}, " + "\"4\" : [ 4, 5 ], \"5\" : [ { \"1\" : 1, \"2\" : 2}, { \"1\" : 3, " + "\"2\" : 4} ], \"6\" : [ [ 0, 0, 0 ], [ 0, 0, 0 ] ]}", + ss.str()); +} diff --git a/src/test/unit/util.hpp b/src/test/unit/util.hpp index 640e141871f..19fafd87c92 100644 --- a/src/test/unit/util.hpp +++ b/src/test/unit/util.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -170,9 +171,21 @@ void reset_std_streams() { * Validate JSON using rapidjson parser. * @param text String of JSON */ -bool is_valid_JSON(std::string& text) { +inline bool is_valid_JSON(const std::string& text) { rapidjson::Document document; - return !document.Parse<0>(text.c_str()).HasParseError(); + const auto& res + = document.Parse(text.c_str()); + if (res.HasParseError()) { + rapidjson::ParseErrorCode err = res.GetParseError(); + + std::cerr << "Error in JSON parsing at offset " << res.GetErrorOffset() + << ": " << std::endl; + + std::cerr << rapidjson::GetParseError_En(err) << std::endl; + } + return !res.HasParseError(); } } // namespace test