From 058188299db4c5e73f913609b8db58770d4b6650 Mon Sep 17 00:00:00 2001 From: Face <69168154+face-hh@users.noreply.github.com> Date: Sat, 9 Aug 2025 14:03:46 +0300 Subject: [PATCH] JSON API, fetch() API --- flumi/Scripts/Constants.gd | 236 +++++++++++++++++++++++- flumi/Scripts/Utils/Lua/JSON.gd | 37 ++++ flumi/Scripts/Utils/Lua/JSON.gd.uid | 1 + flumi/Scripts/Utils/Lua/Network.gd | 240 +++++++++++++++++++++++++ flumi/Scripts/Utils/Lua/Network.gd.uid | 1 + flumi/Scripts/Utils/Lua/ThreadedVM.gd | 6 + 6 files changed, 520 insertions(+), 1 deletion(-) create mode 100644 flumi/Scripts/Utils/Lua/JSON.gd create mode 100644 flumi/Scripts/Utils/Lua/JSON.gd.uid create mode 100644 flumi/Scripts/Utils/Lua/Network.gd create mode 100644 flumi/Scripts/Utils/Lua/Network.gd.uid diff --git a/flumi/Scripts/Constants.gd b/flumi/Scripts/Constants.gd index e6ba3c9..c1fd4f4 100644 --- a/flumi/Scripts/Constants.gd +++ b/flumi/Scripts/Constants.gd @@ -870,7 +870,7 @@ var HTML_CONTENT_DOM_MANIPULATION = """ """.to_utf8_buffer() -var HTML_CONTENT = """ +var HTML_CONTENTc = """ Input Events API Demo @@ -1567,3 +1567,237 @@ var HTML_CONTENTa = """ """.to_utf8_buffer() + +var HTML_CONTENT = """ + Network & JSON API Demo + + + + + + + + + + +

๐ŸŒ Network & JSON API Demo

+ +
+
+

API Examples:

+

Network: fetch(url, {method: "POST", headers: {...}, body: "..."})

+

JSON: JSON.stringify(data) and JSON.parse(jsonString)

+
+ +

Input Controls

+
+
+
+ + +
+
+ + +
+
+
+ +

Network Tests

+
+ + +
+ +

JSON Tests

+
+ + +
+ +
+ +
+ +

Response Log

+
+
Initializing...
+
+ +
+

Network API Features:

+
    +
  • fetch(url, options): Makes HTTP requests with support for all methods
  • +
  • Response methods: text(), json(), ok() for processing responses
  • +
  • Headers & Body: Full control over request headers and body content
  • +
  • Status & StatusText: Access to HTTP response status information
  • +
+

JSON API Features:

+
    +
  • JSON.stringify(): Alias for encode (browser compatibility)
  • +
  • JSON.parse(): Alias for decode (browser compatibility)
  • +
+
+
+ +""".to_utf8_buffer() diff --git a/flumi/Scripts/Utils/Lua/JSON.gd b/flumi/Scripts/Utils/Lua/JSON.gd new file mode 100644 index 0000000..017cf53 --- /dev/null +++ b/flumi/Scripts/Utils/Lua/JSON.gd @@ -0,0 +1,37 @@ +class_name LuaJSONUtils +extends RefCounted + +static func setup_json_api(vm: LuauVM): + vm.lua_newtable() + + vm.lua_pushcallable(_lua_json_parse_handler, "JSON.parse") + vm.lua_setfield(-2, "parse") + + vm.lua_pushcallable(_lua_json_stringify_handler, "JSON.stringify") + vm.lua_setfield(-2, "stringify") + + vm.lua_setglobal("JSON") + +static func _lua_json_parse_handler(vm: LuauVM) -> int: + var json_string: String = vm.luaL_checkstring(1) + + var json = JSON.new() + var parse_result = json.parse(json_string) + + if parse_result == OK: + vm.lua_pushvariant(json.data) + return 1 + else: + # Return nil and error message + vm.lua_pushnil() + vm.lua_pushstring("JSON parse error: " + json.get_error_message()) + return 2 + +static func _lua_json_stringify_handler(vm: LuauVM) -> int: + var value = vm.lua_tovariant(1) + + var json = JSON.new() + var json_string = json.stringify(value) + + vm.lua_pushstring(json_string) + return 1 diff --git a/flumi/Scripts/Utils/Lua/JSON.gd.uid b/flumi/Scripts/Utils/Lua/JSON.gd.uid new file mode 100644 index 0000000..3a9c8b1 --- /dev/null +++ b/flumi/Scripts/Utils/Lua/JSON.gd.uid @@ -0,0 +1 @@ +uid://caxwx127g48l3 diff --git a/flumi/Scripts/Utils/Lua/Network.gd b/flumi/Scripts/Utils/Lua/Network.gd new file mode 100644 index 0000000..a5d866b --- /dev/null +++ b/flumi/Scripts/Utils/Lua/Network.gd @@ -0,0 +1,240 @@ +class_name LuaNetworkUtils +extends RefCounted + +static func setup_network_api(vm: LuauVM): + vm.lua_pushcallable(_lua_fetch_handler, "fetch") + vm.lua_setglobal("fetch") + +static func _lua_fetch_handler(vm: LuauVM) -> int: + var url: String = vm.luaL_checkstring(1) + var options: Dictionary = {} + + if vm.lua_gettop() >= 2 and vm.lua_istable(2): + options = vm.lua_todictionary(2) + + # Default options + var method = options.get("method", "GET").to_upper() + var headers = options.get("headers", {}) + var body = options.get("body", "") + + # Set request options + var headers_array: PackedStringArray = [] + for header_name in headers: + headers_array.append(str(header_name) + ": " + str(headers[header_name])) + + var response_data = make_http_request(url, method, headers_array, body) + + # Create response object with actual data + vm.lua_newtable() + + # Add response properties + vm.lua_pushinteger(response_data.status) + vm.lua_setfield(-2, "status") + + vm.lua_pushstring(response_data.status_text) + vm.lua_setfield(-2, "statusText") + + # Convert response headers to table + vm.lua_newtable() + for header_name in response_data.headers: + vm.lua_pushstring(response_data.headers[header_name]) + vm.lua_setfield(-2, header_name.to_lower()) + vm.lua_setfield(-2, "headers") + + # Store response body + vm.lua_pushstring(response_data.body) + vm.lua_setfield(-2, "_response_body") + + # Add response methods + vm.lua_pushcallable(_response_text_handler, "response.text") + vm.lua_setfield(-2, "text") + + vm.lua_pushcallable(_response_json_handler, "response.json") + vm.lua_setfield(-2, "json") + + vm.lua_pushcallable(_response_ok_handler, "response.ok") + vm.lua_setfield(-2, "ok") + + return 1 + +static func _response_text_handler(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + + vm.lua_getfield(1, "_response_body") + var response_text = vm.lua_tostring(-1) + vm.lua_pop(1) + + vm.lua_pushstring(response_text) + return 1 + +static func _response_json_handler(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + + vm.lua_getfield(1, "_response_body") + var response_text = vm.lua_tostring(-1) + vm.lua_pop(1) + + var json = JSON.new() + var parse_result = json.parse(response_text) + + if parse_result == OK: + vm.lua_pushvariant(json.data) + else: + vm.luaL_error("Invalid JSON in response") + + return 1 + +static func _response_ok_handler(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + + vm.lua_getfield(1, "status") + var status = vm.lua_tointeger(-1) + vm.lua_pop(1) + + vm.lua_pushboolean(status >= 200 and status < 300) + return 1 + +static func make_http_request(url: String, method: String, headers: PackedStringArray, body: String) -> Dictionary: + var http_client = HTTPClient.new() + var response_data = { + "status": 0, + "status_text": "Network Error", + "headers": {}, + "body": "" + } + + # Parse URL + var url_parts = url.split("://") + if url_parts.size() < 2: + response_data.status = 400 + response_data.status_text = "Bad Request - Invalid URL" + return response_data + + var protocol = url_parts[0].to_lower() + var use_ssl = (protocol == "https" or protocol == "gurt+ssl") + var remaining_url = url_parts[1] + + var host_path = remaining_url.split("/", false, 1) + var host = host_path[0] + var path = "/" + (host_path[1] if host_path.size() > 1 else "") + + # Extract port if specified + var port = 80 + if use_ssl: + port = 443 + + var host_parts = host.split(":") + if host_parts.size() > 1: + host = host_parts[0] + port = host_parts[1].to_int() + + # Connect to host + var error: Error + if use_ssl: + var tls_options = TLSOptions.client() + error = http_client.connect_to_host(host, port, tls_options) + else: + error = http_client.connect_to_host(host, port) + if error != OK: + response_data.status = 0 + response_data.status_text = "Connection Failed" + return response_data + + # Wait for connection + var timeout_count = 0 + while http_client.get_status() == HTTPClient.STATUS_CONNECTING or http_client.get_status() == HTTPClient.STATUS_RESOLVING: + http_client.poll() + OS.delay_msec(10) + timeout_count += 1 + if timeout_count > 300: # 3 second timeout + response_data.status = 0 + response_data.status_text = "Connection Timeout" + return response_data + + if http_client.get_status() != HTTPClient.STATUS_CONNECTED: + response_data.status = 0 + response_data.status_text = "Connection Failed" + return response_data + + # Convert method string to HTTPClient.Method enum + var http_method: HTTPClient.Method + match method: + "GET": + http_method = HTTPClient.METHOD_GET + "POST": + http_method = HTTPClient.METHOD_POST + "PUT": + http_method = HTTPClient.METHOD_PUT + "DELETE": + http_method = HTTPClient.METHOD_DELETE + "HEAD": + http_method = HTTPClient.METHOD_HEAD + "OPTIONS": + http_method = HTTPClient.METHOD_OPTIONS + "PATCH": + http_method = HTTPClient.METHOD_PATCH + _: + http_method = HTTPClient.METHOD_GET + + # Make request + error = http_client.request(http_method, path, headers, body) + if error != OK: + response_data.status = 0 + response_data.status_text = "Request Failed" + return response_data + + # Wait for response + timeout_count = 0 + while http_client.get_status() == HTTPClient.STATUS_REQUESTING: + http_client.poll() + OS.delay_msec(10) + timeout_count += 1 + if timeout_count > 1000: # 10 second timeout + response_data.status = 0 + response_data.status_text = "Request Timeout" + return response_data + + if http_client.get_status() != HTTPClient.STATUS_BODY and http_client.get_status() != HTTPClient.STATUS_CONNECTED: + response_data.status = 0 + response_data.status_text = "Request Failed" + return response_data + + # Get response + if http_client.has_response(): + response_data.status = http_client.get_response_code() + + # Get status text + match response_data.status: + 200: response_data.status_text = "OK" + 201: response_data.status_text = "Created" + 204: response_data.status_text = "No Content" + 400: response_data.status_text = "Bad Request" + 401: response_data.status_text = "Unauthorized" + 403: response_data.status_text = "Forbidden" + 404: response_data.status_text = "Not Found" + 500: response_data.status_text = "Internal Server Error" + _: response_data.status_text = "Unknown" + + # Get response headers + var response_headers = http_client.get_response_headers_as_dictionary() + response_data.headers = response_headers + + # Get response body + var body_bytes = PackedByteArray() + timeout_count = 0 + while http_client.get_status() == HTTPClient.STATUS_BODY: + http_client.poll() + var chunk = http_client.read_response_body_chunk() + if chunk.size() > 0: + body_bytes.append_array(chunk) + timeout_count = 0 + else: + OS.delay_msec(10) + timeout_count += 1 + if timeout_count > 1000: # 10 second timeout for body + break + + response_data.body = body_bytes.get_string_from_utf8() + + http_client.close() + return response_data diff --git a/flumi/Scripts/Utils/Lua/Network.gd.uid b/flumi/Scripts/Utils/Lua/Network.gd.uid new file mode 100644 index 0000000..ed77912 --- /dev/null +++ b/flumi/Scripts/Utils/Lua/Network.gd.uid @@ -0,0 +1 @@ +uid://ffy8hyqtivhg diff --git a/flumi/Scripts/Utils/Lua/ThreadedVM.gd b/flumi/Scripts/Utils/Lua/ThreadedVM.gd index 520d7e1..ae379e6 100644 --- a/flumi/Scripts/Utils/Lua/ThreadedVM.gd +++ b/flumi/Scripts/Utils/Lua/ThreadedVM.gd @@ -325,6 +325,12 @@ func _setup_additional_lua_apis(): # Setup Clipboard API for threaded execution LuaClipboardUtils.setup_clipboard_api(lua_vm) + + # Setup Network API for threaded execution + LuaNetworkUtils.setup_network_api(lua_vm) + + # Setup JSON API for threaded execution + LuaJSONUtils.setup_json_api(lua_vm) func _threaded_table_tostring_handler(vm: LuauVM) -> int: vm.luaL_checktype(1, vm.LUA_TTABLE)