Commit c56b5004 authored by Norby, Tom's avatar Norby, Tom
Browse files

Template JSONParser; add tests.

parent 44a307f8
Pipeline #95934 failed with stages
in 5 minutes and 17 seconds
...@@ -8,13 +8,13 @@ ...@@ -8,13 +8,13 @@
#include <sstream> #include <sstream>
#include <vector> #include <vector>
#include "radixcore/value.hh"
#include "radixcore/visibility.hh" #include "radixcore/visibility.hh"
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// see www.json.org for parsing grammar // see www.json.org for parsing grammar
namespace radix namespace radix
{ {
template <class JsonType>
class RADIX_PUBLIC JSONParser class RADIX_PUBLIC JSONParser
{ {
public: public:
...@@ -29,7 +29,7 @@ class RADIX_PUBLIC JSONParser ...@@ -29,7 +29,7 @@ class RADIX_PUBLIC JSONParser
} }
//------------------------------------------------------------------------- //-------------------------------------------------------------------------
Value& root() { return m_root; } JsonType& root() { return m_root; }
//------------------------------------------------------------------------- //-------------------------------------------------------------------------
std::string last_error() std::string last_error()
...@@ -136,7 +136,7 @@ class RADIX_PUBLIC JSONParser ...@@ -136,7 +136,7 @@ class RADIX_PUBLIC JSONParser
} }
//------------------------------------------------------------------------- //-------------------------------------------------------------------------
Value parse_array() JsonType parse_array()
{ {
if (m_po >= m_text.size()) return nullptr; if (m_po >= m_text.size()) return nullptr;
if (m_text[m_po] != '[') return nullptr; if (m_text[m_po] != '[') return nullptr;
...@@ -146,11 +146,11 @@ class RADIX_PUBLIC JSONParser ...@@ -146,11 +146,11 @@ class RADIX_PUBLIC JSONParser
size_t col_prev = m_col; size_t col_prev = m_col;
bool trailing_comma = false; bool trailing_comma = false;
Value parent = Value(DataArray()); JsonType parent = JsonType(DataArray());
for (; m_po < m_text.size(); m_po++, m_col++) for (; m_po < m_text.size(); m_po++, m_col++)
{ {
skip_whitespace(); skip_whitespace();
Value child = parse_value(); JsonType child = parse_value();
if (child.is_null()) if (child.is_null())
{ {
if (m_last_error != "") if (m_last_error != "")
...@@ -168,7 +168,7 @@ class RADIX_PUBLIC JSONParser ...@@ -168,7 +168,7 @@ class RADIX_PUBLIC JSONParser
} }
else if (m_text[m_po] == ']') else if (m_text[m_po] == ']')
break; break;
return Value(); return JsonType();
} }
else else
{ {
...@@ -191,13 +191,13 @@ class RADIX_PUBLIC JSONParser ...@@ -191,13 +191,13 @@ class RADIX_PUBLIC JSONParser
m_last_error = "invalid character '"; m_last_error = "invalid character '";
m_last_error += ch; m_last_error += ch;
m_last_error += "' in array"; m_last_error += "' in array";
return Value(); return JsonType();
} }
} }
if (m_po >= m_text.size() || m_text[m_po] != ']') if (m_po >= m_text.size() || m_text[m_po] != ']')
{ {
m_last_error = "no closing bracket ']' for array"; m_last_error = "no closing bracket ']' for array";
return Value(); return JsonType();
} }
m_po++; m_po++;
m_col++; m_col++;
...@@ -211,9 +211,9 @@ class RADIX_PUBLIC JSONParser ...@@ -211,9 +211,9 @@ class RADIX_PUBLIC JSONParser
// (0|([1-9][0-9]*)) // (0|([1-9][0-9]*))
// (\.[0-9]+)? // (\.[0-9]+)?
// ([Ee][+-]?[0-9]+)? // ([Ee][+-]?[0-9]+)?
Value parse_number() JsonType parse_number()
{ {
if (m_po >= m_text.size()) return Value(); if (m_po >= m_text.size()) return JsonType();
size_t len = 0; size_t len = 0;
...@@ -226,7 +226,7 @@ class RADIX_PUBLIC JSONParser ...@@ -226,7 +226,7 @@ class RADIX_PUBLIC JSONParser
if (m_po >= m_text.size()) if (m_po >= m_text.size())
{ {
m_last_error = "invalid number (no digits after -)"; m_last_error = "invalid number (no digits after -)";
return Value(); return JsonType();
} }
} }
...@@ -235,7 +235,7 @@ class RADIX_PUBLIC JSONParser ...@@ -235,7 +235,7 @@ class RADIX_PUBLIC JSONParser
if (m_po >= m_text.size() || !(ch >= '0' && m_text[m_po] <= '9')) if (m_po >= m_text.size() || !(ch >= '0' && m_text[m_po] <= '9'))
{ {
m_last_error = "invalid number (no digits)"; m_last_error = "invalid number (no digits)";
return Value(); return JsonType();
} }
m_po++; m_po++;
m_col++; m_col++;
...@@ -254,12 +254,13 @@ class RADIX_PUBLIC JSONParser ...@@ -254,12 +254,13 @@ class RADIX_PUBLIC JSONParser
{ {
try try
{ {
Value node = Value(std::stod(std::string(&m_text[m_po - len], len))); JsonType node =
JsonType(std::stod(std::string(&m_text[m_po - len], len)));
return node; return node;
} }
catch (...) catch (...)
{ {
return Value(); return JsonType();
} }
} }
ch = m_text[m_po]; ch = m_text[m_po];
...@@ -273,7 +274,7 @@ class RADIX_PUBLIC JSONParser ...@@ -273,7 +274,7 @@ class RADIX_PUBLIC JSONParser
if (m_po >= m_text.size()) if (m_po >= m_text.size())
{ {
m_last_error = "invalid number (no digits after decimal)"; m_last_error = "invalid number (no digits after decimal)";
return Value(); return JsonType();
} }
ch = m_text[m_po]; ch = m_text[m_po];
size_t n_digits = 0; size_t n_digits = 0;
...@@ -286,7 +287,7 @@ class RADIX_PUBLIC JSONParser ...@@ -286,7 +287,7 @@ class RADIX_PUBLIC JSONParser
if (n_digits == 0) if (n_digits == 0)
{ {
m_last_error = "invalid number (no digits after decimal)"; m_last_error = "invalid number (no digits after decimal)";
return Value(); return JsonType();
} }
} }
...@@ -299,7 +300,7 @@ class RADIX_PUBLIC JSONParser ...@@ -299,7 +300,7 @@ class RADIX_PUBLIC JSONParser
if (m_po >= m_text.size()) if (m_po >= m_text.size())
{ {
m_last_error = "invalid number (no digits for exponent)"; m_last_error = "invalid number (no digits for exponent)";
return Value(); return JsonType();
} }
ch = m_text[m_po]; ch = m_text[m_po];
// [+-]? // [+-]?
...@@ -312,7 +313,7 @@ class RADIX_PUBLIC JSONParser ...@@ -312,7 +313,7 @@ class RADIX_PUBLIC JSONParser
if (m_po >= m_text.size()) if (m_po >= m_text.size())
{ {
m_last_error = "invalid number (no digits for exponent)"; m_last_error = "invalid number (no digits for exponent)";
return Value(); return JsonType();
} }
size_t n_digits = 0; size_t n_digits = 0;
...@@ -325,18 +326,19 @@ class RADIX_PUBLIC JSONParser ...@@ -325,18 +326,19 @@ class RADIX_PUBLIC JSONParser
if (n_digits == 0) if (n_digits == 0)
{ {
m_last_error = "invalid number (no digits after decimal)"; m_last_error = "invalid number (no digits after decimal)";
return Value(); return JsonType();
} }
} }
try try
{ {
Value node = Value(std::stod(std::string(&m_text[m_po - len], len))); JsonType node =
JsonType(std::stod(std::string(&m_text[m_po - len], len)));
return node; return node;
} }
catch (...) catch (...)
{ {
return Value(); return JsonType();
} }
} }
...@@ -357,7 +359,7 @@ class RADIX_PUBLIC JSONParser ...@@ -357,7 +359,7 @@ class RADIX_PUBLIC JSONParser
// only 3 valid literals all in lower case: false, null, true // only 3 valid literals all in lower case: false, null, true
// TODO: need to refactor code to support null return (currently it is // TODO: need to refactor code to support null return (currently it is
// treated as an error) // treated as an error)
Value parse_literal() JsonType parse_literal()
{ {
size_t len = 0; size_t len = 0;
for (; m_po + len < m_text.size(); len++) for (; m_po + len < m_text.size(); len++)
...@@ -373,32 +375,32 @@ class RADIX_PUBLIC JSONParser ...@@ -373,32 +375,32 @@ class RADIX_PUBLIC JSONParser
m_col += len; m_col += len;
if (std::string("true") == literals[i]) if (std::string("true") == literals[i])
{ {
return Value(true); return JsonType(true);
} }
else if (std::string("false") == literals[i]) else if (std::string("false") == literals[i])
{ {
return Value(false); return JsonType(false);
} }
// default to null // default to null
return Value(); return JsonType();
} }
} }
m_last_error = "invalid literal"; m_last_error = "invalid literal";
return Value(); return JsonType();
} }
//------------------------------------------------------------------------- //-------------------------------------------------------------------------
Value parse_object() JsonType parse_object()
{ {
if (m_po >= m_text.size()) return Value(); if (m_po >= m_text.size()) return JsonType();
if (m_text[m_po] != '{') return Value(); if (m_text[m_po] != '{') return JsonType();
m_po++; m_po++;
m_col++; m_col++;
size_t line_prev = m_line; size_t line_prev = m_line;
size_t col_prev = m_col; size_t col_prev = m_col;
bool trailing_comma = false; bool trailing_comma = false;
Value parent = Value(DataObject()); JsonType parent = JsonType(DataObject());
for (; m_po < m_text.size(); m_po++, m_col++) for (; m_po < m_text.size(); m_po++, m_col++)
{ {
skip_whitespace(); skip_whitespace();
...@@ -410,7 +412,7 @@ class RADIX_PUBLIC JSONParser ...@@ -410,7 +412,7 @@ class RADIX_PUBLIC JSONParser
m_last_error = "trailing comma in object"; m_last_error = "trailing comma in object";
m_line = line_prev; m_line = line_prev;
m_col = col_prev; m_col = col_prev;
return Value(); return JsonType();
} }
break; break;
} }
...@@ -425,7 +427,7 @@ class RADIX_PUBLIC JSONParser ...@@ -425,7 +427,7 @@ class RADIX_PUBLIC JSONParser
m_line = line_prev; m_line = line_prev;
m_col = col_prev; m_col = col_prev;
} }
return Value(); return JsonType();
} }
skip_whitespace(); skip_whitespace();
...@@ -433,13 +435,13 @@ class RADIX_PUBLIC JSONParser ...@@ -433,13 +435,13 @@ class RADIX_PUBLIC JSONParser
if (m_po >= m_text.size() || m_text[m_po] != ':') if (m_po >= m_text.size() || m_text[m_po] != ':')
{ {
m_last_error = "no ':' following key in object"; m_last_error = "no ':' following key in object";
return Value(); return JsonType();
} }
m_po++; m_po++;
m_col++; m_col++;
// parse value // parse value
Value child = parse_value(); JsonType child = parse_value();
if (child.is_null()) if (child.is_null())
{ {
if (m_last_error == "") if (m_last_error == "")
...@@ -452,7 +454,7 @@ class RADIX_PUBLIC JSONParser ...@@ -452,7 +454,7 @@ class RADIX_PUBLIC JSONParser
m_line = line_prev; m_line = line_prev;
m_col = col_prev; m_col = col_prev;
} }
return Value(); return JsonType();
} }
else else
{ {
...@@ -474,13 +476,13 @@ class RADIX_PUBLIC JSONParser ...@@ -474,13 +476,13 @@ class RADIX_PUBLIC JSONParser
else else
{ {
m_last_error = "invalid character in object"; m_last_error = "invalid character in object";
return Value(); return JsonType();
} }
} }
if (m_po >= m_text.size() || m_text[m_po] != '}') if (m_po >= m_text.size() || m_text[m_po] != '}')
{ {
m_last_error = "no closing curly bracket '}' for object"; m_last_error = "no closing curly bracket '}' for object";
return Value(); return JsonType();
} }
m_po++; m_po++;
m_col++; m_col++;
...@@ -584,19 +586,19 @@ class RADIX_PUBLIC JSONParser ...@@ -584,19 +586,19 @@ class RADIX_PUBLIC JSONParser
} }
//------------------------------------------------------------------------- //-------------------------------------------------------------------------
Value parse_string() JsonType parse_string()
{ {
std::string str = parse_string_contents(); std::string str = parse_string_contents();
if (m_last_error != "") return Value(); if (m_last_error != "") return JsonType();
return Value(str); return JsonType(str);
} }
//------------------------------------------------------------------------- //-------------------------------------------------------------------------
Value parse_value() JsonType parse_value()
{ {
skip_whitespace(); skip_whitespace();
char ch = m_text[m_po]; char ch = m_text[m_po];
Value node; JsonType node;
if (ch == '"') if (ch == '"')
node = parse_string(); node = parse_string();
else if (ch == '[') else if (ch == '[')
...@@ -617,7 +619,7 @@ class RADIX_PUBLIC JSONParser ...@@ -617,7 +619,7 @@ class RADIX_PUBLIC JSONParser
const char* literals[N_LITERALS]; const char* literals[N_LITERALS];
size_t literal_lens[N_LITERALS]; size_t literal_lens[N_LITERALS];
Value m_root; JsonType m_root;
std::string m_text = ""; std::string m_text = "";
......
INCLUDE(GoogleTest) INCLUDE(GoogleTest)
ADD_GOOGLE_TEST(tstJson.cc NP 1)
ADD_GOOGLE_TEST(tstString.cc NP 1) ADD_GOOGLE_TEST(tstString.cc NP 1)
ADD_GOOGLE_TEST(tstSystem.cc NP 1) ADD_GOOGLE_TEST(tstSystem.cc NP 1)
ADD_GOOGLE_TEST(tstValue.cc NP 1) ADD_GOOGLE_TEST(tstValue.cc NP 1)
configure_file(testfiles/wellformed.json testfiles/wellformed.json COPYONLY)
{
"root object": {
"bool": true,
"int": 1,
"double": 2.34,
"string": "This is a string!",
"array": [
"Array of JSON values.",
1,
2.34,
true
]
}
}
\ No newline at end of file
#include "gtest/gtest.h"
#include "radixcore/json.hh"
#include "radixcore/value.hh"
#include <QDir>
using namespace radix;
// JsonType& root() { return m_root; }
// std::string last_error()
// bool parse()
// bool parse_from_stream(std::istream& in_stream)
// bool parse_from_file(std::string fn)
TEST(JSONParser, parse_from_file)
{
QDir workingDir(".");
{
JSONParser<Value> sunnyDay;
std::string filename =
workingDir.absoluteFilePath("testfiles/wellformed.json").toStdString();
bool success = sunnyDay.parse_from_file(filename);
EXPECT_TRUE(success);
// not really an error just the null terminating at eof
std::string errorMsg = sunnyDay.last_error();
EXPECT_EQ(" at line 14 column 2", errorMsg);
Value root = sunnyDay.root();
Value rootObj = root["root object"];
EXPECT_EQ(Value::TYPE_OBJECT, rootObj.type());
EXPECT_EQ(Value::TYPE_BOOLEAN, rootObj["bool"].type());
EXPECT_TRUE(rootObj["bool"].to_bool());
// reads in as double, not int.
// EXPECT_EQ(Value::TYPE_INTEGER, rootObj["int"].type());
EXPECT_EQ(1, rootObj["int"].to_int());
EXPECT_EQ(Value::TYPE_DOUBLE, rootObj["double"].type());
EXPECT_EQ(2.34, rootObj["double"].to_double());
EXPECT_EQ(Value::TYPE_STRING, rootObj["string"].type());
EXPECT_EQ("This is a string!", rootObj["string"].to_string());
EXPECT_EQ(Value::TYPE_ARRAY, rootObj["array"].type());
DataArray& array = rootObj["array"].as_array();
EXPECT_EQ("Array of JSON values.", array.at(0).to_string());
EXPECT_EQ(1, array.at(1).to_int());
EXPECT_EQ(2.34, array.at(2).to_double());
EXPECT_TRUE(array.at(3).to_bool());
}
{
JSONParser<Value> noFile;
std::string filename =
workingDir.absoluteFilePath("wrong_path/file_doesn't_exist.json")
.toStdString();
bool success = noFile.parse_from_file(filename);
EXPECT_FALSE(success);
std::string errorMsg = noFile.last_error();
EXPECT_EQ("could not open file at line 1 column 1", errorMsg);
}
}
TEST(JSONParser, parse_from_stream)
{
// check code paths in JSONParser class
{
JSONParser<Value> jp;
std::string errorMsg = "";
// trailing junk
std::stringstream ss("{}\r\nfoo");
EXPECT_FALSE(jp.parse_from_stream(ss));
errorMsg = jp.last_error();
EXPECT_EQ("unexpected trailing character(s) at line 2 column 1", errorMsg);
// unclosed array with no child elements
ss.str("[");
EXPECT_FALSE(jp.parse_from_stream(ss));
errorMsg = jp.last_error();
EXPECT_EQ("no closing bracket ']' for array at line 1 column 2", errorMsg);
// unclosed array with at least one child element
ss.str("[ \"bar\"");
EXPECT_FALSE(jp.parse_from_stream(ss));
errorMsg = jp.last_error();
std::string expected("invalid character '\0' in array at line 1 column 8",
49);
EXPECT_EQ(expected, errorMsg);
// trailing comma in array
ss.str("[ \"baz\" , ]");
EXPECT_FALSE(jp.parse_from_stream(ss));
errorMsg = jp.last_error();
EXPECT_EQ("trailing comma in array at line 1 column 9", errorMsg);
// lone '-' at end (considered as invalid number)
ss.str("-");
EXPECT_FALSE(jp.parse_from_stream(ss));
errorMsg = jp.last_error();
EXPECT_EQ("invalid number (no digits after -) at line 1 column 2",
errorMsg);
// lone '-' followed by whitespace (considered as invalid number)
ss.str("- ");
EXPECT_FALSE(jp.parse_from_stream(ss));
errorMsg = jp.last_error();
EXPECT_EQ("invalid number (no digits) at line 1 column 2", errorMsg);
// no digits after decimal
ss.str("5.");
EXPECT_FALSE(jp.parse_from_stream(ss));
errorMsg = jp.last_error();
EXPECT_EQ("invalid number (no digits after decimal) at line 1 column 3",
errorMsg);
// no digits after decimal
ss.str("5. ");
EXPECT_FALSE(jp.parse_from_stream(ss));
errorMsg = jp.last_error();
EXPECT_EQ("invalid number (no digits after decimal) at line 1 column 3",
errorMsg);
// no digits after 'e' in exponent at end
ss.str("1.2e");
EXPECT_FALSE(jp.parse_from_stream(ss));
errorMsg = jp.last_error();
EXPECT_EQ("invalid number (no digits for exponent) at line 1 column 5",
errorMsg);
// no digits after 'e' in exponent at end
ss.str("1.2e-");
EXPECT_FALSE(jp.parse_from_stream(ss