feat: hemar: section checkpoint

This commit is contained in:
2025-05-18 10:27:38 +00:00
parent 19ea8992a0
commit 3493c14686
3 changed files with 333 additions and 4 deletions

View File

@@ -1196,10 +1196,11 @@ render_template(TemplateNode *node, Jsonb *define, StringInfo result, MemoryCont
int debug_msg_size = 4096; int debug_msg_size = 4096;
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)));
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));
switch (current->type) switch (current->type)
{ {
case TEMPLATE_NODE_TEXT: case TEMPLATE_NODE_TEXT:
@@ -1216,7 +1217,6 @@ render_template(TemplateNode *node, Jsonb *define, StringInfo result, MemoryCont
if (current->value->interpolate.key) if (current->value->interpolate.key)
{ {
/* Get the value from the JSONB context using the path */ /* Get the value from the JSONB context using the path */
elog(DEBUG1, "define: %s", JsonbToCString(NULL, &define->root, VARSIZE_ANY_EXHDR(define)));
value = jsonb_get_by_path_internal(define, current->value->interpolate.key, context); value = jsonb_get_by_path_internal(define, current->value->interpolate.key, context);
if (value != NULL) if (value != NULL)
@@ -1263,6 +1263,10 @@ render_template(TemplateNode *node, Jsonb *define, StringInfo result, MemoryCont
pfree(value); pfree(value);
} }
} }
else
{
elog(WARNING, "Interpolation key is not set");
}
break; break;
case TEMPLATE_NODE_EXECUTE: case TEMPLATE_NODE_EXECUTE:
@@ -1272,7 +1276,233 @@ render_template(TemplateNode *node, Jsonb *define, StringInfo result, MemoryCont
case TEMPLATE_NODE_SECTION: case TEMPLATE_NODE_SECTION:
snprintf(debug_msg, debug_msg_size, "%s SECTION: %.50s", debug_msg, current->value->section.iterator); snprintf(debug_msg, debug_msg_size, "%s SECTION: %.50s", debug_msg, current->value->section.iterator);
/* We'll implement section rendering later */ 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 */
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)
{
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 */
JsonbValue *new_context = palloc(sizeof(JsonbValue));
new_context->type = jbvObject;
/* 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 */
JsonbValue key_val;
key_val.type = jbvString;
key_val.val.string.val = pstrdup(ctx_v.val.string.val);
key_val.val.string.len = ctx_v.val.string.len;
JsonbValueToJsonb(&key_val);
}
else if (ctx_token == WJB_VALUE)
{
/* Add the value */
JsonbValueToJsonb(&ctx_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);
JsonbValueToJsonb(&key_val);
JsonbValueToJsonb(&v);
/* Render the section body with the new context */
render_template(current->value->section.body, JsonbValueToJsonb(new_context), result, context);
/* Free the temporary values */
pfree(new_context);
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 */
JsonbValue *new_context = palloc(sizeof(JsonbValue));
new_context->type = jbvObject;
/* 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 */
JsonbValue key_val;
key_val.type = jbvString;
key_val.val.string.val = pstrdup(ctx_v.val.string.val);
key_val.val.string.len = ctx_v.val.string.len;
JsonbValueToJsonb(&key_val);
}
else if (ctx_token == WJB_VALUE)
{
/* Add the value */
JsonbValueToJsonb(&ctx_v);
}
}
/* Add the iterator value (key) */
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);
JsonbValueToJsonb(&key_val);
JsonbValueToJsonb(&v);
/* Get the value */
token = JsonbIteratorNext(&it, &v, true);
/* Add the value */
JsonbValueToJsonb(&v);
/* Render the section body with the new context */
render_template(current->value->section.body, JsonbValueToJsonb(new_context), result, context);
/* Free the temporary values */
pfree(new_context);
}
}
}
}
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; break;
case TEMPLATE_NODE_INCLUDE: case TEMPLATE_NODE_INCLUDE:

View File

@@ -3,4 +3,5 @@ BEGIN;
\ir test_template_parser.sql \ir test_template_parser.sql
\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
ROLLBACK; ROLLBACK;

View File

