From e260e8f7ccd03e43ff875f636331651818e134c8 Mon Sep 17 00:00:00 2001 From: yukkop Date: Tue, 13 May 2025 15:44:55 +0000 Subject: [PATCH] feat!: some section parse issue --- package/c/hemar/hemar--0.1.sql | 63 +-- package/c/hemar/hemar.c | 958 ++++++++++++++++++++++++++++++++- 2 files changed, 987 insertions(+), 34 deletions(-) diff --git a/package/c/hemar/hemar--0.1.sql b/package/c/hemar/hemar--0.1.sql index 850473a..a362488 100755 --- a/package/c/hemar/hemar--0.1.sql +++ b/package/c/hemar/hemar--0.1.sql @@ -1,31 +1,32 @@ --- complain if script is sourced in psql, rather than via CREATE EXTENSION -\echo Use "CREATE EXTENSION hemar" to load this file. \quit - -CREATE SCHEMA hemar; - --- Define the parse_text_with_hectic function that uses hectic library --- Expected usage: --- ```sql --- SELECT "hemar"."render"( --- "declare" := --- jsonb_build_object( --- 'name', 'test', --- 'config', jsonb_build_object( --- 'debug', true, --- 'limit', 100 --- ) --- ), --- "template" := $hemar$ --- {{ name }} {{ config.limit }} --- $hemar$ --- ); --- ``` ---CREATE FUNCTION "hemar"."render"("declare" jsonb, "template" text) ---RETURNS text ---LANGUAGE C STRICT ---AS 'hemar', 'pg_render'; - -CREATE FUNCTION "hemar"."parse"("template" text) -RETURNS text -LANGUAGE C STRICT -AS 'hemar', 'pg_template_parse'; +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION hemar" to load this file. \quit + +CREATE SCHEMA hemar; + +-- Define the template rendering functions using hectic library +-- Expected usage: +-- ```sql +-- SELECT "hemar"."render"( +-- "declare" := +-- jsonb_build_object( +-- 'name', 'test', +-- 'config', jsonb_build_object( +-- 'debug', true, +-- 'limit', 100 +-- ) +-- ), +-- "template" := $hemar$ +-- {{ name }} {{ config.limit }} +-- $hemar$ +-- ); +-- ``` +CREATE FUNCTION "hemar"."render"("declare" jsonb, "template" text) +RETURNS text +LANGUAGE C STRICT +AS 'hemar', 'pg_render'; + +-- Parse function returns the structure of a template for debugging +CREATE FUNCTION "hemar"."parse"("template" text) +RETURNS text +LANGUAGE C STRICT +AS 'hemar', 'pg_template_parse'; diff --git a/package/c/hemar/hemar.c b/package/c/hemar/hemar.c index cec471b..9483000 100755 --- a/package/c/hemar/hemar.c +++ b/package/c/hemar/hemar.c @@ -11,6 +11,23 @@ #include "utils/elog.h" #include "utils/memutils.h" #include "mb/pg_wchar.h" +#include "fmgr.h" +#include "utils/builtins.h" +#include "lib/stringinfo.h" +#include "utils/jsonb.h" +#include "catalog/pg_type.h" +#include "executor/spi.h" +#include "funcapi.h" + +/* Forward declarations */ +static char *get_jsonb_path_value(Datum jsonb_context, const char *path, bool *found); +static Datum get_jsonb_array(Datum jsonb_context, const char *path, bool *found); +static int get_jsonb_array_length(Datum jsonb_array); +static Datum get_jsonb_array_element(Datum jsonb_array, int index); +static Datum create_iterator_context(Datum parent_context, const char *iterator_name, Datum item_context); +static Datum get_jsonb_include_template(Datum jsonb_context, const char *key, bool *found); +static void get_include_data(Datum include_data, char **template_out, Datum *context_out); +static void template_node_to_string(TemplateNode *node, StringInfo result, int indent); /* Helper function to skip whitespace */ static const char * @@ -515,6 +532,8 @@ template_parse(MemoryContext context, const char **s, const TemplateConfig *conf const char *start; TemplateNode *root, *current, *tag_node; bool current_node_filled = false; + const char *tag_prefix; + size_t text_len; if (!template_validate_config(config, error_code)) { @@ -550,14 +569,14 @@ template_parse(MemoryContext context, const char **s, const TemplateConfig *conf *current->value = init_template_value(TEMPLATE_NODE_TEXT); } - size_t text_len = *s - start; + text_len = *s - start; current->value->text.content = MemoryContextStrdup(context, pnstrdup(start, text_len)); current_node_filled = true; } /* Parse the tag */ tag_node = NULL; - const char *tag_prefix = *s + strlen(config->Syntax.Braces.open); + tag_prefix = *s + strlen(config->Syntax.Braces.open); tag_prefix = skip_whitespace(tag_prefix); /* Determine tag type by prefix */ @@ -626,7 +645,7 @@ template_parse(MemoryContext context, const char **s, const TemplateConfig *conf *current->value = init_template_value(TEMPLATE_NODE_TEXT); } - size_t text_len = *s - start; + text_len = *s - start; current->value->text.content = MemoryContextStrdup(context, pnstrdup(start, text_len)); current_node_filled = true; } @@ -704,4 +723,937 @@ template_free_node(TemplateNode *node) pfree(current); current = next; } +} + +/* Render a template with the given context */ +static char * +template_render(MemoryContext context, TemplateNode *node, Datum jsonb_context, bool *error) +{ + StringInfoData result; + TemplateNode *current; + + if (!node || !context || error == NULL) + { + if (error) + *error = true; + return NULL; + } + + *error = false; + initStringInfo(&result); + current = node; + + while (current) + { + switch (current->type) + { + case TEMPLATE_NODE_TEXT: + if (current->value->text.content) + appendStringInfoString(&result, current->value->text.content); + break; + + case TEMPLATE_NODE_INTERPOLATE: + { + char *value = NULL; + bool found = false; + + if (current->value->interpolate.key) + { + /* Extract value from JSONB context */ + value = get_jsonb_path_value(jsonb_context, current->value->interpolate.key, &found); + + if (found && value) + { + appendStringInfoString(&result, value); + pfree(value); + } + } + } + break; + + case TEMPLATE_NODE_SECTION: + { + /* Handle sections (loops) */ + char *collection_path = current->value->section.collection; + Datum array_value; + bool found = false; + int array_length; + int i; + + if (collection_path && current->value->section.body) + { + /* Get array from context */ + array_value = get_jsonb_array(jsonb_context, collection_path, &found); + + if (found) + { + array_length = get_jsonb_array_length(array_value); + + /* Handle empty arrays gracefully */ + if (array_length <= 0) + break; + + for (i = 0; i < array_length; i++) + { + Datum item_context = get_jsonb_array_element(array_value, i); + Datum merged_context; + char *item_result; + bool item_error = false; + + if (item_context == (Datum) 0) + { + /* Create an empty object for null array elements */ + JsonbParseState *parse_state = NULL; + JsonbValue *empty_obj; + + pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL); + empty_obj = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL); + item_context = PointerGetDatum(JsonbValueToJsonb(empty_obj)); + } + + /* Create context with iterator variable */ + merged_context = create_iterator_context(jsonb_context, current->value->section.iterator, item_context); + + if (merged_context == (Datum) 0) + continue; + + /* Render section body with new context */ + item_result = template_render(context, current->value->section.body, merged_context, &item_error); + + if (!item_error && item_result) + { + appendStringInfoString(&result, item_result); + pfree(item_result); + } + else if (item_error) + { + *error = true; + return result.data; + } + } + } + } + } + break; + + case TEMPLATE_NODE_INCLUDE: + { + /* Handle includes */ + char *template_key = current->value->include.key; + Datum include_data; + bool found = false; + + if (template_key) + { + /* Find include template in context */ + include_data = get_jsonb_include_template(jsonb_context, template_key, &found); + + if (found) + { + char *include_template = NULL; + Datum include_context = (Datum) 0; + char *include_result; + bool include_error = false; + + /* Extract template and context */ + get_include_data(include_data, &include_template, &include_context); + + /* Parse and render included template */ + if (include_template) + { + TemplateConfig config = template_default_config(context); + TemplateErrorCode error_code; + const char *template_str = include_template; + TemplateNode *include_node = template_parse(context, &template_str, &config, false, &error_code); + + if (include_node && error_code == TEMPLATE_ERROR_NONE) + { + include_result = template_render(context, include_node, + include_context != (Datum) 0 ? include_context : jsonb_context, + &include_error); + + if (!include_error && include_result) + { + appendStringInfoString(&result, include_result); + pfree(include_result); + } + else + { + *error = true; + } + + template_free_node(include_node); + } + else + { + *error = true; + } + + pfree(include_template); + } + } + } + } + break; + + case TEMPLATE_NODE_EXECUTE: + /* Execute is not implemented in this version */ + break; + + default: + /* Unknown node type */ + break; + } + + if (*error) + break; + + current = current->next; + } + + return result.data; +} + +/* Helper functions for JSONB handling */ +static char * +get_jsonb_path_value(Datum jsonb_context, const char *path, bool *found) +{ + Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_context); + JsonbValue *jbv_result; + JsonbValue key; + JsonbIterator *it; + JsonbIteratorToken token; + char *result = NULL; + + *found = false; + + if (!jb || !path) + return NULL; + + /* Handle simple top-level key */ + if (strchr(path, '.') == NULL && strchr(path, '[') == NULL) + { + key.type = jbvString; + key.val.string.val = (char *) path; + key.val.string.len = strlen(path); + + jbv_result = findJsonbValueFromContainer(&jb->root, JB_FOBJECT, &key); + + if (jbv_result) + { + *found = true; + + if (jbv_result->type == jbvString) + { + result = pnstrdup(jbv_result->val.string.val, jbv_result->val.string.len); + } + else if (jbv_result->type == jbvNumeric) + { + Numeric num = jbv_result->val.numeric; + result = DatumGetCString(DirectFunctionCall1(numeric_out, NumericGetDatum(num))); + } + else if (jbv_result->type == jbvBool) + { + result = pstrdup(jbv_result->val.boolean ? "true" : "false"); + } + else if (jbv_result->type == jbvNull) + { + result = pstrdup(""); + } + else if (jbv_result->type == jbvBinary) + { + /* Convert binary type to string representation */ + StringInfoData buf; + JsonbValue v; + + initStringInfo(&buf); + + it = JsonbIteratorInit((JsonbContainer *)&jbv_result->val.binary); + + while ((token = JsonbIteratorNext(&it, &v, false)) != WJB_DONE) + { + if (token == WJB_VALUE) + { + if (v.type == jbvString) + { + appendBinaryStringInfo(&buf, v.val.string.val, v.val.string.len); + } + else if (v.type == jbvNumeric) + { + Numeric num = v.val.numeric; + char *numstr = DatumGetCString(DirectFunctionCall1(numeric_out, NumericGetDatum(num))); + appendStringInfoString(&buf, numstr); + pfree(numstr); + } + else if (v.type == jbvBool) + { + appendStringInfoString(&buf, v.val.boolean ? "true" : "false"); + } + } + } + + result = buf.data; + } + } + } + else + { + /* Handle nested paths using a JSON path expression */ + /* This is a simplified implementation and would need to be expanded for a full solution */ + char *current_path = pstrdup(path); + char *token; + char *saveptr; + JsonbValue *current_jbv; + + token = strtok_r(current_path, ".", &saveptr); + while (token != NULL) + { + key.type = jbvString; + key.val.string.val = token; + key.val.string.len = strlen(token); + + current_jbv = findJsonbValueFromContainer(&jb->root, JB_FOBJECT, &key); + + if (!current_jbv) + { + pfree(current_path); + return NULL; + } + + token = strtok_r(NULL, ".", &saveptr); + + /* If there are more path segments, current value must be an object */ + if (token != NULL) + { + if (current_jbv->type != jbvBinary) + { + pfree(current_path); + return NULL; + } + + jb = (Jsonb *) DatumGetPointer(JsonbValueToJsonb(current_jbv)); + } + else + { + /* We found the value */ + *found = true; + + if (current_jbv->type == jbvString) + { + result = pnstrdup(current_jbv->val.string.val, current_jbv->val.string.len); + } + else if (current_jbv->type == jbvNumeric) + { + Numeric num = current_jbv->val.numeric; + result = DatumGetCString(DirectFunctionCall1(numeric_out, NumericGetDatum(num))); + } + else if (current_jbv->type == jbvBool) + { + result = pstrdup(current_jbv->val.boolean ? "true" : "false"); + } + else if (current_jbv->type == jbvNull) + { + result = pstrdup(""); + } + } + } + + pfree(current_path); + } + + return result; +} + +static Datum +get_jsonb_array(Datum jsonb_context, const char *path, bool *found) +{ + Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_context); + JsonbValue *jbv_result; + JsonbValue key; + Datum result = (Datum) 0; + + *found = false; + + if (!jb || !path) + return result; + + /* Handle simple top-level key */ + if (strchr(path, '.') == NULL) + { + key.type = jbvString; + key.val.string.val = (char *) path; + key.val.string.len = strlen(path); + + jbv_result = findJsonbValueFromContainer(&jb->root, JB_FOBJECT, &key); + + if (jbv_result && jbv_result->type == jbvBinary) + { + JsonbIterator *it; + JsonbValue v; + JsonbIteratorToken token; + + it = JsonbIteratorInit((JsonbContainer *)&jbv_result->val.binary); + token = JsonbIteratorNext(&it, &v, false); + + if (token == WJB_BEGIN_ARRAY) + { + *found = true; + result = PointerGetDatum(JsonbValueToJsonb(jbv_result)); + } + } + } + else + { + /* Handle nested paths */ + /* This is a simplified implementation */ + char *current_path = pstrdup(path); + char *token; + char *saveptr; + JsonbValue *current_jbv; + + token = strtok_r(current_path, ".", &saveptr); + while (token != NULL) + { + key.type = jbvString; + key.val.string.val = token; + key.val.string.len = strlen(token); + + current_jbv = findJsonbValueFromContainer(&jb->root, JB_FOBJECT, &key); + + if (!current_jbv) + { + pfree(current_path); + return result; + } + + token = strtok_r(NULL, ".", &saveptr); + + /* If there are more path segments, current value must be an object */ + if (token != NULL) + { + if (current_jbv->type != jbvBinary) + { + pfree(current_path); + return result; + } + + jb = (Jsonb *) DatumGetPointer(JsonbValueToJsonb(current_jbv)); + } + else + { + /* Check if the final value is an array */ + if (current_jbv->type == jbvBinary) + { + JsonbIterator *it; + JsonbValue v; + JsonbIteratorToken token; + + it = JsonbIteratorInit((JsonbContainer *)¤t_jbv->val.binary); + token = JsonbIteratorNext(&it, &v, false); + + if (token == WJB_BEGIN_ARRAY) + { + *found = true; + result = PointerGetDatum(JsonbValueToJsonb(current_jbv)); + } + } + } + } + + pfree(current_path); + } + + return result; +} + +static int +get_jsonb_array_length(Datum jsonb_array) +{ + Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_array); + JsonbIterator *it; + JsonbValue v; + JsonbIteratorToken token; + int count = 0; + + if (!jb) + return 0; + + it = JsonbIteratorInit((JsonbContainer *)&jb->root); + + /* Skip the WJB_BEGIN_ARRAY token */ + token = JsonbIteratorNext(&it, &v, false); + + if (token != WJB_BEGIN_ARRAY) + return 0; + + /* Count array elements */ + while ((token = JsonbIteratorNext(&it, &v, false)) != WJB_DONE) + { + if (token == WJB_ELEM) + count++; + } + + return count; +} + +static Datum +get_jsonb_array_element(Datum jsonb_array, int index) +{ + Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_array); + JsonbIterator *it; + JsonbValue v; + JsonbIteratorToken token; + int current_index = 0; + Datum result = (Datum) 0; + JsonbParseState *parse_state = NULL; + JsonbValue *jbv_result; + + if (!jb || index < 0) + return result; + + it = JsonbIteratorInit((JsonbContainer *)&jb->root); + + /* Skip the WJB_BEGIN_ARRAY token */ + token = JsonbIteratorNext(&it, &v, false); + + if (token != WJB_BEGIN_ARRAY) + return result; + + /* Find the element at the specified index */ + while ((token = JsonbIteratorNext(&it, &v, false)) != WJB_DONE) + { + if (token == WJB_ELEM) + { + if (current_index == index) + { + /* Found the element */ + if (v.type == jbvBinary) + { + /* For binary values, just convert directly */ + result = PointerGetDatum(JsonbValueToJsonb(&v)); + } + else + { + /* For scalar values, we need to create a proper JSON value */ + pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL); + + /* Add a dummy key "value" */ + JsonbValue key; + key.type = jbvString; + key.val.string.val = "value"; + key.val.string.len = 5; + + pushJsonbValue(&parse_state, WJB_KEY, &key); + + /* Add the value */ + pushJsonbValue(&parse_state, WJB_VALUE, &v); + + /* Finish the object */ + jbv_result = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL); + + /* Convert to Jsonb */ + result = PointerGetDatum(JsonbValueToJsonb(jbv_result)); + } + break; + } + current_index++; + } + } + + return result; +} + +static Datum +create_iterator_context(Datum parent_context, const char *iterator_name, Datum item_context) +{ + Jsonb *parent_jb = (Jsonb *) DatumGetPointer(parent_context); + Jsonb *item_jb = (Jsonb *) DatumGetPointer(item_context); + JsonbParseState *parse_state = NULL; + JsonbValue *result; + JsonbIterator *it; + JsonbValue v; + JsonbIteratorToken token; + JsonbValue key; + JsonbValue val; + bool is_scalar = false; + + if (!parent_jb || !item_jb || !iterator_name) + return parent_context; + + /* Start with a copy of the parent context */ + pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL); + + /* Copy all fields from parent context */ + it = JsonbIteratorInit((JsonbContainer *)&parent_jb->root); + + /* Skip the WJB_BEGIN_OBJECT token */ + token = JsonbIteratorNext(&it, &v, false); + + if (token != WJB_BEGIN_OBJECT) + return parent_context; + + /* Copy all key-value pairs from parent */ + while ((token = JsonbIteratorNext(&it, &v, false)) != WJB_DONE) + { + if (token == WJB_KEY) + { + /* Copy the key */ + key.type = jbvString; + key.val.string.val = pnstrdup(v.val.string.val, v.val.string.len); + key.val.string.len = v.val.string.len; + + pushJsonbValue(&parse_state, WJB_KEY, &key); + + /* Get and copy the value */ + token = JsonbIteratorNext(&it, &val, false); + pushJsonbValue(&parse_state, WJB_VALUE, &val); + + pfree(key.val.string.val); + } + } + + /* Add the iterator variable */ + key.type = jbvString; + key.val.string.val = (char *) iterator_name; + key.val.string.len = strlen(iterator_name); + + pushJsonbValue(&parse_state, WJB_KEY, &key); + + /* Check if the item is a scalar value wrapped in an object */ + it = JsonbIteratorInit((JsonbContainer *)&item_jb->root); + token = JsonbIteratorNext(&it, &v, false); + + if (token == WJB_BEGIN_OBJECT) + { + /* Check if it's our special scalar wrapper with "value" key */ + token = JsonbIteratorNext(&it, &v, false); + if (token == WJB_KEY && v.type == jbvString && + v.val.string.len == 5 && strncmp(v.val.string.val, "value", 5) == 0) + { + /* Get the scalar value */ + token = JsonbIteratorNext(&it, &v, false); + if (token == WJB_VALUE) + { + is_scalar = true; + pushJsonbValue(&parse_state, WJB_VALUE, &v); + } + } + } + + /* If not a scalar, use the whole item as is */ + if (!is_scalar) + { + it = JsonbIteratorInit((JsonbContainer *)&item_jb->root); + token = JsonbIteratorNext(&it, &v, false); + pushJsonbValue(&parse_state, WJB_VALUE, &v); + } + + /* Finalize the new context object */ + result = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL); + + return PointerGetDatum(JsonbValueToJsonb(result)); +} + +static Datum +get_jsonb_include_template(Datum jsonb_context, const char *key, bool *found) +{ + Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_context); + JsonbValue *jbv_result; + JsonbValue search_key; + Datum result = (Datum) 0; + char *include_key; + + *found = false; + + if (!jb || !key) + return result; + + /* Construct the include key */ + include_key = psprintf("include %s", key); + + /* Look for the include template in the context */ + search_key.type = jbvString; + search_key.val.string.val = include_key; + search_key.val.string.len = strlen(include_key); + + jbv_result = findJsonbValueFromContainer(&jb->root, JB_FOBJECT, &search_key); + + if (jbv_result && jbv_result->type == jbvBinary) + { + *found = true; + result = PointerGetDatum(JsonbValueToJsonb(jbv_result)); + } + + pfree(include_key); + return result; +} + +static void +get_include_data(Datum include_data, char **template_out, Datum *context_out) +{ + Jsonb *jb = (Jsonb *) DatumGetPointer(include_data); + JsonbIterator *it; + JsonbValue v; + JsonbIteratorToken token; + JsonbValue template_key, context_key; + JsonbValue *template_jbv, *context_jbv; + + *template_out = NULL; + *context_out = (Datum) 0; + + if (!jb) + return; + + /* Check if it's an array of include objects */ + it = JsonbIteratorInit((JsonbContainer *)&jb->root); + token = JsonbIteratorNext(&it, &v, false); + + if (token != WJB_BEGIN_ARRAY) + return; + + /* Get the first element (we only support one include for now) */ + token = JsonbIteratorNext(&it, &v, false); + + if (token != WJB_ELEM || v.type != jbvBinary) + return; + + /* Get the include object */ + Jsonb *include_obj = (Jsonb *) DatumGetPointer(JsonbValueToJsonb(&v)); + + /* Look for template and context keys */ + template_key.type = jbvString; + template_key.val.string.val = "template"; + template_key.val.string.len = strlen("template"); + + context_key.type = jbvString; + context_key.val.string.val = "context"; + context_key.val.string.len = strlen("context"); + + template_jbv = findJsonbValueFromContainer(&include_obj->root, JB_FOBJECT, &template_key); + + /* Extract template string */ + if (template_jbv && template_jbv->type == jbvString) + { + *template_out = pnstrdup(template_jbv->val.string.val, template_jbv->val.string.len); + } + + context_jbv = findJsonbValueFromContainer(&include_obj->root, JB_FOBJECT, &context_key); + + /* Extract context object */ + if (context_jbv && context_jbv->type == jbvBinary) + { + *context_out = PointerGetDatum(JsonbValueToJsonb(context_jbv)); + } +} + +/* PostgreSQL function wrappers */ +PG_MODULE_MAGIC; + +PG_FUNCTION_INFO_V1(pg_template_parse); +Datum +pg_template_parse(PG_FUNCTION_ARGS) +{ + text *template_text = PG_GETARG_TEXT_PP(0); + char *template_str = text_to_cstring(template_text); + const char *template_ptr = template_str; + MemoryContext old_context; + MemoryContext parse_context; + TemplateConfig config; + TemplateNode *root = NULL; + TemplateErrorCode error_code = TEMPLATE_ERROR_NONE; + text *result_text = NULL; + StringInfoData result; + + /* Create a memory context for parsing */ + parse_context = AllocSetContextCreate(CurrentMemoryContext, + "Template Parse Context", + ALLOCSET_DEFAULT_SIZES); + + /* Switch to the new context for parsing */ + old_context = MemoryContextSwitchTo(parse_context); + + /* Initialize default config */ + config = template_default_config(parse_context); + + PG_TRY(); + { + /* Parse the template */ + root = template_parse(parse_context, &template_ptr, &config, false, &error_code); + + /* Check for parsing errors */ + if (error_code != TEMPLATE_ERROR_NONE || !root) + { + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("Template parsing error: %s", template_error_to_string(error_code)))); + } + + /* Convert the parsed template to a string representation for debugging */ + initStringInfo(&result); + appendStringInfo(&result, "Template parsed successfully. Structure:\n"); + template_node_to_string(root, &result, 0); + + /* Switch back to the original memory context */ + MemoryContextSwitchTo(old_context); + + /* Return the result */ + result_text = cstring_to_text(result.data); + pfree(result.data); + } + PG_CATCH(); + { + /* Switch back to the original memory context for error handling */ + MemoryContextSwitchTo(old_context); + + /* Clean up */ + if (template_str) + pfree(template_str); + + /* Delete the parse context */ + MemoryContextDelete(parse_context); + + PG_RE_THROW(); + } + PG_END_TRY(); + + /* Clean up */ + MemoryContextDelete(parse_context); + pfree(template_str); + + PG_RETURN_TEXT_P(result_text); +} + +PG_FUNCTION_INFO_V1(pg_render); +Datum +pg_render(PG_FUNCTION_ARGS) +{ + text *template_text = PG_GETARG_TEXT_PP(1); + Jsonb *context_jsonb = PG_GETARG_JSONB_P(0); + char *template_str = text_to_cstring(template_text); + const char *template_ptr = template_str; + MemoryContext old_context; + MemoryContext render_context; + TemplateConfig config; + TemplateNode *root = NULL; + TemplateErrorCode error_code = TEMPLATE_ERROR_NONE; + bool render_error = false; + char *rendered_result = NULL; + text *result_text = NULL; + + /* Create a memory context for rendering */ + render_context = AllocSetContextCreate(CurrentMemoryContext, + "Template Render Context", + ALLOCSET_DEFAULT_SIZES); + + /* Switch to the new context for parsing and rendering */ + old_context = MemoryContextSwitchTo(render_context); + + /* Initialize default config */ + config = template_default_config(render_context); + + PG_TRY(); + { + /* Parse the template */ + root = template_parse(render_context, &template_ptr, &config, false, &error_code); + + /* Check for parsing errors */ + if (error_code != TEMPLATE_ERROR_NONE || !root) + { + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("Template parsing error: %s", template_error_to_string(error_code)))); + } + + /* Render the template with the provided context */ + rendered_result = template_render(render_context, root, PointerGetDatum(context_jsonb), &render_error); + + /* Check for rendering errors */ + if (render_error || !rendered_result) + { + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("Template rendering error"))); + } + + /* Convert the result to a text datum in the original memory context */ + MemoryContextSwitchTo(old_context); + result_text = cstring_to_text(rendered_result); + } + PG_CATCH(); + { + /* Switch back to the original memory context for error handling */ + MemoryContextSwitchTo(old_context); + + /* Clean up */ + if (template_str) + pfree(template_str); + + /* Delete the render context */ + MemoryContextDelete(render_context); + + PG_RE_THROW(); + } + PG_END_TRY(); + + /* Clean up */ + MemoryContextDelete(render_context); + pfree(template_str); + + PG_RETURN_TEXT_P(result_text); +} + +/* Helper function to convert template node to string for debugging */ +static void +template_node_to_string(TemplateNode *node, StringInfo result, int indent) +{ + TemplateNode *current = node; + int i; + + while (current) + { + /* Add indentation */ + for (i = 0; i < indent; i++) + appendStringInfoChar(result, ' '); + + /* Add node type */ + switch (current->type) + { + case TEMPLATE_NODE_TEXT: + appendStringInfo(result, "TEXT: \"%s\"\n", + current->value->text.content ? current->value->text.content : ""); + break; + + case TEMPLATE_NODE_INTERPOLATE: + appendStringInfo(result, "INTERPOLATE: \"%s\"\n", + current->value->interpolate.key ? current->value->interpolate.key : ""); + break; + + case TEMPLATE_NODE_SECTION: + appendStringInfo(result, "SECTION: iterator=\"%s\", collection=\"%s\"\n", + current->value->section.iterator ? current->value->section.iterator : "", + current->value->section.collection ? current->value->section.collection : ""); + + if (current->value->section.body) + { + template_node_to_string(current->value->section.body, result, indent + 2); + } + break; + + case TEMPLATE_NODE_EXECUTE: + appendStringInfo(result, "EXECUTE: \"%s\"\n", + current->value->execute.code ? current->value->execute.code : ""); + break; + + case TEMPLATE_NODE_INCLUDE: + appendStringInfo(result, "INCLUDE: \"%s\"\n", + current->value->include.key ? current->value->include.key : ""); + break; + + default: + appendStringInfo(result, "UNKNOWN NODE TYPE: %d\n", current->type); + break; + } + + current = current->next; + } } \ No newline at end of file