From bcc755d3259503ca2d7e77531fa75be8b9dd4581 Mon Sep 17 00:00:00 2001 From: yukkop Date: Sat, 17 May 2025 13:39:40 +0000 Subject: [PATCH] test: exec --- flake.nix | 28 +- package/c/hectic/docs/templater.md | 50 +-- package/c/hemar/hemar--0.1.sql | 6 + package/c/hemar/hemar.c | 262 ++++++++++++- package/c/hemar/test/mod.sql | 5 + package/c/hemar/test/test_jsonb_path.sql | 8 +- package/c/hemar/test/test_render_exec.sql | 343 ++++++++++++++++++ package/c/hemar/test/test_template_parser.sql | 6 +- 8 files changed, 665 insertions(+), 43 deletions(-) create mode 100755 package/c/hemar/test/mod.sql create mode 100755 package/c/hemar/test/test_render_exec.sql diff --git a/flake.nix b/flake.nix index 3c13033..8596bff 100644 --- a/flake.nix +++ b/flake.nix @@ -177,6 +177,9 @@ jq yq-go curl + (writeScriptBin "hemar-check" '' + ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null vm-postgres 'zsh -c check' + '') ]); # environment @@ -324,18 +327,29 @@ ALTER DATABASE postgres SET client_min_messages TO DEBUG1; CREATE EXTENSION "hemar"; - -- SELECT hemar.parse('{% zalupa %}'); - SELECT hemar.render('{"a": "b"}'::JSONB, 'a {% a %}'); - SELECT hemar.render('{"a": ["b", "c"]}'::JSONB, 'a {% for i in a do text %}'); - SELECT hemar.render('{"a": {"g": ["b", "c"]}}'::JSONB, 'a {% for i in a.g do {% i %} %}'); - SELECT hemar.render('{"a": {"g": ["b", "c"], "b": [{"c": "a"}, {"c": "b"}]}}'::JSONB, 'a {% for i in a.b do {% i.c %} %}'); + \i ${./package/c/hemar/test/mod.sql} + + -- SELECT hemar.parse('{{ zalupa }}'); + -- SELECT hemar.render('{"a": "b"}'::JSONB, 'a {{ a }}'); + -- SELECT hemar.render('{"a": ["b", "c"]}'::JSONB, 'a {{ for i in a do text }}'); + -- SELECT hemar.render('{"a": {"g": ["b", "c"]}}'::JSONB, 'a {{ for i in a.g do {{ i }} }}'); + -- SELECT hemar.render('{"a": {"b": [{"c": "a"}, {"c": "b"}]}}'::JSONB, 'a {{ for i in a.b do text }}'); + -- SELECT hemar.render('{"a": {"g": ["b", "c"], "b": [{"c": "a"}, {"c": "b"}]}}'::JSONB, 'a {{ for i in a.g do text }}'); + -- SELECT hemar.render('{"a": {"g": ["b", "c"]}}'::JSONB, 'a {{ for i in a.g do text {{ i }} text }}'); + -- SELECT hemar.render('{"a": {"g": ["b", "c"]}}'::JSONB, 'a {{ a.g[1] }} {{ a.g[0] }}'); + -- SELECT hemar.render('{"a": {"g": ["b", ["c", "d", "g"]]}}'::JSONB, 'a {{ a.g[1][2] }} {{ a.g[1][1] }} {{ a.g[1][0] }} {{ a.g[0] }}'); ''; }; - environment.systemPackages = with pkgs; [ gdb ]; + environment.systemPackages = with pkgs; [ + gdb + hectic.nvim-pager + (writeScriptBin "check" '' + journalctl -u postgresql.service | grep postgresql-post-start | sed 's|psql:/nix/store/[^:]*:[0-9]*: ||' | sed 's|^[^:]*:[^:]*:[^:]*: ||' | grep -v '^\[.*\]' | ${hectic.prettify-log}/bin/prettify-log --color-output + '') + ]; programs.zsh.shellAliases = self.lib.sharedShellAliases // { conn = "sudo su postgres -c 'psql -p 64317'"; - check = "journalctl -u postgresql"; }; virtualisation = { diff --git a/package/c/hectic/docs/templater.md b/package/c/hectic/docs/templater.md index 069a4ae..8f4a816 100755 --- a/package/c/hectic/docs/templater.md +++ b/package/c/hectic/docs/templater.md @@ -10,11 +10,11 @@ The templating engine supports flexible customization of tag syntax parameters. - **Open Brace** A non-empty string marking the beginning of a tag. - *Example:* `{%` + *Example:* `{{` - **Close Brace** A non-empty string marking the end of a tag. - *Example:* `%}` + *Example:* `}}` --- @@ -31,22 +31,22 @@ Parameters defining syntax for blocks controlling loops or nested structures. *Example:* ` in ` | `#` - **Post-Suffix** - Finalizes the section declaration block. - *Example:* `do ` | `:` + Finalizes the section block. + *Example:* `end ` | `/` *Example* *Section Example:* ```tpl -{% for item in items do - {% item.name %} +{{ for item in items }} + {{ item.name }} some text - {% for inner_item in item.inner_items join '\n' do + {{ for inner_item in item.inner_items join '\n' }}

