feat: hemar: included tag
This commit is contained in:
@@ -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>"
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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 */
|
||||||
@@ -1198,6 +1205,8 @@ render_template(TemplateNode *node, Jsonb *define, StringInfo result, MemoryCont
|
|||||||
|
|
||||||
elog(DEBUG1, "define: %s", JsonbToCString(NULL, &define->root, VARSIZE_ANY_EXHDR(define)));
|
elog(DEBUG1, "define: %s", JsonbToCString(NULL, &define->root, VARSIZE_ANY_EXHDR(define)));
|
||||||
|
|
||||||
|
PG_TRY();
|
||||||
|
{
|
||||||
while (current)
|
while (current)
|
||||||
{
|
{
|
||||||
snprintf(debug_msg, debug_msg_size, "Rendering node type: %.50s", tnt_to_string(current->type));
|
snprintf(debug_msg, debug_msg_size, "Rendering node type: %.50s", tnt_to_string(current->type));
|
||||||
@@ -1529,7 +1538,156 @@ render_template(TemplateNode *node, Jsonb *define, StringInfo result, MemoryCont
|
|||||||
|
|
||||||
case TEMPLATE_NODE_INCLUDE:
|
case TEMPLATE_NODE_INCLUDE:
|
||||||
snprintf(debug_msg, debug_msg_size, "%s INCLUDE: %.50s", debug_msg, current->value->include.key);
|
snprintf(debug_msg, debug_msg_size, "%s INCLUDE: %.50s", debug_msg, current->value->include.key);
|
||||||
/* We'll implement include rendering later */
|
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;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -1537,10 +1695,19 @@ render_template(TemplateNode *node, Jsonb *define, StringInfo result, MemoryCont
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
elog(DEBUG1, "%s", debug_msg);
|
elog(LOG, "%s", debug_msg);
|
||||||
|
|
||||||
current = current->next;
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
\ir test_render_include.sql
|
||||||
ROLLBACK;
|
ROLLBACK;
|
||||||
197
package/c/hemar/test/test_render_include.sql
Executable file
197
package/c/hemar/test/test_render_include.sql
Executable 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 $$;
|
||||||
Reference in New Issue
Block a user