Files
CoreUtils/JString.h
T
Brad Arant 28feaff3d7 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).
2026-08-22 20:53:54 -07:00

203 lines
5.7 KiB
C++

#ifndef __JString_h__
#define __JString_h__
#include "MString.h"
#include <iostream>
#include <sstream>
namespace coreutils {
///
/// Use the JString object when you need a JSON interface to C++. JString uses
/// an MString to store a JSON string representing the object(s) contained.
/// The [] operator is overriden from the ZString and provides a method to
/// access the objects elements using a named path as the index to the items.
///
/// JString default constructor will create an empty JString object pointing
/// to an empty object ("{}").
///
/// Use a constructor or assignment operator to initialize a JString to an
/// initial JSON string.
///
/// Several constructors are available including a handle to a std::FILE
/// 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.
/// Assigning a value dispatches on the C++ type of the right-hand side, so the
/// resulting JSON is correctly typed rather than always being a quoted string:
///
/// 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 {
public:
JString();
void set(ZString &path, ZString &value);
///
/// 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
/// 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);
bool locate(ZString path);
bool notfirst;
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 {
public:
Proxy(JString &jstring, ZString key) : jstring(jstring), key(key) {}
friend std::ostream &operator<<(std::ostream &os, const Proxy &proxy);
operator coreutils::ZString () {
return jstring.find(key);
}
operator coreutils::MString () {
return jstring.find(key);
}
operator std::string () {
return jstring.getString(key);
}
operator double () {
return jstring.getNumber(key);
}
operator bool () {
return jstring.getBool(key);
}
Proxy& operator=(coreutils::MString value) {
jstring.setString(key, value.str());
jstring.changed(key, value);
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:
JString &jstring;
ZString key;
};
Proxy operator[](coreutils::ZString key) {
return Proxy(*this, key);
}
JString & operator=(coreutils:: ZString value);
MString pretty();
virtual void changed(ZString key, ZString value);
private:
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);
};
}
#endif