ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
schema.c
Go to the documentation of this file.
1
16#include <ascii-chat/common.h>
17
18#include <string.h>
19#include <limits.h>
20#include <ctype.h>
21
22// ============================================================================
23// Option Metadata Registry
24// ============================================================================
25// Schema is now built dynamically from options builder configs.
26// See config_schema_build_from_configs() below.
27
28// ============================================================================
29// Dynamic Schema Storage
30// ============================================================================
31
32static config_option_metadata_t *g_dynamic_schema = NULL;
33static size_t g_dynamic_schema_count = 0;
34static bool g_schema_built = false;
35static char **g_dynamic_strings = NULL; // Storage for allocated strings (toml_key, cli_flag, category)
36static size_t g_dynamic_strings_count = 0;
37static size_t g_dynamic_strings_capacity = 0;
38
39// ============================================================================
40// Helper Functions for Dynamic Schema Building
41// ============================================================================
42
46static void str_tolower(char *str) {
47 if (!str) {
48 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid string for str_tolower");
49 return;
50 }
51 for (; *str; str++) {
52 *str = (char)tolower((unsigned char)*str);
53 }
54}
55
59static void dashes_to_underscores(char *str) {
60 if (!str) {
61 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid string for dashes_to_underscores");
62 return;
63 }
64 for (; *str; str++) {
65 if (*str == '-') {
66 *str = '_';
67 }
68 }
69}
70
75static char *generate_toml_key(const char *group, const char *field_name, char *buffer, size_t buffer_size) {
76 if (!group || !field_name || !buffer || buffer_size == 0) {
77 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid arguments for generate_toml_key");
78 return NULL;
79 }
80
81 // Copy group and convert to lowercase
82 size_t group_len = strlen(group);
83 if (group_len >= buffer_size) {
84 SET_ERRNO(ERROR_CONFIG, "Group name is too long for buffer");
85 return NULL;
86 }
87 memcpy(buffer, group, group_len + 1);
88 str_tolower(buffer);
89
90 // Append dot
91 if (group_len + 1 >= buffer_size) {
92 SET_ERRNO(ERROR_CONFIG, "Group name is too long for buffer");
93 return NULL;
94 }
95 buffer[group_len] = '.';
96
97 // Copy field name, convert dashes to underscores, and lowercase
98 size_t field_len = strlen(field_name);
99 if (group_len + 1 + field_len >= buffer_size) {
100 SET_ERRNO(ERROR_INVALID_STATE, "Field name is too long for buffer");
101 return NULL;
102 }
103 memcpy(buffer + group_len + 1, field_name, field_len + 1);
104 dashes_to_underscores(buffer + group_len + 1);
105 str_tolower(buffer + group_len + 1);
106
107 return buffer;
108}
109
114static char *generate_cli_flag(const char *long_name, char *buffer, size_t buffer_size) {
115 if (!long_name || !buffer || buffer_size < 3) { // Need at least "--" + 1 char
116 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid arguments for generate_cli_flag");
117 return NULL;
118 }
119
120 buffer[0] = '-';
121 buffer[1] = '-';
122 size_t name_len = strlen(long_name);
123 if (name_len + 2 >= buffer_size) {
124 SET_ERRNO(ERROR_CONFIG, "Long name is too long for buffer");
125 return NULL;
126 }
127 memcpy(buffer + 2, long_name, name_len + 1);
128 return buffer;
129}
130
134static size_t get_field_size(option_type_t type, size_t offset) {
135 // We can't determine field size from offset alone, so we'll use sizeof() on a dummy struct
136 // For now, use reasonable defaults based on type
137 switch (type) {
138 case OPTION_TYPE_BOOL:
139 return sizeof(bool);
140 case OPTION_TYPE_INT:
141 // Check if this is an unsigned short int field
142 if (offset == offsetof(options_t, webcam_index)) {
143 return sizeof(unsigned short int);
144 }
145 // Check if this is an enum field by offset
146 if (offset == offsetof(options_t, color_mode)) {
147 return sizeof(terminal_color_mode_t);
148 } else if (offset == offsetof(options_t, render_mode)) {
149 return sizeof(render_mode_t);
150 } else if (offset == offsetof(options_t, palette_type)) {
151 return sizeof(palette_type_t);
152 }
153 return sizeof(int);
155 // Check if this is a float field instead of double
156 if (offset == offsetof(options_t, microphone_sensitivity) || offset == offsetof(options_t, speakers_volume)) {
157 return sizeof(float);
158 }
159 // Check field_size at runtime to distinguish float from double
160 // For now, return double size (will be checked when writing)
161 return sizeof(double);
163 // String fields are typically OPTIONS_BUFF_SIZE
164 return OPTIONS_BUFF_SIZE;
166 // CALLBACK options store into regular options_t fields; infer by offset.
167 if (offset == offsetof(options_t, log_file) || offset == offsetof(options_t, port) ||
168 offset == offsetof(options_t, palette_custom) || offset == offsetof(options_t, yt_dlp_options)) {
169 return OPTIONS_BUFF_SIZE;
170 }
171 if (offset == offsetof(options_t, media_seek_timestamp)) {
172 return sizeof(double);
173 }
174 if (offset == offsetof(options_t, microphone_sensitivity) || offset == offsetof(options_t, speakers_volume)) {
175 return sizeof(float);
176 }
177 if (offset == offsetof(options_t, verbose_level)) {
178 return sizeof(unsigned short int);
179 }
180 return sizeof(int);
181 default:
182 return sizeof(int); // Safe default
183 }
184}
185
186// Validation wrapper functions removed - we use builder's validate functions directly
187
188// ============================================================================
189// Dynamic Schema Building
190// ============================================================================
191
195static bool should_add_descriptor(const option_descriptor_t *desc, const option_descriptor_t **existing, size_t count) {
196 if (!desc || desc->type == OPTION_TYPE_ACTION) {
197 return false; // Skip actions (not an error, just filtering)
198 }
199
200 // Check if we already have this exact offset
201 for (size_t i = 0; i < count; i++) {
202 if (existing[i] && existing[i]->offset == desc->offset) {
203 return false; // Already have this offset
204 }
205 }
206 return true;
207}
208
212static char *store_dynamic_string(const char *str) {
213 if (!str) {
214 return NULL;
215 }
216
217 // Grow string storage if needed
218 if (g_dynamic_strings_count >= g_dynamic_strings_capacity) {
219 size_t new_capacity = g_dynamic_strings_capacity == 0 ? 64 : g_dynamic_strings_capacity * 2;
220 char **new_strings = SAFE_REALLOC(g_dynamic_strings, new_capacity * sizeof(char *), char **);
221 if (!new_strings) {
222 SET_ERRNO(ERROR_MEMORY, "Failed to allocate new strings");
223 return NULL;
224 }
225 g_dynamic_strings = new_strings;
226 g_dynamic_strings_capacity = new_capacity;
227 }
228
229 // Allocate and copy the string
230 char *stored = platform_strdup(str);
231 if (!stored) {
232 SET_ERRNO(ERROR_MEMORY, "Failed to allocate new string");
233 return NULL;
234 }
235
236 g_dynamic_strings[g_dynamic_strings_count++] = stored;
237 return stored;
238}
239
241 // Free existing dynamic schema if any
242 if (g_dynamic_schema) {
243 SAFE_FREE(g_dynamic_schema);
244 g_dynamic_schema = NULL;
245 g_dynamic_schema_count = 0;
246 }
247
248 // Free existing string storage
249 if (g_dynamic_strings) {
250 for (size_t i = 0; i < g_dynamic_strings_count; i++) {
251 SAFE_FREE(g_dynamic_strings[i]);
252 }
253 SAFE_FREE(g_dynamic_strings);
254 g_dynamic_strings = NULL;
255 g_dynamic_strings_count = 0;
256 g_dynamic_strings_capacity = 0;
257 }
258
259 // Count total unique descriptors (by offset) across all configs
260 // Use a simple approach: collect all descriptors, then deduplicate by offset
261 const option_descriptor_t *all_descriptors[256] = {0}; // Max expected options
262 size_t descriptor_count = 0;
263
264 // Collect descriptors from all configs
265 for (size_t cfg_idx = 0; cfg_idx < num_configs; cfg_idx++) {
266 const options_config_t *config = configs[cfg_idx];
267 if (!config) {
268 log_debug("Config %zu is NULL, skipping", cfg_idx);
269 continue;
270 }
271
272 if (!config->descriptors || config->num_descriptors == 0) {
273 log_debug("Config %zu has no descriptors", cfg_idx);
274 continue;
275 }
276
277 for (size_t i = 0; i < config->num_descriptors && descriptor_count < 256; i++) {
278 const option_descriptor_t *desc = &config->descriptors[i];
279 if (!desc) {
280 log_warn("Descriptor at index %zu in config %zu is NULL", i, cfg_idx);
281 continue;
282 }
283 if (should_add_descriptor(desc, all_descriptors, descriptor_count)) {
284 all_descriptors[descriptor_count++] = desc;
285 }
286 }
287 }
288
289 // Allocate schema array
290 g_dynamic_schema = SAFE_MALLOC(descriptor_count * sizeof(config_option_metadata_t), config_option_metadata_t *);
291 if (!g_dynamic_schema) {
292 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate dynamic schema");
293 }
294
295 // Build schema entries
296 g_dynamic_schema_count = 0;
297 char toml_key_buffer[BUFFER_SIZE_SMALL];
298 char cli_flag_buffer[256];
299 char category_buffer[64];
300
301 for (size_t i = 0; i < descriptor_count; i++) {
302 const option_descriptor_t *desc = all_descriptors[i];
303 if (!desc || !desc->long_name || !desc->group) {
304 continue;
305 }
306
307 config_option_metadata_t *meta = &g_dynamic_schema[g_dynamic_schema_count++];
308 // Zero-initialize the metadata struct to ensure all fields are initialized
309 memset(meta, 0, sizeof(*meta));
310
311 // Use builder's type directly
312 meta->type = desc->type;
313
314 // Generate category: lowercase the group name from builder
315 SAFE_STRNCPY(category_buffer, desc->group, sizeof(category_buffer) - 1);
316 category_buffer[sizeof(category_buffer) - 1] = '\0';
317 str_tolower(category_buffer);
318
319 // Generate TOML key: "category.field_name" (category is lowercase group, field_name is long_name with
320 // dashes->underscores)
321 if (!generate_toml_key(category_buffer, desc->long_name, toml_key_buffer, sizeof(toml_key_buffer))) {
322 g_dynamic_schema_count--; // Skip this entry
323 continue;
324 }
325
326 // Store generated strings (allocate and store)
327 meta->toml_key = store_dynamic_string(toml_key_buffer);
328 meta->category = store_dynamic_string(category_buffer);
329 if (!meta->toml_key || !meta->category) {
330 // Failed to allocate strings, skip this entry
331 g_dynamic_schema_count--;
332 continue;
333 }
334
335 // Generate CLI flag: "--long-name"
336 if (generate_cli_flag(desc->long_name, cli_flag_buffer, sizeof(cli_flag_buffer))) {
337 meta->cli_flag = store_dynamic_string(cli_flag_buffer);
338 if (!meta->cli_flag) {
339 // Failed to allocate, but continue (cli_flag can be NULL)
340 }
341 } else {
342 meta->cli_flag = NULL;
343 }
344
345 // Set other fields
346 meta->context = OPTION_CONTEXT_BOTH; // Most options can appear in both CLI and config
347 meta->field_offset = desc->offset;
348 meta->field_size = get_field_size(meta->type, desc->offset);
349 // Use builder's validate function directly - it receives the full options struct
350 meta->validate_fn = desc->validate;
351 // Use builder's parse function for CALLBACK types
352 meta->parse_fn = desc->parse_fn;
353 meta->description = desc->help_text;
354
355 // Set mode_bitmask from option descriptor
356 // The descriptor should have mode_bitmask set from the registry
357 meta->mode_bitmask = desc->mode_bitmask;
358
359 // Copy mode_default_getter function pointer
361
362 // Copy constraints from descriptor's metadata
363 // For integer types, copy numeric_range to int_range (always copy, even if min is 0)
364 memset(&meta->constraints, 0, sizeof(meta->constraints));
365 // Always copy numeric_range if descriptor has it (check max or min)
366 if (desc && desc->metadata.numeric_range.max > 0) {
369 }
370 }
371
372 g_schema_built = true;
373 return ASCIICHAT_OK;
374}
375
376// ============================================================================
377// Lookup Functions
378// ============================================================================
379
381 if (!toml_key) {
382 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid arguments for config_schema_get_by_toml_key");
383 return NULL;
384 }
385
386 // Schema must be built before use
387 if (!g_schema_built || !g_dynamic_schema) {
388 SET_ERRNO(ERROR_INVALID_STATE, "Schema not built");
389 return NULL;
390 }
391
392 for (size_t i = 0; i < g_dynamic_schema_count; i++) {
393 if (g_dynamic_schema[i].toml_key && strcmp(g_dynamic_schema[i].toml_key, toml_key) == 0) {
394 return &g_dynamic_schema[i];
395 }
396 }
397
398 return NULL;
399}
400
401const config_option_metadata_t **config_schema_get_by_category(const char *category, size_t *count) {
402 static const config_option_metadata_t *results[64]; // Max options per category
403 size_t result_count = 0;
404
405 if (!category) {
406 if (count) {
407 *count = 0;
408 }
409 return NULL;
410 }
411
412 // Schema must be built before use
413 if (!g_schema_built || !g_dynamic_schema) {
414 if (count) {
415 *count = 0;
416 }
417 SET_ERRNO(ERROR_INVALID_STATE, "Schema not built");
418 return NULL;
419 }
420
421 for (size_t i = 0; i < g_dynamic_schema_count && result_count < 64; i++) {
422 if (g_dynamic_schema[i].category && strcmp(g_dynamic_schema[i].category, category) == 0) {
423 results[result_count++] = &g_dynamic_schema[i];
424 }
425 }
426
427 if (count) {
428 *count = result_count;
429 }
430
431 return (result_count > 0) ? results : NULL;
432}
433
435 // Schema must be built before use
436 if (!g_schema_built || !g_dynamic_schema) {
437 if (count) {
438 *count = 0;
439 }
440 SET_ERRNO(ERROR_INVALID_STATE, "Schema not built");
441 return NULL;
442 }
443
444 if (count) {
445 *count = g_dynamic_schema_count;
446 }
447 return g_dynamic_schema;
448}
449
450// ============================================================================
451// Schema Cleanup
452// ============================================================================
453
455 if (!g_schema_built) {
456 return; // Nothing to clean up
457 }
458
459 // Free all dynamically allocated strings (toml_key, cli_flag, category)
460 if (g_dynamic_strings) {
461 for (size_t i = 0; i < g_dynamic_strings_count; i++) {
462 SAFE_FREE(g_dynamic_strings[i]);
463 }
464 SAFE_FREE(g_dynamic_strings);
465 g_dynamic_strings = NULL;
466 g_dynamic_strings_count = 0;
467 g_dynamic_strings_capacity = 0;
468 }
469
470 // Free the schema array itself
471 if (g_dynamic_schema) {
472 SAFE_FREE(g_dynamic_schema);
473 g_dynamic_schema = NULL;
474 g_dynamic_schema_count = 0;
475 }
476
477 g_schema_built = false;
478}
Options builder API for flexible command-line option configuration.
option_type_t
Option value types.
Definition builder.h:161
@ OPTION_TYPE_INT
Integer value (–count 42)
Definition builder.h:163
@ OPTION_TYPE_DOUBLE
Floating point (–ratio 1.5)
Definition builder.h:165
@ OPTION_TYPE_STRING
String value (–name foo)
Definition builder.h:164
@ OPTION_TYPE_ACTION
Action that executes and may exit (–list-webcams, etc.)
Definition builder.h:167
@ OPTION_TYPE_CALLBACK
Custom parser function.
Definition builder.h:166
@ OPTION_TYPE_BOOL
Boolean flag (–flag, no value)
Definition builder.h:162
⚙️ Common definitions, error codes, macros, and types shared throughout the application
int buffer_size
Size of circular buffer.
Definition grep.c:90
#define BUFFER_SIZE_SMALL
Small buffer size (256 bytes)
#define SAFE_REALLOC(ptr, size, cast)
Definition common.h:284
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_INVALID_STATE
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_CONFIG
Definition error_codes.h:57
@ ERROR_INVALID_PARAM
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
#define log_file(...)
File-only logging - writes to log file only, no stderr output.
Definition log/log.h:646
#define OPTIONS_BUFF_SIZE
Buffer size for option string values.
palette_type_t
Built-in palette type enumeration.
Definition palette.h:84
char * platform_strdup(const char *s)
Duplicate string (strdup replacement)
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
Application limits and constraints.
Validation functions for options parsing.
ASCII Palette Management for Video-to-ASCII Conversion.
📂 Path Manipulation Utilities
Preset option configurations for ascii-chat modes.
const config_option_metadata_t ** config_schema_get_by_category(const char *category, size_t *count)
Get all option metadata for a category.
Definition schema.c:401
void config_schema_destroy(void)
Clean up dynamically allocated schema resources.
Definition schema.c:454
const config_option_metadata_t * config_schema_get_all(size_t *count)
Get all option metadata.
Definition schema.c:434
asciichat_error_t config_schema_build_from_configs(const options_config_t **configs, size_t num_configs)
Build schema dynamically from options builder configs.
Definition schema.c:240
const config_option_metadata_t * config_schema_get_by_toml_key(const char *toml_key)
Get option metadata by TOML key.
Definition schema.c:380
Schema metadata for config file options.
@ OPTION_CONTEXT_BOTH
Can appear in both CLI and config.
Definition schema.h:28
#define bool
Definition stdbool.h:61
Option metadata for config file parsing.
Definition schema.h:49
option_context_t context
Where this option can appear.
Definition schema.h:53
option_type_t type
Value type (from builder)
Definition schema.h:52
size_t field_size
Size of field in options_t.
Definition schema.h:56
bool(* parse_fn)(const char *arg, void *dest, char **error_msg)
Custom parser for callbacks (or NULL)
Definition schema.h:62
option_mode_bitmask_t mode_bitmask
Which modes this option applies to.
Definition schema.h:65
const void *(* mode_default_getter)(asciichat_mode_t mode)
Get mode-specific default value (NULL if not mode-aware)
Definition schema.h:83
union config_option_metadata_t::@20 constraints
size_t field_offset
offsetof(options_t, field) - where to store value
Definition schema.h:55
const char * description
Description for docs/help generation.
Definition schema.h:68
const char * category
Category name (e.g., "network", "client", "audio")
Definition schema.h:54
builder_validate_fn_t validate_fn
Builder's validation function (can be NULL for simple types)
Definition schema.h:59
const char * toml_key
TOML key path (e.g., "network.port", "client.address")
Definition schema.h:50
struct config_option_metadata_t::@20::@21 int_range
const char * cli_flag
CLI flag name (e.g., "--port") or NULL if no CLI flag.
Definition schema.h:51
Option descriptor.
Definition builder.h:241
bool(* validate)(const void *options_struct, char **error_msg)
Definition builder.h:263
option_mode_bitmask_t mode_bitmask
Which modes this option applies to.
Definition builder.h:278
const char * help_text
Description for –help.
Definition builder.h:251
size_t offset
offsetof(struct, field) - where to store value
Definition builder.h:248
const char * group
Group name for help sections (e.g., "NETWORK OPTIONS")
Definition builder.h:252
bool(* parse_fn)(const char *arg, void *dest, char **error_msg)
Definition builder.h:266
const char * long_name
Long option name (e.g., "port")
Definition builder.h:243
const void *(* mode_default_getter)(asciichat_mode_t mode)
Get mode-specific default value (NULL if not mode-aware)
Definition builder.h:284
option_metadata_t metadata
Metadata for shell completions (enums, ranges, examples, etc.)
Definition builder.h:281
option_type_t type
Value type.
Definition builder.h:247
int max
Maximum value (or 0 if no limit)
Definition builder.h:203
int min
Minimum value (or 0 if no limit)
Definition builder.h:202
struct option_metadata_t::@19 numeric_range
Options configuration.
Definition builder.h:401
option_descriptor_t * descriptors
Array of option descriptors.
Definition builder.h:402
size_t num_descriptors
Number of descriptors.
Definition builder.h:403
Consolidated options structure.
Cross-platform system functions interface for ascii-chat.
🖥️ Cross-platform terminal interface for ascii-chat
render_mode_t
Render mode preferences.
Definition terminal.h:662
terminal_color_mode_t
Terminal color support levels.
Definition terminal.h:578