diff --git a/config.m4 b/config.m4 index b8ca6f2..9495a98 100644 --- a/config.m4 +++ b/config.m4 @@ -10,5 +10,5 @@ if test "$PHP_ATTRIBUTES_VALIDATION" != "no"; then fi PHP_ADD_LIBRARY(stdc++, 1, ATTRIBUTES_VALIDATION_SHARED_LIBADD) - PHP_NEW_EXTENSION(attributes_validation, attributes_validation.c src/av_validate_function.c src/av_call_function.c src/av_base_model.c src/av_exception.c src/av_model_configs.c src/helpers/av_wrappers.c src/helpers/av_options.c src/helpers/av_string.c src/helpers/av_error_messages.c src/helpers/av_value_to_string.c src/fields/av_field.c src/fields/av_alias.c src/validators/av_typehint_validator.c, $ext_shared) + PHP_NEW_EXTENSION(attributes_validation, attributes_validation.c src/av_validate_function.c src/av_call_function.c src/av_base_model.c src/av_exception.c src/av_model_configs.c src/helpers/av_wrappers.c src/helpers/av_options.c src/helpers/av_string.c src/helpers/av_error_messages.c src/helpers/av_replace_placeholders.c src/helpers/av_value_to_string.c src/fields/av_field.c src/fields/av_alias.c src/validators/av_typehint_validator.c, $ext_shared) fi diff --git a/src/helpers/av_error_messages.c b/src/helpers/av_error_messages.c index 4dfdd28..fcb5893 100644 --- a/src/helpers/av_error_messages.c +++ b/src/helpers/av_error_messages.c @@ -1,4 +1,5 @@ #include "av_error_messages.h" +#include "av_replace_placeholders.h" #include "../av_base_model.h" #include "Zend/zend_API.h" #include "Zend/zend_exceptions.h" @@ -17,97 +18,6 @@ static const char *av_error_type_messages[] = { [AV_ERROR_TYPE] = "The {field} must be {expected}.", }; -static zend_string *av_replace_placeholders(const char *template, size_t length, av_field *field, av_property_info *prop_info) -{ - struct { - const char *search; - size_t len; - size_t counts; - zend_string *replace; - } table[] = {{"{field}", sizeof("{field}") - 1, 0, field->name}, {"{value}", sizeof("{value}") - 1, 0, NULL}, {"{expected}", sizeof("{expected}") - 1, 0, NULL}}; - size_t table_size = sizeof(table) / sizeof(table[0]); - - size_t max_template_size = length; - size_t total_placeholders = 0; - for (size_t i = 0; i < table_size; i++) { - const char *search_pos = template; - while ((search_pos = php_memnstr(search_pos, table[i].search, table[i].len, template + length))) { - table[i].counts += 1; - search_pos += table[i].len; - } - - if (table[i].counts == 0) - continue; - - total_placeholders += table[i].counts; - - if (table[i].replace == NULL) { - if (i == 1) { // {value} - table[i].replace = av_value_to_string(field->value); - } else if (i == 2) { // {expected} - table[i].replace = build_union_type_string(prop_info->property->type); - } - } - max_template_size += table[i].counts * (ZSTR_LEN(table[i].replace) - table[i].len); - } - - if (total_placeholders == 0) { - return zend_string_init(template, length, 0); - } - - // Allocate and process in a single pass - zend_string *result = zend_string_alloc(max_template_size, 0); - char *output = ZSTR_VAL(result); - size_t output_pos = 0; - - const char *input = template; - const char *input_end = template + length; - - while (input < input_end) { - if (total_placeholders == 0) { - output[output_pos++] = *input++; - continue; - } - - bool replaced = false; - for (size_t i = 0; i < table_size; i++) { - if (table[i].counts == 0) - continue; - if (input + table[i].len > input_end) - continue; - if (memcmp(input, table[i].search, table[i].len) != 0) - continue; - - table[i].counts -= 1; - total_placeholders -= 1; - - memcpy(output + output_pos, ZSTR_VAL(table[i].replace), ZSTR_LEN(table[i].replace)); - output_pos += ZSTR_LEN(table[i].replace); - input += table[i].len; - replaced = true; - break; - } - - if (!replaced) { - output[output_pos++] = *input++; - } - } - - ZEND_ASSERT(output_pos == max_template_size); - - // Null-terminate and truncate to actual size - output[output_pos] = '\0'; - result = zend_string_truncate(result, output_pos, 0); - - for (size_t i = 1; i < table_size; i++) { - if (table[i].replace != NULL) { - zend_string_release(table[i].replace); - } - } - - return result; -} - static zend_always_inline void add_field_error_to_array(zval *errors_array, const char *error_message, size_t length) { zval error_msg; @@ -263,7 +173,7 @@ static zend_always_inline zend_string *build_union_only_basic_types(uint32_t pur return zend_string_truncate(result, output_pos, 0); } -static zend_always_inline zend_string *build_union_type_string(zend_type property_type) +zend_string *build_union_type_string(zend_type property_type) { uint32_t pure_mask = ZEND_TYPE_PURE_MASK(property_type); bool is_simple_union = !ZEND_TYPE_HAS_LIST(property_type) && ZEND_TYPE_IS_SET(property_type) && (pure_mask & (pure_mask - 1)) != 0; diff --git a/src/helpers/av_error_messages.h b/src/helpers/av_error_messages.h index f6416fe..ebde9e1 100644 --- a/src/helpers/av_error_messages.h +++ b/src/helpers/av_error_messages.h @@ -11,11 +11,14 @@ typedef enum { AV_ERROR_TYPE } av_error_type; +// Placeholder substitution +zend_string *av_replace_placeholders(const char *template, size_t length, av_field *field, av_property_info *prop_info); + // Error message generation static zend_string *generate_type_name(const zend_type *type); static bool is_type_enum(const zend_type *type); static zend_string *build_single_type_with_article(const zend_type *type); -static zend_string *build_union_type_string(zend_type property_type); +zend_string *build_union_type_string(zend_type property_type); static zend_string *generate_error_message(av_field *field, zend_type property_type); static bool av_vowel_sound(char c); diff --git a/src/helpers/av_replace_placeholders.c b/src/helpers/av_replace_placeholders.c new file mode 100644 index 0000000..e55a3ce --- /dev/null +++ b/src/helpers/av_replace_placeholders.c @@ -0,0 +1,110 @@ +#include "av_replace_placeholders.h" +#include "av_error_messages.h" +#include "av_value_to_string.h" +#include "av_wrappers.h" +#include + +/* + * Substitutes the {field}, {value} and {expected} placeholders of an error + * message template: + * + * {field} -> field->name + * {value} -> av_value_to_string(field->value) + * {expected} -> build_union_type_string(prop_info->property->type) + * + * Each placeholder may occur multiple times. The result is a freshly + * allocated zend_string that the caller must release with av_string_release. + * + * All Zend internals are reached through the mockable av_wrappers so the + * function can be unit tested in isolation. + */ +zend_string *av_replace_placeholders(const char *template, size_t length, av_field *field, av_property_info *prop_info) +{ + struct { + const char *search; + size_t len; + size_t counts; + zend_string *replace; + } table[] = {{"{field}", sizeof("{field}") - 1, 0, field->name}, {"{value}", sizeof("{value}") - 1, 0, NULL}, {"{expected}", sizeof("{expected}") - 1, 0, NULL}}; + size_t table_size = sizeof(table) / sizeof(table[0]); + + size_t max_template_size = length; + size_t total_placeholders = 0; + for (size_t i = 0; i < table_size; i++) { + const char *search_pos = template; + while ((search_pos = av_memnstr(search_pos, table[i].search, table[i].len, template + length))) { + table[i].counts += 1; + search_pos += table[i].len; + } + + if (table[i].counts == 0) + continue; + + total_placeholders += table[i].counts; + + if (table[i].replace == NULL) { + if (i == 1) { // {value} + table[i].replace = av_value_to_string(field->value); + } else if (i == 2) { // {expected} + table[i].replace = build_union_type_string(prop_info->property->type); + } + } + max_template_size += table[i].counts * (ZSTR_LEN(table[i].replace) - table[i].len); + } + + if (total_placeholders == 0) { + return av_string_init(template, length, 0); + } + + // Allocate and process in a single pass + zend_string *result = av_string_alloc(max_template_size, 0); + char *output = ZSTR_VAL(result); + size_t output_pos = 0; + + const char *input = template; + const char *input_end = template + length; + + while (input < input_end) { + if (total_placeholders == 0) { + output[output_pos++] = *input++; + continue; + } + + bool replaced = false; + for (size_t i = 0; i < table_size; i++) { + if (table[i].counts == 0) + continue; + if (input + table[i].len > input_end) + continue; + if (memcmp(input, table[i].search, table[i].len) != 0) + continue; + + table[i].counts -= 1; + total_placeholders -= 1; + + memcpy(output + output_pos, ZSTR_VAL(table[i].replace), ZSTR_LEN(table[i].replace)); + output_pos += ZSTR_LEN(table[i].replace); + input += table[i].len; + replaced = true; + break; + } + + if (!replaced) { + output[output_pos++] = *input++; + } + } + + ZEND_ASSERT(output_pos == max_template_size); + + // Null-terminate and truncate to actual size + output[output_pos] = '\0'; + result = av_string_truncate(result, output_pos, 0); + + for (size_t i = 1; i < table_size; i++) { + if (table[i].replace != NULL) { + av_string_release(table[i].replace); + } + } + + return result; +} diff --git a/src/helpers/av_replace_placeholders.h b/src/helpers/av_replace_placeholders.h new file mode 100644 index 0000000..a994d51 --- /dev/null +++ b/src/helpers/av_replace_placeholders.h @@ -0,0 +1,15 @@ +#ifndef AV_HELPERS_REPLACE_PLACEHOLDERS_H +#define AV_HELPERS_REPLACE_PLACEHOLDERS_H + +#include "av_structs.h" +#include + +/* + * Substitutes the {field}, {value} and {expected} placeholders of an error + * message template with the corresponding field/value/type strings. + * + * See av_replace_placeholders.c for the substitution rules. + */ +zend_string *av_replace_placeholders(const char *template, size_t length, av_field *field, av_property_info *prop_info); + +#endif /* AV_HELPERS_REPLACE_PLACEHOLDERS_H */ diff --git a/src/helpers/av_wrappers.c b/src/helpers/av_wrappers.c index c7eade9..255f104 100644 --- a/src/helpers/av_wrappers.c +++ b/src/helpers/av_wrappers.c @@ -1,4 +1,5 @@ #include "av_wrappers.h" +#include "php.h" #include "Zend/zend_API.h" #include "Zend/zend_interfaces.h" #include "Zend/zend_list.h" @@ -31,6 +32,16 @@ void av_efree(void *ptr) efree(ptr); } +zend_string *av_string_alloc(size_t length, bool persistent) +{ + return zend_string_alloc(length, persistent); +} + +zend_string *av_string_truncate(zend_string *s, size_t length, bool persistent) +{ + return zend_string_truncate(s, length, persistent); +} + zend_string *av_string_concat3(const char *str1, size_t str1_len, const char *str2, size_t str2_len, const char *str3, size_t str3_len) { return zend_string_concat3(str1, str1_len, str2, str2_len, str3, str3_len); @@ -84,3 +95,8 @@ void av_zval_ptr_dtor(zval *zval_ptr) { zval_ptr_dtor(zval_ptr); } + +const char *av_memnstr(const char *haystack, const char *needle, size_t needle_len, const char *end) +{ + return php_memnstr(haystack, needle, needle_len, end); +} diff --git a/src/helpers/av_wrappers.h b/src/helpers/av_wrappers.h index c39569c..6140c25 100644 --- a/src/helpers/av_wrappers.h +++ b/src/helpers/av_wrappers.h @@ -11,6 +11,8 @@ zend_string *av_string_init(const char *str, size_t len, bool persistent); void av_string_release(zend_string *s); void *av_emalloc(size_t size); void av_efree(void *ptr); +zend_string *av_string_alloc(size_t length, bool persistent); +zend_string *av_string_truncate(zend_string *s, size_t length, bool persistent); /* Additional wrappers used by av_value_to_string to allow mocking in unit tests. */ zend_string *av_string_concat3(const char *str1, size_t str1_len, const char *str2, size_t str2_len, const char *str3, size_t str3_len); @@ -23,4 +25,7 @@ const char *av_rsrc_list_get_rsrc_type(zend_resource *res); zend_result av_call_tostring(zend_object *object, zval *retval); void av_zval_ptr_dtor(zval *zval_ptr); +/* Additional wrappers used by av_replace_placeholders to allow mocking in unit tests. */ +const char *av_memnstr(const char *haystack, const char *needle, size_t needle_len, const char *end); + #endif /* AV_HELPERS_AV_WRAPPERS_H */ diff --git a/tests/c/support/test_helpers.c b/tests/c/support/test_helpers.c index d55846b..b769a61 100644 --- a/tests/c/support/test_helpers.c +++ b/tests/c/support/test_helpers.c @@ -40,3 +40,42 @@ void efree_stub(void *ptr, int num_calls) { free(ptr); } + +zend_string *string_alloc_stub(size_t length, bool persistent, int num_calls) +{ + zend_string *s = malloc(sizeof(zend_string) + length); + if (s) { + s->gc.refcount = 1; + s->gc.u.type_info = 0; + s->h = 0; + s->len = length; + s->val[length] = '\0'; + } + return s; +} + +zend_string *string_truncate_stub(zend_string *s, size_t length, bool persistent, int num_calls) +{ + if (s) { + s->len = length; + s->val[length] = '\0'; + } + return s; +} + +const char *memnstr_stub(const char *haystack, const char *needle, size_t needle_len, const char *end, int num_calls) +{ + (void)num_calls; + if (needle_len == 0) + return haystack; + + size_t haystack_len = (size_t)(end - haystack); + if (haystack_len < needle_len) + return NULL; + + for (const char *p = haystack; p <= end - needle_len; p++) { + if (memcmp(p, needle, needle_len) == 0) + return p; + } + return NULL; +} diff --git a/tests/c/support/test_helpers.h b/tests/c/support/test_helpers.h index 1d5ebed..0f0d8bc 100644 --- a/tests/c/support/test_helpers.h +++ b/tests/c/support/test_helpers.h @@ -8,5 +8,8 @@ extern zend_string *string_init_stub(const char *str, size_t len, bool persisten extern void string_release_stub(zend_string *s, int num_calls); extern void *emalloc_stub(size_t size, int num_calls); extern void efree_stub(void *ptr, int num_calls); +extern zend_string *string_alloc_stub(size_t length, bool persistent, int num_calls); +extern zend_string *string_truncate_stub(zend_string *s, size_t length, bool persistent, int num_calls); +extern const char *memnstr_stub(const char *haystack, const char *needle, size_t needle_len, const char *end, int num_calls); #endif /* TEST_HELPERS_H */ diff --git a/tests/c/test_error_messages.c b/tests/c/test_error_messages.c new file mode 100644 index 0000000..93e30d6 --- /dev/null +++ b/tests/c/test_error_messages.c @@ -0,0 +1,381 @@ +#include "unity.h" +#include "test_helpers.h" +#include "helpers/mock_av_wrappers.h" +#include "helpers/av_replace_placeholders.h" +#include "helpers/av_value_to_string.h" +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Mock callbacks for the wrappers used by av_replace_placeholders (and the +// real av_value_to_string it delegates {value} to). They build real zend_string +// allocations through the malloc-based stub so the produced strings can be +// inspected and freed with av_string_release. +// --------------------------------------------------------------------------- + +static zend_string *concat3_stub(const char *str1, size_t str1_len, const char *str2, size_t str2_len, const char *str3, size_t str3_len, int num_calls) +{ + size_t total = str1_len + str2_len + str3_len; + zend_string *s = string_alloc_stub(total, 0, num_calls); + char *p = s->val; + memcpy(p, str1, str1_len); + p += str1_len; + memcpy(p, str2, str2_len); + p += str2_len; + memcpy(p, str3, str3_len); + p += str3_len; + s->val[total] = '\0'; + s->len = total; + return s; +} + +static zend_string *string_copy_stub(zend_string *s, int num_calls) +{ + return string_init_stub(s->val, s->len, 0, num_calls); +} + +// Manual integer-to-string conversion: avoids snprintf(), which the PHP +// headers (pulled in transitively via av_structs.h -> php.h) redefine to +// ap_php_snprintf, an unresolved symbol in the Ceedling unit-test build. +static zend_string *long_to_str_stub(zend_long num, int num_calls) +{ + char buf[32]; + size_t len = 0; + + if (num == 0) { + buf[len++] = '0'; + } else { + zend_long n = num; + if (n < 0) { + buf[len++] = '-'; + n = -n; + } + char tmp[32]; + size_t tmp_len = 0; + while (n > 0) { + tmp[tmp_len++] = (char)('0' + (n % 10)); + n /= 10; + } + while (tmp_len > 0) + buf[len++] = tmp[--tmp_len]; + } + + return string_init_stub(buf, len, 0, num_calls); +} + +// Doubles are not exercised by these tests; provide a minimal stub. +static zend_string *double_to_str_stub(double num, int num_calls) +{ + (void)num; + return string_init_stub("0", 1, 0, num_calls); +} + +// --------------------------------------------------------------------------- +// Hand-written stub for build_union_type_string (the {expected} provider). +// The real implementation lives in av_error_messages.c, which pulls in the full +// Zend engine and is therefore not linked into this unit test. CMock cannot +// mock it in isolation because it shares its header with the function under +// test, so we provide a minimal definition returning a configurable string. +// --------------------------------------------------------------------------- + +static const char *g_expected_type_string; + +zend_string *build_union_type_string(zend_type property_type) +{ + (void)property_type; + return string_init_stub(g_expected_type_string, strlen(g_expected_type_string), 0, 0); +} + +// --------------------------------------------------------------------------- +// Helpers to build av_field / av_property_info without a running Zend engine. +// --------------------------------------------------------------------------- + +static av_field make_field(const char *name, zval *value) +{ + av_field field; + field.parent = NULL; + field.name = string_init_stub(name, strlen(name), 0, 0); + field.value = value; + return field; +} + +static av_property_info make_prop_info(zend_property_info *prop, zend_type *type) +{ + memset(prop, 0, sizeof(*prop)); + memset(type, 0, sizeof(*type)); + prop->type = *type; + av_property_info info; + info.model = NULL; + info.model_ce = NULL; + info.property = prop; + return info; +} + +// --------------------------------------------------------------------------- +// setUp / tearDown +// --------------------------------------------------------------------------- + +void setUp(void) +{ + av_string_init_Stub(string_init_stub); + av_string_release_Stub(string_release_stub); + av_emalloc_Stub(emalloc_stub); + av_efree_Stub(efree_stub); + av_string_alloc_Stub(string_alloc_stub); + av_string_truncate_Stub(string_truncate_stub); + av_string_concat3_Stub(concat3_stub); + av_string_copy_Stub(string_copy_stub); + av_long_to_str_Stub(long_to_str_stub); + av_double_to_str_Stub(double_to_str_stub); + av_memnstr_Stub(memnstr_stub); + + g_expected_type_string = "an integer"; +} + +void tearDown(void) +{} + +// --------------------------------------------------------------------------- +// No placeholders +// --------------------------------------------------------------------------- + +void test_no_placeholders_returns_copy_of_template(void) +{ + zval value; + ZVAL_NULL(&value); + av_field field = make_field("email", &value); + + const char *template = "A plain message without placeholders."; + zend_string *result = av_replace_placeholders(template, strlen(template), &field, NULL); + TEST_ASSERT_EQUAL_STRING(template, result->val); + av_string_release(result); + av_string_release(field.name); +} + +void test_empty_template_returns_empty_string(void) +{ + zval value; + ZVAL_NULL(&value); + av_field field = make_field("email", &value); + + zend_string *result = av_replace_placeholders("", 0, &field, NULL); + TEST_ASSERT_EQUAL_STRING("", result->val); + TEST_ASSERT_EQUAL(0, result->len); + av_string_release(result); + av_string_release(field.name); +} + +void test_text_without_placeholder_braces_is_not_replaced(void) +{ + zval value; + ZVAL_NULL(&value); + av_field field = make_field("name", &value); + + const char *template = "field {field field field}"; + zend_string *result = av_replace_placeholders(template, strlen(template), &field, NULL); + TEST_ASSERT_EQUAL_STRING(template, result->val); + av_string_release(result); + av_string_release(field.name); +} + +// --------------------------------------------------------------------------- +// {field} placeholder +// --------------------------------------------------------------------------- + +void test_field_placeholder_replaced_with_field_name(void) +{ + zval value; + ZVAL_NULL(&value); + av_field field = make_field("username", &value); + + const char *template = "The {field} field is required."; + zend_string *result = av_replace_placeholders(template, strlen(template), &field, NULL); + TEST_ASSERT_EQUAL_STRING("The username field is required.", result->val); + av_string_release(result); + av_string_release(field.name); +} + +void test_multiple_field_placeholders_all_replaced(void) +{ + zval value; + ZVAL_NULL(&value); + av_field field = make_field("name", &value); + + const char *template = "{field}, {field} and {field} again"; + zend_string *result = av_replace_placeholders(template, strlen(template), &field, NULL); + TEST_ASSERT_EQUAL_STRING("name, name and name again", result->val); + av_string_release(result); + av_string_release(field.name); +} + +void test_adjacent_field_placeholders(void) +{ + zval value; + ZVAL_NULL(&value); + av_field field = make_field("x", &value); + + const char *template = "{field}{field}"; + zend_string *result = av_replace_placeholders(template, strlen(template), &field, NULL); + TEST_ASSERT_EQUAL_STRING("xx", result->val); + av_string_release(result); + av_string_release(field.name); +} + +// --------------------------------------------------------------------------- +// {value} placeholder (delegates to av_value_to_string) +// --------------------------------------------------------------------------- + +void test_value_placeholder_replaced_with_null_value(void) +{ + zval value; + ZVAL_NULL(&value); + av_field field = make_field("count", &value); + + const char *template = "The {field} got {value}."; + zend_string *result = av_replace_placeholders(template, strlen(template), &field, NULL); + TEST_ASSERT_EQUAL_STRING("The count got null.", result->val); + av_string_release(result); + av_string_release(field.name); +} + +void test_value_placeholder_replaced_with_long_value(void) +{ + zval value; + ZVAL_LONG(&value, 42); + av_field field = make_field("count", &value); + + const char *template = "{field} expected a number, got {value}"; + zend_string *result = av_replace_placeholders(template, strlen(template), &field, NULL); + TEST_ASSERT_EQUAL_STRING("count expected a number, got 42", result->val); + av_string_release(result); + av_string_release(field.name); +} + +void test_value_placeholder_replaced_with_string_value(void) +{ + zval value; + ZVAL_STR(&value, string_init_stub("hello", strlen("hello"), 0, 0)); + av_field field = make_field("title", &value); + + const char *template = "{field} must be a string, got {value}"; + zend_string *result = av_replace_placeholders(template, strlen(template), &field, NULL); + TEST_ASSERT_EQUAL_STRING("title must be a string, got 'hello'", result->val); + av_string_release(result); + av_string_release(field.name); + av_string_release(value.value.str); +} + +void test_value_placeholder_replaced_with_boolean_value(void) +{ + zval value; + ZVAL_TRUE(&value); + av_field field = make_field("active", &value); + + const char *template = "The {field} is {value}"; + zend_string *result = av_replace_placeholders(template, strlen(template), &field, NULL); + TEST_ASSERT_EQUAL_STRING("The active is true", result->val); + av_string_release(result); + av_string_release(field.name); +} + +// --------------------------------------------------------------------------- +// {expected} placeholder (delegates to build_union_type_string, stubbed here) +// --------------------------------------------------------------------------- + +void test_expected_placeholder_replaced_with_type_string(void) +{ + g_expected_type_string = "an integer"; + + zval value; + ZVAL_NULL(&value); + av_field field = make_field("age", &value); + + zend_property_info prop; + zend_type type; + av_property_info prop_info = make_prop_info(&prop, &type); + + const char *template = "The {field} must be {expected}."; + zend_string *result = av_replace_placeholders(template, strlen(template), &field, &prop_info); + TEST_ASSERT_EQUAL_STRING("The age must be an integer.", result->val); + av_string_release(result); + av_string_release(field.name); +} + +void test_expected_placeholder_repeated(void) +{ + g_expected_type_string = "a string"; + + zval value; + ZVAL_NULL(&value); + av_field field = make_field("name", &value); + + zend_property_info prop; + zend_type type; + av_property_info prop_info = make_prop_info(&prop, &type); + + const char *template = "{field} must be {expected} or {expected}"; + zend_string *result = av_replace_placeholders(template, strlen(template), &field, &prop_info); + TEST_ASSERT_EQUAL_STRING("name must be a string or a string", result->val); + av_string_release(result); + av_string_release(field.name); +} + +// --------------------------------------------------------------------------- +// Mixed placeholders +// --------------------------------------------------------------------------- + +void test_all_three_placeholders_replaced(void) +{ + g_expected_type_string = "an integer"; + + zval value; + ZVAL_LONG(&value, 7); + av_field field = make_field("count", &value); + + zend_property_info prop; + zend_type type; + av_property_info prop_info = make_prop_info(&prop, &type); + + const char *template = "The {field} must be {expected}, got {value}."; + zend_string *result = av_replace_placeholders(template, strlen(template), &field, &prop_info); + TEST_ASSERT_EQUAL_STRING("The count must be an integer, got 7.", result->val); + av_string_release(result); + av_string_release(field.name); +} + +void test_non_placeholder_braced_text_is_preserved(void) +{ + g_expected_type_string = "a float"; + + zval value; + ZVAL_LONG(&value, 1); + av_field field = make_field("price", &value); + + zend_property_info prop; + zend_type type; + av_property_info prop_info = make_prop_info(&prop, &type); + + // "{fieldx}" is not a placeholder and must be left untouched. + const char *template = "{field} {fieldx} must be {expected}; got {value}"; + zend_string *result = av_replace_placeholders(template, strlen(template), &field, &prop_info); + TEST_ASSERT_EQUAL_STRING("price {fieldx} must be a float; got 1", result->val); + av_string_release(result); + av_string_release(field.name); +} + +void test_standalone_token_followed_by_suffix_is_replaced(void) +{ + zval value; + ZVAL_NULL(&value); + av_field field = make_field("id", &value); + + // "{field}" is a standalone token immediately followed by an unrelated + // "x" that must survive intact. + const char *template = "pre{field}xpost"; + zend_string *result = av_replace_placeholders(template, strlen(template), &field, NULL); + TEST_ASSERT_EQUAL_STRING("preidxpost", result->val); + av_string_release(result); + av_string_release(field.name); +}