diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0c4c257 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + branches: + - "**" + +jobs: + examples: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.23" + + - name: Regenerate examples + run: make -C _examples generate + + - name: Git diff of regenerated files + run: make -C _examples diff + + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.23" + + - name: Install native dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential pkg-config libcurl4-openssl-dev libcjson-dev + + - name: Run generator tests + run: go test ./... diff --git a/README.md b/README.md index e69de29..d87336f 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,144 @@ +# gen-c + +This repo contains the templates used by the `webrpc-gen` CLI to code-generate +webrpc C client code. + +This generator, from a webrpc schema/design file, will code-generate: + +1. Header output + Public C enums, structs, support types, `init` / `free` helpers, client declarations, + and lower-level request / response helpers. + +2. Implementation output + C JSON encode/decode helpers, generated method request / response handling, + and an optional `libcurl`-based client runtime. + +The generated client is intended to speak to any webrpc server language +(Go, nodejs, etc.) as long as the schema features used are supported by this target. + +## Dependencies + +Generated `header` output only depends on the C standard library headers included by +the generated file. + +Generated `impl` output currently depends on: + +- `cJSON` +- `libcurl` + +The generated code targets C99. + +Typical compile / link flags look like: + +```bash +cc -std=c99 \ + $(pkg-config --cflags libcurl libcjson) \ + -c example.gen.c + +cc -std=c99 \ + app.c example.gen.c \ + $(pkg-config --cflags --libs libcurl libcjson) \ + -o app +``` + +Dependency names can vary slightly by platform or package manager. The important part +is that the generated implementation can include `` and link against +`libcurl` and `cJSON`. + +Because the generated implementation uses `cJSON`, exact large 64-bit integer handling +follows `cJSON`'s numeric behavior. If your API needs exact integer round-tripping beyond +normal JSON number precision expectations, prefer `bigint` in the schema instead of +`int64` / `uint64`. + +## Features + +The current generator supports: + +- client code generation +- separate `header` and `impl` emission +- generated DTO structs and enums +- generated `init` / `free` helpers for schema and method wrapper types +- `bigint` values encoded as JSON strings +- `timestamp` values encoded as JSON strings +- `any`, `null`, nested lists, nested maps, and nested structs +- map keys of type `string` and `enum` +- succinct method wire format +- generated lower-level helpers to: + - prepare request bytes without sending them + - send a prepared request with the generated transport + - parse a raw HTTP response into generated response types +- generated `libcurl` client configuration for bearer auth, custom headers, and timeouts + +## Limitations + +The current generator does not support: + +- server generation +- streaming methods +- map keys other than `string` or `enum` +- a shared external transport abstraction; the generated runtime is currently self-contained + +Implementation generation also assumes a companion generated header include via +`-header=`. + +## Usage + +Generate the header: + +```bash +webrpc-gen \ + -schema=example.ridl \ + -target=./local-gen-c \ + -emit=header \ + -out=./example.gen.h +``` + +Generate the implementation: + +```bash +webrpc-gen \ + -schema=example.ridl \ + -target=./local-gen-c \ + -emit=impl \ + -header=example.gen.h \ + -out=./example.gen.c +``` + +When published as a `gen-*` module, this can also be used via: + +```bash +webrpc-gen \ + -schema=example.ridl \ + -target=github.com/webrpc/gen-c@ \ + -emit=header \ + -out=./example.gen.h +``` + +or: + +```bash +webrpc-gen \ + -schema=example.ridl \ + -target=github.com/webrpc/gen-c@ \ + -emit=impl \ + -header=example.gen.h \ + -out=./example.gen.c +``` + +## Set Custom Template Variables + +Change any of the following values by passing `-option="Value"` CLI flag to `webrpc-gen`. + +| webrpc-gen -option | Description | Default value | Version | +| --- | --- | --- | --- | +| `-prefix=` | symbol and type prefix | schema name in `snake_case` | v0.0.1 | +| `-client` | generate client declarations and runtime | `true` | v0.0.1 | +| `-emit=` | emit either `header` or `impl` | `header` | v0.0.1 | +| `-header=` | header include used by `impl` output | `.h` | v0.0.1 | + +## Notes + +- `-target` can be a local template directory or a git module path. +- `bigint` support is string-based by design to avoid precision loss in C JSON handling. +- for precision-sensitive large integer values, prefer `bigint` over `int64` / `uint64` in the schema when targeting this generator +- The generated implementation is tested with smoke, codec, succinct, and reference interop coverage in this repo. diff --git a/_examples/Makefile b/_examples/Makefile new file mode 100644 index 0000000..21584b3 --- /dev/null +++ b/_examples/Makefile @@ -0,0 +1,12 @@ +WEBRPC_GEN_VERSION := v0.37.1 +ROOT := .. +GEN := go run -ldflags="-X github.com/webrpc/webrpc.VERSION=$(WEBRPC_GEN_VERSION)" github.com/webrpc/webrpc/cmd/webrpc-gen@$(WEBRPC_GEN_VERSION) + +.PHONY: generate diff + +generate: + $(GEN) -schema=smoke/example.ridl -target=$(ROOT) -emit=header -out=smoke/example.gen.h + $(GEN) -schema=smoke/example.ridl -target=$(ROOT) -emit=impl -header=example.gen.h -out=smoke/example.gen.c + +diff: + git diff --exit-code -- smoke/example.gen.h smoke/example.gen.c diff --git a/_examples/smoke/example.gen.c b/_examples/smoke/example.gen.c new file mode 100644 index 0000000..0cbf4ca --- /dev/null +++ b/_examples/smoke/example.gen.c @@ -0,0 +1,1072 @@ + + + + + + + + + + + + + + + + + +// smoke v1.0.0 6dc82371b24d044c3b3e2bd4cf5d92af46788fff +// -- +// Code generated by webrpc-gen@v0.37.1 with .. generator. DO NOT EDIT. +// +// webrpc-gen -schema=smoke/example.ridl -target=.. -emit=impl -header=example.gen.h -out=smoke/example.gen.c + +#include "example.gen.h" + +#include +#include +#include +#include +#include + +typedef struct { + char *data; + size_t len; + size_t cap; +} smoke_buffer; + +#if defined(__GNUC__) || defined(__clang__) +#define SMOKE_JSON_UNUSED __attribute__((unused)) +#else +#define SMOKE_JSON_UNUSED +#endif + +static SMOKE_JSON_UNUSED cJSON *smoke_cjson_parse(const char *text) { + return cJSON_ParseWithOpts(text ? text : "", NULL, 1); +} + +static SMOKE_JSON_UNUSED char *smoke_cjson_print_dup(const cJSON *value) { + char *printed; + char *copy; + + if (!value) { + return smoke_strdup("null"); + } + + printed = cJSON_PrintUnformatted(value); + if (!printed) { + return NULL; + } + + copy = smoke_strdup(printed); + cJSON_free(printed); + return copy; +} + +static SMOKE_JSON_UNUSED int smoke_cjson_get_int64_exact( + const cJSON *value, + int64_t *out +) { + double parsed; + double integral; + + if (!out || !cJSON_IsNumber(value)) { + return 0; + } + + parsed = cJSON_GetNumberValue(value); + if (!isfinite(parsed)) { + return 0; + } + if (parsed < (double)INT64_MIN || parsed > (double)INT64_MAX) { + return 0; + } + if (modf(parsed, &integral) != 0.0) { + return 0; + } + + *out = (int64_t)integral; + return 1; +} + +static SMOKE_JSON_UNUSED int smoke_cjson_get_uint64_exact( + const cJSON *value, + uint64_t *out +) { + double parsed; + double integral; + + if (!out || !cJSON_IsNumber(value)) { + return 0; + } + + parsed = cJSON_GetNumberValue(value); + if (!isfinite(parsed)) { + return 0; + } + if (parsed < 0.0 || parsed > (double)UINT64_MAX) { + return 0; + } + if (modf(parsed, &integral) != 0.0) { + return 0; + } + + *out = (uint64_t)integral; + return 1; +} + +static SMOKE_JSON_UNUSED int smoke_cjson_get_double( + const cJSON *value, + double *out +) { + double parsed; + + if (!out || !cJSON_IsNumber(value)) { + return 0; + } + + parsed = cJSON_GetNumberValue(value); + if (!isfinite(parsed)) { + return 0; + } + + *out = parsed; + return 1; +} +static void smoke_set_error( + smoke_error *error, + int code, + int http_status, + const char *name, + const char *message, + const char *cause +) { + if (!error) return; + smoke_error_free(error); + error->code = code; + error->http_status = http_status; + error->name = smoke_strdup(name ? name : "WebrpcError"); + error->message = smoke_strdup(message ? message : "request failed"); + error->cause = cause ? smoke_strdup(cause) : NULL; +} + +static int smoke_buffer_grow(smoke_buffer *buf, size_t need) { + if (buf->cap >= need) return 1; + size_t new_cap = buf->cap ? buf->cap : 1024; + while (new_cap < need) new_cap *= 2; + char *next = (char *)realloc(buf->data, new_cap); + if (!next) return 0; + buf->data = next; + buf->cap = new_cap; + return 1; +} + +static size_t smoke_write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) { + size_t n = size * nmemb; + smoke_buffer *buf = (smoke_buffer *)userdata; + if (!smoke_buffer_grow(buf, buf->len + n + 1)) return 0; + memcpy(buf->data + buf->len, ptr, n); + buf->len += n; + buf->data[buf->len] = '\0'; + return n; +} + +static char *smoke_join_url(const char *base_url, const char *path) { + if (!base_url) return NULL; + if (!path) path = ""; + + size_t base_len = strlen(base_url); + size_t path_len = strlen(path); + int base_has_slash = base_len > 0 && base_url[base_len - 1] == '/'; + int path_has_slash = path_len > 0 && path[0] == '/'; + size_t cap = base_len + path_len + 2; + char *out = (char *)malloc(cap); + if (!out) return NULL; + + if (base_len == 0) { + snprintf(out, cap, "%s", path); + } else if (path_len == 0) { + snprintf(out, cap, "%s", base_url); + } else if (base_has_slash && path_has_slash) { + snprintf(out, cap, "%.*s%s", (int)(base_len - 1), base_url, path); + } else if (!base_has_slash && !path_has_slash) { + snprintf(out, cap, "%s/%s", base_url, path); + } else { + snprintf(out, cap, "%s%s", base_url, path); + } + return out; +} + +static int smoke_append_header(struct curl_slist **headers, const char *header_value) { + struct curl_slist *next; + + if (!headers || !header_value) return 0; + next = curl_slist_append(*headers, header_value); + if (!next) return 0; + *headers = next; + return 1; +} + +static int smoke_header_name_equals(const char *header, const char *name) { + size_t i = 0; + + if (!header || !name) return 0; + + while (header[i] != '\0' && header[i] != ':' && name[i] != '\0' && name[i] != ':') { + unsigned char h = (unsigned char)header[i]; + unsigned char n = (unsigned char)name[i]; + if (h >= 'A' && h <= 'Z') h = (unsigned char)(h - 'A' + 'a'); + if (n >= 'A' && n <= 'Z') n = (unsigned char)(n - 'A' + 'a'); + if (h != n) return 0; + i++; + } + + while (header[i] == ' ' || header[i] == '\t') { + i++; + } + + while (name[i] == ' ' || name[i] == '\t') { + i++; + } + + return (name[i] == '\0' || name[i] == ':') && (header[i] == '\0' || header[i] == ':'); +} + +static int smoke_prepared_request_has_header(const smoke_prepared_request *request, const char *name) { + if (!request || !name || !request->headers) return 0; + + for (size_t i = 0; i < request->headers_count; ++i) { + if (request->headers[i] && smoke_header_name_equals(request->headers[i], name)) { + return 1; + } + } + + return 0; +} + +static int smoke_prepared_request_overrides_header(const smoke_prepared_request *request, const char *header_line) { + if (!request || !header_line) return 0; + + if (smoke_header_name_equals(header_line, "Content-Type")) { + if ((request->content_type && request->content_type[0] != '\0') || + smoke_prepared_request_has_header(request, "Content-Type")) { + return 1; + } + } + + if (!request->headers) return 0; + for (size_t i = 0; i < request->headers_count; ++i) { + if (request->headers[i] && + smoke_header_name_equals(request->headers[i], header_line)) { + return 1; + } + } + + return 0; +} + +static int smoke_curl_runtime_init(smoke_error *error) { + static int initialized = 0; + + if (initialized) { + return 0; + } + + if (curl_global_init(CURL_GLOBAL_DEFAULT) != 0) { + smoke_set_error(error, 0, 0, "TransportError", "curl_global_init failed", NULL); + return -1; + } + + if (atexit(curl_global_cleanup) != 0) { + curl_global_cleanup(); + smoke_set_error(error, 0, 0, "TransportError", "failed to register curl_global_cleanup", NULL); + return -1; + } + + initialized = 1; + return 0; +} + +static int smoke_http_send_request( + const char *base_url, + const smoke_prepared_request *request, + const char *bearer_token, + const struct curl_slist *default_headers, + long timeout_ms, + smoke_http_response *response, + smoke_error *error +) { + const char *http_method; + smoke_http_response result; + int has_content_type = 0; + int has_authorization = 0; + smoke_http_response_init(&result); + + if (!request || !response) { + smoke_set_error(error, 0, 0, "ClientError", "request and response must be non-NULL", NULL); + return -1; + } + + if (smoke_curl_runtime_init(error) != 0) { + return -1; + } + + CURL *curl = curl_easy_init(); + if (!curl) { + smoke_set_error(error, 0, 0, "TransportError", "curl_easy_init failed", NULL); + return -1; + } + + char *url = smoke_join_url(base_url, request->path ? request->path : ""); + if (!url) { + smoke_set_error(error, 0, 0, "TransportError", "failed to build URL", NULL); + curl_easy_cleanup(curl); + return -1; + } + + struct curl_slist *headers = NULL; + for (const struct curl_slist *it = default_headers; it; it = it->next) { + if ((bearer_token && bearer_token[0] != '\0' && smoke_header_name_equals(it->data, "Authorization")) || + smoke_prepared_request_overrides_header(request, it->data)) { + continue; + } + if (!smoke_append_header(&headers, it->data)) { + smoke_set_error(error, 0, 0, "TransportError", "failed to copy request headers", NULL); + curl_slist_free_all(headers); + free(url); + curl_easy_cleanup(curl); + return -1; + } + if (smoke_header_name_equals(it->data, "Content-Type")) { + has_content_type = 1; + } + if (smoke_header_name_equals(it->data, "Authorization")) { + has_authorization = 1; + } + } + + if (request->content_type && request->content_type[0] != '\0') { + size_t need = strlen("Content-Type: ") + strlen(request->content_type) + 1; + char *content_type_header = (char *)malloc(need); + if (!content_type_header) { + smoke_set_error(error, 0, 0, "TransportError", "failed to allocate content-type header", NULL); + curl_slist_free_all(headers); + free(url); + curl_easy_cleanup(curl); + return -1; + } + snprintf(content_type_header, need, "Content-Type: %s", request->content_type); + if (!smoke_append_header(&headers, content_type_header)) { + smoke_set_error(error, 0, 0, "TransportError", "failed to append content-type header", NULL); + free(content_type_header); + curl_slist_free_all(headers); + free(url); + curl_easy_cleanup(curl); + return -1; + } + free(content_type_header); + has_content_type = 1; + } + + for (size_t i = 0; i < request->headers_count; ++i) { + if (!request->headers || !request->headers[i]) { + smoke_set_error(error, 0, 0, "TransportError", "request headers are invalid", NULL); + curl_slist_free_all(headers); + free(url); + curl_easy_cleanup(curl); + return -1; + } + if (!smoke_append_header(&headers, request->headers[i])) { + smoke_set_error(error, 0, 0, "TransportError", "failed to append request header", NULL); + curl_slist_free_all(headers); + free(url); + curl_easy_cleanup(curl); + return -1; + } + if (smoke_header_name_equals(request->headers[i], "Content-Type")) { + has_content_type = 1; + } + if (smoke_header_name_equals(request->headers[i], "Authorization")) { + has_authorization = 1; + } + } + + if (!has_content_type && !smoke_append_header(&headers, "Content-Type: application/json")) { + smoke_set_error(error, 0, 0, "TransportError", "failed to allocate request headers", NULL); + curl_slist_free_all(headers); + free(url); + curl_easy_cleanup(curl); + return -1; + } + if (bearer_token && bearer_token[0] != '\0' && !has_authorization) { + size_t need = strlen("Authorization: Bearer ") + strlen(bearer_token) + 1; + char *auth_header = (char *)malloc(need); + if (!auth_header) { + smoke_set_error(error, 0, 0, "TransportError", "failed to allocate auth header", NULL); + curl_slist_free_all(headers); + free(url); + curl_easy_cleanup(curl); + return -1; + } + snprintf(auth_header, need, "Authorization: Bearer %s", bearer_token); + if (!smoke_append_header(&headers, auth_header)) { + smoke_set_error(error, 0, 0, "TransportError", "failed to append auth header", NULL); + free(auth_header); + curl_slist_free_all(headers); + free(url); + curl_easy_cleanup(curl); + return -1; + } + free(auth_header); + } + + smoke_buffer buf; + memset(&buf, 0, sizeof(buf)); + http_method = request->http_method && request->http_method[0] != '\0' ? request->http_method : "POST"; + + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, http_method); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request->body ? request->body : ""); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)(request->body ? request->body_len : 0)); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms > 0 ? timeout_ms : 10000L); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, smoke_write_cb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf); + + CURLcode rc = curl_easy_perform(curl); + if (rc != CURLE_OK) { + smoke_set_error(error, 0, 0, "TransportError", "HTTP request failed", curl_easy_strerror(rc)); + } else { + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &result.status_code); + result.body = buf.data ? buf.data : smoke_strdup(""); + result.body_len = buf.len; + buf.data = NULL; + } + + free(buf.data); + curl_slist_free_all(headers); + free(url); + curl_easy_cleanup(curl); + if (rc != CURLE_OK) { + smoke_http_response_free(&result); + return -1; + } + + *response = result; + memset(&result, 0, sizeof(result)); + return 0; +} + +static void smoke_parse_rpc_error(const char *body, long http_status, smoke_error *error) { + cJSON *error_name = NULL; + cJSON *error_code = NULL; + cJSON *error_msg = NULL; + cJSON *error_cause = NULL; + cJSON *error_status = NULL; + int64_t parsed_code = 0; + int64_t parsed_status = (int64_t)http_status; + + if (!body || body[0] == '\0') { + smoke_set_error(error, 0, (int)http_status, "WebrpcRequestFailed", "request failed", NULL); + return; + } + + cJSON *root = smoke_cjson_parse(body); + if (!root || !cJSON_IsObject(root)) { + if (root) cJSON_Delete(root); + smoke_set_error(error, 0, (int)http_status, "WebrpcRequestFailed", "request failed", body); + return; + } + + error_name = cJSON_GetObjectItemCaseSensitive(root, "error"); + error_code = cJSON_GetObjectItemCaseSensitive(root, "code"); + error_msg = cJSON_GetObjectItemCaseSensitive(root, "msg"); + error_cause = cJSON_GetObjectItemCaseSensitive(root, "cause"); + error_status = cJSON_GetObjectItemCaseSensitive(root, "status"); + if (error_code && !cJSON_IsNull(error_code)) { + (void)smoke_cjson_get_int64_exact(error_code, &parsed_code); + } + if (error_status && !cJSON_IsNull(error_status)) { + (void)smoke_cjson_get_int64_exact(error_status, &parsed_status); + } + + smoke_set_error( + error, + (int)parsed_code, + (int)parsed_status, + cJSON_IsString(error_name) && cJSON_GetStringValue(error_name) ? cJSON_GetStringValue(error_name) : "WebrpcRequestFailed", + cJSON_IsString(error_msg) && cJSON_GetStringValue(error_msg) ? cJSON_GetStringValue(error_msg) : "request failed", + cJSON_IsString(error_cause) ? cJSON_GetStringValue(error_cause) : NULL + ); + cJSON_Delete(root); +} +static SMOKE_JSON_UNUSED cJSON *smoke_profile_to_json(const smoke_profile *value); +static SMOKE_JSON_UNUSED int smoke_profile_from_json(const cJSON *json, smoke_profile *out, smoke_error *error); + + +static SMOKE_JSON_UNUSED cJSON *smoke_profile_to_json(const smoke_profile *value) { + if (!value) return NULL; + cJSON *root = cJSON_CreateObject(); + if (!root) return NULL; + { + cJSON *field_json = NULL; + + field_json = value->id.digits ? cJSON_CreateString(value->id.digits) : cJSON_CreateNull(); + if (!field_json) goto fail; + if (!cJSON_AddItemToObject(root, "id", field_json)) { + cJSON_Delete(field_json); + goto fail; + } + } + { + cJSON *field_json = NULL; + + field_json = value->name ? cJSON_CreateString(value->name) : cJSON_CreateNull(); + if (!field_json) goto fail; + if (!cJSON_AddItemToObject(root, "name", field_json)) { + cJSON_Delete(field_json); + goto fail; + } + } + { + cJSON *field_json = NULL; + + field_json = cJSON_CreateString(smoke_role_to_string(value->role)); + if (!field_json) goto fail; + if (!cJSON_AddItemToObject(root, "role", field_json)) { + cJSON_Delete(field_json); + goto fail; + } + } + { + if (value->has_tags) { + cJSON *field_json = NULL; +{ + cJSON *array_json = cJSON_CreateArray(); + if (!array_json) goto fail; + for (size_t list_idx_0 = 0; list_idx_0 < value->tags.count; ++list_idx_0) { + cJSON *list_entry_json_0 = NULL; + + list_entry_json_0 = value->tags.items[list_idx_0] ? cJSON_CreateString(value->tags.items[list_idx_0]) : cJSON_CreateNull(); + if (!list_entry_json_0) goto fail; + if (!cJSON_AddItemToArray(array_json, list_entry_json_0)) { + cJSON_Delete(list_entry_json_0); + cJSON_Delete(array_json); + goto fail; + } + } + field_json = array_json; + } + if (!cJSON_AddItemToObject(root, "tags", field_json)) { + cJSON_Delete(field_json); + goto fail; + } + } + } + { + if (value->has_meta) { + cJSON *field_json = NULL; + + { + cJSON *object_json = cJSON_CreateObject(); + if (!object_json) goto fail; + + for (size_t map_idx_0 = 0; map_idx_0 < value->meta.count; ++map_idx_0) { + const char *map_key_0 = value->meta.keys[map_idx_0]; + cJSON *map_value_json_0 = NULL; + + map_value_json_0 = value->meta.values[map_idx_0] ? cJSON_CreateString(value->meta.values[map_idx_0]) : cJSON_CreateNull(); + if (!map_value_json_0) goto fail; + if (!map_key_0) { + cJSON_Delete(map_value_json_0); + cJSON_Delete(object_json); + goto fail; + } + if (!cJSON_AddItemToObject(object_json, map_key_0, map_value_json_0)) { + cJSON_Delete(map_value_json_0); + cJSON_Delete(object_json); + goto fail; + } + } + field_json = object_json; + } + if (!cJSON_AddItemToObject(root, "meta", field_json)) { + cJSON_Delete(field_json); + goto fail; + } + } + } + return root; +fail: + cJSON_Delete(root); + return NULL; +} +static SMOKE_JSON_UNUSED int smoke_profile_from_json(const cJSON *json, smoke_profile *out, smoke_error *error) { + if (!out) { + smoke_set_error(error, 0, 0, "DecodeError", "output pointer is NULL", NULL); + return -1; + } + if (!json || cJSON_IsNull(json)) return 0; + if (!cJSON_IsObject(json)) { + smoke_set_error(error, 0, 0, "DecodeError", "expected JSON object", NULL); + return -1; + } + { + cJSON *field_json = cJSON_GetObjectItemCaseSensitive(json, "id"); + int field_present = field_json != NULL; + if (!field_present || cJSON_IsNull(field_json)) { + smoke_set_error(error, 0, 0, "DecodeError", "missing required field id", NULL); + goto fail; + } + + if (!cJSON_IsString(field_json) || !cJSON_GetStringValue(field_json) || smoke_bigint_set_string(&out->id, cJSON_GetStringValue(field_json)) != 0) { + smoke_set_error(error, 0, 0, "DecodeError", "expected bigint string", NULL); + goto fail; + } + } + { + cJSON *field_json = cJSON_GetObjectItemCaseSensitive(json, "name"); + int field_present = field_json != NULL; + if (!field_present || cJSON_IsNull(field_json)) { + smoke_set_error(error, 0, 0, "DecodeError", "missing required field name", NULL); + goto fail; + } + + if (!cJSON_IsString(field_json) || !cJSON_GetStringValue(field_json)) { + smoke_set_error(error, 0, 0, "DecodeError", "expected string", NULL); + goto fail; + } + out->name = smoke_strdup(cJSON_GetStringValue(field_json)); + if (!out->name) { + smoke_set_error(error, 0, 0, "DecodeError", "out of memory decoding string", NULL); + goto fail; + } + } + { + cJSON *field_json = cJSON_GetObjectItemCaseSensitive(json, "role"); + int field_present = field_json != NULL; + if (!field_present || cJSON_IsNull(field_json)) { + smoke_set_error(error, 0, 0, "DecodeError", "missing required field role", NULL); + goto fail; + } + + if (!cJSON_IsString(field_json) || !cJSON_GetStringValue(field_json) || smoke_role_from_string(cJSON_GetStringValue(field_json), &out->role) != 0) { + smoke_set_error(error, 0, 0, "DecodeError", "expected enum string", NULL); + goto fail; + } + } + { + cJSON *field_json = cJSON_GetObjectItemCaseSensitive(json, "tags"); + int field_present = field_json != NULL; + if (field_present) { + out->has_tags = true; + if (!cJSON_IsNull(field_json)) { +if (!cJSON_IsArray(field_json)) { + smoke_set_error(error, 0, 0, "DecodeError", "expected JSON array", NULL); + goto fail; + } + { + size_t count = (size_t)cJSON_GetArraySize(field_json); + out->tags.items = count ? calloc(count, sizeof(*out->tags.items)) : NULL; + if (count && !out->tags.items) { + smoke_set_error(error, 0, 0, "DecodeError", "out of memory decoding array", NULL); + goto fail; + } + out->tags.count = count; + for (size_t list_idx_0 = 0; list_idx_0 < count; ++list_idx_0) { + cJSON *list_entry_0 = cJSON_GetArrayItem(field_json, (int)list_idx_0); + + if (!cJSON_IsString(list_entry_0) || !cJSON_GetStringValue(list_entry_0)) { + smoke_set_error(error, 0, 0, "DecodeError", "expected string", NULL); + goto fail; + } + out->tags.items[list_idx_0] = smoke_strdup(cJSON_GetStringValue(list_entry_0)); + if (!out->tags.items[list_idx_0]) { + smoke_set_error(error, 0, 0, "DecodeError", "out of memory decoding string", NULL); + goto fail; + } + } + } + } + } + } + { + cJSON *field_json = cJSON_GetObjectItemCaseSensitive(json, "meta"); + int field_present = field_json != NULL; + if (field_present) { + out->has_meta = true; + if (!cJSON_IsNull(field_json)) { + + if (!cJSON_IsObject(field_json)) { + smoke_set_error(error, 0, 0, "DecodeError", "expected JSON object", NULL); + goto fail; + } + + { + size_t count = (size_t)cJSON_GetArraySize(field_json); + out->meta.keys = count ? calloc(count, sizeof(*out->meta.keys)) : NULL; + out->meta.values = count ? calloc(count, sizeof(*out->meta.values)) : NULL; + if (count && (!out->meta.keys || !out->meta.values)) { + smoke_set_error(error, 0, 0, "DecodeError", "out of memory decoding map", NULL); + goto fail; + } + out->meta.count = count; + size_t map_idx_0 = 0; + cJSON *map_entry_0 = NULL; + cJSON_ArrayForEach(map_entry_0, field_json) { + const char *entry_key = map_entry_0 ? map_entry_0->string : NULL; + if (!entry_key) { + smoke_set_error(error, 0, 0, "DecodeError", "missing map key", NULL); + goto fail; + } + + out->meta.keys[map_idx_0] = smoke_strdup(entry_key); + if (!out->meta.keys[map_idx_0]) { + smoke_set_error(error, 0, 0, "DecodeError", "out of memory decoding map key", NULL); + goto fail; + } + + if (!cJSON_IsString(map_entry_0) || !cJSON_GetStringValue(map_entry_0)) { + smoke_set_error(error, 0, 0, "DecodeError", "expected string", NULL); + goto fail; + } + out->meta.values[map_idx_0] = smoke_strdup(cJSON_GetStringValue(map_entry_0)); + if (!out->meta.values[map_idx_0]) { + smoke_set_error(error, 0, 0, "DecodeError", "out of memory decoding string", NULL); + goto fail; + } + map_idx_0++; + } + } + } + } + } + return 0; +fail: + smoke_profile_free(out); + return -1; +} +static int smoke_prepare_json_request( + cJSON *request_json, + const char *path, + smoke_prepared_request *prepared_request, + smoke_error *error +) { + int rc = -1; + char *request_body = NULL; + smoke_prepared_request prepared; + smoke_prepared_request_init(&prepared); + + if (!request_json || !path || !prepared_request) { + smoke_set_error(error, 0, 0, "ClientError", "request_json, path and prepared_request must be non-NULL", NULL); + goto fail; + } + + request_body = smoke_cjson_print_dup(request_json); + if (!request_body) { + smoke_set_error(error, 0, 0, "EncodeError", "failed to print request JSON", NULL); + goto fail; + } + + prepared.http_method = smoke_strdup("POST"); + prepared.path = smoke_strdup(path); + prepared.content_type = smoke_strdup("application/json"); + if (!prepared.http_method || !prepared.path || !prepared.content_type) { + smoke_set_error(error, 0, 0, "EncodeError", "failed to allocate prepared request metadata", NULL); + goto fail; + } + + prepared.body = request_body; + prepared.body_len = strlen(request_body); + request_body = NULL; + + *prepared_request = prepared; + memset(&prepared, 0, sizeof(prepared)); + rc = 0; + +fail: + smoke_prepared_request_free(&prepared); + free(request_body); + return rc; +} + +static int smoke_parse_json_response( + const smoke_http_response *http_response, + cJSON **response_json, + smoke_error *error +) { + if (!http_response || !response_json) { + smoke_set_error(error, 0, 0, "ClientError", "http_response and response_json must be non-NULL", NULL); + return -1; + } + if (http_response->status_code < 200 || http_response->status_code >= 300) { + smoke_parse_rpc_error(http_response->body, http_response->status_code, error); + return -1; + } + + *response_json = smoke_cjson_parse(http_response->body ? http_response->body : "{}"); + if (!*response_json) { + smoke_set_error(error, 0, (int)http_response->status_code, "DecodeError", "failed to parse response JSON", http_response->body); + return -1; + } + + return 0; +} +static cJSON *smoke_smoke_echo_request_to_json(const smoke_smoke_echo_request *value) { + if (!value) return cJSON_CreateObject(); + cJSON *root = cJSON_CreateObject(); + if (!root) return NULL; + { + cJSON *field_json = NULL; + + if (value->profile) { + field_json = smoke_profile_to_json(value->profile); + } else { + field_json = cJSON_CreateNull(); + } + if (!field_json) goto fail; + if (!cJSON_AddItemToObject(root, "profile", field_json)) { + cJSON_Delete(field_json); + goto fail; + } + } + return root; +fail: + cJSON_Delete(root); + return NULL; +} + +static int smoke_smoke_echo_response_from_json(const cJSON *json, smoke_smoke_echo_response *out, smoke_error *error) { + if (!out) { + smoke_set_error(error, 0, 0, "DecodeError", "output pointer is NULL", NULL); + return -1; + } + if (!json || !cJSON_IsObject(json)) { + smoke_set_error(error, 0, 0, "DecodeError", "expected JSON object", NULL); + return -1; + } + { + cJSON *field_json = cJSON_GetObjectItemCaseSensitive(json, "profile"); + int field_present = field_json != NULL; + if (!field_present || cJSON_IsNull(field_json)) { + smoke_set_error(error, 0, 0, "DecodeError", "missing required field profile", NULL); + goto fail; + } + + if (!cJSON_IsObject(field_json)) { + smoke_set_error(error, 0, 0, "DecodeError", "expected JSON object", NULL); + goto fail; + } + out->profile = (smoke_profile *)calloc(1, sizeof(*out->profile)); + if (!out->profile) { + smoke_set_error(error, 0, 0, "DecodeError", "out of memory decoding object", NULL); + goto fail; + } + if (smoke_profile_from_json(field_json, out->profile, error) != 0) { + goto fail; + } + } + return 0; +fail: + smoke_smoke_echo_response_free(out); + return -1; +} + +int smoke_smoke_echo_prepare_request( + const smoke_smoke_echo_request *request, + smoke_prepared_request *prepared_request, + smoke_error *error +) { + int rc = -1; + cJSON *request_json = NULL; + + if (!request || !prepared_request) { + smoke_set_error(error, 0, 0, "ClientError", "request and prepared_request must be non-NULL", NULL); + goto fail; + } + + request_json = smoke_smoke_echo_request_to_json(request); + if (!request_json) { + smoke_set_error(error, 0, 0, "EncodeError", "failed to encode request JSON", NULL); + goto fail; + } + + rc = smoke_prepare_json_request(request_json, "/rpc/Smoke/Echo", prepared_request, error); + +fail: + cJSON_Delete(request_json); + return rc; +} + +int smoke_smoke_echo_parse_response( + const smoke_http_response *http_response, + smoke_smoke_echo_response *response, + smoke_error *error +) { + int rc = -1; + cJSON *response_json = NULL; + smoke_smoke_echo_response parsed_response; + smoke_smoke_echo_response_init(&parsed_response); + + if (!http_response || !response) { + smoke_set_error(error, 0, 0, "ClientError", "http_response and response must be non-NULL", NULL); + goto fail; + } + if (smoke_parse_json_response(http_response, &response_json, error) != 0) { + goto fail; + } + if (smoke_smoke_echo_response_from_json(response_json, &parsed_response, error) != 0) { + goto fail; + } + + *response = parsed_response; + memset(&parsed_response, 0, sizeof(parsed_response)); + rc = 0; + +fail: + smoke_smoke_echo_response_free(&parsed_response); + cJSON_Delete(response_json); + return rc; +} + +struct smoke_smoke_client { + char *base_url; + char *bearer_token; + struct curl_slist *default_headers; + long timeout_ms; +}; + +static void smoke_smoke_client_reset_config(smoke_smoke_client *client) { + if (!client) return; + free(client->bearer_token); + client->bearer_token = NULL; + if (client->default_headers) { + curl_slist_free_all(client->default_headers); + client->default_headers = NULL; + } + client->timeout_ms = 10000L; +} + +int smoke_smoke_client_configure(smoke_smoke_client *client, const smoke_client_options *options) { + if (!client) return 0; + + smoke_smoke_client_reset_config(client); + if (!options) return 1; + + if (options->timeout_ms > 0) { + client->timeout_ms = options->timeout_ms; + } + + if (options->bearer_token) { + client->bearer_token = smoke_strdup(options->bearer_token); + if (!client->bearer_token) { + smoke_smoke_client_reset_config(client); + return 0; + } + } + + if (options->headers_count > 0) { + if (!options->headers) { + smoke_smoke_client_reset_config(client); + return 0; + } + for (size_t i = 0; i < options->headers_count; ++i) { + if (!options->headers[i]) { + smoke_smoke_client_reset_config(client); + return 0; + } + struct curl_slist *next = curl_slist_append(client->default_headers, options->headers[i]); + if (!next) { + smoke_smoke_client_reset_config(client); + return 0; + } + client->default_headers = next; + } + } + + return 1; +} + +smoke_smoke_client *smoke_smoke_client_create(const char *base_url, const smoke_client_options *options) { + smoke_smoke_client *client = + (smoke_smoke_client *)calloc(1, sizeof(*client)); + if (!client) return NULL; + client->base_url = smoke_strdup(base_url ? base_url : ""); + if (!client->base_url) { + free(client); + return NULL; + } + smoke_smoke_client_reset_config(client); + if (!smoke_smoke_client_configure(client, options)) { + free(client->base_url); + client->base_url = NULL; + smoke_smoke_client_reset_config(client); + free(client); + return NULL; + } + return client; +} + +void smoke_smoke_client_destroy(smoke_smoke_client *client) { + if (!client) return; + free(client->base_url); + client->base_url = NULL; + smoke_smoke_client_reset_config(client); + free(client); +} + +int smoke_smoke_client_send_prepared_request( + smoke_smoke_client *client, + const smoke_prepared_request *request, + smoke_http_response *response, + smoke_error *error +) { + if (!client || !request || !response) { + smoke_set_error(error, 0, 0, "ClientError", "client, request, and response must be non-NULL", NULL); + return -1; + } + + return smoke_http_send_request( + client->base_url, + request, + client->bearer_token, + client->default_headers, + client->timeout_ms, + response, + error + ); +} +int smoke_smoke_echo( + smoke_smoke_client *client, + const smoke_smoke_echo_request *request, + smoke_smoke_echo_response *response, + smoke_error *error +) { + int rc = -1; + smoke_prepared_request prepared_request; + smoke_http_response http_response; + smoke_prepared_request_init(&prepared_request); + smoke_http_response_init(&http_response); + + if (!client || !request || !response) { + smoke_set_error(error, 0, 0, "ClientError", "client, request, and response must be non-NULL", NULL); + goto fail; + } + + if (smoke_smoke_echo_prepare_request(request, &prepared_request, error) != 0) { + goto fail; + } + if (smoke_smoke_client_send_prepared_request(client, &prepared_request, &http_response, error) != 0) { + goto fail; + } + if (smoke_smoke_echo_parse_response(&http_response, response, error) != 0) { + goto fail; + } + rc = 0; + +fail: + smoke_prepared_request_free(&prepared_request); + smoke_http_response_free(&http_response); + return rc; +} \ No newline at end of file diff --git a/_examples/smoke/example.gen.h b/_examples/smoke/example.gen.h new file mode 100644 index 0000000..ebc7c8d --- /dev/null +++ b/_examples/smoke/example.gen.h @@ -0,0 +1,374 @@ +#ifndef SMOKE_SMOKE_HEADER_H +#define SMOKE_SMOKE_HEADER_H + +// smoke v1.0.0 6dc82371b24d044c3b3e2bd4cf5d92af46788fff +// -- +// Code generated by webrpc-gen@v0.37.1 with .. generator. DO NOT EDIT. +// +// webrpc-gen -schema=smoke/example.ridl -target=.. -emit=header -out=smoke/example.gen.h + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + char *digits; +} smoke_bigint; + +typedef struct { + char *value; +} smoke_timestamp; + +typedef struct { + int code; + int http_status; + char *name; + char *message; + char *cause; +} smoke_error; + +typedef struct { + char *http_method; + char *path; + char *body; + size_t body_len; + char *content_type; + char **headers; + size_t headers_count; +} smoke_prepared_request; + +typedef struct { + long status_code; + char *body; + size_t body_len; +} smoke_http_response; + +typedef struct { + const char *bearer_token; + const char * const *headers; + size_t headers_count; + long timeout_ms; +} smoke_client_options; + +static inline char *smoke_strdup(const char *value) { + if (!value) return NULL; + size_t n = strlen(value) + 1; + char *copy = (char *)malloc(n); + if (!copy) return NULL; + memcpy(copy, value, n); + return copy; +} + +static inline void smoke_bigint_init(smoke_bigint *value) { + if (!value) return; + value->digits = NULL; +} + +static inline void smoke_bigint_free(smoke_bigint *value) { + if (!value) return; + free(value->digits); + value->digits = NULL; +} + +static inline int smoke_bigint_set_string(smoke_bigint *value, const char *digits) { + if (!value || !digits || digits[0] == '\0') return -1; + + size_t start = 0; + bool negative = false; + if (digits[0] == '+') return -1; + if (digits[0] == '-') { + negative = true; + start = 1; + } + if (digits[start] == '\0') return -1; + + for (size_t i = start; digits[i] != '\0'; ++i) { + if (digits[i] < '0' || digits[i] > '9') return -1; + } + + while (digits[start] == '0' && digits[start + 1] != '\0') { + start++; + } + if (digits[start] == '0') negative = false; + + size_t len = strlen(digits + start); + size_t total = len + (negative ? 2 : 1); + char *normalized = (char *)malloc(total); + if (!normalized) return -1; + + if (negative) { + normalized[0] = '-'; + memcpy(normalized + 1, digits + start, len + 1); + } else { + memcpy(normalized, digits + start, len + 1); + } + + free(value->digits); + value->digits = normalized; + return 0; +} + +static inline void smoke_timestamp_init(smoke_timestamp *value) { + if (!value) return; + value->value = NULL; +} + +static inline void smoke_timestamp_free(smoke_timestamp *value) { + if (!value) return; + free(value->value); + value->value = NULL; +} + +static inline int smoke_timestamp_set_string(smoke_timestamp *value, const char *timestamp) { + if (!value) return -1; + char *copy = smoke_strdup(timestamp); + if (timestamp && !copy) return -1; + free(value->value); + value->value = copy; + return 0; +} + +static inline void smoke_error_init(smoke_error *error) { + if (!error) return; + memset(error, 0, sizeof(*error)); +} + +static inline void smoke_error_free(smoke_error *error) { + if (!error) return; + free(error->name); + free(error->message); + free(error->cause); + memset(error, 0, sizeof(*error)); +} + +static inline void smoke_prepared_request_init(smoke_prepared_request *request) { + if (!request) return; + memset(request, 0, sizeof(*request)); +} + +static inline void smoke_prepared_request_free(smoke_prepared_request *request) { + if (!request) return; + free(request->http_method); + free(request->path); + free(request->body); + free(request->content_type); + if (request->headers) { + for (size_t i = 0; i < request->headers_count; ++i) { + free(request->headers[i]); + } + free(request->headers); + } + memset(request, 0, sizeof(*request)); +} + +static inline int smoke_prepared_request_add_header(smoke_prepared_request *request, const char *header) { + char **next_headers; + char *copy; + + if (!request || !header) return -1; + + next_headers = (char **)realloc(request->headers, sizeof(*next_headers) * (request->headers_count + 1)); + if (!next_headers) return -1; + request->headers = next_headers; + + copy = smoke_strdup(header); + if (!copy) { + return -1; + } + + request->headers[request->headers_count] = copy; + request->headers_count += 1; + return 0; +} + +static inline void smoke_http_response_init(smoke_http_response *response) { + if (!response) return; + memset(response, 0, sizeof(*response)); +} + +static inline void smoke_http_response_free(smoke_http_response *response) { + if (!response) return; + free(response->body); + response->body = NULL; + response->body_len = 0; + response->status_code = 0; +} + +static inline void smoke_client_options_init(smoke_client_options *options) { + if (!options) return; + memset(options, 0, sizeof(*options)); + options->timeout_ms = 10000L; +} + + +typedef struct smoke_profile smoke_profile; +static inline void smoke_profile_init(smoke_profile *value); +static inline void smoke_profile_free(smoke_profile *value); +typedef enum smoke_role { + SMOKE_ROLE_UNKNOWN = 0, + SMOKE_ROLE_USER, + SMOKE_ROLE_ADMIN, +} smoke_role; + +static inline const char *smoke_role_to_string(smoke_role value) { + switch (value) { + case SMOKE_ROLE_UNKNOWN: return "UNKNOWN"; + case SMOKE_ROLE_USER: return "USER"; + case SMOKE_ROLE_ADMIN: return "ADMIN"; + default: return "UNKNOWN"; + } +} + +static inline int smoke_role_from_string(const char *value, smoke_role *out) { + if (!out || !value) return -1; + if (strcmp(value, "USER") == 0) { + *out = SMOKE_ROLE_USER; + return 0; + } + if (strcmp(value, "ADMIN") == 0) { + *out = SMOKE_ROLE_ADMIN; + return 0; + } + *out = SMOKE_ROLE_UNKNOWN; + return -1; +} +struct smoke_profile { + smoke_bigint id; + char * name; + smoke_role role; + bool has_tags; + struct { + char * *items; + size_t count; +} tags; + bool has_meta; + struct { + char * *keys; + char * *values; + size_t count; +} meta; +}; + +static inline void smoke_profile_init(smoke_profile *value) { + if (!value) return; + memset(value, 0, sizeof(*value)); +} + +static inline void smoke_profile_free(smoke_profile *value) { + if (!value) return; + + smoke_bigint_free(&value->id); + + free(value->name); + value->name = NULL; + + + if (value->tags.items) { + for (size_t free_list_idx_0 = 0; free_list_idx_0 < value->tags.count; ++free_list_idx_0) { + + free(value->tags.items[free_list_idx_0]); + value->tags.items[free_list_idx_0] = NULL; + } + free(value->tags.items); + value->tags.items = NULL; + value->tags.count = 0; + } + + if (value->meta.keys || value->meta.values) { + for (size_t free_map_idx_0 = 0; free_map_idx_0 < value->meta.count; ++free_map_idx_0) { + + free(value->meta.keys[free_map_idx_0]); + value->meta.keys[free_map_idx_0] = NULL; + + free(value->meta.values[free_map_idx_0]); + value->meta.values[free_map_idx_0] = NULL; + } + free(value->meta.keys); + free(value->meta.values); + value->meta.keys = NULL; + value->meta.values = NULL; + value->meta.count = 0; + } + memset(value, 0, sizeof(*value)); +} +typedef struct smoke_smoke_echo_request { + smoke_profile * profile; +} smoke_smoke_echo_request; + +static inline void smoke_smoke_echo_request_init(smoke_smoke_echo_request *value) { + if (!value) return; + memset(value, 0, sizeof(*value)); +} + +static inline void smoke_smoke_echo_request_free(smoke_smoke_echo_request *value) { + if (!value) return; + + if (value->profile) { + smoke_profile_free(value->profile); + free(value->profile); + value->profile = NULL; + } + memset(value, 0, sizeof(*value)); +} + +typedef struct smoke_smoke_echo_response { + smoke_profile * profile; +} smoke_smoke_echo_response; + +static inline void smoke_smoke_echo_response_init(smoke_smoke_echo_response *value) { + if (!value) return; + memset(value, 0, sizeof(*value)); +} + +static inline void smoke_smoke_echo_response_free(smoke_smoke_echo_response *value) { + if (!value) return; + + if (value->profile) { + smoke_profile_free(value->profile); + free(value->profile); + value->profile = NULL; + } + memset(value, 0, sizeof(*value)); +} + +typedef struct smoke_smoke_client smoke_smoke_client; + +smoke_smoke_client *smoke_smoke_client_create(const char *base_url, const smoke_client_options *options); +void smoke_smoke_client_destroy(smoke_smoke_client *client); +int smoke_smoke_client_configure(smoke_smoke_client *client, const smoke_client_options *options); +int smoke_smoke_client_send_prepared_request( + smoke_smoke_client *client, + const smoke_prepared_request *request, + smoke_http_response *response, + smoke_error *error +); +int smoke_smoke_echo_prepare_request( + const smoke_smoke_echo_request *request, + smoke_prepared_request *prepared_request, + smoke_error *error +); + +int smoke_smoke_echo_parse_response( + const smoke_http_response *http_response, + smoke_smoke_echo_response *response, + smoke_error *error +); + +int smoke_smoke_echo( + smoke_smoke_client *client, + const smoke_smoke_echo_request *request, + smoke_smoke_echo_response *response, + smoke_error *error +); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // SMOKE_SMOKE_HEADER_H \ No newline at end of file diff --git a/_examples/smoke/example.ridl b/_examples/smoke/example.ridl new file mode 100644 index 0000000..7f40c28 --- /dev/null +++ b/_examples/smoke/example.ridl @@ -0,0 +1,19 @@ +webrpc = v1 + +name = smoke +version = v1.0.0 +basepath = /rpc + +enum Role: uint32 + - USER + - ADMIN + +struct Profile + - id: bigint + - name: string + - role: Role + - tags?: []string + - meta?: map + +service Smoke + - Echo(profile: Profile) => (profile: Profile) diff --git a/cTypeName.go.tmpl b/cTypeName.go.tmpl new file mode 100644 index 0000000..15dc2f3 --- /dev/null +++ b/cTypeName.go.tmpl @@ -0,0 +1,31 @@ +{{- define "cTypeName" -}} +{{- $prefix := .Prefix -}} +{{- $name := .Name -}} +{{- printf "%s_%s" $prefix (snakeCase $name) -}} +{{- end -}} + +{{- define "cEnumValue" -}} +{{- $prefix := .Prefix -}} +{{- $typeName := .TypeName -}} +{{- $fieldName := .FieldName -}} +{{- printf "%s_%s_%s" (toUpper $prefix) (toUpper (snakeCase $typeName)) (toUpper (snakeCase $fieldName)) -}} +{{- end -}} + +{{- define "cFieldName" -}} +{{- $name := .Name -}} +{{- snakeCase $name -}} +{{- end -}} + +{{- define "cMethodRequestTypeName" -}} +{{- $prefix := .Prefix -}} +{{- $serviceName := .ServiceName -}} +{{- $methodName := .MethodName -}} +{{- printf "%s_%s_%s_request" $prefix (snakeCase $serviceName) (snakeCase $methodName) -}} +{{- end -}} + +{{- define "cMethodResponseTypeName" -}} +{{- $prefix := .Prefix -}} +{{- $serviceName := .ServiceName -}} +{{- $methodName := .MethodName -}} +{{- printf "%s_%s_%s_response" $prefix (snakeCase $serviceName) (snakeCase $methodName) -}} +{{- end -}} diff --git a/client.go.tmpl b/client.go.tmpl new file mode 100644 index 0000000..f8e2613 --- /dev/null +++ b/client.go.tmpl @@ -0,0 +1,40 @@ +{{- define "client" -}} +{{- $prefix := .Prefix -}} +{{- $services := .Services -}} + +{{- range $_, $service := $services }} +typedef struct {{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} {{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }}; + +{{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} *{{ printf "%s_%s_client_create" $prefix (snakeCase $service.Name) }}(const char *base_url, const {{ printf "%s_client_options" $prefix }} *options); +void {{ printf "%s_%s_client_destroy" $prefix (snakeCase $service.Name) }}({{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} *client); +int {{ printf "%s_%s_client_configure" $prefix (snakeCase $service.Name) }}({{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} *client, const {{ printf "%s_client_options" $prefix }} *options); +int {{ printf "%s_%s_client_send_prepared_request" $prefix (snakeCase $service.Name) }}( + {{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} *client, + const {{ printf "%s_prepared_request" $prefix }} *request, + {{ printf "%s_http_response" $prefix }} *response, + {{ printf "%s_error" $prefix }} *error +); + +{{- range $_, $method := $service.Methods }} +int {{ printf "%s_%s_%s_prepare_request" $prefix (snakeCase $service.Name) (snakeCase $method.Name) }}( + const {{ template "cMethodRequestTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }} *request, + {{ printf "%s_prepared_request" $prefix }} *prepared_request, + {{ printf "%s_error" $prefix }} *error +); + +int {{ printf "%s_%s_%s_parse_response" $prefix (snakeCase $service.Name) (snakeCase $method.Name) }}( + const {{ printf "%s_http_response" $prefix }} *http_response, + {{ template "cMethodResponseTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }} *response, + {{ printf "%s_error" $prefix }} *error +); + +int {{ printf "%s_%s_%s" $prefix (snakeCase $service.Name) (snakeCase $method.Name) }}( + {{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} *client, + const {{ template "cMethodRequestTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }} *request, + {{ template "cMethodResponseTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }} *response, + {{ printf "%s_error" $prefix }} *error +); + +{{- end }} +{{- end }} +{{- end -}} diff --git a/embed.go b/embed.go new file mode 100644 index 0000000..490a179 --- /dev/null +++ b/embed.go @@ -0,0 +1,6 @@ +package c + +import "embed" + +//go:embed *.go.tmpl +var FS embed.FS diff --git a/generator_test.go b/generator_test.go new file mode 100644 index 0000000..db22f31 --- /dev/null +++ b/generator_test.go @@ -0,0 +1,329 @@ +package c + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" +) + +const webrpcGenModule = "github.com/webrpc/webrpc/cmd/webrpc-gen@v0.37.1" +const webrpcGenVersion = "v0.37.1" + +func TestGenerateSmoke(t *testing.T) { + root := repoRoot(t) + tmp := t.TempDir() + + header := filepath.Join(tmp, "smoke.gen.h") + impl := filepath.Join(tmp, "smoke.gen.c") + + generateC(t, root, filepath.Join(root, "testdata", "smoke.ridl"), header, impl, "smoke") + syntaxCheckHeader(t, header) + syntaxCheckImpl(t, tmp, impl) +} + +func TestGeneratedCodecBehavior(t *testing.T) { + root := repoRoot(t) + tmp := t.TempDir() + + header := filepath.Join(tmp, "codec.gen.h") + impl := filepath.Join(tmp, "codec.gen.c") + generateC(t, root, filepath.Join(root, "testdata", "codec.ridl"), header, impl, "codec_test") + + testMain := filepath.Join(tmp, "codec_test_main.c") + if err := os.WriteFile(testMain, []byte(codecTestProgram), 0o644); err != nil { + t.Fatalf("write codec test program: %v", err) + } + + cflags := pkgConfigFlags(t, "--cflags") + libs := pkgConfigFlags(t, "--libs") + + bin := filepath.Join(tmp, "codec-test") + args := append([]string{"-std=c99", "-Wall", "-Wextra"}, cflags...) + args = append(args, "codec_test_main.c", "-o", bin) + args = append(args, libs...) + + runCmd(t, tmp, "cc", args...) + runCmd(t, tmp, bin) +} + +func TestGeneratedSuccinctMethodBehavior(t *testing.T) { + root := repoRoot(t) + tmp := t.TempDir() + + header := filepath.Join(tmp, "succinct.gen.h") + impl := filepath.Join(tmp, "succinct.gen.c") + generateC(t, root, filepath.Join(root, "testdata", "succinct.ridl"), header, impl, "succinct_test") + + testMain := filepath.Join(tmp, "succinct_test_main.c") + if err := os.WriteFile(testMain, []byte(succinctTestProgram), 0o644); err != nil { + t.Fatalf("write succinct test program: %v", err) + } + + cflags := pkgConfigFlags(t, "--cflags") + libs := pkgConfigFlags(t, "--libs") + + bin := filepath.Join(tmp, "succinct-test") + args := append([]string{"-std=c99", "-Wall", "-Wextra"}, cflags...) + args = append(args, "succinct_test_main.c", "-o", bin) + args = append(args, libs...) + + runCmd(t, tmp, "cc", args...) + runCmd(t, tmp, bin) +} + +func repoRoot(t *testing.T) string { + t.Helper() + _, file, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("failed to resolve repo root") + } + return filepath.Dir(file) +} + +func generateC(t *testing.T, root, schemaPath, headerPath, implPath, prefix string) { + t.Helper() + + baseArgs := []string{ + "run", + "-ldflags=-X github.com/webrpc/webrpc.VERSION=" + webrpcGenVersion, + webrpcGenModule, + "-schema=" + schemaPath, + "-target=" + root, + "-prefix=" + prefix, + } + runCmd(t, root, "go", append(baseArgs, "-emit=header", "-out="+headerPath)...) + runCmd(t, root, "go", append(baseArgs, "-emit=impl", "-header="+filepath.Base(headerPath), "-out="+implPath)...) +} + +func syntaxCheckHeader(t *testing.T, headerPath string) { + t.Helper() + runCmd(t, filepath.Dir(headerPath), "cc", "-x", "c", "-std=c99", "-Wall", "-Wextra", "-fsyntax-only", filepath.Base(headerPath)) +} + +func syntaxCheckImpl(t *testing.T, workdir, implPath string) { + t.Helper() + args := []string{"-x", "c", "-std=c99", "-Wall", "-Wextra", "-fsyntax-only"} + args = append(args, pkgConfigFlags(t, "--cflags")...) + args = append(args, filepath.Base(implPath)) + runCmd(t, workdir, "cc", args...) +} + +func pkgConfigFlags(t *testing.T, mode string) []string { + t.Helper() + + candidates := [][]string{ + {mode, "libcjson", "libcurl"}, + {mode, "cjson", "libcurl"}, + } + + for _, args := range candidates { + cmd := exec.Command("pkg-config", args...) + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if err := cmd.Run(); err == nil { + return strings.Fields(stdout.String()) + } + } + + t.Fatalf("pkg-config failed for cJSON/libcurl using mode %s", mode) + return nil +} + +func runCmd(t *testing.T, dir, name string, args ...string) string { + t.Helper() + + cmd := exec.Command(name, args...) + cmd.Dir = dir + cmd.Env = append(os.Environ(), "GOWORK=off") + + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + t.Fatalf("%s %s failed: %v\nstdout:\n%s\nstderr:\n%s", name, strings.Join(args, " "), err, stdout.String(), stderr.String()) + } + + return stdout.String() +} + +const codecTestProgram = `#include +#include +#include + +#include "codec.gen.c" + +static void fail_msg(const char *msg) { + fprintf(stderr, "%s\n", msg); + exit(1); +} + +static void expect_true(int cond, const char *msg) { + if (!cond) { + fail_msg(msg); + } +} + +int main(void) { + codec_test_payload value; + codec_test_payload_init(&value); + + expect_true(codec_test_bigint_set_string(&value.count, "18446744073709551616") == 0, "count bigint set failed"); + value.explicit_any = codec_test_strdup("{\"k\":[1,true,null]}"); + expect_true(value.explicit_any != NULL, "explicit_any strdup failed"); + value.explicit_null = 1; + + value.nested = (codec_test_nested *)malloc(sizeof(*value.nested)); + expect_true(value.nested != NULL, "nested alloc failed"); + codec_test_nested_init(value.nested); + expect_true(codec_test_bigint_set_string(&value.nested->id, "99") == 0, "nested bigint set failed"); + + value.items.count = 2; + value.items.items = (codec_test_bigint *)calloc(value.items.count, sizeof(*value.items.items)); + expect_true(value.items.items != NULL, "items alloc failed"); + expect_true(codec_test_bigint_set_string(&value.items.items[0], "7") == 0, "item 0 bigint set failed"); + expect_true(codec_test_bigint_set_string(&value.items.items[1], "8") == 0, "item 1 bigint set failed"); + + cJSON *encoded = codec_test_payload_to_json(&value); + expect_true(encoded != NULL, "encode failed"); + + cJSON *count = cJSON_GetObjectItemCaseSensitive(encoded, "count"); + expect_true(cJSON_IsString(count), "count should encode as string"); + expect_true(strcmp(cJSON_GetStringValue(count), "18446744073709551616") == 0, "count bigint string mismatch"); + + cJSON *explicit_any = cJSON_GetObjectItemCaseSensitive(encoded, "explicitAny"); + expect_true(cJSON_IsObject(explicit_any), "explicitAny should encode as object"); + expect_true(cJSON_IsArray(cJSON_GetObjectItemCaseSensitive(explicit_any, "k")), "explicitAny.k should be array"); + + cJSON *explicit_null = cJSON_GetObjectItemCaseSensitive(encoded, "explicitNull"); + expect_true(cJSON_IsNull(explicit_null), "explicitNull should encode as null"); + + cJSON *nested = cJSON_GetObjectItemCaseSensitive(encoded, "nested"); + cJSON *nested_id = cJSON_GetObjectItemCaseSensitive(nested, "id"); + expect_true(cJSON_IsString(nested_id), "nested.id should encode as string"); + expect_true(strcmp(cJSON_GetStringValue(nested_id), "99") == 0, "nested.id bigint string mismatch"); + + cJSON *items = cJSON_GetObjectItemCaseSensitive(encoded, "items"); + expect_true(cJSON_IsArray(items), "items should encode as array"); + expect_true(cJSON_GetArraySize(items) == 2, "items length mismatch"); + expect_true(strcmp(cJSON_GetStringValue(cJSON_GetArrayItem(items, 0)), "7") == 0, "items[0] mismatch"); + expect_true(strcmp(cJSON_GetStringValue(cJSON_GetArrayItem(items, 1)), "8") == 0, "items[1] mismatch"); + + cJSON_Delete(encoded); + codec_test_payload_free(&value); + + { + const char *json_text = "{\"count\":\"42\",\"explicitAny\":null,\"explicitNull\":null,\"maybeAny\":null,\"nested\":{\"id\":\"99\"},\"items\":[\"1\",\"2\"]}"; + cJSON *parsed = codec_test_cjson_parse(json_text); + codec_test_payload decoded; + codec_test_error error; + + expect_true(parsed != NULL, "parse decode JSON failed"); + codec_test_payload_init(&decoded); + codec_test_error_init(&error); + + expect_true(codec_test_payload_from_json(parsed, &decoded, &error) == 0, "decode failed"); + expect_true(decoded.count.digits != NULL && strcmp(decoded.count.digits, "42") == 0, "decoded count mismatch"); + expect_true(decoded.explicit_any != NULL && strcmp(decoded.explicit_any, "null") == 0, "decoded explicitAny should preserve null"); + expect_true(decoded.has_maybe_any, "decoded maybeAny should mark field present"); + expect_true(decoded.maybe_any == NULL, "decoded maybeAny null should remain NULL payload"); + expect_true(decoded.nested != NULL, "decoded nested missing"); + expect_true(decoded.nested->id.digits != NULL && strcmp(decoded.nested->id.digits, "99") == 0, "decoded nested id mismatch"); + expect_true(decoded.items.count == 2, "decoded items length mismatch"); + expect_true(strcmp(decoded.items.items[0].digits, "1") == 0, "decoded items[0] mismatch"); + expect_true(strcmp(decoded.items.items[1].digits, "2") == 0, "decoded items[1] mismatch"); + + codec_test_error_free(&error); + codec_test_payload_free(&decoded); + cJSON_Delete(parsed); + } + + { + const char *json_text = "{\"count\":\"42\",\"explicitAny\":null,\"nested\":{\"id\":\"99\"},\"items\":[]}"; + cJSON *parsed = codec_test_cjson_parse(json_text); + codec_test_payload decoded; + codec_test_error error; + + expect_true(parsed != NULL, "parse missing-required JSON failed"); + codec_test_payload_init(&decoded); + codec_test_error_init(&error); + + expect_true(codec_test_payload_from_json(parsed, &decoded, &error) != 0, "decode should fail when required null field is missing"); + expect_true(error.message != NULL && strstr(error.message, "missing required field explicitNull") != NULL, "missing required field message mismatch"); + + codec_test_error_free(&error); + codec_test_payload_free(&decoded); + cJSON_Delete(parsed); + } + + return 0; +} +` + +const succinctTestProgram = `#include +#include +#include + +#include "succinct.gen.c" + +static void fail_msg(const char *msg) { + fprintf(stderr, "%s\n", msg); + exit(1); +} + +static void expect_true(int cond, const char *msg) { + if (!cond) { + fail_msg(msg); + } +} + +int main(void) { + succinct_test_demo_flatten_request request; + succinct_test_demo_flatten_response response; + cJSON *request_json = NULL; + cJSON *response_json = NULL; + succinct_test_error error; + + succinct_test_demo_flatten_request_init(&request); + succinct_test_demo_flatten_response_init(&response); + succinct_test_error_init(&error); + + request.flatten_request = (succinct_test_flatten_request *)malloc(sizeof(*request.flatten_request)); + expect_true(request.flatten_request != NULL, "request alloc failed"); + succinct_test_flatten_request_init(request.flatten_request); + request.flatten_request->name = succinct_test_strdup("alice"); + expect_true(request.flatten_request->name != NULL, "request name alloc failed"); + request.flatten_request->amount = 42; + + request_json = succinct_test_demo_flatten_request_to_json(&request); + expect_true(request_json != NULL, "succinct request encode failed"); + expect_true(cJSON_IsObject(request_json), "succinct request should encode to direct object"); + expect_true(cJSON_GetObjectItemCaseSensitive(request_json, "flattenRequest") == NULL, "succinct request must not wrap payload"); + expect_true(strcmp(cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(request_json, "name")), "alice") == 0, "succinct request name mismatch"); + expect_true((uint64_t)cJSON_GetNumberValue(cJSON_GetObjectItemCaseSensitive(request_json, "amount")) == 42, "succinct request amount mismatch"); + + response_json = cJSON_CreateObject(); + expect_true(response_json != NULL, "succinct response alloc failed"); + expect_true(cJSON_AddNumberToObject(response_json, "id", 99) != NULL, "succinct response id add failed"); + expect_true(cJSON_AddNumberToObject(response_json, "count", 7) != NULL, "succinct response count add failed"); + + expect_true(succinct_test_demo_flatten_response_from_json(response_json, &response, &error) == 0, "succinct response decode failed"); + expect_true(response.flatten_response != NULL, "succinct response payload missing"); + expect_true(response.flatten_response->id == 99, "succinct response id mismatch"); + expect_true(response.flatten_response->count == 7, "succinct response count mismatch"); + + cJSON_Delete(request_json); + cJSON_Delete(response_json); + succinct_test_demo_flatten_request_free(&request); + succinct_test_demo_flatten_response_free(&response); + succinct_test_error_free(&error); + return 0; +} +` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f5a4f85 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/webrpc/gen-c + +go 1.16 diff --git a/help.go.tmpl b/help.go.tmpl new file mode 100644 index 0000000..2ffd688 --- /dev/null +++ b/help.go.tmpl @@ -0,0 +1,9 @@ +{{- define "help" -}} + {{- stderrPrintf "webrpc C generator\n\n" -}} + {{- stderrPrintf "Options:\n" -}} + {{- stderrPrintf " -prefix= symbol/type prefix (default: schema name)\n" -}} + {{- stderrPrintf " -client generate client declarations (default: true)\n" -}} + {{- stderrPrintf " -emit= emit either header or impl output (default: header)\n" -}} + {{- stderrPrintf " -header= header include used by impl output (default: .h)\n" -}} + {{- stderrPrintf "\nSee github.com/webrpc/gen-c for usage details.\n" -}} +{{- end -}} diff --git a/impl.go.tmpl b/impl.go.tmpl new file mode 100644 index 0000000..2bad123 --- /dev/null +++ b/impl.go.tmpl @@ -0,0 +1,25 @@ +{{- define "impl" -}} +{{- $prefix := .Prefix -}} +{{- $encodeReachable := dict -}} +{{- $decodeReachable := dict -}} +{{ template "collectStructCodecReachability" dict "Types" .Types "Services" .Services "EncodeReachable" $encodeReachable "DecodeReachable" $decodeReachable }} +{{ template "implPreamble" . }} +{{ template "implTransport" . }} + +{{- range $_, $type := .Types }} +{{- if isStructType $type }} +{{- if exists $encodeReachable $type.Name }} +static {{ toUpper $prefix }}_JSON_UNUSED cJSON *{{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }}_to_json(const {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }} *value); +{{- end }} +{{- if exists $decodeReachable $type.Name }} +static {{ toUpper $prefix }}_JSON_UNUSED int {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }}_from_json(const cJSON *json, {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }} *out, {{ printf "%s_error" $prefix }} *error); +{{- end }} +{{- end }} +{{- end }} + +{{ template "implStructJSON" dict "Prefix" $prefix "Types" .Types "EncodeReachable" $encodeReachable "DecodeReachable" $decodeReachable }} +{{ template "implMethodJSON" dict "Prefix" $prefix "BasePath" .BasePath "Services" .Services "Types" .Types }} +{{- if .Client }} +{{ template "implClient" dict "Prefix" $prefix "Services" .Services }} +{{- end }} +{{- end -}} diff --git a/implClient.go.tmpl b/implClient.go.tmpl new file mode 100644 index 0000000..fd8494e --- /dev/null +++ b/implClient.go.tmpl @@ -0,0 +1,149 @@ +{{- define "implClient" -}} +{{- $prefix := .Prefix -}} +{{- range $_, $service := .Services }} +struct {{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} { + char *base_url; + char *bearer_token; + struct curl_slist *default_headers; + long timeout_ms; +}; + +static void {{ printf "%s_%s_client_reset_config" $prefix (snakeCase $service.Name) }}({{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} *client) { + if (!client) return; + free(client->bearer_token); + client->bearer_token = NULL; + if (client->default_headers) { + curl_slist_free_all(client->default_headers); + client->default_headers = NULL; + } + client->timeout_ms = 10000L; +} + +int {{ printf "%s_%s_client_configure" $prefix (snakeCase $service.Name) }}({{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} *client, const {{ printf "%s_client_options" $prefix }} *options) { + if (!client) return 0; + + {{ printf "%s_%s_client_reset_config" $prefix (snakeCase $service.Name) }}(client); + if (!options) return 1; + + if (options->timeout_ms > 0) { + client->timeout_ms = options->timeout_ms; + } + + if (options->bearer_token) { + client->bearer_token = {{ printf "%s_strdup" $prefix }}(options->bearer_token); + if (!client->bearer_token) { + {{ printf "%s_%s_client_reset_config" $prefix (snakeCase $service.Name) }}(client); + return 0; + } + } + + if (options->headers_count > 0) { + if (!options->headers) { + {{ printf "%s_%s_client_reset_config" $prefix (snakeCase $service.Name) }}(client); + return 0; + } + for (size_t i = 0; i < options->headers_count; ++i) { + if (!options->headers[i]) { + {{ printf "%s_%s_client_reset_config" $prefix (snakeCase $service.Name) }}(client); + return 0; + } + struct curl_slist *next = curl_slist_append(client->default_headers, options->headers[i]); + if (!next) { + {{ printf "%s_%s_client_reset_config" $prefix (snakeCase $service.Name) }}(client); + return 0; + } + client->default_headers = next; + } + } + + return 1; +} + +{{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} *{{ printf "%s_%s_client_create" $prefix (snakeCase $service.Name) }}(const char *base_url, const {{ printf "%s_client_options" $prefix }} *options) { + {{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} *client = + ({{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} *)calloc(1, sizeof(*client)); + if (!client) return NULL; + client->base_url = {{ printf "%s_strdup" $prefix }}(base_url ? base_url : ""); + if (!client->base_url) { + free(client); + return NULL; + } + {{ printf "%s_%s_client_reset_config" $prefix (snakeCase $service.Name) }}(client); + if (!{{ printf "%s_%s_client_configure" $prefix (snakeCase $service.Name) }}(client, options)) { + free(client->base_url); + client->base_url = NULL; + {{ printf "%s_%s_client_reset_config" $prefix (snakeCase $service.Name) }}(client); + free(client); + return NULL; + } + return client; +} + +void {{ printf "%s_%s_client_destroy" $prefix (snakeCase $service.Name) }}({{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} *client) { + if (!client) return; + free(client->base_url); + client->base_url = NULL; + {{ printf "%s_%s_client_reset_config" $prefix (snakeCase $service.Name) }}(client); + free(client); +} + +int {{ printf "%s_%s_client_send_prepared_request" $prefix (snakeCase $service.Name) }}( + {{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} *client, + const {{ printf "%s_prepared_request" $prefix }} *request, + {{ printf "%s_http_response" $prefix }} *response, + {{ printf "%s_error" $prefix }} *error +) { + if (!client || !request || !response) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "ClientError", "client, request, and response must be non-NULL", NULL); + return -1; + } + + return {{ printf "%s_http_send_request" $prefix }}( + client->base_url, + request, + client->bearer_token, + client->default_headers, + client->timeout_ms, + response, + error + ); +} + +{{- range $_, $method := $service.Methods }} +int {{ printf "%s_%s_%s" $prefix (snakeCase $service.Name) (snakeCase $method.Name) }}( + {{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} *client, + const {{ template "cMethodRequestTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }} *request, + {{ template "cMethodResponseTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }} *response, + {{ printf "%s_error" $prefix }} *error +) { + int rc = -1; + {{ printf "%s_prepared_request" $prefix }} prepared_request; + {{ printf "%s_http_response" $prefix }} http_response; + {{ printf "%s_prepared_request_init" $prefix }}(&prepared_request); + {{ printf "%s_http_response_init" $prefix }}(&http_response); + + if (!client || !request || !response) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "ClientError", "client, request, and response must be non-NULL", NULL); + goto fail; + } + + if ({{ printf "%s_%s_%s_prepare_request" $prefix (snakeCase $service.Name) (snakeCase $method.Name) }}(request, &prepared_request, error) != 0) { + goto fail; + } + if ({{ printf "%s_%s_client_send_prepared_request" $prefix (snakeCase $service.Name) }}(client, &prepared_request, &http_response, error) != 0) { + goto fail; + } + if ({{ printf "%s_%s_%s_parse_response" $prefix (snakeCase $service.Name) (snakeCase $method.Name) }}(&http_response, response, error) != 0) { + goto fail; + } + rc = 0; + +fail: + {{ printf "%s_prepared_request_free" $prefix }}(&prepared_request); + {{ printf "%s_http_response_free" $prefix }}(&http_response); + return rc; +} + +{{- end }} +{{- end }} +{{- end -}} diff --git a/implCodecReachability.go.tmpl b/implCodecReachability.go.tmpl new file mode 100644 index 0000000..e6ecf71 --- /dev/null +++ b/implCodecReachability.go.tmpl @@ -0,0 +1,42 @@ +{{- define "markStructCodecReachability" -}} +{{- $type := .Type -}} +{{- $types := .Types -}} +{{- $reachable := .Reachable -}} +{{- if isMapType $type }} +{{ template "markStructCodecReachability" dict "Type" (mapValueType $type) "Types" $types "Reachable" $reachable }} +{{- else if isListType $type }} +{{ template "markStructCodecReachability" dict "Type" (listElemType $type) "Types" $types "Reachable" $reachable }} +{{- else if isCoreType $type }} +{{- else if isEnumType $type }} +{{- else if and (hasField $type "Alias") $type.Alias }} +{{ template "markStructCodecReachability" dict "Type" $type.Alias.Type.Type "Types" $types "Reachable" $reachable }} +{{- else if isStructType $type }} + {{- $typeName := toString $type -}} + {{- if not (exists $reachable $typeName) }} + {{- $_ := set $reachable $typeName true -}} + {{- range $_, $candidate := $types }} + {{- if and (isStructType $candidate) (eq $candidate.Name $typeName) }} + {{- range $_, $field := $candidate.Fields }} +{{ template "markStructCodecReachability" dict "Type" $field.Type "Types" $types "Reachable" $reachable }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} +{{- end -}} + +{{- define "collectStructCodecReachability" -}} +{{- $types := .Types -}} +{{- $encodeReachable := .EncodeReachable -}} +{{- $decodeReachable := .DecodeReachable -}} +{{- range $_, $service := .Services }} + {{- range $_, $method := $service.Methods }} + {{- range $_, $input := $method.Inputs }} +{{ template "markStructCodecReachability" dict "Type" $input.Type "Types" $types "Reachable" $encodeReachable }} + {{- end }} + {{- range $_, $output := $method.Outputs }} +{{ template "markStructCodecReachability" dict "Type" $output.Type "Types" $types "Reachable" $decodeReachable }} + {{- end }} + {{- end }} +{{- end }} +{{- end -}} diff --git a/implJSONCodec.go.tmpl b/implJSONCodec.go.tmpl new file mode 100644 index 0000000..a417ba8 --- /dev/null +++ b/implJSONCodec.go.tmpl @@ -0,0 +1,398 @@ +{{- define "assert_supported_map_key_type" -}} +{{- $type := .Type -}} +{{- $types := .Types -}} +{{- if eq (toString $type) "string" -}} +{{- else -}} +{{- $state := dict "matched" false -}} +{{- range $_, $schemaType := $types -}} + {{- if eq (toString $schemaType.Name) (toString $type) -}} + {{- $_ := set $state "matched" true -}} + {{- if isEnumType $schemaType -}} + {{- else if and (hasField $schemaType "Alias") $schemaType.Alias -}} +{{ template "assert_supported_map_key_type" dict "Type" $schemaType.Alias.Type.Type "Types" $types }} + {{- else -}} +{{- fail (printf "C generator error: unsupported map key type %s (only map and map are supported)" (toString $type)) -}} + {{- end -}} + {{- end -}} +{{- end -}} +{{- if not (get $state "matched") -}} +{{- fail (printf "C generator error: unsupported map key type %s (only map and map are supported)" (toString $type)) -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{- define "json_encode_map_key_expr" -}} +{{- $prefix := .Prefix -}} +{{- $type := .Type -}} +{{- $expr := .Expr -}} +{{- $types := .Types -}} +{{- if eq (toString $type) "string" -}} +{{$expr}} +{{- else -}} +{{- $state := dict "matched" false -}} +{{- range $_, $schemaType := $types -}} + {{- if eq (toString $schemaType.Name) (toString $type) -}} + {{- $_ := set $state "matched" true -}} + {{- if isEnumType $schemaType -}} +{{ template "cTypeName" dict "Prefix" $prefix "Name" (toString $type) }}_to_string({{$expr}}) + {{- else if and (hasField $schemaType "Alias") $schemaType.Alias -}} +{{ template "json_encode_map_key_expr" dict "Prefix" $prefix "Type" $schemaType.Alias.Type.Type "Expr" $expr "Types" $types }} + {{- end -}} + {{- end -}} +{{- end -}} +{{- if not (get $state "matched") -}} +NULL +{{- end -}} +{{- end -}} +{{- end -}} + +{{- define "json_decode_map_key_from_string" -}} +{{- $prefix := .Prefix -}} +{{- $type := .Type -}} +{{- $src := .StringExpr -}} +{{- $dest := .DestExpr -}} +{{- $types := .Types -}} +{{- if eq (toString $type) "string" }} + {{$dest}} = {{ printf "%s_strdup" $prefix }}({{$src}}); + if (!{{$dest}}) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "out of memory decoding map key", NULL); + goto fail; + } +{{- else }} +{{- $state := dict "matched" false -}} +{{- range $_, $schemaType := $types }} + {{- if eq (toString $schemaType.Name) (toString $type) }} + {{- $_ := set $state "matched" true -}} + {{- if isEnumType $schemaType }} + if ({{ template "cTypeName" dict "Prefix" $prefix "Name" (toString $type) }}_from_string({{$src}}, &{{$dest}}) != 0) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected enum map key", NULL); + goto fail; + } + {{- else if and (hasField $schemaType "Alias") $schemaType.Alias }} +{{ template "json_decode_map_key_from_string" dict "Prefix" $prefix "Type" $schemaType.Alias.Type.Type "StringExpr" $src "DestExpr" $dest "Types" $types }} + {{- else }} + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "unsupported map key type", NULL); + goto fail; + {{- end }} + {{- end }} +{{- end }} +{{- if not (get $state "matched") }} + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "unsupported map key type", NULL); + goto fail; +{{- end }} +{{- end }} +{{- end -}} + +{{- define "json_encode_value" -}} +{{- $prefix := .Prefix -}} +{{- $type := .Type -}} +{{- $expr := .Expr -}} +{{- $out := .OutVar -}} +{{- $depth := 0 -}} +{{- if exists . "Depth" -}} +{{- $depth = get . "Depth" -}} +{{- end -}} +{{- $types := array -}} +{{- if exists . "Types" -}} +{{- $types = get . "Types" -}} +{{- end -}} +{{- if isMapType $type }} + { + cJSON *object_json = cJSON_CreateObject(); + if (!object_json) goto fail; +{{ template "assert_supported_map_key_type" dict "Type" (mapKeyType $type) "Types" $types }} + for (size_t {{ printf "map_idx_%d" $depth }} = 0; {{ printf "map_idx_%d" $depth }} < {{$expr}}.count; ++{{ printf "map_idx_%d" $depth }}) { + const char *{{ printf "map_key_%d" $depth }} = {{ template "json_encode_map_key_expr" dict "Prefix" $prefix "Type" (mapKeyType $type) "Expr" (printf "%s.keys[%s]" $expr (printf "map_idx_%d" $depth)) "Types" $types }}; + cJSON *{{ printf "map_value_json_%d" $depth }} = NULL; +{{ template "json_encode_value" dict "Prefix" $prefix "Type" (mapValueType $type) "Expr" (printf "%s.values[%s]" $expr (printf "map_idx_%d" $depth)) "OutVar" (printf "map_value_json_%d" $depth) "Depth" (add $depth 1) "Types" $types }} + if (!{{ printf "map_key_%d" $depth }}) { + cJSON_Delete({{ printf "map_value_json_%d" $depth }}); + cJSON_Delete(object_json); + goto fail; + } + if (!cJSON_AddItemToObject(object_json, {{ printf "map_key_%d" $depth }}, {{ printf "map_value_json_%d" $depth }})) { + cJSON_Delete({{ printf "map_value_json_%d" $depth }}); + cJSON_Delete(object_json); + goto fail; + } + } + {{$out}} = object_json; + } +{{- else if isListType $type }} + {{- $elem := listElemType $type -}} + { + cJSON *array_json = cJSON_CreateArray(); + if (!array_json) goto fail; + {{- if eq (toString $elem) "byte" }} + for (size_t {{ printf "list_idx_%d" $depth }} = 0; {{ printf "list_idx_%d" $depth }} < {{$expr}}.len; ++{{ printf "list_idx_%d" $depth }}) { + cJSON *{{ printf "list_entry_json_%d" $depth }} = cJSON_CreateNumber((double){{$expr}}.data[{{ printf "list_idx_%d" $depth }}]); + if (!{{ printf "list_entry_json_%d" $depth }}) { + cJSON_Delete(array_json); + goto fail; + } + if (!cJSON_AddItemToArray(array_json, {{ printf "list_entry_json_%d" $depth }})) { + cJSON_Delete({{ printf "list_entry_json_%d" $depth }}); + cJSON_Delete(array_json); + goto fail; + } + } + {{- else }} + for (size_t {{ printf "list_idx_%d" $depth }} = 0; {{ printf "list_idx_%d" $depth }} < {{$expr}}.count; ++{{ printf "list_idx_%d" $depth }}) { + cJSON *{{ printf "list_entry_json_%d" $depth }} = NULL; +{{ template "json_encode_value" dict "Prefix" $prefix "Type" $elem "Expr" (printf "%s.items[%s]" $expr (printf "list_idx_%d" $depth)) "OutVar" (printf "list_entry_json_%d" $depth) "Depth" (add $depth 1) "Types" $types }} + if (!cJSON_AddItemToArray(array_json, {{ printf "list_entry_json_%d" $depth }})) { + cJSON_Delete({{ printf "list_entry_json_%d" $depth }}); + cJSON_Delete(array_json); + goto fail; + } + } + {{- end }} + {{$out}} = array_json; + } +{{- else if isCoreType $type }} + {{- if eq (toString $type) "string" }} + {{$out}} = {{$expr}} ? cJSON_CreateString({{$expr}}) : cJSON_CreateNull(); + if (!{{$out}}) goto fail; + {{- else if eq (toString $type) "any" }} + if ({{$expr}}) { + {{$out}} = {{ printf "%s_cjson_parse" $prefix }}({{$expr}}); + } else { + {{$out}} = cJSON_CreateNull(); + } + if (!{{$out}}) goto fail; + {{- else if eq (toString $type) "bool" }} + {{$out}} = cJSON_CreateBool({{$expr}} ? 1 : 0); + if (!{{$out}}) goto fail; + {{- else if or (eq (toString $type) "uint") (eq (toString $type) "uint8") (eq (toString $type) "uint16") (eq (toString $type) "uint32") (eq (toString $type) "byte") }} + {{$out}} = cJSON_CreateNumber((double){{$expr}}); + if (!{{$out}}) goto fail; + {{- else if eq (toString $type) "uint64" }} + {{$out}} = cJSON_CreateNumber((double){{$expr}}); + if (!{{$out}}) goto fail; + {{- else if or (eq (toString $type) "int") (eq (toString $type) "int8") (eq (toString $type) "int16") (eq (toString $type) "int32") (eq (toString $type) "int64") }} + {{$out}} = cJSON_CreateNumber((double){{$expr}}); + if (!{{$out}}) goto fail; + {{- else if or (eq (toString $type) "float32") (eq (toString $type) "float64") }} + {{$out}} = cJSON_CreateNumber((double){{$expr}}); + if (!{{$out}}) goto fail; + {{- else if eq (toString $type) "timestamp" }} + {{$out}} = {{$expr}}.value ? cJSON_CreateString({{$expr}}.value) : cJSON_CreateNull(); + if (!{{$out}}) goto fail; + {{- else if eq (toString $type) "bigint" }} + {{$out}} = {{$expr}}.digits ? cJSON_CreateString({{$expr}}.digits) : cJSON_CreateNull(); + if (!{{$out}}) goto fail; + {{- else if eq (toString $type) "null" }} + {{$out}} = cJSON_CreateNull(); + if (!{{$out}}) goto fail; + {{- else }} + {{$out}} = cJSON_CreateNull(); + if (!{{$out}}) goto fail; + {{- end }} +{{- else if isEnumType $type }} + {{$out}} = cJSON_CreateString({{ template "cTypeName" dict "Prefix" $prefix "Name" (toString $type) }}_to_string({{$expr}})); + if (!{{$out}}) goto fail; +{{- else if and (hasField $type "Alias") $type.Alias }} +{{ template "json_encode_value" dict "Prefix" $prefix "Type" $type.Alias.Type.Type "Expr" $expr "OutVar" $out "Depth" $depth "Types" $types }} +{{- else }} + if ({{$expr}}) { + {{$out}} = {{ template "cTypeName" dict "Prefix" $prefix "Name" (toString $type) }}_to_json({{$expr}}); + } else { + {{$out}} = cJSON_CreateNull(); + } + if (!{{$out}}) goto fail; +{{- end }} +{{- end -}} + +{{- define "json_decode_value" -}} +{{- $prefix := .Prefix -}} +{{- $type := .Type -}} +{{- $json := .JsonExpr -}} +{{- $dest := .DestExpr -}} +{{- $depth := 0 -}} +{{- if exists . "Depth" -}} +{{- $depth = get . "Depth" -}} +{{- end -}} +{{- $types := array -}} +{{- if exists . "Types" -}} +{{- $types = get . "Types" -}} +{{- end -}} +{{- if isMapType $type }} + if (!cJSON_IsObject({{$json}})) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected JSON object", NULL); + goto fail; + } +{{ template "assert_supported_map_key_type" dict "Type" (mapKeyType $type) "Types" $types }} + { + size_t count = (size_t)cJSON_GetArraySize({{$json}}); + {{$dest}}.keys = count ? calloc(count, sizeof(*{{$dest}}.keys)) : NULL; + {{$dest}}.values = count ? calloc(count, sizeof(*{{$dest}}.values)) : NULL; + if (count && (!{{$dest}}.keys || !{{$dest}}.values)) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "out of memory decoding map", NULL); + goto fail; + } + {{$dest}}.count = count; + size_t {{ printf "map_idx_%d" $depth }} = 0; + cJSON *{{ printf "map_entry_%d" $depth }} = NULL; + cJSON_ArrayForEach({{ printf "map_entry_%d" $depth }}, {{$json}}) { + const char *entry_key = {{ printf "map_entry_%d" $depth }} ? {{ printf "map_entry_%d" $depth }}->string : NULL; + if (!entry_key) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "missing map key", NULL); + goto fail; + } +{{ template "json_decode_map_key_from_string" dict "Prefix" $prefix "Type" (mapKeyType $type) "StringExpr" "entry_key" "DestExpr" (printf "%s.keys[%s]" $dest (printf "map_idx_%d" $depth)) "Types" $types }} +{{ template "json_decode_value" dict "Prefix" $prefix "Type" (mapValueType $type) "JsonExpr" (printf "map_entry_%d" $depth) "DestExpr" (printf "%s.values[%s]" $dest (printf "map_idx_%d" $depth)) "Depth" (add $depth 1) "Types" $types }} + {{ printf "map_idx_%d" $depth }}++; + } + } +{{- else if isListType $type }} + {{- $elem := listElemType $type -}} + if (!cJSON_IsArray({{$json}})) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected JSON array", NULL); + goto fail; + } + { + size_t count = (size_t)cJSON_GetArraySize({{$json}}); + {{- if eq (toString $elem) "byte" }} + {{$dest}}.data = count ? (uint8_t *)calloc(count, sizeof(*{{$dest}}.data)) : NULL; + if (count && !{{$dest}}.data) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "out of memory decoding byte array", NULL); + goto fail; + } + {{$dest}}.len = count; + for (size_t {{ printf "list_idx_%d" $depth }} = 0; {{ printf "list_idx_%d" $depth }} < count; ++{{ printf "list_idx_%d" $depth }}) { + cJSON *entry = cJSON_GetArrayItem({{$json}}, (int){{ printf "list_idx_%d" $depth }}); + uint64_t entry_value = 0; + if (!{{ printf "%s_cjson_get_uint64_exact" $prefix }}(entry, &entry_value) || entry_value > 255) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected byte array element", NULL); + goto fail; + } + {{$dest}}.data[{{ printf "list_idx_%d" $depth }}] = (uint8_t)entry_value; + } + {{- else }} + {{$dest}}.items = count ? calloc(count, sizeof(*{{$dest}}.items)) : NULL; + if (count && !{{$dest}}.items) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "out of memory decoding array", NULL); + goto fail; + } + {{$dest}}.count = count; + for (size_t {{ printf "list_idx_%d" $depth }} = 0; {{ printf "list_idx_%d" $depth }} < count; ++{{ printf "list_idx_%d" $depth }}) { + cJSON *{{ printf "list_entry_%d" $depth }} = cJSON_GetArrayItem({{$json}}, (int){{ printf "list_idx_%d" $depth }}); +{{ template "json_decode_value" dict "Prefix" $prefix "Type" $elem "JsonExpr" (printf "list_entry_%d" $depth) "DestExpr" (printf "%s.items[%s]" $dest (printf "list_idx_%d" $depth)) "Depth" (add $depth 1) "Types" $types }} + } + {{- end }} + } +{{- else if isCoreType $type }} + {{- if eq (toString $type) "string" }} + if (!cJSON_IsString({{$json}}) || !cJSON_GetStringValue({{$json}})) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected string", NULL); + goto fail; + } + {{$dest}} = {{ printf "%s_strdup" $prefix }}(cJSON_GetStringValue({{$json}})); + if (!{{$dest}}) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "out of memory decoding string", NULL); + goto fail; + } + {{- else if eq (toString $type) "any" }} + {{$dest}} = {{ printf "%s_cjson_print_dup" $prefix }}({{$json}}); + if (!{{$dest}}) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "failed to print arbitrary JSON value", NULL); + goto fail; + } + {{- else if eq (toString $type) "bool" }} + if (!cJSON_IsBool({{$json}})) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected boolean", NULL); + goto fail; + } + {{$dest}} = cJSON_IsTrue({{$json}}); + {{- else if eq (toString $type) "uint64" }} + { + uint64_t parsed = 0; + if (!{{ printf "%s_cjson_get_uint64_exact" $prefix }}({{$json}}, &parsed)) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected uint64 number", NULL); + goto fail; + } + {{$dest}} = parsed; + } + {{- else if or (eq (toString $type) "uint") (eq (toString $type) "uint8") (eq (toString $type) "uint16") (eq (toString $type) "uint32") (eq (toString $type) "byte") }} + { + uint64_t parsed = 0; + if (!{{ printf "%s_cjson_get_uint64_exact" $prefix }}({{$json}}, &parsed)) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected unsigned integer", NULL); + goto fail; + } + if (parsed > UINT64_C({{ if eq (toString $type) "uint8" }}255{{ else if eq (toString $type) "uint16" }}65535{{ else if eq (toString $type) "uint32" }}4294967295{{ else if eq (toString $type) "byte" }}255{{ else }}4294967295{{ end }})) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "unsigned integer out of range", NULL); + goto fail; + } + {{$dest}} = ({{ template "type" dict "Type" $type "Prefix" $prefix "Types" $types }})parsed; + } + {{- else if eq (toString $type) "int64" }} + { + int64_t parsed = 0; + if (!{{ printf "%s_cjson_get_int64_exact" $prefix }}({{$json}}, &parsed)) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected int64 number", NULL); + goto fail; + } + {{$dest}} = parsed; + } + {{- else if or (eq (toString $type) "int") (eq (toString $type) "int8") (eq (toString $type) "int16") (eq (toString $type) "int32") }} + { + int64_t parsed = 0; + if (!{{ printf "%s_cjson_get_int64_exact" $prefix }}({{$json}}, &parsed)) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected integer", NULL); + goto fail; + } + if (parsed < {{ if eq (toString $type) "int8" }}-128LL{{ else if eq (toString $type) "int16" }}-32768LL{{ else if eq (toString $type) "int32" }}-2147483648LL{{ else }}-2147483648LL{{ end }} || + parsed > {{ if eq (toString $type) "int8" }}127LL{{ else if eq (toString $type) "int16" }}32767LL{{ else if eq (toString $type) "int32" }}2147483647LL{{ else }}2147483647LL{{ end }}) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "integer out of range", NULL); + goto fail; + } + {{$dest}} = ({{ template "type" dict "Type" $type "Prefix" $prefix "Types" $types }})parsed; + } + {{- else if or (eq (toString $type) "float32") (eq (toString $type) "float64") }} + { + double parsed = 0; + if (!{{ printf "%s_cjson_get_double" $prefix }}({{$json}}, &parsed)) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected number", NULL); + goto fail; + } + {{$dest}} = ({{ template "type" dict "Type" $type "Prefix" $prefix "Types" $types }})parsed; + } + {{- else if eq (toString $type) "timestamp" }} + if (!cJSON_IsString({{$json}}) || !cJSON_GetStringValue({{$json}}) || {{ printf "%s_timestamp_set_string" $prefix }}(&{{$dest}}, cJSON_GetStringValue({{$json}})) != 0) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected timestamp string", NULL); + goto fail; + } + {{- else if eq (toString $type) "bigint" }} + if (!cJSON_IsString({{$json}}) || !cJSON_GetStringValue({{$json}}) || {{ printf "%s_bigint_set_string" $prefix }}(&{{$dest}}, cJSON_GetStringValue({{$json}})) != 0) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected bigint string", NULL); + goto fail; + } + {{- else if eq (toString $type) "null" }} + if (!cJSON_IsNull({{$json}})) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected null", NULL); + goto fail; + } + {{- end }} +{{- else if isEnumType $type }} + if (!cJSON_IsString({{$json}}) || !cJSON_GetStringValue({{$json}}) || {{ template "cTypeName" dict "Prefix" $prefix "Name" (toString $type) }}_from_string(cJSON_GetStringValue({{$json}}), &{{$dest}}) != 0) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected enum string", NULL); + goto fail; + } +{{- else if and (hasField $type "Alias") $type.Alias }} +{{ template "json_decode_value" dict "Prefix" $prefix "Type" $type.Alias.Type.Type "JsonExpr" $json "DestExpr" $dest "Depth" $depth "Types" $types }} +{{- else }} + if (!cJSON_IsObject({{$json}})) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected JSON object", NULL); + goto fail; + } + {{$dest}} = ({{ template "cTypeName" dict "Prefix" $prefix "Name" (toString $type) }} *)calloc(1, sizeof(*{{$dest}})); + if (!{{$dest}}) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "out of memory decoding object", NULL); + goto fail; + } + if ({{ template "cTypeName" dict "Prefix" $prefix "Name" (toString $type) }}_from_json({{$json}}, {{$dest}}, error) != 0) { + goto fail; + } +{{- end }} +{{- end -}} diff --git a/implMethodJSON.go.tmpl b/implMethodJSON.go.tmpl new file mode 100644 index 0000000..9ccb283 --- /dev/null +++ b/implMethodJSON.go.tmpl @@ -0,0 +1,242 @@ +{{- define "implMethodJSON" -}} +{{- $prefix := .Prefix -}} +static int {{ printf "%s_prepare_json_request" $prefix }}( + cJSON *request_json, + const char *path, + {{ printf "%s_prepared_request" $prefix }} *prepared_request, + {{ printf "%s_error" $prefix }} *error +) { + int rc = -1; + char *request_body = NULL; + {{ printf "%s_prepared_request" $prefix }} prepared; + {{ printf "%s_prepared_request_init" $prefix }}(&prepared); + + if (!request_json || !path || !prepared_request) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "ClientError", "request_json, path and prepared_request must be non-NULL", NULL); + goto fail; + } + + request_body = {{ printf "%s_cjson_print_dup" $prefix }}(request_json); + if (!request_body) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "EncodeError", "failed to print request JSON", NULL); + goto fail; + } + + prepared.http_method = {{ printf "%s_strdup" $prefix }}("POST"); + prepared.path = {{ printf "%s_strdup" $prefix }}(path); + prepared.content_type = {{ printf "%s_strdup" $prefix }}("application/json"); + if (!prepared.http_method || !prepared.path || !prepared.content_type) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "EncodeError", "failed to allocate prepared request metadata", NULL); + goto fail; + } + + prepared.body = request_body; + prepared.body_len = strlen(request_body); + request_body = NULL; + + *prepared_request = prepared; + memset(&prepared, 0, sizeof(prepared)); + rc = 0; + +fail: + {{ printf "%s_prepared_request_free" $prefix }}(&prepared); + free(request_body); + return rc; +} + +static int {{ printf "%s_parse_json_response" $prefix }}( + const {{ printf "%s_http_response" $prefix }} *http_response, + cJSON **response_json, + {{ printf "%s_error" $prefix }} *error +) { + if (!http_response || !response_json) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "ClientError", "http_response and response_json must be non-NULL", NULL); + return -1; + } + if (http_response->status_code < 200 || http_response->status_code >= 300) { + {{ printf "%s_parse_rpc_error" $prefix }}(http_response->body, http_response->status_code, error); + return -1; + } + + *response_json = {{ printf "%s_cjson_parse" $prefix }}(http_response->body ? http_response->body : "{}"); + if (!*response_json) { + {{ printf "%s_set_error" $prefix }}(error, 0, (int)http_response->status_code, "DecodeError", "failed to parse response JSON", http_response->body); + return -1; + } + + return 0; +} + +{{- range $_, $service := .Services }} +{{- range $_, $method := $service.Methods }} +static cJSON *{{ template "cMethodRequestTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }}_to_json(const {{ template "cMethodRequestTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }} *value) { + {{- if $method.Succinct }} + {{- if ne (len $method.Inputs) 1 }} + {{- fail (printf "C generator error: succinct method %s.%s must have exactly one input" $service.Name $method.Name) }} + {{- end }} + if (!value) return cJSON_CreateNull(); + { + cJSON *root = NULL; + {{- $input := index $method.Inputs 0 -}} + {{- $fieldName := (snakeCase $input.Name) -}} +{{ template "json_encode_value" dict "Prefix" $prefix "Type" $input.Type "Expr" (printf "value->%s" $fieldName) "OutVar" "root" "Types" $.Types }} + return root; +fail: + cJSON_Delete(root); + return NULL; + } + {{- else }} + if (!value) return cJSON_CreateObject(); + cJSON *root = cJSON_CreateObject(); + if (!root) return NULL; +{{- range $_, $input := $method.Inputs }} + { + {{- $fieldName := (snakeCase $input.Name) -}} + {{- if $input.Optional }} + if (value->has_{{$fieldName}}) { + {{- end }} + cJSON *field_json = NULL; +{{ template "json_encode_value" dict "Prefix" $prefix "Type" $input.Type "Expr" (printf "value->%s" $fieldName) "OutVar" "field_json" "Types" $.Types }} + if (!cJSON_AddItemToObject(root, "{{$input.Name}}", field_json)) { + cJSON_Delete(field_json); + goto fail; + } + {{- if $input.Optional }} + } + {{- end }} + } +{{- end }} + return root; +fail: + cJSON_Delete(root); + return NULL; + {{- end }} +} + +static int {{ template "cMethodResponseTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }}_from_json(const cJSON *json, {{ template "cMethodResponseTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }} *out, {{ printf "%s_error" $prefix }} *error) { + if (!out) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "output pointer is NULL", NULL); + return -1; + } + {{- if $method.Succinct }} + {{- if ne (len $method.Outputs) 1 }} + {{- fail (printf "C generator error: succinct method %s.%s must have exactly one output" $service.Name $method.Name) }} + {{- end }} + if (!json) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected JSON value", NULL); + return -1; + } + { + {{- $output := index $method.Outputs 0 -}} + {{- $fieldName := (snakeCase $output.Name) -}} +{{ template "json_decode_value" dict "Prefix" $prefix "Type" $output.Type "JsonExpr" "json" "DestExpr" (printf "out->%s" $fieldName) "Types" $.Types }} + return 0; +fail: + {{ template "cMethodResponseTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }}_free(out); + return -1; + } + {{- else }} + if (!json || !cJSON_IsObject(json)) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected JSON object", NULL); + return -1; + } +{{- range $_, $output := $method.Outputs }} + { + cJSON *field_json = cJSON_GetObjectItemCaseSensitive(json, "{{$output.Name}}"); + int field_present = field_json != NULL; + {{- $fieldName := (snakeCase $output.Name) -}} + {{- $allowsRequiredNull := false -}} + {{- if isCoreType $output.Type }} + {{- if or (eq (toString $output.Type) "null") (eq (toString $output.Type) "any") }} + {{- $allowsRequiredNull = true -}} + {{- end -}} + {{- else if and (hasField $output.Type "Alias") $output.Type.Alias }} + {{- if isCoreType $output.Type.Alias.Type.Type }} + {{- if or (eq (toString $output.Type.Alias.Type.Type) "null") (eq (toString $output.Type.Alias.Type.Type) "any") }} + {{- $allowsRequiredNull = true -}} + {{- end -}} + {{- end -}} + {{- end }} + {{- if $output.Optional }} + if (field_present) { + out->has_{{$fieldName}} = true; + if (!cJSON_IsNull(field_json)) { +{{ template "json_decode_value" dict "Prefix" $prefix "Type" $output.Type "JsonExpr" "field_json" "DestExpr" (printf "out->%s" $fieldName) "Types" $.Types }} + } + } + {{- else }} + if (!field_present{{- if not $allowsRequiredNull }} || cJSON_IsNull(field_json){{- end }}) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "missing required field {{$output.Name}}", NULL); + goto fail; + } +{{ template "json_decode_value" dict "Prefix" $prefix "Type" $output.Type "JsonExpr" "field_json" "DestExpr" (printf "out->%s" $fieldName) "Types" $.Types }} + {{- end }} + } +{{- end }} + return 0; +fail: + {{ template "cMethodResponseTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }}_free(out); + return -1; + {{- end }} +} + +int {{ printf "%s_%s_%s_prepare_request" $prefix (snakeCase $service.Name) (snakeCase $method.Name) }}( + const {{ template "cMethodRequestTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }} *request, + {{ printf "%s_prepared_request" $prefix }} *prepared_request, + {{ printf "%s_error" $prefix }} *error +) { + int rc = -1; + cJSON *request_json = NULL; + + if (!request || !prepared_request) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "ClientError", "request and prepared_request must be non-NULL", NULL); + goto fail; + } + + request_json = {{ template "cMethodRequestTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }}_to_json(request); + if (!request_json) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "EncodeError", "failed to encode request JSON", NULL); + goto fail; + } + + rc = {{ printf "%s_prepare_json_request" $prefix }}(request_json, "{{$.BasePath}}{{$service.Name}}/{{$method.Name}}", prepared_request, error); + +fail: + cJSON_Delete(request_json); + return rc; +} + +int {{ printf "%s_%s_%s_parse_response" $prefix (snakeCase $service.Name) (snakeCase $method.Name) }}( + const {{ printf "%s_http_response" $prefix }} *http_response, + {{ template "cMethodResponseTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }} *response, + {{ printf "%s_error" $prefix }} *error +) { + int rc = -1; + cJSON *response_json = NULL; + {{ template "cMethodResponseTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }} parsed_response; + {{ template "cMethodResponseTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }}_init(&parsed_response); + + if (!http_response || !response) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "ClientError", "http_response and response must be non-NULL", NULL); + goto fail; + } + if ({{ printf "%s_parse_json_response" $prefix }}(http_response, &response_json, error) != 0) { + goto fail; + } + if ({{ template "cMethodResponseTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }}_from_json(response_json, &parsed_response, error) != 0) { + goto fail; + } + + *response = parsed_response; + memset(&parsed_response, 0, sizeof(parsed_response)); + rc = 0; + +fail: + {{ template "cMethodResponseTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }}_free(&parsed_response); + cJSON_Delete(response_json); + return rc; +} + +{{- end }} +{{- end }} +{{- end -}} diff --git a/implPreamble.go.tmpl b/implPreamble.go.tmpl new file mode 100644 index 0000000..7ee914f --- /dev/null +++ b/implPreamble.go.tmpl @@ -0,0 +1,122 @@ +{{- define "implPreamble" -}} +{{- $prefix := .Prefix -}} +{{- $header := .Header -}} +// {{.SchemaName}} {{.SchemaVersion}} {{.SchemaHash}} +// -- +// Code generated by webrpc-gen@{{.WebrpcGenVersion}} with {{.WebrpcTarget}} generator. DO NOT EDIT. +// +// {{.WebrpcGenCommand}} + +#include "{{$header}}" + +#include +#include +#include +#include +#include + +typedef struct { + char *data; + size_t len; + size_t cap; +} {{ printf "%s_buffer" $prefix }}; + +#if defined(__GNUC__) || defined(__clang__) +#define {{ toUpper $prefix }}_JSON_UNUSED __attribute__((unused)) +#else +#define {{ toUpper $prefix }}_JSON_UNUSED +#endif + +static {{ toUpper $prefix }}_JSON_UNUSED cJSON *{{ printf "%s_cjson_parse" $prefix }}(const char *text) { + return cJSON_ParseWithOpts(text ? text : "", NULL, 1); +} + +static {{ toUpper $prefix }}_JSON_UNUSED char *{{ printf "%s_cjson_print_dup" $prefix }}(const cJSON *value) { + char *printed; + char *copy; + + if (!value) { + return {{ printf "%s_strdup" $prefix }}("null"); + } + + printed = cJSON_PrintUnformatted(value); + if (!printed) { + return NULL; + } + + copy = {{ printf "%s_strdup" $prefix }}(printed); + cJSON_free(printed); + return copy; +} + +static {{ toUpper $prefix }}_JSON_UNUSED int {{ printf "%s_cjson_get_int64_exact" $prefix }}( + const cJSON *value, + int64_t *out +) { + double parsed; + double integral; + + if (!out || !cJSON_IsNumber(value)) { + return 0; + } + + parsed = cJSON_GetNumberValue(value); + if (!isfinite(parsed)) { + return 0; + } + if (parsed < (double)INT64_MIN || parsed > (double)INT64_MAX) { + return 0; + } + if (modf(parsed, &integral) != 0.0) { + return 0; + } + + *out = (int64_t)integral; + return 1; +} + +static {{ toUpper $prefix }}_JSON_UNUSED int {{ printf "%s_cjson_get_uint64_exact" $prefix }}( + const cJSON *value, + uint64_t *out +) { + double parsed; + double integral; + + if (!out || !cJSON_IsNumber(value)) { + return 0; + } + + parsed = cJSON_GetNumberValue(value); + if (!isfinite(parsed)) { + return 0; + } + if (parsed < 0.0 || parsed > (double)UINT64_MAX) { + return 0; + } + if (modf(parsed, &integral) != 0.0) { + return 0; + } + + *out = (uint64_t)integral; + return 1; +} + +static {{ toUpper $prefix }}_JSON_UNUSED int {{ printf "%s_cjson_get_double" $prefix }}( + const cJSON *value, + double *out +) { + double parsed; + + if (!out || !cJSON_IsNumber(value)) { + return 0; + } + + parsed = cJSON_GetNumberValue(value); + if (!isfinite(parsed)) { + return 0; + } + + *out = parsed; + return 1; +} +{{- end -}} diff --git a/implStructJSON.go.tmpl b/implStructJSON.go.tmpl new file mode 100644 index 0000000..4d720ef --- /dev/null +++ b/implStructJSON.go.tmpl @@ -0,0 +1,133 @@ +{{- define "implStructJSON" -}} +{{- $prefix := .Prefix -}} +{{- $encodeReachable := .EncodeReachable -}} +{{- $decodeReachable := .DecodeReachable -}} +{{- range $_, $type := .Types }} +{{- if isStructType $type }} +{{- if exists $encodeReachable $type.Name }} +static {{ toUpper $prefix }}_JSON_UNUSED cJSON *{{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }}_to_json(const {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }} *value) { + if (!value) return NULL; + cJSON *root = cJSON_CreateObject(); + if (!root) return NULL; +{{- range $_, $field := $type.Fields }} + {{- $json := dict "name" $field.Name "ignored" false "explicit" false -}} + {{- range $_, $meta := $field.Meta }} + {{- if index $meta "json" -}} + {{- $value := index $meta "json" -}} + {{- if eq $value "-" -}} + {{- $_ := set $json "ignored" true -}} + {{- else -}} + {{- $_ := set $json "name" $value -}} + {{- $_ := set $json "explicit" true -}} + {{- end -}} + {{- end -}} + {{- if and (not (get $json "ignored")) (not (get $json "explicit")) (index $meta "go.tag.json") -}} + {{- $tagName := first (split "," (index $meta "go.tag.json")) -}} + {{- if eq $tagName "-" -}} + {{- $_ := set $json "ignored" true -}} + {{- else if $tagName -}} + {{- $_ := set $json "name" $tagName -}} + {{- end -}} + {{- end -}} + {{- end }} + {{- if not (get $json "ignored") }} + { + {{- $fieldName := (snakeCase $field.Name) -}} + {{- if $field.Optional }} + if (value->has_{{$fieldName}}) { + {{- end }} + cJSON *field_json = NULL; +{{ template "json_encode_value" dict "Prefix" $prefix "Type" $field.Type "Expr" (printf "value->%s" $fieldName) "OutVar" "field_json" "Types" $.Types }} + if (!cJSON_AddItemToObject(root, "{{get $json "name"}}", field_json)) { + cJSON_Delete(field_json); + goto fail; + } + {{- if $field.Optional }} + } + {{- end }} + } + {{- end }} +{{- end }} + return root; +fail: + cJSON_Delete(root); + return NULL; +} +{{- end }} + +{{- if exists $decodeReachable $type.Name }} +static {{ toUpper $prefix }}_JSON_UNUSED int {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }}_from_json(const cJSON *json, {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }} *out, {{ printf "%s_error" $prefix }} *error) { + if (!out) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "output pointer is NULL", NULL); + return -1; + } + if (!json || cJSON_IsNull(json)) return 0; + if (!cJSON_IsObject(json)) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected JSON object", NULL); + return -1; + } +{{- range $_, $field := $type.Fields }} + {{- $json := dict "name" $field.Name "ignored" false "explicit" false -}} + {{- range $_, $meta := $field.Meta }} + {{- if index $meta "json" -}} + {{- $value := index $meta "json" -}} + {{- if eq $value "-" -}} + {{- $_ := set $json "ignored" true -}} + {{- else -}} + {{- $_ := set $json "name" $value -}} + {{- $_ := set $json "explicit" true -}} + {{- end -}} + {{- end -}} + {{- if and (not (get $json "ignored")) (not (get $json "explicit")) (index $meta "go.tag.json") -}} + {{- $tagName := first (split "," (index $meta "go.tag.json")) -}} + {{- if eq $tagName "-" -}} + {{- $_ := set $json "ignored" true -}} + {{- else if $tagName -}} + {{- $_ := set $json "name" $tagName -}} + {{- end -}} + {{- end -}} + {{- end }} + {{- if not (get $json "ignored") }} + { + cJSON *field_json = cJSON_GetObjectItemCaseSensitive(json, "{{get $json "name"}}"); + int field_present = field_json != NULL; + {{- $fieldName := (snakeCase $field.Name) -}} + {{- $allowsRequiredNull := false -}} + {{- if isCoreType $field.Type }} + {{- if or (eq (toString $field.Type) "null") (eq (toString $field.Type) "any") }} + {{- $allowsRequiredNull = true -}} + {{- end -}} + {{- else if and (hasField $field.Type "Alias") $field.Type.Alias }} + {{- if isCoreType $field.Type.Alias.Type.Type }} + {{- if or (eq (toString $field.Type.Alias.Type.Type) "null") (eq (toString $field.Type.Alias.Type.Type) "any") }} + {{- $allowsRequiredNull = true -}} + {{- end -}} + {{- end -}} + {{- end }} + {{- if $field.Optional }} + if (field_present) { + out->has_{{$fieldName}} = true; + if (!cJSON_IsNull(field_json)) { +{{ template "json_decode_value" dict "Prefix" $prefix "Type" $field.Type "JsonExpr" "field_json" "DestExpr" (printf "out->%s" $fieldName) "Types" $.Types }} + } + } + {{- else }} + if (!field_present{{- if not $allowsRequiredNull }} || cJSON_IsNull(field_json){{- end }}) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "missing required field {{get $json "name"}}", NULL); + goto fail; + } +{{ template "json_decode_value" dict "Prefix" $prefix "Type" $field.Type "JsonExpr" "field_json" "DestExpr" (printf "out->%s" $fieldName) "Types" $.Types }} + {{- end }} + } + {{- end }} +{{- end }} + return 0; +fail: + {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }}_free(out); + return -1; +} +{{- end }} + +{{- end }} +{{- end }} +{{- end -}} diff --git a/implTransport.go.tmpl b/implTransport.go.tmpl new file mode 100644 index 0000000..a470b09 --- /dev/null +++ b/implTransport.go.tmpl @@ -0,0 +1,371 @@ +{{- define "implTransport" -}} +{{- $prefix := .Prefix -}} +static void {{ printf "%s_set_error" $prefix }}( + {{ printf "%s_error" $prefix }} *error, + int code, + int http_status, + const char *name, + const char *message, + const char *cause +) { + if (!error) return; + {{ printf "%s_error_free" $prefix }}(error); + error->code = code; + error->http_status = http_status; + error->name = {{ printf "%s_strdup" $prefix }}(name ? name : "WebrpcError"); + error->message = {{ printf "%s_strdup" $prefix }}(message ? message : "request failed"); + error->cause = cause ? {{ printf "%s_strdup" $prefix }}(cause) : NULL; +} + +static int {{ printf "%s_buffer_grow" $prefix }}({{ printf "%s_buffer" $prefix }} *buf, size_t need) { + if (buf->cap >= need) return 1; + size_t new_cap = buf->cap ? buf->cap : 1024; + while (new_cap < need) new_cap *= 2; + char *next = (char *)realloc(buf->data, new_cap); + if (!next) return 0; + buf->data = next; + buf->cap = new_cap; + return 1; +} + +static size_t {{ printf "%s_write_cb" $prefix }}(char *ptr, size_t size, size_t nmemb, void *userdata) { + size_t n = size * nmemb; + {{ printf "%s_buffer" $prefix }} *buf = ({{ printf "%s_buffer" $prefix }} *)userdata; + if (!{{ printf "%s_buffer_grow" $prefix }}(buf, buf->len + n + 1)) return 0; + memcpy(buf->data + buf->len, ptr, n); + buf->len += n; + buf->data[buf->len] = '\0'; + return n; +} + +static char *{{ printf "%s_join_url" $prefix }}(const char *base_url, const char *path) { + if (!base_url) return NULL; + if (!path) path = ""; + + size_t base_len = strlen(base_url); + size_t path_len = strlen(path); + int base_has_slash = base_len > 0 && base_url[base_len - 1] == '/'; + int path_has_slash = path_len > 0 && path[0] == '/'; + size_t cap = base_len + path_len + 2; + char *out = (char *)malloc(cap); + if (!out) return NULL; + + if (base_len == 0) { + snprintf(out, cap, "%s", path); + } else if (path_len == 0) { + snprintf(out, cap, "%s", base_url); + } else if (base_has_slash && path_has_slash) { + snprintf(out, cap, "%.*s%s", (int)(base_len - 1), base_url, path); + } else if (!base_has_slash && !path_has_slash) { + snprintf(out, cap, "%s/%s", base_url, path); + } else { + snprintf(out, cap, "%s%s", base_url, path); + } + return out; +} + +static int {{ printf "%s_append_header" $prefix }}(struct curl_slist **headers, const char *header_value) { + struct curl_slist *next; + + if (!headers || !header_value) return 0; + next = curl_slist_append(*headers, header_value); + if (!next) return 0; + *headers = next; + return 1; +} + +static int {{ printf "%s_header_name_equals" $prefix }}(const char *header, const char *name) { + size_t i = 0; + + if (!header || !name) return 0; + + while (header[i] != '\0' && header[i] != ':' && name[i] != '\0' && name[i] != ':') { + unsigned char h = (unsigned char)header[i]; + unsigned char n = (unsigned char)name[i]; + if (h >= 'A' && h <= 'Z') h = (unsigned char)(h - 'A' + 'a'); + if (n >= 'A' && n <= 'Z') n = (unsigned char)(n - 'A' + 'a'); + if (h != n) return 0; + i++; + } + + while (header[i] == ' ' || header[i] == '\t') { + i++; + } + + while (name[i] == ' ' || name[i] == '\t') { + i++; + } + + return (name[i] == '\0' || name[i] == ':') && (header[i] == '\0' || header[i] == ':'); +} + +static int {{ printf "%s_prepared_request_has_header" $prefix }}(const {{ printf "%s_prepared_request" $prefix }} *request, const char *name) { + if (!request || !name || !request->headers) return 0; + + for (size_t i = 0; i < request->headers_count; ++i) { + if (request->headers[i] && {{ printf "%s_header_name_equals" $prefix }}(request->headers[i], name)) { + return 1; + } + } + + return 0; +} + +static int {{ printf "%s_prepared_request_overrides_header" $prefix }}(const {{ printf "%s_prepared_request" $prefix }} *request, const char *header_line) { + if (!request || !header_line) return 0; + + if ({{ printf "%s_header_name_equals" $prefix }}(header_line, "Content-Type")) { + if ((request->content_type && request->content_type[0] != '\0') || + {{ printf "%s_prepared_request_has_header" $prefix }}(request, "Content-Type")) { + return 1; + } + } + + if (!request->headers) return 0; + for (size_t i = 0; i < request->headers_count; ++i) { + if (request->headers[i] && + {{ printf "%s_header_name_equals" $prefix }}(request->headers[i], header_line)) { + return 1; + } + } + + return 0; +} + +static int {{ printf "%s_curl_runtime_init" $prefix }}({{ printf "%s_error" $prefix }} *error) { + static int initialized = 0; + + if (initialized) { + return 0; + } + + if (curl_global_init(CURL_GLOBAL_DEFAULT) != 0) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "curl_global_init failed", NULL); + return -1; + } + + if (atexit(curl_global_cleanup) != 0) { + curl_global_cleanup(); + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "failed to register curl_global_cleanup", NULL); + return -1; + } + + initialized = 1; + return 0; +} + +static int {{ printf "%s_http_send_request" $prefix }}( + const char *base_url, + const {{ printf "%s_prepared_request" $prefix }} *request, + const char *bearer_token, + const struct curl_slist *default_headers, + long timeout_ms, + {{ printf "%s_http_response" $prefix }} *response, + {{ printf "%s_error" $prefix }} *error +) { + const char *http_method; + {{ printf "%s_http_response" $prefix }} result; + int has_content_type = 0; + int has_authorization = 0; + {{ printf "%s_http_response_init" $prefix }}(&result); + + if (!request || !response) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "ClientError", "request and response must be non-NULL", NULL); + return -1; + } + + if ({{ printf "%s_curl_runtime_init" $prefix }}(error) != 0) { + return -1; + } + + CURL *curl = curl_easy_init(); + if (!curl) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "curl_easy_init failed", NULL); + return -1; + } + + char *url = {{ printf "%s_join_url" $prefix }}(base_url, request->path ? request->path : ""); + if (!url) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "failed to build URL", NULL); + curl_easy_cleanup(curl); + return -1; + } + + struct curl_slist *headers = NULL; + for (const struct curl_slist *it = default_headers; it; it = it->next) { + if ((bearer_token && bearer_token[0] != '\0' && {{ printf "%s_header_name_equals" $prefix }}(it->data, "Authorization")) || + {{ printf "%s_prepared_request_overrides_header" $prefix }}(request, it->data)) { + continue; + } + if (!{{ printf "%s_append_header" $prefix }}(&headers, it->data)) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "failed to copy request headers", NULL); + curl_slist_free_all(headers); + free(url); + curl_easy_cleanup(curl); + return -1; + } + if ({{ printf "%s_header_name_equals" $prefix }}(it->data, "Content-Type")) { + has_content_type = 1; + } + if ({{ printf "%s_header_name_equals" $prefix }}(it->data, "Authorization")) { + has_authorization = 1; + } + } + + if (request->content_type && request->content_type[0] != '\0') { + size_t need = strlen("Content-Type: ") + strlen(request->content_type) + 1; + char *content_type_header = (char *)malloc(need); + if (!content_type_header) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "failed to allocate content-type header", NULL); + curl_slist_free_all(headers); + free(url); + curl_easy_cleanup(curl); + return -1; + } + snprintf(content_type_header, need, "Content-Type: %s", request->content_type); + if (!{{ printf "%s_append_header" $prefix }}(&headers, content_type_header)) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "failed to append content-type header", NULL); + free(content_type_header); + curl_slist_free_all(headers); + free(url); + curl_easy_cleanup(curl); + return -1; + } + free(content_type_header); + has_content_type = 1; + } + + for (size_t i = 0; i < request->headers_count; ++i) { + if (!request->headers || !request->headers[i]) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "request headers are invalid", NULL); + curl_slist_free_all(headers); + free(url); + curl_easy_cleanup(curl); + return -1; + } + if (!{{ printf "%s_append_header" $prefix }}(&headers, request->headers[i])) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "failed to append request header", NULL); + curl_slist_free_all(headers); + free(url); + curl_easy_cleanup(curl); + return -1; + } + if ({{ printf "%s_header_name_equals" $prefix }}(request->headers[i], "Content-Type")) { + has_content_type = 1; + } + if ({{ printf "%s_header_name_equals" $prefix }}(request->headers[i], "Authorization")) { + has_authorization = 1; + } + } + + if (!has_content_type && !{{ printf "%s_append_header" $prefix }}(&headers, "Content-Type: application/json")) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "failed to allocate request headers", NULL); + curl_slist_free_all(headers); + free(url); + curl_easy_cleanup(curl); + return -1; + } + if (bearer_token && bearer_token[0] != '\0' && !has_authorization) { + size_t need = strlen("Authorization: Bearer ") + strlen(bearer_token) + 1; + char *auth_header = (char *)malloc(need); + if (!auth_header) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "failed to allocate auth header", NULL); + curl_slist_free_all(headers); + free(url); + curl_easy_cleanup(curl); + return -1; + } + snprintf(auth_header, need, "Authorization: Bearer %s", bearer_token); + if (!{{ printf "%s_append_header" $prefix }}(&headers, auth_header)) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "failed to append auth header", NULL); + free(auth_header); + curl_slist_free_all(headers); + free(url); + curl_easy_cleanup(curl); + return -1; + } + free(auth_header); + } + + {{ printf "%s_buffer" $prefix }} buf; + memset(&buf, 0, sizeof(buf)); + http_method = request->http_method && request->http_method[0] != '\0' ? request->http_method : "POST"; + + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, http_method); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request->body ? request->body : ""); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)(request->body ? request->body_len : 0)); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms > 0 ? timeout_ms : 10000L); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, {{ printf "%s_write_cb" $prefix }}); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf); + + CURLcode rc = curl_easy_perform(curl); + if (rc != CURLE_OK) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "HTTP request failed", curl_easy_strerror(rc)); + } else { + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &result.status_code); + result.body = buf.data ? buf.data : {{ printf "%s_strdup" $prefix }}(""); + result.body_len = buf.len; + buf.data = NULL; + } + + free(buf.data); + curl_slist_free_all(headers); + free(url); + curl_easy_cleanup(curl); + if (rc != CURLE_OK) { + {{ printf "%s_http_response_free" $prefix }}(&result); + return -1; + } + + *response = result; + memset(&result, 0, sizeof(result)); + return 0; +} + +static void {{ printf "%s_parse_rpc_error" $prefix }}(const char *body, long http_status, {{ printf "%s_error" $prefix }} *error) { + cJSON *error_name = NULL; + cJSON *error_code = NULL; + cJSON *error_msg = NULL; + cJSON *error_cause = NULL; + cJSON *error_status = NULL; + int64_t parsed_code = 0; + int64_t parsed_status = (int64_t)http_status; + + if (!body || body[0] == '\0') { + {{ printf "%s_set_error" $prefix }}(error, 0, (int)http_status, "WebrpcRequestFailed", "request failed", NULL); + return; + } + + cJSON *root = {{ printf "%s_cjson_parse" $prefix }}(body); + if (!root || !cJSON_IsObject(root)) { + if (root) cJSON_Delete(root); + {{ printf "%s_set_error" $prefix }}(error, 0, (int)http_status, "WebrpcRequestFailed", "request failed", body); + return; + } + + error_name = cJSON_GetObjectItemCaseSensitive(root, "error"); + error_code = cJSON_GetObjectItemCaseSensitive(root, "code"); + error_msg = cJSON_GetObjectItemCaseSensitive(root, "msg"); + error_cause = cJSON_GetObjectItemCaseSensitive(root, "cause"); + error_status = cJSON_GetObjectItemCaseSensitive(root, "status"); + if (error_code && !cJSON_IsNull(error_code)) { + (void){{ printf "%s_cjson_get_int64_exact" $prefix }}(error_code, &parsed_code); + } + if (error_status && !cJSON_IsNull(error_status)) { + (void){{ printf "%s_cjson_get_int64_exact" $prefix }}(error_status, &parsed_status); + } + + {{ printf "%s_set_error" $prefix }}( + error, + (int)parsed_code, + (int)parsed_status, + cJSON_IsString(error_name) && cJSON_GetStringValue(error_name) ? cJSON_GetStringValue(error_name) : "WebrpcRequestFailed", + cJSON_IsString(error_msg) && cJSON_GetStringValue(error_msg) ? cJSON_GetStringValue(error_msg) : "request failed", + cJSON_IsString(error_cause) ? cJSON_GetStringValue(error_cause) : NULL + ); + cJSON_Delete(root); +} +{{- end -}} diff --git a/interop_test.go b/interop_test.go new file mode 100644 index 0000000..de02681 --- /dev/null +++ b/interop_test.go @@ -0,0 +1,500 @@ +package c + +import ( + "bytes" + "fmt" + "io" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "testing" + "time" +) + +func TestInteropWithWebrpcTest(t *testing.T) { + root := repoRoot(t) + tmp := t.TempDir() + + webrpcTest := ensureWebrpcTestBinary(t) + toolDir := filepath.Dir(webrpcTest) + + schemaText := runCmdOutputEnv(t, root, withPrependedPath(os.Environ(), toolDir), webrpcTest, "-print-schema") + + schemaPath := filepath.Join(tmp, "interop.ridl") + if err := os.WriteFile(schemaPath, []byte(schemaText), 0o644); err != nil { + t.Fatalf("write interop schema: %v", err) + } + + header := filepath.Join(tmp, "interop.gen.h") + impl := filepath.Join(tmp, "interop.gen.c") + generateC(t, root, schemaPath, header, impl, "test") + + testMain := filepath.Join(tmp, "interop_test_main.c") + if err := os.WriteFile(testMain, []byte(interopCTestProgram), 0o644); err != nil { + t.Fatalf("write interop C test program: %v", err) + } + + cflags := pkgConfigFlags(t, "--cflags") + libs := pkgConfigFlags(t, "--libs") + bin := filepath.Join(tmp, "interop-test") + args := append([]string{"-std=c99", "-Wall", "-Wextra"}, cflags...) + args = append(args, "interop_test_main.c", "-o", bin) + args = append(args, libs...) + runCmd(t, tmp, "cc", args...) + + port := freeTCPPort(t) + serverURL := fmt.Sprintf("http://127.0.0.1:%d", port) + + serverCmd := exec.Command(webrpcTest, "-server", "-port="+strconv.Itoa(port), "-timeout=10m") + serverCmd.Env = withPrependedPath(os.Environ(), toolDir) + var serverLog bytes.Buffer + serverCmd.Stdout = &serverLog + serverCmd.Stderr = &serverLog + + if err := serverCmd.Start(); err != nil { + t.Fatalf("start webrpc-test server: %v", err) + } + t.Cleanup(func() { + if serverCmd.Process != nil { + _ = serverCmd.Process.Kill() + } + _ = serverCmd.Wait() + }) + + waitForTCP(t, port, 10*time.Second, &serverLog) + runCmd(t, tmp, bin, serverURL) +} + +func ensureWebrpcTestBinary(t *testing.T) string { + t.Helper() + + cacheDir := filepath.Join(os.TempDir(), "gen-c-webrpc-bin", webrpcGenVersion, runtime.GOOS+"-"+runtime.GOARCH) + name := "webrpc-test" + if runtime.GOOS == "windows" { + name += ".exe" + } + binPath := filepath.Join(cacheDir, name) + + if info, err := os.Stat(binPath); err == nil && info.Mode().IsRegular() { + return binPath + } + + if err := os.MkdirAll(cacheDir, 0o755); err != nil { + t.Fatalf("create webrpc-test cache dir: %v", err) + } + + if url, ok := webrpcTestReleaseURL(); ok { + if err := downloadExecutable(url, binPath); err == nil { + return binPath + } else { + t.Logf("download webrpc-test binary failed, falling back to go install: %v", err) + } + } + + env := append(os.Environ(), "GOWORK=off", "GOBIN="+cacheDir) + runCmdEnv(t, cacheDir, env, "go", "install", "github.com/webrpc/webrpc/cmd/webrpc-test@"+webrpcGenVersion) + return binPath +} + +func webrpcTestReleaseURL() (string, bool) { + goos := runtime.GOOS + goarch := runtime.GOARCH + + switch goos { + case "darwin", "linux": + default: + return "", false + } + + switch goarch { + case "amd64", "arm64": + default: + return "", false + } + + return fmt.Sprintf( + "https://github.com/webrpc/webrpc/releases/download/%s/webrpc-test.%s-%s", + webrpcGenVersion, + goos, + goarch, + ), true +} + +func downloadExecutable(url, dst string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected HTTP status %s", resp.Status) + } + + tmp := dst + ".tmp" + f, err := os.Create(tmp) + if err != nil { + return err + } + + if _, err := io.Copy(f, resp.Body); err != nil { + _ = f.Close() + _ = os.Remove(tmp) + return err + } + + if err := f.Close(); err != nil { + _ = os.Remove(tmp) + return err + } + + if err := os.Chmod(tmp, 0o755); err != nil { + _ = os.Remove(tmp) + return err + } + + return os.Rename(tmp, dst) +} + +func freeTCPPort(t *testing.T) int { + t.Helper() + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("reserve tcp port: %v", err) + } + defer ln.Close() + + return ln.Addr().(*net.TCPAddr).Port +} + +func waitForTCP(t *testing.T, port int, timeout time.Duration, serverLog *bytes.Buffer) { + t.Helper() + + deadline := time.Now().Add(timeout) + address := net.JoinHostPort("127.0.0.1", strconv.Itoa(port)) + + for time.Now().Before(deadline) { + conn, err := net.DialTimeout("tcp", address, 200*time.Millisecond) + if err == nil { + _ = conn.Close() + return + } + time.Sleep(100 * time.Millisecond) + } + + t.Fatalf("webrpc-test server did not become ready on %s\nserver output:\n%s", address, serverLog.String()) +} + +func withPrependedPath(env []string, dir string) []string { + pathValue := dir + if current, ok := lookupEnv(env, "PATH"); ok && current != "" { + pathValue = dir + string(os.PathListSeparator) + current + } + return upsertEnv(env, "PATH", pathValue) +} + +func lookupEnv(env []string, key string) (string, bool) { + prefix := key + "=" + for _, entry := range env { + if strings.HasPrefix(entry, prefix) { + return strings.TrimPrefix(entry, prefix), true + } + } + return "", false +} + +func upsertEnv(env []string, key, value string) []string { + prefix := key + "=" + result := make([]string, 0, len(env)+1) + found := false + for _, entry := range env { + if strings.HasPrefix(entry, prefix) { + result = append(result, prefix+value) + found = true + continue + } + result = append(result, entry) + } + if !found { + result = append(result, prefix+value) + } + return result +} + +func runCmdEnv(t *testing.T, dir string, env []string, name string, args ...string) string { + t.Helper() + + cmd := exec.Command(name, args...) + cmd.Dir = dir + cmd.Env = env + + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + t.Fatalf("%s %s failed: %v\nstdout:\n%s\nstderr:\n%s", name, strings.Join(args, " "), err, stdout.String(), stderr.String()) + } + + return stdout.String() +} + +func runCmdOutputEnv(t *testing.T, dir string, env []string, name string, args ...string) string { + t.Helper() + return runCmdEnv(t, dir, env, name, args...) +} + +const interopCTestProgram = `#include +#include +#include + +#include "interop.gen.c" + +static void fail_msg(const char *msg) { + fprintf(stderr, "%s\n", msg); + exit(1); +} + +static void expect_true(int cond, const char *msg) { + if (!cond) { + fail_msg(msg); + } +} + +int main(int argc, char **argv) { + const char *base_url; + test_test_api_client *client; + test_error error; + + if (argc != 2) { + fail_msg("expected base URL argument"); + } + + base_url = argv[1]; + client = test_test_api_client_create(base_url, NULL); + expect_true(client != NULL, "failed to create client"); + + test_error_init(&error); + + { + test_test_api_get_empty_request request; + test_test_api_get_empty_response response; + test_test_api_get_empty_request_init(&request); + test_test_api_get_empty_response_init(&response); + expect_true(test_test_api_get_empty(client, &request, &response, &error) == 0, "GetEmpty failed"); + test_test_api_get_empty_response_free(&response); + } + + { + test_test_api_get_error_request request; + test_test_api_get_error_response response; + test_test_api_get_error_request_init(&request); + test_test_api_get_error_response_init(&response); + expect_true(test_test_api_get_error(client, &request, &response, &error) != 0, "GetError should fail"); + expect_true(error.code == 0, "GetError code mismatch"); + expect_true(error.http_status == 400, "GetError HTTP status mismatch"); + expect_true(error.name != NULL && strcmp(error.name, "WebrpcEndpoint") == 0, "GetError name mismatch"); + expect_true(error.message != NULL && strcmp(error.message, "endpoint error") == 0, "GetError message mismatch"); + test_error_free(&error); + test_error_init(&error); + test_test_api_get_error_response_free(&response); + } + + { + test_test_api_get_one_request get_request; + test_test_api_get_one_response get_response; + test_test_api_send_one_request send_request; + test_test_api_send_one_response send_response; + + test_test_api_get_one_request_init(&get_request); + test_test_api_get_one_response_init(&get_response); + test_test_api_send_one_response_init(&send_response); + + expect_true(test_test_api_get_one(client, &get_request, &get_response, &error) == 0, "GetOne failed"); + expect_true(get_response.one != NULL, "GetOne payload missing"); + expect_true(get_response.one->id == 1, "GetOne id mismatch"); + expect_true(get_response.one->name != NULL && strcmp(get_response.one->name, "one") == 0, "GetOne name mismatch"); + + test_test_api_send_one_request_init(&send_request); + send_request.one = get_response.one; + expect_true(test_test_api_send_one(client, &send_request, &send_response, &error) == 0, "SendOne failed"); + + test_test_api_send_one_response_free(&send_response); + test_test_api_get_one_response_free(&get_response); + } + + { + test_test_api_get_multi_request get_request; + test_test_api_get_multi_response get_response; + test_test_api_send_multi_request send_request; + test_test_api_send_multi_response send_response; + + test_test_api_get_multi_request_init(&get_request); + test_test_api_get_multi_response_init(&get_response); + test_test_api_send_multi_response_init(&send_response); + + expect_true(test_test_api_get_multi(client, &get_request, &get_response, &error) == 0, "GetMulti failed"); + expect_true(get_response.one != NULL && get_response.two != NULL && get_response.three != NULL, "GetMulti payload missing"); + expect_true(strcmp(get_response.one->name, "one") == 0, "GetMulti one mismatch"); + expect_true(strcmp(get_response.two->name, "two") == 0, "GetMulti two mismatch"); + expect_true(strcmp(get_response.three->name, "three") == 0, "GetMulti three mismatch"); + + test_test_api_send_multi_request_init(&send_request); + send_request.one = get_response.one; + send_request.two = get_response.two; + send_request.three = get_response.three; + expect_true(test_test_api_send_multi(client, &send_request, &send_response, &error) == 0, "SendMulti failed"); + + test_test_api_send_multi_response_free(&send_response); + test_test_api_get_multi_response_free(&get_response); + } + + { + test_test_api_get_complex_request get_request; + test_test_api_get_complex_response get_response; + test_test_api_send_complex_request send_request; + test_test_api_send_complex_response send_response; + size_t i; + int found_read = 0; + int found_write = 0; + + test_test_api_get_complex_request_init(&get_request); + test_test_api_get_complex_response_init(&get_response); + test_test_api_send_complex_response_init(&send_response); + + expect_true(test_test_api_get_complex(client, &get_request, &get_response, &error) == 0, "GetComplex failed"); + expect_true(get_response.complex != NULL, "GetComplex payload missing"); + + expect_true(get_response.complex->meta.count == 2, "GetComplex meta count mismatch"); + expect_true(get_response.complex->meta_nested_example.count == 1, "GetComplex nested meta count mismatch"); + expect_true(get_response.complex->names_list.count == 3, "GetComplex names list count mismatch"); + expect_true(strcmp(get_response.complex->names_list.items[0], "John") == 0, "GetComplex names list item 0 mismatch"); + expect_true(strcmp(get_response.complex->names_list.items[1], "Alice") == 0, "GetComplex names list item 1 mismatch"); + expect_true(strcmp(get_response.complex->names_list.items[2], "Jakob") == 0, "GetComplex names list item 2 mismatch"); + expect_true(get_response.complex->nums_list.count == 4, "GetComplex nums list count mismatch"); + expect_true(get_response.complex->nums_list.items[3] == 4534643543LL, "GetComplex nums list item mismatch"); + expect_true(get_response.complex->double_array.count == 2, "GetComplex double array outer count mismatch"); + expect_true(get_response.complex->double_array.items[0].count == 1, "GetComplex double array inner count mismatch"); + expect_true(strcmp(get_response.complex->double_array.items[0].items[0], "testing") == 0, "GetComplex double array first value mismatch"); + expect_true(strcmp(get_response.complex->double_array.items[1].items[0], "api") == 0, "GetComplex double array second value mismatch"); + expect_true(get_response.complex->list_of_maps.count == 1, "GetComplex list_of_maps count mismatch"); + expect_true(get_response.complex->list_of_maps.items[0].count == 3, "GetComplex list_of_maps entry count mismatch"); + expect_true(get_response.complex->list_of_users.count == 1, "GetComplex list_of_users count mismatch"); + expect_true(get_response.complex->list_of_users.items[0] != NULL, "GetComplex list_of_users item missing"); + expect_true(get_response.complex->list_of_users.items[0]->id == 1, "GetComplex list_of_users id mismatch"); + expect_true(strcmp(get_response.complex->list_of_users.items[0]->username, "John-Doe") == 0, "GetComplex list_of_users username mismatch"); + expect_true(strcmp(get_response.complex->list_of_users.items[0]->role, "admin") == 0, "GetComplex list_of_users role mismatch"); + expect_true(get_response.complex->map_of_users.count == 1, "GetComplex map_of_users count mismatch"); + expect_true(get_response.complex->user != NULL, "GetComplex user missing"); + expect_true(get_response.complex->user->id == 1, "GetComplex user id mismatch"); + expect_true(strcmp(get_response.complex->user->username, "John-Doe") == 0, "GetComplex user username mismatch"); + expect_true(strcmp(get_response.complex->user->role, "admin") == 0, "GetComplex user role mismatch"); + expect_true(get_response.complex->status == TEST_STATUS_AVAILABLE, "GetComplex status mismatch"); + + for (i = 0; i < get_response.complex->list_of_maps.items[0].count; ++i) { + const char *key = get_response.complex->list_of_maps.items[0].keys[i]; + uint32_t value = get_response.complex->list_of_maps.items[0].values[i]; + if (strcmp(key, "john") == 0) { + expect_true(value == 1, "GetComplex list_of_maps john mismatch"); + } else if (strcmp(key, "alice") == 0) { + expect_true(value == 2, "GetComplex list_of_maps alice mismatch"); + } else if (strcmp(key, "Jakob") == 0) { + expect_true(value == 251, "GetComplex list_of_maps Jakob mismatch"); + } else { + fail_msg("GetComplex list_of_maps unexpected key"); + } + } + + for (i = 0; i < get_response.complex->map_of_users.count; ++i) { + const char *key = get_response.complex->map_of_users.keys[i]; + test_user *value = get_response.complex->map_of_users.values[i]; + if (strcmp(key, "admin") != 0) { + fail_msg("GetComplex map_of_users unexpected key"); + } + expect_true(value != NULL, "GetComplex map_of_users value missing"); + expect_true(value->id == 1, "GetComplex map_of_users id mismatch"); + expect_true(strcmp(value->username, "John-Doe") == 0, "GetComplex map_of_users username mismatch"); + expect_true(strcmp(value->role, "admin") == 0, "GetComplex map_of_users role mismatch"); + } + + test_test_api_send_complex_request_init(&send_request); + send_request.complex = get_response.complex; + expect_true(test_test_api_send_complex(client, &send_request, &send_response, &error) == 0, "SendComplex failed"); + + test_test_api_send_complex_response_free(&send_response); + test_test_api_get_complex_response_free(&get_response); + + { + test_test_api_get_enum_map_request request; + test_test_api_get_enum_map_response response; + + test_test_api_get_enum_map_request_init(&request); + test_test_api_get_enum_map_response_init(&response); + + expect_true(test_test_api_get_enum_map(client, &request, &response, &error) == 0, "GetEnumMap failed"); + expect_true(response.map.count == 2, "GetEnumMap count mismatch"); + for (i = 0; i < response.map.count; ++i) { + if (response.map.keys[i] == TEST_ACCESS_READ) { + expect_true(response.map.values[i] == 1, "GetEnumMap READ mismatch"); + found_read = 1; + } else if (response.map.keys[i] == TEST_ACCESS_WRITE) { + expect_true(response.map.values[i] == 2, "GetEnumMap WRITE mismatch"); + found_write = 1; + } else { + fail_msg("GetEnumMap unexpected key"); + } + } + expect_true(found_read, "GetEnumMap missing READ"); + expect_true(found_write, "GetEnumMap missing WRITE"); + + test_test_api_get_enum_map_response_free(&response); + } + } + + { + test_test_api_get_enum_list_request request; + test_test_api_get_enum_list_response response; + + test_test_api_get_enum_list_request_init(&request); + test_test_api_get_enum_list_response_init(&response); + + expect_true(test_test_api_get_enum_list(client, &request, &response, &error) == 0, "GetEnumList failed"); + expect_true(response.list.count == 2, "GetEnumList length mismatch"); + expect_true(response.list.items[0] == TEST_STATUS_AVAILABLE, "GetEnumList item 0 mismatch"); + expect_true(response.list.items[1] == TEST_STATUS_NOT_AVAILABLE, "GetEnumList item 1 mismatch"); + + test_test_api_get_enum_list_response_free(&response); + } + + { + test_test_api_get_schema_error_request request; + test_test_api_get_schema_error_response response; + + test_test_api_get_schema_error_request_init(&request); + test_test_api_get_schema_error_response_init(&response); + request.code = 100; + + expect_true(test_test_api_get_schema_error(client, &request, &response, &error) != 0, "GetSchemaError should fail"); + expect_true(error.code == 100, "GetSchemaError code mismatch"); + expect_true(error.http_status == 429, "GetSchemaError HTTP status mismatch"); + expect_true(error.name != NULL && strcmp(error.name, "RateLimited") == 0, "GetSchemaError name mismatch"); + expect_true(error.message != NULL && strcmp(error.message, "too many requests") == 0, "GetSchemaError message mismatch"); + expect_true(error.cause != NULL && strcmp(error.cause, "1000 req/min exceeded") == 0, "GetSchemaError cause mismatch"); + + test_error_free(&error); + test_error_init(&error); + test_test_api_get_schema_error_response_free(&response); + } + + test_error_free(&error); + test_test_api_client_destroy(client); + return 0; +} +` diff --git a/main.go.tmpl b/main.go.tmpl new file mode 100644 index 0000000..d5de45b --- /dev/null +++ b/main.go.tmpl @@ -0,0 +1,264 @@ +{{- define "main" -}} + +{{- $opts := dict -}} +{{- $defaultPrefix := (snakeCase .SchemaName) -}} +{{- $emitClient := (ternary (eq (default .Opts.client "true") "false") false true) -}} +{{- set $opts "client" $emitClient -}} +{{- set $opts "emit" (default .Opts.emit "header") -}} +{{- set $opts "prefix" (default .Opts.prefix $defaultPrefix) -}} +{{- set $opts "header" (default .Opts.header (printf "%s.h" (get $opts "prefix"))) -}} + +{{- if exists .Opts "help" -}} + {{- template "help" $opts -}} + {{- exit 0 -}} +{{- end -}} + +{{- range $k, $v := .Opts }} + {{- if not (exists $opts $k) -}} + {{- stderrPrintf "-%v=%q is not supported target option\n\nUsage:\n" $k $v -}} + {{- template "help" $opts -}} + {{- exit 1 -}} + {{- end -}} +{{- end -}} + +{{- if ne .WebrpcVersion "v1" -}} + {{- stderrPrintf "%s generator error: unsupported webrpc version %s\n" .WebrpcTarget .WebrpcVersion -}} + {{- exit 1 -}} +{{- end -}} + +{{- if not (minVersion .WebrpcGenVersion "v0.14.0") -}} + {{- stderrPrintf "%s generator error: unsupported webrpc-gen version %s, please update\n" .WebrpcTarget .WebrpcGenVersion -}} + {{- exit 1 -}} +{{- end -}} + +{{- if not (in $opts.emit "header" "impl") -}} + {{- stderrPrintf "%s generator error: unsupported emit mode %s (expected header or impl)\n" .WebrpcTarget $opts.emit -}} + {{- exit 1 -}} +{{- end -}} + +{{- $prefix := $opts.prefix -}} +{{- if eq $opts.emit "impl" -}} +{{ template "impl" dict "Prefix" $prefix "Header" $opts.header "SchemaName" .SchemaName "SchemaVersion" .SchemaVersion "SchemaHash" .SchemaHash "BasePath" (default (and (hasField . "BasePath") .BasePath) "/rpc/") "WebrpcGenVersion" .WebrpcGenVersion "WebrpcTarget" .WebrpcTarget "WebrpcGenCommand" .WebrpcGenCommand "Types" .Types "Services" .Services "Errors" .Errors "WebrpcErrors" .WebrpcErrors "Opts" $opts "Client" $emitClient }} +{{- else -}} +{{- $guard := printf "%s_%s_%s_H" (toUpper (snakeCase .SchemaName)) (toUpper (snakeCase $prefix)) (toUpper $opts.emit) -}} +#ifndef {{$guard}} +#define {{$guard}} + +// {{.SchemaName}} {{.SchemaVersion}} {{.SchemaHash}} +// -- +// Code generated by webrpc-gen@{{.WebrpcGenVersion}} with {{.WebrpcTarget}} generator. DO NOT EDIT. +// +// {{.WebrpcGenCommand}} + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + char *digits; +} {{ printf "%s_bigint" $prefix }}; + +typedef struct { + char *value; +} {{ printf "%s_timestamp" $prefix }}; + +typedef struct { + int code; + int http_status; + char *name; + char *message; + char *cause; +} {{ printf "%s_error" $prefix }}; + +typedef struct { + char *http_method; + char *path; + char *body; + size_t body_len; + char *content_type; + char **headers; + size_t headers_count; +} {{ printf "%s_prepared_request" $prefix }}; + +typedef struct { + long status_code; + char *body; + size_t body_len; +} {{ printf "%s_http_response" $prefix }}; + +typedef struct { + const char *bearer_token; + const char * const *headers; + size_t headers_count; + long timeout_ms; +} {{ printf "%s_client_options" $prefix }}; + +static inline char *{{ printf "%s_strdup" $prefix }}(const char *value) { + if (!value) return NULL; + size_t n = strlen(value) + 1; + char *copy = (char *)malloc(n); + if (!copy) return NULL; + memcpy(copy, value, n); + return copy; +} + +static inline void {{ printf "%s_bigint_init" $prefix }}({{ printf "%s_bigint" $prefix }} *value) { + if (!value) return; + value->digits = NULL; +} + +static inline void {{ printf "%s_bigint_free" $prefix }}({{ printf "%s_bigint" $prefix }} *value) { + if (!value) return; + free(value->digits); + value->digits = NULL; +} + +static inline int {{ printf "%s_bigint_set_string" $prefix }}({{ printf "%s_bigint" $prefix }} *value, const char *digits) { + if (!value || !digits || digits[0] == '\0') return -1; + + size_t start = 0; + bool negative = false; + if (digits[0] == '+') return -1; + if (digits[0] == '-') { + negative = true; + start = 1; + } + if (digits[start] == '\0') return -1; + + for (size_t i = start; digits[i] != '\0'; ++i) { + if (digits[i] < '0' || digits[i] > '9') return -1; + } + + while (digits[start] == '0' && digits[start + 1] != '\0') { + start++; + } + if (digits[start] == '0') negative = false; + + size_t len = strlen(digits + start); + size_t total = len + (negative ? 2 : 1); + char *normalized = (char *)malloc(total); + if (!normalized) return -1; + + if (negative) { + normalized[0] = '-'; + memcpy(normalized + 1, digits + start, len + 1); + } else { + memcpy(normalized, digits + start, len + 1); + } + + free(value->digits); + value->digits = normalized; + return 0; +} + +static inline void {{ printf "%s_timestamp_init" $prefix }}({{ printf "%s_timestamp" $prefix }} *value) { + if (!value) return; + value->value = NULL; +} + +static inline void {{ printf "%s_timestamp_free" $prefix }}({{ printf "%s_timestamp" $prefix }} *value) { + if (!value) return; + free(value->value); + value->value = NULL; +} + +static inline int {{ printf "%s_timestamp_set_string" $prefix }}({{ printf "%s_timestamp" $prefix }} *value, const char *timestamp) { + if (!value) return -1; + char *copy = {{ printf "%s_strdup" $prefix }}(timestamp); + if (timestamp && !copy) return -1; + free(value->value); + value->value = copy; + return 0; +} + +static inline void {{ printf "%s_error_init" $prefix }}({{ printf "%s_error" $prefix }} *error) { + if (!error) return; + memset(error, 0, sizeof(*error)); +} + +static inline void {{ printf "%s_error_free" $prefix }}({{ printf "%s_error" $prefix }} *error) { + if (!error) return; + free(error->name); + free(error->message); + free(error->cause); + memset(error, 0, sizeof(*error)); +} + +static inline void {{ printf "%s_prepared_request_init" $prefix }}({{ printf "%s_prepared_request" $prefix }} *request) { + if (!request) return; + memset(request, 0, sizeof(*request)); +} + +static inline void {{ printf "%s_prepared_request_free" $prefix }}({{ printf "%s_prepared_request" $prefix }} *request) { + if (!request) return; + free(request->http_method); + free(request->path); + free(request->body); + free(request->content_type); + if (request->headers) { + for (size_t i = 0; i < request->headers_count; ++i) { + free(request->headers[i]); + } + free(request->headers); + } + memset(request, 0, sizeof(*request)); +} + +static inline int {{ printf "%s_prepared_request_add_header" $prefix }}({{ printf "%s_prepared_request" $prefix }} *request, const char *header) { + char **next_headers; + char *copy; + + if (!request || !header) return -1; + + next_headers = (char **)realloc(request->headers, sizeof(*next_headers) * (request->headers_count + 1)); + if (!next_headers) return -1; + request->headers = next_headers; + + copy = {{ printf "%s_strdup" $prefix }}(header); + if (!copy) { + return -1; + } + + request->headers[request->headers_count] = copy; + request->headers_count += 1; + return 0; +} + +static inline void {{ printf "%s_http_response_init" $prefix }}({{ printf "%s_http_response" $prefix }} *response) { + if (!response) return; + memset(response, 0, sizeof(*response)); +} + +static inline void {{ printf "%s_http_response_free" $prefix }}({{ printf "%s_http_response" $prefix }} *response) { + if (!response) return; + free(response->body); + response->body = NULL; + response->body_len = 0; + response->status_code = 0; +} + +static inline void {{ printf "%s_client_options_init" $prefix }}({{ printf "%s_client_options" $prefix }} *options) { + if (!options) return; + memset(options, 0, sizeof(*options)); + options->timeout_ms = 10000L; +} + +{{ template "types" dict "Prefix" $prefix "Types" .Types "Services" .Services }} + +{{- if $emitClient }} +{{ template "client" dict "Prefix" $prefix "Services" .Services }} +{{- end }} + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // {{$guard}} +{{- end -}} +{{- end -}} diff --git a/testdata/codec.ridl b/testdata/codec.ridl new file mode 100644 index 0000000..bb9a8ea --- /dev/null +++ b/testdata/codec.ridl @@ -0,0 +1,19 @@ +webrpc = v1 + +name = codec-test +version = v1.0.0 +basepath = /rpc + +struct Nested + - id: bigint + +struct Payload + - count: bigint + - explicitAny: any + - explicitNull: null + - maybeAny?: any + - nested: Nested + - items: []bigint + +service Codec + - Echo(payload: Payload) => (payload: Payload) diff --git a/testdata/smoke.ridl b/testdata/smoke.ridl new file mode 100644 index 0000000..7f40c28 --- /dev/null +++ b/testdata/smoke.ridl @@ -0,0 +1,19 @@ +webrpc = v1 + +name = smoke +version = v1.0.0 +basepath = /rpc + +enum Role: uint32 + - USER + - ADMIN + +struct Profile + - id: bigint + - name: string + - role: Role + - tags?: []string + - meta?: map + +service Smoke + - Echo(profile: Profile) => (profile: Profile) diff --git a/testdata/succinct.ridl b/testdata/succinct.ridl new file mode 100644 index 0000000..5ba4126 --- /dev/null +++ b/testdata/succinct.ridl @@ -0,0 +1,16 @@ +webrpc = v1 + +name = succinct-test +version = v1.0.0 +basepath = /rpc + +struct FlattenRequest + - name: string + - amount: uint64 + +struct FlattenResponse + - id: uint64 + - count: uint64 + +service Demo + - Flatten(FlattenRequest) => (FlattenResponse) diff --git a/type.go.tmpl b/type.go.tmpl new file mode 100644 index 0000000..5012317 --- /dev/null +++ b/type.go.tmpl @@ -0,0 +1,75 @@ +{{- define "type" -}} +{{- $type := .Type -}} +{{- $prefix := .Prefix -}} +{{- $types := array -}} +{{- if exists . "Types" -}} +{{- $types = get . "Types" -}} +{{- end -}} + +{{- if isMapType $type -}} + {{- template "assert_supported_map_key_type" dict "Type" (mapKeyType $type) "Types" $types -}} +struct { + {{ template "type" dict "Type" (mapKeyType $type) "Prefix" $prefix "Types" $types }} *keys; + {{ template "type" dict "Type" (mapValueType $type) "Prefix" $prefix "Types" $types }} *values; + size_t count; +} +{{- else if isListType $type -}} + {{- $elem := listElemType $type -}} + {{- if eq (toString $elem) "byte" -}} +struct { + uint8_t *data; + size_t len; +} + {{- else -}} +struct { + {{ template "type" dict "Type" $elem "Prefix" $prefix "Types" $types }} *items; + size_t count; +} + {{- end -}} +{{- else if isCoreType $type -}} + {{- if eq (toString $type) "byte" -}}uint8_t + {{- else if eq (toString $type) "bool" -}}bool + {{- else if eq (toString $type) "uint" -}}unsigned int + {{- else if eq (toString $type) "uint8" -}}uint8_t + {{- else if eq (toString $type) "uint16" -}}uint16_t + {{- else if eq (toString $type) "uint32" -}}uint32_t + {{- else if eq (toString $type) "uint64" -}}uint64_t + {{- else if eq (toString $type) "int" -}}int + {{- else if eq (toString $type) "int8" -}}int8_t + {{- else if eq (toString $type) "int16" -}}int16_t + {{- else if eq (toString $type) "int32" -}}int32_t + {{- else if eq (toString $type) "int64" -}}int64_t + {{- else if eq (toString $type) "float32" -}}float + {{- else if eq (toString $type) "float64" -}}double + {{- else if eq (toString $type) "string" -}}char * + {{- else if eq (toString $type) "timestamp" -}}{{ printf "%s_timestamp" $prefix }} + {{- else if eq (toString $type) "bigint" -}}{{ printf "%s_bigint" $prefix }} + {{- else if eq (toString $type) "any" -}}char * + {{- else if eq (toString $type) "null" -}}bool + {{- else -}}void * + {{- end -}} +{{- else if isEnumType $type -}} +{{ template "cTypeName" dict "Prefix" $prefix "Name" (toString $type) }} +{{- else if and (hasField $type "Alias") $type.Alias -}} +{{ template "type" dict "Type" $type.Alias.Type.Type "Prefix" $prefix "Types" $types }} +{{- else if isString $type -}} + {{- $resolved := dict "matched" false -}} + {{- range $_, $schemaType := $types -}} + {{- if eq (toString $schemaType.Name) (toString $type) -}} + {{- $_ := set $resolved "matched" true -}} + {{- if isEnumType $schemaType -}} +{{ template "cTypeName" dict "Prefix" $prefix "Name" (toString $type) }} + {{- else if and (hasField $schemaType "Alias") $schemaType.Alias -}} +{{ template "type" dict "Type" $schemaType.Alias.Type.Type "Prefix" $prefix "Types" $types }} + {{- else -}} +{{ template "cTypeName" dict "Prefix" $prefix "Name" (toString $type) }} * + {{- end -}} + {{- end -}} + {{- end -}} + {{- if not (get $resolved "matched") -}} +{{ template "cTypeName" dict "Prefix" $prefix "Name" (toString $type) }} * + {{- end -}} +{{- else -}} +{{ template "cTypeName" dict "Prefix" $prefix "Name" (toString $type) }} * +{{- end -}} +{{- end -}} diff --git a/types.go.tmpl b/types.go.tmpl new file mode 100644 index 0000000..fbae0a2 --- /dev/null +++ b/types.go.tmpl @@ -0,0 +1,223 @@ +{{- define "types" -}} +{{- $prefix := .Prefix -}} +{{- $types := .Types -}} +{{- $services := .Services -}} + +{{- range $_, $type := $types }} +{{- if isStructType $type }} +typedef struct {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }} {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }}; +static inline void {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }}_init({{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }} *value); +static inline void {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }}_free({{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }} *value); + +{{- end }} +{{- end }} + +{{- range $_, $type := $types }} +{{- if isEnumType $type }} +typedef enum {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }} { + {{ printf "%s_%s_UNKNOWN" (toUpper $prefix) (toUpper (snakeCase $type.Name)) }} = 0, +{{- range $_, $field := $type.Fields }} + {{ template "cEnumValue" dict "Prefix" $prefix "TypeName" $type.Name "FieldName" $field.Name }}, +{{- end }} +} {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }}; + +static inline const char *{{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }}_to_string({{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }} value) { + switch (value) { + case {{ printf "%s_%s_UNKNOWN" (toUpper $prefix) (toUpper (snakeCase $type.Name)) }}: return "UNKNOWN"; +{{- range $_, $field := $type.Fields }} + case {{ template "cEnumValue" dict "Prefix" $prefix "TypeName" $type.Name "FieldName" $field.Name }}: return "{{$field.Name}}"; +{{- end }} + default: return "UNKNOWN"; + } +} + +static inline int {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }}_from_string(const char *value, {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }} *out) { + if (!out || !value) return -1; +{{- range $_, $field := $type.Fields }} + if (strcmp(value, "{{$field.Name}}") == 0) { + *out = {{ template "cEnumValue" dict "Prefix" $prefix "TypeName" $type.Name "FieldName" $field.Name }}; + return 0; + } +{{- end }} + *out = {{ printf "%s_%s_UNKNOWN" (toUpper $prefix) (toUpper (snakeCase $type.Name)) }}; + return -1; +} + +{{- end }} +{{- end }} + +{{- range $_, $type := $types }} +{{- if isStructType $type }} +struct {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }} { +{{- range $_, $field := $type.Fields }} + {{- $json := dict "name" $field.Name "ignored" false "explicit" false -}} + {{- range $_, $meta := $field.Meta }} + {{- if index $meta "json" -}} + {{- $value := index $meta "json" -}} + {{- if eq $value "-" -}} + {{- $_ := set $json "ignored" true -}} + {{- else -}} + {{- $_ := set $json "name" $value -}} + {{- $_ := set $json "explicit" true -}} + {{- end -}} + {{- end -}} + {{- if and (not (get $json "ignored")) (not (get $json "explicit")) (index $meta "go.tag.json") -}} + {{- $tagName := first (split "," (index $meta "go.tag.json")) -}} + {{- if eq $tagName "-" -}} + {{- $_ := set $json "ignored" true -}} + {{- else if $tagName -}} + {{- $_ := set $json "name" $tagName -}} + {{- end -}} + {{- end -}} + {{- end }} + {{- if not (get $json "ignored") }} + {{- $fieldName := (snakeCase $field.Name) -}} + {{- if $field.Optional }} + bool has_{{$fieldName}}; + {{- end }} + {{ template "type" dict "Type" $field.Type "Prefix" $prefix "Types" $types }} {{$fieldName}}; + {{- end }} +{{- end }} +}; + +static inline void {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }}_init({{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }} *value) { + if (!value) return; + memset(value, 0, sizeof(*value)); +} + +static inline void {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }}_free({{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }} *value) { + if (!value) return; +{{- range $_, $field := $type.Fields }} + {{- $json := dict "ignored" false -}} + {{- range $_, $meta := $field.Meta }} + {{- if index $meta "json" -}} + {{- if eq (index $meta "json") "-" -}} + {{- $_ := set $json "ignored" true -}} + {{- else -}} + {{- $_ := set $json "ignored" false -}} + {{- end -}} + {{- end -}} + {{- if and (not (get $json "ignored")) (index $meta "go.tag.json") -}} + {{- if eq (first (split "," (index $meta "go.tag.json"))) "-" -}} + {{- $_ := set $json "ignored" true -}} + {{- end -}} + {{- end -}} + {{- end }} + {{- if not (get $json "ignored") }} +{{ template "freeField" dict "FieldExpr" (printf "value->%s" (snakeCase $field.Name)) "Type" $field.Type "Prefix" $prefix }} + {{- end }} +{{- end }} + memset(value, 0, sizeof(*value)); +} + +{{- end }} +{{- end }} + +{{- range $_, $service := $services }} +{{- range $_, $method := $service.Methods }} +typedef struct {{ template "cMethodRequestTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }} { +{{- range $_, $input := $method.Inputs }} + {{- $fieldName := (snakeCase $input.Name) -}} + {{- if $input.Optional }} + bool has_{{$fieldName}}; + {{- end }} + {{ template "type" dict "Type" $input.Type "Prefix" $prefix "Types" $types }} {{$fieldName}}; +{{- end }} +} {{ template "cMethodRequestTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }}; + +static inline void {{ template "cMethodRequestTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }}_init({{ template "cMethodRequestTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }} *value) { + if (!value) return; + memset(value, 0, sizeof(*value)); +} + +static inline void {{ template "cMethodRequestTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }}_free({{ template "cMethodRequestTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }} *value) { + if (!value) return; +{{- range $_, $input := $method.Inputs }} +{{ template "freeField" dict "FieldExpr" (printf "value->%s" (snakeCase $input.Name)) "Type" $input.Type "Prefix" $prefix }} +{{- end }} + memset(value, 0, sizeof(*value)); +} + +typedef struct {{ template "cMethodResponseTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }} { +{{- range $_, $output := $method.Outputs }} + {{- $fieldName := (snakeCase $output.Name) -}} + {{- if $output.Optional }} + bool has_{{$fieldName}}; + {{- end }} + {{ template "type" dict "Type" $output.Type "Prefix" $prefix "Types" $types }} {{$fieldName}}; +{{- end }} +} {{ template "cMethodResponseTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }}; + +static inline void {{ template "cMethodResponseTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }}_init({{ template "cMethodResponseTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }} *value) { + if (!value) return; + memset(value, 0, sizeof(*value)); +} + +static inline void {{ template "cMethodResponseTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }}_free({{ template "cMethodResponseTypeName" dict "Prefix" $prefix "ServiceName" $service.Name "MethodName" $method.Name }} *value) { + if (!value) return; +{{- range $_, $output := $method.Outputs }} +{{ template "freeField" dict "FieldExpr" (printf "value->%s" (snakeCase $output.Name)) "Type" $output.Type "Prefix" $prefix }} +{{- end }} + memset(value, 0, sizeof(*value)); +} + +{{- end }} +{{- end }} +{{- end -}} + +{{- define "freeField" -}} +{{- $expr := .FieldExpr -}} +{{- $type := .Type -}} +{{- $prefix := .Prefix -}} +{{- $depth := 0 -}} +{{- if exists . "Depth" -}} +{{- $depth = get . "Depth" -}} +{{- end -}} +{{- if isMapType $type }} + if ({{$expr}}.keys || {{$expr}}.values) { + for (size_t {{ printf "free_map_idx_%d" $depth }} = 0; {{ printf "free_map_idx_%d" $depth }} < {{$expr}}.count; ++{{ printf "free_map_idx_%d" $depth }}) { +{{ template "freeField" dict "FieldExpr" (printf "%s.keys[%s]" $expr (printf "free_map_idx_%d" $depth)) "Type" (mapKeyType $type) "Prefix" $prefix "Depth" (add $depth 1) }} +{{ template "freeField" dict "FieldExpr" (printf "%s.values[%s]" $expr (printf "free_map_idx_%d" $depth)) "Type" (mapValueType $type) "Prefix" $prefix "Depth" (add $depth 1) }} + } + free({{$expr}}.keys); + free({{$expr}}.values); + {{$expr}}.keys = NULL; + {{$expr}}.values = NULL; + {{$expr}}.count = 0; + } +{{- else if isListType $type }} + {{- $elem := listElemType $type -}} + {{- if eq (toString $elem) "byte" }} + free({{$expr}}.data); + {{$expr}}.data = NULL; + {{$expr}}.len = 0; + {{- else }} + if ({{$expr}}.items) { + for (size_t {{ printf "free_list_idx_%d" $depth }} = 0; {{ printf "free_list_idx_%d" $depth }} < {{$expr}}.count; ++{{ printf "free_list_idx_%d" $depth }}) { +{{ template "freeField" dict "FieldExpr" (printf "%s.items[%s]" $expr (printf "free_list_idx_%d" $depth)) "Type" $elem "Prefix" $prefix "Depth" (add $depth 1) }} + } + free({{$expr}}.items); + {{$expr}}.items = NULL; + {{$expr}}.count = 0; + } + {{- end }} +{{- else if isCoreType $type }} + {{- if or (eq (toString $type) "string") (eq (toString $type) "any") }} + free({{$expr}}); + {{$expr}} = NULL; + {{- else if eq (toString $type) "timestamp" }} + {{ printf "%s_timestamp_free" $prefix }}(&{{$expr}}); + {{- else if eq (toString $type) "bigint" }} + {{ printf "%s_bigint_free" $prefix }}(&{{$expr}}); + {{- end }} +{{- else if isEnumType $type }} +{{- else if and (hasField $type "Alias") $type.Alias }} +{{ template "freeField" dict "FieldExpr" $expr "Type" $type.Alias.Type.Type "Prefix" $prefix }} +{{- else if isStructType $type }} + if ({{$expr}}) { + {{ template "cTypeName" dict "Prefix" $prefix "Name" (toString $type) }}_free({{$expr}}); + free({{$expr}}); + {{$expr}} = NULL; + } +{{- end }} +{{- end -}}