From c2f67a0dcc1ffa0a38907b995dfce025403fd9a3 Mon Sep 17 00:00:00 2001 From: dev0 Date: Sun, 30 Nov 2025 05:02:36 +0530 Subject: [PATCH] Added HttpClient, DataOps and JSON. Upgraded dep handling --- .gitmodules | 9 -- CMake/FindDeps.cmake | 127 +++++++++++++++++ CMakeLists.txt | 6 +- Src/IACore/CMakeLists.txt | 25 +++- Src/IACore/imp/cpp/DataOps.cpp | 168 ++++++++++++++++++++++ Src/IACore/imp/cpp/HttpClient.cpp | 205 +++++++++++++++++++++++++++ Src/IACore/imp/cpp/JSON.cpp | 43 ++++++ Src/IACore/inc/IACore/DataOps.hpp | 32 +++++ Src/IACore/inc/IACore/HttpClient.hpp | 195 +++++++++++++++++++++++++ Src/IACore/inc/IACore/JSON.hpp | 58 ++++++++ Src/IACore/inc/IACore/PCH.hpp | 2 + Vendor/CMakeLists.txt | 10 -- Vendor/expected | 1 - Vendor/mimalloc | 1 - Vendor/unordered_dense | 1 - 15 files changed, 855 insertions(+), 28 deletions(-) delete mode 100644 .gitmodules create mode 100644 CMake/FindDeps.cmake create mode 100644 Src/IACore/imp/cpp/DataOps.cpp create mode 100644 Src/IACore/imp/cpp/HttpClient.cpp create mode 100644 Src/IACore/imp/cpp/JSON.cpp create mode 100644 Src/IACore/inc/IACore/DataOps.hpp create mode 100644 Src/IACore/inc/IACore/HttpClient.hpp create mode 100644 Src/IACore/inc/IACore/JSON.hpp delete mode 100644 Vendor/CMakeLists.txt delete mode 160000 Vendor/expected delete mode 160000 Vendor/mimalloc delete mode 160000 Vendor/unordered_dense diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index a7d2ac2..0000000 --- a/.gitmodules +++ /dev/null @@ -1,9 +0,0 @@ -[submodule "Vendor/mimalloc"] - path = Vendor/mimalloc - url = https://github.com/microsoft/mimalloc -[submodule "Vendor/expected"] - path = Vendor/expected - url = https://github.com/TartanLlama/expected -[submodule "Vendor/unordered_dense"] - path = Vendor/unordered_dense - url = https://github.com/martinus/unordered_dense diff --git a/CMake/FindDeps.cmake b/CMake/FindDeps.cmake new file mode 100644 index 0000000..d9347b4 --- /dev/null +++ b/CMake/FindDeps.cmake @@ -0,0 +1,127 @@ +include(FetchContent) + +FetchContent_Declare( + httplib + GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git + GIT_TAG v0.14.3 + SYSTEM + EXCLUDE_FROM_ALL + OVERRIDE_FIND_PACKAGE +) + +FetchContent_Declare( + OpenSSL + GIT_REPOSITORY https://github.com/janbar/openssl-cmake.git + GIT_TAG master + SYSTEM + EXCLUDE_FROM_ALL + OVERRIDE_FIND_PACKAGE +) + +FetchContent_Declare( + nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.11.3 + SYSTEM + EXCLUDE_FROM_ALL + OVERRIDE_FIND_PACKAGE +) + +FetchContent_Declare( + glaze + GIT_REPOSITORY https://github.com/stephenberry/glaze.git + GIT_TAG v4.3.1 + SYSTEM + EXCLUDE_FROM_ALL + OVERRIDE_FIND_PACKAGE +) + +FetchContent_Declare( + simdjson + GIT_REPOSITORY https://github.com/simdjson/simdjson.git + GIT_TAG v3.11.0 + SYSTEM + EXCLUDE_FROM_ALL + OVERRIDE_FIND_PACKAGE +) + +FetchContent_Declare( + ZLIB + GIT_REPOSITORY https://github.com/madler/zlib.git + GIT_TAG v1.3.1 + SYSTEM + EXCLUDE_FROM_ALL + OVERRIDE_FIND_PACKAGE +) + +FetchContent_Declare( + zstd + GIT_REPOSITORY https://github.com/facebook/zstd.git + GIT_TAG v1.5.6 + SOURCE_SUBDIR build/cmake + SYSTEM + EXCLUDE_FROM_ALL + OVERRIDE_FIND_PACKAGE +) + +FetchContent_Declare( + mimalloc + GIT_REPOSITORY https://github.com/microsoft/mimalloc.git + GIT_TAG v2.1.7 + SYSTEM + EXCLUDE_FROM_ALL + OVERRIDE_FIND_PACKAGE +) + +FetchContent_Declare( + tl-expected + GIT_REPOSITORY https://github.com/TartanLlama/expected.git + GIT_TAG v1.1.0 + SYSTEM + EXCLUDE_FROM_ALL + OVERRIDE_FIND_PACKAGE +) + +FetchContent_Declare( + unordered_dense + GIT_REPOSITORY https://github.com/martinus/unordered_dense.git + GIT_TAG v4.4.0 + SYSTEM + EXCLUDE_FROM_ALL + OVERRIDE_FIND_PACKAGE +) + +find_package(ZLIB REQUIRED) +if(TARGET zlibstatic AND NOT TARGET ZLIB::ZLIB) + add_library(ZLIB::ZLIB ALIAS zlibstatic) +elseif(TARGET zlib AND NOT TARGET ZLIB::ZLIB) + add_library(ZLIB::ZLIB ALIAS zlib) +endif() + +find_package(zstd REQUIRED) +find_package(glaze REQUIRED) +find_package(simdjson REQUIRED) +find_package(nlohmann_json REQUIRED) +find_package(unordered_dense REQUIRED) + +find_package(OpenSSL REQUIRED) +if(TARGET ssl AND NOT TARGET OpenSSL::SSL) + add_library(OpenSSL::SSL ALIAS ssl) + message(STATUS "Patched OpenSSL::SSL alias for Curl") +endif() +if(TARGET crypto AND NOT TARGET OpenSSL::Crypto) + add_library(OpenSSL::Crypto ALIAS crypto) + message(STATUS "Patched OpenSSL::Crypto alias for Curl") +endif() + +set(MI_BUILD_SHARED ON CACHE BOOL "" FORCE) +set(MI_BUILD_STATIC ON CACHE BOOL "" FORCE) +set(MI_BUILD_TESTS OFF CACHE BOOL "" FORCE) +find_package(mimalloc REQUIRED) + +set(EXPECTED_BUILD_TESTS OFF CACHE BOOL "" FORCE) +find_package(tl-expected REQUIRED) + +set(HTTPLIB_REQUIRE_OPENSSL ON CACHE BOOL "" FORCE) +set(HTTPLIB_REQUIRE_ZLIB ON CACHE BOOL "" FORCE) +find_package(httplib REQUIRED) diff --git a/CMakeLists.txt b/CMakeLists.txt index 01e92d4..9e2d8c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,8 @@ project(IACore) enable_language(C) +include(CMake/FindDeps.cmake) + # Default to ON if root, OFF if dependency option(IACore_BUILD_TESTS "Build unit tests" ${PROJECT_IS_TOP_LEVEL}) @@ -54,10 +56,6 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU" ) endif() -add_definitions(-D_CRT_SECURE_NO_WARNINGS) - -add_subdirectory(Vendor/) - add_subdirectory(Src/) if(IACore_BUILD_TESTS) diff --git a/Src/IACore/CMakeLists.txt b/Src/IACore/CMakeLists.txt index fa687b2..dde91b4 100644 --- a/Src/IACore/CMakeLists.txt +++ b/Src/IACore/CMakeLists.txt @@ -1,11 +1,14 @@ set(SRC_FILES + "imp/cpp/JSON.cpp" "imp/cpp/IACore.cpp" "imp/cpp/Logger.cpp" "imp/cpp/FileOps.cpp" "imp/cpp/AsyncOps.cpp" + "imp/cpp/DataOps.cpp" "imp/cpp/SocketOps.cpp" "imp/cpp/StringOps.cpp" "imp/cpp/ProcessOps.cpp" + "imp/cpp/HttpClient.cpp" "imp/cpp/StreamReader.cpp" "imp/cpp/StreamWriter.cpp" ) @@ -15,7 +18,20 @@ add_library(IACore STATIC ${SRC_FILES}) target_include_directories(IACore PUBLIC inc/) target_include_directories(IACore PRIVATE imp/hpp/) -target_link_libraries(IACore PUBLIC tl::expected unordered_dense::unordered_dense) +target_link_libraries(IACore PUBLIC + libzstd_static + ZLIB::ZLIB + tl::expected + glaze::glaze + simdjson::simdjson + nlohmann_json::nlohmann_json + unordered_dense::unordered_dense +) + +target_link_libraries(IACore PRIVATE + httplib::httplib + OpenSSL::SSL +) if(WIN32) target_link_libraries(IACore PUBLIC mimalloc-static) @@ -34,4 +50,9 @@ target_compile_options(IACore INTERFACE define_property(TARGET PROPERTY USE_EXCEPTIONS BRIEF_DOCS "If ON, this target is allowed to use C++ exceptions." FULL_DOCS "Prevents IACore from propagating -fno-exceptions to this target." -) \ No newline at end of file +) + +target_compile_definitions(IACore PRIVATE + CPPHTTPLIB_OPENSSL_SUPPORT + CPPHTTPLIB_ZLIB_SUPPORT +) diff --git a/Src/IACore/imp/cpp/DataOps.cpp b/Src/IACore/imp/cpp/DataOps.cpp new file mode 100644 index 0000000..30f43e5 --- /dev/null +++ b/Src/IACore/imp/cpp/DataOps.cpp @@ -0,0 +1,168 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include + +#include +#include + +namespace IACore +{ + Expected, String> DataOps::ZlibInflate(IN Span data) + { + z_stream zs{}; + zs.zalloc = Z_NULL; + zs.zfree = Z_NULL; + zs.opaque = Z_NULL; + + if (inflateInit2(&zs, 15 + 32) != Z_OK) + return MakeUnexpected("Failed to initialize zlib inflate"); + + zs.next_in = const_cast(data.data()); + zs.avail_in = static_cast(data.size()); + + Vector outBuffer; + outBuffer.resize(data.size() * 2); + + int ret; + do + { + if (zs.total_out >= outBuffer.size()) + { + outBuffer.resize(outBuffer.size() * 2); + } + + zs.next_out = reinterpret_cast(outBuffer.data() + zs.total_out); + zs.avail_out = static_cast(outBuffer.size() - zs.total_out); + + ret = inflate(&zs, Z_NO_FLUSH); + + } while (ret == Z_OK); + + inflateEnd(&zs); + + if (ret != Z_STREAM_END) + return MakeUnexpected("Failed to inflate, corrupt or incomplete data"); + + outBuffer.resize(zs.total_out); + return outBuffer; + } + + Expected, String> DataOps::ZlibDeflate(IN Span data) + { + z_stream zs{}; + zs.zalloc = Z_NULL; + zs.zfree = Z_NULL; + zs.opaque = Z_NULL; + + if (deflateInit(&zs, Z_DEFAULT_COMPRESSION) != Z_OK) + return MakeUnexpected("Failed to initialize zlib deflate"); + + zs.next_in = const_cast(data.data()); + zs.avail_in = static_cast(data.size()); + + Vector outBuffer; + + outBuffer.resize(deflateBound(&zs, data.size())); + + zs.next_out = reinterpret_cast(outBuffer.data()); + zs.avail_out = static_cast(outBuffer.size()); + + int ret = deflate(&zs, Z_FINISH); + + if (ret != Z_STREAM_END) + { + deflateEnd(&zs); + return MakeUnexpected("Failed to deflate, ran out of buffer memory"); + } + + outBuffer.resize(zs.total_out); + + deflateEnd(&zs); + return outBuffer; + } + + Expected, String> DataOps::ZstdInflate(IN Span data) + { + unsigned long long const contentSize = ZSTD_getFrameContentSize(data.data(), data.size()); + + if (contentSize == ZSTD_CONTENTSIZE_ERROR) + return MakeUnexpected("Failed to inflate: Not valid ZSTD compressed data"); + + if (contentSize != ZSTD_CONTENTSIZE_UNKNOWN) + { + // FAST PATH: We know the size + Vector outBuffer; + outBuffer.resize(static_cast(contentSize)); + + size_t const dSize = ZSTD_decompress(outBuffer.data(), outBuffer.size(), data.data(), data.size()); + + if (ZSTD_isError(dSize)) + return MakeUnexpected(std::format("Failed to inflate: {}", ZSTD_getErrorName(dSize))); + + return outBuffer; + } + + ZSTD_DCtx *dctx = ZSTD_createDCtx(); + Vector outBuffer; + outBuffer.resize(data.size() * 2); + + ZSTD_inBuffer input = {data.data(), data.size(), 0}; + ZSTD_outBuffer output = {outBuffer.data(), outBuffer.size(), 0}; + + size_t ret; + do + { + ret = ZSTD_decompressStream(dctx, &output, &input); + + if (ZSTD_isError(ret)) + { + ZSTD_freeDCtx(dctx); + return MakeUnexpected(std::format("Failed to inflate: {}", ZSTD_getErrorName(ret))); + } + + if (output.pos == output.size) + { + size_t newSize = outBuffer.size() * 2; + outBuffer.resize(newSize); + output.dst = outBuffer.data(); + output.size = newSize; + } + + } while (ret != 0); + + outBuffer.resize(output.pos); + ZSTD_freeDCtx(dctx); + + return outBuffer; + } + + Expected, String> DataOps::ZstdDeflate(IN Span data) + { + size_t const maxDstSize = ZSTD_compressBound(data.size()); + + Vector outBuffer; + outBuffer.resize(maxDstSize); + + size_t const compressedSize = ZSTD_compress(outBuffer.data(), maxDstSize, data.data(), data.size(), 3); + + if (ZSTD_isError(compressedSize)) + return MakeUnexpected(std::format("Failed to deflate: {}", ZSTD_getErrorName(compressedSize))); + + outBuffer.resize(compressedSize); + return outBuffer; + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/HttpClient.cpp b/Src/IACore/imp/cpp/HttpClient.cpp new file mode 100644 index 0000000..0258f18 --- /dev/null +++ b/Src/IACore/imp/cpp/HttpClient.cpp @@ -0,0 +1,205 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include + +#include + +namespace IACore +{ + httplib::Headers BuildHeaders(IN Span headers, IN PCCHAR defaultContentType) + { + httplib::Headers out; + bool hasContentType = false; + + for (const auto &h : headers) + { + std::string key = HttpClient::HeaderTypeToString(h.first); // Your existing helper + out.emplace(key, h.second); + + if (h.first == HttpClient::EHeaderType::CONTENT_TYPE) + hasContentType = true; + } + + if (!hasContentType && defaultContentType) + out.emplace("Content-Type", defaultContentType); + return out; + } + + HttpClient::HttpClient(IN CONST String &host) + : m_client(new httplib::Client(host)), m_lastResponseCode(EResponseCode::INTERNAL_SERVER_ERROR) + { + } + + HttpClient::~HttpClient() + { + } + + Expected HttpClient::RawGet(IN CONST String &path, IN Span headers, + IN PCCHAR defaultContentType) + { + auto httpHeaders = BuildHeaders(headers, defaultContentType); + + static_cast(m_client)->enable_server_certificate_verification(false); + auto res = static_cast(m_client)->Get(path.c_str(), httpHeaders); + + if (res) + { + m_lastResponseCode = static_cast(res->status); + if (res->status >= 200 && res->status < 300) + return res->body; + else + return MakeUnexpected(std::format("HTTP Error {}", res->status)); + } + + return MakeUnexpected(std::format("Network Error: {}", httplib::to_string(res.error()))); + } + + Expected HttpClient::RawPost(IN CONST String &path, IN Span headers, + IN CONST String &body, IN PCCHAR defaultContentType) + { + auto httpHeaders = BuildHeaders(headers, defaultContentType); + + String contentType = defaultContentType; + if (httpHeaders.count("Content-Type")) + { + contentType = httpHeaders.find("Content-Type")->second; + } + + static_cast(m_client)->enable_server_certificate_verification(false); + auto res = static_cast(m_client)->Post(path.c_str(), httpHeaders, body, contentType.c_str()); + + if (res) + { + m_lastResponseCode = static_cast(res->status); + if (res->status >= 200 && res->status < 300) + return res->body; + else + return MakeUnexpected(std::format("HTTP Error {}", res->status)); + } + + return MakeUnexpected(std::format("Network Error: {}", httplib::to_string(res.error()))); + } +} // namespace IACore + +namespace IACore +{ + HttpClient::Header HttpClient::CreateHeader(IN EHeaderType key, IN CONST String &value) + { + return std::make_pair(key, value); + } + + String HttpClient::UrlEncode(IN CONST String &value) + { + std::stringstream escaped; + escaped.fill('0'); + escaped << std::hex << std::uppercase; + + for (char c : value) + { + if (std::isalnum(static_cast(c)) || c == '-' || c == '_' || c == '.' || c == '~') + escaped << c; + else + escaped << '%' << std::setw(2) << static_cast(static_cast(c)); + } + + return escaped.str(); + } + + String HttpClient::UrlDecode(IN CONST String &value) + { + String result; + result.reserve(value.length()); + + for (size_t i = 0; i < value.length(); ++i) + { + if (value[i] == '%' && i + 2 < value.length()) + { + std::string hexStr = value.substr(i + 1, 2); + char decodedChar = static_cast(std::strtol(hexStr.c_str(), nullptr, 16)); + result += decodedChar; + i += 2; + } + else if (value[i] == '+') + result += ' '; + else + result += value[i]; + } + + return result; + } + + String HttpClient::HeaderTypeToString(IN EHeaderType type) + { + switch (type) + { + case EHeaderType::ACCEPT: + return "Accept"; + case EHeaderType::ACCEPT_CHARSET: + return "Accept-Charset"; + case EHeaderType::ACCEPT_ENCODING: + return "Accept-Encoding"; + case EHeaderType::ACCEPT_LANGUAGE: + return "Accept-Language"; + case EHeaderType::AUTHORIZATION: + return "Authorization"; + case EHeaderType::CACHE_CONTROL: + return "Cache-Control"; + case EHeaderType::CONNECTION: + return "Connection"; + case EHeaderType::CONTENT_LENGTH: + return "Content-Length"; + case EHeaderType::CONTENT_TYPE: + return "Content-Type"; + case EHeaderType::COOKIE: + return "Cookie"; + case EHeaderType::DATE: + return "Date"; + case EHeaderType::EXPECT: + return "Expect"; + case EHeaderType::HOST: + return "Host"; + case EHeaderType::IF_MATCH: + return "If-Match"; + case EHeaderType::IF_MODIFIED_SINCE: + return "If-Modified-Since"; + case EHeaderType::IF_NONE_MATCH: + return "If-None-Match"; + case EHeaderType::ORIGIN: + return "Origin"; + case EHeaderType::PRAGMA: + return "Pragma"; + case EHeaderType::PROXY_AUTHORIZATION: + return "Proxy-Authorization"; + case EHeaderType::RANGE: + return "Range"; + case EHeaderType::REFERER: + return "Referer"; + case EHeaderType::TE: + return "TE"; + case EHeaderType::UPGRADE: + return "Upgrade"; + case EHeaderType::USER_AGENT: + return "User-Agent"; + case EHeaderType::VIA: + return "Via"; + case EHeaderType::WARNING: + return "Warning"; + default: + return ""; + } + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/JSON.cpp b/Src/IACore/imp/cpp/JSON.cpp new file mode 100644 index 0000000..0f63f06 --- /dev/null +++ b/Src/IACore/imp/cpp/JSON.cpp @@ -0,0 +1,43 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include + +namespace IACore +{ + Expected JSON::Parse(IN CONST String &json) + { + const auto parseResult = nlohmann::json::parse(json, nullptr, false, true); + if (parseResult.is_discarded()) + return MakeUnexpected("Failed to parse JSON"); + return parseResult; + } + + Expected JSON::ParseReadOnly(IN CONST String &json) + { + simdjson::error_code error{}; + simdjson::dom::parser parser; + simdjson::dom::object object; + if ((error = parser.parse(json).get(object))) + return MakeUnexpected(std::format("Failed to parse JSON : {}", simdjson::error_message(error))); + return object; + } + + String JSON::Encode(IN nlohmann::json data) + { + return data.dump(); + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/DataOps.hpp b/Src/IACore/inc/IACore/DataOps.hpp new file mode 100644 index 0000000..b0d5050 --- /dev/null +++ b/Src/IACore/inc/IACore/DataOps.hpp @@ -0,0 +1,32 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include + +namespace IACore +{ + class DataOps + { + public: + STATIC Expected, String> ZlibInflate(IN Span data); + STATIC Expected, String> ZlibDeflate(IN Span data); + + STATIC Expected, String> ZstdInflate(IN Span data); + STATIC Expected, String> ZstdDeflate(IN Span data); + }; +} \ No newline at end of file diff --git a/Src/IACore/inc/IACore/HttpClient.hpp b/Src/IACore/inc/IACore/HttpClient.hpp new file mode 100644 index 0000000..0cd85ee --- /dev/null +++ b/Src/IACore/inc/IACore/HttpClient.hpp @@ -0,0 +1,195 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include + +namespace IACore +{ + class HttpClient + { + public: + enum class EHeaderType + { + ACCEPT, + ACCEPT_CHARSET, + ACCEPT_ENCODING, + ACCEPT_LANGUAGE, + AUTHORIZATION, + CACHE_CONTROL, + CONNECTION, + CONTENT_LENGTH, + CONTENT_TYPE, + COOKIE, + DATE, + EXPECT, + HOST, + IF_MATCH, + IF_MODIFIED_SINCE, + IF_NONE_MATCH, + ORIGIN, + PRAGMA, + PROXY_AUTHORIZATION, + RANGE, + REFERER, + TE, + UPGRADE, + USER_AGENT, + VIA, + WARNING + }; + + enum class EResponseCode : INT32 + { + // 1xx Informational + CONTINUE = 100, + SWITCHING_PROTOCOLS = 101, + PROCESSING = 102, + EARLY_HINTS = 103, + + // 2xx Success + OK = 200, + CREATED = 201, + ACCEPTED = 202, + NON_AUTHORITATIVE_INFORMATION = 203, + NO_CONTENT = 204, + RESET_CONTENT = 205, + PARTIAL_CONTENT = 206, + MULTI_STATUS = 207, + ALREADY_REPORTED = 208, + IM_USED = 226, + + // 3xx Redirection + MULTIPLE_CHOICES = 300, + MOVED_PERMANENTLY = 301, + FOUND = 302, + SEE_OTHER = 303, + NOT_MODIFIED = 304, + USE_PROXY = 305, + TEMPORARY_REDIRECT = 307, + PERMANENT_REDIRECT = 308, + + // 4xx Client Error + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + PAYMENT_REQUIRED = 402, + FORBIDDEN = 403, + NOT_FOUND = 404, + METHOD_NOT_ALLOWED = 405, + NOT_ACCEPTABLE = 406, + PROXY_AUTHENTICATION_REQUIRED = 407, + REQUEST_TIMEOUT = 408, + CONFLICT = 409, + GONE = 410, + LENGTH_REQUIRED = 411, + PRECONDITION_FAILED = 412, + PAYLOAD_TOO_LARGE = 413, + URI_TOO_LONG = 414, + UNSUPPORTED_MEDIA_TYPE = 415, + RANGE_NOT_SATISFIABLE = 416, + EXPECTATION_FAILED = 417, + IM_A_TEAPOT = 418, + MISDIRECTED_REQUEST = 421, + UNPROCESSABLE_ENTITY = 422, + LOCKED = 423, + FAILED_DEPENDENCY = 424, + TOO_EARLY = 425, + UPGRADE_REQUIRED = 426, + PRECONDITION_REQUIRED = 428, + TOO_MANY_REQUESTS = 429, + REQUEST_HEADER_FIELDS_TOO_LARGE = 431, + UNAVAILABLE_FOR_LEGAL_REASONS = 451, + + // 5xx Server Error + INTERNAL_SERVER_ERROR = 500, + NOT_IMPLEMENTED = 501, + BAD_GATEWAY = 502, + SERVICE_UNAVAILABLE = 503, + GATEWAY_TIMEOUT = 504, + HTTP_VERSION_NOT_SUPPORTED = 505, + VARIANT_ALSO_NEGOTIATES = 506, + INSUFFICIENT_STORAGE = 507, + LOOP_DETECTED = 508, + NOT_EXTENDED = 510, + NETWORK_AUTHENTICATION_REQUIRED = 511 + }; + + using Header = KeyValuePair; + + public: + HttpClient(IN CONST String &host); + ~HttpClient(); + + public: + // Automatically adds the following headers, if not present: + // 1) EHeaderType::CONTENT_TYPE = defaultContentType + Expected RawGet(IN CONST String &path, IN Span headers, + IN PCCHAR defaultContentType = "application/x-www-form-urlencoded"); + // Automatically adds the following headers, if not present: + // 1) EHeaderType::CONTENT_TYPE = defaultContentType + Expected RawPost(IN CONST String &path, IN Span headers, IN CONST String &body, + IN PCCHAR defaultContentType = "application/x-www-form-urlencoded"); + + template + Expected<_response_type, String> JsonGet(IN CONST String &path, IN Span headers); + + template + Expected<_response_type, String> JsonPost(IN CONST String &path, IN Span headers, + IN CONST _payload_type &body); + + public: + STATIC String UrlEncode(IN CONST String &value); + STATIC String UrlDecode(IN CONST String &value); + + STATIC String HeaderTypeToString(IN EHeaderType type); + STATIC Header CreateHeader(IN EHeaderType key, IN CONST String &value); + + public: + EResponseCode LastResponseCode() + { + return m_lastResponseCode; + } + + private: + PVOID m_client{}; + EResponseCode m_lastResponseCode; + }; + + template + Expected<_response_type, String> HttpClient::JsonGet(IN CONST String &path, IN Span headers) + { + const auto rawResponse = RawGet(path, headers, "application/json"); + if (!rawResponse) + return MakeUnexpected(rawResponse.error()); + if (LastResponseCode() != EResponseCode::OK) + return MakeUnexpected(std::format("Server responded with code {}", (INT32) LastResponseCode())); + return JSON::ParseToStruct<_response_type>(*rawResponse); + } + + template + Expected<_response_type, String> HttpClient::JsonPost(IN CONST String &path, IN Span headers, + IN CONST _payload_type &body) + { + const auto encodedBody = IA_TRY(JSON::EncodeStruct(body)); + const auto rawResponse = RawPost(path, headers, encodedBody, "application/json"); + if (!rawResponse) + return MakeUnexpected(rawResponse.error()); + if (LastResponseCode() != EResponseCode::OK) + return MakeUnexpected(std::format("Server responded with code {}", (INT32) LastResponseCode())); + return JSON::ParseToStruct<_response_type>(*rawResponse); + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/JSON.hpp b/Src/IACore/inc/IACore/JSON.hpp new file mode 100644 index 0000000..e6b779b --- /dev/null +++ b/Src/IACore/inc/IACore/JSON.hpp @@ -0,0 +1,58 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include + +#include +#include +#include + +namespace IACore +{ + class JSON + { + private: + STATIC CONSTEXPR AUTO GLAZE_JSON_OPTS = glz::opts{.error_on_unknown_keys = false}; + + public: + STATIC Expected Parse(IN CONST String &json); + STATIC Expected ParseReadOnly(IN CONST String &json); + template STATIC Expected<_object_type, String> ParseToStruct(IN CONST String &json); + + STATIC String Encode(IN nlohmann::json data); + template STATIC Expected EncodeStruct(IN CONST _object_type &data); + }; + + template Expected<_object_type, String> JSON::ParseToStruct(IN CONST String &json) + { + _object_type result{}; + const auto parseError = glz::read(result, json); + if (parseError) + return MakeUnexpected(std::format("JSON Error: {}", glz::format_error(parseError, json))); + return result; + } + + template Expected JSON::EncodeStruct(IN CONST _object_type &data) + { + String result; + const auto encodeError = glz::write_json(data, result); + if (encodeError) + return MakeUnexpected(std::format("JSON Error: {}", glz::format_error(encodeError))); + return result; + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/PCH.hpp b/Src/IACore/inc/IACore/PCH.hpp index 5e4ec4b..c3bc514 100644 --- a/Src/IACore/inc/IACore/PCH.hpp +++ b/Src/IACore/inc/IACore/PCH.hpp @@ -33,6 +33,7 @@ # include # include # include +# include # include # include # include @@ -540,6 +541,7 @@ template using UniquePtr = std::unique_ptr<_value_type>; template using Deque = std::deque<_value_type>; template using Pair = std::pair<_type_a, _type_b>; template using Tuple = std::tuple; +template using KeyValuePair = std::pair<_key_type, _value_type>; template using Expected = tl::expected<_expected_type, _unexpected_type>; diff --git a/Vendor/CMakeLists.txt b/Vendor/CMakeLists.txt deleted file mode 100644 index 52d1203..0000000 --- a/Vendor/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ - -set(EXPECTED_BUILD_TESTS OFF) -add_subdirectory(expected/) - -set(MI_BUILD_TESTS OFF) -set(MI_BUILD_STATIC ON) -set(MI_BUILD_SHARED ON) -add_subdirectory(mimalloc/) - -add_subdirectory(unordered_dense/) diff --git a/Vendor/expected b/Vendor/expected deleted file mode 160000 index 1770e35..0000000 --- a/Vendor/expected +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1770e3559f2f6ea4a5fb4f577ad22aeb30fbd8e4 diff --git a/Vendor/mimalloc b/Vendor/mimalloc deleted file mode 160000 index 09a2709..0000000 --- a/Vendor/mimalloc +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 09a27098aa6e9286518bd9c74e6ffa7199c3f04e diff --git a/Vendor/unordered_dense b/Vendor/unordered_dense deleted file mode 160000 index 3234af2..0000000 --- a/Vendor/unordered_dense +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3234af2c03549bc85656bfd3a86993bf1cd8aef1