@reactiive/ennio 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/EnnioCore.podspec +61 -0
  2. package/LICENSE +21 -0
  3. package/README.md +50 -0
  4. package/android/CMakeLists.txt +40 -0
  5. package/android/build.gradle +64 -0
  6. package/cpp/ElementMatcher.cpp +661 -0
  7. package/cpp/ElementMatcher.hpp +244 -0
  8. package/cpp/EnnioLog.hpp +182 -0
  9. package/cpp/HybridEnnio.cpp +1161 -0
  10. package/cpp/HybridEnnio.hpp +174 -0
  11. package/cpp/IdleMonitor.hpp +277 -0
  12. package/cpp/Protocol.cpp +135 -0
  13. package/cpp/Protocol.hpp +47 -0
  14. package/cpp/SelectorCriteria.hpp +281 -0
  15. package/cpp/SelectorParser.cpp +649 -0
  16. package/cpp/SelectorParser.hpp +94 -0
  17. package/cpp/ShadowTreeTraverser.cpp +305 -0
  18. package/cpp/ShadowTreeTraverser.hpp +142 -0
  19. package/cpp/TestIDRegistry.cpp +109 -0
  20. package/cpp/TestIDRegistry.hpp +84 -0
  21. package/dist/cli.js +16221 -0
  22. package/ios/EnnioAutoInit.mm +338 -0
  23. package/ios/EnnioDebugBanner.h +19 -0
  24. package/ios/EnnioDebugBanner.mm +178 -0
  25. package/ios/EnnioRuntimeHelper.h +264 -0
  26. package/ios/EnnioRuntimeHelper.mm +2443 -0
  27. package/lib/Ennio.nitro.d.ts +263 -0
  28. package/lib/Ennio.nitro.d.ts.map +1 -0
  29. package/lib/Ennio.nitro.js +2 -0
  30. package/lib/Ennio.nitro.js.map +1 -0
  31. package/lib/index.d.ts +16 -0
  32. package/lib/index.d.ts.map +1 -0
  33. package/lib/index.js +45 -0
  34. package/lib/index.js.map +1 -0
  35. package/nitro.json +24 -0
  36. package/nitrogen/generated/.gitattributes +1 -0
  37. package/nitrogen/generated/android/EnnioCore+autolinking.cmake +81 -0
  38. package/nitrogen/generated/android/EnnioCore+autolinking.gradle +27 -0
  39. package/nitrogen/generated/android/EnnioCoreOnLoad.cpp +49 -0
  40. package/nitrogen/generated/android/EnnioCoreOnLoad.hpp +34 -0
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/ennio/EnnioCoreOnLoad.kt +35 -0
  42. package/nitrogen/generated/ios/EnnioCore+autolinking.rb +62 -0
  43. package/nitrogen/generated/ios/EnnioCore-Swift-Cxx-Bridge.cpp +17 -0
  44. package/nitrogen/generated/ios/EnnioCore-Swift-Cxx-Bridge.hpp +27 -0
  45. package/nitrogen/generated/ios/EnnioCore-Swift-Cxx-Umbrella.hpp +38 -0
  46. package/nitrogen/generated/ios/EnnioCoreAutolinking.mm +35 -0
  47. package/nitrogen/generated/ios/EnnioCoreAutolinking.swift +16 -0
  48. package/nitrogen/generated/shared/c++/ExtendedElementInfo.hpp +118 -0
  49. package/nitrogen/generated/shared/c++/HybridEnnioSpec.cpp +44 -0
  50. package/nitrogen/generated/shared/c++/HybridEnnioSpec.hpp +93 -0
  51. package/nitrogen/generated/shared/c++/LayoutMetrics.hpp +103 -0
  52. package/nitrogen/generated/shared/c++/ScrollDirection.hpp +84 -0
  53. package/package.json +78 -0
  54. package/react-native.config.js +14 -0
  55. package/src/Ennio.nitro.ts +363 -0
  56. package/src/cli/hid-daemon.py +129 -0
  57. package/src/index.ts +72 -0
