feat: hemar: included tag

This commit is contained in:
2025-05-18 18:51:21 +00:00
parent 5cabf6c04d
commit 746779e952
4 changed files with 628 additions and 263 deletions

View File

@@ -94,32 +94,32 @@ Includes content from other templates.
```json ```json
// Separate context // Separate context
{ {
"include inner_template": [ "include": {
{ "inner_template": {
"template": "{{ field }}", "template": "{{ field }}",
"context": { "field": "value" } "context": { "field": "value" }
} }
] }
} }
// Shared root context // Shared root context
{ {
"field": "value", "field": "value",
"include inner_template": [ "include": {
{ "inner_template": {
"template": "{{ field }}" "template": "{{ field }}"
} }
] }
} }
// Plain text inclusion // Plain text inclusion
{ {
"field": "value", "field": "value",
"include inner_template": [ "include": {
{ "inner_template": {
"content": "<p>value</p>" "content": "<p>value</p>"
} }
] }
} }
``` ```

View File

@@ -695,7 +695,7 @@ template_parse_execute(MemoryContext context, const char **s_ptr,
/* If we've reached the matching closing brace, we're done */ /* If we've reached the matching closing brace, we're done */
if (brace_level == 0) { if (brace_level == 0) {
break; break;
} }
*s += strlen(config->Syntax.Braces.close) - 1; /* -1 because we'll increment s below */ *s += strlen(config->Syntax.Braces.close) - 1; /* -1 because we'll increment s below */
@@ -709,10 +709,17 @@ template_parse_execute(MemoryContext context, const char **s_ptr,
code_len = *s - code_start; code_len = *s - code_start;
// FIXME(yukkop): Are we realy need to trim this whitespaces?
/* Trim trailing whitespace */ /* Trim trailing whitespace */
while (code_len > 0 && isspace((unsigned char)code_start[code_len - 1])) while (code_len > 0 && isspace((unsigned char)code_start[code_len - 1]))
code_len--; code_len--;
/* Trim leading whitespace */
while (code_len > 0 && isspace((unsigned char)*code_start)) {
code_start++;
code_len--;
}
node->value->execute.code = MemoryContextStrdup(context, pnstrdup(code_start, code_len)); node->value->execute.code = MemoryContextStrdup(context, pnstrdup(code_start, code_len));
/* Check for closing brace */ /* Check for closing brace */
@@ -1197,221 +1204,143 @@ render_template(TemplateNode *node, Jsonb *define, StringInfo result, MemoryCont
char *debug_msg = palloc(debug_msg_size); char *debug_msg = palloc(debug_msg_size);
elog(DEBUG1, "define: %s", JsonbToCString(NULL, &define->root, VARSIZE_ANY_EXHDR(define))); elog(DEBUG1, "define: %s", JsonbToCString(NULL, &define->root, VARSIZE_ANY_EXHDR(define)));
while (current) PG_TRY();
{ {
snprintf(debug_msg, debug_msg_size, "Rendering node type: %.50s", tnt_to_string(current->type)); while (current)
switch (current->type)
{ {
case TEMPLATE_NODE_TEXT: snprintf(debug_msg, debug_msg_size, "Rendering node type: %.50s", tnt_to_string(current->type));
snprintf(debug_msg, debug_msg_size, "%s TEXT: %.50s", debug_msg, current->value->text.content); switch (current->type)
if (current->value->text.content) {
{ case TEMPLATE_NODE_TEXT:
/* Preserve whitespace in text nodes */ snprintf(debug_msg, debug_msg_size, "%s TEXT: %.50s", debug_msg, current->value->text.content);
appendStringInfoString(result, current->value->text.content); if (current->value->text.content)
} {
break; /* Preserve whitespace in text nodes */
appendStringInfoString(result, current->value->text.content);
case TEMPLATE_NODE_INTERPOLATE: }
snprintf(debug_msg, debug_msg_size, "%s INTERPOLATE: %.50s", debug_msg, current->value->interpolate.key); break;
if (current->value->interpolate.key)
{ case TEMPLATE_NODE_INTERPOLATE:
/* Get the value from the JSONB context using the path */ snprintf(debug_msg, debug_msg_size, "%s INTERPOLATE: %.50s", debug_msg, current->value->interpolate.key);
value = jsonb_get_by_path_internal(define, current->value->interpolate.key, context); if (current->value->interpolate.key)
{
/* Get the value from the JSONB context using the path */
value = jsonb_get_by_path_internal(define, current->value->interpolate.key, context);
if (value != NULL)
{
/* Convert the value to a string based on its type */
switch (value->type)
{
case jbvString:
/* Preserve whitespace in string values */
snprintf(debug_msg, debug_msg_size, "%s VALUE: %.50s", debug_msg, value->val.string.val);
appendStringInfoString(result, value->val.string.val);
break;
case jbvNumeric:
str_value = DatumGetCString(DirectFunctionCall1(numeric_out,
NumericGetDatum(value->val.numeric)));
appendStringInfoString(result, str_value);
pfree(str_value);
break;
case jbvBool:
appendStringInfoString(result, value->val.boolean ? "true" : "false");
break;
case jbvNull:
/* For null values, we don't output anything */
break;
case jbvBinary:
/* For complex types (objects/arrays), convert to JSON string */
str_value = DatumGetCString(DirectFunctionCall1(jsonb_out,
JsonbPGetDatum(JsonbValueToJsonb(value))));
appendStringInfoString(result, str_value);
pfree(str_value);
break;
default:
elog(WARNING, "Unsupported JSONB value type in interpolation: %s",
jbv_type_to_string(value->type));
break;
}
/* Free the value since it was allocated in our context */
pfree(value);
}
}
else
{
elog(WARNING, "Interpolation key is not set");
}
break;
case TEMPLATE_NODE_EXECUTE:
snprintf(debug_msg, debug_msg_size, "%s EXECUTE: %.50s", debug_msg, current->value->execute.code);
render_execute_tag(current->value->execute.code, define, result, context);
break;
case TEMPLATE_NODE_SECTION:
snprintf(debug_msg, debug_msg_size, "%s SECTION: %.50s", debug_msg, current->value->section.iterator);
if (!current->value->section.collection)
{
elog(WARNING, "Section collection is not set");
break;
}
if (!current->value->section.iterator)
{
elog(WARNING, "Section iterator is not set");
break;
}
value = jsonb_get_by_path_internal(define, current->value->section.collection, context);
if (value != NULL) if (value != NULL)
{ {
/* Convert the value to a string based on its type */ /* Convert the value to a string based on its type */
/* must render body with context (define) concatenated with iterator item */
switch (value->type) switch (value->type)
{ {
case jbvString: case jbvString:
/* Preserve whitespace in string values */ /* iterate by string, where item is char */
snprintf(debug_msg, debug_msg_size, "%s VALUE: %.50s", debug_msg, value->val.string.val);
appendStringInfoString(result, value->val.string.val);
break;
case jbvNumeric:
str_value = DatumGetCString(DirectFunctionCall1(numeric_out,
NumericGetDatum(value->val.numeric)));
appendStringInfoString(result, str_value);
pfree(str_value);
break;
case jbvBool:
appendStringInfoString(result, value->val.boolean ? "true" : "false");
break;
case jbvNull:
/* For null values, we don't output anything */
break;
case jbvBinary:
/* For complex types (objects/arrays), convert to JSON string */
str_value = DatumGetCString(DirectFunctionCall1(jsonb_out,
JsonbPGetDatum(JsonbValueToJsonb(value))));
appendStringInfoString(result, str_value);
pfree(str_value);
break;
default:
elog(WARNING, "Unsupported JSONB value type in interpolation: %s",
jbv_type_to_string(value->type));
break;
}
/* Free the value since it was allocated in our context */
pfree(value);
}
}
else
{
elog(WARNING, "Interpolation key is not set");
}
break;
case TEMPLATE_NODE_EXECUTE:
snprintf(debug_msg, debug_msg_size, "%s EXECUTE: %.50s", debug_msg, current->value->execute.code);
render_execute_tag(current->value->execute.code, define, result, context);
break;
case TEMPLATE_NODE_SECTION:
snprintf(debug_msg, debug_msg_size, "%s SECTION: %.50s", debug_msg, current->value->section.iterator);
if (!current->value->section.collection)
{
elog(WARNING, "Section collection is not set");
break;
}
if (!current->value->section.iterator)
{
elog(WARNING, "Section iterator is not set");
break;
}
value = jsonb_get_by_path_internal(define, current->value->section.collection, context);
if (value != NULL)
{
/* Convert the value to a string based on its type */
/* must render body with context (define) concatenated with iterator item */
switch (value->type)
{
case jbvString:
/* iterate by string, where item is char */
{
const char *str = value->val.string.val;
for (int i = 0; i < value->val.string.len; i++)
{ {
/* Create a new context with the current character */ const char *str = value->val.string.val;
JsonbValue char_val; for (int i = 0; i < value->val.string.len; i++)
char_val.type = jbvString;
char_val.val.string.val = pstrdup((char[]){str[i], '\0'});
char_val.val.string.len = 1;
/* Create a new context with the iterator value */
JsonbValue *new_context = palloc(sizeof(JsonbValue));
new_context->type = jbvObject;
/* Create a new JSONB object */
JsonbParseState *parse_state = NULL;
JsonbValue *res = pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL);
/* Copy the original context */
JsonbIterator *it = JsonbIteratorInit(&define->root);
JsonbIteratorToken token;
JsonbValue v;
while ((token = JsonbIteratorNext(&it, &v, true)) != WJB_DONE)
{ {
if (token == WJB_KEY) /* Create a new context with the current character */
{ JsonbValue char_val;
/* Add the key */ char_val.type = jbvString;
pushJsonbValue(&parse_state, WJB_KEY, &v); char_val.val.string.val = pstrdup((char[]){str[i], '\0'});
} char_val.val.string.len = 1;
else if (token == WJB_VALUE)
{ /* Create a new context with the iterator value */
/* Add the value */ JsonbValue *new_context = palloc(sizeof(JsonbValue));
pushJsonbValue(&parse_state, WJB_VALUE, &v); new_context->type = jbvObject;
}
} /* Create a new JSONB object */
/* Add the iterator value */
JsonbValue key_val;
key_val.type = jbvString;
key_val.val.string.val = pstrdup(current->value->section.iterator);
key_val.val.string.len = strlen(current->value->section.iterator);
pushJsonbValue(&parse_state, WJB_KEY, &key_val);
pushJsonbValue(&parse_state, WJB_VALUE, &char_val);
/* Finish the object */
res = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL);
/* Convert to Jsonb */
Jsonb *context_jsonb = JsonbValueToJsonb(res);
/* Render the section body with the new context */
render_template(current->value->section.body, context_jsonb, result, context);
/* Free the temporary values */
pfree(char_val.val.string.val);
pfree(key_val.val.string.val);
pfree(new_context);
}
}
break;
case jbvNumeric:
elog(WARNING, "Numeric values cannot be used as section collections");
break;
case jbvBool:
if (value->val.boolean)
{
/* Render the section body with the original context */
render_template(current->value->section.body, define, result, context);
}
break;
case jbvNull:
/* Don't render anything for null values */
break;
case jbvBinary:
/* iterate by array as expected or object where item is key/value pair */
{
JsonbIterator *it = JsonbIteratorInit((JsonbContainer *)value->val.binary.data);
JsonbIteratorToken token;
JsonbValue v;
/* Get the container type */
token = JsonbIteratorNext(&it, &v, true);
if (token == WJB_BEGIN_ARRAY)
{
/* Iterate through array elements */
int index = 0;
while ((token = JsonbIteratorNext(&it, &v, true)) != WJB_END_ARRAY)
{
/* Create a new context with the current element */
JsonbParseState *parse_state = NULL; JsonbParseState *parse_state = NULL;
JsonbValue *res = pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL); JsonbValue *res = pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL);
/* Copy the original context */ /* Copy the original context */
JsonbIterator *ctx_it = JsonbIteratorInit(&define->root); JsonbIterator *it = JsonbIteratorInit(&define->root);
JsonbIteratorToken ctx_token; JsonbIteratorToken token;
JsonbValue ctx_v; JsonbValue v;
while ((ctx_token = JsonbIteratorNext(&ctx_it, &ctx_v, true)) != WJB_DONE) while ((token = JsonbIteratorNext(&it, &v, true)) != WJB_DONE)
{ {
if (ctx_token == WJB_KEY) if (token == WJB_KEY)
{ {
/* Add the key */ /* Add the key */
pushJsonbValue(&parse_state, WJB_KEY, &ctx_v); pushJsonbValue(&parse_state, WJB_KEY, &v);
} }
else if (ctx_token == WJB_VALUE) else if (token == WJB_VALUE)
{ {
/* Add the value */ /* Add the value */
pushJsonbValue(&parse_state, WJB_VALUE, &ctx_v); pushJsonbValue(&parse_state, WJB_VALUE, &v);
} }
} }
@@ -1421,7 +1350,7 @@ render_template(TemplateNode *node, Jsonb *define, StringInfo result, MemoryCont
key_val.val.string.val = pstrdup(current->value->section.iterator); key_val.val.string.val = pstrdup(current->value->section.iterator);
key_val.val.string.len = strlen(current->value->section.iterator); key_val.val.string.len = strlen(current->value->section.iterator);
pushJsonbValue(&parse_state, WJB_KEY, &key_val); pushJsonbValue(&parse_state, WJB_KEY, &key_val);
pushJsonbValue(&parse_state, WJB_VALUE, &v); pushJsonbValue(&parse_state, WJB_VALUE, &char_val);
/* Finish the object */ /* Finish the object */
res = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL); res = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL);
@@ -1433,18 +1362,46 @@ render_template(TemplateNode *node, Jsonb *define, StringInfo result, MemoryCont
render_template(current->value->section.body, context_jsonb, result, context); render_template(current->value->section.body, context_jsonb, result, context);
/* Free the temporary values */ /* Free the temporary values */
pfree(char_val.val.string.val);
pfree(key_val.val.string.val); pfree(key_val.val.string.val);
index++; pfree(new_context);
} }
} }
else if (token == WJB_BEGIN_OBJECT) break;
case jbvNumeric:
elog(WARNING, "Numeric values cannot be used as section collections");
break;
case jbvBool:
if (value->val.boolean)
{ {
/* Iterate through object key-value pairs */ /* Render the section body with the original context */
while ((token = JsonbIteratorNext(&it, &v, true)) != WJB_END_OBJECT) render_template(current->value->section.body, define, result, context);
}
break;
case jbvNull:
/* Don't render anything for null values */
break;
case jbvBinary:
/* iterate by array as expected or object where item is key/value pair */
{
JsonbIterator *it = JsonbIteratorInit((JsonbContainer *)value->val.binary.data);
JsonbIteratorToken token;
JsonbValue v;
/* Get the container type */
token = JsonbIteratorNext(&it, &v, true);
if (token == WJB_BEGIN_ARRAY)
{ {
if (token == WJB_KEY) /* Iterate through array elements */
int index = 0;
while ((token = JsonbIteratorNext(&it, &v, true)) != WJB_END_ARRAY)
{ {
/* Create a new context with the current key-value pair */ /* Create a new context with the current element */
JsonbParseState *parse_state = NULL; JsonbParseState *parse_state = NULL;
JsonbValue *res = pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL); JsonbValue *res = pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL);
@@ -1467,39 +1424,15 @@ render_template(TemplateNode *node, Jsonb *define, StringInfo result, MemoryCont
} }
} }
/* Add the iterator object with key and value */ /* Add the iterator value */
JsonbValue key_val; JsonbValue key_val;
key_val.type = jbvString; key_val.type = jbvString;
key_val.val.string.val = pstrdup(current->value->section.iterator); key_val.val.string.val = pstrdup(current->value->section.iterator);
key_val.val.string.len = strlen(current->value->section.iterator); key_val.val.string.len = strlen(current->value->section.iterator);
pushJsonbValue(&parse_state, WJB_KEY, &key_val); pushJsonbValue(&parse_state, WJB_KEY, &key_val);
pushJsonbValue(&parse_state, WJB_VALUE, &v);
/* Create an object for the iterator */ /* Finish the object */
JsonbParseState *item_parse_state = NULL;
JsonbValue *item_res = pushJsonbValue(&item_parse_state, WJB_BEGIN_OBJECT, NULL);
/* Add the key */
key_val.val.string.val = pstrdup("key");
key_val.val.string.len = strlen("key");
pushJsonbValue(&item_parse_state, WJB_KEY, &key_val);
pushJsonbValue(&item_parse_state, WJB_VALUE, &v);
/* Get the value */
token = JsonbIteratorNext(&it, &v, true);
/* Add the value */
key_val.val.string.val = pstrdup("value");
key_val.val.string.len = strlen("value");
pushJsonbValue(&item_parse_state, WJB_KEY, &key_val);
pushJsonbValue(&item_parse_state, WJB_VALUE, &v);
/* Finish the iterator object */
item_res = pushJsonbValue(&item_parse_state, WJB_END_OBJECT, NULL);
/* Add the iterator object to the context */
pushJsonbValue(&parse_state, WJB_VALUE, item_res);
/* Finish the context object */
res = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL); res = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL);
/* Convert to Jsonb */ /* Convert to Jsonb */
@@ -1510,37 +1443,271 @@ render_template(TemplateNode *node, Jsonb *define, StringInfo result, MemoryCont
/* Free the temporary values */ /* Free the temporary values */
pfree(key_val.val.string.val); pfree(key_val.val.string.val);
index++;
}
}
else if (token == WJB_BEGIN_OBJECT)
{
/* Iterate through object key-value pairs */
while ((token = JsonbIteratorNext(&it, &v, true)) != WJB_END_OBJECT)
{
if (token == WJB_KEY)
{
/* Create a new context with the current key-value pair */
JsonbParseState *parse_state = NULL;
JsonbValue *res = pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL);
/* Copy the original context */
JsonbIterator *ctx_it = JsonbIteratorInit(&define->root);
JsonbIteratorToken ctx_token;
JsonbValue ctx_v;
while ((ctx_token = JsonbIteratorNext(&ctx_it, &ctx_v, true)) != WJB_DONE)
{
if (ctx_token == WJB_KEY)
{
/* Add the key */
pushJsonbValue(&parse_state, WJB_KEY, &ctx_v);
}
else if (ctx_token == WJB_VALUE)
{
/* Add the value */
pushJsonbValue(&parse_state, WJB_VALUE, &ctx_v);
}
}
/* Add the iterator object with key and value */
JsonbValue key_val;
key_val.type = jbvString;
key_val.val.string.val = pstrdup(current->value->section.iterator);
key_val.val.string.len = strlen(current->value->section.iterator);
pushJsonbValue(&parse_state, WJB_KEY, &key_val);
/* Create an object for the iterator */
JsonbParseState *item_parse_state = NULL;
JsonbValue *item_res = pushJsonbValue(&item_parse_state, WJB_BEGIN_OBJECT, NULL);
/* Add the key */
key_val.val.string.val = pstrdup("key");
key_val.val.string.len = strlen("key");
pushJsonbValue(&item_parse_state, WJB_KEY, &key_val);
pushJsonbValue(&item_parse_state, WJB_VALUE, &v);
/* Get the value */
token = JsonbIteratorNext(&it, &v, true);
/* Add the value */
key_val.val.string.val = pstrdup("value");
key_val.val.string.len = strlen("value");
pushJsonbValue(&item_parse_state, WJB_KEY, &key_val);
pushJsonbValue(&item_parse_state, WJB_VALUE, &v);
/* Finish the iterator object */
item_res = pushJsonbValue(&item_parse_state, WJB_END_OBJECT, NULL);
/* Add the iterator object to the context */
pushJsonbValue(&parse_state, WJB_VALUE, item_res);
/* Finish the context object */
res = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL);
/* Convert to Jsonb */
Jsonb *context_jsonb = JsonbValueToJsonb(res);
/* Render the section body with the new context */
render_template(current->value->section.body, context_jsonb, result, context);
/* Free the temporary values */
pfree(key_val.val.string.val);
}
} }
} }
} }
} break;
break;
default:
default: elog(WARNING, "Unsupported JSONB value type in section: %s",
elog(WARNING, "Unsupported JSONB value type in section: %s", jbv_type_to_string(value->type));
jbv_type_to_string(value->type)); break;
break; }
/* Free the value since it was allocated in our context */
pfree(value);
} }
break;
/* Free the value since it was allocated in our context */ case TEMPLATE_NODE_INCLUDE:
pfree(value); snprintf(debug_msg, debug_msg_size, "%s INCLUDE: %.50s", debug_msg, current->value->include.key);
} if (current->value->include.key)
break; {
/* Construct the path with include prefix */
case TEMPLATE_NODE_INCLUDE: char *include_path = psprintf("include.%s", current->value->include.key);
snprintf(debug_msg, debug_msg_size, "%s INCLUDE: %.50s", debug_msg, current->value->include.key); elog(DEBUG1, "Include path: %s", include_path);
/* We'll implement include rendering later */
break; /* Get the include data from the context */
JsonbValue *include_data = jsonb_get_by_path_internal(define, include_path, context);
default:
elog(WARNING, "Unknown template node type: %d", current->type); elog(DEBUG1, "Include data: %s", JsonbToCString(NULL, &JsonbValueToJsonb(include_data)->root, VARSIZE_ANY_EXHDR(JsonbValueToJsonb(include_data))));
break;
if (include_data != NULL && include_data->type == jbvBinary)
{
JsonbIterator *it = JsonbIteratorInit((JsonbContainer *)include_data->val.binary.data);
JsonbIteratorToken token;
JsonbValue v;
/* Get the container type */
token = JsonbIteratorNext(&it, &v, true);
if (token == WJB_BEGIN_OBJECT)
{
/* Check for content first (plain text inclusion) */
bool found_content = false;
bool found_template = false;
JsonbValue *content = NULL;
JsonbValue *template = NULL;
JsonbValue *include_context = NULL;
while ((token = JsonbIteratorNext(&it, &v, true)) != WJB_END_OBJECT)
{
elog(DEBUG1, "Token %s, Type: %s, Check: %s", jbt_type_to_string(token), jbv_type_to_string(v.type), token == WJB_KEY && v.type == jbvString ? "true" : "false");
if (token == WJB_KEY && v.type == jbvString)
{
if (strncmp(v.val.string.val, "content", 7) == 0)
{
token = JsonbIteratorNext(&it, &v, true);
if (token == WJB_VALUE && v.type == jbvString)
{
/* Create a proper copy of the string value */
content = palloc(sizeof(JsonbValue));
content->type = jbvString;
content->val.string.len = v.val.string.len;
content->val.string.val = palloc(v.val.string.len + 1);
memcpy(content->val.string.val, v.val.string.val, v.val.string.len);
content->val.string.val[v.val.string.len] = '\0';
found_content = true;
elog(DEBUG1, "Content value: %s", content->val.string.val);
}
else {
elog(DEBUG1, "Unknown key: %.7s", v.val.string.val);
}
}
else if (strncmp(v.val.string.val, "template", 8) == 0)
{
token = JsonbIteratorNext(&it, &v, true);
if (token == WJB_VALUE && v.type == jbvString)
{
/* Create a proper copy of the template string */
template = palloc(sizeof(JsonbValue));
template->type = jbvString;
template->val.string.len = v.val.string.len;
template->val.string.val = palloc(v.val.string.len + 1);
memcpy(template->val.string.val, v.val.string.val, v.val.string.len);
template->val.string.val[v.val.string.len] = '\0';
found_template = true;
elog(DEBUG1, "Template value: %s", template->val.string.val);
}
else {
elog(DEBUG1, "Unknown key: %.8s", v.val.string.val);
}
}
else if (strncmp(v.val.string.val, "context", 7) == 0)
{
token = JsonbIteratorNext(&it, &v, true);
if (token == WJB_VALUE && v.type == jbvBinary)
{
include_context = palloc(sizeof(JsonbValue));
*include_context = v;
}
else {
elog(DEBUG1, "Unknown key: %.7s", v.val.string.val);
}
} else {
elog(DEBUG1, "Unknown key: %.7s", v.val.string.val);
}
}
}
if (found_content)
{
/* Plain text inclusion */
appendStringInfoString(result, content->val.string.val);
pfree(content);
}
else if (found_template)
{
/* Template inclusion */
const char *template_ptr = template->val.string.val;
TemplateNode *include_root = NULL;
TemplateErrorCode error_code = TEMPLATE_ERROR_NONE;
TemplateConfig config = template_default_config(context);
/* Parse the included template */
include_root = template_parse(context, &template_ptr, &config, false, &error_code);
if (include_root)
{
if (include_context)
{
/* Use separate context */
Jsonb *context_jsonb = JsonbValueToJsonb(include_context);
render_template(include_root, context_jsonb, result, context);
pfree(include_context);
}
else
{
/* Use shared root context */
render_template(include_root, define, result, context);
}
template_free_node(include_root);
}
pfree(template);
}
else
{
elog(WARNING, "Include data must have either 'content' or 'template' field");
}
}
else
{
elog(WARNING, "Include data must be an object");
}
/* Free the include data */
pfree(include_data);
}
else
{
elog(WARNING, "Include data not found or invalid type");
}
}
else
{
elog(WARNING, "Include key is not set");
}
break;
default:
elog(WARNING, "Unknown template node type: %d", current->type);
break;
}
elog(LOG, "%s", debug_msg);
current = current->next;
} }
elog(DEBUG1, "%s", debug_msg);
current = current->next;
} }
PG_CATCH();
{
ErrorData *edata = CopyErrorData();
elog(WARNING, "Error rendering template {\"message\":\"%s\", \"detail\":\"%s\", \"hint\":\"%s\", \"context\":\"%s\"}", edata->message, edata->detail ? edata->detail : "none", edata->hint ? edata->hint : "none", edata->context ? edata->context : "none");
FreeErrorData(edata);
FlushErrorState();
}
PG_END_TRY();
pfree(debug_msg); pfree(debug_msg);
} }
@@ -2254,4 +2421,4 @@ pg_jsonb_get_by_path(PG_FUNCTION_ARGS)
/* Return the result as a new Jsonb */ /* Return the result as a new Jsonb */
PG_RETURN_JSONB_P(JsonbValueToJsonb(&tmp_val)); PG_RETURN_JSONB_P(JsonbValueToJsonb(&tmp_val));
} }

