Backlog/lib/zargs/src/ArgumentRegistry.zig

303 lines
11 KiB
Zig

const std = @import("std");
const metadata = @import("metadata.zig");
const ParsedValue = @import("ArgumentType.zig").ParsedValue;
/// Central registry for all command-line arguments
/// Manages argument metadata, tracks modules, and provides lookup functionality
pub const ArgumentRegistry = struct {
/// Memory allocator
allocator: std.mem.Allocator,
/// Map from argument name (e.g., "verbose", "v") to metadata
/// Both long names and short flags are stored here
/// Metadata is owned and must be freed
arguments: std.StringHashMap(metadata.ArgumentMetadata),
/// Map from argument name to list of modules that registered it
/// Used for collision detection and help text generation
modules_by_arg: std.StringHashMap(std.ArrayListUnmanaged([]const u8)),
/// Set of struct type names that have been registered
/// Prevents duplicate registration
registered_types: std.StringHashMap(void),
/// Owned copy of argv (only if setArgv was called)
argv: []const [:0]u8,
/// Whether help was requested (--help or -h)
help_requested: bool = false,
/// Track if we've done the initial argv scan for help flag
argv_scanned: bool = false,
/// Parsed values storage
/// Maps argument name to parsed value
parsed_values: std.StringHashMap(ParsedValue),
/// Track which argument keys are allocated (short flags)
/// Long argument names come from field names (comptime strings) and shouldn't be freed
allocated_keys: std.StringHashMap(void),
/// Initialize a new argument registry
pub fn init(allocator: std.mem.Allocator) ArgumentRegistry {
return .{
.argv = std.process.argsAlloc(allocator) catch unreachable,
.allocator = allocator,
.arguments = std.StringHashMap(metadata.ArgumentMetadata).init(allocator),
.modules_by_arg = std.StringHashMap(std.ArrayListUnmanaged([]const u8)).init(allocator),
.registered_types = std.StringHashMap(void).init(allocator),
.parsed_values = std.StringHashMap(ParsedValue).init(allocator),
.allocated_keys = std.StringHashMap(void).init(allocator),
};
}
/// Clean up all resources
pub fn deinit(self: *ArgumentRegistry) void {
// Clean up modules_by_arg lists
var modules_iter = self.modules_by_arg.valueIterator();
while (modules_iter.next()) |list| {
list.deinit(self.allocator);
}
self.modules_by_arg.deinit();
// Clean up argument keys (only short flags that were allocated)
var key_iter = self.allocated_keys.keyIterator();
while (key_iter.next()) |key| {
self.allocator.free(key.*);
}
self.allocated_keys.deinit();
self.arguments.deinit();
self.registered_types.deinit();
// Clean up parsed values
var values_iter = self.parsed_values.valueIterator();
while (values_iter.next()) |value| {
// Free memory for string types
switch (value.*) {
.string => |str| self.allocator.free(str),
.string_list => |list| {
for (list) |str| {
self.allocator.free(str);
}
self.allocator.free(list);
},
.enum_type => |enum_val| self.allocator.free(enum_val.name),
else => {},
}
}
self.parsed_values.deinit();
// Free argv if we own it (only if setArgv was called)
std.process.argsFree(self.allocator, self.argv);
}
/// Check if a type has already been registered
pub fn isTypeRegistered(self: *const ArgumentRegistry, comptime T: type) bool {
const type_name = @typeName(T);
return self.registered_types.contains(type_name);
}
/// Scan argv for help flag without full parsing
pub fn scanForHelp(self: *ArgumentRegistry) void {
if (self.argv_scanned) return;
self.argv_scanned = true;
const argv = self.argv;
for (argv[1..]) |arg| {
// arg is already [:0]const u8, no need to span it
if (std.mem.eql(u8, arg, "--help") or
std.mem.eql(u8, arg, "-h"))
{
self.help_requested = true;
return;
}
}
}
/// Check if help was requested (scans argv lazily)
pub fn isHelpRequested(self: *ArgumentRegistry) bool {
self.scanForHelp();
return self.help_requested;
}
/// Mark a type as registered
pub fn markTypeRegistered(self: *ArgumentRegistry, comptime T: type) !void {
const type_name = @typeName(T);
try self.registered_types.put(type_name, {});
}
/// Look up argument metadata by name (long or short form)
pub fn getArgument(self: *const ArgumentRegistry, name: []const u8) ?*const metadata.ArgumentMetadata {
if (self.arguments.getPtr(name)) |ptr| {
return ptr;
}
return null;
}
/// Get list of modules that registered a specific argument
pub fn getModulesForArg(self: *const ArgumentRegistry, name: []const u8) ?std.ArrayListUnmanaged([]const u8) {
return self.modules_by_arg.get(name);
}
/// Get a parsed value by argument name
pub fn getParsedValue(self: *const ArgumentRegistry, name: []const u8) ?ParsedValue {
return self.parsed_values.get(name);
}
/// Store a parsed value
/// Frees the old value if it exists and is a string type
pub fn storeParsedValue(self: *ArgumentRegistry, name: []const u8, value: ParsedValue) !void {
// Check if there's an old value we need to free
if (self.parsed_values.get(name)) |old_value| {
switch (old_value) {
.string => |str| self.allocator.free(str),
.string_list => |list| {
for (list) |str| {
self.allocator.free(str);
}
self.allocator.free(list);
},
.enum_type => |enum_val| self.allocator.free(enum_val.name),
else => {},
}
}
try self.parsed_values.put(name, value);
}
/// Lazy populate: register metadata, parse argv, and populate struct
/// This is the main entry point for lazy parsing
pub fn populate(
self: *ArgumentRegistry,
comptime T: type,
comptime module_name: []const u8,
allocator: std.mem.Allocator,
) !T {
// Register metadata if not already done
if (!self.isTypeRegistered(T)) {
try self.registerMetadata(T, module_name);
}
// Parse argv on-demand for this type only
const argv = self.argv;
try self.parseArgvForType(T, argv);
// Populate and return the struct
const parsing = @import("parsing.zig");
return parsing.populateStruct(T, self, allocator);
}
/// Parse argv only for arguments relevant to a specific type
/// Ignores unknown arguments (they may belong to other modules)
fn parseArgvForType(self: *ArgumentRegistry, comptime T: type, argv: []const [:0]const u8) !void {
_ = T; // Type is used implicitly via registered metadata
const parsing = @import("parsing.zig");
// Parse argv, ignoring unknown arguments
try parsing.parseArgv(self, argv);
}
// ========================================================================
// Registration Methods
// ========================================================================
/// Register metadata for a struct type
/// INTERNAL USE ONLY: For normal use, call populate() instead
/// This is only public for testing and internal library use
pub fn registerMetadata(
self: *ArgumentRegistry,
comptime T: type,
comptime module_name: []const u8,
) !void {
// Skip if already registered
if (self.isTypeRegistered(T)) {
return;
}
// Extract and register each field directly
const type_info = @typeInfo(T);
if (type_info != .@"struct") {
@compileError("registerMetadata requires a struct type");
}
inline for (type_info.@"struct".fields) |field| {
const field_meta = metadata.extractFieldMetadata(T, field);
try self.registerArgument(&field_meta, module_name);
}
// Mark type as registered
try self.markTypeRegistered(T);
}
/// Register a single argument with collision detection
fn registerArgument(
self: *ArgumentRegistry,
arg_meta: *const metadata.ArgumentMetadata,
module_name: []const u8,
) !void {
// Check if argument already exists (long form)
const long_exists = self.arguments.getPtr(arg_meta.arg_name);
if (long_exists) |existing| {
// Compatible collision: same type
if (existing.arg_type == arg_meta.arg_type) {
// Add this module to the list
try self.addModuleForArg(arg_meta.arg_name, module_name);
// Don't return yet - we might need to register short form
} else {
// Incompatible collision: different types
return error.IncompatibleArgumentType;
}
} else {
// Register the argument (long form) - store a copy
try self.arguments.put(arg_meta.arg_name, arg_meta.*);
try self.addModuleForArg(arg_meta.arg_name, module_name);
}
// Register short form if present
if (arg_meta.short) |short_char| {
// Create a persistent string for the short key
const short_key = try self.allocator.alloc(u8, 1);
short_key[0] = short_char;
// Check for short flag collision
if (self.arguments.getPtr(short_key)) |existing| {
// Check if types are compatible
if (existing.arg_type == arg_meta.arg_type) {
// Compatible collision
try self.addModuleForArg(short_key, module_name);
self.allocator.free(short_key); // Free the temporary key
return;
}
self.allocator.free(short_key); // Free the temporary key
return error.IncompatibleArgumentType;
}
// No collision - register the short form (key will be owned by the hash map)
try self.arguments.put(short_key, arg_meta.*);
try self.addModuleForArg(short_key, module_name);
// Track that this key was allocated and needs to be freed
try self.allocated_keys.put(short_key, {});
}
}
/// Add a module to the list for an argument
fn addModuleForArg(self: *ArgumentRegistry, arg_name: []const u8, module_name: []const u8) !void {
const entry = try self.modules_by_arg.getOrPut(arg_name);
if (!entry.found_existing) {
entry.value_ptr.* = std.ArrayListUnmanaged([]const u8){};
}
try entry.value_ptr.append(self.allocator, module_name);
}
/// Check if an argument is registered
pub fn hasArgument(self: *const ArgumentRegistry, name: []const u8) bool {
return self.arguments.contains(name);
}
/// Get the number of registered arguments
pub fn argumentCount(self: *const ArgumentRegistry) usize {
return self.arguments.count();
}
};