@@ -0,0 +1,174 @@
1
+ #pragma once
2
+
3
+ // Include the generated spec
4
+ #include "../nitrogen/generated/shared/c++/HybridEnnioSpec.hpp"
5
+
6
+ #include <memory>
7
+ #include <string>
8
+ #include <vector>
9
+ #include <functional>
10
+ #include <mutex>
11
+
12
+ // React Native Fabric headers
13
+ #include <react/renderer/core/ShadowNode.h>
14
+ #include <react/renderer/uimanager/UIManager.h>
15
+
16
+ // JSI for runtime + dispatcher access
17
+ #include <jsi/jsi.h>
18
+
19
+ namespace margelo::nitro { class Dispatcher; }
20
+
21
+ // Internal components
22
+ #include "Protocol.hpp"
23
+ #include "TestIDRegistry.hpp"
24
+ #include "ShadowTreeTraverser.hpp"
25
+ #include "SelectorCriteria.hpp"
26
+ #include "SelectorParser.hpp"
27
+ #include "ElementMatcher.hpp"
28
+
29
+ namespace margelo::nitro::ennio {
30
+
31
+ using ShadowNodePtr = std::shared_ptr<const facebook::react::ShadowNode>;
32
+ using RuntimeExecutor = std::function<void(std::function<void(facebook::jsi::Runtime&)>&&)>;
33
+
34
+ /**
35
+ * HybridEnnio - Main Nitro HybridObject for E2E testing
36
+ *
37
+ * Provides direct access to React Native's Fabric shadow tree
38
+ * for fast, reliable E2E testing without instrumentation.
39
+ */
40
+ class HybridEnnio : public HybridEnnioSpec {
41
+ public:
42
+ HybridEnnio();
43
+ ~HybridEnnio() override = default;
44
+
45
+ // ============================================
46
+ // Element Queries
47
+ // ============================================
48
+ bool exists(const std::string& testID) override;
49
+ bool isVisible(const std::string& testID) override;
50
+ std::variant<nitro::NullType, std::string> getText(const std::string& testID) override;
51
+
52
+ // ============================================
53
+ // Synchronization
54
+ // ============================================
55
+ bool waitForIdle(double timeoutMs) override;
56
+ void synchronize() override;
57
+
58
+ // Wake the moment React fires onCommitFiberRoot, capped at maxMs.
59
+ // Replaces blind sleep settles in the CLI with an early-wake; cap
60
+ // is the safety floor so the worst case is identical to a sleep.
61
+ bool waitForNextCommit(double maxMs) override;
62
+
63
+ // ============================================
64
+ // Selector-based Queries (Full Maestro Parity)
65
+ // ============================================
66
+ std::variant<nitro::NullType, ExtendedElementInfo> findBySelector(const std::string& selectorJson) override;
67
+ std::vector<ExtendedElementInfo> findAllBySelector(const std::string& selectorJson) override;
68
+ bool existsBySelector(const std::string& selectorJson) override;
69
+ std::variant<nitro::NullType, std::string> getTextBySelector(const std::string& selectorJson) override;
70
+ bool isVisibleBySelector(const std::string& selectorJson) override;
71
+
72
+ // ============================================
73
+ // Alert/Modal Handling
74
+ // ============================================
75
+ bool isAlertPresent() override;
76
+ std::string getAlertText() override;
77
+ std::vector<std::string> getAlertButtons() override;
78
+
79
+ // ============================================
80
+ // Fast-mode Writes
81
+ // ============================================
82
+ bool scroll(const std::string& testID, ScrollDirection direction, double distance) override;
83
+ bool scrollTo(const std::string& scrollViewTestID, const std::string& elementTestID) override;
84
+ bool swipeAtPoints(double x1, double y1, double x2, double y2, double durationMs) override;
85
+ bool pressHardwareKey(double keyCode) override;
86
+ bool backGesture() override;
87
+ bool hideKeyboard() override;
88
+ bool tapAlertButton(const std::string& buttonText) override;
89
+ bool dismissAlert() override;
90
+ bool copyToClipboard(const std::string& text) override;
91
+ bool pasteFromClipboard(const std::string& testID) override;
92
+
93
+ // ============================================
94
+ // Initialization (called from JS)
95
+ // ============================================
96
+
97
+ /**
98
+ * Initialize with UIManager reference for shadow tree access
99
+ * Must be called before using query/action methods
100
+ */
101
+ void initialize(
102
+ std::weak_ptr<facebook::react::UIManager> uiManager,
103
+ facebook::react::SurfaceId surfaceId
104
+ );
105
+
106
+ /**
107
+ * Check if the module is properly initialized
108
+ */
109
+ bool isInitialized() const;
110
+
111
+ /**
112
+ * JS-thread executor — wraps `RCTInstance.callFunctionOnBufferedRuntimeExecutor:`
113
+ * (or any equivalent scheduler) so background dispatch worker
114
+ * threads can schedule result-writes back onto JS. Stored once
115
+ * during bootstrap by `EnnioAutoInit`.
116
+ */
117
+ using JSThreadExecutor = std::function<void(std::function<void(facebook::jsi::Runtime&)>&&)>;
118
+ static void setJSThreadExecutor(JSThreadExecutor exec);
119
+
120
+ /**
121
+ * Pure-native bootstrap. Called from `EnnioAutoInit`'s post-start
122
+ * hook on the JS thread (after the runtime is initialised).
123
+ * Captures the runtime, evaluates the commit-signal walker, installs
124
+ * `__ennioDispatch` JSI host function so the external CLI can drive
125
+ * the runner via Hermes Inspector `Runtime.evaluate`. Idempotent.
126
+ */
127
+ static void nativeBootstrap(facebook::jsi::Runtime& runtime);
128
+
129
+
130
+ private:
131
+ // Shadow tree access
132
+ std::weak_ptr<facebook::react::UIManager> uiManager_;
133
+ facebook::react::SurfaceId surfaceId_ = 0;
134
+ mutable std::mutex mutex_;
135
+
136
+ // Screen dimensions for visibility checks
137
+ float screenWidth_ = 0;
138
+ float screenHeight_ = 0;
139
+
140
+ /**
141
+ * Get the current shadow tree root for the surface
142
+ */
143
+ ShadowNodePtr getShadowTreeRoot() const;
144
+
145
+ /**
146
+ * Find a node by testID using registry or tree traversal
147
+ */
148
+ ShadowNodePtr findNode(const std::string& testID) const;
149
+
150
+ /**
151
+ * Run one request through the central command table. Same path is
152
+ * driven from JSI (`__ennioDispatch`) — the CLI hands us a Request
153
+ * over Hermes Inspector CDP and we hand back a Response.
154
+ */
155
+ ::ennio::Response handleCommand(const ::ennio::Request& request);
156
+
157
+ /**
158
+ * Convert internal LayoutMetrics to Nitro struct
159
+ */
160
+ LayoutMetrics convertLayoutMetrics(const ::ennio::LayoutMetrics& metrics) const;
161
+
162
+ /**
163
+ * Convert internal ExtendedElementInfo to Nitro struct
164
+ */
165
+ ExtendedElementInfo convertExtendedElementInfo(const ::ennio::ExtendedElementInfo& info) const;
166
+
167
+ /**
168
+ * Find a node by selector criteria
169
+ */
170
+ ShadowNodePtr findNodeBySelector(const ::ennio::SelectorCriteria& criteria) const;
171
+
172
+ };
173
+
174
+ } // namespace margelo::nitro::ennio
@@ -0,0 +1,277 @@
1
+ #pragma once
2
+
3
+ #include <atomic>
4
+ #include <chrono>
5
+ #include <condition_variable>
6
+ #include <functional>
7
+ #include <mutex>
8
+
9
+ namespace ennio {
10
+
11
+ /**
12
+ * IdleMonitor tracks pending work to determine when the UI is idle.
13
+ *
14
+ * It monitors:
15
+ * - Shadow tree commits/mounts
16
+ * - JS thread tasks
17
+ * - Network requests (optional)
18
+ *
19
+ * This is a singleton that can be accessed from anywhere in the app.
20
+ */
21
+ class IdleMonitor {
22
+ public:
23
+ static IdleMonitor& getInstance() {
24
+ static IdleMonitor instance;
25
+ return instance;
26
+ }
27
+
28
+ // Prevent copying
29
+ IdleMonitor(const IdleMonitor&) = delete;
30
+ IdleMonitor& operator=(const IdleMonitor&) = delete;
31
+
32
+ // ============================================
33
+ // Shadow Tree Tracking
34
+ // ============================================
35
+
36
+ /**
37
+ * Called when a shadow tree commit starts
38
+ */
39
+ void onShadowTreeCommitStart() {
40
+ pendingCommits_.fetch_add(1);
41
+ lastActivityTime_ = std::chrono::steady_clock::now();
42
+ }
43
+
44
+ /**
45
+ * Called when a shadow tree commit completes
46
+ */
47
+ void onShadowTreeCommitEnd() {
48
+ pendingCommits_.fetch_sub(1);
49
+ lastActivityTime_ = std::chrono::steady_clock::now();
50
+ checkAndNotify();
51
+ }
52
+
53
+ /**
54
+ * Called when a shadow tree mount starts
55
+ */
56
+ void onShadowTreeMountStart() {
57
+ pendingMounts_.fetch_add(1);
58
+ lastActivityTime_ = std::chrono::steady_clock::now();
59
+ }
60
+
61
+ /**
62
+ * Called when a shadow tree mount completes
63
+ */
64
+ void onShadowTreeMountEnd() {
65
+ pendingMounts_.fetch_sub(1);
66
+ lastActivityTime_ = std::chrono::steady_clock::now();
67
+ checkAndNotify();
68
+ }
69
+
70
+ // ============================================
71
+ // JS Thread Tracking
72
+ // ============================================
73
+
74
+ /**
75
+ * Called when a JS task starts
76
+ */
77
+ void onJSTaskStart() {
78
+ pendingJSTasks_.fetch_add(1);
79
+ lastActivityTime_ = std::chrono::steady_clock::now();
80
+ }
81
+
82
+ /**
83
+ * Called when a JS task completes
84
+ */
85
+ void onJSTaskEnd() {
86
+ pendingJSTasks_.fetch_sub(1);
87
+ lastActivityTime_ = std::chrono::steady_clock::now();
88
+ checkAndNotify();
89
+ }
90
+
91
+ // ============================================
92
+ // Network Request Tracking (optional)
93
+ // ============================================
94
+
95
+ /**
96
+ * Called when a network request starts
97
+ */
98
+ void onNetworkRequestStart() {
99
+ pendingNetworkRequests_.fetch_add(1);
100
+ lastActivityTime_ = std::chrono::steady_clock::now();
101
+ }
102
+
103
+ /**
104
+ * Called when a network request completes
105
+ */
106
+ void onNetworkRequestEnd() {
107
+ pendingNetworkRequests_.fetch_sub(1);
108
+ lastActivityTime_ = std::chrono::steady_clock::now();
109
+ checkAndNotify();
110
+ }
111
+
112
+ // ============================================
113
+ // Animation Tracking
114
+ // ============================================
115
+
116
+ /**
117
+ * Called when an animation starts
118
+ */
119
+ void onAnimationStart() {
120
+ pendingAnimations_.fetch_add(1);
121
+ lastActivityTime_ = std::chrono::steady_clock::now();
122
+ }
123
+
124
+ /**
125
+ * Called when an animation completes
126
+ */
127
+ void onAnimationEnd() {
128
+ pendingAnimations_.fetch_sub(1);
129
+ lastActivityTime_ = std::chrono::steady_clock::now();
130
+ checkAndNotify();
131
+ }
132
+
133
+ // ============================================
134
+ // Status Queries
135
+ // ============================================
136
+
137
+ /**
138
+ * Check if the UI is currently idle
139
+ * Returns true if:
140
+ * - No pending commits or mounts
141
+ * - No pending JS tasks
142
+ * - (Optionally) No pending network requests
143
+ * - (Optionally) No pending animations
144
+ */
145
+ bool isIdle(bool includeNetwork = false, bool includeAnimations = false) const {
146
+ if (pendingCommits_.load() > 0) return false;
147
+ if (pendingMounts_.load() > 0) return false;
148
+ if (pendingJSTasks_.load() > 0) return false;
149
+ if (includeNetwork && pendingNetworkRequests_.load() > 0) return false;
150
+ if (includeAnimations && pendingAnimations_.load() > 0) return false;
151
+ return true;
152
+ }
153
+
154
+ /**
155
+ * Get the time since the last activity
156
+ */
157
+ std::chrono::milliseconds timeSinceLastActivity() const {
158
+ auto now = std::chrono::steady_clock::now();
159
+ return std::chrono::duration_cast<std::chrono::milliseconds>(
160
+ now - lastActivityTime_.load()
161
+ );
162
+ }
163
+
164
+ /**
165
+ * Wait for idle state with timeout
166
+ * @param timeoutMs Maximum time to wait in milliseconds
167
+ * @param includeNetwork Whether to wait for network requests
168
+ * @param includeAnimations Whether to wait for animations
169
+ * @param stabilityMs How long the system must be idle before returning
170
+ * @return true if idle state was reached, false on timeout
171
+ */
172
+ bool waitForIdle(
173
+ int timeoutMs,
174
+ bool includeNetwork = false,
175
+ bool includeAnimations = false,
176
+ int stabilityMs = 100
177
+ ) {
178
+ auto deadline = std::chrono::steady_clock::now() +
179
+ std::chrono::milliseconds(timeoutMs);
180
+
181
+ std::unique_lock<std::mutex> lock(mutex_);
182
+
183
+ while (std::chrono::steady_clock::now() < deadline) {
184
+ // Check if idle
185
+ if (isIdle(includeNetwork, includeAnimations)) {
186
+ // Check stability - has it been idle long enough?
187
+ auto idleTime = timeSinceLastActivity();
188
+ if (idleTime.count() >= stabilityMs) {
189
+ return true;
190
+ }
191
+
192
+ // Wait for remaining stability time
193
+ auto remainingStability = std::chrono::milliseconds(stabilityMs) - idleTime;
194
+ cv_.wait_for(lock, remainingStability);
195
+ } else {
196
+ // Wait for notification or timeout
197
+ auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(
198
+ deadline - std::chrono::steady_clock::now()
199
+ );
200
+ if (remaining.count() <= 0) {
201
+ return false;
202
+ }
203
+ cv_.wait_for(lock, remaining);
204
+ }
205
+ }
206
+
207
+ return isIdle(includeNetwork, includeAnimations);
208
+ }
209
+
210
+ // ============================================
211
+ // Debug Information
212
+ // ============================================
213
+
214
+ /**
215
+ * Get current pending counts for debugging
216
+ */
217
+ struct PendingCounts {
218
+ int commits;
219
+ int mounts;
220
+ int jsTasks;
221
+ int networkRequests;
222
+ int animations;
223
+ };
224
+
225
+ PendingCounts getPendingCounts() const {
226
+ return {
227
+ pendingCommits_.load(),
228
+ pendingMounts_.load(),
229
+ pendingJSTasks_.load(),
230
+ pendingNetworkRequests_.load(),
231
+ pendingAnimations_.load()
232
+ };
233
+ }
234
+
235
+ /**
236
+ * Reset all counters (for testing)
237
+ */
238
+ void reset() {
239
+ pendingCommits_.store(0);
240
+ pendingMounts_.store(0);
241
+ pendingJSTasks_.store(0);
242
+ pendingNetworkRequests_.store(0);
243
+ pendingAnimations_.store(0);
244
+ lastActivityTime_.store(std::chrono::steady_clock::now());
245
+ }
246
+
247
+ private:
248
+ IdleMonitor()
249
+ : pendingCommits_(0)
250
+ , pendingMounts_(0)
251
+ , pendingJSTasks_(0)
252
+ , pendingNetworkRequests_(0)
253
+ , pendingAnimations_(0)
254
+ , lastActivityTime_(std::chrono::steady_clock::now()) {}
255
+
256
+ void checkAndNotify() {
257
+ if (isIdle()) {
258
+ cv_.notify_all();
259
+ }
260
+ }
261
+
262
+ // Atomic counters for thread-safe tracking
263
+ std::atomic<int> pendingCommits_;
264
+ std::atomic<int> pendingMounts_;
265
+ std::atomic<int> pendingJSTasks_;
266
+ std::atomic<int> pendingNetworkRequests_;
267
+ std::atomic<int> pendingAnimations_;
268
+
269
+ // Last activity timestamp
270
+ std::atomic<std::chrono::steady_clock::time_point> lastActivityTime_;
271
+
272
+ // Mutex and condition variable for waiting
273
+ mutable std::mutex mutex_;
274
+ std::condition_variable cv_;
275
+ };
276
+
277
+ } // namespace ennio
@@ -0,0 +1,135 @@
1
+ #include "Protocol.hpp"
2
+
3
+ #include <iomanip>
4
+ #include <sstream>
5
+ #include <string>
6
+
7
+ namespace ennio {
8
+
9
+ // Escape characters that would otherwise break the toJSON() string when
10
+ // `id` or `error` carries an external value (request id, exception
11
+ // message). Control chars get \uXXXX form, the rest match the strict
12
+ // JSON escape set.
13
+ static std::string jsonEscape(const std::string& str) {
14
+ std::string out;
15
+ out.reserve(str.size() + 8);
16
+ for (char c : str) {
17
+ switch (c) {
18
+ case '"': out += "\\\""; break;
19
+ case '\\': out += "\\\\"; break;
20
+ case '\n': out += "\\n"; break;
21
+ case '\r': out += "\\r"; break;
22
+ case '\t': out += "\\t"; break;
23
+ default:
24
+ if (static_cast<unsigned char>(c) < 0x20) {
25
+ char buf[8];
26
+ snprintf(buf, sizeof(buf), "\\u%04x", static_cast<unsigned char>(c));
27
+ out += buf;
28
+ } else {
29
+ out += c;
30
+ }
31
+ }
32
+ }
33
+ return out;
34
+ }
35
+
36
+ namespace json {
37
+
38
+ std::string stringify(const std::string& key, const std::string& value) {
39
+ return "\"" + key + "\":\"" + value + "\"";
40
+ }
41
+
42
+ std::string stringify(const std::string& key, bool value) {
43
+ return "\"" + key + "\":" + (value ? "true" : "false");
44
+ }
45
+
46
+ std::string stringify(const std::string& key, int value) {
47
+ return "\"" + key + "\":" + std::to_string(value);
48
+ }
49
+
50
+ std::string stringify(const std::string& key, double value) {
51
+ std::ostringstream oss;
52
+ oss << std::setprecision(10) << value;
53
+ return "\"" + key + "\":" + oss.str();
54
+ }
55
+
56
+ std::string parseString(const std::string& json, const std::string& key) {
57
+ std::string search = "\"" + key + "\":\"";
58
+ size_t pos = json.find(search);
59
+ if (pos == std::string::npos) {
60
+ return "";
61
+ }
62
+ pos += search.length();
63
+ std::string result;
64
+ bool escaped = false;
65
+ for (size_t i = pos; i < json.size(); i++) {
66
+ char c = json[i];
67
+ if (escaped) {
68
+ switch (c) {
69
+ case '"': result += '"'; break;
70
+ case '\\': result += '\\'; break;
71
+ case 'n': result += '\n'; break;
72
+ case 'r': result += '\r'; break;
73
+ case 't': result += '\t'; break;
74
+ default: result += c; break;
75
+ }
76
+ escaped = false;
77
+ } else if (c == '\\') {
78
+ escaped = true;
79
+ } else if (c == '"') {
80
+ break;
81
+ } else {
82
+ result += c;
83
+ }
84
+ }
85
+ return result;
86
+ }
87
+
88
+ bool parseBool(const std::string& json, const std::string& key) {
89
+ std::string search = "\"" + key + "\":";
90
+ size_t pos = json.find(search);
91
+ if (pos == std::string::npos) {
92
+ return false;
93
+ }
94
+ pos += search.length();
95
+ return json.substr(pos, 4) == "true";
96
+ }
97
+
98
+ int parseInt(const std::string& json, const std::string& key) {
99
+ std::string search = "\"" + key + "\":";
100
+ size_t pos = json.find(search);
101
+ if (pos == std::string::npos) {
102
+ return 0;
103
+ }
104
+ pos += search.length();
105
+ return std::stoi(json.substr(pos));
106
+ }
107
+
108
+ double parseDouble(const std::string& json, const std::string& key) {
109
+ std::string search = "\"" + key + "\":";
110
+ size_t pos = json.find(search);
111
+ if (pos == std::string::npos) {
112
+ return 0.0;
113
+ }
114
+ pos += search.length();
115
+ return std::stod(json.substr(pos));
116
+ }
117
+
118
+ } // namespace json
119
+
120
+ std::string Response::toJSON() const {
121
+ std::ostringstream oss;
122
+ oss << "{";
123
+ oss << "\"id\":\"" << jsonEscape(id) << "\",";
124
+ oss << "\"success\":" << (success ? "true" : "false");
125
+ if (!data.empty()) {
126
+ oss << ",\"data\":" << data;
127
+ }
128
+ if (!error.empty()) {
129
+ oss << ",\"error\":\"" << jsonEscape(error) << "\"";
130
+ }
131
+ oss << "}";
132
+ return oss.str();
133
+ }
134
+
135
+ } // namespace ennio
@@ -0,0 +1,47 @@
1
+ #pragma once
2
+
3
+ #include <string>
4
+
5
+ namespace ennio {
6
+
7
+ // Request envelope. Built by the CLI side as a JSON-RPC payload, hands
8
+ // off to HybridEnnio::handleCommand() inside a background dispatch
9
+ // worker. `payload` is a JSON string (not parsed) — the handler does
10
+ // its own type-specific extraction via cpp/json helpers.
11
+ struct Request {
12
+ std::string id;
13
+ std::string type;
14
+ std::string payload;
15
+ };
16
+
17
+ // Response envelope. `data` is a pre-rendered JSON fragment (object,
18
+ // array, bool, number, string literal — whatever the handler emits).
19
+ // `Response::toJSON()` wraps it into the full reply object the CLI
20
+ // expects: `{"id":..,"success":..,"data":..,"error":..}`.
21
+ struct Response {
22
+ std::string id;
23
+ bool success;
24
+ std::string data;
25
+ std::string error;
26
+
27
+ std::string toJSON() const;
28
+ };
29
+
30
+ // Tiny, dependency-free JSON helpers. Used by the dispatch handlers to
31
+ // peel typed values out of `Request.payload` (which is a JSON string,
32
+ // not a parsed object). Loose enough to handle the CLI's serialiser
33
+ // without pulling in a real JSON library; strict enough to break loudly
34
+ // if the wire format drifts.
35
+ namespace json {
36
+ std::string stringify(const std::string& key, const std::string& value);
37
+ std::string stringify(const std::string& key, bool value);
38
+ std::string stringify(const std::string& key, int value);
39
+ std::string stringify(const std::string& key, double value);
40
+
41
+ std::string parseString(const std::string& json, const std::string& key);
42
+ bool parseBool(const std::string& json, const std::string& key);
43
+ int parseInt(const std::string& json, const std::string& key);
44
+ double parseDouble(const std::string& json, const std::string& key);
45
+ }
46
+
47
+ } // namespace ennio