From 91b0f50b74e6c5489c64f9f7441df919355d73ac Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Thu, 9 Apr 2026 16:06:08 +0300 Subject: [PATCH 1/5] Initial implementation --- CJSON_TRANSITION_PLAN.md | 59 ++++ README.md | 138 ++++++++++ REFACTOR_NOTES.md | 23 ++ cTypeName.go.tmpl | 31 +++ client.go.tmpl | 40 +++ embed.go | 6 + generator_test.go | 329 ++++++++++++++++++++++ go.mod | 3 + help.go.tmpl | 9 + impl.go.tmpl | 25 ++ implClient.go.tmpl | 149 ++++++++++ implCodecReachability.go.tmpl | 42 +++ implJSONCodec.go.tmpl | 398 +++++++++++++++++++++++++++ implMethodJSON.go.tmpl | 242 ++++++++++++++++ implPreamble.go.tmpl | 122 +++++++++ implStructJSON.go.tmpl | 133 +++++++++ implTransport.go.tmpl | 371 +++++++++++++++++++++++++ interop_test.go | 500 ++++++++++++++++++++++++++++++++++ main.go.tmpl | 264 ++++++++++++++++++ testdata/codec.ridl | 19 ++ testdata/smoke.ridl | 19 ++ testdata/succinct.ridl | 16 ++ type.go.tmpl | 75 +++++ types.go.tmpl | 223 +++++++++++++++ 24 files changed, 3236 insertions(+) create mode 100644 CJSON_TRANSITION_PLAN.md create mode 100644 REFACTOR_NOTES.md create mode 100644 cTypeName.go.tmpl create mode 100644 client.go.tmpl create mode 100644 embed.go create mode 100644 generator_test.go create mode 100644 go.mod create mode 100644 help.go.tmpl create mode 100644 impl.go.tmpl create mode 100644 implClient.go.tmpl create mode 100644 implCodecReachability.go.tmpl create mode 100644 implJSONCodec.go.tmpl create mode 100644 implMethodJSON.go.tmpl create mode 100644 implPreamble.go.tmpl create mode 100644 implStructJSON.go.tmpl create mode 100644 implTransport.go.tmpl create mode 100644 interop_test.go create mode 100644 main.go.tmpl create mode 100644 testdata/codec.ridl create mode 100644 testdata/smoke.ridl create mode 100644 testdata/succinct.ridl create mode 100644 type.go.tmpl create mode 100644 types.go.tmpl diff --git a/CJSON_TRANSITION_PLAN.md b/CJSON_TRANSITION_PLAN.md new file mode 100644 index 0000000..f4ae595 --- /dev/null +++ b/CJSON_TRANSITION_PLAN.md @@ -0,0 +1,59 @@ +# cJSON Transition Plan + +## Goal + +Restore `cJSON` as the only JSON backend for `gen-c` and downstream `c-sdk`, +while keeping the recent refactor, correctness fixes, and implementation-size +improvements. + +## Plan + +1. Keep all recent refactor and quality improvements in `gen-c`. + Preserve: + - split template structure + - required `null` / `any` decode fix + - one-time curl init cleanup + - prefix-aware header guards + - reachability-based codec pruning + - shallow method `prepare` / `parse` dedup + +2. Replace only the JSON backend layer in `gen-c`. + Move from direct `json-c` usage back to direct `cJSON` usage. + Do not add a compatibility shim. + Keep `bigint` encoded/decoded as JSON string. + +3. Make removal of `json-c` a hard constraint. + After this work, there should be: + - no `json-c` dependency in `gen-c` + - no `json-c` dependency in `c-sdk` + - no generated code that references `json-c` + - no leftover `json-c`-specific helpers, includes, docs, or build flags + +4. Regenerate WAAS from the updated `gen-c`. + Produce fresh `waas.gen.h` and `waas.gen.c`. + Keep the benefits of the refactor and size optimizations. + Reapply only the temporary downstream missing-`iss` tolerance patch if still + needed. + +5. Move `c-sdk` back to `cJSON` without losing the other integration work. + Revert build/docs/formula/dependency changes from `json-c` to `cJSON`. + Keep the updated WAAS generated client structure and all non-JSON-related + improvements intact. + +6. Validate both repos end to end. + `gen-c`: + - `go test ./...` + - regenerate WAAS successfully + - syntax-check generated C + `c-sdk`: + - configure + - build + - `ctest` + +## Acceptance Criteria + +- RTOS-friendly dependency story is restored with `cJSON` +- big numbers remain string-based +- all recent refactor / correctness / size wins remain in place +- no `json-c` artifacts remain in generator, generated code, or `c-sdk` + integration diff --git a/README.md b/README.md index e69de29..c88b88f 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,138 @@ +# 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`. + +## 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. +- The generated implementation is tested with smoke, codec, succinct, and reference interop coverage in this repo. diff --git a/REFACTOR_NOTES.md b/REFACTOR_NOTES.md new file mode 100644 index 0000000..1475264 --- /dev/null +++ b/REFACTOR_NOTES.md @@ -0,0 +1,23 @@ +# gen-c Refactor Notes + +Current focus: split `impl.go.tmpl` into smaller template units without changing generated output. + +Checklist: +- [x] Identify high-value seams in `impl.go.tmpl` +- [x] Extract runtime/JSON preamble into `implPreamble.go.tmpl` +- [x] Extract transport/error helpers into `implTransport.go.tmpl` +- [x] Extract struct JSON generation into `implStructJSON.go.tmpl` +- [x] Extract method JSON generation into `implMethodJSON.go.tmpl` +- [x] Extract client implementation into `implClient.go.tmpl` +- [x] Extract recursive JSON codec templates into `implJSONCodec.go.tmpl` +- [x] Reduce `impl.go.tmpl` to orchestration only +- [x] Review diff for behavior-preserving refactor only + +Validation done: +- `go test ./...` +- generated WAAS impl with `webrpc-gen v0.36.0` and confirmed private struct codecs are pruned to reachable encode/decode paths +- syntax-checked generated WAAS impl with `cc -std=c99 -Wall -Wextra -fsyntax-only $(pkg-config --cflags libcjson || pkg-config --cflags cjson)` + +Still worth doing later: +- add a template parsing/generation smoke test so refactors validate generated output, not just Go package compilation +- add `_examples` and a small interoperability check 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 -}} From ae613be5b9735d960bf4da2b84518a143045853a Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Thu, 9 Apr 2026 16:07:42 +0300 Subject: [PATCH 2/5] Remove temporary planning docs --- CJSON_TRANSITION_PLAN.md | 59 ---------------------------------------- REFACTOR_NOTES.md | 23 ---------------- 2 files changed, 82 deletions(-) delete mode 100644 CJSON_TRANSITION_PLAN.md delete mode 100644 REFACTOR_NOTES.md diff --git a/CJSON_TRANSITION_PLAN.md b/CJSON_TRANSITION_PLAN.md deleted file mode 100644 index f4ae595..0000000 --- a/CJSON_TRANSITION_PLAN.md +++ /dev/null @@ -1,59 +0,0 @@ -# cJSON Transition Plan - -## Goal - -Restore `cJSON` as the only JSON backend for `gen-c` and downstream `c-sdk`, -while keeping the recent refactor, correctness fixes, and implementation-size -improvements. - -## Plan - -1. Keep all recent refactor and quality improvements in `gen-c`. - Preserve: - - split template structure - - required `null` / `any` decode fix - - one-time curl init cleanup - - prefix-aware header guards - - reachability-based codec pruning - - shallow method `prepare` / `parse` dedup - -2. Replace only the JSON backend layer in `gen-c`. - Move from direct `json-c` usage back to direct `cJSON` usage. - Do not add a compatibility shim. - Keep `bigint` encoded/decoded as JSON string. - -3. Make removal of `json-c` a hard constraint. - After this work, there should be: - - no `json-c` dependency in `gen-c` - - no `json-c` dependency in `c-sdk` - - no generated code that references `json-c` - - no leftover `json-c`-specific helpers, includes, docs, or build flags - -4. Regenerate WAAS from the updated `gen-c`. - Produce fresh `waas.gen.h` and `waas.gen.c`. - Keep the benefits of the refactor and size optimizations. - Reapply only the temporary downstream missing-`iss` tolerance patch if still - needed. - -5. Move `c-sdk` back to `cJSON` without losing the other integration work. - Revert build/docs/formula/dependency changes from `json-c` to `cJSON`. - Keep the updated WAAS generated client structure and all non-JSON-related - improvements intact. - -6. Validate both repos end to end. - `gen-c`: - - `go test ./...` - - regenerate WAAS successfully - - syntax-check generated C - `c-sdk`: - - configure - - build - - `ctest` - -## Acceptance Criteria - -- RTOS-friendly dependency story is restored with `cJSON` -- big numbers remain string-based -- all recent refactor / correctness / size wins remain in place -- no `json-c` artifacts remain in generator, generated code, or `c-sdk` - integration diff --git a/REFACTOR_NOTES.md b/REFACTOR_NOTES.md deleted file mode 100644 index 1475264..0000000 --- a/REFACTOR_NOTES.md +++ /dev/null @@ -1,23 +0,0 @@ -# gen-c Refactor Notes - -Current focus: split `impl.go.tmpl` into smaller template units without changing generated output. - -Checklist: -- [x] Identify high-value seams in `impl.go.tmpl` -- [x] Extract runtime/JSON preamble into `implPreamble.go.tmpl` -- [x] Extract transport/error helpers into `implTransport.go.tmpl` -- [x] Extract struct JSON generation into `implStructJSON.go.tmpl` -- [x] Extract method JSON generation into `implMethodJSON.go.tmpl` -- [x] Extract client implementation into `implClient.go.tmpl` -- [x] Extract recursive JSON codec templates into `implJSONCodec.go.tmpl` -- [x] Reduce `impl.go.tmpl` to orchestration only -- [x] Review diff for behavior-preserving refactor only - -Validation done: -- `go test ./...` -- generated WAAS impl with `webrpc-gen v0.36.0` and confirmed private struct codecs are pruned to reachable encode/decode paths -- syntax-checked generated WAAS impl with `cc -std=c99 -Wall -Wextra -fsyntax-only $(pkg-config --cflags libcjson || pkg-config --cflags cjson)` - -Still worth doing later: -- add a template parsing/generation smoke test so refactors validate generated output, not just Go package compilation -- add `_examples` and a small interoperability check From 4d1a1202f86fc61f0b49f33d1b66dbf5fb4fe3be Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Thu, 9 Apr 2026 16:11:46 +0300 Subject: [PATCH 3/5] Add CI workflows --- .github/workflows/ci.yml | 40 ++ _examples/Makefile | 12 + _examples/smoke/example.gen.c | 1072 +++++++++++++++++++++++++++++++++ _examples/smoke/example.gen.h | 374 ++++++++++++ _examples/smoke/example.ridl | 19 + 5 files changed, 1517 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 _examples/Makefile create mode 100644 _examples/smoke/example.gen.c create mode 100644 _examples/smoke/example.gen.h create mode 100644 _examples/smoke/example.ridl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..491d4cd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + pull_request: + +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/_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) From 9436be1f9dd867129193cbfb3f11bc5b046f8a46 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Thu, 9 Apr 2026 16:25:04 +0300 Subject: [PATCH 4/5] Document cJSON integer precision limits --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index c88b88f..d87336f 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,11 @@ Dependency names can vary slightly by platform or package manager. The important 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: @@ -135,4 +140,5 @@ Change any of the following values by passing `-option="Value"` CLI flag to `web - `-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. From 367d56b599839fa4e5589623f9658b300b3f4924 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Thu, 9 Apr 2026 16:26:13 +0300 Subject: [PATCH 5/5] Avoid duplicate CI runs --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 491d4cd..0c4c257 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,11 @@ name: CI on: push: + branches: + - master pull_request: + branches: + - "**" jobs: examples: