diff --git a/bserv/include/bserv/database.hpp b/bserv/include/bserv/database.hpp index 91eefc8..2c90854 100644 --- a/bserv/include/bserv/database.hpp +++ b/bserv/include/bserv/database.hpp @@ -173,6 +173,15 @@ namespace bserv { } }; + template <> + class db_value : public db_parameter { + public: + db_value(const std::nullptr_t&) {} + std::string get_value(raw_db_transaction_type&) { + return "null"; + } + }; + template class db_value> : public db_parameter { private: @@ -204,6 +213,68 @@ namespace bserv { } }; + class unsupported_json_value_type : public std::exception { + public: + unsupported_json_value_type() = default; + const char* what() const { return "unsupported json value type"; } + }; + + template <> + class db_value : public db_parameter { + private: + boost::json::value value_; + boost::json::value copy_json_value( + const boost::json::value& value) const { + if (value.is_bool()) { + return value.as_bool(); + } + else if (value.is_double()) { + return value.as_double(); + } + else if (value.is_int64()) { + return value.as_int64(); + } + else if (value.is_null()) { + return nullptr; + } + else if (value.is_string()) { + return value.as_string(); + } + else if (value.is_uint64()) { + return value.as_uint64(); + } + else { + throw unsupported_json_value_type{}; + } + } + public: + db_value(const boost::json::value& value) + : value_{ copy_json_value(value) } {} + std::string get_value(raw_db_transaction_type& tx) { + if (value_.is_bool()) { + return db_value{value_.as_bool()}.get_value(tx); + } + else if (value_.is_double()) { + return db_value{value_.as_double()}.get_value(tx); + } + else if (value_.is_int64()) { + return db_value{value_.as_int64()}.get_value(tx); + } + else if (value_.is_null()) { + return db_value{nullptr}.get_value(tx); + } + else if (value_.is_string()) { + return db_value{value_.as_string()}.get_value(tx); + } + else if (value_.is_uint64()) { + return db_value{value_.as_uint64()}.get_value(tx); + } + else { + throw unsupported_json_value_type{}; + } + } + }; + namespace db_internal { template @@ -280,6 +351,9 @@ namespace bserv { if (!row[field_idx].is_null()) { obj[name_] = row[field_idx].as(); } + else { + obj[name_] = nullptr; + } } }; @@ -293,6 +367,9 @@ namespace bserv { if (!row[field_idx].is_null()) { obj[name_] = row[field_idx].c_str(); } + else { + obj[name_] = nullptr; + } } }; diff --git a/test/DBTest.cpp b/test/DBTest.cpp new file mode 100644 index 0000000..84edb4a --- /dev/null +++ b/test/DBTest.cpp @@ -0,0 +1,94 @@ +#include +#include +#include +#include +bserv::db_relation_to_object orm_test{ + bserv::make_db_field("id"), + bserv::make_db_field("name"), + bserv::make_db_field("active"), + bserv::make_db_field>("email"), + bserv::make_db_field>("code") +}; +boost::json::object add( + boost::json::object&& params, + std::shared_ptr conn) { + if (!params.contains("name")) { + return { + {"missing", "name"} + }; + } + if (!params.contains("active")) params["active"] = true; + if (!params.contains("email")) params["email"] = nullptr; + if (!params.contains("code")) params["code"] = nullptr; + bserv::db_transaction tx{ conn }; + bserv::db_result r = tx.exec( + "insert into db_test (name, active, email, code) values (?, ?, ?, ?);", + params["name"], params["active"], params["email"], params["code"]); + lgdebug << r.query(); + tx.commit(); + return { + {"successfully", "added"} + }; +} +boost::json::object update( + const std::string& name, + boost::json::object&& params, + std::shared_ptr conn) { + if (!params.contains("name")) { + return { + {"missing", "name"} + }; + } + if (!params.contains("active")) params["active"] = true; + if (!params.contains("email")) params["email"] = nullptr; + if (!params.contains("code")) params["code"] = nullptr; + bserv::db_transaction tx{ conn }; + bserv::db_result r = tx.exec( + "update db_test set name = ?, active = ?, email = ?, code = ? where name = ?;", + params["name"], params["active"], params["email"], params["code"], name); + lgdebug << r.query(); + tx.commit(); + return { + {"successfully", "updated"} + }; +} +boost::json::object find( + const std::string& name, + std::shared_ptr conn) { + bserv::db_transaction tx{ conn }; + bserv::db_result r = tx.exec("select * from db_test where name = ?;", name); + lgdebug << r.query(); + auto item = orm_test.convert_to_optional(r); + if (item) { + return { + {"item", item.value()} + }; + } + else { + return { + {"not", "found"} + }; + } +} +int main() +{ + std::string config_content = bserv::utils::file::read_bin("../config.json"); + boost::json::object config_obj = boost::json::parse(config_content).as_object(); + bserv::server_config config; + config.set_db_conn_str(config_obj["conn-str"].as_string().c_str()); + bserv::server{ + config, + { + bserv::make_path("/add", &add, + bserv::placeholders::json_params, + bserv::placeholders::db_connection_ptr), + bserv::make_path("/update/", &update, + bserv::placeholders::_1, + bserv::placeholders::json_params, + bserv::placeholders::db_connection_ptr), + bserv::make_path("/find/", &find, + bserv::placeholders::_1, + bserv::placeholders::db_connection_ptr) + } + }; +} \ No newline at end of file diff --git a/test/DBTest.py b/test/DBTest.py new file mode 100644 index 0000000..9f43572 --- /dev/null +++ b/test/DBTest.py @@ -0,0 +1,90 @@ +import uuid +import string +import random +import requests +from multiprocessing import Process +from time import time +from pprint import pprint + +char_string = string.ascii_letters + string.digits + +def get_string(n): + return ''.join(random.choice(char_string) for _ in range(n)) + +def get_bool(): + if random.randint(0, 1) == 0: + return False + else: # r == 1 + return True + +def get_null_email(): + if random.randint(0, 1) == 0: + return None + else: + return get_string(5) + "@" + get_string(5) + ".com" + +def get_null_code(): + if random.randint(0, 1) == 0: + return None + else: + return random.randint(0, 7) + +def create_item(): + return { + "name": str(uuid.uuid4()), + "active": get_bool(), + "email": get_null_email(), + "code": get_null_code() + } + +def update_item(item): + if random.randint(0, 1) == 0: + item["name"] = str(uuid.uuid4()) + if random.randint(0, 1) == 0: + item["active"] = get_bool() + if random.randint(0, 1) == 0: + item["email"] = get_null_email() + if random.randint(0, 1) == 0: + item["code"] = get_null_code() + +P = 200 # number of concurrent processes +N = 20 # number of posts + +def test(): + session = requests.session() + item = create_item() + if {"successfully": "added"} \ + != session.post("http://localhost:8080/add", json=item).json(): + print('test failed') + res = session.get(f"http://localhost:8080/find/{item['name']}").json() + item["id"] = res["item"]["id"] + if res != {"item": item}: + print('test failed') + for _ in range(N): + old_name = item['name'] + update_item(item) + if {"successfully": "updated"} \ + != session.post(f"http://localhost:8080/update/{old_name}", json=item).json(): + print('test failed') + if {"item": item} \ + != session.get(f"http://localhost:8080/find/{item['name']}").json(): + print('test failed') + + +if __name__ == '__main__': + processes = [Process(target=test) for _ in range(P)] + + print('starting') + + start = time() + + for p in processes: + p.start() + + for p in processes: + p.join() + + end = time() + + print('test ended') + print('elapsed: ', end - start) diff --git a/test/DBTest.sql b/test/DBTest.sql new file mode 100644 index 0000000..e612ea1 --- /dev/null +++ b/test/DBTest.sql @@ -0,0 +1,8 @@ +DROP TABLE IF EXISTS db_test; +CREATE TABLE db_test ( + id serial PRIMARY KEY, + name character varying(255) NOT NULL UNIQUE, + active boolean NOT NULL, + email character varying(255), + code int +); \ No newline at end of file diff --git a/test/DBTest.vcxproj b/test/DBTest.vcxproj new file mode 100644 index 0000000..0b80426 --- /dev/null +++ b/test/DBTest.vcxproj @@ -0,0 +1,148 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + {7c95e12b-d7aa-4329-86aa-9fe3d8b2a5f2} + DBTest + 10.0 + + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + true + + + false + + + true + + + false + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + + + + + \ No newline at end of file diff --git a/test/DBTest.vcxproj.filters b/test/DBTest.vcxproj.filters new file mode 100644 index 0000000..0fdc3e4 --- /dev/null +++ b/test/DBTest.vcxproj.filters @@ -0,0 +1,22 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + 源文件 + + + \ No newline at end of file diff --git a/test/DBTest.vcxproj.user b/test/DBTest.vcxproj.user new file mode 100644 index 0000000..88a5509 --- /dev/null +++ b/test/DBTest.vcxproj.user @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/test/RequestSessionTest.cpp b/test/RequestSessionTest.cpp new file mode 100644 index 0000000..fd48560 --- /dev/null +++ b/test/RequestSessionTest.cpp @@ -0,0 +1,55 @@ +#include +#include +#include +boost::json::object hello() { + return { + {"hello", "world"} + }; +} +boost::json::object echo( + boost::json::object&& params, + std::shared_ptr session_ptr) { + bserv::session_type& session = *session_ptr; + if (!session.count("count")) session["count"] = 0; + session["count"] = session["count"].as_int64() + 1; + return { + {"id", "echo"}, + {"obj", params} + }; +} +boost::json::object echo2( + const std::string& id) { + return { + {"id", "echo2"}, + {"str", id} + }; +} +boost::json::object get( + std::shared_ptr session_ptr) { + bserv::session_type& session = *session_ptr; + if (session.count("count")) + return { + {"id", "get"}, + {"val", session["count"]} + }; + return { + {"id", "get"}, + {"val", 0} + }; +} +int main() +{ + bserv::server{ + bserv::server_config{}, + { + bserv::make_path("/hello", &hello), + bserv::make_path("/echo", &echo, + bserv::placeholders::json_params, + bserv::placeholders::session), + bserv::make_path("/echo/", &echo2, + bserv::placeholders::_1), + bserv::make_path("/get", &get, + bserv::placeholders::session) + } + }; +} \ No newline at end of file diff --git a/test/RequestSessionTest.py b/test/RequestSessionTest.py new file mode 100644 index 0000000..572cc15 --- /dev/null +++ b/test/RequestSessionTest.py @@ -0,0 +1,66 @@ +import uuid +import requests +import random +from multiprocessing import Process +from time import time +from pprint import pprint + +def size_test(): + session = requests.session() + length = 4 * 1024 * 1024 # ~4MB + data = {"id": "a" * length} + res = session.post("http://localhost:8080/echo", json=data).json() + if {"id": "echo", "obj": data} != res: + print("size test: failed") + else: + print("size test: ok") + print() + +P = 200 # number of concurrent processes +N = 20 # for each process, the number of posts + +def test(): + session = requests.session() + if {"hello": "world"} \ + != session.get("http://localhost:8080/hello").json(): + print('test failed!') + if {"id": "get", "val": 0} \ + != session.get("http://localhost:8080/get").json(): + print('test failed!') + cnt = 0 + for _ in range(N): + session_id = str(uuid.uuid4()) + if random.randint(0, 1) == 0: + if {"id": "echo2", "str": session_id} \ + != session.get(f"http://localhost:8080/echo/{session_id}").json(): + print('test failed!') + else: + data = {"id": session_id} + if {"id": "echo", "obj": data} \ + != session.post("http://localhost:8080/echo", json=data).json(): + print('test failed!') + cnt += 1 + if {"id": "get", "val": cnt} \ + != session.get("http://localhost:8080/get").json(): + print('test failed!') + + +if __name__ == '__main__': + size_test() + + processes = [Process(target=test) for _ in range(P)] + + print('starting') + + start = time() + + for p in processes: + p.start() + + for p in processes: + p.join() + + end = time() + + print('test ended') + print('elapsed: ', end - start) diff --git a/test/RequestSessionTest.vcxproj b/test/RequestSessionTest.vcxproj new file mode 100644 index 0000000..1836d60 --- /dev/null +++ b/test/RequestSessionTest.vcxproj @@ -0,0 +1,148 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + {5b8cbaca-b951-4804-aed5-f3f1a4fa9e1b} + RequestSessionTest + 10.0 + + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + true + + + false + + + true + + + false + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + + + + + \ No newline at end of file diff --git a/test/RequestSessionTest.vcxproj.filters b/test/RequestSessionTest.vcxproj.filters new file mode 100644 index 0000000..cd8ada1 --- /dev/null +++ b/test/RequestSessionTest.vcxproj.filters @@ -0,0 +1,22 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + 源文件 + + + \ No newline at end of file diff --git a/test/RequestSessionTest.vcxproj.user b/test/RequestSessionTest.vcxproj.user new file mode 100644 index 0000000..88a5509 --- /dev/null +++ b/test/RequestSessionTest.vcxproj.user @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/test/Test.sln b/test/Test.sln new file mode 100644 index 0000000..5e6a1aa --- /dev/null +++ b/test/Test.sln @@ -0,0 +1,57 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31911.196 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RequestSessionTest", "RequestSessionTest.vcxproj", "{5B8CBACA-B951-4804-AED5-F3F1A4FA9E1B}" + ProjectSection(ProjectDependencies) = postProject + {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296} = {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "bserv", "..\bserv\bserv.vcxproj", "{F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DBTest", "DBTest.vcxproj", "{7C95E12B-D7AA-4329-86AA-9FE3D8B2A5F2}" + ProjectSection(ProjectDependencies) = postProject + {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296} = {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296} + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5B8CBACA-B951-4804-AED5-F3F1A4FA9E1B}.Debug|x64.ActiveCfg = Debug|x64 + {5B8CBACA-B951-4804-AED5-F3F1A4FA9E1B}.Debug|x64.Build.0 = Debug|x64 + {5B8CBACA-B951-4804-AED5-F3F1A4FA9E1B}.Debug|x86.ActiveCfg = Debug|Win32 + {5B8CBACA-B951-4804-AED5-F3F1A4FA9E1B}.Debug|x86.Build.0 = Debug|Win32 + {5B8CBACA-B951-4804-AED5-F3F1A4FA9E1B}.Release|x64.ActiveCfg = Release|x64 + {5B8CBACA-B951-4804-AED5-F3F1A4FA9E1B}.Release|x64.Build.0 = Release|x64 + {5B8CBACA-B951-4804-AED5-F3F1A4FA9E1B}.Release|x86.ActiveCfg = Release|Win32 + {5B8CBACA-B951-4804-AED5-F3F1A4FA9E1B}.Release|x86.Build.0 = Release|Win32 + {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296}.Debug|x64.ActiveCfg = Debug|x64 + {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296}.Debug|x64.Build.0 = Debug|x64 + {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296}.Debug|x86.ActiveCfg = Debug|Win32 + {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296}.Debug|x86.Build.0 = Debug|Win32 + {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296}.Release|x64.ActiveCfg = Release|x64 + {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296}.Release|x64.Build.0 = Release|x64 + {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296}.Release|x86.ActiveCfg = Release|Win32 + {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296}.Release|x86.Build.0 = Release|Win32 + {7C95E12B-D7AA-4329-86AA-9FE3D8B2A5F2}.Debug|x64.ActiveCfg = Debug|x64 + {7C95E12B-D7AA-4329-86AA-9FE3D8B2A5F2}.Debug|x64.Build.0 = Debug|x64 + {7C95E12B-D7AA-4329-86AA-9FE3D8B2A5F2}.Debug|x86.ActiveCfg = Debug|Win32 + {7C95E12B-D7AA-4329-86AA-9FE3D8B2A5F2}.Debug|x86.Build.0 = Debug|Win32 + {7C95E12B-D7AA-4329-86AA-9FE3D8B2A5F2}.Release|x64.ActiveCfg = Release|x64 + {7C95E12B-D7AA-4329-86AA-9FE3D8B2A5F2}.Release|x64.Build.0 = Release|x64 + {7C95E12B-D7AA-4329-86AA-9FE3D8B2A5F2}.Release|x86.ActiveCfg = Release|Win32 + {7C95E12B-D7AA-4329-86AA-9FE3D8B2A5F2}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B400CC30-B6D6-404C-9A92-BD75FA0E9A9F} + EndGlobalSection +EndGlobal