diff --git a/package/c/hectic/docs/templater.md b/package/c/hectic/docs/templater.md index 8f4a816..3aa1453 100755 --- a/package/c/hectic/docs/templater.md +++ b/package/c/hectic/docs/templater.md @@ -94,32 +94,32 @@ Includes content from other templates. ```json // Separate context { - "include inner_template": [ - { + "include": { + "inner_template": { "template": "{{ field }}", "context": { "field": "value" } } - ] + } } // Shared root context { "field": "value", - "include inner_template": [ - { + "include": { + "inner_template": { "template": "{{ field }}" } - ] + } } // Plain text inclusion { "field": "value", - "include inner_template": [ - { + "include": { + "inner_template": { "content": "

value

" } - ] + } } ``` diff --git a/package/c/hemar/hemar.c b/package/c/hemar/hemar.c index 2510424..7183394 100755 --- a/package/c/hemar/hemar.c +++ b/package/c/hemar/hemar.c @@ -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 (brace_level == 0) { - break; + break; } *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; + // FIXME(yukkop): Are we realy need to trim this whitespaces? /* Trim trailing whitespace */ while (code_len > 0 && isspace((unsigned char)code_start[code_len - 1])) 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)); /* Check for closing brace */ @@ -1197,221 +1204,143 @@ render_template(TemplateNode *node, Jsonb *define, StringInfo result, MemoryCont char *debug_msg = palloc(debug_msg_size); 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)); - switch (current->type) + while (current) { - case TEMPLATE_NODE_TEXT: - snprintf(debug_msg, debug_msg_size, "%s TEXT: %.50s", debug_msg, current->value->text.content); - if (current->value->text.content) - { - /* Preserve whitespace in text nodes */ - appendStringInfoString(result, current->value->text.content); - } - break; - - case TEMPLATE_NODE_INTERPOLATE: - snprintf(debug_msg, debug_msg_size, "%s INTERPOLATE: %.50s", debug_msg, current->value->interpolate.key); - 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); + snprintf(debug_msg, debug_msg_size, "Rendering node type: %.50s", tnt_to_string(current->type)); + switch (current->type) + { + case TEMPLATE_NODE_TEXT: + snprintf(debug_msg, debug_msg_size, "%s TEXT: %.50s", debug_msg, current->value->text.content); + if (current->value->text.content) + { + /* Preserve whitespace in text nodes */ + appendStringInfoString(result, current->value->text.content); + } + break; + + case TEMPLATE_NODE_INTERPOLATE: + snprintf(debug_msg, debug_msg_size, "%s INTERPOLATE: %.50s", debug_msg, current->value->interpolate.key); + 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) { /* 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: - /* 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) - { - /* 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++) + /* iterate by string, where item is char */ { - /* Create a new context with the current character */ - JsonbValue char_val; - 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) + const char *str = value->val.string.val; + for (int i = 0; i < value->val.string.len; i++) { - if (token == WJB_KEY) - { - /* Add the key */ - pushJsonbValue(&parse_state, WJB_KEY, &v); - } - else if (token == WJB_VALUE) - { - /* Add the value */ - pushJsonbValue(&parse_state, WJB_VALUE, &v); - } - } - - /* 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 */ + /* Create a new context with the current character */ + JsonbValue char_val; + 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 *ctx_it = JsonbIteratorInit(&define->root); - JsonbIteratorToken ctx_token; - JsonbValue ctx_v; + JsonbIterator *it = JsonbIteratorInit(&define->root); + JsonbIteratorToken token; + 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 */ - 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 */ - 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.len = strlen(current->value->section.iterator); pushJsonbValue(&parse_state, WJB_KEY, &key_val); - pushJsonbValue(&parse_state, WJB_VALUE, &v); + pushJsonbValue(&parse_state, WJB_VALUE, &char_val); /* Finish the object */ 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); /* Free the temporary values */ + pfree(char_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 */ - while ((token = JsonbIteratorNext(&it, &v, true)) != WJB_END_OBJECT) + /* 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) { - 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; 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; 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, &v); - /* 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 */ + /* Finish the object */ res = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL); /* Convert to Jsonb */ @@ -1510,37 +1443,271 @@ render_template(TemplateNode *node, Jsonb *define, StringInfo result, MemoryCont /* Free the temporary values */ 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; - - default: - elog(WARNING, "Unsupported JSONB value type in section: %s", - jbv_type_to_string(value->type)); - break; + break; + + default: + elog(WARNING, "Unsupported JSONB value type in section: %s", + jbv_type_to_string(value->type)); + break; + } + + /* Free the value since it was allocated in our context */ + pfree(value); } + break; - /* Free the value since it was allocated in our context */ - pfree(value); - } - break; - - case TEMPLATE_NODE_INCLUDE: - snprintf(debug_msg, debug_msg_size, "%s INCLUDE: %.50s", debug_msg, current->value->include.key); - /* We'll implement include rendering later */ - break; - - default: - elog(WARNING, "Unknown template node type: %d", current->type); - break; + case TEMPLATE_NODE_INCLUDE: + snprintf(debug_msg, debug_msg_size, "%s INCLUDE: %.50s", debug_msg, current->value->include.key); + if (current->value->include.key) + { + /* Construct the path with include prefix */ + char *include_path = psprintf("include.%s", current->value->include.key); + elog(DEBUG1, "Include path: %s", include_path); + + /* Get the include data from the context */ + JsonbValue *include_data = jsonb_get_by_path_internal(define, include_path, context); + + elog(DEBUG1, "Include data: %s", JsonbToCString(NULL, &JsonbValueToJsonb(include_data)->root, VARSIZE_ANY_EXHDR(JsonbValueToJsonb(include_data)))); + + 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); } @@ -2254,4 +2421,4 @@ pg_jsonb_get_by_path(PG_FUNCTION_ARGS) /* Return the result as a new Jsonb */ PG_RETURN_JSONB_P(JsonbValueToJsonb(&tmp_val)); -} \ No newline at end of file +} diff --git a/package/c/hemar/test/mod.sql b/package/c/hemar/test/mod.sql index f40cd7b..e889cb6 100755 --- a/package/c/hemar/test/mod.sql +++ b/package/c/hemar/test/mod.sql @@ -4,4 +4,5 @@ BEGIN; \ir test_render_exec.sql \ir test_render_interpolate.sql \ir test_render_section.sql -ROLLBACK; \ No newline at end of file + \ir test_render_include.sql +ROLLBACK; diff --git a/package/c/hemar/test/test_render_include.sql b/package/c/hemar/test/test_render_include.sql new file mode 100755 index 0000000..2a63bdd --- /dev/null +++ b/package/c/hemar/test/test_render_include.sql @@ -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": "

Hello World

" + } + } + }'::jsonb, + $hemar${{ include inner_template }}$hemar$ + ); + + IF result = '

Hello World

' THEN + RAISE NOTICE 'Test %: Plain text inclusion works correctly', total_tests; + passed_tests := passed_tests + 1; + ELSE + RAISE WARNING 'Test %: failed, Expected "

Hello World

", 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": "
Welcome
" + }, + "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:
Welcome
+ 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 $$;