150 lines
5.8 KiB
C++
150 lines
5.8 KiB
C++
// core/json — the ONE hand-rolled JSON lexical layer (Q-W1; audit T2-02 / §2
|
||
// "Parser ×4"). Pure: standard library only — NO REAPER, NO SWELL, NO VST3.
|
||
//
|
||
// This module owns the lexical half of the house JSON dialect: the escape-aware
|
||
// string literal (incl. \uXXXX + surrogate pairs re-encoded as UTF-8), the bare
|
||
// scalar tokens, the number parses (strtod/strtoll with full-token + ERANGE
|
||
// rejection), key+':' consumption, unknown-value skipping, and the emit side
|
||
// (escaping, %.17g / %d / %lld number rendering, the scoped object writer).
|
||
// The DOMAIN grammars — which keys exist, what shape each value takes, what is
|
||
// rejected at the model boundary — stay in the consumers (bank_model, bank_book,
|
||
// view_mode_model, owned_manifest, tail_control). One lexical definition means
|
||
// the five decoders can no longer drift on tolerance or escaping.
|
||
//
|
||
// Byte-compatibility contract (load-bearing): the emit helpers reproduce the
|
||
// prior per-module writers EXACTLY — writeEscaped's escape set, %.17g for
|
||
// doubles (shortest form that round-trips every IEEE-754 double bit-for-bit),
|
||
// plain decimal for ints — so a re-serialized blob is byte-identical to what
|
||
// the pre-extraction writers produced. This was a structural dedupe, not a
|
||
// format change; persisted .rpp ext-state must not shift by a byte.
|
||
|
||
#pragma once
|
||
|
||
#include <cstdint>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
namespace reasampler::json {
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// emit helpers (writer side)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// Appends `s` as a quoted JSON string literal: the seven short escapes, \uXXXX
|
||
// for remaining control chars, everything else verbatim (UTF-8 passes through).
|
||
void writeEscaped(std::string& out, const std::string& s);
|
||
|
||
// Number rendering. %.17g is the shortest form that round-trips every IEEE-754
|
||
// double exactly, so deserialize(serialize(x)) == x holds bit-for-bit.
|
||
std::string numToStr(double v);
|
||
std::string numToStr(std::int64_t v);
|
||
std::string numToStr(int v);
|
||
|
||
// Flat homogeneous arrays: ["a","b"] / [1,2]. Empty vector -> "[]".
|
||
void writeStringArray(std::string& out, const std::vector<std::string>& v);
|
||
void writeIntArray(std::string& out, const std::vector<int>& v);
|
||
|
||
// Scoped object writer: appends '{' on construction and '}' on destruction, with
|
||
// comma separation handled internally. Nested values are written by keyBegin()
|
||
// followed by the caller emitting the value (e.g. a nested Writer scope or an
|
||
// array). NOTE the destructor-close means an enclosing scope must END (brace
|
||
// block) before the built string is returned — see the NRVO note in the
|
||
// consumers' serialize() implementations.
|
||
class Writer {
|
||
public:
|
||
explicit Writer(std::string& out) : out_(out) { out_ += '{'; }
|
||
~Writer() { out_ += '}'; }
|
||
|
||
Writer(const Writer&) = delete;
|
||
Writer& operator=(const Writer&) = delete;
|
||
|
||
// "key":<rawValue> — rawValue appended verbatim (numbers, bools, null,
|
||
// pre-serialized nested blobs).
|
||
void keyRaw(const char* key, const std::string& rawValue) {
|
||
sep();
|
||
writeEscaped(out_, key);
|
||
out_ += ':';
|
||
out_ += rawValue;
|
||
}
|
||
// "key":"value" — value escaped.
|
||
void keyStr(const char* key, const std::string& value) {
|
||
sep();
|
||
writeEscaped(out_, key);
|
||
out_ += ':';
|
||
writeEscaped(out_, value);
|
||
}
|
||
// "key": — caller writes the value immediately after.
|
||
void keyBegin(const char* key) {
|
||
sep();
|
||
writeEscaped(out_, key);
|
||
out_ += ':';
|
||
}
|
||
|
||
private:
|
||
void sep() {
|
||
if (first_) first_ = false; else out_ += ',';
|
||
}
|
||
std::string& out_;
|
||
bool first_ = true;
|
||
};
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Reader — the lexical cursor (parser side)
|
||
// ---------------------------------------------------------------------------
|
||
//
|
||
// Every method returns false on malformed input and never reads out of bounds.
|
||
// Only the subset the house writers emit is supported. The reader borrows the
|
||
// input string — it must outlive the Reader.
|
||
class Reader {
|
||
public:
|
||
explicit Reader(const std::string& s) : s_(s) {}
|
||
|
||
bool eof() const { return pos_ >= s_.size(); }
|
||
void skipWs();
|
||
|
||
// Consumes `c` (after whitespace). False without advancing past `c` if the
|
||
// next non-ws char differs.
|
||
bool consume(char c);
|
||
|
||
// JSON string literal (escapes + \uXXXX incl. surrogate pairs -> UTF-8).
|
||
bool parseString(std::string& out);
|
||
|
||
// Bare token (number / true / false / null) up to the next structural char.
|
||
bool parseRawScalar(std::string& out);
|
||
|
||
// Numbers: full-token parse; trailing bytes or ERANGE reject. parseInt
|
||
// additionally rejects values outside [INT_MIN, INT_MAX].
|
||
bool parseDouble(double& out);
|
||
bool parseInt64(std::int64_t& out);
|
||
bool parseInt(int& out);
|
||
|
||
bool parseBool(bool& out);
|
||
|
||
// Peeks for the `null` token; consumes it if present (wasNull=true),
|
||
// otherwise leaves the position untouched (wasNull=false). Returns false
|
||
// only on eof.
|
||
bool expectNullOr(bool& wasNull);
|
||
|
||
// An object member key + ':'.
|
||
bool parseKey(std::string& key);
|
||
|
||
// Homogeneous arrays. Appends to `out`; empty array is valid.
|
||
bool parseStringArray(std::vector<std::string>& out);
|
||
bool parseIntArray(std::vector<int>& out);
|
||
|
||
// Skips one value of any shape (string / object / array / bare scalar) —
|
||
// forward-compat for unknown keys.
|
||
bool skipValue();
|
||
|
||
// Captures the raw source text of one value verbatim (string-aware brace
|
||
// matching), so a nested blob can be handed to its own parser — the
|
||
// bank_book -> BankModel::deserialize seam.
|
||
bool captureValue(std::string& raw);
|
||
|
||
private:
|
||
const std::string& s_;
|
||
std::size_t pos_ = 0;
|
||
};
|
||
|
||
} // namespace reasampler::json
|