Skip to content

Commit 92bb655

Browse files
committed
feat(streaming): Make streaming primary, add body_or_error, rename NOTE_NO_BUFFERED
Resolves ISSUE-env-get-streaming-and-overflow-error.md by making the streaming response path the recommended default and turning the silent failure modes around tree-mode access into actionable, discoverable errors. env.get docs + overflow message (the original issue) - Overlay adds prose to env.get's Doxygen comment calling out that the response is unbounded when no `name` is given, and recommending the streaming .into(JsonSink&) overload (with set_response_buffer() as the fallback when a bounded staging buffer is acceptable). - spec_parser.py now combines OpenAPI `summary` + `description` so the overlay can extend prose without overwriting upstream summaries. - generate.py's markdown stripper now ignores `_` boundaries inside word characters, preserving snake_case identifiers in `code spans` that the old regex was destroying (`set_response_buffer` → `setresponsebuffer`, `_temp.qo` → `temp.qo`, `ALT_DFU` → `ALTDFU`, `ver_major` → `vermajor`, etc.). Cascades through several api/*.hpp. - The response-overflow error at protocol.hpp/transact.hpp now names both escape hatches: `set_response_buffer()` and `.into(JsonSink&)`. Zero AVR flash cost (NOTE_SHORT_ERRORS collapses the literal to "E"). New test in test_buffered_bridge.cpp pins the contract. Loud failure on body() in streaming mode - New `Response::body_or_error()` returns an explicit Result<const JsonReader*> with Error::NotReady + actionable message when the response was populated by the streaming SAX path. Old body() stays for back-compat (now [[nodiscard]]). - `Response::was_streaming_parse()` exposes the flag for callers that want to branch on mode at runtime. - streaming_parse_used_ flag is set by Sink::reset and lives next to the two unique_ptrs in the buffered section of Response. - New test in test_body.cpp covers both modes. NOTE_NO_BUFFERED → NOTE_NO_JSON_TREE - Canonical macro renamed to match the user-facing vocabulary (streaming vs tree, not streaming vs buffered). - NOTE_NO_BUFFERED is honoured as a deprecated alias via a back-compat shim in note_config.hpp — existing user build_flags keep working. - Compile-check tests use the new name; one new compile-check (note_no_buffered_alias.cpp) pins the back-compat shim. Streaming as the primary path - New `Notecard(ITransact&, Allocator = {})` convenience ctor eliminates the nullptr-backend boilerplate when wiring streaming with a non-Protocol transport. - docs/getting-started.md leads with the streaming pattern; tree mode is shown as the opt-in when body() or the lambda request builder is needed. - docs/streaming-and-tree.md documents body_or_error(), the per- Response RAM savings of setting NOTE_NO_JSON_TREE=1 (24 bytes on 64-bit hosts, ~12 bytes on 32-bit embedded), and the rationale for not pursuing per-instance compile-time gating (covered by the macro flag + the runtime check). - docs/feature-flags.md + docs/environment-variables.md updated.
1 parent 67b980c commit 92bb655

77 files changed

Lines changed: 937 additions & 362 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/environment-variables.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ if (rsp) {
178178
}
179179
```
180180

181-
On AVR-class builds (`NOTE_MINIMAL` sets `NOTE_NO_BUFFERED=1`), tree mode is compiled out — the `transact` + `scan` pattern above is the only option for dynamic keys.
181+
On AVR-class builds (`NOTE_MINIMAL` sets `NOTE_NO_JSON_TREE=1`), tree mode is compiled out — the `transact` + `scan` pattern above is the only option for dynamic keys.
182182

183183
## C++ level
184184

docs/feature-flags.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ build_flags = -DNOTE_MINIMAL -UNDEF_NOTE_NO_CRC
4444
| Flag | Default | Minimal | Effect | Savings |
4545
|------|---------|---------|--------|---------|
4646
| `NOTE_JSONB` | `0` | **(M)** `1` | Use [JSONB binary wire format](jsonb.md) instead of JSON text. Requests/responses encoded as COBS-framed binary opcodes. CRC is bypassed (COBS provides framing but not integrity — CRC handles that separately). Raw JSON string bodies are a compile error — use lambdas or typed structs. Requires Notecard firmware 11.x+. | ~1.9 KB flash (replaces JSON builder/lexer with the smaller JSONB builder/parser) |
47-
| `NOTE_NO_BUFFERED` | off | **(M)** on | Disable `JsonBackend`/`JsonReader` tree parse path. Only streaming SAX parse available. (Macro name retained for backwards compatibility.) | ~2-4 KB flash, ~300 B RAM |
47+
| `NOTE_NO_JSON_TREE` | off | **(M)** on | Disable `JsonBackend`/`JsonReader` tree-mode parse path (`.body()`, `.body_or_error()`, `parse(reader)`). Only streaming SAX parse available. The legacy spelling `NOTE_NO_BUFFERED` is honoured as a deprecated alias. | ~2-4 KB flash, ~300 B RAM |
4848
| `NOTE_NO_CRC` | off | **(M)** on | Disable CRC32 on request/response framing. (CRC not supported by JSONB on Notecard)| ~200 B flash, 64 B .data (LUT) |
4949
| `NOTE_NO_MD5` | off | **(M)** on | Disable MD5 for binary transfer verification. | ~512 B .data (tables) |
5050
| `NOTE_NO_STD_STRING` | off | **(M)** on | Disable `std::string`-dependent features (debug wire tracing, some transport methods). Required for AVR (no `<string>` header). | Enables AVR builds |

docs/getting-started.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,24 +48,24 @@ A minimal `main.cpp` — replace `MySerialHal` with whatever talks to your hardw
4848
```cpp
4949
#include <note/notecard.hpp>
5050
#include <note/api.hpp>
51-
#include <note/backends/cjson.hpp>
5251
#include <note/link/serial.hpp>
5352
#include <note/protocol.hpp>
5453

5554
int main() {
5655
MySerialHal hal; // your serial HAL impl
5756
note::link::SerialFramer serial_hal(hal); // note::Hal — wire framing
5857
note::Protocol transport(serial_hal); // ITransact — wire protocol
59-
note::backends::CjsonBackend backend; // tree-mode JSON backend
6058

61-
note::Notecard nc(backend, transport);
59+
note::Notecard nc(transport); // streaming — no JSON backend needed
6260
note::Api api(nc);
6361

6462
auto r = api.card.version().execute();
6563
if (r) std::printf("Notecard %.*s\n", (int)r.version.size(), r.version.data());
6664
}
6765
```
6866

67+
Streaming is the recommended default — typed response fields, struct body parsing via `.into(struct)`, and the rest of the API surface work without a JSON tree-mode backend linked, and the wire response can be arbitrarily large. Switch to tree mode (`Notecard nc(backend, transport);` with a `note::backends::CjsonBackend backend;`) only when you need `response.body()` for ad-hoc field walks or the `nc.request("endpoint", [&](auto& b){ … })` lambda builder — see [`docs/streaming-and-tree.md`](streaming-and-tree.md) for the full tradeoff.
68+
6969
Build: `cmake -S . -B build && cmake --build build`. The runnable companion to this section is [`examples/stdcpp/getting-started.cpp`](../examples/stdcpp/getting-started.cpp), which uses a mock transport so you can experiment without hardware. See [`examples/stdcpp/README.md`](../examples/stdcpp/README.md) for the full example index.
7070

7171
### ESP-IDF
@@ -76,7 +76,7 @@ The HAL implementation for ESP-IDF UART/I2C drivers is yours to wire (the same `
7676

7777
## Your first request
7878

79-
> Throughout the rest of this page, `nc` is the API surface. On Arduino, that's the `Notecard nc;` you declared. On stdcpp/ESP-IDF, after `Notecard nc(backend, transport); Api api(nc);` you write `api.card.version()` instead — the calls are identical, the receiver isn't. See [using-the-api.md](using-the-api.md) for the full picture.
79+
> Throughout the rest of this page, `nc` is the API surface. On Arduino, that's the `Notecard nc;` you declared. On stdcpp/ESP-IDF, after `Notecard nc(transport); Api api(nc);` you write `api.card.version()` instead — the calls are identical, the receiver isn't. See [using-the-api.md](using-the-api.md) for the full picture.
8080
8181
Walking through one full request — `card.version`, the simplest readable Notecard endpoint — top to bottom:
8282

docs/streaming-and-tree.md

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,21 @@ if (r && r.body()) {
4242
}
4343
```
4444

45-
In streaming mode the response bytes are gone once the SAX parser has run, so `r.body()` returns null. The same use case is served by committing the body shape ahead of time, either with a struct via `.into(struct)` or with a SAX `JsonSink` for fully ad-hoc parsing:
45+
In streaming mode the response bytes are gone once the SAX parser has run, so `r.body()` returns null. Because a null return is also the legitimate "response carried no body field" result in tree mode, the two cases look identical to the caller — for mixed builds where the same code path may run in either mode, prefer `r.body_or_error()`, which returns an explicit `Error::NotReady` when the Notecard was in streaming mode:
46+
47+
```cpp
48+
auto r = nc.note.get("data.qi").execute();
49+
if (r) {
50+
auto safe = r.body_or_error();
51+
if (safe.has_value()) {
52+
// *safe is a const JsonReader*, may still be null if no body field
53+
} else {
54+
// safe.error().code == Error::NotReady — running in streaming mode
55+
}
56+
}
57+
```
58+
59+
The same use case is served by committing the body shape ahead of time, either with a struct via `.into(struct)` or with a SAX `JsonSink` for fully ad-hoc parsing:
4660

4761
```cpp
4862
struct Readings {
@@ -112,11 +126,24 @@ This table summarizes the surfaces touched by the mode choice. Every other surfa
112126
| Binary transfers (COBS) | yes | yes |
113127
| Error handling (`ApiResult`) | yes | yes |
114128
| Lambda request builder (`nc.request(...)`) | yes ||
115-
| `response.body() -> JsonReader*` | yes ||
129+
| `response.body() -> JsonReader*` | yes | — (null) |
130+
| `response.body_or_error()` | success | `Err::NotReady` |
116131
| Requires `JsonBackend` | yes | no |
117132
| Zero-heap capable | depends on backend | yes |
118133

119-
Defining `NOTE_NO_BUFFERED` removes tree mode entirely (a saving of roughly 2 to 4 KB of flash). `NOTE_MINIMAL` sets this automatically.
134+
Defining `NOTE_NO_JSON_TREE` removes tree mode entirely (a saving of roughly 2 to 4 KB of flash). `NOTE_MINIMAL` sets this automatically. The legacy name `NOTE_NO_BUFFERED` is honoured as an alias for backward compatibility; new code should use `NOTE_NO_JSON_TREE`.
135+
136+
`NOTE_NO_JSON_TREE` also drops two `std::unique_ptr<JsonReader>` fields and a discriminator bool from every `Response` struct — measured at **24 bytes per response** on 64-bit hosts (and ~12 bytes on 32-bit embedded targets) even for code that never calls `body()`. The header-only `body()` / `body_or_error()` / `parse(reader)` methods themselves are DCE'd by the linker when unreferenced, so the flash cost of leaving the flag off is near zero — the per-response RAM is the real penalty. Streaming-only projects on memory-constrained MCUs should set `NOTE_NO_JSON_TREE=1` for the RAM win even if their code already happens to be streaming-clean.
137+
138+
## Why there is no per-instance compile-time gate
139+
140+
`response.body()` exists when the build allows tree mode. There is no separate `StreamingResponse` type that omits `body()` at compile time when *this particular Notecard instance* uses streaming. That sounds like a useful safety net, and it was considered, but it doesn't pay off:
141+
142+
- The shape needed — `Notecard<note::Streaming>` vs `Notecard<note::JsonTree>` with mode-aware `Response` traits — would require templating `Notecard`, `StaticNotecard`, and the `Api` wrapper on a mode tag, and threading that tag through every generated endpoint. Existing `Notecard nc;` syntax would need a migration shim.
143+
- It only catches one case: mixed builds where the user wrote streaming-only code but left `NOTE_NO_JSON_TREE` off. That case is already caught at runtime by `body_or_error()` — explicit `Error::NotReady` with an actionable message — and `was_streaming_parse()` is available for introspection. Whole-program elimination is covered by the macro flag.
144+
- For users who only ever use streaming, setting `NOTE_NO_JSON_TREE=1` removes `body()` from the compiled API surface entirely. That's the same compile-time safety the template approach would deliver, without the architectural surface.
145+
146+
So the safety story is: **(1)** macro flag for whole-program compile-time removal; **(2)** `body_or_error()` + `was_streaming_parse()` for runtime detection in mixed builds; **(3)** `[[nodiscard]]` on `body()` so the compiler nags about unused returns. Per-instance compile-time gating isn't planned.
120147

121148
## See also
122149

notecard-api.openapi.overlay.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@
122122
}
123123
}
124124
}
125+
},
126+
"/env/get": {
127+
"get": {
128+
"description": "When called without a `name` (or `names`), `env.get` returns the full environment-variable set known to the Notecard. The response size scales with the Notehub project and may exceed the default response buffer, surfacing as `Error::Overflow`. For this shape, prefer the streaming `.into(JsonSink&)` overload — body events stream off the wire and the env set can be arbitrarily large. As an alternative, `Notecard::set_response_buffer()` can enlarge the staging buffer if a bounded response is acceptable."
129+
}
125130
}
126131
}
127132
}

src/note/api.hpp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2340,6 +2340,15 @@ class Api {
23402340
23412341
/// Returns a single environment variable, or all variables according to
23422342
/// precedence rules.
2343+
///
2344+
/// When called without a `name` (or `names`), `env.get` returns the
2345+
/// full environment-variable set known to the Notecard. The response
2346+
/// size scales with the Notehub project and may exceed the default
2347+
/// response buffer, surfacing as `Error::Overflow`. For this shape,
2348+
/// prefer the streaming `.into(JsonSink&)` overload — body events
2349+
/// stream off the wire and the env set can be arbitrarily large. As an
2350+
/// alternative, `Notecard::set_response_buffer()` can enlarge the
2351+
/// staging buffer if a bounded response is acceptable.
23432352
auto get() { return create_<api::EnvGet>(); }
23442353
23452354
/// Get the time of the update to any environment variable managed by
@@ -2665,20 +2674,20 @@ class Api {
26652674
26662675
#if __cplusplus >= 202002L
26672676
/// Add a "device health" log message to send to Notehub on the next
2668-
/// sync via the healthhost.qo Notefile.
2677+
/// sync via the _health_host.qo Notefile.
26692678
template<typename T_ = TargetT_>
26702679
requires (target_supports<T_, api::HubLog>())
26712680
auto log() { return create_<api::HubLog>(); }
26722681
26732682
/// Add a "device health" log message to send to Notehub on the next
2674-
/// sync via the healthhost.qo Notefile.
2683+
/// sync via the _health_host.qo Notefile.
26752684
template<typename T_ = TargetT_>
26762685
requires (!target_supports<T_, api::HubLog>() && !T_::strict)
26772686
[[deprecated("hub.log is not available on this target")]]
26782687
auto log() { return create_<api::HubLog>(); }
26792688
#else
26802689
/// Add a "device health" log message to send to Notehub on the next
2681-
/// sync via the healthhost.qo Notefile.
2690+
/// sync via the _health_host.qo Notefile.
26822691
auto log() { return create_<api::HubLog>(); }
26832692
#endif
26842693

src/note/api/card_attn.hpp

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ struct CardAttn {
390390

391391
#pragma GCC diagnostic push
392392
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
393-
#if !NOTE_NO_BUFFERED
393+
#if !NOTE_NO_JSON_TREE
394394
static Response parse(std::unique_ptr<JsonReader> reader_) {
395395
Response rsp;
396396
{ note::string_view arr_[8]; auto n_ = reader_->get_string_array("files", arr_, 8); for (size_t i_ = 0; i_ < n_; ++i_) rsp.files.add(arr_[i_]); }
@@ -422,7 +422,7 @@ struct CardAttn {
422422
return rsp;
423423
}
424424
#pragma GCC diagnostic pop
425-
#endif // !NOTE_NO_BUFFERED
425+
#endif // !NOTE_NO_JSON_TREE
426426

427427
// SAX sink — zero-allocation streaming parse into Response fields.
428428
// String fields are interned into the StringPool immediately, so
@@ -512,7 +512,7 @@ struct CardAttn {
512512
}
513513
#endif
514514

515-
#if !NOTE_NO_BUFFERED
515+
#if !NOTE_NO_JSON_TREE
516516
private:
517517
std::unique_ptr<JsonReader> reader_;
518518
#endif
@@ -831,7 +831,7 @@ struct CardAttn {
831831
/// field will not be present when the attention pin is `LOW`.
832832
note::ResponseField<bool> set{};
833833

834-
#if !NOTE_NO_BUFFERED
834+
#if !NOTE_NO_JSON_TREE
835835
static Response parse(std::unique_ptr<JsonReader> reader_) {
836836
Response rsp;
837837
if (reader_->has("set")) rsp.set = reader_->get_bool("set");
@@ -847,7 +847,7 @@ struct CardAttn {
847847
if (reader_.has("set")) rsp.set = reader_.get_bool("set");
848848
return rsp;
849849
}
850-
#endif // !NOTE_NO_BUFFERED
850+
#endif // !NOTE_NO_JSON_TREE
851851

852852
// SAX sink — zero-allocation streaming parse into Response fields.
853853
// String fields are interned into the StringPool immediately, so
@@ -880,7 +880,7 @@ struct CardAttn {
880880
}
881881
#endif
882882

883-
#if !NOTE_NO_BUFFERED
883+
#if !NOTE_NO_JSON_TREE
884884
private:
885885
std::unique_ptr<JsonReader> reader_;
886886
#endif
@@ -1183,7 +1183,7 @@ struct CardAttn {
11831183
/// field will not be present when the attention pin is `LOW`.
11841184
note::ResponseField<bool> set{};
11851185

1186-
#if !NOTE_NO_BUFFERED
1186+
#if !NOTE_NO_JSON_TREE
11871187
static Response parse(std::unique_ptr<JsonReader> reader_) {
11881188
Response rsp;
11891189
if (reader_->has("set")) rsp.set = reader_->get_bool("set");
@@ -1199,7 +1199,7 @@ struct CardAttn {
11991199
if (reader_.has("set")) rsp.set = reader_.get_bool("set");
12001200
return rsp;
12011201
}
1202-
#endif // !NOTE_NO_BUFFERED
1202+
#endif // !NOTE_NO_JSON_TREE
12031203

12041204
// SAX sink — zero-allocation streaming parse into Response fields.
12051205
// String fields are interned into the StringPool immediately, so
@@ -1232,7 +1232,7 @@ struct CardAttn {
12321232
}
12331233
#endif
12341234

1235-
#if !NOTE_NO_BUFFERED
1235+
#if !NOTE_NO_JSON_TREE
12361236
private:
12371237
std::unique_ptr<JsonReader> reader_;
12381238
#endif
@@ -1661,7 +1661,7 @@ struct CardAttn {
16611661
/// time) that the payload was stored by the Notecard.
16621662
note::ResponseField<note::json_int_t> time{};
16631663

1664-
#if !NOTE_NO_BUFFERED
1664+
#if !NOTE_NO_JSON_TREE
16651665
static Response parse(std::unique_ptr<JsonReader> reader_) {
16661666
Response rsp;
16671667
if (reader_->has("payload")) rsp.payload = reader_->get_string("payload");
@@ -1679,7 +1679,7 @@ struct CardAttn {
16791679
if (reader_.has("time")) rsp.time = reader_.get_int("time");
16801680
return rsp;
16811681
}
1682-
#endif // !NOTE_NO_BUFFERED
1682+
#endif // !NOTE_NO_JSON_TREE
16831683

16841684
// SAX sink — zero-allocation streaming parse into Response fields.
16851685
// String fields are interned into the StringPool immediately, so
@@ -1725,7 +1725,7 @@ struct CardAttn {
17251725
}
17261726
#endif
17271727

1728-
#if !NOTE_NO_BUFFERED
1728+
#if !NOTE_NO_JSON_TREE
17291729
private:
17301730
std::unique_ptr<JsonReader> reader_;
17311731
#endif
@@ -2211,7 +2211,7 @@ struct CardAttn {
22112211

22122212
#pragma GCC diagnostic push
22132213
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
2214-
#if !NOTE_NO_BUFFERED
2214+
#if !NOTE_NO_JSON_TREE
22152215
static Response parse(std::unique_ptr<JsonReader> reader_) {
22162216
Response rsp;
22172217
{ note::string_view arr_[8]; auto n_ = reader_->get_string_array("files", arr_, 8); for (size_t i_ = 0; i_ < n_; ++i_) rsp.files.add(arr_[i_]); }
@@ -2239,7 +2239,7 @@ struct CardAttn {
22392239
return rsp;
22402240
}
22412241
#pragma GCC diagnostic pop
2242-
#endif // !NOTE_NO_BUFFERED
2242+
#endif // !NOTE_NO_JSON_TREE
22432243

22442244
// SAX sink — zero-allocation streaming parse into Response fields.
22452245
// String fields are interned into the StringPool immediately, so
@@ -2314,7 +2314,7 @@ struct CardAttn {
23142314
}
23152315
#endif
23162316

2317-
#if !NOTE_NO_BUFFERED
2317+
#if !NOTE_NO_JSON_TREE
23182318
private:
23192319
std::unique_ptr<JsonReader> reader_;
23202320
#endif

src/note/api/card_aux.hpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ struct CardAux {
493493

494494
#pragma GCC diagnostic push
495495
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
496-
#if !NOTE_NO_BUFFERED
496+
#if !NOTE_NO_JSON_TREE
497497
static Response parse(std::unique_ptr<JsonReader> reader_) {
498498
Response rsp;
499499
if (reader_->has("mode")) rsp.mode = reader_->get_string("mode");
@@ -523,7 +523,7 @@ struct CardAux {
523523
return rsp;
524524
}
525525
#pragma GCC diagnostic pop
526-
#endif // !NOTE_NO_BUFFERED
526+
#endif // !NOTE_NO_JSON_TREE
527527

528528
// SAX sink — zero-allocation streaming parse into Response fields.
529529
// String fields are interned into the StringPool immediately, so
@@ -592,7 +592,7 @@ struct CardAux {
592592
}
593593
#endif
594594

595-
#if !NOTE_NO_BUFFERED
595+
#if !NOTE_NO_JSON_TREE
596596
private:
597597
std::unique_ptr<JsonReader> reader_;
598598
#endif

0 commit comments

Comments
 (0)