|
11 | 11 | /// transport path). |
12 | 12 |
|
13 | 13 | #include "json.hpp" |
| 14 | +#include "json_sax.hpp" |
14 | 15 | #include "json_sax_streaming.hpp" |
15 | 16 | #include "lexer/sax_adapter.hpp" |
16 | 17 | #include "link/cobs.hpp" |
@@ -105,10 +106,27 @@ class StreamingJsonbBuilder : public JsonBuilder { |
105 | 106 | return *this; |
106 | 107 | } |
107 | 108 |
|
108 | | - JsonBuilder& add_raw(string_view, string_view) override { |
109 | | - // No-op — raw JSON fragments cannot be embedded in JSONB. |
110 | | - // BodyValue's raw-string constructor is disabled when NOTE_JSONB=1 |
111 | | - // so this path is unreachable in normal use. |
| 109 | + JsonBuilder& add_raw(string_view key, string_view json_fragment) override { |
| 110 | +#if !NOTE_MINIMAL |
| 111 | + // Re-encode the raw JSON fragment as JSONB opcodes by SAX-parsing |
| 112 | + // it through ReplaySink (defined below). The fragment must be a |
| 113 | + // JSON object — sax_parse rejects other shapes — so a body like |
| 114 | + // `R"({"temp":22.5})"` lands as `kItem(key) + kBeginObject + ... |
| 115 | + // + kEndObject` in the wire stream. |
| 116 | + // |
| 117 | + // Gated off under NOTE_MINIMAL so the AVR/JSONB build doesn't |
| 118 | + // pull the full-text SAX parser (sax_parse, parse_double, |
| 119 | + // snprintf-%g) into the binary — that combination is ~6 KB of |
| 120 | + // flash that the 32 KB Uno target can't absorb. The BodyValue |
| 121 | + // raw-string constructor is correspondingly disabled in body.hpp |
| 122 | + // under the same combination, so callers can't reach this code |
| 123 | + // path with a no-op underneath. |
| 124 | + if (json_fragment.empty()) return *this; |
| 125 | + ReplaySink sink(*this, key); |
| 126 | + (void)sax_parse(json_fragment, sink); |
| 127 | +#else |
| 128 | + (void)key; (void)json_fragment; |
| 129 | +#endif |
112 | 130 | return *this; |
113 | 131 | } |
114 | 132 |
|
@@ -182,6 +200,89 @@ class StreamingJsonbBuilder : public JsonBuilder { |
182 | 200 | JsonWriter& writer_; |
183 | 201 | bool closed_ = false; |
184 | 202 |
|
| 203 | + // SAX → JSONB-opcode replay. Used by add_raw to translate a raw JSON |
| 204 | + // fragment (e.g. a body literal) into JSONB opcodes one event at a |
| 205 | + // time. Nested so it can reach the private emit_* helpers directly. |
| 206 | + // |
| 207 | + // Context tracking: a single uint32_t stack records "are we in an |
| 208 | + // array?" per nesting level (1 bit per level, LSB = current). 32 |
| 209 | + // levels is far beyond the depth of any Notecard body in practice. |
| 210 | + struct ReplaySink : JsonSink { |
| 211 | + StreamingJsonbBuilder& b; |
| 212 | + string_view outer_key; |
| 213 | + uint32_t array_stack = 0; |
| 214 | + int depth = 0; |
| 215 | + |
| 216 | + ReplaySink(StreamingJsonbBuilder& bb, string_view k) : b(bb), outer_key(k) {} |
| 217 | + |
| 218 | + void push(bool is_arr) { array_stack = (array_stack << 1) | (is_arr ? 1u : 0u); } |
| 219 | + void pop() { array_stack >>= 1; } |
| 220 | + bool in_array() const { return (array_stack & 1u) != 0; } |
| 221 | + |
| 222 | + // Emit kItem(k) when the current value is a field of an object. |
| 223 | + // Skipped for array elements (no kItem) and for the root (the |
| 224 | + // outer_key was emitted by the caller via on_object_begin / |
| 225 | + // on_array_begin at depth 0). |
| 226 | + void emit_key(string_view k) { |
| 227 | + if (depth > 0 && !in_array()) b.emit_item(k); |
| 228 | + } |
| 229 | + |
| 230 | + void on_object_begin(string_view k) override { |
| 231 | + if (depth == 0) b.emit_item(outer_key); |
| 232 | + else if (!in_array()) b.emit_item(k); |
| 233 | + b.emit(jsonb::kBeginObject); |
| 234 | + push(false); |
| 235 | + depth++; |
| 236 | + } |
| 237 | + void on_object_end(string_view) override { |
| 238 | + b.emit(jsonb::kEndObject); |
| 239 | + depth--; |
| 240 | + pop(); |
| 241 | + } |
| 242 | + void on_array_begin(string_view k) override { |
| 243 | + if (depth == 0) b.emit_item(outer_key); |
| 244 | + else if (!in_array()) b.emit_item(k); |
| 245 | + b.emit(jsonb::kBeginArray); |
| 246 | + push(true); |
| 247 | + depth++; |
| 248 | + } |
| 249 | + void on_array_end(string_view) override { |
| 250 | + b.emit(jsonb::kEndArray); |
| 251 | + depth--; |
| 252 | + pop(); |
| 253 | + } |
| 254 | + void on_null(string_view k) override { |
| 255 | + emit_key(k); |
| 256 | + b.emit(jsonb::kNull); |
| 257 | + } |
| 258 | + void on_bool(string_view k, bool v) override { |
| 259 | + emit_key(k); |
| 260 | + b.emit(v ? jsonb::kTrue : jsonb::kFalse); |
| 261 | + } |
| 262 | + void on_string(string_view k, string_view v) override { |
| 263 | + emit_key(k); |
| 264 | + b.emit(jsonb::kString); |
| 265 | + b.writer_.write(v.data(), v.size()); |
| 266 | + b.emit('\0'); |
| 267 | + } |
| 268 | + void on_number(string_view k, string_view raw) override { |
| 269 | + emit_key(k); |
| 270 | + bool is_float = false; |
| 271 | + for (char c : raw) { |
| 272 | + if (c == '.' || c == 'e' || c == 'E') { is_float = true; break; } |
| 273 | + } |
| 274 | + if (is_float) { |
| 275 | + double d = note::parse_double(raw); |
| 276 | + b.emit(jsonb::kDouble); |
| 277 | + b.emit_bytes(&d, 8); |
| 278 | + } else { |
| 279 | + json_int_t i = note::parse_int(raw); |
| 280 | + b.emit(jsonb::kInt32); |
| 281 | + b.emit_le32(static_cast<int32_t>(i)); |
| 282 | + } |
| 283 | + } |
| 284 | + }; |
| 285 | + |
185 | 286 | void emit(uint8_t opcode) { |
186 | 287 | writer_.write(reinterpret_cast<const char*>(&opcode), 1); |
187 | 288 | } |
|
0 commit comments