From d04a78de23a8fe3d9575ffa39f21c92175084a5f Mon Sep 17 00:00:00 2001 From: yukkop Date: Mon, 7 Apr 2025 16:54:11 +0000 Subject: [PATCH] feat: `hectic C`: logging settings --- flake.nix | 2 +- package/c/hectic/hectic.c | 349 +++++++++++++++++++++++- package/c/hectic/hectic.h | 59 ++++ package/c/hmpl/hmpl.c | 36 ++- package/c/hmpl/hmpl.h | 34 ++- package/c/hmpl/main.c | 2 +- package/c/hmpl/test/test_section_tags.c | 313 +++++++++++++++++++++ 7 files changed, 773 insertions(+), 22 deletions(-) create mode 100755 package/c/hmpl/test/test_section_tags.c diff --git a/flake.nix b/flake.nix index 60ec1bc..92ca0a2 100644 --- a/flake.nix +++ b/flake.nix @@ -140,7 +140,7 @@ shells = self.devShells.${system}; in { c = pkgs.mkShell { - buildInputs = (with pkgs; [ inotify-tools gdb gcc ]) ++ (with self.packages.${system}; [ hectic nvim-pager watch ]); + buildInputs = (with pkgs; [ inotify-tools gdb gcc ]) ++ (with self.packages.${system}; [ c-hectic nvim-pager watch ]); PAGER = "${self.packages.${system}.nvim-pager}/bin/pager"; }; default = pkgs.mkShell { diff --git a/package/c/hectic/hectic.c b/package/c/hectic/hectic.c index 440e9d4..ee7a955 100644 --- a/package/c/hectic/hectic.c +++ b/package/c/hectic/hectic.c @@ -1,8 +1,46 @@ #include "hectic.h" +#include +#include // For strdup, strchr, etc. + +// On systems without strsep, provide a custom implementation +#ifndef _GNU_SOURCE +#define _GNU_SOURCE 1 +#endif + +#ifndef HAVE_STRSEP +char *strsep(char **stringp, const char *delim) { + char *start = *stringp; + char *p; + + if (!start) + return NULL; + + p = start; + while (*p && !strchr(delim, *p)) + p++; + + if (*p) { + *p++ = '\0'; + *stringp = p; + } else { + *stringp = NULL; + } + + return start; +} +#endif + +// Forward declarations +void free_log_rules(); +const char* json_type_to_string(JsonType type); // Global color mode variable definition ColorMode color_mode = COLOR_MODE_AUTO; +// Global logging variables +LogLevel current_log_level = LOG_LEVEL_INFO; +LogRule *log_rules = NULL; // Linked list of log rules + const char* color_mode_to_string(ColorMode mode) { switch (mode) { case COLOR_MODE_AUTO: return "AUTO"; @@ -74,25 +112,35 @@ LogLevel log_level_from_string(const char *level_str) { return LOG_LEVEL_INFO; } -LogLevel current_log_level = LOG_LEVEL_INFO; - void logger_level_reset() { current_log_level = LOG_LEVEL_INFO; + free_log_rules(); } void logger_level(LogLevel level) { current_log_level = level; + free_log_rules(); // Clear any complex rules } void init_logger(void) { - // Read log level from environment + // Read log level or rules from environment const char* env_level = getenv("LOG_LEVEL"); - current_log_level = log_level_from_string(env_level); - // Log initialization with appropriate message if (env_level) { - fprintf(stderr, "INIT: Logger initialized with level %s from environment\n", - log_level_to_string(current_log_level)); + // Check if it's a complex rule format (contains '=' or ',') + if (strchr(env_level, '=') || strchr(env_level, ',')) { + if (logger_parse_rules(env_level)) { + fprintf(stderr, "INIT: Logger initialized with complex rules from environment\n"); + } else { + fprintf(stderr, "INIT: Failed to parse complex log rules, using default level INFO\n"); + current_log_level = LOG_LEVEL_INFO; + } + } else { + // Simple log level + current_log_level = log_level_from_string(env_level); + fprintf(stderr, "INIT: Logger initialized with level %s from environment\n", + log_level_to_string(current_log_level)); + } } else { fprintf(stderr, "INIT: Logger initialized with default level %s\n", log_level_to_string(current_log_level)); @@ -106,7 +154,9 @@ char* raise_message( int line, const char *format, ...) { - if (level < current_log_level) { + // Check against the effective log level for this context + LogLevel effective_level = logger_get_effective_level(file, func, line); + if (level < effective_level) { return NULL; } @@ -1086,4 +1136,287 @@ char* json_to_debug_str(Arena *arena, Json json) { strcat(result, "}"); return result; +} + +// Clean up existing log rules +void free_log_rules() { + LogRule *rule = log_rules; + while (rule) { + LogRule *next = rule->next; + if (rule->file_pattern) free(rule->file_pattern); + if (rule->function_pattern) free(rule->function_pattern); + free(rule); + rule = next; + } + log_rules = NULL; +} + +// Add a new log rule to the rule chain +LogRule* add_log_rule(LogLevel level, const char *file_pattern, const char *function_pattern, + int line_start, int line_end) { + LogRule *rule = (LogRule*)malloc(sizeof(LogRule)); + if (!rule) return NULL; + + rule->level = level; + rule->file_pattern = file_pattern ? strdup(file_pattern) : NULL; + rule->function_pattern = function_pattern ? strdup(function_pattern) : NULL; + rule->line_start = line_start; + rule->line_end = line_end; + rule->next = NULL; + + // Add to the end of the list + if (!log_rules) { + log_rules = rule; + } else { + LogRule *last = log_rules; + while (last->next) { + last = last->next; + } + last->next = rule; + } + + return rule; +} + +// Parse a line range specification (start:end) +void parse_line_range(const char *range_str, int *start, int *end) { + if (!range_str) { + *start = -1; + *end = -1; + return; + } + + char *endptr; + *start = strtol(range_str, &endptr, 10); + + if (*endptr == ':') { + *end = strtol(endptr + 1, NULL, 10); + } else { + *end = *start; + } + + if (*start <= 0) *start = -1; + if (*end <= 0) *end = -1; +} + +// Parse a complex rule string and set up log rules +int logger_parse_rules(const char *rules_str) { + if (!rules_str || !*rules_str) return 0; + + // Clean up existing rules + free_log_rules(); + + // Make a copy of the rules string since we'll be modifying it + char *rules_copy = strdup(rules_str); + if (!rules_copy) return 0; + + // First rule sets the default level + char *next_rule = rules_copy; + char *token = strsep(&next_rule, ","); + current_log_level = log_level_from_string(token); + + // Process the remaining rules + while (next_rule && *next_rule) { + // Extract rule definition: pattern=level + char *rule_def = strsep(&next_rule, ","); + char *level_str = strchr(rule_def, '='); + + if (!level_str) continue; // Invalid rule + + *level_str = '\0'; // Split pattern and level + level_str++; + + // Parse the rule pattern + char *pattern = rule_def; + char *file_pattern = NULL; + char *function_pattern = NULL; + char *line_range = NULL; + + // Check for line range in file pattern + char *at_sign = strchr(pattern, '@'); + if (at_sign) { + *at_sign = '\0'; + file_pattern = pattern; + pattern = at_sign + 1; + + // Check for line range or another @ for function + char *colon = strchr(pattern, ':'); + char *second_at = strchr(pattern, '@'); + + if (second_at && (!colon || second_at < colon)) { + // Format: file@function@line_range + *second_at = '\0'; + function_pattern = pattern; + line_range = second_at + 1; + } else if (colon) { + // Format: file@line_range + line_range = pattern; + } else { + // Format: file@function + function_pattern = pattern; + } + } else { + // Just file pattern + file_pattern = pattern; + } + + // If file pattern is empty, set to NULL + if (file_pattern && !*file_pattern) file_pattern = NULL; + + // If function pattern is empty, set to NULL + if (function_pattern && !*function_pattern) function_pattern = NULL; + + // Parse line range + int line_start = -1, line_end = -1; + parse_line_range(line_range, &line_start, &line_end); + + // Create a new rule + LogLevel level = log_level_from_string(level_str); + add_log_rule(level, file_pattern, function_pattern, line_start, line_end); + } + + free(rules_copy); + return 1; +} + +// Check if a file matches a pattern +static int match_file_pattern(const char *file, const char *pattern) { + if (!pattern) return 1; // NULL pattern matches any file + + // Extract the filename part without the path + const char *filename = strrchr(file, '/'); + if (!filename) filename = file; + else filename++; // Skip the '/' + + return fnmatch(pattern, filename, 0) == 0 || fnmatch(pattern, file, 0) == 0; +} + +// Check if a function matches a pattern +static int match_function_pattern(const char *func, const char *pattern) { + if (!pattern) return 1; // NULL pattern matches any function + return fnmatch(pattern, func, 0) == 0; +} + +// Get the effective log level for a specific context +LogLevel logger_get_effective_level(const char *file, const char *func, int line) { + // If no rules are defined, use the global level + if (!log_rules) return current_log_level; + + // Default to the global log level + LogLevel effective_level = current_log_level; + + // Check each rule in order + for (LogRule *rule = log_rules; rule; rule = rule->next) { + int file_match = match_file_pattern(file, rule->file_pattern); + int function_match = match_function_pattern(func, rule->function_pattern); + int line_match = (rule->line_start == -1 || (line >= rule->line_start && + (rule->line_end == -1 || line <= rule->line_end))); + + // If all conditions match, use this rule's level + if (file_match && function_match && line_match) { + effective_level = rule->level; + // Don't break here - later rules can override earlier ones + } + } + + return effective_level; +} + +// Add a new log rule programmatically +int logger_add_rule(LogLevel level, const char *file_pattern, const char *function_pattern, + int line_start, int line_end) { + return add_log_rule(level, file_pattern, function_pattern, line_start, line_end) != NULL; +} + +// Print all current logging rules to stderr +void logger_print_rules() { + fprintf(stderr, "Current logging rules:\n"); + fprintf(stderr, " Default level: %s\n", log_level_to_string(current_log_level)); + + int rule_count = 0; + for (LogRule *rule = log_rules; rule; rule = rule->next) { + fprintf(stderr, " Rule %d: Level=%s, File=%s, Function=%s, Lines=%d:%d\n", + ++rule_count, + log_level_to_string(rule->level), + rule->file_pattern ? rule->file_pattern : "", + rule->function_pattern ? rule->function_pattern : "", + rule->line_start, rule->line_end); + } + + if (rule_count == 0) { + fprintf(stderr, " No specific rules defined\n"); + } +} + +// Helper to format a rule as a string +static void format_rule_to_buffer(char *buffer, size_t size, LogRule *rule) { + char line_range[32] = ""; + + // Format line range if specified + if (rule->line_start > 0) { + if (rule->line_end > 0 && rule->line_end != rule->line_start) { + snprintf(line_range, sizeof(line_range), "%d:%d", rule->line_start, rule->line_end); + } else { + snprintf(line_range, sizeof(line_range), "%d", rule->line_start); + } + } + + // Format the complete rule + if (rule->file_pattern && rule->function_pattern && line_range[0]) { + // File + function + line range + snprintf(buffer, size, "%s@%s@%s=%s", + rule->file_pattern, rule->function_pattern, line_range, + log_level_to_string(rule->level)); + } else if (rule->file_pattern && rule->function_pattern) { + // File + function + snprintf(buffer, size, "%s@%s=%s", + rule->file_pattern, rule->function_pattern, + log_level_to_string(rule->level)); + } else if (rule->file_pattern && line_range[0]) { + // File + line range + snprintf(buffer, size, "%s@%s=%s", + rule->file_pattern, line_range, + log_level_to_string(rule->level)); + } else if (rule->file_pattern) { + // Just file + snprintf(buffer, size, "%s=%s", + rule->file_pattern, + log_level_to_string(rule->level)); + } else { + // Empty rule (shouldn't happen) + snprintf(buffer, size, "EMPTY=%s", log_level_to_string(rule->level)); + } +} + +// Format all rules into a string +char* logger_rules_to_string(Arena *arena) { + if (!arena) return NULL; + + // Allocate a buffer in the arena (estimate size needed) + size_t estimated_size = 1024; // Start with 1KB + char *buffer = arena_alloc(arena, estimated_size); + if (!buffer) return NULL; + + // Initialize with default level + int pos = snprintf(buffer, estimated_size, "%s", log_level_to_string(current_log_level)); + + // Add each rule + for (LogRule *rule = log_rules; rule; rule = rule->next) { + // Format the rule + char rule_str[256]; + format_rule_to_buffer(rule_str, sizeof(rule_str), rule); + + // Check buffer space and add to result + if (pos + strlen(rule_str) + 2 < estimated_size) { + buffer[pos++] = ','; + strcpy(buffer + pos, rule_str); + pos += strlen(rule_str); + } else { + // Buffer too small, just stop + strcat(buffer, ",..."); + break; + } + } + + return buffer; } \ No newline at end of file diff --git a/package/c/hectic/hectic.h b/package/c/hectic/hectic.h index d34f9af..266ea16 100644 --- a/package/c/hectic/hectic.h +++ b/package/c/hectic/hectic.h @@ -129,6 +129,19 @@ typedef enum { LOG_LEVEL_EXCEPTION } LogLevel; +/** + * Structure for complex log level rule + * Allows specifying log levels per file, function, and line range + */ +typedef struct LogRule { + LogLevel level; // Log level for this rule + char *file_pattern; // File pattern to match (can be NULL) + char *function_pattern; // Function pattern to match (can be NULL) + int line_start; // Start line number (-1 for any) + int line_end; // End line number (-1 for any) + struct LogRule *next; // Next rule in the chain +} LogRule; + void logger_level_reset(); void init_logger(void); @@ -137,6 +150,39 @@ void logger_level(LogLevel level); LogLevel log_level_from_string(const char *level_str); +/** + * Set complex logging rules from a string + * Format: DEFAULT_LEVEL,@=LEVEL,@:=LEVEL,... + * Example: "INFO,main.c@main=DEBUG,helper.c@10:50=TRACE" + * + * @param rules_str The rule string to parse + * @return 1 on success, 0 on failure + */ +int logger_parse_rules(const char *rules_str); + +/** + * Set complex logging rule programmatically + * + * @param level Log level for this rule + * @param file_pattern File pattern to match (NULL for any file) + * @param function_pattern Function pattern to match (NULL for any function) + * @param line_start Start line number (-1 for any) + * @param line_end End line number (-1 for any) + * @return 1 on success, 0 on failure + */ +int logger_add_rule(LogLevel level, const char *file_pattern, const char *function_pattern, + int line_start, int line_end); + +/** + * Get the effective log level for a message based on complex rules + * + * @param file Source file where log was generated + * @param func Function where log was generated + * @param line Line number where log was generated + * @return The effective log level for this context + */ +LogLevel logger_get_effective_level(const char *file, const char *func, int line); + /** * Core logging function that formats and outputs log messages. * @@ -386,4 +432,17 @@ char* json_to_debug_str(Arena *arena, Json json); #define DEBUGSTR_Slice(arena, value) slice_to_debug_str(arena, value) #define DEBUGSTR_Json(arena, value) json_to_debug_str(arena, value) +/** + * Print all current logging rules to stderr for debugging + */ +void logger_print_rules(); + +/** + * Dump all active logging rules into a string + * + * @param arena Memory arena to allocate the string in + * @return String representation of all rules, or NULL on error + */ +char* logger_rules_to_string(Arena *arena); + #endif // EPRINTF_H \ No newline at end of file diff --git a/package/c/hmpl/hmpl.c b/package/c/hmpl/hmpl.c index 12c01b7..742212d 100644 --- a/package/c/hmpl/hmpl.c +++ b/package/c/hmpl/hmpl.c @@ -1,7 +1,7 @@ #include "hmpl.h" Json *eval_object(Arena *arena, const Json * const context, const char * const query) { - raise_debug("eval_object(%p, %s, %s)", arena, json_to_string(arena, context), query); + raise_debug("eval_object(%p, %s, %s)", arena, json_to_string(DISPOSABLE_ARENA, context), query); if (!context || !query) return NULL; const Json *res = context; @@ -9,14 +9,14 @@ Json *eval_object(Arena *arena, const Json * const context, const char * const q while ((dot = strchr(key, '.')) != NULL) { *dot = '\0'; - raise_debug("res: %s, key: %s, query: %s", json_to_string(arena, res), key, query); + raise_debug("eval_object: key: %s", key); res = json_get_object_item(res, key); if (!res) return NULL; key = dot + 1; } - raise_debug("res: %s, key: %s, query: %s", json_to_string(arena, res), key, query); + raise_debug("eval_object: final key: %s", key); return json_get_object_item(res, key); } @@ -58,8 +58,8 @@ void hmpl_render_interpolation_tags(Arena *arena, char **text_ptr, const Json * continue; } - // Вычисляем длину замены от начала {{[prefix] до конца }} - int replace_length = (end - start) + 2; // +2 для "}}" + // Calculate the replacement length from the beginning of {{[prefix] to the end }} + int replace_length = (end - start) + 2; // +2 for "}}" char *new_text = arena_repstr(arena, current_text, start_index, @@ -74,6 +74,10 @@ void hmpl_render_interpolation_tags(Arena *arena, char **text_ptr, const Json * } } +void hmpl_render_interpolation_tags_opts(Arena *arena, char **text_ptr, const Json *context, const HmplInterpolationTagsOptions *options) { + hmpl_render_interpolation_tags(arena, text_ptr, context, options->prefix); +} + // {{item#array}}...{{/array}} void hmpl_render_section_tags(Arena *arena, char **text_ptr, Json *context, const char * const prefix_start, const char * const prefix_end, const char * const separator_pattern) { raise_debug("hmpl_render_section_tags(%p, %s, , %s, %s, %s)", arena, *text_ptr, prefix_start, prefix_end, separator_pattern); @@ -207,9 +211,15 @@ void hmpl_render_section_tags(Arena *arena, char **text_ptr, Json *context, cons char *prefix = arena_alloc(arena, element_name_length + 2); snprintf(prefix, element_name_length + 2, "%s.", element_name); + raise_debug("Processing element with prefix: %s", prefix); + raise_debug("Block before processing: %s", block); hmpl_render_interpolation_tags(arena, &block, elem, prefix); - raise_trace("block after: %s", block); + raise_debug("Block after interpolation: %s", block); + + // Recursively process nested sections + hmpl_render_section_tags(arena, &block, elem, prefix_start, prefix_end, separator_pattern); + raise_debug("Block after section processing: %s", block); size_t block_len = strlen(block); memcpy(replacement + replacement_offset, block, block_len); @@ -239,19 +249,25 @@ void hmpl_render_section_tags(Arena *arena, char **text_ptr, Json *context, cons } } -void hmpl_render_with_arena(Arena *arena, char **text, const Json * const context) { +void hmpl_render_section_tags_opts(Arena *arena, char **text_ptr, const Json *context, const HmplSectionTagsOptions *options) { + // Create a copy of the context without const qualifier for compatibility with hmpl_render_section_tags + hmpl_render_section_tags(arena, text_ptr, (Json*)context, options->prefix_start, options->prefix_end, options->separator_pattern); +} + +void hmpl_render_with_arena(Arena *arena, char **text, const Json * const context, const HmplOptions * const options) { if (context->type != JSON_OBJECT) { raise_exception("Malformed context: context is not json"); exit(1); } - hmpl_render_interpolation_tags(arena, text, context, ""); + hmpl_render_interpolation_tags_opts(arena, text, context, &options->interpolation_tags_options); + hmpl_render_section_tags_opts(arena, text, context, &options->section_tags_options); } -void hmpl_render(char **text, const Json * const context) { +void hmpl_render(char **text, const Json * const context, const HmplOptions * const options) { Arena arena = arena_init(MEM_MiB); - hmpl_render_with_arena(&arena, text, context); + hmpl_render_with_arena(&arena, text, context, options); arena_free(&arena); } diff --git a/package/c/hmpl/hmpl.h b/package/c/hmpl/hmpl.h index 1e93406..7f48bff 100644 --- a/package/c/hmpl/hmpl.h +++ b/package/c/hmpl/hmpl.h @@ -8,6 +8,32 @@ #include #include "hectic.h" +typedef struct { + char *prefix_start; + char *prefix_end; + char *separator_pattern; +} HmplSectionTagsOptions; + +typedef struct { + char *prefix; +} HmplInterpolationTagsOptions; + +typedef struct { + HmplSectionTagsOptions section_tags_options; + HmplInterpolationTagsOptions interpolation_tags_options; +} HmplOptions; + +static const HmplOptions DEFAULT_OPTIONS = { + .section_tags_options = { + .prefix_start = "", + .prefix_end = "/", + .separator_pattern = "#" + }, + .interpolation_tags_options = { + .prefix = "" + } +}; + void init_cjson_with_arenas(Arena *arena); char *eval_string(Arena *arena, const Json * const context, const char * const key); @@ -15,10 +41,14 @@ char *eval_string(Arena *arena, const Json * const context, const char * const k /* Modified: text is passed by reference so we can update it and free old allocations */ void hmpl_render_interpolation_tags(Arena *arena, char **text_ptr, const Json * const context, const char * const prefix); +void hmpl_render_interpolation_tags_opts(Arena *arena, char **text_ptr, const Json *context, const HmplInterpolationTagsOptions *options); + void hmpl_render_section_tags(Arena *arena, char **text_ptr, Json *context, const char * const prefix_start, const char * const prefix_end, const char * const separator_pattern); -void hmpl_render_with_arena(Arena *arena, char **text, const Json * const context); +void hmpl_render_section_tags_opts(Arena *arena, char **text_ptr, const Json *context, const HmplSectionTagsOptions *options); -void hmpl_render(char **text, const Json * const context); +void hmpl_render_with_arena(Arena *arena, char **text, const Json * const context, const HmplOptions * const options); + +void hmpl_render(char **text, const Json * const context, const HmplOptions * const options); #endif // EPRINTF_HMPL diff --git a/package/c/hmpl/main.c b/package/c/hmpl/main.c index d986674..bc01900 100644 --- a/package/c/hmpl/main.c +++ b/package/c/hmpl/main.c @@ -38,7 +38,7 @@ int main(int argc, char *argv[]) { text = arena_strdup(&arena, ""); } - hmpl_render_with_arena(&arena, &text, context); + hmpl_render_with_arena(&arena, &text, context, &DEFAULT_OPTIONS); printf("%s", text); arena_free(&arena); diff --git a/package/c/hmpl/test/test_section_tags.c b/package/c/hmpl/test/test_section_tags.c new file mode 100755 index 0000000..4fbe52d --- /dev/null +++ b/package/c/hmpl/test/test_section_tags.c @@ -0,0 +1,313 @@ +#include "../hmpl.h" +#include "hectic.h" +#include +#include +#include + +const HmplOptions options = { + .section_tags_options = { + .prefix_start = "", + .prefix_end = "/", + .separator_pattern = "#" + }, + .interpolation_tags_options = { + .prefix = "" + } +}; + +// Function for comparing results +void assert_rendered(const char* template, const char* expected, const char* json_str) { + Arena arena = arena_init(MEM_KiB); + + // Parse JSON string into an object + Json *json = json_parse(&arena, &json_str); + if (!json) { + printf("ERROR: Failed to parse JSON: %s\n", json_str); + exit(1); + } + + // Create a copy of the template for modification + char *result = arena_strdup(&arena, template); + + // Render the template + hmpl_render_with_arena(&arena, &result, json, &options); + + // Check the result + if (strcmp(result, expected) != 0) { + printf("ERROR:\n"); + printf("Template: %s\n", template); + printf("Expected: %s\n", expected); + printf("Received: %s\n", result); + exit(1); + } else { + printf("SUCCESS: Template correctly rendered\n"); + } + + arena_free(&arena); +} + +// -------------------------------------------------- +// -- Simple section tag with surrounding text -- +// -------------------------------------------------- + +#define TEST_SIMPLE_SECTION_CONTEXT \ + "{" \ + " \"users\": [" \ + " {\"name\": \"John\", \"age\": 30}," \ + " {\"name\": \"Mary\", \"age\": 25}," \ + " {\"name\": \"Alex\", \"age\": 35}" \ + " ]," \ + " \"count\": 3" \ + "}" + +#define TEST_SIMPLE_SECTION_TEMPLATE \ + "User list:\n" \ + "{{item#users}}
  • {{item.name}}, age: {{item.age}}
  • {{/users}}\n" \ + "Total users: {{count}}" + +#define TEST_SIMPLE_SECTION_RESULT \ + "User list:\n" \ + "
  • John, age: 30
  • Mary, age: 25
  • Alex, age: 35
  • \n" \ + "Total users: 3" + +void test_simple_section_tags(Arena *arena) { + raise_notice("Testing simple section tag with surrounding text"); + const char *context_text = arena_strdup(arena, TEST_SIMPLE_SECTION_CONTEXT); + Json *context = json_parse(arena, &context_text); + if (!context) { raise_exception("Malformed json"); exit(1); } + + char *text = arena_strdup(arena, TEST_SIMPLE_SECTION_TEMPLATE); + raise_notice("Template:\n%s", text); + raise_notice("Context: %s", json_to_string(arena, context)); + + hmpl_render_with_arena(arena, &text, context, &options); + raise_notice("Result:\n%s", text); + + assert(strcmp(text, TEST_SIMPLE_SECTION_RESULT) == 0); +} + +// ----------------------------------- +// -- Nested section tags -- +// ----------------------------------- + +#define TEST_NESTED_SECTION_CONTEXT \ + "{" \ + " \"department\": \"Development\"," \ + " \"teams\": [" \ + " {" \ + " \"name\": \"Frontend\"," \ + " \"members\": [" \ + " {\"name\": \"John\", \"role\": \"Developer\"}," \ + " {\"name\": \"Mary\", \"role\": \"Designer\"}" \ + " ]" \ + " }," \ + " {" \ + " \"name\": \"Backend\"," \ + " \"members\": [" \ + " {\"name\": \"Alex\", \"role\": \"Developer\"}," \ + " {\"name\": \"Helen\", \"role\": \"Tester\"}" \ + " ]" \ + " }" \ + " ]" \ + "}" + +#define TEST_NESTED_SECTION_TEMPLATE \ + "Department: {{department}}\n" \ + "{{item#teams}}Team: {{item.name}}\n" \ + " {{item#item.members}}Member: {{item.name}} ({{item.role}}){{/item.members}}\n" \ + "{{/teams}}" + +#define TEST_NESTED_SECTION_RESULT \ + "Department: Development\n" \ + "Team: Frontend\n" \ + " Member: John (Developer)Member: Mary (Designer)\n" \ + "Team: Backend\n" \ + " Member: Alex (Developer)Member: Helen (Tester)\n" + +void test_nested_section_tags(Arena *arena) { + raise_notice("Testing nested section tags"); + const char *context_text = arena_strdup(arena, TEST_NESTED_SECTION_CONTEXT); + Json *context = json_parse(arena, &context_text); + if (!context) { raise_exception("Malformed json"); exit(1); } + + char *text = arena_strdup(arena, TEST_NESTED_SECTION_TEMPLATE); + raise_notice("Template:\n%s", text); + raise_notice("Context: %s", json_to_string(arena, context)); + + hmpl_render_with_arena(arena, &text, context, &options); + raise_notice("Result:\n%s", text); + + assert(strcmp(text, TEST_NESTED_SECTION_RESULT) == 0); +} + +// ----------------------------------- +// -- Empty array in section tag -- +// ----------------------------------- + +#define TEST_EMPTY_ARRAY_CONTEXT \ + "{" \ + " \"tasks\": []" \ + "}" + +#define TEST_EMPTY_ARRAY_TEMPLATE \ + "Tasks: {{item#tasks}}ID: {{item.id}} - {{item.description}}{{/tasks}}" + +#define TEST_EMPTY_ARRAY_RESULT \ + "Tasks: " + +void test_empty_array_section_tags(Arena *arena) { + raise_notice("Testing empty array in section tag"); + const char *context_text = arena_strdup(arena, TEST_EMPTY_ARRAY_CONTEXT); + Json *context = json_parse(arena, &context_text); + if (!context) { raise_exception("Malformed json"); exit(1); } + + char *text = arena_strdup(arena, TEST_EMPTY_ARRAY_TEMPLATE); + raise_notice("Template:\n%s", text); + raise_notice("Context: %s", json_to_string(arena, context)); + + hmpl_render_with_arena(arena, &text, context, &options); + raise_notice("Result:\n%s", text); + + assert(strcmp(text, TEST_EMPTY_ARRAY_RESULT) == 0); +} + +// ----------------------------------- +// -- HTML template with section tags -- +// ----------------------------------- + +#define TEST_HTML_CONTEXT \ + "{" \ + " \"title\": \"My List\"," \ + " \"items\": [" \ + " {\"name\": \"Item 1\", \"type\": \"important\"}," \ + " {\"name\": \"Item 2\", \"type\": \"normal\"}," \ + " {\"name\": \"Item 3\", \"type\": \"normal\"}" \ + " ]," \ + " \"footer\": \"© 2023\"" \ + "}" + +#define TEST_HTML_TEMPLATE \ + "\n" \ + "\n" \ + "\n" \ + " {{title}}\n" \ + "\n" \ + "\n" \ + "

    {{title}}

    \n" \ + "
      \n" \ + " {{item#items}}
    • {{item.name}}
    • {{/items}}\n" \ + "
    \n" \ + "
    {{footer}}
    \n" \ + "\n" \ + "" + +#define TEST_HTML_RESULT \ + "\n" \ + "\n" \ + "\n" \ + " My List\n" \ + "\n" \ + "\n" \ + "

    My List

    \n" \ + "
      \n" \ + "
    • Item 1
    • Item 2
    • Item 3
    • \n" \ + "
    \n" \ + " \n" \ + "\n" \ + "" + +void test_html_section_tags(Arena *arena) { + raise_notice("Testing HTML template with section tags"); + const char *context_text = arena_strdup(arena, TEST_HTML_CONTEXT); + Json *context = json_parse(arena, &context_text); + if (!context) { raise_exception("Malformed json"); exit(1); } + + char *text = arena_strdup(arena, TEST_HTML_TEMPLATE); + raise_notice("Template:\n%s", text); + raise_notice("Context: %s", json_to_string(arena, context)); + + hmpl_render_with_arena(arena, &text, context, &options); + raise_notice("Result:\n%s", text); + + assert(strcmp(text, TEST_HTML_RESULT) == 0); +} + +// ----------------------------------- +// -- Report with nested sections -- +// ----------------------------------- + +#define TEST_REPORT_CONTEXT \ + "{" \ + " \"period\": \"March 2023\"," \ + " \"data\": [" \ + " {" \ + " \"title\": \"Sales\"," \ + " \"value\": 1000," \ + " \"details\": [" \ + " {\"name\": \"Product A\", \"value\": 500}," \ + " {\"name\": \"Product B\", \"value\": 300}," \ + " {\"name\": \"Product C\", \"value\": 200}" \ + " ]" \ + " }," \ + " {" \ + " \"title\": \"Expenses\"," \ + " \"value\": 700," \ + " \"details\": [" \ + " {\"name\": \"Rent\", \"value\": 300}," \ + " {\"name\": \"Salary\", \"value\": 400}" \ + " ]" \ + " }" \ + " ]," \ + " \"summary\": 300" \ + "}" + +#define TEST_REPORT_TEMPLATE \ + "Report for {{period}}\n\n" \ + "{{row#data}}* {{row.title}}: {{row.value}}\n" \ + " {{detail#row.details}} - {{detail.name}}: {{detail.value}}\n{{/row.details}}\n" \ + "{{/data}}\n" \ + "Total: {{summary}}" + +#define TEST_REPORT_RESULT \ + "Report for March 2023\n\n" \ + "* Sales: 1000\n" \ + " - Product A: 500\n - Product B: 300\n - Product C: 200\n\n" \ + "* Expenses: 700\n" \ + " - Rent: 300\n - Salary: 400\n\n" \ + "Total: 300" + +void test_report_section_tags(Arena *arena) { + raise_notice("Testing report with nested sections"); + const char *context_text = arena_strdup(arena, TEST_REPORT_CONTEXT); + Json *context = json_parse(arena, &context_text); + if (!context) { raise_exception("Malformed json"); exit(1); } + + char *text = arena_strdup(arena, TEST_REPORT_TEMPLATE); + raise_notice("Template:\n%s", text); + raise_notice("Context: %s", json_to_string(arena, context)); + + hmpl_render_with_arena(arena, &text, context, &options); + raise_notice("Result:\n%s", text); + + assert(strcmp(text, TEST_REPORT_RESULT) == 0); +} + +int main(void) { + init_logger(); + Arena arena = arena_init(MEM_MiB); + + test_simple_section_tags(&arena); + arena_reset(&arena); + test_nested_section_tags(&arena); + //arena_reset(&arena); + //test_empty_array_section_tags(&arena); + //arena_reset(&arena); + //test_html_section_tags(&arena); + //arena_reset(&arena); + //test_report_section_tags(&arena); + + printf("All tests passed successfully!\n"); + + arena_free(&arena); + return 0; +} \ No newline at end of file