Skip to main content

Custom Validators

Create specialized validation logic beyond built-in validators for application-specific requirements.

Validator Types

Argus supports two validator execution timings:

TypeWhenFunction SignatureUse Case
Pre-validator (ORDER_PRE)Before type conversionint func(argus_t *argus, void *value_ptr, validator_data_t data)Raw string validation
Post-validator (ORDER_POST)After type conversionint func(argus_t *argus, void *option_ptr, validator_data_t data)Typed value validation

Basic Custom Validators

Validate converted values:

// Even number validator (post-conversion)
int even_validator(argus_t *argus, void *option_ptr, validator_data_t data)
{
argus_option_t *option = (argus_option_t *)option_ptr;
(void)data; // Unused

if (option->value.as_int % 2 != 0) {
ARGUS_PARSING_ERROR(argus, "Value must be even, got %d", option->value.as_int);
return ARGUS_ERROR_INVALID_VALUE;
}
return ARGUS_SUCCESS;
}

// Helper macro
#define V_EVEN() \
MAKE_VALIDATOR(even_validator, NULL, _V_DATA_CUSTOM_(NULL), ORDER_POST)

// Usage
OPTION_INT('n', "number", HELP("Even number"), VALIDATOR(V_EVEN()))

Passing Data to Validators

Use validator_data_t to pass configuration:

// Divisible-by validator with parameter
int divisible_validator(argus_t *argus, void *option_ptr, validator_data_t data)
{
argus_option_t *option = (argus_option_t *)option_ptr;
int divisor = (int)data.custom;

if (option->value.as_int % divisor != 0) {
ARGUS_PARSING_ERROR(argus, "Value must be divisible by %d", divisor);
return ARGUS_ERROR_INVALID_VALUE;
}
return ARGUS_SUCCESS;
}

#define V_DIVISIBLE_BY(n) \
MAKE_VALIDATOR(divisible_validator, NULL, _V_DATA_CUSTOM_(n), ORDER_POST)

// Usage
OPTION_INT('p', "port", HELP("Port (multiple of 100)"),
VALIDATOR(V_DIVISIBLE_BY(100)))

Context-Aware Validation

Access other options for cross-validation:

// Validate that max > min
int max_greater_than_min_validator(argus_t *argus, void *option_ptr, validator_data_t data)
{
argus_option_t *option = (argus_option_t *)option_ptr;
const char *min_option = (const char *)data.custom;

int max_value = option->value.as_int;
int min_value = argus_get(argus, min_option).as_int;

if (max_value <= min_value) {
ARGUS_PARSING_ERROR(argus, "Max value %d must be greater than min value %d",
max_value, min_value);
return ARGUS_ERROR_INVALID_VALUE;
}

return ARGUS_SUCCESS;
}

#define V_GREATER_THAN(option_name) \
MAKE_VALIDATOR(max_greater_than_min_validator, NULL, _V_DATA_CUSTOM_(option_name), ORDER_POST)

ARGUS_OPTIONS(
options,
OPTION_INT('m', "min", HELP("Minimum value")),
OPTION_INT('M', "max", HELP("Maximum value"),
VALIDATOR(V_GREATER_THAN("min"))),
)

Advanced Examples

// Company email domain validator
int company_email_validator(argus_t *argus, void *option_ptr, validator_data_t data)
{
argus_option_t *option = (argus_option_t *)option_ptr;
const char *required_domain = (const char *)data.custom;
const char *email = option->value.as_string;

const char *at = strchr(email, '@');
if (!at) {
ARGUS_PARSING_ERROR(argus, "Invalid email format");
return ARGUS_ERROR_INVALID_FORMAT;
}

if (strcmp(at + 1, required_domain) != 0) {
ARGUS_PARSING_ERROR(argus, "Email must use domain '%s'", required_domain);
return ARGUS_ERROR_INVALID_VALUE;
}

return ARGUS_SUCCESS;
}

#define V_COMPANY_EMAIL(domain) \
MAKE_VALIDATOR(company_email_validator, NULL, _V_DATA_CUSTOM_(domain), ORDER_POST)

// Usage
OPTION_STRING('e', "email", HELP("Company email"),
VALIDATOR(V_COMPANY_EMAIL("company.com")))

Collection Validation

Validate array and map contents:

