NAH 2.0.16
Native Application Host - Library API Reference
Loading...
Searching...
No Matches
nah_json.h
Go to the documentation of this file.
1/*
2 * NAH JSON - JSON Parsing for NAH Types
3 *
4 * This file provides JSON serialization and deserialization for all NAH types.
5 * Requires nlohmann/json.
6 *
7 * SPDX-License-Identifier: Apache-2.0
8 */
9
10#ifndef NAH_JSON_H
11#define NAH_JSON_H
12
13#ifdef __cplusplus
14
15#include "nah_core.h"
16#include <nlohmann/json.hpp>
17
18namespace nah {
19namespace json {
20
21using json = nlohmann::json;
22
23// ============================================================================
24// PARSE RESULTS
25// ============================================================================
26
27template<typename T>
28struct ParseResult {
29 bool ok = false;
30 std::string error;
31 T value;
32 std::vector<std::string> warnings;
33};
34
35// ============================================================================
36// HELPER FUNCTIONS
37// ============================================================================
38
39namespace detail {
40
41inline std::string get_string(const json& j, const std::string& key, const std::string& default_val = "") {
42 if (j.contains(key) && j[key].is_string()) {
43 return j[key].get<std::string>();
44 }
45 return default_val;
46}
47
48inline std::vector<std::string> get_string_array(const json& j, const std::string& key) {
49 std::vector<std::string> result;
50 if (j.contains(key) && j[key].is_array()) {
51 for (const auto& item : j[key]) {
52 if (item.is_string()) {
53 result.push_back(item.get<std::string>());
54 }
55 }
56 }
57 return result;
58}
59
60inline bool get_bool(const json& j, const std::string& key, bool default_val = false) {
61 if (j.contains(key) && j[key].is_boolean()) {
62 return j[key].get<bool>();
63 }
64 return default_val;
65}
66
67} // namespace detail
68
69// ============================================================================
70// ENV VALUE PARSING
71// ============================================================================
72
73inline core::EnvValue parse_env_value(const json& j) {
74 core::EnvValue ev;
75
76 if (j.is_string()) {
77 ev.op = core::EnvOp::Set;
78 ev.value = j.get<std::string>();
79 return ev;
80 }
81
82 if (j.is_object()) {
83 std::string op_str = detail::get_string(j, "op", "set");
84 auto op = core::parse_env_op(op_str);
85 ev.op = op.value_or(core::EnvOp::Set);
86 ev.value = detail::get_string(j, "value");
87 ev.separator = detail::get_string(j, "separator", ":");
88 }
89
90 return ev;
91}
92
93inline core::EnvMap parse_env_map(const json& j) {
94 core::EnvMap result;
95 if (j.is_object()) {
96 for (auto& [key, val] : j.items()) {
97 result[key] = parse_env_value(val);
98 }
99 }
100 return result;
101}
102
103// ============================================================================
104// TRUST INFO PARSING
105// ============================================================================
106
107inline core::TrustInfo parse_trust_info(const json& j) {
108 core::TrustInfo ti;
109
110 std::string state_str = detail::get_string(j, "state", "unknown");
111 auto state = core::parse_trust_state(state_str);
112 ti.state = state.value_or(core::TrustState::Unknown);
113
114 ti.source = detail::get_string(j, "source");
115 ti.evaluated_at = detail::get_string(j, "evaluated_at");
116 ti.expires_at = detail::get_string(j, "expires_at");
117 ti.inputs_hash = detail::get_string(j, "inputs_hash");
118
119 if (j.contains("details") && j["details"].is_object()) {
120 for (auto& [key, val] : j["details"].items()) {
121 if (val.is_string()) {
122 ti.details[key] = val.get<std::string>();
123 }
124 }
125 }
126
127 return ti;
128}
129
130// ============================================================================
131// LOADER CONFIG PARSING
132// ============================================================================
133
134inline core::LoaderConfig parse_loader_config(const json& j) {
135 core::LoaderConfig lc;
136 lc.exec_path = detail::get_string(j, "exec_path");
137 lc.args_template = detail::get_string_array(j, "args_template");
138 return lc;
139}
140
141// ============================================================================
142// COMPONENT PARSING
143// ============================================================================
144
145inline core::ComponentDecl parse_component(const json& j) {
146 core::ComponentDecl comp;
147
148 comp.id = detail::get_string(j, "id");
149 comp.name = detail::get_string(j, "name");
150 comp.description = detail::get_string(j, "description");
151 comp.icon = detail::get_string(j, "icon");
152 comp.entrypoint = detail::get_string(j, "entrypoint");
153 comp.uri_pattern = detail::get_string(j, "uri_pattern");
154 comp.loader = detail::get_string(j, "loader");
155 comp.standalone = detail::get_bool(j, "standalone", true);
156 comp.hidden = detail::get_bool(j, "hidden", false);
157
158 // Component-specific environment
159 if (j.contains("environment") && j["environment"].is_object()) {
160 comp.environment = parse_env_map(j["environment"]);
161 }
162
163 // Component-specific permissions
164 if (j.contains("permissions") && j["permissions"].is_object()) {
165 comp.permissions_filesystem = detail::get_string_array(j["permissions"], "filesystem");
166 comp.permissions_network = detail::get_string_array(j["permissions"], "network");
167 }
168
169 // Metadata
170 if (j.contains("metadata") && j["metadata"].is_object()) {
171 for (auto& [key, value] : j["metadata"].items()) {
172 if (value.is_string()) {
173 comp.metadata[key] = value.get<std::string>();
174 }
175 }
176 }
177
178 return comp;
179}
180
181// ============================================================================
182// APP DECLARATION PARSING
183// ============================================================================
184
185inline ParseResult<core::AppDeclaration> parse_app_declaration(const std::string& json_str) {
186 ParseResult<core::AppDeclaration> result;
187
188 try {
189 json j = json::parse(json_str);
190
191 // Handle nested "app" structure if present
192 if (j.contains("app") && j["app"].is_object()) {
193 j = j["app"];
194 }
195
196 auto& app = result.value;
197
198 // Identity (nested in v1.1.0 format, flat in older format)
199 if (j.contains("identity") && j["identity"].is_object()) {
200 // New format: app.identity
201 auto& identity = j["identity"];
202 app.id = detail::get_string(identity, "id");
203 app.version = detail::get_string(identity, "version");
204 app.nak_id = detail::get_string(identity, "nak_id");
205 app.nak_version_req = detail::get_string(identity, "nak_version_req");
206 } else {
207 // Legacy flat format
208 app.id = detail::get_string(j, "id");
209 app.version = detail::get_string(j, "version");
210
211 // NAK requirements - check multiple possible formats
212 if (j.contains("nak") && j["nak"].is_object()) {
213 // Legacy: nak.id and nak.version_req
214 app.nak_id = detail::get_string(j["nak"], "id");
215 app.nak_version_req = detail::get_string(j["nak"], "version_req");
216 } else {
217 // Flat format: nak_id and nak_version_req
218 app.nak_id = detail::get_string(j, "nak_id");
219 app.nak_version_req = detail::get_string(j, "nak_version_req");
220 }
221 }
222
223 if (app.id.empty()) {
224 result.error = "missing required field: id";
225 return result;
226 }
227 if (app.version.empty()) {
228 result.error = "missing required field: version";
229 return result;
230 }
231
232 // Execution (nested in v1.1.0 format)
233 if (j.contains("execution") && j["execution"].is_object()) {
234 // New format: app.execution
235 auto& execution = j["execution"];
236 app.entrypoint_path = detail::get_string(execution, "entrypoint");
237 app.entrypoint_args = detail::get_string_array(execution, "args");
238 app.nak_loader = detail::get_string(execution, "loader"); // Optional loader preference
239 } else if (j.contains("entrypoint")) {
240 // Legacy format
241 if (j["entrypoint"].is_object()) {
242 app.entrypoint_path = detail::get_string(j["entrypoint"], "path");
243 app.entrypoint_args = detail::get_string_array(j["entrypoint"], "args");
244 } else if (j["entrypoint"].is_string()) {
245 app.entrypoint_path = j["entrypoint"].get<std::string>();
246 }
247 } else {
248 app.entrypoint_path = detail::get_string(j, "entrypoint_path");
249 app.entrypoint_args = detail::get_string_array(j, "entrypoint_args");
250 }
251
252 if (app.entrypoint_path.empty()) {
253 result.error = "missing required field: entrypoint path";
254 return result;
255 }
256
257 // Layout (nested in v1.1.0 format)
258 if (j.contains("layout") && j["layout"].is_object()) {
259 // New format: app.layout
260 auto& layout = j["layout"];
261 app.lib_dirs = detail::get_string_array(layout, "lib_dirs");
262 app.asset_dirs = detail::get_string_array(layout, "asset_dirs");
263 } else {
264 // Legacy flat format
265 app.lib_dirs = detail::get_string_array(j, "lib_dirs");
266 app.asset_dirs = detail::get_string_array(j, "asset_dirs");
267 }
268
269 // Environment
270 app.env_vars = detail::get_string_array(j, "env_vars");
271 if (j.contains("environment") && j["environment"].is_object()) {
272 // Also support environment object (v1.1.0 format)
273 for (auto& [key, value] : j["environment"].items()) {
274 if (value.is_string()) {
275 app.env_vars.push_back(key + "=" + value.get<std::string>());
276 }
277 }
278 }
279
280 // Asset exports
281 if (j.contains("exports") && j["exports"].is_array()) {
282 for (const auto& exp : j["exports"]) {
283 core::AssetExportDecl aed;
284 aed.id = detail::get_string(exp, "id");
285 aed.path = detail::get_string(exp, "path");
286 aed.type = detail::get_string(exp, "type");
287 app.asset_exports.push_back(aed);
288 }
289 } else if (j.contains("asset_exports") && j["asset_exports"].is_array()) {
290 for (const auto& exp : j["asset_exports"]) {
291 core::AssetExportDecl aed;
292 aed.id = detail::get_string(exp, "id");
293 aed.path = detail::get_string(exp, "path");
294 aed.type = detail::get_string(exp, "type");
295 app.asset_exports.push_back(aed);
296 }
297 }
298
299 // Permissions
300 if (j.contains("permissions") && j["permissions"].is_object()) {
301 app.permissions_filesystem = detail::get_string_array(j["permissions"], "filesystem");
302 app.permissions_network = detail::get_string_array(j["permissions"], "network");
303 }
304
305 // Metadata (can be flat or in metadata object)
306 if (j.contains("metadata") && j["metadata"].is_object()) {
307 app.description = detail::get_string(j["metadata"], "description");
308 app.author = detail::get_string(j["metadata"], "author");
309 app.license = detail::get_string(j["metadata"], "license");
310 app.homepage = detail::get_string(j["metadata"], "homepage");
311 } else {
312 app.description = detail::get_string(j, "description");
313 app.author = detail::get_string(j, "author");
314 app.license = detail::get_string(j, "license");
315 app.homepage = detail::get_string(j, "homepage");
316 }
317
318 // Components (new in component architecture)
319 if (j.contains("components") && j["components"].is_object()) {
320 const auto& comps = j["components"];
321 if (comps.contains("provides") && comps["provides"].is_array()) {
322 for (const auto& comp_json : comps["provides"]) {
323 app.components.push_back(parse_component(comp_json));
324 }
325 }
326 }
327
328 result.ok = true;
329
330 } catch (const json::exception& e) {
331 result.error = std::string("JSON parse error: ") + e.what();
332 }
333
334 return result;
335}
336
337// ============================================================================
338// HOST ENVIRONMENT PARSING
339// ============================================================================
340
341inline ParseResult<core::HostEnvironment> parse_host_environment(const json& j,
342 const std::string& source_path = "") {
343 ParseResult<core::HostEnvironment> result;
344
345 try {
346 auto& host_env = result.value;
347
348 host_env.source_path = source_path;
349
350 // Environment section
351 if (j.contains("environment") && j["environment"].is_object()) {
352 host_env.vars = parse_env_map(j["environment"]);
353 }
354
355 // Paths section
356 if (j.contains("paths") && j["paths"].is_object()) {
357 host_env.paths.library_prepend = detail::get_string_array(j["paths"], "library_prepend");
358 host_env.paths.library_append = detail::get_string_array(j["paths"], "library_append");
359 }
360
361 // Overrides section
362 if (j.contains("overrides") && j["overrides"].is_object()) {
363 const auto& ovr = j["overrides"];
364 host_env.overrides.allow_env_overrides = detail::get_bool(ovr, "allow_env_overrides", true);
365 host_env.overrides.allowed_env_keys = detail::get_string_array(ovr, "allowed_env_keys");
366 }
367
368 result.ok = true;
369
370 } catch (const json::exception& e) {
371 result.error = std::string("JSON parse error: ") + e.what();
372 }
373
374 return result;
375}
376
377inline ParseResult<core::HostEnvironment> parse_host_environment(const std::string& json_str,
378 const std::string& source_path = "") {
379 ParseResult<core::HostEnvironment> result;
380
381 try {
382 json j = json::parse(json_str);
383 return parse_host_environment(j, source_path);
384 } catch (const json::exception& e) {
385 result.error = std::string("JSON parse error: ") + e.what();
386 }
387
388 return result;
389}
390
391// ============================================================================
392// INSTALL RECORD PARSING
393// ============================================================================
394
395inline ParseResult<core::InstallRecord> parse_install_record(const std::string& json_str,
396 const std::string& source_path = "") {
397 ParseResult<core::InstallRecord> result;
398
399 try {
400 json j = json::parse(json_str);
401 auto& ir = result.value;
402
403 ir.source_path = source_path;
404
405 // Install section
406 if (j.contains("install") && j["install"].is_object()) {
407 ir.install.instance_id = detail::get_string(j["install"], "instance_id");
408 }
409
410 if (ir.install.instance_id.empty()) {
411 result.error = "missing required field: install.instance_id";
412 return result;
413 }
414
415 // App section (audit only)
416 if (j.contains("app") && j["app"].is_object()) {
417 ir.app.id = detail::get_string(j["app"], "id");
418 ir.app.version = detail::get_string(j["app"], "version");
419 ir.app.nak_id = detail::get_string(j["app"], "nak_id");
420 ir.app.nak_version_req = detail::get_string(j["app"], "nak_version_req");
421 }
422
423 // NAK section
424 if (j.contains("nak") && j["nak"].is_object()) {
425 ir.nak.id = detail::get_string(j["nak"], "id");
426 ir.nak.version = detail::get_string(j["nak"], "version");
427 ir.nak.record_ref = detail::get_string(j["nak"], "record_ref");
428 ir.nak.loader = detail::get_string(j["nak"], "loader");
429 ir.nak.selection_reason = detail::get_string(j["nak"], "selection_reason");
430 }
431
432 // Paths section
433 if (j.contains("paths") && j["paths"].is_object()) {
434 ir.paths.install_root = detail::get_string(j["paths"], "install_root");
435 }
436
437 if (ir.paths.install_root.empty()) {
438 result.error = "missing required field: paths.install_root";
439 return result;
440 }
441
442 // Provenance section
443 if (j.contains("provenance") && j["provenance"].is_object()) {
444 ir.provenance.package_hash = detail::get_string(j["provenance"], "package_hash");
445 ir.provenance.installed_at = detail::get_string(j["provenance"], "installed_at");
446 ir.provenance.installed_by = detail::get_string(j["provenance"], "installed_by");
447 ir.provenance.source = detail::get_string(j["provenance"], "source");
448 }
449
450 // Trust section
451 if (j.contains("trust") && j["trust"].is_object()) {
452 ir.trust = parse_trust_info(j["trust"]);
453 }
454
455 // Overrides section
456 if (j.contains("overrides") && j["overrides"].is_object()) {
457 const auto& ovr = j["overrides"];
458
459 if (ovr.contains("environment") && ovr["environment"].is_object()) {
460 ir.overrides.environment = parse_env_map(ovr["environment"]);
461 }
462
463 if (ovr.contains("arguments") && ovr["arguments"].is_object()) {
464 ir.overrides.arguments.prepend = detail::get_string_array(ovr["arguments"], "prepend");
465 ir.overrides.arguments.append = detail::get_string_array(ovr["arguments"], "append");
466 }
467
468 if (ovr.contains("paths") && ovr["paths"].is_object()) {
469 ir.overrides.paths.library_prepend = detail::get_string_array(ovr["paths"], "library_prepend");
470 }
471 }
472
473 result.ok = true;
474
475 } catch (const json::exception& e) {
476 result.error = std::string("JSON parse error: ") + e.what();
477 }
478
479 return result;
480}
481
482// ============================================================================
483// RUNTIME DESCRIPTOR PARSING
484// ============================================================================
485
486inline ParseResult<core::RuntimeDescriptor> parse_runtime_descriptor(const std::string& json_str,
487 const std::string& source_path = "") {
488 ParseResult<core::RuntimeDescriptor> result;
489
490 try {
491 json j = json::parse(json_str);
492 auto& rd = result.value;
493
494 rd.source_path = source_path;
495
496 // NAK section
497 if (j.contains("nak") && j["nak"].is_object()) {
498 rd.nak.id = detail::get_string(j["nak"], "id");
499 rd.nak.version = detail::get_string(j["nak"], "version");
500 }
501
502 if (rd.nak.id.empty()) {
503 result.error = "missing required field: nak.id";
504 return result;
505 }
506 if (rd.nak.version.empty()) {
507 result.error = "missing required field: nak.version";
508 return result;
509 }
510
511 // Paths section
512 if (j.contains("paths") && j["paths"].is_object()) {
513 rd.paths.root = detail::get_string(j["paths"], "root");
514 rd.paths.resource_root = detail::get_string(j["paths"], "resource_root");
515 rd.paths.lib_dirs = detail::get_string_array(j["paths"], "lib_dirs");
516 }
517
518 if (rd.paths.root.empty()) {
519 result.error = "missing required field: paths.root";
520 return result;
521 }
522
523 if (rd.paths.resource_root.empty()) {
524 rd.paths.resource_root = rd.paths.root;
525 }
526
527 // Environment section
528 if (j.contains("environment") && j["environment"].is_object()) {
529 rd.environment = parse_env_map(j["environment"]);
530 }
531
532 // Loaders section
533 if (j.contains("loaders") && j["loaders"].is_object()) {
534 for (auto& [name, config] : j["loaders"].items()) {
535 rd.loaders[name] = parse_loader_config(config);
536 }
537 }
538
539 // Execution section
540 if (j.contains("execution") && j["execution"].is_object()) {
541 rd.execution.present = true;
542 rd.execution.cwd = detail::get_string(j["execution"], "cwd");
543 }
544
545 // Provenance section
546 if (j.contains("provenance") && j["provenance"].is_object()) {
547 rd.provenance.package_hash = detail::get_string(j["provenance"], "package_hash");
548 rd.provenance.installed_at = detail::get_string(j["provenance"], "installed_at");
549 rd.provenance.installed_by = detail::get_string(j["provenance"], "installed_by");
550 rd.provenance.source = detail::get_string(j["provenance"], "source");
551 }
552
553 result.ok = true;
554
555 } catch (const json::exception& e) {
556 result.error = std::string("JSON parse error: ") + e.what();
557 }
558
559 return result;
560}
561
562// ============================================================================
563// LAUNCH CONTRACT SERIALIZATION (already in nah_core.h, re-export here)
564// ============================================================================
565
566using core::serialize_contract;
567using core::serialize_result;
568
569// ============================================================================
570// LAUNCH CONTRACT PARSING (for cached contracts)
571// ============================================================================
572
573inline ParseResult<core::LaunchContract> parse_launch_contract(const std::string& json_str) {
574 ParseResult<core::LaunchContract> result;
575
576 try {
577 json j = json::parse(json_str);
578 auto& c = result.value;
579
580 // App section
581 if (j.contains("app") && j["app"].is_object()) {
582 c.app.id = detail::get_string(j["app"], "id");
583 c.app.version = detail::get_string(j["app"], "version");
584 c.app.root = detail::get_string(j["app"], "root");
585 c.app.entrypoint = detail::get_string(j["app"], "entrypoint");
586 }
587
588 // NAK section
589 if (j.contains("nak") && j["nak"].is_object()) {
590 c.nak.id = detail::get_string(j["nak"], "id");
591 c.nak.version = detail::get_string(j["nak"], "version");
592 c.nak.root = detail::get_string(j["nak"], "root");
593 c.nak.resource_root = detail::get_string(j["nak"], "resource_root");
594 c.nak.record_ref = detail::get_string(j["nak"], "record_ref");
595 }
596
597 // Execution section
598 if (j.contains("execution") && j["execution"].is_object()) {
599 c.execution.binary = detail::get_string(j["execution"], "binary");
600 c.execution.arguments = detail::get_string_array(j["execution"], "arguments");
601 c.execution.cwd = detail::get_string(j["execution"], "cwd");
602 c.execution.library_path_env_key = detail::get_string(j["execution"], "library_path_env_key");
603 c.execution.library_paths = detail::get_string_array(j["execution"], "library_paths");
604 }
605
606 // Environment section
607 if (j.contains("environment") && j["environment"].is_object()) {
608 for (auto& [key, val] : j["environment"].items()) {
609 if (val.is_string()) {
610 c.environment[key] = val.get<std::string>();
611 }
612 }
613 }
614
615 // Enforcement section
616 if (j.contains("enforcement") && j["enforcement"].is_object()) {
617 c.enforcement.filesystem = detail::get_string_array(j["enforcement"], "filesystem");
618 c.enforcement.network = detail::get_string_array(j["enforcement"], "network");
619 }
620
621 // Trust section
622 if (j.contains("trust") && j["trust"].is_object()) {
623 c.trust = parse_trust_info(j["trust"]);
624 }
625
626 // Capability usage section
627 if (j.contains("capability_usage") && j["capability_usage"].is_object()) {
628 c.capability_usage.present = detail::get_bool(j["capability_usage"], "present");
629 c.capability_usage.required_capabilities =
630 detail::get_string_array(j["capability_usage"], "required_capabilities");
631 c.capability_usage.optional_capabilities =
632 detail::get_string_array(j["capability_usage"], "optional_capabilities");
633 c.capability_usage.critical_capabilities =
634 detail::get_string_array(j["capability_usage"], "critical_capabilities");
635 }
636
637 result.ok = true;
638
639 } catch (const json::exception& e) {
640 result.error = std::string("JSON parse error: ") + e.what();
641 }
642
643 return result;
644}
645
646} // namespace json
647} // namespace nah
648
649#endif // __cplusplus
650
651#endif // NAH_JSON_H