From 2229cf072bf01cd3d380203cde878bd59a87ddd0 Mon Sep 17 00:00:00 2001 From: yukkop Date: Sat, 19 Apr 2025 20:02:10 +0000 Subject: [PATCH] feat: `hel`: render without execute --- flake.nix | 12 +- package/c/hectic/hectic.h | 4 +- package/c/hectic/test/06-templater.c | 2 +- package/c/hel/Makefile | 13 + package/c/hel/hel--0.1.sql | 26 ++ package/c/hel/hel.c | 307 ++++++++++++++++++ .../postgreact.control => hel/hel.control} | 2 +- package/c/{postgreact => hel}/make.sh | 6 +- package/c/hel/test/01-test.c | 0 package/c/postgreact/Makefile | 10 - package/c/postgreact/default.nix | 41 --- package/c/postgreact/postgreact--0.1.sql | 8 - package/c/postgreact/postgreact.c | 16 - 13 files changed, 359 insertions(+), 88 deletions(-) create mode 100644 package/c/hel/Makefile create mode 100755 package/c/hel/hel--0.1.sql create mode 100755 package/c/hel/hel.c rename package/c/{postgreact/postgreact.control => hel/hel.control} (57%) rename package/c/{postgreact => hel}/make.sh (93%) create mode 100755 package/c/hel/test/01-test.c delete mode 100644 package/c/postgreact/Makefile delete mode 100755 package/c/postgreact/default.nix delete mode 100755 package/c/postgreact/postgreact--0.1.sql delete mode 100755 package/c/postgreact/postgreact.c diff --git a/flake.nix b/flake.nix index f33b7ed..a980e2b 100644 --- a/flake.nix +++ b/flake.nix @@ -437,8 +437,8 @@ } { pname = "postgrect"; version = "0.1"; - src = ./package/c/postgreact; - nativeBuildInputs = with prev; [pkg-config]; + src = ./package/c/hel; + nativeBuildInputs = (with prev; [pkg-config]) ++ [ self.packages.${prev.system}.c-hectic ]; preInstall = ''mkdir $out''; }; in { @@ -447,25 +447,25 @@ http = buildHttpExt "17"; pg_smtp_client = buildSmtpExt "17"; plhaskell = buildPlHaskellExt "17"; - postgreact = buildHelloExt "17"; + hel = buildHelloExt "17"; };}; postgresql_16 = prev.postgresql_16 // {pkgs = prev.postgresql_16.pkgs // { http = buildHttpExt "16"; pg_smtp_client = buildSmtpExt "16"; plhaskell = buildPlHaskellExt "16"; - postgreact = buildHelloExt "16"; + hel = buildHelloExt "16"; };}; postgresql_15 = prev.postgresql_15 // {pkgs = prev.postgresql_15.pkgs // { http = buildHttpExt "15"; pg_smtp_client = buildSmtpExt "15"; plhaskell = buildPlHaskellExt "15"; - postgreact = buildHelloExt "15"; + hel = buildHelloExt "15"; };}; postgresql_14 = prev.postgresql_14 // {pkgs = prev.postgresql_14.pkgs // { http = buildHttpExt "14"; pg_smtp_client = buildSmtpExt "14"; plhaskell = buildPlHaskellExt "14"; - postgreact = buildHelloExt "14"; + hel = buildHelloExt "14"; };}; writers = let writeC = diff --git a/package/c/hectic/hectic.h b/package/c/hectic/hectic.h index a05aada..3e02d83 100644 --- a/package/c/hectic/hectic.h +++ b/package/c/hectic/hectic.h @@ -603,11 +603,11 @@ struct Json { RESULT(Json, Json); -#define json_parse(arena, s) json_parse__(__FILE__, __func__, __LINE__, arena, s) Json *json_parse__(const char* file, const char* func, int line, Arena *arena, const char **s); +#define json_parse(arena, s) json_parse__(__FILE__, __func__, __LINE__, arena, s) -#define json_to_string(arena, item) json_to_string__(__FILE__, __func__, __LINE__, arena, item) char *json_to_string__(const char* file, const char* func, int line, Arena *arena, const Json * const item); +#define json_to_string(arena, item) json_to_string__(__FILE__, __func__, __LINE__, arena, item) #define json_to_string_with_opts(arena, item, raw) json_to_string_with_opts__(__FILE__, __func__, __LINE__, arena, item, raw) char *json_to_string_with_opts__(const char* file, const char* func, int line, Arena *arena, const Json * const item, JsonRawOpt raw); diff --git a/package/c/hectic/test/06-templater.c b/package/c/hectic/test/06-templater.c index 37e9a2c..56e2c51 100755 --- a/package/c/hectic/test/06-templater.c +++ b/package/c/hectic/test/06-templater.c @@ -43,7 +43,7 @@ " struct TemplateSectionValue section = {\n" \ " iterator = %p \"item\",\n" \ " collection = %p \"items\",\n" \ - " body = {\n" \ + " struct TemplateNode body = {\n" \ " enum type = TEXT 0 ,\n" \ " union TemplateValue value = {\n" \ " struct TemplateTextValue text = {\n" \ diff --git a/package/c/hel/Makefile b/package/c/hel/Makefile new file mode 100644 index 0000000..6b5b62f --- /dev/null +++ b/package/c/hel/Makefile @@ -0,0 +1,13 @@ +MODULE_big = hel +OBJS = hel.o +EXTENSION = hel +DATA = hel--0.1.sql + +PG_CONFIG = pg_config + +PGXS := $(shell $(PG_CONFIG) --pgxs) + +# Add hectic library path +CFLAGS += -I -lhectic + +include $(PGXS) \ No newline at end of file diff --git a/package/c/hel/hel--0.1.sql b/package/c/hel/hel--0.1.sql new file mode 100755 index 0000000..6a4d958 --- /dev/null +++ b/package/c/hel/hel--0.1.sql @@ -0,0 +1,26 @@ +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION hel" to load this file. \quit + +CREATE SCHEMA hel; + +-- Define the parse_text_with_hectic function that uses hectic library +-- Expected usage: +-- ```sql +-- SELECT "hel"."render"( +-- "declare" := +-- jsonb_build_object( +-- 'name', 'test', +-- 'config', jsonb_build_object( +-- 'debug', true, +-- 'limit', 100 +-- ) +-- ), +-- "template" := $hel$ +-- {{ name }} {{ config.limit }} +-- $hel$ +-- ); +-- ``` +CREATE FUNCTION "hel"."render"("declare" json, "template" text) +RETURNS text +AS 'hel', 'render' +LANGUAGE C STRICT; \ No newline at end of file diff --git a/package/c/hel/hel.c b/package/c/hel/hel.c new file mode 100755 index 0000000..027012a --- /dev/null +++ b/package/c/hel/hel.c @@ -0,0 +1,307 @@ +#include "postgres.h" +#include "fmgr.h" +#include "utils/builtins.h" +#include "utils/json.h" +#include "hectic.h" +#include + +#ifdef PG_MODULE_MAGIC +PG_MODULE_MAGIC; +#endif + +/* Helper function to get a JSON value by key path */ +static Json *json_get_by_path(Arena *arena, const Json *context, const char *key_path) { + if (!context || !key_path || !*key_path) { + return NULL; + } + + char *path_copy = arena_strdup(arena, key_path); + char *token = strtok(path_copy, "."); + Json *current = (Json*)context; + + while (token && current) { + current = json_get_object_item(current, token); + token = strtok(NULL, "."); + } + + return current; +} + +/* Convert JSON value to string */ +static char *json_value_to_string(Arena *arena, const Json *json) { + if (!json) { + return ""; + } + + switch (json->type) { + case JSON_STRING: + return json->value.string; + case JSON_NUMBER: { + char *buf = arena_alloc(arena, 64); + snprintf(buf, 64, "%.6g", json->value.number); + return buf; + } + case JSON_BOOL: + return json->value.boolean ? "true" : "false"; + case JSON_NULL: + return ""; + case JSON_ARRAY: + case JSON_OBJECT: + return json_to_string(arena, json); + default: + return ""; + } +} + +/* Forward declaration for recursive function */ +static char *render_template_node(Arena *arena, const TemplateNode *node, const Json *context); + +/* Render a text node */ +static char *render_text_node(Arena *arena, const TemplateNode *node) { + if (!node || node->type != TEMPLATE_NODE_TEXT) { + return ""; + } + + return node->value.text.content; +} + +/* Render an interpolation node */ +static char *render_interpolation_node(Arena *arena, const TemplateNode *node, const Json *context) { + if (!node || node->type != TEMPLATE_NODE_INTERPOLATE || !context) { + return ""; + } + + const char *key = node->value.interpolate.key; + Json *value = json_get_by_path(arena, context, key); + + if (!value) { + return ""; + } + + return json_value_to_string(arena, value); +} + +/* Render a section node (for loop) */ +static char *render_section_node(Arena *arena, const TemplateNode *node, const Json *context) { + if (!node || node->type != TEMPLATE_NODE_SECTION || !context) { + return ""; + } + + const char *collection_key = node->value.section.collection; + const char *iterator_name = node->value.section.iterator; + TemplateNode *body = node->value.section.body; + + Json *collection = json_get_by_path(arena, context, collection_key); + + if (!collection || collection->type != JSON_ARRAY) { + return ""; + } + + size_t buffer_size = 1024; + char *buffer = arena_alloc(arena, buffer_size); + size_t buffer_pos = 0; + + Json *item = collection->value.child; + while (item) { + const char *empty_json = "{}"; + Json *iter_context = json_parse(arena, &empty_json); + if (!iter_context) { + return ""; + } + + Json *item_json = arena_alloc(arena, sizeof(Json)); + memcpy(item_json, item, sizeof(Json)); + item_json->key = arena_strdup(arena, iterator_name); + item_json->next = NULL; + + char *rendered_body = render_template_node(arena, body, iter_context); + + size_t rendered_len = strlen(rendered_body); + if (buffer_pos + rendered_len + 1 > buffer_size) { + buffer_size = (buffer_pos + rendered_len + 1) * 2; + buffer = arena_realloc(arena, buffer, buffer_size / 2, buffer_size); + } + + strcpy(buffer + buffer_pos, rendered_body); + buffer_pos += rendered_len; + + item = item->next; + } + + buffer[buffer_pos] = '\0'; + return buffer; +} + +/* Render an include node */ +static char *render_include_node(Arena *arena, const TemplateNode *node, const Json *context) { + if (!node || node->type != TEMPLATE_NODE_INCLUDE || !context) { + return ""; + } + + const char *include_key = node->value.include.key; + Json *include_value = json_get_by_path(arena, context, include_key); + + if (!include_value || include_value->type != JSON_ARRAY) { + return ""; + } + + char *buffer = arena_alloc(arena, 1024); + size_t buffer_pos = 0; + + Json *include_item = include_value->value.child; + while (include_item) { + if (include_item->type == JSON_OBJECT) { + Json *template_json = json_get_object_item(include_item, "template"); + Json *content_json = json_get_object_item(include_item, "content"); + Json *context_json = json_get_object_item(include_item, "context"); + + if (template_json && template_json->type == JSON_STRING) { + const char *template_str = template_json->value.string; + const Json *include_context = context_json ? context_json : context; + + TemplateConfig config = template_default_config(); + TemplateResult template_result = template_parse(arena, &template_str, &config); + + if (!IS_RESULT_ERROR(template_result)) { + TemplateNode template_node = RESULT_SOME_VALUE(template_result); + + char *rendered = render_template_node(arena, &template_node, include_context); + + buffer_pos += sprintf(buffer + buffer_pos, "%s", rendered); + } + } else if (content_json && content_json->type == JSON_STRING) { + buffer_pos += sprintf(buffer + buffer_pos, "%s", content_json->value.string); + } + } + + include_item = include_item->next; + } + + buffer[buffer_pos] = '\0'; + return buffer; +} + +/* Render a template node tree recursively */ +static char *render_template_node(Arena *arena, const TemplateNode *node, const Json *context) { + if (!node) { + return ""; + } + + size_t buffer_size = 4096; + char *output = arena_alloc(arena, buffer_size); + size_t output_pos = 0; + + const TemplateNode *current = node; + while (current) { + char *rendered = NULL; + + switch (current->type) { + case TEMPLATE_NODE_TEXT: + rendered = render_text_node(arena, current); + break; + + case TEMPLATE_NODE_INTERPOLATE: + rendered = render_interpolation_node(arena, current, context); + break; + + case TEMPLATE_NODE_SECTION: + rendered = render_section_node(arena, current, context); + break; + + case TEMPLATE_NODE_INCLUDE: + rendered = render_include_node(arena, current, context); + break; + + case TEMPLATE_NODE_EXECUTE: + todo; + rendered = ""; + break; + + default: + rendered = ""; + break; + } + + size_t rendered_len = strlen(rendered); + if (output_pos + rendered_len + 1 > buffer_size) { + buffer_size = (output_pos + rendered_len + 1) * 2; + output = arena_realloc(arena, output, buffer_size / 2, buffer_size); + } + + strcpy(output + output_pos, rendered); + output_pos += rendered_len; + + if (current->children) { + char *children_rendered = render_template_node(arena, current->children, context); + size_t children_len = strlen(children_rendered); + + if (output_pos + children_len + 1 > buffer_size) { + buffer_size = (output_pos + children_len + 1) * 2; + output = arena_realloc(arena, output, buffer_size / 2, buffer_size); + } + + strcpy(output + output_pos, children_rendered); + output_pos += children_len; + } + + current = current->next; + } + + output[output_pos] = '\0'; + return output; +} + +/* Define the function render */ +PG_FUNCTION_INFO_V1(render); + +/* + * Function to render templates using hectic library with JSON context + * Arguments: + * 1. declare - JSON context for rendering + * 2. template - The template text to render + */ +Datum render(PG_FUNCTION_ARGS) +{ + text *context_text = PG_GETARG_TEXT_PP(0); + text *template_text = PG_GETARG_TEXT_PP(1); + + /* Convert input text to C string */ + char *template_str = text_to_cstring(template_text); + char *context_str = text_to_cstring(context_text); + + /* Initialize arena for memory management */ + Arena arena = arena_init(MEM_MiB); + + /* Parse the JSON context */ + const char *json_ptr = context_str; + Json *context = json_parse(&arena, &json_ptr); + + if (!context) { + arena_free(&arena); + ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("Invalid JSON context"))); + } + + /* Parse the template text */ + const char *template_ptr = template_str; + TemplateConfig config = template_default_config(); + TemplateResult template_result = template_parse(&arena, &template_ptr, &config); + + if (IS_RESULT_ERROR(template_result)) { + arena_free(&arena); + ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("Failed to parse template: %s", + RESULT_ERROR_MESSAGE(template_result)))); + } + + /* Render the template */ + TemplateNode root_node = RESULT_SOME_VALUE(template_result); + char *result_str = render_template_node(&arena, &root_node, context); + + /* Prepare return value */ + text *result = cstring_to_text(result_str); + + arena_free(&arena); + + PG_RETURN_TEXT_P(result); +} \ No newline at end of file diff --git a/package/c/postgreact/postgreact.control b/package/c/hel/hel.control similarity index 57% rename from package/c/postgreact/postgreact.control rename to package/c/hel/hel.control index aab9bfd..fae58b8 100755 --- a/package/c/postgreact/postgreact.control +++ b/package/c/hel/hel.control @@ -1,3 +1,3 @@ comment = 'My first extension' default_version = '0.1' -module_pathname = '$libdir/postgreact' \ No newline at end of file +module_pathname = '$libdir/hel' \ No newline at end of file diff --git a/package/c/postgreact/make.sh b/package/c/hel/make.sh similarity index 93% rename from package/c/postgreact/make.sh rename to package/c/hel/make.sh index 3f8e91c..ade14d9 100755 --- a/package/c/postgreact/make.sh +++ b/package/c/hel/make.sh @@ -87,11 +87,11 @@ case "$MODE" in mkdir -p target echo "# Building PostgreSQL extension" # shellcheck disable=SC2086 - gcc $CFLAGS $OPTFLAGS -I$PG_INCLUDE -shared -o target/postgreact.so postgreact.c + gcc $CFLAGS $OPTFLAGS -I$PG_INCLUDE -shared -o target/hel.so hel.c # Copy extension files to target directory - cp postgreact.control target/ - cp postgreact--0.1.sql target/ + cp hel.control target/ + cp hel--0.1.sql target/ echo "Build complete. Files available in target/ directory." ;; diff --git a/package/c/hel/test/01-test.c b/package/c/hel/test/01-test.c new file mode 100755 index 0000000..e69de29 diff --git a/package/c/postgreact/Makefile b/package/c/postgreact/Makefile deleted file mode 100644 index 0ca01f0..0000000 --- a/package/c/postgreact/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -MODULE_big = postgreact -OBJS = postgreact.o -EXTENSION = postgreact - -DATA = $(wildcard *.sql) - -PG_CONFIG = pg_config - -PGXS := $(shell $(PG_CONFIG) --pgxs) -include $(PGXS) diff --git a/package/c/postgreact/default.nix b/package/c/postgreact/default.nix deleted file mode 100755 index 4dd43ee..0000000 --- a/package/c/postgreact/default.nix +++ /dev/null @@ -1,41 +0,0 @@ -{ - lib, - stdenv, - postgresql, - ... -}: - -stdenv.mkDerivation { - pname = "postgreact"; - version = "0.1"; - - src = ./.; - - buildInputs = [ - postgresql - ]; - - buildPhase = '' - mkdir -p target - sh ./make.sh build - ''; - - installPhase = '' - mkdir -p $out/lib/postgresql $out/share/postgresql/extension - - # Install compiled library - install -m 755 -D target/postgreact.so $out/lib/postgresql/postgreact.so - - # Install control and SQL files - install -m 644 -D postgreact.control $out/share/postgresql/extension/postgreact.control - install -m 644 -D postgreact--0.1.sql $out/share/postgresql/extension/postgreact--0.1.sql - ''; - - meta = with lib; { - description = "PostgreSQL extension for reactive functions"; - homepage = "https://github.com/yukkop/util.nix"; - license = licenses.mit; - platforms = postgresql.meta.platforms; - maintainers = with maintainers; [ ]; - }; -} diff --git a/package/c/postgreact/postgreact--0.1.sql b/package/c/postgreact/postgreact--0.1.sql deleted file mode 100755 index 73c7685..0000000 --- a/package/c/postgreact/postgreact--0.1.sql +++ /dev/null @@ -1,8 +0,0 @@ --- complain if script is sourced in psql, rather than via CREATE EXTENSION -\echo Use "CREATE EXTENSION postgreact" to load this file. \quit - --- Define the hello function that uses our C implementation -CREATE FUNCTION hello() -RETURNS text -AS 'postgreact', 'hello' -LANGUAGE C STRICT; diff --git a/package/c/postgreact/postgreact.c b/package/c/postgreact/postgreact.c deleted file mode 100755 index 98b34ec..0000000 --- a/package/c/postgreact/postgreact.c +++ /dev/null @@ -1,16 +0,0 @@ -#include "postgres.h" -#include "fmgr.h" -#include "utils/builtins.h" /* for text_to_cstring and cstring_to_text */ - -#ifdef PG_MODULE_MAGIC -PG_MODULE_MAGIC; -#endif - -/* Define the function hello */ -PG_FUNCTION_INFO_V1(hello); - -/* Implement the function */ -Datum hello(PG_FUNCTION_ARGS) -{ - PG_RETURN_TEXT_P(cstring_to_text("Hello, world!")); -} \ No newline at end of file