diff --git a/.github/workflows/clang-tidy.yaml b/.github/workflows/clang-tidy.yaml new file mode 100644 index 0000000..85e9f6d --- /dev/null +++ b/.github/workflows/clang-tidy.yaml @@ -0,0 +1,32 @@ +name: clang-tidy-review + +on: + pull_request: + paths: + - '**.c' + - '**.h' + workflow_dispatch: + +jobs: + clang-tidy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install libcurl + run: sudo apt-get update && sudo apt-get install -y curl libcurl4-openssl-dev + + - name: CMake + working-directory: ./sdks/c/ + env: + CC: clang + run: cmake -B ./build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + + - name: reviewdog with clang-tidy + uses: arkedge/action-clang-tidy@v1.2.0 + with: + workdir: ./sdks/c/build/ + + - name: Build + working-directory: ./sdks/c/ + run: cmake -S . -B ./build diff --git a/.gitignore b/.gitignore index ea8c4bf..1ec8302 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ /target +sdks/c/build +.vscode/settings.json + diff --git a/README.md b/README.md index 08268e9..0657f89 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ What I wanted to create: - In the Rust programming language - With a live feed of changes (it felt like a cool demo for my personal site) -I started implementing an http web server using Axum. I did this because I am used to working with http and rest interfaces. I might implement my own connection method, but this will be fine for now. +I started implementing an http web server using Axum. I did this because I am used to working with http and rest interfaces. I might implement my own connection protocol, but this will be fine for now. Since I was using Axum I also added a `/metrics` endpoint for Prometheus. The "database" part I created with a simple HashMap, and protected it behind a RWMutex. @@ -38,5 +38,16 @@ A sample of the WAL received by a WebSocket client: ``` The live demo is available in my personal website: https://rcpassos.me/projects/kv -And the source to that is here: -https://github.com/auyer/auyer.github.io/blob/main/src/lib/services.ts +And the source for that is here: +https://github.com/auyer/auyer.github.io/blob/main/src/lib/services.js + +# SDKs +I am also creating SDKs for this project using different languages. +Since the server uses REST with HTTP for the database interface, this is the main part that needs to be implemented in the SDKs. +The Live Feed is implemented using WebSockets, and is a less important feature. + + +| Feature/SDK | C | Rust +|---------------- |--- |------ +| Rest Endpoints | x | +| Live Feed (WS) | | diff --git a/sdks/c/.clang-format b/sdks/c/.clang-format new file mode 100644 index 0000000..0a1bab2 --- /dev/null +++ b/sdks/c/.clang-format @@ -0,0 +1,57 @@ +--- +Language: Cpp +# BasedOnStyle: Mozilla +AlignAfterOpenBracket: AlwaysBreak +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Right +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +BinPackArguments: false +BinPackParameters: false +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: true +BreakStringLiterals: true +ColumnLimit: 120 +ContinuationIndentWidth: 4 +DerivePointerAlignment: false +IncludeBlocks: Preserve +IndentCaseLabels: true +IndentPPDirectives: AfterHash +IndentWidth: 4 +IndentWrappedFunctionNames: true +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 100000 +PointerAlignment: Right +ReflowComments: true +SortIncludes: true +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 4 +UseTab: Always +... diff --git a/sdks/c/.clang-tidy b/sdks/c/.clang-tidy new file mode 100644 index 0000000..2f39995 --- /dev/null +++ b/sdks/c/.clang-tidy @@ -0,0 +1,121 @@ +--- +Checks: ' + -*, + clang-*, + bugprone-assert-side-effect, + bugprone-bool-pointer-implicit-conversion, + bugprone-incorrect-roundings, + bugprone-integer-division, + bugprone-macro-parentheses, + bugprone-macro-repeated-side-effects, + bugprone-misplaced-widening-cast, + bugprone-multiple-statement-macro, + bugprone-sizeof-expression, + bugprone-suspicious-enum-usage, + bugprone-suspicious-missing-comma, + bugprone-suspicious-semicolon, + bugprone-terminating-continue, + bugprone-too-small-loop-variable, + cppcoreguidelines-avoid-goto, + misc-definitions-in-headers, + misc-misplaced-const, + misc-redundant-expression, + misc-unused-parameters, + readability-braces-around-statements, + readability-const-return-type, + readability-else-after-return, + readability-function-size, + readability-implicit-bool-conversion, + readability-inconsistent-declaration-parameter-name, + readability-isolate-declaration, + readability-magic-numbers, + readability-misplaced-array-index, + readability-named-parameter, + readability-non-const-parameter, + readability-redundant-control-flow, + readability-redundant-declaration, + readability-redundant-preprocessor, + readability-uppercase-literal-suffix, + readability-identifier-naming, + ' +WarningsAsErrors: ' + bugprone-assert-side-effect, + bugprone-bool-pointer-implicit-conversion, + bugprone-incorrect-roundings, + bugprone-integer-division, + bugprone-macro-parentheses, + bugprone-macro-repeated-side-effects, + bugprone-misplaced-widening-cast, + bugprone-multiple-statement-macro, + bugprone-sizeof-expression, + bugprone-suspicious-enum-usage, + bugprone-suspicious-missing-comma, + bugprone-suspicious-semicolon, + bugprone-terminating-continue, + bugprone-too-small-loop-variable, + cppcoreguidelines-avoid-goto, + misc-definitions-in-headers, + misc-misplaced-const, + misc-redundant-expression, + misc-unused-parameters, + readability-braces-around-statements, + readability-const-return-type, + readability-else-after-return, + readability-function-size, + readability-implicit-bool-conversion, + readability-inconsistent-declaration-parameter-name, + readability-isolate-declaration, + readability-magic-numbers, + readability-misplaced-array-index, + readability-named-parameter, + readability-non-const-parameter, + readability-redundant-control-flow, + readability-redundant-declaration, + readability-redundant-preprocessor, + readability-uppercase-literal-suffix, + # readability-identifier-naming, + ' + +# From the docs: "Output warnings from headers matching this filter" +# But the goal should be to exclude(!) the headers for which clang-tidy is not called, +# e.g., for naming convention checks. DO NOT USE this field if you don't want to analyze +# header files just because they're included (seems to work). +# HeaderFilterRegex: '$' +# https://github.com/Kitware/CMake/blob/master/.clang-tidy +HeaderFilterRegex: '.*\.(h|hxx|cxx)$' +AnalyzeTemporaryDtors: false +FormatStyle: none +User: martin +CheckOptions: + - { key: bugprone-assert-side-effect.AssertMacros, value: assert } + - { key: bugprone-assert-side-effect.CheckFunctionCalls, value: '0' } + - { key: bugprone-misplaced-widening-cast.CheckImplicitCasts, value: '1' } + - { key: bugprone-sizeof-expression.WarnOnSizeOfConstant, value: '1' } + - { key: bugprone-sizeof-expression.WarnOnSizeOfIntegerExpression, value: '1' } + - { key: bugprone-sizeof-expression.WarnOnSizeOfThis, value: '1' } + - { key: bugprone-sizeof-expression.WarnOnSizeOfCompareToConstant, value: '1' } + - { key: bugprone-suspicious-enum-usage.StrictMode, value: '0' } + - { key: bugprone-suspicious-missing-comma.MaxConcatenatedTokens, value: '5' } + - { key: bugprone-suspicious-missing-comma.RatioThreshold, value: '0.200000' } + - { key: bugprone-suspicious-missing-comma.SizeThreshold, value: '5' } + - { key: misc-definitions-in-headers.HeaderFileExtensions, value: ',h,hh,hpp,hxx' } + - { key: misc-definitions-in-headers.UseHeaderFileExtension, value: '1' } + - { key: readability-braces-around-statements.ShortStatementLines, value: '1' } + - { key: readability-function-size.LineThreshold, value: '500' } + - { key: readability-function-size.StatementThreshold, value: '800' } + - { key: readability-function-size.ParameterThreshold, value: '10' } + - { key: readability-function-size.NestingThreshold, value: '6' } + - { key: readability-function-size.VariableThreshold, value: '15' } + - { key: readability-implicit-bool-conversion.AllowIntegerConditions, value: '0' } + - { key: readability-implicit-bool-conversion.AllowPointerConditions, value: '0' } + - { key: readability-implicit-bool-conversion.AllowPointerConditions, value: '0' } + - { key: readability-inconsistent-declaration-parameter-name.IgnoreMacros, value: '1' } + - { key: readability-inconsistent-declaration-parameter-name.Strict, value: '1' } + - { key: readability-magic-numbers.IgnoredFloatingPointValues, value: '1.0;100.0;' } + - { key: readability-magic-numbers.IgnoredIntegerValues, value: '1;2;3;4;' } + - { key: readability-magic-numbers.IgnorePowersOf2IntegerValues, value: '0' } + - { key: readability-magic-numbers.IgnoreAllFloatingPointValues, value: '0' } + - { key: readability-redundant-declaration.IgnoreMacros, value: '1' } + - { key: readability-redundant-function-ptr-dereference, value: '1' } + - { key: readability-uppercase-literal-suffix.IgnoreMacros, value: '0' } + - { key: readability-uppercase-literal-suffix.IgnoreMacros, value: '0' } \ No newline at end of file diff --git a/sdks/c/CMakeLists.txt b/sdks/c/CMakeLists.txt new file mode 100644 index 0000000..56186c6 --- /dev/null +++ b/sdks/c/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.27.6) + +project(MemoryKV) + +add_executable(${PROJECT_NAME} example.c) + +set_property(TARGET ${PROJECT_NAME} PROPERTY C_STANDARD 23) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(libcurl REQUIRED IMPORTED_TARGET libcurl>=7.17.0) + +add_subdirectory(libMemoryKV) + +target_include_directories(${PROJECT_NAME} PUBLIC libMemoryKV/) +target_link_directories(${PROJECT_NAME} PUBLIC libMemoryKV/) + +target_link_libraries(${PROJECT_NAME} PUBLIC PkgConfig::libcurl libMemoryKV) diff --git a/sdks/c/Makefile b/sdks/c/Makefile new file mode 100644 index 0000000..a5f0f49 --- /dev/null +++ b/sdks/c/Makefile @@ -0,0 +1,5 @@ +cmake: + cmake -S . -B build + +cmake_make: + cd build && make \ No newline at end of file diff --git a/sdks/c/build/.gitkeep b/sdks/c/build/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/sdks/c/compile_flags.txt b/sdks/c/compile_flags.txt new file mode 100644 index 0000000..8e4a4e0 --- /dev/null +++ b/sdks/c/compile_flags.txt @@ -0,0 +1,3 @@ +-I./libMemoryKV +-llibMemoryKV +-std=c2x diff --git a/sdks/c/example.c b/sdks/c/example.c new file mode 100644 index 0000000..daaf8f5 --- /dev/null +++ b/sdks/c/example.c @@ -0,0 +1,95 @@ +#include +#include +#include +#include + +void print_memkv_result(memkv_result *response) { + if (!response) { + fprintf(stderr, "Error: response is NULL\n"); + + } else if (response->success) { + printf("Success! Result: %s\n", response->result); + } else { + fprintf(stderr, "Error: %s\n", response->error); + } + free(response); +} + +int main(void) { + memkv_client *client; + + client = memkv_client_new("http://localhost:8080"); + + memkv_result *response; + + char key[] = "c_sdk"; + char key2[] = "c_sdk2"; + char key3[] = "a_sdk"; + + fprintf(stdout, "This example will create, and delete in 3 keys: %s, %s and %s\n", key, key2, key3); + + // put key + static const char put_body[] = "{" + " \"name\" : \"MemoryKV Example Body\"," + " \"content\" : \"json\"" + "}"; + + fprintf(stdout, "\nPut Key '%s'\n", key); + response = memkv_put_key(client, key, put_body); + print_memkv_result(response); + + // list key + fprintf(stdout, "\nList Keys\n"); + response = memkv_list_keys(client); + print_memkv_result(response); + + // get key + fprintf(stdout, "\nGet Key '%s'\n", key); + response = memkv_get_key(client, key); + print_memkv_result(response); + + fprintf(stdout, "\nPut Key '%s'\n", key2); + response = memkv_put_key(client, key2, put_body); + print_memkv_result(response); + + fprintf(stdout, "\nPut Key '%s'\n", key3); + response = memkv_put_key(client, key3, put_body); + print_memkv_result(response); + + fprintf(stdout, "\nPut Key '%s' (again)\n", key3); + response = memkv_put_key(client, key3, put_body); + print_memkv_result(response); + + char prefix[] = "c"; + + fprintf(stdout, "\nList Keys With Prefix '%s'\n", prefix); + response = memkv_list_keys_with_prefix(client, prefix); + print_memkv_result(response); + + prefix[0] = 'a'; + fprintf(stdout, "\nList Keys With Prefix '%s'\n", prefix); + response = memkv_list_keys_with_prefix(client, prefix); + print_memkv_result(response); + + // delete key + prefix[0] = 'c'; + fprintf(stdout, "\nMaking a delete Key Request with Prefix '%s'\n", prefix); + response = memkv_delete_keys_with_prefix(client, prefix); + print_memkv_result(response); + + // delete all keys + fprintf(stdout, "\nDelete all Keys\n"); + response = memkv_delete_all_keys(client); + print_memkv_result(response); + + fprintf(stdout, "\nMaking a delete Key '%s'\n", key3); + response = memkv_delete_key(client, key3); + print_memkv_result(response); + + // list key + fprintf(stdout, "\nList Keys\n"); + response = memkv_list_keys(client); + print_memkv_result(response); + + free(client); +} diff --git a/sdks/c/libMemoryKV/CMakeLists.txt b/sdks/c/libMemoryKV/CMakeLists.txt new file mode 100644 index 0000000..fdc8aba --- /dev/null +++ b/sdks/c/libMemoryKV/CMakeLists.txt @@ -0,0 +1,6 @@ +add_library(libMemoryKV libMemoryKV.c) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(libcurl REQUIRED IMPORTED_TARGET libcurl>=7.17.0) + +target_link_libraries(${PROJECT_NAME} PUBLIC PkgConfig::libcurl) \ No newline at end of file diff --git a/sdks/c/libMemoryKV/libMemoryKV.c b/sdks/c/libMemoryKV/libMemoryKV.c new file mode 100644 index 0000000..6cd3078 --- /dev/null +++ b/sdks/c/libMemoryKV/libMemoryKV.c @@ -0,0 +1,276 @@ +#include +#include +#include +#include +#include + +// string_carrier struct to store the body results from curl with easy reallocation +typedef struct string_carrier { + char *ptr; + size_t len; + bool error_flag; +} string_carrier; + +// init_string_carrier initializes the string_carrier struct to store the body results from curl +static void init_string_carrier(string_carrier *s) { + s->len = 0; + s->error_flag = false; + s->ptr = malloc(s->len + 1); + if (s->ptr != NULL) { + // set ptr as null terminator + s->ptr[0] = '\0'; + } else { + // set error flag to true if malloc fails + s->error_flag = true; + fprintf(stderr, "memkv client: Error in INIT, malloc() failed\n"); + } +} + +static string_carrier *new_string_carrier() { + string_carrier *s = malloc(sizeof *s); + if (!s) { + free(s); + return NULL; + } + init_string_carrier(s); + if (s->error_flag || s->ptr == NULL) { + free(s); + printf("Error in new_string_carrier\n"); + return NULL; + } + return s; +} + +// string_carrier_writefunc is the callback function to read the body from libcurl into a string_carrier +static size_t string_carrier_writefunc(void *ptr, size_t size, size_t nmemb, struct string_carrier *s) { + // new length to realocate response chunks from libcurl + size_t new_len = s->len + (size * nmemb); + void *const tmp = realloc(s->ptr, new_len + 1); + if (!tmp) { + fprintf(stderr, "memkv client: Error in string_carrier_writefunc Callback, realloc() failed\n"); + s->error_flag = true; + } else { + /* Now s->ptr points to the new chunk of memory. */ + s->ptr = tmp; + // we already know the length of the string, so we can just copy it with memcpy instead of strlcpy + memcpy(s->ptr + s->len, ptr, new_len + 1); + s->len = new_len; + } + + return size * nmemb; +} + +// memkv_client stores the configurations used to connect to the MemoryKV server +// use `memkv_client_new` to create one, or `memkv_init_client` to initialize an existing instance +typedef struct memkv_client { + char *host; +} memkv_client; + +void memkv_init_client(memkv_client *client, char *host) { + curl_global_init(CURL_GLOBAL_ALL); + client->host = host; +} + +memkv_client *memkv_client_new(char *host) { + memkv_client *client = malloc(sizeof *client); + if (client == NULL) { + printf("Error in memkv_client_new"); + return NULL; + } + memkv_init_client(client, host); + return client; +} + +static char *build_url(const char *host, const char *params) { + // snprintf returns the number of characters that would have been written if called with NULL + unsigned long size_needed = snprintf(NULL, 0, "%s/%s", host, params); + // use that number to allocate a buffer of the right size + char *url = malloc(size_needed + 1); + // write the string_carrier to the buffer + sprintf(url, "%s/%s", host, params); + return url; +} + +typedef struct { + bool success; + union { + char *result; + char *error; + }; +} memkv_result; + +static const char base_curl_error[] = "Error from curl"; +static const char unknown_error_msg[] = "unknown error"; + +static void make_curl_error(memkv_result *r, const char *err) { + r->success = false; + + if (strlen(err) == 0) { + r->error = malloc(sizeof(unknown_error_msg)); + // strlcpy(r->error, unknown_error_msg, sizeof(unknown_error_msg) + sizeof(r->error)); + strcpy(r->error, unknown_error_msg); + + return; + } + + unsigned long size_needed = snprintf(NULL, 0, "%s : %s", base_curl_error, err); + + r->error = malloc(size_needed + 1); + + snprintf(r->error, size_needed + 1, "%s : %s", base_curl_error, err); +} + +// init_memkv_result initializes the memkv_result struct with success as false +memkv_result *init_memkv_result() { + + memkv_result *r = malloc(sizeof *r); + + r->result = malloc(1); + if (r->result == NULL) { + free(r); + printf("Error in init_memkv_result"); + return NULL; + } + + r->success = false; + return r; +} + +static memkv_result *memkv_execute_request( + const char *url, + const char *custom_req, + const char *content_type, + const char *post_data) { + + memkv_result *r = init_memkv_result(); + + CURLcode res; + + // TODO move this to the client ? + CURL *curl = curl_easy_init(); + + if (!curl) { + make_curl_error(r, "Failed starting curl."); + return r; + } + + string_carrier *s = new_string_carrier(); + + if (s == NULL || s->error_flag) { + make_curl_error(r, "Failed to initialize string_carrier"); + return r; + } + + int curl_parameter_responses = 0; + curl_parameter_responses += curl_easy_setopt(curl, CURLOPT_URL, url); + + if (custom_req) { + curl_parameter_responses += curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, custom_req); + } + + struct curl_slist *headers = NULL; + + if (content_type) { + /* default type with postfields is application/x-www-form-urlencoded, + change it if you want. */ + headers = curl_slist_append(headers, content_type); + curl_parameter_responses += curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + } + + if (post_data) { + curl_parameter_responses += curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data); + } + + curl_parameter_responses += curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + + printf("Curl parameters responses %d %s\n", curl_parameter_responses, curl_parameter_responses ? "true" : "false"); + + int curl_body_responses = 0; + + curl_body_responses += curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, string_carrier_writefunc); + curl_body_responses += curl_easy_setopt(curl, CURLOPT_WRITEDATA, s); + + printf("Curl body responses %d %s\n", curl_body_responses, curl_body_responses ? "true" : "false"); + + res = curl_easy_perform(curl); + + curl_easy_cleanup(curl); + + if (headers) { + curl_slist_free_all(headers); + } + + if (res != CURLE_OK) { + const char *err = curl_easy_strerror(res); + make_curl_error(r, err); + return r; + } + + if (s->error_flag) { + make_curl_error(r, "Failed to get results from server."); + return r; + } + + r->success = true; + // updates the result pointer to point to the string_carrier->ptr + r->result = s->ptr; + free(s); + + return r; +} + +memkv_result *memkv_get_key(memkv_client *client, const char *key) { + char *url = build_url(client->host, key); + memkv_result *r = memkv_execute_request(url, NULL, NULL, NULL); + free(url); + return r; +} + +memkv_result *memkv_delete_key(memkv_client *client, const char *key) { + char *url = build_url(client->host, key); + memkv_result *r = memkv_execute_request(url, "DELETE", NULL, NULL); + free(url); + return r; +} + +memkv_result *memkv_put_key(memkv_client *client, const char *key, const char *put_body) { + char *url = build_url(client->host, key); + memkv_result *r = memkv_execute_request(url, "PUT", "Content-Type: application/octet-stream", put_body); + free(url); + return r; +} + +memkv_result *memkv_list_keys(memkv_client *client) { + char *url = build_url(client->host, "keys"); + memkv_result *r = memkv_execute_request(url, NULL, NULL, NULL); + free(url); + return r; +} + +memkv_result *memkv_list_keys_with_prefix(memkv_client *client, const char *key_prefix) { + char *url_part1 = build_url(client->host, "keys"); + // TODO: build URL in one call + char *url = build_url(url_part1, key_prefix); + free(url_part1); + + memkv_result *r = memkv_execute_request(url, NULL, NULL, NULL); + free(url); + return r; +} + +memkv_result *memkv_delete_keys_with_prefix(memkv_client *client, const char *key_prefix) { + char *url_part1 = build_url(client->host, "keys"); + // TODO: build URL in one call + char *url = build_url(url_part1, key_prefix); + free(url_part1); + memkv_result *r = memkv_execute_request(url, "DELETE", NULL, NULL); + free(url); + return r; +} + +memkv_result *memkv_delete_all_keys(memkv_client *client) { + char *url = build_url(client->host, "keys"); + memkv_result *r = memkv_execute_request(url, "DELETE", NULL, NULL); + free(url); + return r; +} diff --git a/sdks/c/libMemoryKV/libMemoryKV.h b/sdks/c/libMemoryKV/libMemoryKV.h new file mode 100644 index 0000000..d8ba046 --- /dev/null +++ b/sdks/c/libMemoryKV/libMemoryKV.h @@ -0,0 +1,47 @@ +#include + +// memkv_client stores the configurations used to connect to the MemoryKV server +// use `memkv_client_new` to create one, or `memkv_init_client` to initialize an existing instance +typedef struct memkv_client { + char *host; +} memkv_client; + +// memkv_result stores a success boolean to indicate if the request was successful or not. +// the success variable should be checked before reading results +// in case of success, the result will be stored in the result attribute `memkv_result->result` +// in case of error, the error reason will be stored in the error attribute `memkv_result->error` +typedef struct { + bool success : 1; + union { + char *result; + char *error; + }; +} memkv_result; + +// memkv_client_new creates a new memkv_client instance with a host url pre filled +memkv_client *memkv_client_new(char *host); + +// memkv_init_client initializes an existing memkv_client with a provided URL +// If this function returns non-zero, something went wrong and you cannot use the other underlying curl functions. +int memkv_init_client(struct memkv_client *client, char *host); + +// memkv_get_key fetches the value of a key from the MemoryKV server +memkv_result *memkv_get_key(struct memkv_client *client, const char *key); + +// memkv_delete_key deletes a key from the MemoryKV server, and return a value if it was deleted +memkv_result *memkv_delete_key(struct memkv_client *client, const char *key); + +// memkv_put_key puts a key value pair in the MemoryKV server, and return a value if there was one in the key beafore +memkv_result *memkv_put_key(struct memkv_client *client, const char *key, const char *put_body); + +// memkv_list_keys lists all the keys in the MemoryKV server +memkv_result *memkv_list_keys(struct memkv_client *client); + +// memkv_list_keys_with_prefix lists all the keys in the MemoryKV server with a given prefix +memkv_result *memkv_list_keys_with_prefix(struct memkv_client *client, const char *key_prefix); + +// memkv_delete_key deletes a key from the MemoryKV server, and return the keys that were deleted +memkv_result *memkv_delete_keys_with_prefix(struct memkv_client *client, const char *key_prefix); + +// memkv_delete_all_keys deletes all keys from the MemoryKV server, and returns list of deletes keys +memkv_result *memkv_delete_all_keys(struct memkv_client *client);