// Array element validator
int array_all_positive_validator(argus_t *argus, void *option_ptr, validator_data_t data)
{
argus_option_t *option = (argus_option_t *)option_ptr;
(void)data; // Unused

for (size_t i = 0; i < option->value_count; i++) {
int value = option->value.as_array[i].as_int;
if (value <= 0) {
ARGUS_PARSING_ERROR(argus, "All values must be positive, got %d at index %zu",
value, i);
return ARGUS_ERROR_INVALID_VALUE;
}
}

return ARGUS_SUCCESS;
}

#define V_ALL_POSITIVE() \
MAKE_VALIDATOR(array_all_positive_validator, NULL, _V_DATA_CUSTOM_(NULL), ORDER_POST)

// Usage
OPTION_ARRAY_INT('p', "ports", HELP("Port numbers"),
VALIDATOR(V_COUNT(1, 10), V_ALL_POSITIVE()))

Custom Formatters

Add custom formatting to display validator constraints in help output:

// Divisibility validator with formatter
int divisible_validator(argus_t *argus, void *option_ptr, validator_data_t data)
{
argus_option_t *option = (argus_option_t *)option_ptr;
int divisor = (int)data.custom;

if (option->value.as_int % divisor != 0) {
ARGUS_PARSING_ERROR(argus, "Value must be divisible by %d", divisor);
return ARGUS_ERROR_INVALID_VALUE;
}
return ARGUS_SUCCESS;
}

// Formatter function
char *format_divisible_validator(validator_data_t data)
{
char *result = malloc(64);
if (!result)
return NULL;

snprintf(result, 64, "divisible by %d", (int)data.custom);
return result;
}

#define V_DIVISIBLE_BY(n) \
MAKE_VALIDATOR(divisible_validator, format_divisible_validator, _V_DATA_CUSTOM_(n), ORDER_POST)

// Usage
OPTION_INT('p', "port", HELP("Port number"), VALIDATOR(V_DIVISIBLE_BY(100)))

Generated help:

  -p, --port <NUM>  - Port number (divisible by 100)

Formatter Implementation

// Template for formatter functions
char *format_my_validator(validator_data_t data)
{
// Allocate memory for result
char *result = malloc(BUFFER_SIZE);
if (!result)
return NULL;

// Format based on your data
snprintf(result, BUFFER_SIZE, "your descriptive format");

return result; // Memory will be freed by caller
}

Integration with MAKE_VALIDATOR:

#define MY_VALIDATOR(param) \
MAKE_VALIDATOR(my_validator_func, format_my_validator, \
_V_DATA_CUSTOM_(param), ORDER_POST)

The formatter automatically integrates with the help system and appears in option descriptions or hints depending on the output format.

Combining Validators

Multiple validators execute in order:

ARGUS_OPTIONS(
options,
// Multiple validators: pre-validation, then post-validation
OPTION_STRING('p', "password", HELP("Secure password"),
VALIDATOR(V_LENGTH(8, 128), // Built-in
V_MIXED_CASE(), // Custom pre-validator
V_NO_COMMON_PASSWORDS())), // Custom post-validator

// Range + custom validation
OPTION_INT('p', "port", HELP("Even port number"),
VALIDATOR(V_RANGE(1, 65535), V_EVEN())),
)

Execution order:

  1. Pre-validators (ORDER_PRE)
  2. Type conversion
  3. Post-validators (ORDER_POST)
  4. Built-in validation (choices, etc.)

Best Practices

// ✅ Good validator design
int descriptive_validator(argus_t *argus, void *option_ptr, validator_data_t data)
{
// Single responsibility
// Clear error messages
// Proper error codes
ARGUS_PARSING_ERROR(argus, "Username must start with a letter and contain only alphanumeric characters");
return ARGUS_ERROR_INVALID_VALUE;
}

// ✅ Reusable with parameters
#define V_MIN_WORDS(count) \
MAKE_VALIDATOR(min_words_validator, NULL, _V_DATA_CUSTOM_(count), ORDER_POST)

// ❌ Avoid these patterns
int bad_validator(argus_t *argus, void *option_ptr, validator_data_t data)
{
// Too many responsibilities
// Vague error message
ARGUS_PARSING_ERROR(argus, "Invalid input");
return ARGUS_ERROR_INVALID_VALUE;
}

What's Next?

Validator Design

Focus on single responsibility and clear error messages. Users should understand exactly what's wrong and how to fix it.