ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
schema.c
Go to the documentation of this file.
1
7#include <ascii-chat/options/schema.h>
8#include <ascii-chat/options/validation.h>
9#include <ascii-chat/options/options.h>
10#include <ascii-chat/options/builder.h>
11#include <ascii-chat/options/presets.h>
12#include <ascii-chat/util/path.h>
13#include <ascii-chat/video/palette.h>
14#include <ascii-chat/platform/terminal.h>
15#include <ascii-chat/platform/system.h>
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);
154 case OPTION_TYPE_DOUBLE:
155 // Check if this is a float field instead of double
156 if (offset == offsetof(options_t, microphone_sensitivity)) {
157 return sizeof(float);
158 } else if (offset == offsetof(options_t, speakers_volume)) {
159 return sizeof(float);
160 }
161 // Check field_size at runtime to distinguish float from double
162 // For now, return double size (will be checked when writing)
163 return sizeof(double);
164 case OPTION_TYPE_STRING:
165 // String fields are typically OPTIONS_BUFF_SIZE
166 return OPTIONS_BUFF_SIZE;
167 case OPTION_TYPE_CALLBACK:
168 // CALLBACK options store into regular options_t fields; infer by offset.
169 if (offset == offsetof(options_t, log_file) || offset == offsetof(options_t, port) ||
170 offset == offsetof(options_t, palette_custom) || offset == offsetof(options_t, yt_dlp_options)) {
171 return OPTIONS_BUFF_SIZE;
172 }
173 if (offset == offsetof(options_t, media_seek_timestamp)) {
174 return sizeof(double);
175 }
176 if (offset == offsetof(options_t, microphone_sensitivity) || offset == offsetof(options_t, speakers_volume)) {
177 return sizeof(float);
178 }
179 if (offset == offsetof(options_t, verbose_level)) {
180 return sizeof(unsigned short int);
181 }
182 return sizeof(int);
183 default:
184 return sizeof(int); // Safe default
185 }
186}
187
188// Validation wrapper functions removed - we use builder's validate functions directly
189
190// ============================================================================
191// Dynamic Schema Building
192// ============================================================================
193
197static bool should_add_descriptor(const option_descriptor_t *desc, const option_descriptor_t **existing, size_t count) {
198 if (!desc || desc->type == OPTION_TYPE_ACTION) {
199 return false; // Skip actions (not an error, just filtering)
200 }
201
202 // Check if we already have this exact offset
203 for (size_t i = 0; i < count; i++) {
204 if (existing[i] && existing[i]->offset == desc->offset) {
205 return false; // Already have this offset
206 }
207 }
208 return true;
209}
210
214static char *store_dynamic_string(const char *str) {
215 if (!str) {
216 return NULL;
217 }
218
219 // Grow string storage if needed
220 if (g_dynamic_strings_count >= g_dynamic_strings_capacity) {
221 size_t new_capacity = g_dynamic_strings_capacity == 0 ? 64 : g_dynamic_strings_capacity * 2;
222 char **new_strings = SAFE_REALLOC(g_dynamic_strings, new_capacity * sizeof(char *), char **);
223 if (!new_strings) {
224 SET_ERRNO(ERROR_MEMORY, "Failed to allocate new strings");
225 return NULL;
226 }
227 g_dynamic_strings = new_strings;
228 g_dynamic_strings_capacity = new_capacity;
229 }
230
231 // Allocate and copy the string
232 char *stored = platform_strdup(str);
233 if (!stored) {
234 SET_ERRNO(ERROR_MEMORY, "Failed to allocate new string");
235 return NULL;
236 }
237
238 g_dynamic_strings[g_dynamic_strings_count++] = stored;
239 return stored;
240}
241
242asciichat_error_t config_schema_build_from_configs(const options_config_t **configs, size_t num_configs) {
243 // Free existing dynamic schema if any
244 if (g_dynamic_schema) {
245 SAFE_FREE(g_dynamic_schema);
246 g_dynamic_schema = NULL;
247 g_dynamic_schema_count = 0;
248 }
249
250 // Free existing string storage
251 if (g_dynamic_strings) {
252 for (size_t i = 0; i < g_dynamic_strings_count; i++) {
253 SAFE_FREE(g_dynamic_strings[i]);
254 }
255 SAFE_FREE(g_dynamic_strings);
256 g_dynamic_strings = NULL;
257 g_dynamic_strings_count = 0;
258 g_dynamic_strings_capacity = 0;
259 }
260
261 // Count total unique descriptors (by offset) across all configs
262 // Use a simple approach: collect all descriptors, then deduplicate by offset
263 const option_descriptor_t *all_descriptors[256] = {0}; // Max expected options
264 size_t descriptor_count = 0;
265
266 // Collect descriptors from all configs
267 for (size_t cfg_idx = 0; cfg_idx < num_configs; cfg_idx++) {
268 const options_config_t *config = configs[cfg_idx];
269 if (!config) {
270 continue;
271 }
272
273 for (size_t i = 0; i < config->num_descriptors && descriptor_count < 256; i++) {
274 const option_descriptor_t *desc = &config->descriptors[i];
275 if (should_add_descriptor(desc, all_descriptors, descriptor_count)) {
276 all_descriptors[descriptor_count++] = desc;
277 }
278 }
279 }
280
281 // Allocate schema array
282 g_dynamic_schema = SAFE_MALLOC(descriptor_count * sizeof(config_option_metadata_t), config_option_metadata_t *);
283 if (!g_dynamic_schema) {
284 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate dynamic schema");
285 }
286
287 // Build schema entries
288 g_dynamic_schema_count = 0;
289 char toml_key_buffer[BUFFER_SIZE_SMALL];
290 char cli_flag_buffer[256];
291 char category_buffer[64];
292
293 for (size_t i = 0; i < descriptor_count; i++) {
294 const option_descriptor_t *desc = all_descriptors[i];
295 if (!desc || !desc->long_name || !desc->group) {
296 continue;
297 }
298
299 config_option_metadata_t *meta = &g_dynamic_schema[g_dynamic_schema_count++];
300 // Zero-initialize the metadata struct to ensure all fields are initialized
301 memset(meta, 0, sizeof(*meta));
302
303 // Use builder's type directly
304 meta->type = desc->type;
305
306 // Generate category: lowercase the group name from builder
307 SAFE_STRNCPY(category_buffer, desc->group, sizeof(category_buffer) - 1);
308 category_buffer[sizeof(category_buffer) - 1] = '\0';
309 str_tolower(category_buffer);
310
311 // Generate TOML key: "category.field_name" (category is lowercase group, field_name is long_name with
312 // dashes->underscores)
313 if (!generate_toml_key(category_buffer, desc->long_name, toml_key_buffer, sizeof(toml_key_buffer))) {
314 g_dynamic_schema_count--; // Skip this entry
315 continue;
316 }
317
318 // Store generated strings (allocate and store)
319 meta->toml_key = store_dynamic_string(toml_key_buffer);
320 meta->category = store_dynamic_string(category_buffer);
321 if (!meta->toml_key || !meta->category) {
322 // Failed to allocate strings, skip this entry
323 g_dynamic_schema_count--;
324 continue;
325 }
326
327 // Generate CLI flag: "--long-name"
328 if (generate_cli_flag(desc->long_name, cli_flag_buffer, sizeof(cli_flag_buffer))) {
329 meta->cli_flag = store_dynamic_string(cli_flag_buffer);
330 if (!meta->cli_flag) {
331 // Failed to allocate, but continue (cli_flag can be NULL)
332 }
333 } else {
334 meta->cli_flag = NULL;
335 }
336
337 // Set other fields
338 meta->context = OPTION_CONTEXT_BOTH; // Most options can appear in both CLI and config
339 meta->field_offset = desc->offset;
340 meta->field_size = get_field_size(meta->type, desc->offset);
341 // Use builder's validate function directly - it receives the full options struct
342 meta->validate_fn = desc->validate;
343 // Use builder's parse function for CALLBACK types
344 meta->parse_fn = desc->parse_fn;
345 meta->description = desc->help_text;
346
347 // Set mode_bitmask from option descriptor
348 // The descriptor should have mode_bitmask set from the registry
349 meta->mode_bitmask = desc->mode_bitmask;
350
351 // Copy mode_default_getter function pointer
352 meta->mode_default_getter = desc->mode_default_getter;
353
354 // Copy constraints from descriptor's metadata
355 // For integer types, copy numeric_range to int_range (always copy, even if min is 0)
356 memset(&meta->constraints, 0, sizeof(meta->constraints));
357 // Always copy numeric_range if descriptor has it (check max or min)
358 if (desc && desc->metadata.numeric_range.max > 0) {
359 meta->constraints.int_range.min = desc->metadata.numeric_range.min;
360 meta->constraints.int_range.max = desc->metadata.numeric_range.max;
361 }
362 }
363
364 g_schema_built = true;
365 return ASCIICHAT_OK;
366}
367
368// ============================================================================
369// Lookup Functions
370// ============================================================================
371
372const config_option_metadata_t *config_schema_get_by_toml_key(const char *toml_key) {
373 if (!toml_key) {
374 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid arguments for config_schema_get_by_toml_key");
375 return NULL;
376 }
377
378 // Schema must be built before use
379 if (!g_schema_built || !g_dynamic_schema) {
380 SET_ERRNO(ERROR_INVALID_STATE, "Schema not built");
381 return NULL;
382 }
383
384 for (size_t i = 0; i < g_dynamic_schema_count; i++) {
385 if (g_dynamic_schema[i].toml_key && strcmp(g_dynamic_schema[i].toml_key, toml_key) == 0) {
386 return &g_dynamic_schema[i];
387 }
388 }
389
390 return NULL;
391}
392
393const config_option_metadata_t **config_schema_get_by_category(const char *category, size_t *count) {
394 static const config_option_metadata_t *results[64]; // Max options per category
395 size_t result_count = 0;
396
397 if (!category) {
398 if (count) {
399 *count = 0;
400 }
401 return NULL;
402 }
403
404 // Schema must be built before use
405 if (!g_schema_built || !g_dynamic_schema) {
406 if (count) {
407 *count = 0;
408 }
409 SET_ERRNO(ERROR_INVALID_STATE, "Schema not built");
410 return NULL;
411 }
412
413 for (size_t i = 0; i < g_dynamic_schema_count && result_count < 64; i++) {
414 if (g_dynamic_schema[i].category && strcmp(g_dynamic_schema[i].category, category) == 0) {
415 results[result_count++] = &g_dynamic_schema[i];
416 }
417 }
418
419 if (count) {
420 *count = result_count;
421 }
422
423 return (result_count > 0) ? results : NULL;
424}
425
426const config_option_metadata_t *config_schema_get_all(size_t *count) {
427 // Schema must be built before use
428 if (!g_schema_built || !g_dynamic_schema) {
429 if (count) {
430 *count = 0;
431 }
432 SET_ERRNO(ERROR_INVALID_STATE, "Schema not built");
433 return NULL;
434 }
435
436 if (count) {
437 *count = g_dynamic_schema_count;
438 }
439 return g_dynamic_schema;
440}
441
442// ============================================================================
443// Schema Cleanup
444// ============================================================================
445
447 if (!g_schema_built) {
448 return; // Nothing to clean up
449 }
450
451 // Free all dynamically allocated strings (toml_key, cli_flag, category)
452 if (g_dynamic_strings) {
453 for (size_t i = 0; i < g_dynamic_strings_count; i++) {
454 SAFE_FREE(g_dynamic_strings[i]);
455 }
456 SAFE_FREE(g_dynamic_strings);
457 g_dynamic_strings = NULL;
458 g_dynamic_strings_count = 0;
459 g_dynamic_strings_capacity = 0;
460 }
461
462 // Free the schema array itself
463 if (g_dynamic_schema) {
464 SAFE_FREE(g_dynamic_schema);
465 g_dynamic_schema = NULL;
466 g_dynamic_schema_count = 0;
467 }
468
469 g_schema_built = false;
470}
int buffer_size
Size of circular buffer.
Definition grep.c:84
char * platform_strdup(const char *s)
const config_option_metadata_t ** config_schema_get_by_category(const char *category, size_t *count)
Definition schema.c:393
void config_schema_destroy(void)
Definition schema.c:446
const config_option_metadata_t * config_schema_get_all(size_t *count)
Definition schema.c:426
asciichat_error_t config_schema_build_from_configs(const options_config_t **configs, size_t num_configs)
Definition schema.c:242
const config_option_metadata_t * config_schema_get_by_toml_key(const char *toml_key)
Definition schema.c:372
#define bool
Definition stdbool.h:22