View File

@@ -4,4 +4,5 @@ BEGIN;
\ir test_render_exec.sql \ir test_render_exec.sql
\ir test_render_interpolate.sql \ir test_render_interpolate.sql
\ir test_render_section.sql \ir test_render_section.sql
ROLLBACK; \ir test_render_include.sql
ROLLBACK;

View File

@@ -0,0 +1,197 @@
-- Test include tag functionality
DO $$
DECLARE
result text;
total_tests integer := 0;
passed_tests integer := 0;
BEGIN
-- Test 1: Plain text inclusion
total_tests := total_tests + 1;
BEGIN
result := hemar.render(
'{
"include": {
"inner_template": {
"content": "<p>Hello World</p>"
}
}
}'::jsonb,
$hemar${{ include inner_template }}$hemar$
);
IF result = '<p>Hello World</p>' THEN
RAISE NOTICE 'Test %: Plain text inclusion works correctly', total_tests;
passed_tests := passed_tests + 1;
ELSE
RAISE WARNING 'Test %: failed, Expected "<p>Hello World</p>", got "%"', total_tests, result;
END IF;
EXCEPTION WHEN OTHERS THEN
RAISE WARNING 'Test %: Plain text inclusion: FAILED with error: %', total_tests, SQLERRM;
END;
-- Test 2: Template with separate context
total_tests := total_tests + 1;
result := hemar.render(
'{
"include": {
"inner_template": {
"template": "Hello {{ name }}!",
"context": {
"name": "John"
}
}
}
}'::jsonb,
$hemar${{ include inner_template }}$hemar$
);
IF result = 'Hello John!' THEN
RAISE NOTICE 'Test %: Template with separate context works correctly', total_tests;
passed_tests := passed_tests + 1;
ELSE
RAISE WARNING 'Test %: failed, Expected "Hello John!", got "%"', total_tests, result;
END IF;
-- Test 3: Template with shared context
total_tests := total_tests + 1;
result := hemar.render(
'{
"name": "John",
"include": {
"inner_template": {
"template": "Hello {{ name }}!"
}
}
}'::jsonb,
$hemar${{ include inner_template }}$hemar$
);
IF result = 'Hello John!' THEN
RAISE NOTICE 'Test % passed: Template with shared context works correctly', total_tests;
passed_tests := passed_tests + 1;
ELSE
RAISE WARNING 'Test % failed: Expected "Hello John!", got "%"', total_tests, result;
END IF;
-- Test 4: Nested includes
total_tests := total_tests + 1;
result := hemar.render(
'{
"include": {
"outer_template": {
"template": "Outer: {{ include inner_template }}",
"context": {
"include": {
"inner_template": {
"template": "Inner: {{ name }}",
"context": {
"name": "John"
}
}
}
}
}
}
}'::jsonb,
$hemar${{ include outer_template }}$hemar$
);
IF result = 'Outer: Inner: John' THEN
RAISE NOTICE 'Test % passed: Nested includes work correctly', total_tests;
passed_tests := passed_tests + 1;
ELSE
RAISE WARNING 'Test % failed: Expected "Outer: Inner: John", got "%"', total_tests, result;
END IF;
-- Test 5: Complex template with multiple includes
total_tests := total_tests + 1;
result := hemar.render(
'{
"include": {
"header": {
"content": "<header>Welcome</header>"
},
"content": {
"template": "Hello {{ user.name }}!",
"context": {
"user": {
"name": "John"
}
}
},
"footer": {
"template": "Copyright {{ year }}",
"context": {
"year": "2024"
}
}
}
}'::jsonb,
$hemar$Header: {{ include header }}
Content: {{ include content }}
Footer: {{ include footer }}$hemar$
);
IF result = 'Header: <header>Welcome</header>
Content: Hello John!
Footer: Copyright 2024' THEN
RAISE NOTICE 'Test % passed: Complex template with multiple includes works correctly', total_tests;
passed_tests := passed_tests + 1;
ELSE
RAISE WARNING 'Test % failed: Expected , got "%"', total_tests, result;
END IF;
-- Test 6: Error handling - missing include data
total_tests := total_tests + 1;
BEGIN
result := hemar.render(
'{{ include missing_template }}',
'{}'::jsonb
);
RAISE WARNING 'Test % failed: Should have raised an error for missing include data', total_tests;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Test % passed: Error handling for missing include data works correctly', total_tests;
passed_tests := passed_tests + 1;
END;
-- Test 7: Error handling - invalid include data
total_tests := total_tests + 1;
BEGIN
result := hemar.render(
'{
"include": {
"invalid_template": "not an object"
}
}'::jsonb,
'{{ include invalid_template }}'
);
IF result = '' THEN
RAISE NOTICE 'Test % passed: Error handling for invalid include data works correctly', total_tests;
passed_tests := passed_tests + 1;
ELSE
RAISE WARNING 'Test % failed: Expected "", got "%"', total_tests, result;
END IF;
EXCEPTION WHEN OTHERS THEN
RAISE WARNING 'Test % failed: Should have raised an error for invalid include data', total_tests;
END;
IF passed_tests = total_tests THEN
RAISE NOTICE '------------------------------------';
RAISE NOTICE 'SUMMARY: % of % template include tests passed (100%%)',
passed_tests, total_tests;
RAISE NOTICE '------------------------------------';
ELSE
RAISE WARNING '------------------------------------';
RAISE WARNING 'SUMMARY: % of % template include tests passed (%)',
passed_tests,
total_tests,
round((passed_tests::numeric / total_tests::numeric) * 100, 2) || '%';
RAISE WARNING '------------------------------------';
END IF;
IF passed_tests != total_tests THEN
RAISE EXCEPTION 'Tests failed: % of % template include tests did not pass', (total_tests - passed_tests), total_tests;
END IF;
END $$;