@@ -0,0 +1,98 @@
-- Test section rendering
DO $$
DECLARE
test_result text;
expected text;
BEGIN
-- Test 1: String iteration
test_result := hemar.render(
'{"text": "Hello"}'::jsonb,
'{{for char in text}}{{char}}{{end}}'
);
expected := 'Hello';
ASSERT test_result = expected, format('Test 1: String iteration: FAILED. Expected "%s", got "%s"', expected, test_result);
RAISE NOTICE 'Test 1: String iteration: PASSED';
-- Test 2: Array iteration
test_result := hemar.render(
'{"numbers": [1, 2, 3]}'::jsonb,
'{{for num in numbers}}{{num}}{{end}}'
);
expected := '123';
ASSERT test_result = expected, format('Test 2: Array iteration: FAILED. Expected "%s", got "%s"', expected, test_result);
RAISE NOTICE 'Test 2: Array iteration: PASSED';
-- Test 3: Object iteration
test_result := hemar.render(
'{"user": {"name": "John", "age": 30}}'::jsonb,
'{{for item in user}}{{item.key}}: {{item.value}}{{end}}'
);
expected := 'name: Johnage: 30';
ASSERT test_result = expected, format('Test 3: Object iteration: FAILED. Expected "%s", got "%s"', expected, test_result);
RAISE NOTICE 'Test 3: Object iteration: PASSED';
-- Test 4: Boolean condition (true)
test_result := hemar.render(
'{"show": true}'::jsonb,
'{{for show in show}}Content{{end}}'
);
expected := 'Content';
ASSERT test_result = expected, format('Test 4: Boolean condition (true): FAILED. Expected "%s", got "%s"', expected, test_result);
RAISE NOTICE 'Test 4: Boolean condition (true): PASSED';
-- Test 5: Boolean condition (false)
test_result := hemar.render(
'{"show": false}'::jsonb,
'{{for show in show}}Content{{end}}'
);
expected := '';
ASSERT test_result = expected, format('Test 5: Boolean condition (false): FAILED. Expected "%s", got "%s"', expected, test_result);
RAISE NOTICE 'Test 5: Boolean condition (false): PASSED';
-- Test 6: Nested sections
test_result := hemar.render(
'{"items": [{"name": "Item 1", "tags": ["tag1", "tag2"]}, {"name": "Item 2", "tags": ["tag3"]}]}'::jsonb,
'{{for item in items}}{{item.name}}: {{for tag in item.tags}}{{tag}} {{end}}{{end}}'
);
expected := 'Item 1: tag1 tag2 Item 2: tag3 ';
ASSERT test_result = expected, format('Test 6: Nested sections: FAILED. Expected "%s", got "%s"', expected, test_result);
RAISE NOTICE 'Test 6: Nested sections: PASSED';
-- Test 7: Section with context
test_result := hemar.render(
'{"items": ["a", "b"], "prefix": "Item: "}'::jsonb,
'{{for item in items}}{{prefix}}{{item}}{{end}}'
);
expected := 'Item: aItem: b';
ASSERT test_result = expected, format('Test 7: Section with context: FAILED. Expected "%s", got "%s"', expected, test_result);
RAISE NOTICE 'Test 7: Section with context: PASSED';
-- Test 8: Empty array
test_result := hemar.render(
'{"items": []}'::jsonb,
'{{for item in items}}{{item}}{{end}}'
);
expected := '';
ASSERT test_result = expected, format('Test 8: Empty array: FAILED. Expected "%s", got "%s"', expected, test_result);
RAISE NOTICE 'Test 8: Empty array: PASSED';
-- Test 9: Empty object
test_result := hemar.render(
'{"user": {}}'::jsonb,
'{{for key in user}}{{key}}{{end}}'
);
expected := '';
ASSERT test_result = expected, format('Test 9: Empty object: FAILED. Expected "%s", got "%s"', expected, test_result);
RAISE NOTICE 'Test 9: Empty object: PASSED';
-- Test 10: Invalid collection type (number)
test_result := hemar.render(
'{"number": 42}'::jsonb,
'{{for item in number}}{{item}}{{end}}'
);
expected := '';
ASSERT test_result = expected, format('Test 10: Invalid collection type: FAILED. Expected "%s", got "%s"', expected, test_result);
RAISE NOTICE 'Test 10: Invalid collection type: PASSED (error raised as expected)';
RAISE NOTICE 'All section rendering tests completed successfully!';
END $$;