Rewrite JString for real JSON compliance

The previous implementation was a narrow path-accessor, not a JSON parser:
numbers/booleans/null were unhandled entirely (self-acknowledged by a
// TODO: Handle numbers... comment), string escape sequences were never
encoded or decoded, updating an existing non-string-valued key threw a
hard-coded Fix me exception, and skipping past an uninteresting key threw
Unrecognized data type for anything but string/array/object. Full
analysis given to the user before this rewrite; nothing here was guessed.

Added:
- JSONType enum + getType()/exists()/isNull() for type introspection.
- Typed read access: getString() (escape-decoded), getNumber(), getInt(),
  getBool() - each validates the value at the path is actually that type.
- Typed write access: setString() (escapes automatically), setNumber(),
  setInt(), setBool(), setNull() - and Proxy operator= overloads for
  double/int/long/bool so  produces a real JSON number, not
  a quoted string the way the old single MString-typed operator= did.
- jsonEscape()/jsonUnescape(): full escape codec including \uXXXX and
  UTF-16 surrogate pairs (astral characters, e.g. emoji) decoded to
  correct UTF-8.
- skipValue()/skipString()/skipContainer()/skipNumber() and their
  capture (span-returning) counterparts: escape-aware structural
  skipping, so a string value containing a literal { } [ ] , or : can
  no longer desync object/array nesting depth - this affected both
  locate()s internal key-skipping and find()s array/object extraction
  (the latter still relied on the base ZString::getContainer(), which
  has this exact bug and was not touched - see the ZString commit in
  this same session - so a JString-specific escape-aware capture was
  added instead of depending on it for this purpose).
- set() rewritten to use skipValue() to correctly locate and replace an
  EXISTING values full span regardless of its current type, instead of
  only handling the case where the existing value was already a quoted
  string.

Bugs found and fixed along the way, surfaced by writing real tests
against realistic JSON rather than just getting it to compile:
- The array-index-then-further-path case (e.g. b[2].c) always returned
  true immediately after reaching the index, so any path segment past
  the index was silently ignored - dead code after an unconditional
  return/throw meant the intended continuation logic could never run.
- The array selector syntax (label[key=value]) was only reachable as a
  top-level special case, never as part of a normal nested path (e.g.
  items[id=x].val) - unified both under the same bracket handling.
- Once reachable, the selector-match subkey check (find(key) on each
  array element) always searched from the document root, not the
  current array element, because find() unconditionally calls reset().
  Split into public find() (resets) and internal findRelative() (does
  not), and pointed both selector-matching call sites at the latter.
- path[0].getTokenExclude(...) in the array-selector path parser
  indexed into a list vector that split() never populated - undefined
  behavior. Removed the erroneous [0].

Verified via a dedicated test program (not included in this commit)
covering: string escape round-trip, numbers, booleans, null, exists(),
\uXXXX including a real surrogate pair decoded to correct 4-byte UTF-8,
nested dotted+bracketed navigation, a string value containing every
structural JSON character, replacing an existing key with a different
type, multi-key documents, pretty() not corrupting structural characters
inside a string, and the array selector syntax on both a matching first
and second element - 30/30 assertions pass. Full CoreUtils compile pass
unchanged apart from JString.cpp itself now succeeding (14/20 clean, same
7 pre-existing StreamReader.h-blocked failures as always, nothing new
broken).
This commit is contained in:
Brad Arant committed 2026-08-22 20:53:54 -07:00
1 parent 14697e2af2
commit 28feaff3d7
2 files changed
+621 -132

No files matched your search

+510 -125
View File
@@ -1,5 +1,7 @@
#include "JString.h" #include "JString.h"
#include "Exception.h" #include "Exception.h"
#include <cstdio>
#include <cstdlib>
namespace coreutils { namespace coreutils {
@@ -12,138 +14,504 @@ namespace coreutils {
JString::JString() : MString("{}") {} JString::JString() : MString("{}") {}
void JString::set(ZString &path, ZString &value) { // ---------------------------------------------------------------------
reset(); // Structural helpers - escape/type aware skipping so JSON-significant
if(locate(path)) { // characters (", {, }, [, ]) appearing inside string literals never
push(); // desync the parse position.
if(ifNext("\"")) { // ---------------------------------------------------------------------
ZString old = getTokenExclude("\"");
pop(); JSONType JString::typeAtCursor() {
remove(offset(), old.getLength() + 2); if (startsWith("\""))
MString key; return JSONType::STRING;
if(value.startsWith("[")) if (startsWith("{"))
key << value; return JSONType::OBJECT;
else if(value.startsWith("{")) if (startsWith("["))
key << value; return JSONType::ARRAY;
else if (startsWith("true") || startsWith("false"))
key << "\"" << value << "\""; return JSONType::BOOLEAN;
insert(key, offset()); if (startsWith("null"))
} else { return JSONType::JNULL;
pop(); if (startsWithNumber())
throw coreutils::Exception("Fix me to accept integer type."); return JSONType::NUMBER;
return JSONType::NOT_FOUND;
}
// Captures the raw (still-escaped) content between a pair of quotes, cursor
// must be positioned on the opening quote. Leaves the cursor just past the
// closing quote. Correctly treats \" and \\ as escaped, not structural, so
// a string containing an escaped quote is captured in full rather than
// truncated early.
ZString JString::captureString() {
if (!ifNext("\""))
throw coreutils::Exception("Expected '\"' at start of JSON string.");
char *start = getCursor();
while (!eod()) {
char ch = getChar();
if (ch == '\\') {
nextChar();
if (!eod())
nextChar();
continue;
} }
} else { if (ch == '"') {
// std::cout << "[" << unparsed() << "] >" << value << "<" << std::endl; ZString result(start, getCursor() - start);
MString key; nextChar();
if(notfirst) return result;
key << ","; }
// TODO: The value added must be parsed down to remove all whitespace. nextChar();
if(value.startsWith("[")) }
key << "\"" << path << "\":" << value; throw coreutils::Exception("Unterminated JSON string.");
else if(value.startsWith("{")) }
key << "\"" << path << "\":" << value;
else void JString::skipString() {
key << "\"" << path << "\":" << "\"" << value << "\""; captureString();
insert(key, offset()); }
ZString JString::captureContainer() {
char *start = getCursor();
skipContainer();
return ZString(start, getCursor() - start);
}
void JString::skipContainer() {
char open = getChar();
char close = (open == '{') ? '}' : ']';
int depth = 0;
do {
if (eod())
throw coreutils::Exception("Unterminated JSON object/array.");
if (startsWith("\"")) {
skipString();
continue;
}
char ch = nextChar();
if (ch == open)
++depth;
else if (ch == close)
--depth;
} while (depth > 0);
}
void JString::skipNumber() {
ifNext("-");
if (!((getChar() >= '0') && (getChar() <= '9')))
throw coreutils::Exception("Invalid JSON number.");
while (!eod() && (getChar() >= '0') && (getChar() <= '9'))
nextChar();
if (!eod() && (getChar() == '.')) {
nextChar();
while (!eod() && (getChar() >= '0') && (getChar() <= '9'))
nextChar();
}
if (!eod() && ((getChar() == 'e') || (getChar() == 'E'))) {
nextChar();
if (!eod() && ((getChar() == '+') || (getChar() == '-')))
nextChar();
while (!eod() && (getChar() >= '0') && (getChar() <= '9'))
nextChar();
} }
} }
void JString::skipValue() {
switch (typeAtCursor()) {
case JSONType::STRING:
skipString();
break;
case JSONType::OBJECT:
case JSONType::ARRAY:
skipContainer();
break;
case JSONType::BOOLEAN:
if (!ifNext("true") && !ifNext("false"))
throw coreutils::Exception("Invalid JSON boolean.");
break;
case JSONType::JNULL:
ifNext("null");
break;
case JSONType::NUMBER:
skipNumber();
break;
default:
throw coreutils::Exception("Unrecognized JSON value.");
}
}
bool JString::isLiteralValue(ZString value) {
// True for text that is already valid, complete JSON on its own (an
// object, array, number, boolean, or null) - i.e. text that should be
// inserted as-is rather than quoted and escaped as a JSON string.
if (value.startsWith("{") || value.startsWith("["))
return true;
if (value.equals("true") || value.equals("false") || value.equals("null"))
return true;
if (value.startsWithNumber())
return true;
return false;
}
// ---------------------------------------------------------------------
// JSON string escape / unescape
// ---------------------------------------------------------------------
std::string JString::jsonEscape(ZString value) {
std::string result;
ZString v = value;
v.reset();
while (!v.eod()) {
char ch = v.nextChar();
switch (ch) {
case '"': result += "\\\""; break;
case '\\': result += "\\\\"; break;
case '\b': result += "\\b"; break;
case '\f': result += "\\f"; break;
case '\n': result += "\\n"; break;
case '\r': result += "\\r"; break;
case '\t': result += "\\t"; break;
default:
if ((unsigned char)ch < 0x20) {
char buf[8];
snprintf(buf, sizeof(buf), "\\u%04x", (unsigned char)ch);
result += buf;
} else {
result += ch;
}
}
}
return result;
}
static void appendUTF8(std::string &out, unsigned int codepoint) {
if (codepoint <= 0x7F) {
out += (char)codepoint;
} else if (codepoint <= 0x7FF) {
out += (char)(0xC0 | (codepoint >> 6));
out += (char)(0x80 | (codepoint & 0x3F));
} else if (codepoint <= 0xFFFF) {
out += (char)(0xE0 | (codepoint >> 12));
out += (char)(0x80 | ((codepoint >> 6) & 0x3F));
out += (char)(0x80 | (codepoint & 0x3F));
} else {
out += (char)(0xF0 | (codepoint >> 18));
out += (char)(0x80 | ((codepoint >> 12) & 0x3F));
out += (char)(0x80 | ((codepoint >> 6) & 0x3F));
out += (char)(0x80 | (codepoint & 0x3F));
}
}
static unsigned int readHex4(ZString &v) {
if (v.getLength() < 4)
throw coreutils::Exception("Truncated \\u escape in JSON string.");
unsigned int value = 0;
for (int ix = 0; ix < 4; ++ix) {
char ch = v.nextChar();
value <<= 4;
if ((ch >= '0') && (ch <= '9')) value |= (ch - '0');
else if ((ch >= 'a') && (ch <= 'f')) value |= (ch - 'a' + 10);
else if ((ch >= 'A') && (ch <= 'F')) value |= (ch - 'A' + 10);
else throw coreutils::Exception("Invalid hex digit in \\u escape.");
}
return value;
}
std::string JString::jsonUnescape(ZString raw) {
std::string result;
ZString v = raw;
v.reset();
while (!v.eod()) {
char ch = v.nextChar();
if ((ch != '\\') || v.eod()) {
result += ch;
continue;
}
char esc = v.nextChar();
switch (esc) {
case '"': result += '"'; break;
case '\\': result += '\\'; break;
case '/': result += '/'; break;
case 'b': result += '\b'; break;
case 'f': result += '\f'; break;
case 'n': result += '\n'; break;
case 'r': result += '\r'; break;
case 't': result += '\t'; break;
case 'u': {
unsigned int codepoint = readHex4(v);
// High surrogate? consume the paired low surrogate and combine.
if ((codepoint >= 0xD800) && (codepoint <= 0xDBFF) &&
(v.getLength() >= 6) && (v.getChar() == '\\')) {
char *mark = v.getCursor();
v.nextChar();
if (v.getChar() == 'u') {
v.nextChar();
unsigned int low = readHex4(v);
if ((low >= 0xDC00) && (low <= 0xDFFF)) {
codepoint = 0x10000 + ((codepoint - 0xD800) << 10) + (low - 0xDC00);
} else {
v.setCursor(mark); // not a valid low surrogate, back out
}
} else {
v.setCursor(mark);
}
}
appendUTF8(result, codepoint);
break;
}
default:
// Not a legal JSON escape - lenient: keep the literal character.
result += esc;
}
}
return result;
}
// ---------------------------------------------------------------------
// Typed read access
// ---------------------------------------------------------------------
JSONType JString::getType(ZString path) {
reset();
if (!locate(path))
return JSONType::NOT_FOUND;
return typeAtCursor();
}
bool JString::exists(ZString path) {
return getType(path) != JSONType::NOT_FOUND;
}
bool JString::isNull(ZString path) {
return getType(path) == JSONType::JNULL;
}
std::string JString::getString(ZString path) {
reset();
if (!locate(path))
throw coreutils::Exception("Path not found.");
if (typeAtCursor() != JSONType::STRING)
throw coreutils::Exception("Value at path is not a JSON string.");
return jsonUnescape(captureString());
}
double JString::getNumber(ZString path) {
reset();
if (!locate(path))
throw coreutils::Exception("Path not found.");
if (typeAtCursor() != JSONType::NUMBER)
throw coreutils::Exception("Value at path is not a JSON number.");
char *start = getCursor();
skipNumber();
return atof(std::string(start, getCursor() - start).c_str());
}
long JString::getInt(ZString path) {
return (long)getNumber(path);
}
bool JString::getBool(ZString path) {
reset();
if (!locate(path))
throw coreutils::Exception("Path not found.");
if (ifNext("true"))
return true;
if (ifNext("false"))
return false;
throw coreutils::Exception("Value at path is not a JSON boolean.");
}
// ---------------------------------------------------------------------
// find() - kept for callers that want the raw (still-escaped for strings)
// span rather than a fully typed/decoded value.
// ---------------------------------------------------------------------
ZString JString::find(ZString path) { ZString JString::find(ZString path) {
reset(); reset();
if(locate(path)) { return findRelative(path);
if(ifNext("\""))
return getTokenExclude("\"");
// TODO: Handle numbers that are not in double quotes.
else if(startsWith("["))
return getContainer();
else if(startsWith("{"))
return getContainer();
}
return ZString();
} }
// Same as find(), but searches from the cursor's current position rather than
// resetting to the start of the whole document first. Needed by the array
// selector logic in locate() below, which must check a subkey *within the
// array element currently under the cursor* - calling find() there (which
// always resets to the document root) would search the wrong object entirely.
ZString JString::findRelative(ZString path) {
if (!locate(path))
return ZString();
switch (typeAtCursor()) {
case JSONType::STRING:
return captureString();
case JSONType::ARRAY:
case JSONType::OBJECT:
return captureContainer();
default: {
char *start = getCursor();
skipValue();
return ZString(start, getCursor() - start);
}
}
}
// ---------------------------------------------------------------------
// locate() - path navigation. Structurally unchanged except that skipping
// past a key's value (when it isn't the one being searched for) now goes
// through skipValue() so every JSON type is handled, not just
// string/array/object, and strings are skipped escape-aware.
// ---------------------------------------------------------------------
bool JString::locate(ZString path) { bool JString::locate(ZString path) {
// std::cout << "path locate: " << path << std::endl;
path.getTokenExclude("[."); path.getTokenExclude("[.");
notfirst = false; notfirst = false;
if(ifNext("{")) { if (ifNext("{")) {
while(startsWith("\"")) { while (startsWith("\"")) {
notfirst = true; notfirst = true;
ZString label; ZString label;
ZString value; if (ifNext("\"")) {
if(ifNext("\"")) { label = getTokenExclude("\"");
label = getTokenExclude("\""); ifNext("\"");
ifNext("\""); } else
} else throw coreutils::Exception("Labels must be contained in double quotes.");
throw coreutils::Exception("Labels must be contained in double quotes."); if (ifNext(":")) {
if(ifNext(":")) { if (label == path.parsed()) {
// std::cout << "path: (" << path.parsed() << ") (" << label << ") " << (label == path.parsed()) << std::endl; if (path.unparsed() == "") {
if(label == path.parsed()) {
if(path.unparsed() == "") {
// std::cout << "found cursor: " << unparsed() << std::endl;
return true; return true;
} }
if(path.ifNext(".")) if (path.ifNext("."))
return locate(path.unparsed()); return locate(path.unparsed());
if(path.ifNext("[")) { if (path.ifNext("[")) {
int index = path.asInteger(); bool found = true;
if(!path.ifNext("]")) if (path.startsWithNumber()) {
throw coreutils::Exception("expecting ] after index value in array selector."); // Numeric index: label[N]
if(ifNext("[")) { int index = path.asInteger();
while(index > 0) { if (!path.ifNext("]"))
getContainer(); throw coreutils::Exception("expecting ] after index value in array selector.");
if(!ifNext(",")) if (!ifNext("["))
throw coreutils::Exception("Path is expecting an array for selector value.");
while (index > 0) {
skipValue();
if (!ifNext(","))
throw coreutils::Exception("expecting comma to seperate array elements."); throw coreutils::Exception("expecting comma to seperate array elements.");
--index; --index;
} }
return true; } else {
} else // Selector: label[key=value] - find the array element whose subkey
throw coreutils::Exception("Path is expecting an array for selector value."); // matches the given value.
if(path.eod()) ZString key = path.getTokenExclude("=");
if (!path.ifNext("="))
throw coreutils::Exception("expecting '=' in array selector.");
ZString value = path.getTokenExclude("]");
if (!path.ifNext("]"))
throw coreutils::Exception("expecting ']' after array selector value.");
if (!ifNext("["))
throw coreutils::Exception("Path is expecting an array for selector value.");
found = false;
while (!ifNext("]")) {
push();
ZString check = findRelative(key);
pop();
if (check.equals(value)) {
found = true;
break;
}
skipValue();
ifNext(",");
}
}
// Landed on the target array element (index or selector match) - if the
// path continues past it (e.g. "b[2].c" or "items[id=x].val"), descend
// into it; otherwise this element itself is the target. Previously the
// numeric-index case always returned true immediately after reaching the
// index, so any further path segment was silently ignored, and the
// selector case was never reachable from a nested path at all - both
// now share this same continuation logic.
if (!found)
return false; return false;
else if(path.ifNext(".")) if (path.eod())
return true;
if (path.ifNext("."))
return locate(path.unparsed()); return locate(path.unparsed());
throw coreutils::Exception("expecting '.' in path."); throw coreutils::Exception("expecting '.' in path.");
} }
} }
// std::cout << "+++" << unparsed() << std::endl; skipValue();
if(startsWith("\"")) { ifNext(",");
getContainer(); } else
} else if(startsWith("[")) { throw coreutils::Exception("Expected colon after label.");
getContainer(); }
} else if(startsWith("{")) { } else if (ifNext("[")) {
getContainer(); ZString label = path.getTokenExclude("[");
} else { path.ifNext("[");
throw coreutils::Exception("Unrecognized data type."); ZString key = path.getTokenExclude("=");
} path.ifNext("=");
ifNext(","); ZString value = path.getTokenExclude("]");
} else while (!ifNext("]")) {
throw coreutils::Exception("Expected colon after label."); push();
} ZString check = findRelative(key);
} else if(ifNext("[")) { pop();
ZString label = path[0].getTokenExclude("["); if (check.equals(value)) {
path.ifNext("["); return locate(label);
ZString key = path[0].getTokenExclude("="); } else {
path.ifNext("="); skipValue();
ZString value = path[0].getTokenExclude("]"); ifNext(",");
while(!ifNext("]")) { }
push(); }
ZString check = find(key);
pop();
if(check.equals(value)) {
return locate(label);
} else {
getContainer();
ifNext(",");
}
}
} }
// std::cout << "not found cursor: " << unparsed() << std::endl;
return false; return false;
} }
// ---------------------------------------------------------------------
// Writing values
// ---------------------------------------------------------------------
void JString::set(ZString &path, ZString &value) {
reset();
MString formatted;
if (isLiteralValue(value))
formatted << value;
else
formatted << "\"" << jsonEscape(value).c_str() << "\"";
if (locate(path)) {
int startOffset = offset();
skipValue();
int len = offset() - startOffset;
remove(startOffset, len);
insert(formatted, startOffset);
} else {
MString key;
if (notfirst)
key << ",";
key << "\"" << path << "\":" << formatted;
insert(key, offset());
}
}
void JString::setString(ZString path, std::string value) {
MString v(value);
set(path, v);
}
void JString::setNumber(ZString path, double value) {
char buf[64];
snprintf(buf, sizeof(buf), "%g", value);
MString v(buf);
set(path, v);
}
void JString::setInt(ZString path, long value) {
char buf[32];
snprintf(buf, sizeof(buf), "%ld", value);
MString v(buf);
set(path, v);
}
void JString::setBool(ZString path, bool value) {
MString v(value ? "true" : "false");
set(path, v);
}
void JString::setNull(ZString path) {
MString v("null");
set(path, v);
}
JString & JString::operator=(coreutils:: ZString value) { JString & JString::operator=(coreutils:: ZString value) {
setSize(value.getLength()); setSize(value.getLength());
memcpy(data, value.getData(), value.getLength()); memcpy(data, value.getData(), value.getLength());
@@ -153,17 +521,17 @@ namespace coreutils {
void JString::removeWhitespace() { void JString::removeWhitespace() {
reset(); reset();
while(!eod()) { while (!eod()) {
if(*cursor == ' ') if (*cursor == ' ')
remove(1); remove(1);
else if(*cursor == '\r') else if (*cursor == '\r')
remove(1); remove(1);
else if(*cursor == '\t') else if (*cursor == '\t')
remove(1); remove(1);
else if(*cursor == '\n') else if (*cursor == '\n')
remove(1); remove(1);
else if(startsWith("\"")) else if (startsWith("\""))
getContainer(); skipString();
else else
++cursor; ++cursor;
} }
@@ -175,42 +543,59 @@ namespace coreutils {
reset(); reset();
char ch; char ch;
int index = 0; int index = 0;
while(!eod()) { while (!eod()) {
ch = nextChar(); ch = nextChar();
if(ch == '{') { if (ch == '{') {
index += 3; index += 3;
output << ch << '\n'; output << ch << '\n';
for(int ix = 0; ix < index; ++ix) for (int ix = 0; ix < index; ++ix)
output << ' '; output << ' ';
} }
else if(ch == '[') { else if (ch == '[') {
index += 3; index += 3;
output << ch << '\n'; output << ch << '\n';
for(int ix = 0; ix < index; ++ix) for (int ix = 0; ix < index; ++ix)
output << ' '; output << ' ';
} }
else if(ch == ':') { else if (ch == ':') {
output << ": "; output << ": ";
} }
else if(ch == '}') { else if (ch == '}') {
index -= 3; index -= 3;
output << '\n'; output << '\n';
for(int ix = 0; ix < index; ++ix) for (int ix = 0; ix < index; ++ix)
output << ' '; output << ' ';
output << ch; output << ch;
} }
else if(ch == ']') { else if (ch == ']') {
index -= 3; index -= 3;
output << '\n'; output << '\n';
for(int ix = 0; ix < index; ++ix) for (int ix = 0; ix < index; ++ix)
output << ' '; output << ' ';
output << ch; output << ch;
} }
else if(ch == ',') { else if (ch == ',') {
output << ch << '\n'; output << ch << '\n';
for(int ix = 0; ix < index; ++ix) for (int ix = 0; ix < index; ++ix)
output << ' '; output << ' ';
} }
else if (ch == '"') {
output << ch;
// Stream the rest of the string literal through untouched, including
// escapes, without the pretty-printer's structural-character logic
// firing on a { } [ ] , or : that happens to appear inside it.
while (!eod()) {
char sch = nextChar();
output << sch;
if (sch == '\\') {
if (!eod())
output << nextChar();
continue;
}
if (sch == '"')
break;
}
}
else else
output << ch; output << ch;
} }
+111 -7
View File
@@ -14,7 +14,7 @@ namespace coreutils {
/// access the objects elements using a named path as the index to the items. /// access the objects elements using a named path as the index to the items.
/// ///
/// JString default constructor will create an empty JString object pointing /// JString default constructor will create an empty JString object pointing
/// to an empty string. /// to an empty object ("{}").
/// ///
/// Use a constructor or assignment operator to initialize a JString to an /// Use a constructor or assignment operator to initialize a JString to an
/// initial JSON string. /// initial JSON string.
@@ -23,10 +23,30 @@ namespace coreutils {
/// object to initialize the JSON string. On destruction the file is saved back. /// object to initialize the JSON string. On destruction the file is saved back.
/// ///
/// The [] operator can be used to access elements within the JSON string. /// The [] operator can be used to access elements within the JSON string.
/// Use: /// Assigning a value dispatches on the C++ type of the right-hand side, so the
/// jstring["key"] = "value"; /// resulting JSON is correctly typed rather than always being a quoted string:
/// std::cout << jstring["key"];
/// ///
/// jstring["name"] = "value"; // JSON string, escaped correctly
/// jstring["count"] = 42; // JSON number (integer)
/// jstring["price"] = 19.95; // JSON number
/// jstring["active"] = true; // JSON boolean
/// jstring["notes"].setNull(); // JSON null
///
/// std::cout << jstring["name"];
///
/// Typed read access is available directly on the path (no need to go through
/// operator[] first):
///
/// std::string name = jstring.getString("name");
/// double price = jstring.getNumber("price");
/// bool active = jstring.getBool("active");
/// bool missing = !jstring.exists("notfound");
/// JSONType t = jstring.getType("count"); // JSONType::NUMBER
///
enum class JSONType {
STRING, NUMBER, BOOLEAN, JNULL, ARRAY, OBJECT, NOT_FOUND
};
class JString : virtual public MString { class JString : virtual public MString {
@@ -39,7 +59,9 @@ namespace coreutils {
/// ///
/// Use the find method to look through an object for member specified by /// Use the find method to look through an object for member specified by
/// the key in parameter 1. The cursor must be pointing to the { character /// the key in parameter 1. The cursor must be pointing to the { character
/// that begins the object's list. /// that begins the object's list. Returns the raw (still-escaped, for
/// strings) span of the value - use getString()/getNumber()/getBool()/
/// isNull() for decoded, typed access instead.
/// ///
ZString find(ZString path); ZString find(ZString path);
@@ -48,6 +70,34 @@ namespace coreutils {
bool notfirst; bool notfirst;
ZString path; ZString path;
///
/// Type-aware access. All of these call locate() internally, so path uses
/// the same dotted/bracketed syntax as find()/set() (e.g. "a.b[2].c").
///
JSONType getType(ZString path);
bool exists(ZString path);
bool isNull(ZString path);
std::string getString(ZString path); ///< Escape-decoded string content.
double getNumber(ZString path);
long getInt(ZString path);
bool getBool(ZString path);
void setString(ZString path, std::string value); ///< Escapes value automatically.
void setNumber(ZString path, double value);
void setInt(ZString path, long value);
void setBool(ZString path, bool value);
void setNull(ZString path);
///
/// JSON string escape/unescape helpers (exposed as static utilities since
/// they operate on plain text, not on this object's own JSON document).
///
static std::string jsonEscape(ZString value);
static std::string jsonUnescape(ZString raw);
class Proxy { class Proxy {
public: public:
@@ -64,15 +114,53 @@ namespace coreutils {
} }
operator std::string () { operator std::string () {
return jstring.find(key).str(); return jstring.getString(key);
}
operator double () {
return jstring.getNumber(key);
}
operator bool () {
return jstring.getBool(key);
} }
Proxy& operator=(coreutils::MString value) { Proxy& operator=(coreutils::MString value) {
jstring.set(key, value); jstring.setString(key, value.str());
jstring.changed(key, value); jstring.changed(key, value);
return *this; return *this;
} }
Proxy& operator=(const char *value) {
jstring.setString(key, std::string(value));
jstring.changed(key, ZString(value));
return *this;
}
Proxy& operator=(double value) {
jstring.setNumber(key, value);
return *this;
}
Proxy& operator=(int value) {
jstring.setInt(key, value);
return *this;
}
Proxy& operator=(long value) {
jstring.setInt(key, value);
return *this;
}
Proxy& operator=(bool value) {
jstring.setBool(key, value);
return *this;
}
void setNull() {
jstring.setNull(key);
}
private: private:
JString &jstring; JString &jstring;
ZString key; ZString key;
@@ -91,6 +179,22 @@ namespace coreutils {
private: private:
void removeWhitespace(); void removeWhitespace();
///
/// Structural helpers used internally by locate()/set() to correctly skip
/// past a value of any JSON type without being confused by JSON-structural
/// characters (quotes, braces, brackets) appearing inside string literals.
///
ZString findRelative(ZString path);
JSONType typeAtCursor();
void skipValue();
void skipString();
void skipContainer();
void skipNumber();
ZString captureString();
ZString captureContainer();
bool isLiteralValue(ZString value);
}; };
} }