From 725f6eea30b79e20d6eb527d8cbcaee1474cf60c Mon Sep 17 00:00:00 2001 From: yukkop Date: Thu, 15 May 2025 23:57:18 +0000 Subject: [PATCH] test: `hemar`: puh --- package/c/hemar/hemar.c | 248 +++-- package/c/hemar/test/test_template_parser.sql | 846 +++++++++--------- 2 files changed, 610 insertions(+), 484 deletions(-) diff --git a/package/c/hemar/hemar.c b/package/c/hemar/hemar.c index a6a537a..33b40de 100755 --- a/package/c/hemar/hemar.c +++ b/package/c/hemar/hemar.c @@ -168,12 +168,9 @@ template_error_to_string(TemplateErrorCode code, TemplateConfig *config) strcat(message, "` keyword in section block"); return message; case TEMPLATE_ERROR_NO_BEGIN_IN_SECTION: - message = "Not found `"; - strcat(message, config->Syntax.Section.begin); - strcat(message, "` keyword in section block"); - return message; + return "Missing end tag for section"; case TEMPLATE_ERROR_UNEXPECTED_SECTION_END: - return "Unexpected section end"; + return "Unexpected section end or missing end tag"; case TEMPLATE_ERROR_NESTED_INCLUDE: return "Nested include"; case TEMPLATE_ERROR_NESTED_EXECUTE: @@ -201,7 +198,7 @@ template_default_config(MemoryContext context) config.Syntax.Braces.close = "}}"; config.Syntax.Section.control = "for "; config.Syntax.Section.source = "in "; - config.Syntax.Section.begin = "do "; + config.Syntax.Section.begin = ""; /* No longer used, but keep for backward compatibility */ config.Syntax.Interpolate.invoke = ""; config.Syntax.Include.invoke = "include "; config.Syntax.Execute.invoke = "exec "; @@ -336,6 +333,11 @@ template_parse_interpolation(MemoryContext context, const char **s_ptr, } key_len = *s - key_start; + + /* Trim trailing whitespace if it was terminated by whitespace */ + while (key_len > 0 && isspace((unsigned char)key_start[key_len - 1])) + key_len--; + node->value->interpolate.key = MemoryContextStrdup(context, pnstrdup(key_start, key_len)); elog(DEBUG1, "TPI: Parsing: %s", node->value->interpolate.key); @@ -423,7 +425,7 @@ template_parse_section(MemoryContext context, const char **s_ptr, while (**s != '\0') { if (isspace((unsigned char)**s) || - strncmp(*s, config->Syntax.Section.begin, strlen(config->Syntax.Section.begin)) == 0) + strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0) break; if (strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0) @@ -434,14 +436,6 @@ template_parse_section(MemoryContext context, const char **s_ptr, return NULL; } - if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0) - { - if (error_code) - *error_code = TEMPLATE_ERROR_NO_BEGIN_IN_SECTION; - template_free_node(node); - return NULL; - } - (*s)++; } @@ -449,60 +443,72 @@ template_parse_section(MemoryContext context, const char **s_ptr, node->value->section.collection = MemoryContextStrdup(context, pnstrdup(collection_start, collection_len)); elog(DEBUG1, "TPS: Parsed section collection: %s", node->value->section.collection); - /* Check for 'do' keyword */ + /* Skip whitespace before closing brace */ *s = skip_whitespace(*s); - // TODO: why check begin second time, first in while - if (strncmp(*s, config->Syntax.Section.begin, strlen(config->Syntax.Section.begin)) != 0) + + /* Check for closing brace */ + if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) != 0) { if (error_code) - *error_code = TEMPLATE_UNEXPECTED_OPEN_BRACES_AFFTER_SECTION_SOURCE; + *error_code = TEMPLATE_ERROR_UNEXPECTED_SECTION_END; template_free_node(node); return NULL; } - *s += strlen(config->Syntax.Section.begin); - *s = skip_whitespace(*s); + /* Move past the closing brace */ + *s += strlen(config->Syntax.Braces.close); - /* Check if there's a closing brace right after 'do' */ - if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0) - { - /* Empty section body */ - elog(DEBUG1, "TPS: Parsed empty section body"); - *s_ptr = *s + strlen(config->Syntax.Braces.close); - node->value->section.body = NULL; - return node; - } - - /* Parse the body as a normal template */ + /* Start of the body content */ const char *body_start = *s; - const char *original_s = *s; - - int inner_braces_opened_count = 0; + const char *end_tag_start = NULL; + int nesting_level = 1; /* Start at 1, our current section */ - /* Find the end of the section */ - while (**s) { - // s = {% a %} %} - elog(DEBUG2, "TPS: Step, braces opened: %d, s: %s", inner_braces_opened_count, *s); - if (strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0) { - elog(DEBUG2, "TPS: inner_braces_opened_count++"); - inner_braces_opened_count++; - } - if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0) { - if (inner_braces_opened_count > 0) { - elog(DEBUG2, "TPS: inner_braces_opened_count--"); - inner_braces_opened_count--; - } - else { - elog(DEBUG2, "TPS: exit"); - break; - } - } + elog(DEBUG1, "TPS: Starting to parse section body at position: %s", body_start); + elog(DEBUG1, "TPS: Looking for end tag"); + + /* Find the matching end tag, accounting for nested sections */ + while (**s != '\0') + { + if (strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0) + { + /* We found an opening brace */ + const char *tag_start = *s + strlen(config->Syntax.Braces.open); + const char *tag_ptr = tag_start; + + /* Skip whitespace after opening brace */ + while (*tag_ptr && isspace((unsigned char)*tag_ptr)) + tag_ptr++; + + /* Check if this is a new section tag */ + if (strncmp(tag_ptr, config->Syntax.Section.control, strlen(config->Syntax.Section.control)) == 0) + { + /* Found a nested section, increase nesting level */ + nesting_level++; + elog(DEBUG1, "TPS: Found nested section, nesting level: %d, at position: %s", nesting_level, *s); + } + /* Check if this is an end tag */ + else if (strncmp(tag_ptr, "end", 3) == 0) + { + /* Found an end tag, decrease nesting level */ + nesting_level--; + elog(DEBUG1, "TPS: Found end tag, nesting level: %d, at position: %s", nesting_level, *s); + + if (nesting_level == 0) + { + /* This is our matching end tag */ + end_tag_start = *s; + break; + } + } + } + (*s)++; } - if (!**s) + /* Check if we found a matching end tag */ + if (nesting_level > 0 || !end_tag_start) { - /* Unexpected end of string before closing brace */ + elog(WARNING, "TPS: No matching end tag found for section, nesting level: %d", nesting_level); if (error_code) *error_code = TEMPLATE_ERROR_UNEXPECTED_SECTION_END; template_free_node(node); @@ -510,7 +516,7 @@ template_parse_section(MemoryContext context, const char **s_ptr, } /* Extract the body content */ - size_t body_len = *s - body_start; + size_t body_len = end_tag_start - body_start; char *body_content = pnstrdup(body_start, body_len); elog(DEBUG1, "TPS: Section body content: %s", body_content); @@ -530,8 +536,26 @@ template_parse_section(MemoryContext context, const char **s_ptr, pfree(body_content); node->value->section.body = body_node; + /* Skip past the end tag */ + *s = end_tag_start; + *s += strlen(config->Syntax.Braces.open); + *s = skip_whitespace(*s); + *s += 3; /* Skip "end" */ + *s = skip_whitespace(*s); + + /* Check for closing brace of end tag */ + if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) != 0) + { + elog(WARNING, "TPS: No closing brace for end tag at position: %s", *s); + if (error_code) + *error_code = TEMPLATE_ERROR_UNEXPECTED_SECTION_END; + template_free_node(node); + return NULL; + } + /* Set the pointer to after the closing brace */ *s_ptr = *s + strlen(config->Syntax.Braces.close); + elog(DEBUG1, "TPS: Successfully parsed section, returning at position: %s", *s_ptr); return node; } @@ -574,6 +598,11 @@ template_parse_include(MemoryContext context, const char **s_ptr, } include_len = *s - include_start; + + /* Trim trailing whitespace if it was terminated by whitespace */ + while (include_len > 0 && isspace((unsigned char)include_start[include_len - 1])) + include_len--; + node->value->include.key = MemoryContextStrdup(context, pnstrdup(include_start, include_len)); *s = skip_whitespace(*s); @@ -612,23 +641,55 @@ template_parse_execute(MemoryContext context, const char **s_ptr, *s = skip_whitespace(*s); code_start = *s; + /* Track quote state to handle SQL content properly */ + bool in_single_quote = false; + bool in_double_quote = false; + bool escaped = false; + while (**s != '\0') { - if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0) - break; - - if (strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0) - { - if (error_code) - *error_code = TEMPLATE_ERROR_NESTED_EXECUTE; - template_free_node(node); - return NULL; + /* Handle escaping */ + if (**s == '\\') { + escaped = !escaped; + (*s)++; + continue; } + /* Handle quotes - only toggle quote state if not escaped */ + if (!escaped) { + if (**s == '\'') { + in_single_quote = !in_single_quote; + } else if (**s == '"') { + in_double_quote = !in_double_quote; + } + } + + /* Only check for closing braces when not inside quotes */ + if (!in_single_quote && !in_double_quote) { + if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0) { + break; + } + + /* Check for nested opening braces */ + if (strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0) { + if (error_code) + *error_code = TEMPLATE_ERROR_NESTED_EXECUTE; + template_free_node(node); + return NULL; + } + } + + /* Reset escaped flag after processing a character */ + escaped = false; (*s)++; } code_len = *s - code_start; + + /* Trim trailing whitespace */ + while (code_len > 0 && isspace((unsigned char)code_start[code_len - 1])) + code_len--; + node->value->execute.code = MemoryContextStrdup(context, pnstrdup(code_start, code_len)); /* Check for closing brace */ @@ -710,25 +771,52 @@ template_parse(MemoryContext context, const char **s, const TemplateConfig *conf tag_prefix = *s + strlen(config->Syntax.Braces.open); tag_prefix = skip_whitespace(tag_prefix); - /* Determine tag type by prefix */ - if (strncmp(tag_prefix, config->Syntax.Section.control, strlen(config->Syntax.Section.control)) == 0) - { + /* Find the longest matching prefix to determine tag type */ + typedef struct { + const char *prefix; + int tag_type; + } PrefixMatch; + + PrefixMatch matches[] = { + {config->Syntax.Section.control, 1}, + {config->Syntax.Include.invoke, 2}, + {config->Syntax.Execute.invoke, 3}, + {config->Syntax.Interpolate.invoke, 4} + }; + + int matched_type = 0; + size_t max_length = 0; + + /* Find longest match (in case when one prefix is part of another) */ + for (int i = 0; i < 4; i++) { + if (strncmp(tag_prefix, matches[i].prefix, strlen(matches[i].prefix)) == 0) { + /* >= because one of the prefixes may be empty */ + if (strlen(matches[i].prefix) >= max_length) { + max_length = strlen(matches[i].prefix); + matched_type = matches[i].tag_type; + } + } + } + + /* Choose the tag parser based on the matched type */ + if (matched_type == 1) { + /* Section tag */ + elog(LOG, "TPE: Parsing section tag at position: %.50s", *s); tag_node = template_parse_section(context, s, config, error_code); - } - else if (strncmp(tag_prefix, config->Syntax.Include.invoke, strlen(config->Syntax.Include.invoke)) == 0) - { + } else if (matched_type == 2) { + /* Include tag */ + elog(LOG, "TPE: Parsing include tag at position: %.50s", *s); tag_node = template_parse_include(context, s, config, error_code); - } - else if (strncmp(tag_prefix, config->Syntax.Execute.invoke, strlen(config->Syntax.Execute.invoke)) == 0) - { + } else if (matched_type == 3) { + /* Execute tag */ + elog(LOG, "TPE: Parsing include tag at position: %.50s", *s); tag_node = template_parse_execute(context, s, config, error_code); - } - else if (strncmp(tag_prefix, config->Syntax.Interpolate.invoke, strlen(config->Syntax.Interpolate.invoke)) == 0) - { + } else if (matched_type == 4) { + /* Interpolation tag */ + elog(LOG, "TPE: Parsing interpolation tag at position: %.50s", *s); tag_node = template_parse_interpolation(context, s, config, error_code); - } - else - { + } else { + /* Unknown tag type */ if (error_code) *error_code = TEMPLATE_ERROR_UNKNOWN_TAG; template_free_node(root); diff --git a/package/c/hemar/test/test_template_parser.sql b/package/c/hemar/test/test_template_parser.sql index 8d83f44..88606f6 100755 --- a/package/c/hemar/test/test_template_parser.sql +++ b/package/c/hemar/test/test_template_parser.sql @@ -1,405 +1,443 @@ --- Test file for hemar template parser --- Run with: psql -f test_template_parser.sql - --- Load extension if not already loaded --- CREATE EXTENSION IF NOT EXISTS hemar; - --- Create test function to validate template parsing -CREATE OR REPLACE FUNCTION test_template_parse(template_text text, expected_structure text) RETURNS boolean AS $$ -DECLARE - parsed_result text; - passed boolean; -BEGIN - BEGIN - parsed_result := hemar.parse(template_text); - passed := position(expected_structure in parsed_result) > 0; - EXCEPTION - WHEN OTHERS THEN - passed := false; - END; - - IF NOT passed THEN - RAISE WARNING 'Template parsing test failed!'; - RAISE WARNING 'Template: %', template_text; - RAISE WARNING 'Expected to find: %', expected_structure; - RAISE WARNING 'Actual result: %', parsed_result; - END IF; - - RETURN passed; -END; -$$ LANGUAGE plpgsql; - --- Run the tests -DO $$ -DECLARE - total_tests integer := 0; - passed_tests integer := 0; - result boolean; -BEGIN - RAISE NOTICE 'Starting template parser tests...'; - - -- Test 1: Simple interpolation - total_tests := total_tests + 1; - result := test_template_parse( - '{{ simple_var }}', - 'INTERPOLATE: "simple_var"' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Simple interpolation - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Simple interpolation - FAILED', total_tests; - END IF; - - -- Test 2: Interpolation with surrounding text - total_tests := total_tests + 1; - result := test_template_parse( - 'Hello, {{ name }}!', - 'TEXT: "Hello, " -INTERPOLATE: "name" -TEXT: "!"' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Interpolation with surrounding text - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Interpolation with surrounding text - FAILED', total_tests; - END IF; - - -- Test 3: Simple section (for loop) - total_tests := total_tests + 1; - result := test_template_parse( - '{{ for item in items do }}{{ item }}{{ end }}', - 'SECTION: iterator="item", collection="items"' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Simple section (for loop) - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Simple section (for loop) - FAILED', total_tests; - END IF; - - -- Test 4: Section with nested interpolation - total_tests := total_tests + 1; - result := test_template_parse( - '{{ for item in items do }}Name: {{ item.name }}{{ end }}', - 'SECTION: iterator="item", collection="items" - TEXT: "Name: " - INTERPOLATE: "item.name"' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Section with nested interpolation - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Section with nested interpolation - FAILED', total_tests; - END IF; - - -- Test 5: Nested sections - total_tests := total_tests + 1; - result := test_template_parse( - '{{ for item in items do }}{{ for subitem in item.subitems do }}{{ subitem }}{{ end }}{{ end }}', - 'SECTION: iterator="item", collection="items" - SECTION: iterator="subitem", collection="item.subitems" - INTERPOLATE: "subitem"' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Nested sections - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Nested sections - FAILED', total_tests; - END IF; - - -- Test 6: Include tag - total_tests := total_tests + 1; - result := test_template_parse( - '{{ include template_name }}', - 'INCLUDE: "template_name"' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Include tag - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Include tag - FAILED', total_tests; - END IF; - - -- Test 7: Execute tag - total_tests := total_tests + 1; - result := test_template_parse( - '{{ exec RETURN my_function(arg1, arg2) }}', - 'EXECUTE: "RETURN my_function(arg1, arg2)"' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Execute tag - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Execute tag - FAILED', total_tests; - END IF; - - -- Test 8: Complex mixed template - total_tests := total_tests + 1; - result := test_template_parse( - '
{{ for item in items do }}

{{ item.name }}

{{ include item.template }}{{ end }}
', - 'TEXT: "
" -SECTION: iterator="item", collection="items" - TEXT: "

" - INTERPOLATE: "item.name" - TEXT: "

" - INCLUDE: "item.template" -TEXT: "
"' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Complex mixed template - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Complex mixed template - FAILED', total_tests; - END IF; - - -- Test 9: Execute tag with complex SQL - total_tests := total_tests + 1; - result := test_template_parse( - '{{ exec - IF condition THEN - RETURN ''value1''; - ELSE - RETURN ''value2''; - END IF; - }}', - 'EXECUTE: "IF condition THEN' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Execute tag with complex SQL - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Execute tag with complex SQL - FAILED', total_tests; - END IF; - - -- Test 10: Whitespace handling - total_tests := total_tests + 1; - result := test_template_parse( - '{{ spaced_var }}', - 'INTERPOLATE: "spaced_var"' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Whitespace handling - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Whitespace handling - FAILED', total_tests; - END IF; - - -- Test 11: Multiple consecutive tags - total_tests := total_tests + 1; - result := test_template_parse( - '{{ var1 }}{{ var2 }}{{ var3 }}', - 'INTERPOLATE: "var1" -INTERPOLATE: "var2" -INTERPOLATE: "var3"' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Multiple consecutive tags - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Multiple consecutive tags - FAILED', total_tests; - END IF; - - -- Test 12: Section with multiple nested elements - total_tests := total_tests + 1; - result := test_template_parse( - '{{ for item in items do }} -

{{ item.title }}

-

{{ item.description }}

- {{ include item.footer }} - {{ end }}', - 'SECTION: iterator="item", collection="items" - TEXT: " -

" - INTERPOLATE: "item.title" - TEXT: "

-

" - INTERPOLATE: "item.description" - TEXT: "

- " - INCLUDE: "item.footer" - TEXT: " - "' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Section with multiple nested elements - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Section with multiple nested elements - FAILED', total_tests; - END IF; - - -- Test 13: Empty template - total_tests := total_tests + 1; - result := test_template_parse( - '', - 'TEXT: ""' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Empty template - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Empty template - FAILED', total_tests; - END IF; - - -- Test 14: Just text, no tags - total_tests := total_tests + 1; - result := test_template_parse( - 'Just plain text, no tags here.', - 'TEXT: "Just plain text, no tags here."' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Just text, no tags - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Just text, no tags - FAILED', total_tests; - END IF; - - -- Test 15: Complex example from documentation - total_tests := total_tests + 1; - result := test_template_parse( - '
text before
- - {{ include inner_template }} - - {{ name }} - - {{ for item in array do }} - some text: {{ name2 }} - {{ item.name }} - {{ end }} - -
code insertion:
- {{ exec - context + ''{"name3": "zalupa"}''; - - IF context->condition THEN - RAISE INFO ''some log''; - - RETURN ''some text''; - END - RETURN ''some other text''; - }} - - ', - 'TEXT: "
text before
- - " -INCLUDE: "inner_template" -TEXT: " - - " -INTERPOLATE: "name" -TEXT: " - - " -SECTION: iterator="item", collection="array"' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Complex example from documentation - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Complex example from documentation - FAILED', total_tests; - END IF; - - -- Test 16: Multiple nested sections - total_tests := total_tests + 1; - result := test_template_parse( - '{{ for a in items do }} - {{ for b in a.items do }} - {{ for c in b.items do }} - {{ c.name }} - {{ end }} - {{ end }} - {{ end }}', - 'SECTION: iterator="a", collection="items" - TEXT: " - " - SECTION: iterator="b", collection="a.items" - TEXT: " - " - SECTION: iterator="c", collection="b.items" - TEXT: " - " - INTERPOLATE: "c.name" - TEXT: " - "' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Multiple nested sections - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Multiple nested sections - FAILED', total_tests; - END IF; - - -- Test 17: Interpolation with special characters - total_tests := total_tests + 1; - result := test_template_parse( - '{{ special@field }}', - 'INTERPOLATE: "special@field"' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Interpolation with special characters - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Interpolation with special characters - FAILED', total_tests; - END IF; - - -- Test 18: Section with complex iterator and collection names - total_tests := total_tests + 1; - result := test_template_parse( - '{{ for complex_item.with.dots in complex_collection[0].items do }}{{ end }}', - 'SECTION: iterator="complex_item.with.dots", collection="complex_collection[0].items"' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Section with complex iterator and collection names - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Section with complex iterator and collection names - FAILED', total_tests; - END IF; - - -- Test 19: Include with complex path - total_tests := total_tests + 1; - result := test_template_parse( - '{{ include templates[0].nested.path }}', - 'INCLUDE: "templates[0].nested.path"' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Include with complex path - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Include with complex path - FAILED', total_tests; - END IF; - - -- Test 20: Execute with complex SQL and quotes - total_tests := total_tests + 1; - result := test_template_parse( - '{{ exec SELECT ''text with "double" quotes'' AS result; }}', - 'EXECUTE: "SELECT ''text with "double" quotes'' AS result;"' - ); - IF result THEN - passed_tests := passed_tests + 1; - RAISE NOTICE 'Test %: Execute with complex SQL and quotes - PASSED', total_tests; - ELSE - RAISE WARNING 'Test %: Execute with complex SQL and quotes - FAILED', total_tests; - END IF; - - -- Print summary - IF passed_tests = total_tests THEN - RAISE NOTICE '------------------------------------'; - RAISE NOTICE 'SUMMARY: % of % tests passed (100%%)', - passed_tests, total_tests; - RAISE NOTICE '------------------------------------'; - ELSE - RAISE WARNING '------------------------------------'; - RAISE WARNING 'SUMMARY: % of % 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 % tests did not pass', (total_tests - passed_tests), total_tests; - END IF; +-- Test file for hemar template parser +-- Run with: psql -f test_template_parser.sql + +-- Load extension if not already loaded +-- CREATE EXTENSION IF NOT EXISTS hemar; + +-- Create test function to validate template parsing +CREATE OR REPLACE FUNCTION test_template_parse(template_text text, expected_structure text) RETURNS boolean AS $$ +DECLARE + parsed_result text; + passed boolean; +BEGIN + BEGIN + parsed_result := hemar.parse(template_text); + + IF parsed_result IS NULL THEN + RAISE WARNING 'Parser returned NULL for template: %', template_text; + RETURN false; + END IF; + + passed := position(expected_structure in parsed_result) > 0; + + IF NOT passed THEN + RAISE WARNING 'Template parsing test failed!'; + RAISE WARNING 'Template: %', template_text; + RAISE WARNING 'Expected to find: %', expected_structure; + RAISE WARNING 'Actual result: %', parsed_result; + END IF; + + RETURN passed; + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'Exception during parsing: % (state: %)', SQLERRM, SQLSTATE; + RAISE WARNING 'Template: %', template_text; + RETURN false; + END; +END; +$$ LANGUAGE plpgsql; + +-- Run the tests +DO $$ +DECLARE + total_tests integer := 0; + passed_tests integer := 0; + result boolean; +BEGIN + PERFORM pg_sleep(2); + RAISE NOTICE 'Starting template parser tests...'; + + -- Test 1: Simple interpolation + total_tests := total_tests + 1; + result := test_template_parse( + $hemar1${{ simple_var }}$hemar1$, + $expected1$INTERPOLATE: "simple_var"$expected1$ + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Simple interpolation - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Simple interpolation - FAILED', total_tests; + END IF; + + -- Test 2: Interpolation with surrounding text + total_tests := total_tests + 1; + result := test_template_parse( + $hemar2$Hello, {{ name }}!$hemar2$, + $expected2$TEXT: "Hello, " +INTERPOLATE: "name" +TEXT: "!"$expected2$ + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Interpolation with surrounding text - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Interpolation with surrounding text - FAILED', total_tests; + END IF; + + -- Test 3: Simple section (for loop) + total_tests := total_tests + 1; + result := test_template_parse( + $hemar3${{ for item in items }}{{ item }}{{ end }}$hemar3$, + $expected3$SECTION: iterator="item", collection="items"$expected3$ + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Simple section (for loop) - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Simple section (for loop) - FAILED', total_tests; + END IF; + + -- Test 4: Section with nested interpolation + total_tests := total_tests + 1; + result := test_template_parse( + $hemar4${{ for item in items }}Name: {{ item.name }}{{ end }}$hemar4$, + $expected4$SECTION: iterator="item", collection="items" + TEXT: "Name: " + INTERPOLATE: "item.name"$expected4$ + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Section with nested interpolation - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Section with nested interpolation - FAILED', total_tests; + END IF; + + -- Test 5: Nested sections + total_tests := total_tests + 1; + result := test_template_parse( + $hemar5${{ for item in items }}{{ for subitem in item.subitems }}{{ subitem }}{{ end }}{{ end }}$hemar5$, + $expected5$SECTION: iterator="item", collection="items" + SECTION: iterator="subitem", collection="item.subitems" + INTERPOLATE: "subitem"$expected5$ + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Nested sections - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Nested sections - FAILED', total_tests; + END IF; + + -- Test 6: Include tag + total_tests := total_tests + 1; + result := test_template_parse( + $hemar6${{ include template_name }}$hemar6$, + $expected6$INCLUDE: "template_name"$expected6$ + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Include tag - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Include tag - FAILED', total_tests; + END IF; + + -- Test 7: Execute tag + total_tests := total_tests + 1; + result := test_template_parse( + $hemar7${{ exec RETURN my_function(arg1, arg2) }}$hemar7$, + $expected7$EXECUTE: "RETURN my_function(arg1, arg2)"$expected7$ + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Execute tag - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Execute tag - FAILED', total_tests; + END IF; + + -- Test 8: Complex mixed template + total_tests := total_tests + 1; + result := test_template_parse( + $hemar8$
{{ for item in items }}

{{ item.name }}

{{ include item.template }}{{ end }}
$hemar8$, + $expected8$TEXT: "
" +SECTION: iterator="item", collection="items" + TEXT: "

" + INTERPOLATE: "item.name" + TEXT: "

" + INCLUDE: "item.template" +TEXT: "
"$expected8$ + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Complex mixed template - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Complex mixed template - FAILED', total_tests; + END IF; + + -- Test 9: Execute tag with complex SQL + total_tests := total_tests + 1; + result := test_template_parse( + $template9${{ exec + IF condition THEN + RETURN 'value1'; + ELSE + RETURN 'value2'; + END IF; + }}$template9$, + $expected9$EXECUTE: "IF condition THEN + RETURN 'value1'; + ELSE + RETURN 'value2'; + END IF;"$expected9$ + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Execute tag with complex SQL - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Execute tag with complex SQL - FAILED', total_tests; + END IF; + + -- Test 10: Whitespace handling + total_tests := total_tests + 1; + result := test_template_parse( + $hemar10${{ spaced_var }}$hemar10$, + $expected10$INTERPOLATE: "spaced_var"$expected10$ + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Whitespace handling - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Whitespace handling - FAILED', total_tests; + END IF; + + -- Test 11: Multiple consecutive tags + total_tests := total_tests + 1; + result := test_template_parse( + $hemar11${{ var1 }}{{ var2 }}{{ var3 }}$hemar11$, + $expected11$INTERPOLATE: "var1" +INTERPOLATE: "var2" +INTERPOLATE: "var3"$expected11$ + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Multiple consecutive tags - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Multiple consecutive tags - FAILED', total_tests; + END IF; + + -- Test 12: Section with multiple nested elements + total_tests := total_tests + 1; + result := test_template_parse( + $hemar12${{ for item in items }} +

{{ item.title }}

+

{{ item.description }}

+ {{ include item.footer }} + {{ end }}$hemar12$, + $expected12$SECTION: iterator="item", collection="items" + TEXT: " +

" + INTERPOLATE: "item.title" + TEXT: "

+

" + INTERPOLATE: "item.description" + TEXT: "

+ " + INCLUDE: "item.footer" + TEXT: " + "$expected12$ + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Section with multiple nested elements - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Section with multiple nested elements - FAILED', total_tests; + END IF; + + -- Test 13: Empty template + total_tests := total_tests + 1; + result := test_template_parse( + '', + 'TEXT: ""' + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Empty template - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Empty template - FAILED', total_tests; + END IF; + + -- Test 14: Just text, no tags + total_tests := total_tests + 1; + result := test_template_parse( + $hemar14$Just plain text, no tags here.$hemar14$, + $expected14$TEXT: "Just plain text, no tags here."$expected14$ + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Just text, no tags - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Just text, no tags - FAILED', total_tests; + END IF; + + -- Test 15: Complex example from documentation + total_tests := total_tests + 1; + result := test_template_parse( + $template15$
text before
+ + {{ include inner_template }} + + {{ name }} + + {{ for item in array }} + some text: {{ name2 }} + {{ item.name }} + {{ end }} + +
code insertion:
+ // FIXME: IT NEED A SPECE PIZDEZZZZ + {{ exec + context + '{"name3": "zalupa"}'; + + IF context->condition THEN + RAISE INFO 'some log'; + + RETURN 'some text'; + END + RETURN 'some other text'; + }} + + $template15$, + $expected15$TEXT: "
text before
+ + " +INCLUDE: "inner_template" +TEXT: " + + " +INTERPOLATE: "name" +TEXT: " + + " +SECTION: iterator="item", collection="array" + TEXT: " + some text: " + INTERPOLATE: "name2" + TEXT: " + " + INTERPOLATE: "item.name" + TEXT: " + " +TEXT: " + +
code insertion:
+ " +EXECUTE: "context + '{"name3": "zalupa"}'; + + IF context->condition THEN + RAISE INFO 'some log'; + + RETURN 'some text'; + END + RETURN 'some other text';" +TEXT: " + +
...
"$expected15$ + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Complex example from documentation - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Complex example from documentation - FAILED', total_tests; + END IF; + + -- Test 16: Multiple nested sections + total_tests := total_tests + 1; + result := test_template_parse( + '{{ for a in items }} + {{ for b in a.items }} + {{ for c in b.items }} + {{ c.name }} + {{ end }} + {{ end }} + {{ end }}', + 'SECTION: iterator="a", collection="items" + TEXT: " + " + SECTION: iterator="b", collection="a.items" + TEXT: " + " + SECTION: iterator="c", collection="b.items" + TEXT: " + " + INTERPOLATE: "c.name" + TEXT: " + "' + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Multiple nested sections - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Multiple nested sections - FAILED', total_tests; + END IF; + + -- Test 17: Interpolation with special characters + total_tests := total_tests + 1; + result := test_template_parse( + '{{ special@field }}', + 'INTERPOLATE: "special@field"' + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Interpolation with special characters - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Interpolation with special characters - FAILED', total_tests; + END IF; + + -- Test 18: Section with complex iterator and collection names + total_tests := total_tests + 1; + result := test_template_parse( + '{{ for complex_item.with.dots in complex_collection[0].items }}{{ end }}', + 'SECTION: iterator="complex_item.with.dots", collection="complex_collection[0].items"' + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Section with complex iterator and collection names - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Section with complex iterator and collection names - FAILED', total_tests; + END IF; + + -- Test 19: Include with complex path + total_tests := total_tests + 1; + result := test_template_parse( + '{{ include templates[0].nested.path }}', + 'INCLUDE: "templates[0].nested.path"' + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Include with complex path - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Include with complex path - FAILED', total_tests; + END IF; + + -- Test 20: Execute with complex SQL and quotes + total_tests := total_tests + 1; + result := test_template_parse( + $template20$ + {{ exec SELECT 'text with "double" quotes' AS result; }} + $template20$, + $expected20$EXECUTE: "SELECT 'text with "double" quotes' AS result;"$expected20$ + ); + IF result THEN + passed_tests := passed_tests + 1; + RAISE NOTICE 'Test %: Execute with complex SQL and quotes - PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Execute with complex SQL and quotes - FAILED', total_tests; + END IF; + + -- Print summary + IF passed_tests = total_tests THEN + RAISE NOTICE '------------------------------------'; + RAISE NOTICE 'SUMMARY: % of % tests passed (100%%)', + passed_tests, total_tests; + RAISE NOTICE '------------------------------------'; + ELSE + RAISE WARNING '------------------------------------'; + RAISE WARNING 'SUMMARY: % of % 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 % tests did not pass', (total_tests - passed_tests), total_tests; + END IF; END $$; \ No newline at end of file