Commit 83b07aec authored by Aleksandr Sasha Sergeev's avatar Aleksandr Sasha Sergeev Committed by Facebook GitHub Bot

Add path to problematic element into json print_error

Summary:
To facilitate debugging.

Privacy note: now `what()` message may contain part of JSON.

parser_error `what()` message:

- before the change:
`folly::toJson: JSON object value was a NaN`

- after the change:
`folly::toJson: JSON object value was a NaN when serializing value at "outerKey"->"innerKey"`

Reviewed By: yfeldblum

Differential Revision: D27623818

fbshipit-source-id: 86e61a0fed500eddafbd3e3bb1b9ed363ba8fcae
parent 40233942
......@@ -19,6 +19,7 @@
#include <algorithm>
#include <functional>
#include <iterator>
#include <sstream>
#include <type_traits>
#include <boost/algorithm/string.hpp>
......@@ -54,21 +55,66 @@ parse_error make_parse_error(
}
struct Printer {
// Context class is allows to restore the path to element that we are about to
// print so that if error happens we can throw meaningful exception.
class Context {
public:
Context(const Context* parent_context, const dynamic& key)
: parent_context_(parent_context), key_(key), is_key_(false) {}
Context(const Context* parent_context, const dynamic& key, bool is_key)
: parent_context_(parent_context), key_(key), is_key_(is_key) {}
// Return location description of a context as a chain of keys
// ex., '"outherKey"->"innerKey"'.
std::string locationDescription() const {
std::vector<std::string> keys;
const Context* ptr = parent_context_;
while (ptr) {
keys.push_back(ptr->getName());
ptr = ptr->parent_context_;
}
keys.push_back(getName());
std::ostringstream stream;
std::reverse_copy(
keys.begin(),
keys.end() - 1,
std::ostream_iterator<std::string>(stream, "->"));
// Add current key.
stream << keys.back();
return stream.str();
}
std::string getName() const {
return Printer::toStringOr(key_, "<unprintable>");
}
std::string typeDescription() const { return is_key_ ? "key" : "value"; }
private:
const Context* const parent_context_;
const dynamic& key_;
bool is_key_;
};
explicit Printer(
std::string& out, unsigned* indentLevel, serialization_opts const* opts)
: out_(out), indentLevel_(indentLevel), opts_(*opts) {}
void operator()(dynamic const& v) const {
void operator()(dynamic const& v, const Context& context) const {
(*this)(v, &context);
}
void operator()(dynamic const& v, const Context* context) const {
switch (v.type()) {
case dynamic::DOUBLE:
if (!opts_.allow_nan_inf) {
if (std::isnan(v.asDouble())) {
throw json::print_error(
"folly::toJson: JSON object value was a NaN");
"folly::toJson: JSON object value was a NaN when serializing " +
contextDescription(context));
}
if (std::isinf(v.asDouble())) {
throw json::print_error(
"folly::toJson: JSON object value was an INF");
"folly::toJson: JSON object value was an INF when serializing " +
contextDescription(context));
}
}
toAppend(
......@@ -94,10 +140,10 @@ struct Printer {
escapeString(v.asString(), out_, opts_);
break;
case dynamic::OBJECT:
printObject(v);
printObject(v, context);
break;
case dynamic::ARRAY:
printArray(v);
printArray(v, context);
break;
default:
CHECK(0) << "Bad type " << v.type();
......@@ -105,28 +151,33 @@ struct Printer {
}
private:
void printKV(const std::pair<const dynamic, dynamic>& p) const {
void printKV(
const std::pair<const dynamic, dynamic>& p,
const Context* context) const {
if (!opts_.allow_non_string_keys && !p.first.isString()) {
throw json::print_error(
"folly::toJson: JSON object key was not a "
"string");
"folly::toJson: JSON object key " +
toStringOr(p.first, "<unprintable key>") +
" was not a string when serializing key at " +
Context(context, p.first, true).locationDescription());
}
(*this)(p.first);
(*this)(p.first, Context(context, p.first, true)); // Key
mapColon();
(*this)(p.second);
(*this)(p.second, Context(context, p.first, false)); // Value
}
template <typename Iterator>
void printKVPairs(Iterator begin, Iterator end) const {
printKV(*begin);
void printKVPairs(
Iterator begin, Iterator end, const Context* context) const {
printKV(*begin, context);
for (++begin; begin != end; ++begin) {
out_ += ',';
newline();
printKV(*begin);
printKV(*begin, context);
}
}
void printObject(dynamic const& o) const {
void printObject(dynamic const& o, const Context* context) const {
if (o.empty()) {
out_ += "{}";
return;
......@@ -149,16 +200,38 @@ struct Printer {
} else {
sort_keys_by(refs.begin(), refs.end(), std::less<>());
}
printKVPairs(refs.cbegin(), refs.cend());
printKVPairs(refs.cbegin(), refs.cend(), context);
} else {
printKVPairs(o.items().begin(), o.items().end());
printKVPairs(o.items().begin(), o.items().end(), context);
}
outdent();
newline();
out_ += '}';
}
void printArray(dynamic const& a) const {
static std::string toStringOr(dynamic const& v, const char* placeholder) {
try {
std::string result;
unsigned indentLevel = 0;
serialization_opts opts;
opts.allow_nan_inf = true;
opts.allow_non_string_keys = true;
Printer printer(result, &indentLevel, &opts);
printer(v, nullptr);
return result;
} catch (...) {
return placeholder;
}
}
static std::string contextDescription(const Context* context) {
if (!context) {
return "<undefined location>";
}
return context->typeDescription() + " at " + context->locationDescription();
}
void printArray(dynamic const& a, const Context* context) const {
if (a.empty()) {
out_ += "[]";
return;
......@@ -167,11 +240,11 @@ struct Printer {
out_ += '[';
indent();
newline();
(*this)(a[0]);
for (auto& val : range(std::next(a.begin()), a.end())) {
(*this)(a[0], Context(context, dynamic(0)));
for (auto it = std::next(a.begin()); it != a.end(); ++it) {
out_ += ',';
newline();
(*this)(val);
(*this)(*it, Context(context, dynamic(std::distance(a.begin(), it))));
}
outdent();
newline();
......@@ -659,7 +732,7 @@ std::string serialize(dynamic const& dyn, serialization_opts const& opts) {
std::string ret;
unsigned indentLevel = 0;
Printer p(ret, opts.pretty_formatting ? &indentLevel : nullptr, &opts);
p(dyn);
p(dyn, nullptr);
return ret;
}
......
......@@ -399,6 +399,49 @@ TEST(Json, Produce) {
EXPECT_EQ("NaN", folly::json::serialize(parseJson("NaN"), opts));
}
TEST(Json, PrintExceptionErrorMessages) {
folly::json::serialization_opts opts;
opts.allow_nan_inf = false;
try {
const char* jsonWithInfValue =
"[{}, {}, {\"outerKey\":{\"innerKey\": Infinity}}]";
EXPECT_THROW(
toJson(folly::json::serialize(parseJson(jsonWithInfValue), opts)),
print_error);
toJson(folly::json::serialize(parseJson(jsonWithInfValue), opts));
} catch (const print_error& error) {
EXPECT_EQ(
std::string(
"folly::toJson: JSON object value was an INF when serializing value at 2->\"outerKey\"->\"innerKey\""),
error.what());
}
try {
const char* jsonWithNanValue = "[{\"outerKey\":{\"innerKey\": NaN}}]";
EXPECT_THROW(
toJson(folly::json::serialize(parseJson(jsonWithNanValue), opts)),
print_error);
toJson(folly::json::serialize(parseJson(jsonWithNanValue), opts));
} catch (const print_error& error) {
EXPECT_EQ(
std::string(
"folly::toJson: JSON object value was a NaN when serializing value at 0->\"outerKey\"->\"innerKey\""),
error.what());
}
try {
const dynamic jsonWithNonStringKey(
dynamic::object("abc", "xyz")(42.33, "asd"));
EXPECT_THROW(toJson(jsonWithNonStringKey), print_error);
toJson(jsonWithNonStringKey);
} catch (const print_error& error) {
EXPECT_EQ(
std::string(
"folly::toJson: JSON object key 42.33 was not a string when serializing key at 42.33"),
error.what());
}
}
TEST(Json, JsonEscape) {
folly::json::serialization_opts opts;
EXPECT_EQ(
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment