From 7934a339e9b2319e0234e8f2ca5d952eff243c05 Mon Sep 17 00:00:00 2001
From: Ferenc Szontágh <szf@fsociety.hu>
Date: Fri, 18 Apr 2025 21:45:32 +0000
Subject: [PATCH] improve curl

---
 src/VoidScript.hpp                    |    3 
 test_scripts/curl_json.vs             |   38 +++
 Modules/CurlModule/src/CurlModule.cpp |  150 +++++++++++++-
 Modules/CurlModule/README.md          |   80 ++++++++
 test_scripts/json.vs                  |   49 ++++
 CMakeLists.txt                        |    2 
 Modules/CurlModule/src/CurlModule.hpp |   12 +
 src/Modules/JsonModule.hpp            |  239 +++++++++++++++++++++++
 8 files changed, 557 insertions(+), 16 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 968ad4b..6fd7b3f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -156,7 +156,7 @@
 endif()
 
 # Plugin modules options
-option(BUILD_MODULE_CURL "Enable building CurlModule" OFF)
+option(BUILD_MODULE_CURL "Enable building CurlModule" ON)
 
 if (BUILD_MODULE_CURL)
  add_subdirectory(Modules/CurlModule)
diff --git a/Modules/CurlModule/README.md b/Modules/CurlModule/README.md
new file mode 100644
index 0000000..c45484b
--- /dev/null
+++ b/Modules/CurlModule/README.md
@@ -0,0 +1,80 @@
+# CurlModule
+
+This module provides HTTP GET and POST functionality via libcurl in VoidScript.
+
+## Functions
+
+### curlGet
+`curlGet(url [, options]) -> string`
+
+- `url` (string): The HTTP/HTTPS URL to request.
+- `options` (object, optional): Configuration object with fields:
+  - `timeout` (int or float): Maximum time in seconds to wait for the request.
+  - `follow_redirects` (bool): Whether to follow HTTP redirects (default: false).
+  - `headers` (object): Custom HTTP headers as key/value pairs. Keys must be valid identifiers (no hyphens).
+
+Returns the response body as a string.
+
+### curlPost
+`curlPost(url, data [, options]) -> string`
+
+- `url` (string): The HTTP/HTTPS URL to send the POST request.
+- `data` (string): Request body (e.g., a JSON-encoded payload).
+- `options` (object, optional): Same as for `curlGet`. If `headers` does not include a `Content-Type`,
+  `Content-Type: application/json` is automatically added.
+
+Returns the response body as a string.
+
+## Examples
+
+Basic GET:
+```vs
+string $resp = curlGet("https://jsonplaceholder.typicode.com/todos/1");
+printnl($resp);
+```
+
+GET with options:
+```vs
+string $resp = curlGet("https://jsonplaceholder.typicode.com/todos/1", {
+    timeout: 5,
+    follow_redirects: true,
+    headers: {
+        Accept: "application/json"
+    }
+});
+printnl($resp);
+```
+
+Basic POST (JSON payload):
+```vs
+object $payload = { string title: "foo", string body: "bar", int userId: 1 };
+string $json = json_encode($payload);
+string $resp = curlPost("https://jsonplaceholder.typicode.com/posts", $json);
+printnl($resp);
+```
+
+POST with options:
+```vs
+object $payload = { string name: "Alice", int age: 30 };
+string $json = json_encode($payload);
+string $resp = curlPost("https://example.com/api/users", $json, {
+    timeout: 10,
+    follow_redirects: true,
+    headers: {
+        X_Test: "CustomValue"
+    }
+});
+printnl($resp);
+```
+
+## Integration
+
+Ensure the `CurlModule` is registered before running scripts:
+```cpp
+Modules::ModuleManager::instance().addModule(
+    std::make_unique<Modules::CurlModule>()
+);
+Modules::ModuleManager::instance().registerAll();
+```
+
+Place this file alongside `CMakeLists.txt` and `src/` in the `Modules/CurlModule/` folder.
\ No newline at end of file
diff --git a/Modules/CurlModule/src/CurlModule.cpp b/Modules/CurlModule/src/CurlModule.cpp
index ed7174e..4e2748a 100644
--- a/Modules/CurlModule/src/CurlModule.cpp
+++ b/Modules/CurlModule/src/CurlModule.cpp
@@ -6,6 +6,7 @@
 #include <stdexcept>
 #include <string>
 #include <vector>
+#include <algorithm>
 
 // Callback for libcurl to write received data into a std::string
 static size_t write_callback(void* ptr, size_t size, size_t nmemb, void* userdata) {
@@ -29,58 +30,181 @@
 
 
 Symbols::Value Modules::CurlModule::curlPost(const std::vector<Symbols::Value>& args) {
-    if (args.size() != 2) {
-        throw std::runtime_error("curlPost: missing URL and data arguments");
+    // curlPost: url, data [, options]
+    if (args.size() < 2 || args.size() > 3) {
+        throw std::runtime_error("curlPost: expects url, data, and optional options object");
     }
     std::string url  = Symbols::Value::to_string(args[0]);
     std::string data = Symbols::Value::to_string(args[1]);
-
+    struct curl_slist *headers = nullptr;
+    long timeoutSec = 0;
+    bool follow = false;
+    bool haveContentType = false;
+    if (args.size() == 3) {
+        using namespace Symbols;
+        if (args[2].getType() != Variables::Type::OBJECT) {
+            throw std::runtime_error("curlPost: options must be object");
+        }
+        const auto & obj = std::get<Value::ObjectMap>(args[2].get());
+        for (const auto & kv : obj) {
+            const std::string & key = kv.first;
+            const Value & v = kv.second;
+            if (key == "timeout") {
+                using namespace Variables;
+                switch (v.getType()) {
+                    case Type::INTEGER:
+                        timeoutSec = v.get<int>(); break;
+                    case Type::DOUBLE:
+                        timeoutSec = static_cast<long>(v.get<double>()); break;
+                    case Type::FLOAT:
+                        timeoutSec = static_cast<long>(v.get<float>()); break;
+                    default:
+                        throw std::runtime_error("curlPost: timeout must be number");
+                }
+            } else if (key == "follow_redirects") {
+                if (v.getType() != Variables::Type::BOOLEAN) {
+                    throw std::runtime_error("curlPost: follow_redirects must be boolean");
+                }
+                follow = v.get<bool>();
+            } else if (key == "headers") {
+                if (v.getType() != Variables::Type::OBJECT) {
+                    throw std::runtime_error("curlPost: headers must be object");
+                }
+                const auto & hobj = std::get<Value::ObjectMap>(v.get());
+                for (const auto & hk : hobj) {
+                    if (hk.second.getType() != Variables::Type::STRING) {
+                        throw std::runtime_error("curlPost: header values must be string");
+                    }
+                    std::string hdr = hk.first + ": " + hk.second.get<std::string>();
+                    std::string lower = hk.first;
+                    std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
+                    if (lower == "content-type") {
+                        haveContentType = true;
+                    }
+                    headers = curl_slist_append(headers, hdr.c_str());
+                }
+            } else {
+                throw std::runtime_error("curlPost: unknown option '" + key + "'");
+            }
+        }
+    }
     CURL * curl = curl_easy_init();
     if (!curl) {
+        if (headers) curl_slist_free_all(headers);
         throw std::runtime_error("curl: failed to initialize");
     }
-
     std::string response;
     curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
+    if (timeoutSec > 0) {
+        curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeoutSec);
+    }
+    if (follow) {
+        curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
+    }
+    if (!haveContentType) {
+        headers = curl_slist_append(headers, "Content-Type: application/json");
+    }
+    if (headers) {
+        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+    }
     curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
     curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
     curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
-
     CURLcode res = curl_easy_perform(curl);
+    if (headers) curl_slist_free_all(headers);
     if (res != CURLE_OK) {
         std::string error = curl_easy_strerror(res);
         curl_easy_cleanup(curl);
         throw std::runtime_error("curl: request failed: " + error);
     }
-
     curl_easy_cleanup(curl);
     return Symbols::Value(response);
 }
 
 Symbols::Value Modules::CurlModule::curlGet(const std::vector<Symbols::Value>& args) {
-    if (args.size() != 1) {
-        throw std::runtime_error("curlGet: missing URL argument");
+    // curlGet: url [, options]
+    if (args.size() < 1 || args.size() > 2) {
+        throw std::runtime_error("curlGet: expects url and optional options object");
     }
-
     std::string url = Symbols::Value::to_string(args[0]);
-
+    // parse options
+    struct curl_slist *headers = nullptr;
+    long timeoutSec = 0;
+    bool follow = false;
+    if (args.size() == 2) {
+        using namespace Symbols;
+        if (args[1].getType() != Variables::Type::OBJECT) {
+            throw std::runtime_error("curlGet: options must be object");
+        }
+        const auto & obj = std::get<Value::ObjectMap>(args[1].get());
+        for (const auto & kv : obj) {
+            const std::string & key = kv.first;
+            const Value & v = kv.second;
+            if (key == "timeout") {
+                using namespace Variables;
+                switch (v.getType()) {
+                    case Type::INTEGER:
+                        timeoutSec = v.get<int>();
+                        break;
+                    case Type::DOUBLE:
+                        timeoutSec = static_cast<long>(v.get<double>());
+                        break;
+                    case Type::FLOAT:
+                        timeoutSec = static_cast<long>(v.get<float>());
+                        break;
+                    default:
+                        throw std::runtime_error("curlGet: timeout must be number");
+                }
+            } else if (key == "follow_redirects") {
+                if (v.getType() != Symbols::Variables::Type::BOOLEAN) {
+                    throw std::runtime_error("curlGet: follow_redirects must be boolean");
+                }
+                follow = v.get<bool>();
+            } else if (key == "headers") {
+                if (v.getType() != Symbols::Variables::Type::OBJECT) {
+                    throw std::runtime_error("curlGet: headers must be object");
+                }
+                const auto & hobj = std::get<Value::ObjectMap>(v.get());
+                for (const auto & hk : hobj) {
+                    if (hk.second.getType() != Symbols::Variables::Type::STRING) {
+                        throw std::runtime_error("curlGet: header values must be string");
+                    }
+                    std::string line = hk.first + ": " + hk.second.get<std::string>();
+                    headers = curl_slist_append(headers, line.c_str());
+                }
+            } else {
+                throw std::runtime_error("curlGet: unknown option '" + key + "'");
+            }
+        }
+    }
+    // initialize handle
     CURL * curl = curl_easy_init();
     if (!curl) {
+        if (headers) curl_slist_free_all(headers);
         throw std::runtime_error("curl: failed to initialize");
     }
-
     std::string response;
     curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
+    if (timeoutSec > 0) {
+        curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeoutSec);
+    }
+    if (follow) {
+        curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
+    }
+    if (headers) {
+        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+    }
     curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
     curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
-
     CURLcode res = curl_easy_perform(curl);
+    if (headers) {
+        curl_slist_free_all(headers);
+    }
     if (res != CURLE_OK) {
         std::string error = curl_easy_strerror(res);
         curl_easy_cleanup(curl);
         throw std::runtime_error("curl: request failed: " + error);
     }
-
     curl_easy_cleanup(curl);
     return Symbols::Value(response);
 }
diff --git a/Modules/CurlModule/src/CurlModule.hpp b/Modules/CurlModule/src/CurlModule.hpp
index c2d30ec..cd8192a 100644
--- a/Modules/CurlModule/src/CurlModule.hpp
+++ b/Modules/CurlModule/src/CurlModule.hpp
@@ -16,12 +16,20 @@
     void registerModule() override;
     
     /**
-     * @brief Perform HTTP GET: curlGet(url)
+     * @brief Perform HTTP GET: curlGet(url [, options])
+     * options is an object with optional fields:
+     *   timeout (int or double seconds),
+     *   follow_redirects (bool),
+     *   headers (object mapping header names to values)
      */
     Symbols::Value curlGet(const std::vector<Symbols::Value>& args);
     
     /**
-     * @brief Perform HTTP POST: curlPost(url, data)
+     * @brief Perform HTTP POST: curlPost(url, data [, options])
+     * options is an object with optional fields:
+     *   timeout (int or double seconds),
+     *   follow_redirects (bool),
+     *   headers (object mapping header names to values)
      */
     Symbols::Value curlPost(const std::vector<Symbols::Value>& args);
 };
diff --git a/src/Modules/JsonModule.hpp b/src/Modules/JsonModule.hpp
new file mode 100644
index 0000000..604b92e
--- /dev/null
+++ b/src/Modules/JsonModule.hpp
@@ -0,0 +1,239 @@
+// JsonModule.hpp
+#ifndef MODULES_JSONMODULE_HPP
+#define MODULES_JSONMODULE_HPP
+
+#include <string>
+#include <map>
+#include <variant>
+#include <cctype>
+#include <stdexcept>
+#include <sstream>
+#include "BaseModule.hpp"
+#include "ModuleManager.hpp"
+#include "Symbols/Value.hpp"
+#include "Symbols/VariableTypes.hpp"
+
+namespace Modules {
+
+/**
+ * @brief Module providing JSON encode/decode functions.
+ *   json_encode(value) -> string
+ *   json_decode(string) -> object/value
+ */
+class JsonModule : public BaseModule {
+  public:
+    void registerModule() override {
+        auto &mgr = ModuleManager::instance();
+        // json_encode: serialize a Value to JSON string
+        mgr.registerFunction("json_encode", [](const std::vector<Symbols::Value> &args) {
+            using namespace Symbols;
+            if (args.size() != 1) {
+                throw std::runtime_error("json_encode expects 1 argument");
+            }
+            // forward to encoder
+            std::function<std::string(const Value &)> encode;
+            encode = [&](const Value &v) -> std::string {
+                const auto &var = v.get();
+                return std::visit(
+                    [&](auto &&x) -> std::string {
+                        using T = std::decay_t<decltype(x)>;
+                        if constexpr (std::is_same_v<T, bool>) {
+                            return x ? "true" : "false";
+                        } else if constexpr (std::is_same_v<T, int> || std::is_same_v<T, double> || std::is_same_v<T, float>) {
+                            return std::to_string(x);
+                        } else if constexpr (std::is_same_v<T, std::string>) {
+                            // escape string
+                            std::string out = "\"";
+                            for (char c : x) {
+                                switch (c) {
+                                    case '"': out += "\\\""; break;
+                                    case '\\': out += "\\\\"; break;
+                                    case '\b': out += "\\b"; break;
+                                    case '\f': out += "\\f"; break;
+                                    case '\n': out += "\\n"; break;
+                                    case '\r': out += "\\r"; break;
+                                    case '\t': out += "\\t"; break;
+                                    default:
+                                        if (static_cast<unsigned char>(c) < 0x20) {
+                                            // control character
+                                            char buf[7];
+                                            std::snprintf(buf, sizeof(buf), "\\u%04x", c);
+                                            out += buf;
+                                        } else {
+                                            out += c;
+                                        }
+                                }
+                            }
+                            out += "\"";
+                            return out;
+                        } else if constexpr (std::is_same_v<T, Value::ObjectMap>) {
+                            std::string out = "{";
+                            bool first = true;
+                            for (const auto &kv : x) {
+                                if (!first) out += ",";
+                                first = false;
+                                // key
+                                out += '"';
+                                // escape key string
+                                for (char c : kv.first) {
+                                    switch (c) {
+                                        case '"': out += "\\\""; break;
+                                        case '\\': out += "\\\\"; break;
+                                        case '\b': out += "\\b"; break;
+                                        case '\f': out += "\\f"; break;
+                                        case '\n': out += "\\n"; break;
+                                        case '\r': out += "\\r"; break;
+                                        case '\t': out += "\\t"; break;
+                                        default:
+                                            if (static_cast<unsigned char>(c) < 0x20) {
+                                                char buf[7];
+                                                std::snprintf(buf, sizeof(buf), "\\u%04x", c);
+                                                out += buf;
+                                            } else {
+                                                out += c;
+                                            }
+                                    }
+                                }
+                                out += '"';
+                                out += ':';
+                                out += encode(kv.second);
+                            }
+                            out += "}";
+                            return out;
+                        } else {
+                            return "null";
+                        }
+                    },
+                    var);
+            };
+            std::string result = encode(args[0]);
+            return Symbols::Value(result);
+        });
+        // json_decode: parse JSON string to Value (object/value)
+        mgr.registerFunction("json_decode", [](const std::vector<Symbols::Value> &args) {
+            using namespace Symbols;
+            if (args.size() != 1) {
+                throw std::runtime_error("json_decode expects 1 argument");
+            }
+            if (args[0].getType() != Variables::Type::STRING) {
+                throw std::runtime_error("json_decode expects a JSON string");
+            }
+            const std::string s = args[0].get<std::string>();
+            struct Parser {
+                const std::string &s;
+                size_t pos = 0;
+                Parser(const std::string &str) : s(str), pos(0) {}
+                void skip() {
+                    while (pos < s.size() && std::isspace(static_cast<unsigned char>(s[pos]))) pos++;
+                }
+                std::string parseString() {
+                    skip();
+                    if (s[pos] != '"') throw std::runtime_error("Invalid JSON string");
+                    pos++;
+                    std::string out;
+                    while (pos < s.size()) {
+                        char c = s[pos++];
+                        if (c == '"') break;
+                        if (c == '\\') {
+                            if (pos >= s.size()) break;
+                            char e = s[pos++];
+                            switch (e) {
+                                case '"': out += '"'; break;
+                                case '\\': out += '\\'; break;
+                                case '/': out += '/'; break;
+                                case 'b': out += '\b'; break;
+                                case 'f': out += '\f'; break;
+                                case 'n': out += '\n'; break;
+                                case 'r': out += '\r'; break;
+                                case 't': out += '\t'; break;
+                                default: out += e; break;
+                            }
+                        } else {
+                            out += c;
+                        }
+                    }
+                    return out;
+                }
+                Value parseNumber() {
+                    skip();
+                    size_t start = pos;
+                    if (s[pos] == '-') pos++;
+                    while (pos < s.size() && std::isdigit(static_cast<unsigned char>(s[pos]))) pos++;
+                    bool isDouble = false;
+                    if (pos < s.size() && s[pos] == '.') {
+                        isDouble = true;
+                        pos++;
+                        while (pos < s.size() && std::isdigit(static_cast<unsigned char>(s[pos]))) pos++;
+                    }
+                    std::string num = s.substr(start, pos - start);
+                    try {
+                        if (isDouble) {
+                            return Value(std::stod(num));
+                        }
+                        return Value(std::stoi(num));
+                    } catch (...) {
+                        throw std::runtime_error("Invalid JSON number: " + num);
+                    }
+                }
+                Value parseBool() {
+                    skip();
+                    if (s.compare(pos, 4, "true") == 0) {
+                        pos += 4;
+                        return Value(true);
+                    } else if (s.compare(pos, 5, "false") == 0) {
+                        pos += 5;
+                        return Value(false);
+                    }
+                    throw std::runtime_error("Invalid JSON boolean");
+                }
+                Value parseNull() {
+                    skip();
+                    if (s.compare(pos, 4, "null") == 0) {
+                        pos += 4;
+                        return Value::makeNull();
+                    }
+                    throw std::runtime_error("Invalid JSON null");
+                }
+                Value parseObject() {
+                    skip();
+                    if (s[pos] != '{') throw std::runtime_error("Invalid JSON object");
+                    pos++;
+                    skip();
+                    Value::ObjectMap obj;
+                    if (s[pos] == '}') { pos++; return Value(obj); }
+                    while (pos < s.size()) {
+                        skip();
+                        std::string key = parseString();
+                        skip();
+                        if (s[pos] != ':') throw std::runtime_error("Expected ':' in object");
+                        pos++;
+                        skip();
+                        Value val = parseValue();
+                        obj.emplace(key, val);
+                        skip();
+                        if (s[pos] == ',') { pos++; continue; }
+                        if (s[pos] == '}') { pos++; break; }
+                        throw std::runtime_error("Expected ',' or '}' in object");
+                    }
+                    return Value(obj);
+                }
+                Value parseValue() {
+                    skip();
+                    if (pos >= s.size()) throw std::runtime_error("Empty JSON");
+                    char c = s[pos];
+                    if (c == '{') return parseObject();
+                    if (c == '"') { std::string str = parseString(); return Value(str); }
+                    if (c == 't' || c == 'f') return parseBool();
+                    if (c == 'n') return parseNull();
+                    if (c == '-' || std::isdigit(static_cast<unsigned char>(c))) return parseNumber();
+                    throw std::runtime_error(std::string("Invalid JSON value at pos ") + std::to_string(pos));
+                }
+            } parser(s);
+            Value result = parser.parseValue();
+            return result;
+        });
+    }
+};
+
+} // namespace Modules
+#endif // MODULES_JSONMODULE_HPP
\ No newline at end of file
diff --git a/src/VoidScript.hpp b/src/VoidScript.hpp
index af39b5f..077cd50 100644
--- a/src/VoidScript.hpp
+++ b/src/VoidScript.hpp
@@ -11,6 +11,7 @@
 #include "Modules/PrintModule.hpp"
 #include "Modules/TypeofModule.hpp"
 #include "Modules/FileModule.hpp"
+#include "Modules/JsonModule.hpp"
 #include "Parser/Parser.hpp"
 
 class VoidScript {
@@ -60,6 +61,8 @@
         Modules::ModuleManager::instance().addModule(std::make_unique<Modules::TypeofModule>());
         // file I/O builtin
         Modules::ModuleManager::instance().addModule(std::make_unique<Modules::FileModule>());
+        // JSON encode/decode builtin
+        Modules::ModuleManager::instance().addModule(std::make_unique<Modules::JsonModule>());
         this->files.emplace(this->files.begin(), file);
 
         lexer->setKeyWords(Parser::Parser::keywords);
diff --git a/test_scripts/curl_json.vs b/test_scripts/curl_json.vs
new file mode 100644
index 0000000..51e0b54
--- /dev/null
+++ b/test_scripts/curl_json.vs
@@ -0,0 +1,38 @@
+# CURL + JSON Feature Test
+# GET request
+string $url = "https://jsonplaceholder.typicode.com/todos/1";
+string $resp = curlGet($url);
+printnl("GET raw: ", $resp);
+object $data = json_decode($resp);
+printnl("ID: ", $data->id, " Title: ", $data->title, " Completed: ", $data->completed);
+
+# POST request
+string $postUrl = "https://jsonplaceholder.typicode.com/posts";
+object $payload = {
+    string title: "foo",
+    string body: "bar",
+    int userId: 1
+};
+string $postData = json_encode($payload);
+printnl("POST data: ", $postData);
+string $postResp = curlPost($postUrl, $postData);
+printnl("POST raw: ", $postResp);
+object $postJson = json_decode($postResp);
+// The response from JSONPlaceholder includes only the new id
+printnl("POST ID: ", $postJson->id);
+// --- GET with options ---
+printnl("GET with options (timeout=5, follow_redirects, Accept header):");
+string $optGet = curlGet($url, {
+    timeout: 5,
+    follow_redirects: true,
+    headers: { Accept: "application/json" }
+});
+printnl($optGet);
+// --- POST with options ---
+printnl("POST with options (timeout=5, follow_redirects, X_Test header):");
+string $optPost = curlPost($postUrl, $postData, {
+    timeout: 5,
+    follow_redirects: true,
+    headers: { X_Test: "CustomValue" }
+});
+printnl($optPost);
\ No newline at end of file
diff --git a/test_scripts/json.vs b/test_scripts/json.vs
new file mode 100644
index 0000000..ae83897
--- /dev/null
+++ b/test_scripts/json.vs
@@ -0,0 +1,49 @@
+# JSON Encode/Decode Feature Test
+# Define an object with nested data
+object $user = {
+    string name: "Alice",
+    int age: 30,
+    boolean active: true,
+    object prefs: {
+        string theme: "dark",
+        boolean notifications: false
+    }
+};
+
+// Encode to JSON string
+string $json = json_encode($user);
+printnl("Encoded JSON: ", $json);
+
+// Decode back to object
+object $parsed = json_decode($json);
+// Re-encode to verify round-trip
+string $json2 = json_encode($parsed);
+printnl("Re-encoded JSON: ", $json2);
+// --- Simple value tests ---
+// Integer
+int $num = 42;
+string $num_json = json_encode($num);
+printnl("Encoded integer: ", $num_json);
+int $num_decoded = json_decode($num_json);
+printnl("Decoded integer: ", $num_decoded);
+
+// String
+string $str = "Hello, VoidScript!";
+string $str_json = json_encode($str);
+printnl("Encoded string: ", $str_json);
+string $str_decoded = json_decode($str_json);
+printnl("Decoded string: ", $str_decoded);
+
+// Boolean
+boolean $flag = true;
+string $flag_json = json_encode($flag);
+printnl("Encoded boolean: ", $flag_json);
+boolean $flag_decoded = json_decode($flag_json);
+printnl("Decoded boolean: ", $flag_decoded);
+
+// Double
+double $pi = 3.14159;
+string $pi_json = json_encode($pi);
+printnl("Encoded double: ", $pi_json);
+double $pi_decoded = json_decode($pi_json);
+printnl("Decoded double: ", $pi_decoded);
\ No newline at end of file

--
Gitblit v1.9.3