some other text

- {% inner_item %} - %} + {{ inner_item }} + {{ end }} \n -%} +{{ end }} ``` *Context Example:* ```json @@ -68,7 +68,7 @@ Inserts variable values or expression results directly into templates. *Interpolation Example:* ```tpl - {% interpolation_field %} + {{ interpolation_field }} ``` *Context Example:* ```json @@ -87,7 +87,7 @@ Includes content from other templates. *Include Example:* ```tpl text before - {% include inner_template %} + {{ include inner_template }} ``` *Context Examples:* @@ -96,7 +96,7 @@ Includes content from other templates. { "include inner_template": [ { - "template": "{% field %}", + "template": "{{ field }}", "context": { "field": "value" } } ] @@ -107,7 +107,7 @@ Includes content from other templates. "field": "value", "include inner_template": [ { - "template": "{% field %}" + "template": "{{ field }}" } ] } @@ -124,7 +124,7 @@ Includes content from other templates. ``` ## Execution Tags -**Note:** Currently not included in C library; implemented as a wrapper on applicable platforms. +**Note:** implemented as a wrapper on applicable platforms, in that case must evel Postgresql functions. Enables calling functions with arguments, or execute code. Have hardcoded context var - alows use template context - **Prefix** Denotes a function call. @@ -132,8 +132,8 @@ Enables calling functions with arguments, or execute code. Have hardcoded contex *Function Example:* ```tpl - {% exec RETURN my_function(context->arg1, context->arg2, 'literal') %} - {% exec RETURN 'aaaaa' %} + {{ exec RETURN my_function(context->arg1, context->arg2, 'literal') }} + {{ exec RETURN 'aaaaa' }} ``` ## Notes @@ -149,17 +149,17 @@ Enables calling functions with arguments, or execute code. Have hardcoded contex ```tpl
text before
- {% include inner_template %} + {{ include inner_template }} - {% name %} + {{ name }} - {% for item in array do - some text: {% name2 %} - {% item.name %} - %} + {{ for item in array }} + some text: {{ name2 }} + {{ item.name }} + {{ end }}
code insertion:
- {% execute + {{ execute context + '{"name3": "zalupa"}'; IF context->condition THEN @@ -168,7 +168,7 @@ Enables calling functions with arguments, or execute code. Have hardcoded contex RETURN 'some text'; END RETURN 'some other text'; - %} + }} ``` \ No newline at end of file diff --git a/package/c/hemar/hemar--0.1.sql b/package/c/hemar/hemar--0.1.sql index b8f5818..f187dbe 100755 --- a/package/c/hemar/hemar--0.1.sql +++ b/package/c/hemar/hemar--0.1.sql @@ -13,3 +13,9 @@ CREATE FUNCTION "hemar"."jsonb_get_by_path"("json" jsonb, "path" text) RETURNS jsonb LANGUAGE C STRICT AS 'hemar', 'pg_jsonb_get_by_path'; + +-- Template rendering function +CREATE FUNCTION "hemar"."render"("define" jsonb, "template" text) +RETURNS text +LANGUAGE C STRICT +AS 'hemar', 'pg_template_render'; diff --git a/package/c/hemar/hemar.c b/package/c/hemar/hemar.c index 71a1564..191ab1f 100755 --- a/package/c/hemar/hemar.c +++ b/package/c/hemar/hemar.c @@ -21,6 +21,8 @@ /* Forward declarations */ static void template_node_to_string(TemplateNode *node, StringInfo result, int indent); +static void render_template(TemplateNode *node, Jsonb *define, StringInfo result, MemoryContext context); +static void render_execute_tag(const char *code, Jsonb *define, StringInfo result, MemoryContext context); static const char * jbt_type_to_string(JsonbIteratorToken type) @@ -494,9 +496,9 @@ template_parse_section(MemoryContext context, const char **s_ptr, { /* This is our matching end tag */ end_tag_start = *s; - break; - } - } + break; + } + } } (*s)++; @@ -679,7 +681,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 */ @@ -1094,6 +1096,258 @@ template_node_to_string(TemplateNode *node, StringInfo result, int indent) } } +/* Template rendering function */ +PG_FUNCTION_INFO_V1(pg_template_render); +Datum +pg_template_render(PG_FUNCTION_ARGS) +{ + Jsonb *define = PG_GETARG_JSONB_P(0); + text *template_text = PG_GETARG_TEXT_PP(1); + char *template_str = text_to_cstring(template_text); + const char *template_ptr = template_str; + MemoryContext old_context, render_context; + TemplateConfig config; + TemplateNode *root = NULL; + TemplateErrorCode error_code = TEMPLATE_ERROR_NONE; + StringInfoData result; + text *result_text = NULL; + + /* Create a memory context for rendering */ + render_context = AllocSetContextCreate(CurrentMemoryContext, + "Template Render Context", + ALLOCSET_DEFAULT_SIZES); + + /* Switch to the new context for rendering */ + old_context = MemoryContextSwitchTo(render_context); + + /* Initialize default config */ + config = template_default_config(render_context); + + PG_TRY(); + { + /* Parse the template */ + root = template_parse(render_context, &template_ptr, &config, false, &error_code); + + /* Check for parsing errors */ + if (error_code != TEMPLATE_ERROR_NONE || !root) + { + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("Template parsing error: %s", template_error_to_string(error_code, &config)))); + } + + /* Initialize the result buffer */ + initStringInfo(&result); + + /* Render the template */ + render_template(root, define, &result, render_context); + + /* Switch back to the original memory context */ + MemoryContextSwitchTo(old_context); + + /* Return the result */ + result_text = cstring_to_text(result.data); + pfree(result.data); + } + PG_CATCH(); + { + /* Switch back to the original memory context for error handling */ + MemoryContextSwitchTo(old_context); + + /* Clean up */ + if (template_str) + pfree(template_str); + + /* Delete the render context */ + MemoryContextDelete(render_context); + + PG_RE_THROW(); + } + PG_END_TRY(); + + /* Clean up */ + MemoryContextDelete(render_context); + pfree(template_str); + + PG_RETURN_TEXT_P(result_text); +} + +/* Helper function to render a template node */ +static void +render_template(TemplateNode *node, Jsonb *define, StringInfo result, MemoryContext context) +{ + TemplateNode *current = node; + + while (current) + { + switch (current->type) + { + case TEMPLATE_NODE_TEXT: + if (current->value->text.content) + appendStringInfoString(result, current->value->text.content); + break; + + case TEMPLATE_NODE_EXECUTE: + render_execute_tag(current->value->execute.code, define, result, context); + break; + + /* We'll implement these later */ + case TEMPLATE_NODE_INTERPOLATE: + case TEMPLATE_NODE_SECTION: + case TEMPLATE_NODE_INCLUDE: + default: + /* Skip for now */ + break; + } + + current = current->next; + } +} + +/* Helper function to calculate a simple hash of a string */ +static uint32_t +calculate_string_hash(const char *str) +{ + uint32_t hash = 5381; + int c; + + while ((c = *str++)) + hash = ((hash << 5) + hash) + c; /* hash * 33 + c */ + + return hash; +} + +/* Helper function to render an execute tag */ +static void +render_execute_tag(const char *code, Jsonb *define, StringInfo result, MemoryContext context) +{ + int ret; + StringInfoData query; + StringInfoData exec_result; + char *trimmed_code; + size_t code_len; + uint32_t code_hash; + char func_name[64]; + bool isnull; + bool function_exists; + + /* Connect to SPI */ + if ((ret = SPI_connect()) < 0) + ereport(ERROR, + (errcode(ERRCODE_CONNECTION_EXCEPTION), + errmsg("SPI_connect failed: %s", SPI_result_code_string(ret)))); + + /* Create the query with the context variable */ + initStringInfo(&query); + initStringInfo(&exec_result); + + /* Trim trailing semicolon if present to avoid double semicolons */ + code_len = strlen(code); + trimmed_code = pstrdup(code); + while (code_len > 0 && (trimmed_code[code_len-1] == ';' || isspace((unsigned char)trimmed_code[code_len-1]))) { + trimmed_code[--code_len] = '\0'; + } + + /* Calculate hash of the code */ + code_hash = calculate_string_hash(trimmed_code); + snprintf(func_name, sizeof(func_name), "cache-%x", code_hash); + + /* Check if function exists */ + appendStringInfo(&query, + "SELECT EXISTS (SELECT 1 FROM pg_proc p " + "JOIN pg_namespace n ON p.pronamespace = n.oid " + "WHERE n.nspname = 'hemar' AND p.proname = '%s');", + func_name); + + ret = SPI_execute(query.data, true, 0); + if (ret != SPI_OK_SELECT) + { + SPI_finish(); + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("Failed to check function existence: %s", SPI_result_code_string(ret)))); + } + + function_exists = DatumGetBool(SPI_getbinval(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 1, &isnull)); + + /* Reset query buffer for function creation */ + resetStringInfo(&query); + + /* Only create function if it doesn't exist */ + if (!function_exists) + { + elog(NOTICE, "Caching function %s", func_name); + elog(DEBUG1, "Content: %s", trimmed_code); + + appendStringInfo(&query, + "CREATE OR REPLACE FUNCTION \"hemar\".\"%s\"(context jsonb) RETURNS text LANGUAGE plpgsql AS $$ " + "BEGIN " + " %s; " + " RETURN '';" // NOTICE(yukkop): Trailing return in case user does not return anything + "END $$;", + func_name, + trimmed_code); + + /* Execute the query */ + ret = SPI_execute(query.data, false, 0); + + if (ret != SPI_OK_UTILITY) + { + SPI_finish(); + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("Failed to execute SQL in template: %s", SPI_result_code_string(ret)))); + } + } + + /* Reset query buffer for function execution */ + resetStringInfo(&query); + + /* Execute the function */ + appendStringInfo(&query, "SELECT \"hemar\".\"%s\"($1);", func_name); + + /* Prepare arguments for SPI_execute_with_args */ + Oid argtypes[1] = {JSONBOID}; + Datum argvalues[1] = {JsonbPGetDatum(define)}; + + ret = SPI_execute_with_args(query.data, 1, argtypes, argvalues, NULL, true, 0); + + if (ret != SPI_OK_SELECT) + { + SPI_finish(); + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("Failed to execute function: %s", SPI_result_code_string(ret)))); + } + + /* Get the result */ + if (SPI_processed > 0) + { + Datum content = SPI_getbinval(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 1, &isnull); + + if (!isnull) + { + char *content_str = TextDatumGetCString(content); + appendStringInfoString(&exec_result, content_str); + pfree(content_str); + } + } + + /* Append any captured output to the result */ + if (exec_result.len > 0) + { + appendStringInfoString(result, exec_result.data); + } + + /* Clean up */ + pfree(query.data); + pfree(exec_result.data); + pfree(trimmed_code); + + /* Disconnect from SPI */ + SPI_finish(); +} + /* Function declarations */ PG_FUNCTION_INFO_V1(pg_jsonb_get_by_path); diff --git a/package/c/hemar/test/mod.sql b/package/c/hemar/test/mod.sql new file mode 100755 index 0000000..9ee06ff --- /dev/null +++ b/package/c/hemar/test/mod.sql @@ -0,0 +1,5 @@ +BEGIN; + \ir test_jsonb_path.sql + \ir test_template_parser.sql + \ir test_render_exec.sql +ROLLBACK; \ No newline at end of file diff --git a/package/c/hemar/test/test_jsonb_path.sql b/package/c/hemar/test/test_jsonb_path.sql index d247438..286a6c7 100755 --- a/package/c/hemar/test/test_jsonb_path.sql +++ b/package/c/hemar/test/test_jsonb_path.sql @@ -482,12 +482,12 @@ BEGIN -- Print summary IF passed_tests = total_tests THEN RAISE NOTICE '------------------------------------'; - RAISE NOTICE 'SUMMARY: % of % tests passed (100%%)', + RAISE NOTICE 'SUMMARY: % of % jsonb_get_by_path tests passed (100%%)', passed_tests, total_tests; RAISE NOTICE '------------------------------------'; ELSE RAISE WARNING '------------------------------------'; - RAISE WARNING 'SUMMARY: % of % tests passed (%)', + RAISE WARNING 'SUMMARY: % of % jsonb_get_by_path tests passed (%)', passed_tests, total_tests, round((passed_tests::numeric / total_tests::numeric) * 100, 2) || '%'; @@ -495,6 +495,6 @@ BEGIN END IF; IF passed_tests != total_tests THEN - RAISE EXCEPTION 'Tests failed: % of % tests did not pass', (total_tests - passed_tests), total_tests; + RAISE EXCEPTION 'Tests failed: % of % jsonb_get_by_path tests did not pass', (total_tests - passed_tests), total_tests; END IF; -END $$; +END $$; \ No newline at end of file diff --git a/package/c/hemar/test/test_render_exec.sql b/package/c/hemar/test/test_render_exec.sql new file mode 100755 index 0000000..a122c0a --- /dev/null +++ b/package/c/hemar/test/test_render_exec.sql @@ -0,0 +1,343 @@ +-- Test the render function with execute tags +CREATE EXTENSION IF NOT EXISTS hemar; + +DO $$ +DECLARE + total_tests INT := 0; + passed_tests INT := 0; + test_result TEXT; + expected TEXT; + passed BOOLEAN; +BEGIN + -- Test 1: Simple execute tag that sets a variable + total_tests := total_tests + 1; + test_result := hemar.render( + '{"name": "John", "age": 30}'::jsonb, + 'Hello {{ exec PERFORM 1; }}' + ); + expected := 'Hello '; + passed := test_result = expected; + passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END); + IF passed THEN + RAISE NOTICE 'Test %: Simple execute tag: PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Simple execute tag: FAILED. Expected "%", got "%"', + total_tests, expected, test_result; + END IF; + + -- Test 2: Execute tag with context access + total_tests := total_tests + 1; + DROP TABLE IF EXISTS test_output; + CREATE TEMP TABLE test_output (value TEXT); + + test_result := hemar.render( + '{"name": "John", "age": 30}'::jsonb, + $expected$Hello {{ exec INSERT INTO test_output VALUES (context->'name'); }}$expected$ + ); + + SELECT value INTO expected FROM test_output; + passed := expected = '"John"'; + passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END); + IF passed THEN + RAISE NOTICE 'Test %: Execute tag with context access: PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Execute tag with context access: FAILED. Expected "John", got "%"', + total_tests, expected; + END IF; + + -- Test 3: Execute tag with quotes and complex SQL + total_tests := total_tests + 1; + DROP TABLE IF EXISTS test_output; + CREATE TEMP TABLE test_output (value TEXT); + + test_result := hemar.render( + '{"items": [{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}]}'::jsonb, + $expected$Items: {{ exec + INSERT INTO test_output + SELECT jsonb_array_elements(context->'items')->>'name'; + }}$expected$ + ); + + SELECT string_agg(value, ', ' ORDER BY value) INTO expected FROM test_output; + passed := expected = 'Item 1, Item 2'; + passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END); + IF passed THEN + RAISE NOTICE 'Test %: Execute tag with complex SQL: PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Execute tag with complex SQL: FAILED. Expected "Item 1, Item 2", got "%"', + total_tests, expected; + END IF; + + -- Test 4: Execute tag with output capture + total_tests := total_tests + 1; + test_result := hemar.render( + '{"name": "John", "age": 30}'::jsonb, + $expected$Hello {{ exec RETURN context->>'name'; }}$expected$ + ); + expected := 'Hello John'; + passed := test_result = expected; + passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END); + IF passed THEN + RAISE NOTICE 'Test %: Execute tag with output capture: PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Execute tag with output capture: FAILED. Expected "%", got "%"', + total_tests, expected, test_result; + END IF; + + -- Test 5: Execute tag with complex output + total_tests := total_tests + 1; + test_result := hemar.render( + '{"items": [{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}]}'::jsonb, + $expected$Items: {{ exec + RETURN (SELECT string_agg(value, ', ') + FROM ( + SELECT jsonb_array_elements(context->'items')->>'name' as value + ) t); + }}$expected$ + ); + expected := 'Items: Item 1, Item 2'; + passed := test_result = expected; + passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END); + IF passed THEN + RAISE NOTICE 'Test %: Execute tag with complex output: PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Execute tag with complex output: FAILED. Expected "%", got "%"', + total_tests, expected, test_result; + END IF; + + -- Test 6: Execute tag with multiple statements + total_tests := total_tests + 1; + test_result := hemar.render( + '{"name": "John", "age": 30}'::jsonb, + $expected$Hello {{ exec + DECLARE + v_name TEXT; + BEGIN + v_name := context->>'name'; + RETURN v_name; + END; + }}$expected$ + ); + expected := 'Hello John'; + passed := test_result = expected; + passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END); + IF passed THEN + RAISE NOTICE 'Test %: Execute tag with multiple statements: PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Execute tag with multiple statements: FAILED. Expected "%", got "%"', + total_tests, expected, test_result; + END IF; + + -- Test 7: Execute tag with array operations + total_tests := total_tests + 1; + test_result := hemar.render( + '{"numbers": [1, 2, 3, 4, 5]}'::jsonb, + $expected$Sum: {{ exec + RETURN (SELECT sum(value::int) + FROM jsonb_array_elements_text(context->'numbers') as value); + }}$expected$ + ); + expected := 'Sum: 15'; + passed := test_result = expected; + passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END); + IF passed THEN + RAISE NOTICE 'Test %: Execute tag with array operations: PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Execute tag with array operations: FAILED. Expected "%", got "%"', + total_tests, expected, test_result; + END IF; + + -- Test 8: Execute tag with nested JSON operations + total_tests := total_tests + 1; + test_result := hemar.render( + '{"user": {"profile": {"settings": {"theme": "dark", "notifications": true}}}}'::jsonb, + $expected$Settings: {{ exec + RETURN context->'user'->'profile'->'settings'->>'theme'; + }}$expected$ + ); + expected := 'Settings: dark'; + passed := test_result = expected; + passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END); + IF passed THEN + RAISE NOTICE 'Test %: Execute tag with nested JSON operations: PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Execute tag with nested JSON operations: FAILED. Expected "%", got "%"', + total_tests, expected, test_result; + END IF; + + -- Test 9: Execute tag with conditional logic + total_tests := total_tests + 1; + test_result := hemar.render( + '{"age": 25, "country": "US"}'::jsonb, + $expected$Status: {{ exec + DECLARE + v_status TEXT; + BEGIN + IF (context->>'age')::int >= 21 AND context->>'country' = 'US' THEN + v_status := 'Adult in US'; + ELSE + v_status := 'Other'; + END IF; + RETURN v_status; + END; + }}$expected$ + ); + expected := 'Status: Adult in US'; + passed := test_result = expected; + passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END); + IF passed THEN + RAISE NOTICE 'Test %: Execute tag with conditional logic: PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Execute tag with conditional logic: FAILED. Expected "%", got "%"', + total_tests, expected, test_result; + END IF; + + -- Test 10: Execute tag with string manipulation + total_tests := total_tests + 1; + test_result := hemar.render( + '{"text": "hello world"}'::jsonb, + $expected$Text: {{ exec + RETURN upper(context->>'text'); + }}$expected$ + ); + expected := 'Text: HELLO WORLD'; + passed := test_result = expected; + passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END); + IF passed THEN + RAISE NOTICE 'Test %: Execute tag with string manipulation: PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Execute tag with string manipulation: FAILED. Expected "%", got "%"', + total_tests, expected, test_result; + END IF; + + -- Test 11: Execute tag with date operations + total_tests := total_tests + 1; + test_result := hemar.render( + '{"date": "2024-03-15"}'::jsonb, + $expected$Date: {{ exec + RETURN to_char((context->>'date')::date, 'Month DD, YYYY'); + }}$expected$ + ); + expected := 'Date: March 15, 2024'; + passed := test_result = expected; + passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END); + IF passed THEN + RAISE NOTICE 'Test %: Execute tag with date operations: PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Execute tag with date operations: FAILED. Expected "%", got "%"', + total_tests, expected, test_result; + END IF; + + -- Test 12: Execute tag with aggregation + total_tests := total_tests + 1; + test_result := hemar.render( + '{"scores": [85, 92, 78, 95, 88]}'::jsonb, + $expected$Stats: {{ exec + RETURN (SELECT format('Avg: %s, Max: %s', + round(avg(value::float)::numeric, 1), + max(value::int)) + FROM jsonb_array_elements_text(context->'scores') as value); + }}$expected$ + ); + expected := 'Stats: Avg: 87.6, Max: 95'; + passed := test_result = expected; + passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END); + IF passed THEN + RAISE NOTICE 'Test %: Execute tag with aggregation: PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Execute tag with aggregation: FAILED. Expected "%", got "%"', + total_tests, expected, test_result; + END IF; + + -- Test 13: Execute tag with error handling + total_tests := total_tests + 1; + test_result := hemar.render( + '{"value": "not_a_number"}'::jsonb, + $expected$Result: {{ exec + BEGIN + RETURN (context->>'value')::int::text; + EXCEPTION WHEN OTHERS THEN + RETURN 'Error: Invalid number'; + END; + }}$expected$ + ); + expected := 'Result: Error: Invalid number'; + passed := test_result = expected; + passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END); + IF passed THEN + RAISE NOTICE 'Test %: Execute tag with error handling: PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Execute tag with error handling: FAILED. Expected "%", got "%"', + total_tests, expected, test_result; + END IF; + + -- Test 14: Execute tag with complex JSON transformation + total_tests := total_tests + 1; + test_result := hemar.render( + '{"users": [{"name": "Alice", "roles": ["admin", "user"]}, {"name": "Bob", "roles": ["user"]}]}'::jsonb, + $expected$Users: {{ exec + RETURN (SELECT string_agg( + format('%s (%s)', + user_data->>'name', + (SELECT string_agg(role, ', ') + FROM jsonb_array_elements_text(user_data->'roles') as role) + ), + '; ' + ) + FROM jsonb_array_elements(context->'users') as user_data); + }}$expected$ + ); + expected := 'Users: Alice (admin, user); Bob (user)'; + passed := test_result = expected; + passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END); + IF passed THEN + RAISE NOTICE 'Test %: Execute tag with complex JSON transformation: PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Execute tag with complex JSON transformation: FAILED. Expected "%", got "%"', + total_tests, expected, test_result; + END IF; + + -- Test 15: Execute tag with empty/null handling + total_tests := total_tests + 1; + test_result := hemar.render( + '{"name": null, "items": []}'::jsonb, + $expected$Result: {{ exec + DECLARE + v_name TEXT; + v_count INT; + BEGIN + v_name := COALESCE(context->>'name', 'Unknown'); + v_count := jsonb_array_length(context->'items'); + RETURN format('Name: %s, Items: %s', v_name, v_count); + END; + }}$expected$ + ); + expected := 'Result: Name: Unknown, Items: 0'; + passed := test_result = expected; + passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END); + IF passed THEN + RAISE NOTICE 'Test %: Execute tag with empty/null handling: PASSED', total_tests; + ELSE + RAISE WARNING 'Test %: Execute tag with empty/null handling: FAILED. Expected "%", got "%"', + total_tests, expected, test_result; + END IF; + + -- Print summary + IF passed_tests = total_tests THEN + RAISE NOTICE '------------------------------------'; + RAISE NOTICE 'SUMMARY: % of % render exec tests passed (100%%)', + passed_tests, total_tests; + RAISE NOTICE '------------------------------------'; + ELSE + RAISE WARNING '------------------------------------'; + RAISE WARNING 'SUMMARY: % of % render exec 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 % render exec tests did not pass', (total_tests - passed_tests), total_tests; + END IF; +END $$; \ No newline at end of file diff --git a/package/c/hemar/test/test_template_parser.sql b/package/c/hemar/test/test_template_parser.sql index f406c06..5d4783d 100755 --- a/package/c/hemar/test/test_template_parser.sql +++ b/package/c/hemar/test/test_template_parser.sql @@ -1064,12 +1064,12 @@ SECTION: iterator="section", collection="page.sections"$expected40$ -- Print summary IF passed_tests = total_tests THEN RAISE NOTICE '------------------------------------'; - RAISE NOTICE 'SUMMARY: % of % tests passed (100%%)', + RAISE NOTICE 'SUMMARY: % of % template parser tests passed (100%%)', passed_tests, total_tests; RAISE NOTICE '------------------------------------'; ELSE RAISE WARNING '------------------------------------'; - RAISE WARNING 'SUMMARY: % of % tests passed (%)', + RAISE WARNING 'SUMMARY: % of % template parser tests passed (%)', passed_tests, total_tests, round((passed_tests::numeric / total_tests::numeric) * 100, 2) || '%'; @@ -1077,6 +1077,6 @@ SECTION: iterator="section", collection="page.sections"$expected40$ END IF; IF passed_tests != total_tests THEN - RAISE EXCEPTION 'Tests failed: % of % tests did not pass', (total_tests - passed_tests), total_tests; + RAISE EXCEPTION 'Tests failed: % of % template parser tests did not pass', (total_tests - passed_tests), total_tests; END IF; END $$; \ No newline at end of file