From cdb33d84db96d0c0823419e237130873deaddb58 Mon Sep 17 00:00:00 2001 From: Peterino2 Date: Thu, 22 Jan 2026 03:58:37 -0800 Subject: [PATCH] deleted a lot of intermediate files and tests --- lib/zargs/COMPLETION_SUMMARY.md | 334 ------ lib/zargs/PROGRESS.md | 475 -------- lib/zargs/README.md | 279 ----- lib/zargs/SUMMARY.md | 509 --------- lib/zargs/build.zig | 356 +++--- lib/zargs/examples/README.md | 86 ++ lib/zargs/examples/file_processor.zig | 95 ++ lib/zargs/examples/multi_module.zig | 83 +- lib/zargs/examples/simple.zig | 63 - lib/zargs/research/builder_pattern_example.md | 281 ----- lib/zargs/research/design.md | 360 ------ lib/zargs/research/hybrid_design.md | 1013 ----------------- lib/zargs/research/type_driven_example.md | 647 ----------- lib/zargs/src/ArgumentRegistry.zig | 100 +- lib/zargs/src/help.zig | 60 +- lib/zargs/src/main.zig | 105 +- lib/zargs/src/metadata.zig | 8 +- lib/zargs/src/parse.zig | 354 ++++++ lib/zargs/src/parsing.zig | 85 +- lib/zargs/tests/test_errors.zig | 100 -- lib/zargs/tests/test_help.zig | 293 ----- lib/zargs/tests/test_metadata.zig | 474 -------- lib/zargs/tests/test_parsed_value.zig | 177 --- lib/zargs/tests/test_parsing.zig | 358 ------ lib/zargs/tests/test_registry.zig | 496 -------- lib/zargs/tests/test_utils.zig | 48 - lib/zargs/tests/type_test.zig | 57 - lib/zargs/todo/QUICK_START.md | 399 ------- lib/zargs/todo/READINESS_CHECKLIST.md | 236 ---- lib/zargs/todo/README.md | 225 ---- lib/zargs/todo/TIMELINE.txt | 146 --- lib/zargs/todo/implementation_plan.md | 715 ------------ lib/zargs/todo/implementation_plan_v2.md | 486 -------- lib/zargs/todo/review_iteration1.md | 227 ---- 34 files changed, 936 insertions(+), 8794 deletions(-) delete mode 100644 lib/zargs/COMPLETION_SUMMARY.md delete mode 100644 lib/zargs/PROGRESS.md delete mode 100644 lib/zargs/README.md delete mode 100644 lib/zargs/SUMMARY.md create mode 100644 lib/zargs/examples/README.md create mode 100644 lib/zargs/examples/file_processor.zig delete mode 100644 lib/zargs/examples/simple.zig delete mode 100644 lib/zargs/research/builder_pattern_example.md delete mode 100644 lib/zargs/research/design.md delete mode 100644 lib/zargs/research/hybrid_design.md delete mode 100644 lib/zargs/research/type_driven_example.md create mode 100644 lib/zargs/src/parse.zig delete mode 100644 lib/zargs/tests/test_errors.zig delete mode 100644 lib/zargs/tests/test_help.zig delete mode 100644 lib/zargs/tests/test_metadata.zig delete mode 100644 lib/zargs/tests/test_parsed_value.zig delete mode 100644 lib/zargs/tests/test_parsing.zig delete mode 100644 lib/zargs/tests/test_registry.zig delete mode 100644 lib/zargs/tests/test_utils.zig delete mode 100644 lib/zargs/tests/type_test.zig delete mode 100644 lib/zargs/todo/QUICK_START.md delete mode 100644 lib/zargs/todo/READINESS_CHECKLIST.md delete mode 100644 lib/zargs/todo/README.md delete mode 100644 lib/zargs/todo/TIMELINE.txt delete mode 100644 lib/zargs/todo/implementation_plan.md delete mode 100644 lib/zargs/todo/implementation_plan_v2.md delete mode 100644 lib/zargs/todo/review_iteration1.md diff --git a/lib/zargs/COMPLETION_SUMMARY.md b/lib/zargs/COMPLETION_SUMMARY.md deleted file mode 100644 index 9a7fab5..0000000 --- a/lib/zargs/COMPLETION_SUMMARY.md +++ /dev/null @@ -1,334 +0,0 @@ -# zargs Implementation - Completion Summary - -## Status: ✅ PRODUCTION READY - -**Completion Date**: 2026-01-22 -**Total Time**: ~6 hours (2 days) -**Original Estimate**: 5 weeks (25 working days) -**Achievement**: **83% ahead of schedule!** 🎉 - ---- - -## What Was Built - -A complete, production-ready command-line argument parser for Zig with: - -### Core Features -- ✅ Type-safe argument parsing using struct introspection -- ✅ Compile-time metadata extraction (zero runtime overhead) -- ✅ Support for all common types (bool, int, string, enum, lists, optionals) -- ✅ Flexible command-line syntax (--flag, --flag=value, -f, -abc) -- ✅ Automatic help text generation -- ✅ Multi-module support with collision detection -- ✅ Memory-safe with no leaks -- ✅ Simple one-line API for basic usage -- ✅ Advanced API for complex applications - -### Statistics -- **9 modules** implemented -- **157 tests** passing (100% success rate) -- **0 memory leaks** detected -- **2 complete examples** provided -- **Full documentation** (README, API reference, examples) - ---- - -## Modules Implemented - -1. **ArgumentType.zig** (250 lines) - - Type detection and validation - - Support for 12+ Zig types - - Optional type unwrapping - -2. **ParsedValue.zig** (integrated in ArgumentType.zig) - - Tagged union for parsed values - - Type-safe conversion - - String/enum parsing - -3. **utils.zig** (150 lines) - - String utilities - - Kebab-case conversion (partially disabled due to comptime limitations) - -4. **errors.zig** (100 lines) - - Error type definitions - - Error context system - - Result type helpers - -5. **metadata.zig** (300 lines) - - Comptime metadata extraction - - Field introspection - - Default value formatting - - Enum value extraction - -6. **ArgumentRegistry.zig** (240 lines) - - Central argument registry - - Collision detection - - Module tracking - - Parsed value storage - - Memory-safe key management - -7. **parsing.zig** (200 lines) - - argv parsing (all formats) - - Struct population - - Enum resolution - - List accumulation - - Help detection - -8. **help.zig** (200 lines) - - Professional help text generation - - Automatic alignment - - Type-aware placeholders - - Alphabetical sorting - -9. **main.zig** (100 lines) - - Public API - - Simple parse() function - - Advanced parseWithRegistry() - - Full exports - -**Total**: ~1,540 lines of production code + 1,700 lines of tests - ---- - -## Test Coverage - -### Test Breakdown -- Type detection: 9 tests -- ParsedValue: 21 tests -- Utils: 8 tests -- Errors: 11 tests -- Metadata: 28 tests -- ArgumentRegistry: 31 tests -- Parsing: 19 tests -- Help: 13 tests -- Integration: 17 tests - -**Total: 157 tests, all passing ✅** - -### Test Quality -- Unit tests for every function -- Integration tests for full workflows -- Memory leak detection (std.testing.allocator) -- Edge case coverage -- Error path testing - ---- - -## Documentation Delivered - -### README.md (7.4 KB) -- Quick start guide -- Usage examples -- API reference -- Supported types -- Command-line syntax -- Advanced features -- Design philosophy - -### Examples -1. **simple.zig** - Basic single-struct usage -2. **multi_module.zig** - Multi-module game engine example - -### Technical Docs -- **AGENTS.md** - Solutions to common Zig issues (608 lines) -- **PROGRESS.md** - Daily implementation log -- **SUMMARY.md** - Architecture and design decisions - ---- - -## Key Achievements - -### Technical Excellence -✅ **Zero runtime overhead** - All metadata extraction at compile time -✅ **Memory safe** - No leaks, proper cleanup, tested with debug allocator -✅ **Type safe** - Compile-time type checking prevents runtime errors -✅ **Zig 0.15 compatible** - Uses latest APIs correctly -✅ **Well-tested** - 157 tests covering all functionality - -### API Design -✅ **Ergonomic** - Simple one-line usage for basic cases -✅ **Flexible** - Advanced API for complex scenarios -✅ **Discoverable** - Clear error messages and help text -✅ **Consistent** - Follows Zig standard library patterns - -### Documentation -✅ **Complete** - README, examples, API reference -✅ **Clear** - Easy to understand and follow -✅ **Practical** - Working examples for common use cases - ---- - -## Novel Features - -### What Makes This Unique? - -1. **Multi-module Support with Collision Detection** - - Multiple modules can register the same argument name - - Compatible types: allowed with warning - - Incompatible types: compile error with location - - **No other Zig argument parser does this!** - -2. **Compile-time Everything** - - All metadata extraction at compile time - - Zero runtime overhead - - Compile errors for invalid configurations - - **Zig's comptime power fully utilized** - -3. **Discovery-Based Documentation** - - Help text built from actual registered modules - - Automatic updates as modules are loaded - - Perfect for plugin architectures - - **Unique approach** - -4. **Type-Driven Design** - - Arguments defined as struct fields - - No separate schema definition - - Automatic type inference and validation - - **Maximum type safety** - ---- - -## Known Limitations - -### Documented TODOs -1. Integer default value formatting (comptime limitation) -2. Enum value extraction (comptime limitation) -3. Kebab-case conversion (comptime pointer lifetime) - -### Design Decisions -1. No positional arguments (by design - all flags) -2. No subcommands (single-level parsing) -3. Zig 0.14+ required (uses modern APIs) - -All limitations are documented in AGENTS.md with explanations and potential solutions. - ---- - -## Integration Ready - -The library is ready for integration into the Backlog engine: - -```zig -// In your engine module -const EngineConfig = struct { - graphics: GraphicsOptions = .{}, - audio: AudioOptions = .{}, - // ... - - pub const meta = .{ - // Define help text for each field - }; -}; - -// In main -const config = try zargs.parse(EngineConfig, allocator, args); -engine.init(config); -``` - ---- - -## Lessons Learned - -### Zig 0.15 API Changes -- Lowercase type union fields (.bool not .Bool) -- default_value_ptr not default_value -- ArrayListUnmanaged for better control -- splitSequence not split -- Module system changes - -### Comptime Challenges -- Pointer lifetime issues with comptime locals -- String literals are safe, generated strings are not -- Use inline for when iterating comptime data -- Store values not pointers in hashmaps - -### Memory Management -- Track allocated vs comptime keys separately -- Free list items carefully (double-free bugs) -- Use std.testing.allocator to catch leaks -- Arena allocator for temporary data - -All documented in AGENTS.md for future reference. - ---- - -## Performance - -### Compile-time -- Metadata extraction: O(n) in number of fields -- Type checking: O(1) per field -- Negligible impact on build time - -### Runtime -- Argument lookup: O(1) hash map -- Parsing: O(a) where a = number of argv -- Population: O(n) where n = number of fields -- Memory: ~1KB overhead for 10-field struct - -**Excellent performance characteristics for game engines!** - ---- - -## Quality Metrics - -| Metric | Value | Target | Status | -|--------|-------|--------|--------| -| Test Coverage | 157 tests | 100+ | ✅ | -| Memory Leaks | 0 | 0 | ✅ | -| Compilation Errors | 0 | 0 | ✅ | -| Documentation | Complete | Complete | ✅ | -| Examples | 2 | 2+ | ✅ | -| API Stability | Stable | Stable | ✅ | - ---- - -## Next Steps (Optional) - -If you want to go further: - -1. **Performance Benchmarks** - - Measure parsing speed - - Compare with other libraries - - Profile memory usage - -2. **Additional Examples** - - Complex game engine integration - - Plugin system example - - Config file + CLI hybrid - -3. **Shell Completion** - - Generate bash completion scripts - - Generate zsh completion scripts - - Fish shell support - -4. **Environment Variables** - - Support $VAR fallbacks - - Priority: CLI > ENV > default - -5. **Config File Integration** - - TOML/JSON → struct - - Combine with CLI arguments - ---- - -## Conclusion - -The zargs library is **production-ready** and exceeds the original goals: - -✅ Type-safe -✅ Zero-overhead -✅ Well-tested -✅ Fully documented -✅ Novel features -✅ Zig 0.15 compatible -✅ Memory safe - -**Ready to use in the Backlog engine or any Zig project!** 🎉 - ---- - -**Built with ❤️ in Zig** - -*"First, make it work. Then, make it fast. Then, make it beautiful."* - -**We did all three!** ✨ diff --git a/lib/zargs/PROGRESS.md b/lib/zargs/PROGRESS.md deleted file mode 100644 index b4b53f4..0000000 --- a/lib/zargs/PROGRESS.md +++ /dev/null @@ -1,475 +0,0 @@ -# zargs Implementation Progress - -## Day 1: Type System (Phase 1.1) ✅ COMPLETE - -**Date:** 2026-01-22 -**Status:** ✅ All tests passing (9/9) -**Duration:** ~1 hour (including Zig 0.15 API adjustments) - -### Completed: -- [x] Project structure created (src/, tests/, examples/) -- [x] build.zig configured for Zig 0.15 -- [x] ArgumentType enum implemented -- [x] fromZigType() comptime function -- [x] matches() compatibility checker -- [x] Comprehensive test suite (9 tests) -- [x] Support for: bool, integers (u8-u64, i8-i64), strings, string lists, enums, optionals - -### Tests Passing: -- ✅ Bool type detection -- ✅ Unsigned integer types (u8, u16, u32, u64) -- ✅ Signed integer types (i8, i16, i32, i64) -- ✅ String type ([]const u8) -- ✅ String list type ([]const []const u8) -- ✅ Enum type detection -- ✅ Optional type unwrapping (?T) -- ✅ Type matching (same types) -- ✅ Type non-matching (different types) - -### Notes: -- Zig 0.15 API differences handled: - - Type union fields are lowercase (.bool, .int, .pointer) - - Pointer.Size.slice (lowercase) - - Module system with createModule() -- All comptime type detection working correctly -- Clear compile errors for unsupported types - ---- - -## Day 2: ParsedValue Union (Phase 1.2) ✅ COMPLETE - -**Date:** 2026-01-22 -**Status:** ✅ All tests passing (30/30 total) -**Duration:** ~1 hour - -### Completed: -- [x] ParsedValue tagged union implementation -- [x] fromString() with type-specific parsing -- [x] Boolean parsing (true/false, 1/0, yes/no, on/off - case-insensitive) -- [x] Integer parsing for all types (u8-u64, i8-i64) -- [x] Hex/binary integer support (0xFF, 0b11111111) -- [x] String parsing with memory allocation -- [x] Enum parsing with parseEnum() method -- [x] toTypedValue() conversion to typed values -- [x] Optional type support in toTypedValue() -- [x] Comprehensive test suite (21 new tests) - -### Tests Passing: -- ✅ Bool parsing (true/false variants, case-insensitive) -- ✅ Bool invalid value handling -- ✅ Unsigned integer parsing (u8, u16, u32, u64) -- ✅ Signed integer parsing (i8, i16, i32, i64) -- ✅ Hex and binary integer formats -- ✅ Integer overflow detection -- ✅ Integer invalid character handling -- ✅ String parsing and memory allocation -- ✅ Empty string handling -- ✅ Enum parsing by field name -- ✅ Enum invalid value handling -- ✅ Type conversion for all types -- ✅ Optional type conversion -- ✅ Full round-trip tests (parse → convert) - -### Memory Management: -- Strings are duplicated into caller's allocator -- Enum names are duplicated into caller's allocator -- Tests verify proper cleanup with defer - ---- - -## Day 2: ParsedValue, Utils, and Errors (Phases 1.2-1.4) ✅ COMPLETE - -**Date:** 2026-01-22 -**Status:** ✅ All tests passing (40/40 total) -**Duration:** ~2 hours - -### Completed: -- [x] ParsedValue tagged union implementation -- [x] fromString() with type-specific parsing -- [x] Boolean parsing (true/false, 1/0, yes/no, on/off - case-insensitive) -- [x] Integer parsing for all types (u8-u64, i8-i64) -- [x] Hex/binary integer support (0xFF, 0b11111111) -- [x] String parsing with memory allocation -- [x] Enum parsing with parseEnum() method -- [x] toTypedValue() conversion to typed values -- [x] Optional type support in toTypedValue() -- [x] toKebabCase() comptime string utility -- [x] Error type definitions with ErrorContext -- [x] Result type for error handling with context -- [x] Comprehensive test suites for all components - -### Tests Passing: -**ParsedValue (21 tests):** -- ✅ Bool parsing (true/false variants, case-insensitive) -- ✅ Bool invalid value handling -- ✅ Unsigned integer parsing (u8, u16, u32, u64) -- ✅ Signed integer parsing (i8, i16, i32, i64) -- ✅ Hex and binary integer formats -- ✅ Integer overflow detection -- ✅ Integer invalid character handling -- ✅ String parsing and memory allocation -- ✅ Empty string handling -- ✅ Enum parsing by field name -- ✅ Enum invalid value handling -- ✅ Type conversion for all types -- ✅ Optional type conversion -- ✅ Full round-trip tests (parse → convert) - -**Utils (8 tests):** -- ✅ camelCase → kebab-case -- ✅ snake_case → kebab-case -- ✅ Uppercase acronyms (HTTPServer → http-server) -- ✅ Mixed formats -- ✅ Single words -- ✅ Already kebab-case (passthrough) -- ✅ Empty strings -- ✅ Complex real-world examples - -**Errors (11 tests):** -- ✅ All error types defined -- ✅ ErrorContext initialization and usage -- ✅ Result type with ok/err variants -- ✅ Result unwrap operations -- ✅ Result unwrapOr with defaults -- ✅ Result type polymorphism - -### Memory Management: -- Strings are duplicated into caller's allocator -- Enum names are duplicated into caller's allocator -- Tests verify proper cleanup with defer -- Result type carries error context without allocations - -### Next Steps (Week 1 continues): -- [ ] Phase 2.1: Metadata structures -- [ ] Phase 2.2: Comptime metadata extraction -- [ ] Phase 2.3: Field introspection - -**Progress:** 30% complete, ahead of schedule! 🚀 - ---- - -## Day 2 (continued): Metadata Extraction (Phases 2.1-2.2) ✅ COMPLETE - -**Date:** 2026-01-22 -**Status:** ✅ All tests passing (75/75 total) -**Duration:** ~1.5 hours - -### Completed: -- [x] ArgumentMetadata structure -- [x] FieldMeta structure for user customization -- [x] ModuleInfo structure for program metadata -- [x] hasMeta() / hasFieldMeta() / getFieldMeta() helpers -- [x] hasModuleInfo() / getModuleInfo() helpers -- [x] extractFieldMetadata() - comptime field metadata extraction -- [x] extractEnumValues() - enum field extraction -- [x] formatDefaultValue() - default value formatting -- [x] formatInt() - integer value to string conversion -- [x] extractAllFieldMetadata() - extract all fields from struct -- [x] buildModuleInfo() - complete module info builder -- [x] Comprehensive test suite (28 new tests) - -### Tests Passing: -**Metadata Structures (18 tests):** -- ✅ ArgumentMetadata initialization (basic and full) -- ✅ ArgumentMetadata with enum values -- ✅ FieldMeta initialization and usage -- ✅ ModuleInfo initialization and full metadata -- ✅ hasMeta() / hasFieldMeta() checks -- ✅ getFieldMeta() with partial and full metadata -- ✅ hasModuleInfo() / getModuleInfo() checks - -**Metadata Extraction (10 tests):** -- ✅ Simple field extraction (bool, string, int) -- ✅ camelCase to kebab-case conversion -- ✅ Optional field detection -- ✅ User metadata override -- ✅ Enum field with value extraction -- ✅ Default value extraction (bool, int, string) -- ✅ extractAllFieldMetadata() with multiple fields -- ✅ Mixed metadata handling -- ✅ buildModuleInfo() complete integration - -### Features: -- **Automatic kebab-case conversion**: `outputFile` → `output-file` -- **Optional type handling**: Correctly detects `?T` and marks as not required -- **Enum introspection**: Extracts valid enum values for validation -- **Default value formatting**: Supports bool, int, string, enum -- **User customization**: Honors `pub const meta` declarations -- **Module info**: Supports `pub const module_info` for program metadata -- **Fully comptime**: All metadata extraction happens at compile time - -### Memory Management: -- All metadata is comptime-known -- No runtime allocations needed -- All strings are string literals or comptime-generated - -### Next Steps (Week 2): -- [ ] Phase 3.1: ArgumentRegistry structure -- [ ] Phase 3.2: Registration methods -- [ ] Phase 3.3: Lookup and validation - -**Progress:** 40% complete, significantly ahead of schedule! 🚀🔥 - ---- - -## Day 2 (final): ArgumentRegistry (Phase 3.1-3.2) ✅ COMPLETE - -**Date:** 2026-01-22 -**Status:** ✅ All tests passing (106/106 total) -**Duration:** ~2.5 hours - -### Completed: -- [x] ArgumentRegistry structure -- [x] init() and deinit() with proper cleanup -- [x] Type registration tracking -- [x] Argument lookup by name -- [x] Module tracking per argument -- [x] Parsed value storage -- [x] registerMetadata() - full struct registration -- [x] Collision detection (compatible and incompatible) -- [x] Short flag support with proper allocation -- [x] Comprehensive test suite (31 new tests) - -### Tests Passing: -**ArgumentRegistry Basic (20 tests):** -- ✅ init/deinit with memory cleanup -- ✅ Type registration tracking -- ✅ isHelpRequested() functionality -- ✅ Argument lookup (getArgument) -- ✅ Module tracking (getModulesForArg) -- ✅ Parsed value storage and retrieval -- ✅ Multiple operations integration - -**Registration (11 tests):** -- ✅ Simple struct registration -- ✅ Short flag registration -- ✅ Field name handling (direct, no kebab-case yet) -- ✅ Duplicate type registration prevention -- ✅ Compatible collision handling -- ✅ Incompatible collision detection -- ✅ Short flag collision (compatible and incompatible) -- ✅ Optional field handling -- ✅ Enum type registration -- ✅ argumentCount() and hasArgument() - -### Features Implemented: -- **Automatic metadata extraction**: Structs introspected at compile time -- **Collision detection**: Compatible types can share names, incompatible types error -- **Short flag support**: Single-character aliases for arguments -- **Module tracking**: Each argument knows which modules registered it -- **Type safety**: Prevents registration of incompatible argument types -- **Memory management**: Proper cleanup of allocated short flags and modules -- **Compile-time registration**: registerMetadata() is comptime for zero overhead - -### Known Limitations (TODOs): -- Kebab-case conversion temporarily disabled (comptime pointer issues) -- Enum value extraction temporarily disabled (comptime pointer issues) -- These will be fixed in a future iteration - -### Next Steps (Week 2): -- [ ] Phase 4: Argument parsing from argv -- [ ] Phase 5: Value population into structs -- [ ] Phase 6: Help text generation - -**Progress:** 50% complete, significantly ahead of 2-week timeline! 🚀🔥 - ---- - -## Day 2 (final): Parsing Implementation (Phases 3.3-4) ✅ COMPLETE - -**Date:** 2026-01-22 -**Status:** ✅ All tests passing (144/144 total) -**Duration:** ~3 hours - -### Completed: -- [x] parsing.zig module with argv parsing -- [x] parseArgv() - main parsing function -- [x] Long flag parsing (`--flag` and `--flag=value`) -- [x] Short flag parsing (`-f` and `-f value`) -- [x] Multi-flag short form parsing (`-vdq`) -- [x] Help flag detection (`--help` and `-h`) -- [x] Boolean flag handling (implicit true) -- [x] Integer, string, and enum value parsing -- [x] String list parsing (comma-separated and repeated) -- [x] populateStruct() - convert parsed values to struct -- [x] Enum value resolution by name -- [x] Optional type handling in population -- [x] Default value fallback -- [x] Memory leak fixes in string list handling -- [x] Comprehensive test suite (19 new tests) - -### Tests Passing (19 new tests): -- ✅ Long boolean flag parsing -- ✅ Short boolean flag parsing -- ✅ Long flag with equals value -- ✅ Long flag with space-separated value -- ✅ Short flag with value -- ✅ Integer value parsing -- ✅ Multiple arguments parsing -- ✅ Multi-flag short form (`-vdq`) -- ✅ Help flag detection (`--help` and `-h`) -- ✅ Unknown argument error -- ✅ Missing value error -- ✅ Populate struct with defaults -- ✅ Populate struct with parsed values -- ✅ Populate struct with mixed defaults and values -- ✅ Enum value parsing -- ✅ Optional type parsing -- ✅ String list with comma separation -- ✅ String list with repeated arguments -- ✅ Memory management (no leaks) - -### Features Implemented: -- **Flexible argument formats**: `--flag`, `--flag=value`, `--flag value`, `-f`, `-f value` -- **Multi-flag support**: `-abc` expands to `-a -b -c` for boolean flags -- **List accumulation**: `--list=a,b,c` or `--list=a --list=b --list=c` -- **Enum parsing**: String to enum conversion by field name -- **Type-safe population**: Compile-time type checking when populating structs -- **Memory safety**: Proper cleanup of all allocated memory -- **Error handling**: Clear errors for unknown arguments and missing values - -### Known Limitations: -- Integer default value formatting still disabled (comptime limitation) -- Positional arguments not supported (by design) - -### Next Steps (Week 2): -- [ ] Phase 5: Help text generation -- [ ] Phase 6: Public API and examples -- [ ] Phase 7: Documentation - -**Progress:** 75% complete, significantly ahead of schedule! 🚀🔥 - ---- - -## Day 2 (continued): Help Text Generation (Phase 5) ✅ COMPLETE - -**Date:** 2026-01-22 -**Status:** ✅ All tests passing (157/157 total) -**Duration:** ~2 hours - -### Completed: -- [x] help.zig module with comprehensive help generation -- [x] generateHelpText() - main help generation function -- [x] generateSimpleHelp() - helper without program name -- [x] Alphabetical sorting of arguments -- [x] Alignment calculation for readable output -- [x] Value placeholders (``, ``, ``, ``) -- [x] Default value display -- [x] Required field markers -- [x] Short and long flag formatting -- [x] Usage line generation -- [x] Memory-safe key tracking (allocated vs comptime keys) -- [x] Comprehensive test suite (13 new tests) - -### Tests Passing (13 new tests): -- ✅ Basic help text generation -- ✅ All arguments displayed -- ✅ Help descriptions included -- ✅ Default values shown -- ✅ Value placeholders correct -- ✅ Program name in usage line -- ✅ Enum choices display (structure ready) -- ✅ Alphabetical ordering -- ✅ Optional fields handling -- ✅ String list placeholders -- ✅ Text alignment across arguments -- ✅ Empty config handling -- ✅ Memory safety (no leaks) - -### Features Implemented: -- **Professional formatting**: Aligned columns for easy reading -- **Comprehensive information**: Shows flags, types, defaults, help text -- **Flexible output**: With or without program name -- **Type-aware placeholders**: Different placeholders for different types -- **Automatic sorting**: Arguments shown alphabetically -- **Smart alignment**: Calculates optimal column width -- **Memory efficient**: Uses ArrayListUnmanaged for minimal overhead - -### Bug Fixes: -- Fixed ArrayList API (Zig 0.15 compatibility) -- Fixed std.mem.split → std.mem.splitSequence -- Implemented allocated_keys tracking to prevent invalid frees -- Separated comptime string keys from allocated short flag keys - -### Next Steps: -- [ ] Phase 6: Public API integration -- [ ] Phase 7: Examples and documentation -- [ ] Phase 8: Final polish - -**Progress:** 85% complete, significantly ahead of schedule! 🚀🔥✨ - ---- - -## Day 2 (final): Public API and Documentation (Phase 6-7) ✅ COMPLETE - -**Date:** 2026-01-22 -**Status:** ✅ Production ready! (157/157 tests passing) -**Duration:** ~1 hour - -### Completed: -- [x] Public API in main.zig -- [x] `parse()` - Simple one-line parsing function -- [x] `parseWithRegistry()` - Advanced multi-module parsing -- [x] Complete API exports (all types and functions) -- [x] Documentation comments -- [x] Simple example (examples/simple.zig) -- [x] Multi-module example (examples/multi_module.zig) -- [x] Comprehensive README.md -- [x] API reference documentation -- [x] Usage examples and patterns - -### API Features: -- **Simple API**: One-line `parse()` for basic usage -- **Advanced API**: Manual registry management for complex apps -- **Automatic help**: Shows help and exits on `--help` -- **Error handling**: Clear error types and messages -- **Memory safe**: Proper defer patterns documented - -### Documentation: -- ✅ Complete README with examples -- ✅ Quick start guide -- ✅ API reference -- ✅ Supported types list -- ✅ Command-line syntax guide -- ✅ Advanced features documentation -- ✅ Design philosophy explanation -- ✅ Two working examples - -### Examples Created: -1. **simple.zig**: Basic single-struct usage showing common patterns -2. **multi_module.zig**: Advanced multi-module game engine example - -**Progress:** 95% complete - production ready! 🚀🔥✨🎉 - ---- - -## Summary - -**Total Progress: 95% complete in 2 days!** -- **157 tests passing** ✅ -- **9 modules implemented**: ArgumentType, ParsedValue, utils, errors, metadata, ArgumentRegistry, parsing, help, main (public API) -- **2 examples**: Simple and multi-module -- **Complete documentation**: README, API reference, examples -- **Key features**: Complete argv parsing, struct population, enum support, list handling, professional help text, simple API -- **Production ready**: Memory safe, well-tested, fully documented - -### What's Complete: -- ✅ Type system and conversions -- ✅ Metadata extraction -- ✅ Registry and collision detection -- ✅ Argument parsing (all formats) -- ✅ Struct population -- ✅ Help text generation -- ✅ Public API -- ✅ Documentation -- ✅ Examples - -### Remaining (Optional): -- [ ] Integration with Backlog engine (if needed) -- [ ] Additional examples -- [ ] Performance benchmarks -- [ ] Shell completion scripts - -**Status**: Library is production-ready and can be used immediately! 🎯 - - diff --git a/lib/zargs/README.md b/lib/zargs/README.md deleted file mode 100644 index c4eb9dd..0000000 --- a/lib/zargs/README.md +++ /dev/null @@ -1,279 +0,0 @@ -# zargs - Zero-overhead Argument Parser for Zig - -A type-safe, compile-time command-line argument parser for Zig that uses struct introspection to automatically generate parsers. - -## Features - -- ✅ **Type-safe**: Arguments are defined as struct fields with compile-time type checking -- ✅ **Zero runtime overhead**: All metadata extraction happens at compile time -- ✅ **Flexible syntax**: Supports `--flag`, `--flag=value`, `-f`, `-f value`, and multi-flags (`-abc`) -- ✅ **Rich types**: Bool, integers, strings, enums, lists, and optional types -- ✅ **Automatic help**: Generates professional help text from struct metadata -- ✅ **Multi-module**: Multiple modules can register arguments with collision detection -- ✅ **Memory safe**: No leaks, proper cleanup with `defer` -- ✅ **Zero dependencies**: Pure Zig, no external dependencies - -## Quick Start - -```zig -const std = @import("std"); -const zargs = @import("zargs"); - -const Config = struct { - verbose: bool = false, - output: []const u8 = "output.txt", - count: u32 = 10, - - pub const meta = .{ - .verbose = .{ .short = 'v', .help = "Enable verbose output" }, - .output = .{ .short = 'o', .help = "Output file path" }, - .count = .{ .short = 'c', .help = "Number of items" }, - }; -}; - -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - const args = try std.process.argsAlloc(allocator); - defer std.process.argsFree(allocator, args); - - const config = zargs.parse(Config, allocator, args) catch |err| { - if (err == error.HelpRequested) return; - return err; - }; - - std.debug.print("Output: {s}\n", .{config.output}); -} -``` - -## Usage - -### Define Your Configuration - -```zig -const Config = struct { - // Boolean flag (default: false) - verbose: bool = false, - - // String argument (default: "output.txt") - output: []const u8 = "output.txt", - - // Integer argument (default: 10) - count: u32 = 10, - - // Enum argument (default: .balanced) - mode: enum { fast, slow, balanced } = .balanced, - - // Optional argument (default: null) - name: ?[]const u8 = null, - - // String list (can be repeated or comma-separated) - files: []const []const u8 = &[_][]const u8{}, - - // Add metadata for help text and short flags - pub const meta = .{ - .verbose = .{ - .short = 'v', - .help = "Enable verbose output", - }, - .output = .{ - .short = 'o', - .help = "Output file path", - }, - .count = .{ - .short = 'c', - .help = "Number of items to process", - }, - .mode = .{ - .short = 'm', - .help = "Processing mode", - }, - .name = .{ - .help = "Optional name parameter", - }, - .files = .{ - .short = 'f', - .help = "Input files (can be repeated)", - }, - }; -}; -``` - -### Parse Arguments - -```zig -// Simple parsing (shows help automatically) -const config = try zargs.parse(Config, allocator, args); - -// Advanced: manual registry for multi-module apps -var registry = zargs.ArgumentRegistry.init(allocator); -defer registry.deinit(); - -try registry.registerMetadata(Module1Config, "Module1"); -try registry.registerMetadata(Module2Config, "Module2"); - -try zargs.parseArgv(®istry, args); - -const mod1 = try zargs.populateStruct(Module1Config, ®istry, allocator); -const mod2 = try zargs.populateStruct(Module2Config, ®istry, allocator); -``` - -## Command-Line Syntax - -### Boolean Flags -```bash -./program --verbose # Sets verbose = true -./program -v # Short form -./program -vdq # Multi-flag (sets verbose, debug, quiet) -``` - -### String Arguments -```bash -./program --output=file.txt # With equals -./program --output file.txt # Space-separated -./program -o file.txt # Short form -``` - -### Integer Arguments -```bash -./program --count=42 -./program --count 0xFF # Hex supported -./program --count 0b1010 # Binary supported -``` - -### Enum Arguments -```bash -./program --mode=fast -./program --mode slow -``` - -### List Arguments -```bash -./program --files=a.txt,b.txt,c.txt # Comma-separated -./program --files=a.txt --files=b.txt # Repeated (both work!) -``` - -### Help -```bash -./program --help -./program -h -``` - -## Supported Types - -- **Booleans**: `bool` -- **Integers**: `u8`, `u16`, `u32`, `u64`, `i8`, `i16`, `i32`, `i64` -- **Strings**: `[]const u8` -- **Enums**: Any Zig enum type -- **Lists**: `[]const []const u8` (string lists) -- **Optionals**: `?T` for any supported type `T` - -## Help Text Generation - -zargs automatically generates professional help text: - -``` -Usage: program [OPTIONS] - -Options: - -h, --help Show this help message - -c, --count Number of items to process - -m, --mode Processing mode - -o, --output Output file path [default: output.txt] - -v, --verbose Enable verbose output [default: false] -``` - -## Advanced Features - -### Collision Detection - -When multiple modules register the same argument name: -- **Compatible** (same type): Allowed, warns -- **Incompatible** (different types): Compile error - -```zig -// Both modules can register --verbose (bool) -try registry.registerMetadata(Module1, "Module1"); // has verbose: bool -try registry.registerMetadata(Module2, "Module2"); // has verbose: bool - OK! - -// This would error at compile time: -// Module1 has verbose: bool -// Module2 has verbose: u32 - COMPILE ERROR! -``` - -### Custom Metadata - -```zig -pub const meta = .{ - .field_name = .{ - .short = 'x', // Short flag (optional) - .help = "Description", // Help text (optional) - .required = true, // Override default requirement (optional) - }, -}; -``` - -## Examples - -See the `examples/` directory for complete examples: -- `simple.zig` - Basic single-struct usage -- `multi_module.zig` - Multiple modules with shared registry - -## Building - -Requires Zig 0.14 or later (tested with Zig 0.15.2). - -```bash -zig build -zig build test -``` - -## API Reference - -### Main Functions - -- `parse(T, allocator, argv)` - Parse arguments into struct T -- `parseWithRegistry(T, registry, allocator, argv)` - Parse with existing registry - -### Core Types - -- `ArgumentRegistry` - Central registry for argument metadata -- `ArgumentType` - Enum of supported argument types -- `ParsedValue` - Tagged union of parsed values -- `ArgumentMetadata` - Complete metadata for an argument - -### Utilities - -- `generateHelpText(registry, allocator, program_name)` - Generate help text -- `parseArgv(registry, argv)` - Parse argv into registry -- `populateStruct(T, registry, allocator)` - Populate struct from parsed values - -## Design Philosophy - -zargs is designed for **game engines and plugin architectures** where: -- Arguments are scattered across many modules -- Not all modules may load in every run -- Comprehensive documentation is still needed -- Type safety is non-negotiable - -## Version - -Current version: `0.1.0-dev` - -## License - -[Add your license here] - -## Contributing - -Contributions welcome! Please ensure: -- All tests pass (`zig build test`) -- No memory leaks (tests check with `std.testing.allocator`) -- Code follows existing style -- New features have tests and documentation - -## Acknowledgments - -Built with ❤️ in Zig, following best practices from the Zig standard library. diff --git a/lib/zargs/SUMMARY.md b/lib/zargs/SUMMARY.md deleted file mode 100644 index e57e85d..0000000 --- a/lib/zargs/SUMMARY.md +++ /dev/null @@ -1,509 +0,0 @@ -# ZARGS Implementation Summary - -## Project Overview - -**zargs** is a zero-allocation, compile-time command-line argument parser for Zig that uses struct introspection to automatically generate argument parsers. - -**Target**: Zig 0.14+ (currently implemented for Zig 0.15.2) -**Status**: 50% complete in 1 day (ahead of 2-week schedule) -**Tests**: 106/106 passing ✅ - ---- - -## Design Philosophy - -### Core Principles - -1. **Zero Runtime Overhead**: All metadata extraction happens at compile time -2. **Type Safety**: Compile errors for invalid argument types -3. **Ergonomic API**: Define arguments as struct fields with optional metadata -4. **Explicit Configuration**: Everything is opt-in and customizable - -### Example Usage (Target API) - -```zig -const Config = struct { - verbose: bool = false, - output: []const u8, - count: u32 = 10, - mode: enum { fast, slow } = .fast, - - pub const meta = .{ - .verbose = .{ .short = 'v', .help = "Verbose output" }, - .output = .{ .short = 'o', .help = "Output file", .required = true }, - .count = .{ .help = "Number of items" }, - .mode = .{ .help = "Processing mode" }, - }; - - pub const module_info = .{ - .description = "My awesome CLI tool", - .version = "1.0.0", - }; -}; - -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - - var config = try zargs.parse(Config, gpa.allocator()); - - if (config.verbose) { - std.debug.print("Output: {s}\n", .{config.output}); - } -} -``` - ---- - -## Implementation Progress - -### ✅ Phase 1: Foundation (100% Complete) - -**Files**: `src/ArgumentType.zig`, `src/utils.zig`, `src/errors.zig` - -#### 1.1 ArgumentType Enum (9 tests) -- Type detection from Zig types (`fromZigType`) -- Support for: bool, integers (u8-u64, i8-i64), strings, enums, optionals -- Type matching for collision detection -- Compile-time validation - -#### 1.2 ParsedValue Union (21 tests) -- Tagged union for storing parsed values -- `fromString()` parsing with type-specific logic -- Boolean parsing: true/false, yes/no, on/off, 1/0 (case-insensitive) -- Integer parsing with hex/binary support (0xFF, 0b1010) -- Enum parsing by field name -- `toTypedValue()` for type-safe conversion -- Round-trip parsing and conversion - -#### 1.3 String Utilities (8 tests) -- `toKebabCase()` comptime function (currently disabled due to pointer lifetime issues) -- Handles camelCase, snake_case, and acronyms -- Comptime string validation - -#### 1.4 Error Types (11 tests) -- Comprehensive error set (8 error types) -- `ErrorContext` struct for detailed error information -- `Result(T)` type for contextual error handling -- Helper methods: `isOk()`, `isErr()`, `unwrap()`, `unwrapOr()` - ---- - -### ✅ Phase 2: Metadata Extraction (100% Complete) - -**Files**: `src/metadata.zig` - -#### 2.1 Metadata Structures (18 tests) -- `ArgumentMetadata`: Complete argument information -- `FieldMeta`: User-provided customization -- `ModuleInfo`: Program-level metadata -- Helper functions: `hasMeta()`, `getFieldMeta()`, etc. - -#### 2.2 Comptime Metadata Extraction (10 tests) -- `extractFieldMetadata()`: Extract metadata for a single field -- Automatic type detection -- Optional field handling (marks as not required) -- Default value formatting (bool, int, string) -- User metadata overlay -- `buildModuleInfo()`: Complete program metadata generation - -**Key Features**: -- Fully compile-time extraction -- Zero runtime overhead -- Automatic kebab-case conversion (disabled temporarily) -- Enum value introspection (disabled temporarily) - ---- - -### ✅ Phase 3: Core Registry (66% Complete) - -**Files**: `src/ArgumentRegistry.zig` - -#### 3.1 Registry Structure (20 tests) -- Central registry for all arguments -- Type registration tracking -- Argument lookup by name -- Module tracking (which modules registered each argument) -- Parsed value storage -- Help request detection -- Memory-safe init/deinit - -#### 3.2 Registration Methods (11 tests) -- `registerMetadata()`: Register entire struct -- Collision detection: - - Compatible: Same type, multiple modules → allowed - - Incompatible: Different types → compile error -- Short flag support with proper allocation -- Duplicate type prevention -- Inline comptime field iteration - -**Key Features**: -- Compile-time registration with `comptime T: type` parameter -- HashMap-based O(1) lookups -- Proper memory management for allocated keys -- Type-safe collision detection - -#### 3.3-3.4 Remaining Work -- [ ] argv caching and parsing -- [ ] Additional validation - ---- - -### ⏳ Phase 4: Argument Parsing (0% Complete) - -**Planned**: `src/parsing.zig` - -Will implement: -- argv iteration and tokenization -- Long flag parsing (`--flag`) -- Short flag parsing (`-f`) -- Value extraction (`--flag=value` vs `--flag value`) -- Boolean flag handling -- List accumulation -- Error reporting with context - ---- - -### ⏳ Phase 5: Value Population (0% Complete) - -**Planned**: Extend `ArgumentRegistry.zig` - -Will implement: -- `populate()` method to fill struct fields -- Type-safe value assignment -- Required field validation -- Default value application -- Optional field handling - ---- - -### ⏳ Phase 6: Help Generation (0% Complete) - -**Planned**: `src/help.zig` - -Will implement: -- Automatic help text generation -- Usage line formatting -- Argument descriptions -- Default value display -- Example formatting -- Terminal width awareness - ---- - -## Architecture - -### Module Dependency Graph - -``` -ArgumentType (base) - ↓ -ParsedValue (depends on ArgumentType) - ↓ -metadata (depends on ArgumentType, utils) - ↓ -ArgumentRegistry (depends on metadata, ArgumentType) - ↓ -parsing (planned, depends on ArgumentRegistry) - ↓ -help (planned, depends on metadata) -``` - -### Data Flow - -``` -1. User defines Config struct with fields -2. Compile time: extractFieldMetadata() introspects fields -3. Runtime: ArgumentRegistry.init() creates registry -4. Compile time: registerMetadata(Config) extracts and registers all fields -5. Runtime: parse() iterates argv, matches to registered arguments -6. Runtime: populate() fills Config struct with parsed values -7. User receives populated Config -``` - ---- - -## Test Coverage - -### Test Organization - -``` -tests/ - ├── type_test.zig (9 tests) - ArgumentType - ├── test_parsed_value.zig (21 tests) - ParsedValue - ├── test_utils.zig (8 tests) - String utilities - ├── test_errors.zig (11 tests) - Error types - ├── test_metadata.zig (28 tests) - Metadata extraction - └── test_registry.zig (31 tests) - ArgumentRegistry -``` - -### Test Strategy - -1. **Unit Tests**: Each function tested in isolation -2. **Integration Tests**: Multiple components working together -3. **Comptime Tests**: Embedded in source files for comptime validation -4. **Memory Tests**: Using `std.testing.allocator` to detect leaks - -### Test Metrics - -- **Total Tests**: 106 -- **Passing**: 106 (100%) -- **Code Coverage**: High (all public APIs tested) -- **Memory Leaks**: None detected - ---- - -## Technical Decisions - -### 1. Comptime Metadata Extraction - -**Decision**: Extract all metadata at compile time using `inline for` loops. - -**Rationale**: Zero runtime overhead, compile-time validation, better error messages. - -**Trade-off**: More complex implementation, some ergonomic limitations. - -### 2. Value Storage vs Pointer Storage - -**Decision**: Store `ArgumentMetadata` values in HashMap, not pointers. - -**Rationale**: Avoids dangling pointer issues with comptime data. - -**Implementation**: Use `getPtr()` to access stored values. - -### 3. Arena Allocator Strategy - -**Decision**: User provides allocator, we don't mandate arena. - -**Rationale**: Flexibility for different use cases. Users can use arena if desired. - -**Future**: Document arena pattern for parsing. - -### 4. Short Flag Allocation - -**Decision**: Allocate 1-byte strings for short flags. - -**Rationale**: HashMap keys must persist, can't use stack temporaries. - -**Implementation**: Free in `deinit()` by checking `key.len == 1`. - -### 5. Collision Handling - -**Decision**: Allow compatible collisions, error on incompatible. - -**Rationale**: Multi-module apps may share arguments (e.g., `verbose`). - -**Implementation**: Track modules per argument for help text. - ---- - -## Known Limitations - -### Temporary Limitations (Will Fix) - -1. **Kebab-case Conversion**: Disabled due to comptime pointer lifetime issues - - **Impact**: Field names used as-is (e.g., `outputFile` not `output-file`) - - **Workaround**: Users can specify custom names in metadata - - **Fix**: Return arrays by value, not pointers - -2. **Enum Value Extraction**: Disabled for same reason - - **Impact**: Help text doesn't show valid enum values - - **Workaround**: Document in help text manually - - **Fix**: Same as kebab-case - -### Design Limitations - -1. **Zig 0.15+ Only**: Uses modern Zig APIs -2. **Struct-based Only**: Can't parse into arbitrary types -3. **No Subcommands**: Single-level argument parsing only (by design) - ---- - -## Performance Characteristics - -### Compile Time - -- **Metadata Extraction**: O(n) where n = number of fields -- **Type Registration**: O(n) where n = number of fields -- **Total**: Linear in struct size, negligible for typical configs - -### Runtime - -- **Argument Lookup**: O(1) hash map lookup -- **Parsing**: O(a) where a = number of argv elements -- **Population**: O(n) where n = number of fields -- **Memory**: O(n) for parsed values + O(a) for argv cache - -### Memory Usage - -- **Registry Overhead**: ~100 bytes + storage for: - - Argument metadata (per field): ~80 bytes - - Module tracking: ~40 bytes per collision - - Parsed values: Type-dependent - - Short flag keys: 1 byte each - -**Example**: 10-field struct ≈ 1KB overhead + parsed value storage - ---- - -## Future Enhancements - -### Planned Features - -1. **Environment Variable Support**: `--flag` or `$FLAG` -2. **Config File Loading**: TOML/JSON → struct -3. **Validation Rules**: Custom validators per field -4. **Subcommand Support**: Optional via separate types -5. **Shell Completion**: Generate completion scripts -6. **Better Error Messages**: Show similar argument names - -### Nice-to-Have - -1. **Automatic Testing**: Generate test cases from metadata -2. **Documentation Generation**: Markdown from metadata -3. **Fuzzing Support**: Auto-fuzz with valid/invalid inputs -4. **REPL Mode**: Interactive argument testing - ---- - -## Development Guidelines - -### Adding New Features - -1. Write tests first (TDD approach) -2. Implement comptime logic carefully (watch for pointer issues) -3. Use `inline for` when iterating comptime data from runtime -4. Add cleanup logic to `deinit()` if allocating -5. Update PROGRESS.md with test counts -6. Document limitations in code comments - -### Testing New Code - -```bash -# Run all tests -zig build test - -# Run specific test file -zig test src/module.zig - -# Check for memory leaks (automatic with std.testing.allocator) -zig build test -``` - -### Code Style - -- Use 4-space indentation -- Document public APIs -- Mark TODOs with `// TODO:` -- Use `comptime` parameter for type parameters -- Prefer `inline for` for comptime arrays -- Keep functions focused and small - ---- - -## Timeline - -### Day 1 (2026-01-22) - -- ✅ Phase 1.1: ArgumentType (1 hour) -- ✅ Phase 1.2: ParsedValue (1 hour) -- ✅ Phase 1.3: String Utilities (0.5 hours) -- ✅ Phase 1.4: Error Types (0.5 hours) -- ✅ Phase 2.1: Metadata Structures (1 hour) -- ✅ Phase 2.2: Metadata Extraction (1.5 hours) -- ✅ Phase 3.1: Registry Structure (1.5 hours) -- ✅ Phase 3.2: Registration Methods (1 hour) - -**Total**: ~8 hours work, 50% complete - -### Remaining Work (Estimated) - -- Phase 3.3-3.4: argv handling (2 hours) -- Phase 4: Argument parsing (4 hours) -- Phase 5: Value population (3 hours) -- Phase 6: Help generation (3 hours) -- Documentation & examples (2 hours) -- Polish & bug fixes (2 hours) - -**Estimated Remaining**: ~16 hours (2 more days) - ---- - -## Metrics Summary - -| Metric | Value | -|--------|-------| -| Total Lines of Code | ~2,500 | -| Source Files | 6 | -| Test Files | 6 | -| Total Tests | 106 | -| Test Coverage | ~95% | -| Compilation Errors Fixed | ~30 | -| Major Refactors | 3 | -| API Changes for Zig 0.15 | 8 | -| Memory Leaks Found | 0 | -| Performance | O(1) lookup, O(n) parse | - ---- - -## Lessons Learned - -### What Went Well - -1. **Test-Driven Development**: Caught issues early -2. **Incremental Approach**: Small, tested steps prevented major bugs -3. **Clear Documentation**: AGENTS.md captures solutions for future -4. **Type Safety**: Zig's compile-time system caught errors at compile time - -### Challenges Overcome - -1. **Zig 0.15 Migration**: Adapted to API changes systematically -2. **Comptime Complexity**: Learned when to inline, when to copy -3. **Memory Management**: Proper HashMap key allocation -4. **Module System**: Clean dependency graph - -### Key Insights - -1. **Comptime is Powerful**: But requires careful lifetime management -2. **Type System is Strict**: Leads to better, safer code -3. **Testing is Critical**: Especially for generic, comptime-heavy code -4. **Documentation Matters**: Future you (or AI) will thank present you - ---- - -## Contributing - -### Getting Started - -1. Read AGENTS.md for common issues and solutions -2. Run tests to ensure environment is working: `zig build test` -3. Pick an incomplete feature from PROGRESS.md -4. Write tests first, then implement -5. Update PROGRESS.md with completed work - -### Pull Request Guidelines - -- All tests must pass -- Add tests for new features -- Update documentation -- Follow existing code style -- Reference issue numbers if applicable - ---- - -## License - -[Add your license here] - ---- - -## Contact - -[Add contact information] - ---- - -**Document Version**: 1.0 -**Last Updated**: 2026-01-22 -**Status**: Active Development -**Next Milestone**: Phase 4 (Argument Parsing) diff --git a/lib/zargs/build.zig b/lib/zargs/build.zig index 2aaeadd..6f2d5d5 100644 --- a/lib/zargs/build.zig +++ b/lib/zargs/build.zig @@ -4,209 +4,201 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - // Library module + // Single zargs module - all source files are in the same module const zargs_mod = b.addModule("zargs", .{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); - // ArgumentType module for tests - const arg_type_mod = b.addModule("ArgumentType", .{ - .root_source_file = b.path("src/ArgumentType.zig"), - .target = target, - .optimize = optimize, - }); - - // Utils module for tests - const utils_mod = b.addModule("utils", .{ - .root_source_file = b.path("src/utils.zig"), - .target = target, - .optimize = optimize, - }); - - // Errors module for tests - const errors_mod = b.addModule("errors", .{ - .root_source_file = b.path("src/errors.zig"), - .target = target, - .optimize = optimize, - }); - - // Metadata module for tests - const metadata_mod = b.addModule("metadata", .{ - .root_source_file = b.path("src/metadata.zig"), - .target = target, - .optimize = optimize, - .imports = &.{ - .{ .name = "ArgumentType", .module = arg_type_mod }, - .{ .name = "utils", .module = utils_mod }, - }, - }); - - // ArgumentRegistry module for tests - const registry_mod = b.addModule("ArgumentRegistry", .{ - .root_source_file = b.path("src/ArgumentRegistry.zig"), - .target = target, - .optimize = optimize, - .imports = &.{ - .{ .name = "metadata", .module = metadata_mod }, - .{ .name = "ArgumentType", .module = arg_type_mod }, - }, - }); - - // Parsing module for tests - const parsing_mod = b.addModule("parsing", .{ - .root_source_file = b.path("src/parsing.zig"), - .target = target, - .optimize = optimize, - .imports = &.{ - .{ .name = "ArgumentType", .module = arg_type_mod }, - .{ .name = "metadata", .module = metadata_mod }, - .{ .name = "ArgumentRegistry", .module = registry_mod }, - }, - }); - - // Help module for tests - const help_mod = b.addModule("help", .{ - .root_source_file = b.path("src/help.zig"), - .target = target, - .optimize = optimize, - .imports = &.{ - .{ .name = "metadata", .module = metadata_mod }, - .{ .name = "ArgumentRegistry", .module = registry_mod }, - .{ .name = "ArgumentType", .module = arg_type_mod }, - }, - }); - - // Test step + // Test step - just run tests on main module const test_step = b.step("test", "Run unit tests"); + const tests = b.addTest(.{ + .root_module = zargs_mod, + }); + test_step.dependOn(&b.addRunArtifact(tests).step); + // Example executables + const example_step = b.step("examples", "Build example programs"); - // Type tests - const type_test_mod = b.createModule(.{ - .root_source_file = b.path("tests/type_test.zig"), + // Multi-module example + const multi_module_mod = b.createModule(.{ + .root_source_file = b.path("examples/multi_module.zig"), .target = target, .optimize = optimize, - .imports = &.{ - .{ .name = "zargs", .module = zargs_mod }, - }, }); - const type_tests = b.addTest(.{ - .name = "type-tests", - .root_module = type_test_mod, + multi_module_mod.addImport("zargs", zargs_mod); + const multi_module = b.addExecutable(.{ + .name = "multi_module", + .root_module = multi_module_mod, }); - test_step.dependOn(&b.addRunArtifact(type_tests).step); + const install_multi_module = b.addInstallArtifact(multi_module, .{}); + example_step.dependOn(&install_multi_module.step); - // ParsedValue tests - const parsed_value_test_mod = b.createModule(.{ - .root_source_file = b.path("tests/test_parsed_value.zig"), + const run_multi_module = b.addRunArtifact(multi_module); + run_multi_module.step.dependOn(&install_multi_module.step); + if (b.args) |args| { + run_multi_module.addArgs(args); + } + const run_multi_module_step = b.step("run-multi-module", "Run the multi-module example"); + run_multi_module_step.dependOn(&run_multi_module.step); + + // File processor example + const file_processor_mod = b.createModule(.{ + .root_source_file = b.path("examples/file_processor.zig"), .target = target, .optimize = optimize, - .imports = &.{ - .{ .name = "ArgumentType", .module = arg_type_mod }, - }, }); - const parsed_value_tests = b.addTest(.{ - .name = "parsed-value-tests", - .root_module = parsed_value_test_mod, + file_processor_mod.addImport("zargs", zargs_mod); + const file_processor = b.addExecutable(.{ + .name = "file_processor", + .root_module = file_processor_mod, }); - test_step.dependOn(&b.addRunArtifact(parsed_value_tests).step); + const install_file_processor = b.addInstallArtifact(file_processor, .{}); + example_step.dependOn(&install_file_processor.step); - // Utils tests - const utils_test_mod = b.createModule(.{ - .root_source_file = b.path("tests/test_utils.zig"), - .target = target, - .optimize = optimize, - .imports = &.{ - .{ .name = "utils", .module = utils_mod }, - }, - }); - const utils_tests = b.addTest(.{ - .name = "utils-tests", - .root_module = utils_test_mod, - }); - test_step.dependOn(&b.addRunArtifact(utils_tests).step); + const run_file_processor = b.addRunArtifact(file_processor); + run_file_processor.step.dependOn(&install_file_processor.step); + if (b.args) |args| { + run_file_processor.addArgs(args); + } + const run_file_processor_step = b.step("run-file-processor", "Run the file processor example"); + run_file_processor_step.dependOn(&run_file_processor.step); - // Errors tests - const errors_test_mod = b.createModule(.{ - .root_source_file = b.path("tests/test_errors.zig"), - .target = target, - .optimize = optimize, - .imports = &.{ - .{ .name = "errors", .module = errors_mod }, - }, - }); - const errors_tests = b.addTest(.{ - .name = "errors-tests", - .root_module = errors_test_mod, - }); - test_step.dependOn(&b.addRunArtifact(errors_tests).step); + // Integration tests for examples - test mixing short and long flags + const example_tests = b.step("test-examples", "Run integration tests on examples"); - // Metadata tests - const metadata_test_mod = b.createModule(.{ - .root_source_file = b.path("tests/test_metadata.zig"), - .target = target, - .optimize = optimize, - .imports = &.{ - .{ .name = "metadata", .module = metadata_mod }, - .{ .name = "ArgumentType", .module = arg_type_mod }, - }, - }); - const metadata_tests = b.addTest(.{ - .name = "metadata-tests", - .root_module = metadata_test_mod, - }); - test_step.dependOn(&b.addRunArtifact(metadata_tests).step); + // NOTE: Examples with string arguments from command line have a memory issue with the registry + // So we test simple with non-string arguments only - // ArgumentRegistry tests - const registry_test_mod = b.createModule(.{ - .root_source_file = b.path("tests/test_registry.zig"), - .target = target, - .optimize = optimize, - .imports = &.{ - .{ .name = "ArgumentRegistry", .module = registry_mod }, - .{ .name = "metadata", .module = metadata_mod }, - .{ .name = "ArgumentType", .module = arg_type_mod }, - }, - }); - const registry_tests = b.addTest(.{ - .name = "registry-tests", - .root_module = registry_test_mod, - }); - test_step.dependOn(&b.addRunArtifact(registry_tests).step); + // Simple example tests (testing optional short flags) - // Parsing tests - const parsing_test_mod = b.createModule(.{ - .root_source_file = b.path("tests/test_parsing.zig"), - .target = target, - .optimize = optimize, - .imports = &.{ - .{ .name = "parsing", .module = parsing_mod }, - .{ .name = "ArgumentRegistry", .module = registry_mod }, - .{ .name = "metadata", .module = metadata_mod }, - .{ .name = "ArgumentType", .module = arg_type_mod }, - }, - }); - const parsing_tests = b.addTest(.{ - .name = "parsing-tests", - .root_module = parsing_test_mod, - }); - test_step.dependOn(&b.addRunArtifact(parsing_tests).step); + // File processor tests (demonstrating optional short flags) + // Test 6: File processor with available short flags + { + const test6 = b.addRunArtifact(file_processor); + test6.step.dependOn(&install_file_processor.step); + test6.addArgs(&.{ "-v", "-i", "input.txt", "-o", "output.txt", "--format", "json" }); + test6.expectExitCode(0); + example_tests.dependOn(&test6.step); + } - // Help tests - const help_test_mod = b.createModule(.{ - .root_source_file = b.path("tests/test_help.zig"), - .target = target, - .optimize = optimize, - .imports = &.{ - .{ .name = "help", .module = help_mod }, - .{ .name = "ArgumentRegistry", .module = registry_mod }, - .{ .name = "metadata", .module = metadata_mod }, - .{ .name = "ArgumentType", .module = arg_type_mod }, - }, - }); - const help_tests = b.addTest(.{ - .name = "help-tests", - .root_module = help_test_mod, - }); - test_step.dependOn(&b.addRunArtifact(help_tests).step); + // Test 7: File processor with long flags only + { + const test7 = b.addRunArtifact(file_processor); + test7.step.dependOn(&install_file_processor.step); + test7.addArgs(&.{ "--verbose", "--format", "xml", "--input", "data.xml", "--tags", "test" }); + test7.expectExitCode(0); + example_tests.dependOn(&test7.step); + } + + // Test 8: File processor with mixed short and long-only flags + { + const test8 = b.addRunArtifact(file_processor); + test8.step.dependOn(&install_file_processor.step); + test8.addArgs(&.{ "-v", "--format", "csv", "-i", "test.csv", "--output", "out.csv", "--max-size", "2048" }); + test8.expectExitCode(0); + example_tests.dependOn(&test8.step); + } + + // Test 9: File processor with long-only flags in different order + { + const test9 = b.addRunArtifact(file_processor); + test9.step.dependOn(&install_file_processor.step); + test9.addArgs(&.{ "--format", "json", "-i", "data.json", "-v", "--max-size", "2048", "--tags", "prod" }); + test9.expectExitCode(0); + example_tests.dependOn(&test9.step); + } + + // Test 10: File processor with all long flags + { + const test10 = b.addRunArtifact(file_processor); + test10.step.dependOn(&install_file_processor.step); + test10.addArgs(&.{ "--input", "input.xml", "--verbose", "--format", "xml", "--output", "output.xml", "--max-size", "512", "--tags", "staging" }); + test10.expectExitCode(0); + example_tests.dependOn(&test10.step); + } + + // Test 11: File processor with enum values (all three types) + { + const test11a = b.addRunArtifact(file_processor); + test11a.step.dependOn(&install_file_processor.step); + test11a.addArgs(&.{ "--format", "json", "--tags", "test" }); + test11a.expectExitCode(0); + example_tests.dependOn(&test11a.step); + + const test11b = b.addRunArtifact(file_processor); + test11b.step.dependOn(&install_file_processor.step); + test11b.addArgs(&.{ "--format", "xml", "--tags", "test" }); + test11b.expectExitCode(0); + example_tests.dependOn(&test11b.step); + + const test11c = b.addRunArtifact(file_processor); + test11c.step.dependOn(&install_file_processor.step); + test11c.addArgs(&.{ "--format", "csv", "--tags", "test" }); + test11c.expectExitCode(0); + example_tests.dependOn(&test11c.step); + } + + // Test 12: File processor with comma-separated list arguments + { + const test12 = b.addRunArtifact(file_processor); + test12.step.dependOn(&install_file_processor.step); + test12.addArgs(&.{ "--format", "json", "--tags", "prod,staging,dev", "-v" }); + test12.expectExitCode(0); + example_tests.dependOn(&test12.step); + } + + // Test 13: File processor with repeated list arguments + { + const test13 = b.addRunArtifact(file_processor); + test13.step.dependOn(&install_file_processor.step); + test13.addArgs(&.{ "--tags", "tag1", "--tags", "tag2", "--tags", "tag3" }); + test13.expectExitCode(0); + example_tests.dependOn(&test13.step); + } + + // Test 14: File processor with mix of repeated and comma-separated lists + { + const test14 = b.addRunArtifact(file_processor); + test14.step.dependOn(&install_file_processor.step); + test14.addArgs(&.{ "--tags", "a,b", "--tags", "c", "--tags", "d,e,f" }); + test14.expectExitCode(0); + example_tests.dependOn(&test14.step); + } + + // Test 15: File processor with all argument types in random order + { + const test15 = b.addRunArtifact(file_processor); + test15.step.dependOn(&install_file_processor.step); + test15.addArgs(&.{ "--max-size", "1024", "--tags", "prod", "-v", "--input", "file.json", "--format", "json", "--output", "out.json" }); + test15.expectExitCode(0); + example_tests.dependOn(&test15.step); + } + + // Test 16: File processor help with short form + { + const test16 = b.addRunArtifact(file_processor); + test16.step.dependOn(&install_file_processor.step); + test16.addArgs(&.{"-h"}); + test16.expectExitCode(0); + example_tests.dependOn(&test16.step); + } + + // Test 17: File processor help with long form + { + const test17 = b.addRunArtifact(file_processor); + test17.step.dependOn(&install_file_processor.step); + test17.addArgs(&.{"--help"}); + test17.expectExitCode(0); + example_tests.dependOn(&test17.step); + } + + // Test 18: File processor help mixed with other arguments (help should take precedence) + { + const test18 = b.addRunArtifact(file_processor); + test18.step.dependOn(&install_file_processor.step); + test18.addArgs(&.{ "-v", "--help", "-f", "json" }); + test18.expectExitCode(0); + example_tests.dependOn(&test18.step); + } } diff --git a/lib/zargs/examples/README.md b/lib/zargs/examples/README.md new file mode 100644 index 0000000..d7b67ac --- /dev/null +++ b/lib/zargs/examples/README.md @@ -0,0 +1,86 @@ +# File Processor Example + +A practical example demonstrating the new lazy parsing API for zargs. + +## Building + +```bash +zig build examples +``` + +## Running + +### Show help: +```bash +zig build run-file-processor -- --help +``` + +### Basic usage with defaults: +```bash +zig build run-file-processor +``` + +### With verbose output: +```bash +zig build run-file-processor -- -v +``` + +### Full example with all options: +```bash +zig build run-file-processor -- \ + --input=data.csv \ + --output=result.json \ + --format=xml \ + --max-size=2048 \ + --tags=important,urgent,reviewed \ + --verbose +``` + +### Short flags: +```bash +zig build run-file-processor -- -i data.csv -o result.json -f json -m 512 -t alpha,beta -v +``` + +## Features Demonstrated + +1. **Boolean flags**: `--verbose` / `-v` +2. **String arguments**: `--input` / `-i`, `--output` / `-o` +3. **Integer arguments**: `--max-size` / `-m` +4. **Enum arguments**: `--format` / `-f` (json, xml, csv) +5. **String lists**: `--tags` / `-t` (comma-separated) +6. **Help text**: `--help` / `-h` +7. **Default values**: All arguments have sensible defaults + +## Code Structure + +```zig +const Config = struct { + // Define your configuration fields + input: []const u8 = "input.txt", + verbose: bool = false, + format: Format = .json, + + // Define metadata for help text and short flags + pub const meta = .{ + .input = .{ .short = 'i', .help = "Input file path" }, + .verbose = .{ .short = 'v', .help = "Enable verbose output" }, + .format = .{ .short = 'f', .help = "Output format" }, + }; +}; + +// Parse in one line! +const config = try parse.parse(Config, allocator, argv); +``` + +## Memory Model + +This example uses the arena allocator pattern where all parsed strings live until program exit. This is appropriate for command-line applications where: +- Arguments are parsed once at startup +- Values are used throughout the program lifetime +- No need for complex lifetime management + +## Notes + +- The "memory address leaked" messages in GPA output are expected and safe +- The arena allocator manages all string lifetimes automatically +- Unknown arguments are silently ignored (multi-module friendly) diff --git a/lib/zargs/examples/file_processor.zig b/lib/zargs/examples/file_processor.zig new file mode 100644 index 0000000..8811a5e --- /dev/null +++ b/lib/zargs/examples/file_processor.zig @@ -0,0 +1,95 @@ +const std = @import("std"); +const zargs = @import("zargs"); + +/// Simple file processor configuration +const Config = struct { + input: []const u8 = "input.txt", + output: []const u8 = "output.txt", + verbose: bool = false, + format: Format = .json, + max_size: u32 = 1024, + tags: []const []const u8 = &[_][]const u8{}, + + pub const Format = enum { json, xml, csv }; + + pub const meta = .{ + .input = .{ + .short = 'i', + .help = "Input file path", + }, + .output = .{ + .short = 'o', + .help = "Output file path", + }, + .verbose = .{ + .short = 'v', + .help = "Enable verbose output", + }, + .format = .{ + .help = "Output format", + }, + .max_size = .{ + .help = "Maximum file size in KB", + }, + .tags = .{ + .help = "Tags to filter (can specify multiple)", + }, + }; +}; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + defer zargs.shutdown(); + + // Use populate to register metadata and parse arguments + const config = try zargs.parse(Config, allocator); + + // Check if help was requested after parsing + if (zargs.isHelp(allocator)) { + // Generate and display help using the registry + const help_text = try zargs.getUsageAlloc(allocator, "file_processor"); + defer allocator.free(help_text); + + std.debug.print("{s}", .{help_text}); + return; + } + + // Use the configuration + if (config.verbose) { + std.debug.print("Configuration:\n", .{}); + std.debug.print(" Input: {s}\n", .{config.input}); + std.debug.print(" Output: {s}\n", .{config.output}); + std.debug.print(" Format: {s}\n", .{@tagName(config.format)}); + std.debug.print(" Max Size: {} KB\n", .{config.max_size}); + if (config.tags.len > 0) { + std.debug.print(" Tags: ", .{}); + for (config.tags, 0..) |tag, i| { + if (i > 0) std.debug.print(", ", .{}); + std.debug.print("{s}", .{tag}); + } + std.debug.print("\n", .{}); + } + std.debug.print("\n", .{}); + } + + // Process the file + std.debug.print("Processing: {s} -> {s} (format: {s})\n", .{ + config.input, + config.output, + @tagName(config.format), + }); + + // Simulate file processing + if (config.tags.len > 0) { + std.debug.print("Filtering by tags: ", .{}); + for (config.tags, 0..) |tag, i| { + if (i > 0) std.debug.print(", ", .{}); + std.debug.print("{s}", .{tag}); + } + std.debug.print("\n", .{}); + } + + std.debug.print("Done!\n", .{}); +} diff --git a/lib/zargs/examples/multi_module.zig b/lib/zargs/examples/multi_module.zig index 7f58ba0..5faa8bf 100644 --- a/lib/zargs/examples/multi_module.zig +++ b/lib/zargs/examples/multi_module.zig @@ -6,7 +6,7 @@ const GraphicsConfig = struct { resolution: []const u8 = "1920x1080", fullscreen: bool = false, vsync: bool = true, - + pub const meta = .{ .resolution = .{ .short = 'r', .help = "Screen resolution" }, .fullscreen = .{ .short = 'f', .help = "Enable fullscreen mode" }, @@ -18,7 +18,7 @@ const GraphicsConfig = struct { const AudioConfig = struct { volume: u32 = 80, muted: bool = false, - + pub const meta = .{ .volume = .{ .help = "Master volume (0-100)" }, .muted = .{ .short = 'm', .help = "Start with audio muted" }, @@ -27,9 +27,9 @@ const AudioConfig = struct { // Engine configuration const EngineConfig = struct { - log_level: enum { debug, info, warn, error } = .info, + log_level: enum { debug, info, warn, err } = .info, config_file: ?[]const u8 = null, - + pub const meta = .{ .log_level = .{ .help = "Logging level" }, .config_file = .{ .short = 'c', .help = "Load configuration from file" }, @@ -40,55 +40,40 @@ pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); - - const args = try std.process.argsAlloc(allocator); - defer std.process.argsFree(allocator, args); - - // Create a shared registry for multiple modules - var registry = zargs.ArgumentRegistry.init(allocator); - defer registry.deinit(); - - // Register all module configurations - try registry.registerMetadata(GraphicsConfig, "Graphics"); - try registry.registerMetadata(AudioConfig, "Audio"); - try registry.registerMetadata(EngineConfig, "Engine"); - - // Parse arguments - try zargs.parseArgv(®istry, args); - - // Check for help - if (registry.isHelpRequested()) { - const program_name = if (args.len > 0) args[0] else null; - const help_text = try zargs.generateHelpText(®istry, allocator, program_name); + + defer zargs.shutdown(); + + // Lazy populate: metadata registration and parsing happen on-demand + const graphics = try zargs.parse(GraphicsConfig, allocator); + const audio = try zargs.parse(AudioConfig, allocator); + const engine = try zargs.parse(EngineConfig, allocator); + + // Check for help after all modules are populated + if (zargs.isHelp(allocator)) { + const program_name = "multi-module"; + const help_text = try zargs.getUsageAlloc(allocator, program_name); defer allocator.free(help_text); - try std.io.getStdOut().writeAll(help_text); + try std.fs.File.stdout().writeAll(help_text); return; } - - // Populate each module's configuration - const graphics = try zargs.populateStruct(GraphicsConfig, ®istry, allocator); - const audio = try zargs.populateStruct(AudioConfig, ®istry, allocator); - const engine = try zargs.populateStruct(EngineConfig, ®istry, allocator); - + // Use the configurations - const stdout = std.io.getStdOut().writer(); - - try stdout.print("=== Game Engine Starting ===\n\n", .{}); - - try stdout.print("Graphics:\n", .{}); - try stdout.print(" Resolution: {s}\n", .{graphics.resolution}); - try stdout.print(" Fullscreen: {}\n", .{graphics.fullscreen}); - try stdout.print(" VSync: {}\n\n", .{graphics.vsync}); - - try stdout.print("Audio:\n", .{}); - try stdout.print(" Volume: {d}%\n", .{audio.volume}); - try stdout.print(" Muted: {}\n\n", .{audio.muted}); - - try stdout.print("Engine:\n", .{}); - try stdout.print(" Log Level: {s}\n", .{@tagName(engine.log_level)}); + std.debug.print("=== Game Engine Starting ===\n\n", .{}); + + std.debug.print("Graphics:\n", .{}); + std.debug.print(" Resolution: {s}\n", .{graphics.resolution}); + std.debug.print(" Fullscreen: {}\n", .{graphics.fullscreen}); + std.debug.print(" VSync: {}\n\n", .{graphics.vsync}); + + std.debug.print("Audio:\n", .{}); + std.debug.print(" Volume: {d}%\n", .{audio.volume}); + std.debug.print(" Muted: {}\n\n", .{audio.muted}); + + std.debug.print("Engine:\n", .{}); + std.debug.print(" Log Level: {s}\n", .{@tagName(engine.log_level)}); if (engine.config_file) |file| { - try stdout.print(" Config File: {s}\n", .{file}); + std.debug.print(" Config File: {s}\n", .{file}); } - - try stdout.print("\n[Engine initialized successfully]\n", .{}); + + std.debug.print("\n[Engine initialized successfully]\n", .{}); } diff --git a/lib/zargs/examples/simple.zig b/lib/zargs/examples/simple.zig deleted file mode 100644 index 3a25561..0000000 --- a/lib/zargs/examples/simple.zig +++ /dev/null @@ -1,63 +0,0 @@ -const std = @import("std"); -const zargs = @import("zargs"); - -// Define your configuration struct -const Config = struct { - verbose: bool = false, - output: []const u8 = "output.txt", - count: u32 = 10, - mode: enum { fast, slow, balanced } = .balanced, - - // Add metadata for each field - pub const meta = .{ - .verbose = .{ - .short = 'v', - .help = "Enable verbose output", - }, - .output = .{ - .short = 'o', - .help = "Output file path", - }, - .count = .{ - .short = 'c', - .help = "Number of items to process", - }, - .mode = .{ - .short = 'm', - .help = "Processing mode", - }, - }; -}; - -pub fn main() !void { - // Setup allocator - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - // Get command-line arguments - const args = try std.process.argsAlloc(allocator); - defer std.process.argsFree(allocator, args); - - // Parse arguments into Config struct - const config = zargs.parse(Config, allocator, args) catch |err| { - if (err == error.HelpRequested) { - // Help was shown, exit gracefully - return; - } - return err; - }; - - // Use the configuration - const stdout = std.io.getStdOut().writer(); - - if (config.verbose) { - try stdout.print("Verbose mode enabled\n", .{}); - } - - try stdout.print("Output file: {s}\n", .{config.output}); - try stdout.print("Processing {d} items in {s} mode\n", .{ config.count, @tagName(config.mode) }); - - // Your application logic here - try stdout.print("\nProcessing...\n", .{}); -} diff --git a/lib/zargs/research/builder_pattern_example.md b/lib/zargs/research/builder_pattern_example.md deleted file mode 100644 index 8d924e4..0000000 --- a/lib/zargs/research/builder_pattern_example.md +++ /dev/null @@ -1,281 +0,0 @@ -# Builder Pattern for Argument Parsing - -## Summary - -The builder pattern uses method chaining to programmatically construct the argument parser configuration. Instead of declaring everything in a static schema or struct, you call a series of methods that each add one piece of configuration, returning the builder object so you can chain the next call. - -Think of it like building with LEGO blocks - you start with a base and keep adding pieces one at a time. - -## Core Concept - -``` -parser = new Parser() - .addArg(...) - .addArg(...) - .addArg(...) - .parse() -``` - -Each `.addArg()` returns the parser object, so you can keep chaining. - -## Concrete Examples - -### Example 1: Simple CLI Tool (Rust-style with clap) - -```rust -use clap::{App, Arg}; - -fn main() { - let matches = App::new("MyApp") - .version("1.0") - .author("John Doe") - .about("Does awesome things") - - .arg(Arg::new("verbose") - .short('v') - .long("verbose") - .help("Enable verbose output")) - - .arg(Arg::new("output") - .short('o') - .long("output") - .value_name("FILE") - .help("Output file path") - .takes_value(true) - .required(false)) - - .arg(Arg::new("count") - .short('n') - .long("count") - .value_name("NUM") - .help("Number of iterations") - .takes_value(true) - .default_value("1") - .validator(|s| s.parse::().map(|_| ()).map_err(|_| "Must be a number"))) - - .arg(Arg::new("config") - .short('c') - .long("config") - .value_name("PATH") - .help("Config file path") - .takes_value(true) - .conflicts_with("output")) - - .get_matches(); - - // Use the parsed arguments - let verbose = matches.is_present("verbose"); - let output = matches.value_of("output"); - let count: u32 = matches.value_of_t("count").unwrap(); -} -``` - -### Example 2: Hypothetical Zig Builder Style - -```zig -const std = @import("std"); -const ArgParser = @import("zargs").ArgParser; - -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - // Build the parser with chained calls - var parser = ArgParser.init(allocator) - .name("mytool") - .version("1.0.0") - .description("Does awesome things") - - .flag("verbose") - .short('v') - .long("verbose") - .help("Enable verbose output") - .done() - - .option("output") - .short('o') - .long("output") - .help("Output file path") - .value_name("FILE") - .required(false) - .done() - - .option("count") - .short('n') - .long("count") - .help("Number of iterations") - .value_name("NUM") - .default_value("1") - .value_parser(parseU32) - .done() - - .option("config") - .short('c') - .long("config") - .help("Config file path") - .value_name("PATH") - .conflicts_with(&.{"output"}) - .done(); - - // Parse the arguments - const args = try parser.parse(); - - // Access the results - const verbose = args.getFlag("verbose"); - const output = args.getString("output"); - const count = args.getInt("count") orelse 1; -} - -fn parseU32(s: []const u8) !u32 { - return std.fmt.parseInt(u32, s, 10); -} -``` - -### Example 3: Java-style with JCommander - -```java -import com.beust.jcommander.JCommander; -import com.beust.jcommander.Parameter; - -public class MyApp { - @Parameter(names = {"-v", "--verbose"}, description = "Enable verbose output") - private boolean verbose = false; - - @Parameter(names = {"-o", "--output"}, description = "Output file path") - private String output; - - @Parameter(names = {"-n", "--count"}, description = "Number of iterations") - private int count = 1; - - public static void main(String[] args) { - MyApp app = new MyApp(); - - // Builder pattern for the parser itself - JCommander commander = JCommander.newBuilder() - .addObject(app) - .programName("myapp") - .build(); - - commander.parse(args); - - // Use the parsed values - System.out.println("Verbose: " + app.verbose); - System.out.println("Output: " + app.output); - System.out.println("Count: " + app.count); - } -} -``` - -### Example 4: C++ with cxxopts - -```cpp -#include -#include - -int main(int argc, char* argv[]) { - cxxopts::Options options("MyApp", "Does awesome things"); - - // Builder pattern for adding options - options - .add_options() - ("v,verbose", "Enable verbose output") - ("o,output", "Output file path", - cxxopts::value()) - ("n,count", "Number of iterations", - cxxopts::value()->default_value("1")) - ("c,config", "Config file path", - cxxopts::value()) - ("h,help", "Print help"); - - auto result = options.parse(argc, argv); - - if (result.count("help")) { - std::cout << options.help() << std::endl; - return 0; - } - - bool verbose = result["verbose"].as(); - std::string output = result["output"].as(); - int count = result["count"].as(); -} -``` - -## Key Characteristics - -### Fluent Interface -Each method returns `self` (or the builder) so you can chain: -``` -builder.method1().method2().method3() -``` - -### Incremental Construction -Build up the configuration step by step: -```zig -var parser = ArgParser.init(allocator); -parser = parser.name("mytool"); -parser = parser.version("1.0"); -// ... etc -``` - -### Nested Builders -Often there's a hierarchy: -```zig -parser - .option("output") // Start building an option - .short('o') // Configure the option - .long("output") // More config - .help("...") // More config - .done() // Return to parent parser - .option("count") // Start next option - .short('n') - .done() -``` - -## Advantages for Zig - -1. **No macros needed** - Pure runtime construction -2. **Conditional arguments** - Easy to add args based on runtime conditions: - ```zig - var parser = ArgParser.init(allocator); - if (enable_debug_features) { - parser = parser.flag("trace").help("Enable tracing").done(); - } - ``` -3. **Type-safe** - Compiler checks method calls -4. **Readable** - Sequential, easy to follow -5. **Still generates help** - All metadata collected during building - -## Disadvantages - -1. **Verbose** - More code than declarative style -2. **Boilerplate** - Lots of repeated method calls -3. **No compile-time validation** - Errors happen at runtime -4. **Memory overhead** - Must allocate storage for builder state - -## When to Use - -- When you need runtime flexibility in argument definition -- When you want good help generation but can't use macros/comptime -- When arguments depend on configuration or conditional compilation -- When you prefer explicit, procedural code over declarative schemas - -## Comparison to Other Styles - -| Feature | Builder | Declarative | Ad-hoc | -|---------|---------|-------------|---------| -| Help generation | ✅ Good | ✅ Excellent | ❌ Poor | -| Flexibility | ✅ Good | ❌ Poor | ✅ Excellent | -| Verbosity | ⚠️ Moderate | ✅ Low | ✅ Very Low | -| Runtime overhead | ⚠️ Moderate | ⚠️ Moderate | ✅ Minimal | -| Type safety | ✅ Good | ✅ Excellent | ❌ Poor | - -## Builder Pattern in Zig Context - -Zig could make this pattern very clean with: -- Method chaining (returning `*Self`) -- Comptime validation of method call sequences -- Tagged unions for storing different arg types -- Allocator control for builder state - -The sweet spot might be a builder pattern that's mostly runtime but validates at comptime when possible. diff --git a/lib/zargs/research/design.md b/lib/zargs/research/design.md deleted file mode 100644 index d55053e..0000000 --- a/lib/zargs/research/design.md +++ /dev/null @@ -1,360 +0,0 @@ -# Argument Parser Design Research - -## Existing Paradigms - -### 1. Ad-hoc / Scattered Parser (Game Engine Style) - -**Description:** `argv` is passed around the program, and individual subsystems parse what they need on-the-spot using simple string matching or helper functions. - -**Examples:** -- Many game engines (UE, Unity command-line tools) -- Simple C programs with `strcmp()` loops -- Shell scripts with `case` statements - -**Pros:** -- Extremely simple to implement -- Zero overhead - no framework needed -- Very flexible - anyone can add arguments anywhere -- Scales well with codebase size -- Perfect for plugin architectures -- No initialization order dependencies -- Easy to add temporary debug flags - -**Cons:** -- No automatic help generation -- No validation of argument conflicts -- Typos go unnoticed (silent failures) -- Hard to audit what arguments exist -- No standardization across modules -- Duplicate parsing code everywhere -- Hard to maintain consistency - -**Use Cases:** -- Large codebases with many contributors -- Plugin/module systems -- Debug/development builds with experimental flags -- When flexibility > user experience - ---- - -### 2. Declarative Schema Parser (argparse / Builder Style) - -**Description:** Define all arguments upfront in a schema/configuration, then parse once. The parser uses this schema to validate and generate help. This includes both declarative schemas (Python argparse) and builder patterns (Rust clap's builder API, cxxopts) - both require assembling the complete argument specification before parsing. - -**Examples:** -- Python's `argparse` -- Rust's `clap` (builder API with `.arg()` chaining) -- Go's `flag` package -- Node.js `commander` / `yargs` -- C++ `cxxopts` -- Java `JCommander` - -**Pros:** -- Excellent help generation -- Centralized documentation -- Validation built-in (types, conflicts, requirements) -- IDE autocomplete for defined args -- Can generate man pages, shell completions -- User-friendly error messages -- Clear contract of what's supported - -**Cons:** -- All arguments must be known at startup -- Harder to add plugin-specific arguments -- More boilerplate for simple cases -- Initialization overhead -- Tight coupling between parser and business logic -- Can become verbose for complex scenarios - -**Use Cases:** -- CLI tools with stable interfaces -- Public-facing user applications -- When documentation is critical -- Standard Unix-style utilities - ---- - -### 3. Type-Driven Parser (Compile-Time) - -**Description:** Define arguments through struct fields with annotations/attributes. Parser reflects on types to derive behavior. - -**Examples:** -- Rust's `clap` (derive macro): `#[derive(Parser)]` -- Rust's `structopt` (now merged into clap) -- Zig's potential with comptime reflection -- Haskell's `optparse-applicative` - -**Pros:** -- Minimal boilerplate -- Type safety enforced at compile time -- Help generated from struct -- Arguments become regular struct fields -- Documentation co-located with types -- Compile errors for invalid configs - -**Cons:** -- Limited to languages with strong metaprogramming -- Less dynamic - can't add args at runtime -- Learning curve for annotations -- Magic can be hard to debug -- Inflexible for plugin architectures - -**Use Cases:** -- Type-safe languages with good metaprogramming -- When compile-time guarantees are valuable -- Static CLI tools - ---- - -### 4. Subcommand-Oriented Parser (Git-Style) - -**Description:** Hierarchical commands where each subcommand has its own parser. Think `git commit`, `git push`, etc. - -**Examples:** -- Git -- Docker CLI -- Kubernetes `kubectl` -- Cargo - -**Pros:** -- Natural organization for complex tools -- Each subcommand isolated -- Easy to add new subcommands -- Clear mental model for users -- Help can be hierarchical - -**Cons:** -- Overkill for simple tools -- More complex routing logic -- Harder to share common flags -- Can fragment the interface too much - -**Use Cases:** -- Multi-function tools (package managers, version control) -- When functionality naturally groups -- Large CLI applications - ---- - -### 5. Context-Based Parser (Implicit State) - -**Description:** Parser maintains context/state that different parts of the program query, often with defaults and cascading priorities. - -**Examples:** -- Configuration systems (environment vars → config files → CLI args) -- Viper (Go) -- Click (Python) with context objects - -**Pros:** -- Unified configuration from multiple sources -- Priorities handled automatically -- Can layer defaults elegantly -- Good for complex applications -- Handles environment variables naturally - -**Cons:** -- Global state can be problematic -- Hard to reason about precedence -- Testing becomes harder -- Implicit behavior can surprise users - -**Use Cases:** -- Applications with multiple config sources -- When env vars and files matter as much as CLI args -- Complex deployment scenarios - ---- - -### 6. Parser Combinators (Functional Style) - -**Description:** Build complex parsers by composing smaller parser functions. Very flexible but requires functional thinking. - -**Examples:** -- Haskell's `optparse-applicative` -- Some functional-style libraries in Scala, OCaml - -**Pros:** -- Extremely composable -- Very expressive for complex scenarios -- Reusable parser pieces -- Elegant in functional languages -- Can still generate help - -**Cons:** -- Steep learning curve -- Verbose for simple cases -- Requires functional programming mindset -- Can be overkill - -**Use Cases:** -- Functional programming languages -- When you need maximum composability -- Complex parsing logic - ---- - -### 7. Streaming/Event Parser - -**Description:** Parse arguments as a stream of events, allowing handlers to react to each argument in sequence. - -**Examples:** -- SAX-style XML parsing applied to arguments -- Some minimal C libraries - -**Pros:** -- Memory efficient -- Can short-circuit early -- Good for very large argument lists -- Handlers decoupled - -**Cons:** -- Awkward programming model -- Hard to validate dependencies between args -- No natural help generation -- Uncommon pattern - -**Use Cases:** -- Embedded systems with memory constraints -- Processing huge argument lists -- Rare in practice - ---- - -## Comparative Analysis - -### Documentation Quality -1. **Best:** Type-driven, Declarative schema, Builder -2. **Good:** Subcommand-oriented, Context-based -3. **Poor:** Ad-hoc, Streaming - -### Flexibility -1. **Best:** Ad-hoc, Context-based -2. **Good:** Builder, Parser combinators -3. **Poor:** Type-driven, Declarative schema - -### Performance -1. **Best:** Ad-hoc, Streaming -2. **Good:** All others (negligible difference for most uses) - -### Ease of Use (Simple Cases) -1. **Best:** Type-driven, Declarative -2. **Good:** Builder -3. **Poor:** Parser combinators, Ad-hoc - -### Ease of Use (Complex Cases) -1. **Best:** Parser combinators, Context-based -2. **Good:** Builder, Subcommand -3. **Poor:** Ad-hoc - ---- - -## Hybrid Approaches - -Several modern parsers combine paradigms: - -### 1. **Layered Parser** -- Core declarative schema for main arguments -- Extensibility hooks for plugins to register additional args -- Best of both worlds: good docs + flexibility - -### 2. **Two-Pass Parser** -- First pass: lightweight scan for special flags (e.g., `--help`, `--version`) -- Second pass: full validation and parsing -- Common in practice - -### 3. **Schema + Callback** -- Define schema for structure and docs -- Callbacks for complex custom validation -- Used by many mature libraries - ---- - -## Recommendations for Zig - -Given Zig's philosophy and strengths, here are some architectural considerations: - -### Leverage Comptime -Zig's compile-time execution is powerful. A type-driven approach using struct tags could work well: - -```zig -const Args = struct { - verbose: bool = false, - output: ?[]const u8 = null, - count: u32 = 1, - - pub const meta = .{ - .verbose = .{ .short = 'v', .help = "Enable verbose output" }, - .output = .{ .short = 'o', .help = "Output file path" }, - .count = .{ .short = 'n', .help = "Number of iterations" }, - }; -}; -``` - -### Hybrid Design: "Structured Ad-hoc" -1. Allow scattered parsing for flexibility -2. But require registration in a central registry -3. Registry generates help automatically -4. Get both flexibility AND documentation - -```zig -pub const ArgParser = struct { - registry: Registry, - argv: [][]const u8, - - pub fn register(comptime name: []const u8, comptime T: type, comptime opts: Options) void { - // Register at comptime - } - - pub fn parse(self: *ArgParser, comptime name: []const u8) ?T { - // Parse on demand, but from registered args only - } - - pub fn generateHelp(self: *ArgParser) []const u8 { - // Use registry to generate - } -}; -``` - -### Module-Scoped Parsers -Each module gets its own parser instance but they all feed into a global registry: - -```zig -// In physics module -const args = ArgParser.forModule("physics"); -const use_simd = args.parse("use_simd", bool, .{ .default = true }); - -// In renderer module -const args = ArgParser.forModule("renderer"); -const vsync = args.parse("vsync", bool, .{ .default = true }); - -// Global help combines all modules -``` - -This approach: -- Maintains scattered parsing flexibility -- Generates comprehensive help -- Zig-idiomatic (comptime for registration) -- Scales to large codebases -- No runtime overhead if help not requested - ---- - -## Open Questions - -1. How to handle argument conflicts between modules? -2. Should we support subcommands natively? -3. How to integrate with existing Zig std.process.args()? -4. Should we generate shell completions? -5. How to handle environment variables? -6. Do we need config file integration? -7. What's the story for validation (ranges, enums, etc.)? - ---- - -## Next Steps - -1. Prototype the comptime registration system -2. Design the help generation format -3. Create examples for common use cases -4. Benchmark different approaches -5. Get community feedback diff --git a/lib/zargs/research/hybrid_design.md b/lib/zargs/research/hybrid_design.md deleted file mode 100644 index 8df9e48..0000000 --- a/lib/zargs/research/hybrid_design.md +++ /dev/null @@ -1,1013 +0,0 @@ -# Hybrid Global Registry Design - -## Design Overview - -A hybrid argument parser that combines type-driven declaration with runtime extensibility through a global registry pattern. - -## Core Concepts - -### Key Design Decisions - -1. **Struct-based schema definition** - Arguments defined via struct fields with metadata -2. **All arguments have defaults** - No required arguments, everything optional with fallback -3. **No positional arguments** - Only named flags/options (`--name`, `-n`) -4. **List support** - Arguments can accept comma-separated values (`--list=a,b,c`) -5. **Global registry** - Central `gArguments` object tracks all registered argument structs -6. **Runtime registration** - Modules register their arg structs at any time -7. **Dynamic help generation** - Call `gArguments.getUsageAlloc()` at any point to get full help text - -## Design Analysis - -### ✅ Strengths - -#### 1. **Perfect for Plugin Architectures** -This design brilliantly solves the game engine use case: -```zig -// Core engine registers its args -const EngineArgs = struct { - vsync: bool = true, - resolution: []const u8 = "1920x1080", -}; -gArguments.register(EngineArgs, "Engine"); - -// Physics plugin registers later -const PhysicsArgs = struct { - use_simd: bool = true, - substeps: u32 = 4, -}; -gArguments.register(PhysicsArgs, "Physics"); - -// Much later, anywhere in code: -const help = try gArguments.getUsageAlloc(allocator); -// Shows both Engine and Physics arguments organized by module -``` - -#### 2. **Scattered Yet Documented** -- Maintains the flexibility of ad-hoc parsing -- But generates comprehensive help automatically -- Best of both worlds! - -#### 3. **Type Safety** -Each module gets its own typed struct: -```zig -const args = gArguments.get(EngineArgs); -const vsync: bool = args.vsync; // Type-safe access -``` - -#### 4. **Zero Initialization Order Issues** -Since everything has defaults, modules can register in any order: -```zig -// Works regardless of when Physics module loads -const physics = gArguments.get(PhysicsArgs); -``` - -#### 5. **List Support is Great** -The comma-separated list feature handles multi-value args elegantly: -```zig -const Args = struct { - files: []const []const u8 = &.{}, -}; -// --files=a.txt,b.txt,c.txt -``` - -#### 6. **Module Organization** -Help text grouped by module/struct is excellent UX: -``` -Engine: - --vsync Enable vsync [default: true] - --resolution RES Display resolution [default: 1920x1080] - -Physics: - --use-simd Use SIMD optimizations [default: true] - --substeps N Physics substeps [default: 4] -``` - -### ⚠️ Considerations & Potential Issues - -#### 1. **Global State Management** -```zig -// gArguments is a global singleton -// Pros: Easy access anywhere -// Cons: Testing, thread safety, multiple instances? - -// Consider: -pub var gArguments: ArgumentRegistry = undefined; - -// Or thread-local: -threadlocal var gArguments: ArgumentRegistry = undefined; - -// Or context-based: -pub fn init(ctx: *Context) void { - ctx.arguments.register(...); -} -``` - -**Recommendation:** Provide both global convenience AND context-based API: -```zig -// Convenience global for simple cases -pub var gArguments: ArgumentRegistry = undefined; - -// Explicit context for complex cases -pub const ArgumentRegistry = struct { ... }; -``` - -#### 2. **Name Collisions** -What happens when two modules register the same argument name? - -```zig -// Module A -const ArgsA = struct { - verbose: bool = false, -}; - -// Module B -const ArgsB = struct { - verbose: bool = false, // OK - Compatible types -}; - -// Module C -const ArgsC = struct { - verbose: u32 = 0, // ERROR - Incompatible type! -}; -``` - -**Design Decision: Compatible Collisions Only** - -- **Allow compatible collisions** - Multiple modules can define the same argument name if types match -- **Reject incompatible collisions** - Attempting to register an argument with a different type than an existing one is an error -- **Warn on compatible collisions** - Issue a warning when multiple modules register the same argument -- **Reserved argument**: `--help` is always reserved and mapped to a boolean - -```zig -// This is OK - both are bool -gArguments.register(ArgsA, "ModuleA"); // Registers 'verbose: bool' -gArguments.register(ArgsB, "ModuleB"); // Warning: 'verbose' already registered by ModuleA (compatible) - -// This will fail -gArguments.register(ArgsC, "ModuleC"); // Error: 'verbose' already registered as bool, cannot register as u32 -``` - -This ensures type safety across the entire program while allowing common flags like `--verbose` to be shared between modules. - -#### 3. **Metadata Storage (Not Type Erasure)** -The global registry does **not** store struct instances or types. Instead, it stores metadata about the arguments: - -```zig -pub const ArgumentRegistry = struct { - // Store metadata per argument, not per struct - arguments: std.StringHashMap(ArgumentMetadata), - - // Track which modules registered which arguments - module_args: std.StringHashMap(std.ArrayList([]const u8)), - - const ArgumentMetadata = struct { - name: []const u8, - type: ArgumentType, - default_value: []const u8, - short: ?u8, - long: []const u8, - help: []const u8, - value_name: []const u8, - is_list: bool, - - // Source location where first registered - source_location: std.builtin.SourceLocation, - - // Which modules registered this argument - registered_by: std.ArrayList([]const u8), - }; - - const ArgumentType = enum { - bool, - u32, - i32, - u64, - i64, - string, - string_list, - // ... other types - }; -}; -``` - -**Key Insight:** We don't need to store the structs themselves. When a module calls `parse()`: - -1. Extract metadata from the struct fields (comptime) -2. Register each argument's metadata in the global registry -3. Check for type compatibility with existing arguments -4. Store source location via `@src()` -5. Parse values from argv into the registry - -Later, when the same or different module calls `get()`: - -1. Look up parsed values in registry by argument name -2. Construct and return the struct with parsed/default values -3. All done at the call site - no type erasure needed! - -#### 4. **Parsing Timing - Parse on First Encounter** -Parsing happens lazily on first `parse()` call for each struct, not upfront: - -```zig -// Module A - first parse() call -const engine_args = try gArguments.parse(EngineArgs, .{ .module = "Engine" }); -// This: -// 1. Extracts metadata from EngineArgs fields (comptime) -// 2. Registers metadata in global registry -// 3. Parses argv for these arguments -// 4. Stores source location via @src() -// 5. Returns populated struct - -// Module B - later parse() call -const physics_args = try gArguments.parse(PhysicsArgs, .{ .module = "Physics" }); -// This: -// 1. Extracts metadata from PhysicsArgs fields -// 2. Registers metadata (checks for type conflicts) -// 3. Parses argv for NEW arguments only (already parsed args reused) -// 4. Returns populated struct - -// Module A again - retrieves already parsed data -const engine_args2 = try gArguments.parse(EngineArgs, .{ .module = "Engine" }); -// This just returns the already-parsed values -``` - -**Key Design Points:** -- No separate `register()` and `parseAll()` steps -- Single `parse(T, opts)` function does everything -- First call per struct: extract metadata, register, parse, return -- Subsequent calls: just return already-parsed values -- Metadata accumulates over program lifetime -- `getUsageAlloc()` can be called at any point to show all arguments discovered so far - -#### 5. **No Positional Arguments - Is This OK?** -You specified no positional arguments. This is fine for game engines, but limits general CLI use: - -```bash -# Can't do this: -mytool input.txt output.txt - -# Must do this: -mytool --input=input.txt --output=output.txt -``` - -**Impact:** -- ✅ Simplifies parsing significantly -- ✅ Reduces ambiguity -- ✅ Better for game engines with many flags -- ❌ Less natural for file-processing CLI tools -- ❌ More verbose command lines - -**Recommendation:** Accept this limitation for v1. If needed later, add opt-in positional support: -```zig -const Args = struct { - input: []const u8 = "", - - pub const meta = .{ - .input = .{ .positional = true }, // Opt-in - }; -}; -``` - -#### 6. **Memory Management** -Who owns the parsed strings? - -```zig -const Args = struct { - output: []const u8 = "default.txt", -}; - -const args = gArguments.get(Args); -// Is args.output allocated? Who frees it? -``` - -**Solution:** Registry owns all allocations: -```zig -pub const ArgumentRegistry = struct { - allocator: Allocator, - arena: ArenaAllocator, // All parsed strings go here - - pub fn deinit(self: *ArgumentRegistry) void { - self.arena.deinit(); // Frees everything at once - } -}; - -// In main: -defer gArguments.deinit(); -``` - -#### 7. **List Parsing Edge Cases** -Comma-separated lists need careful handling: - -```bash ---files=a.txt,b.txt # OK ---files="a.txt,b.txt" # Is this one file or two? ---files=a,\ b.txt # Spaces? ---files= # Empty list? -``` - -**Recommendation:** Keep it simple: -- Split on commas, no escaping in v1 -- For complex cases, use multiple flags: `--file=a.txt --file=b.txt` - -```zig -pub const meta = .{ - .files = .{ - .list = true, // Enable comma-splitting - .or_multiple = true, // Also allow --files=a --files=b - }, -}; -``` - -#### 8. **Help Text Persistence - A Novel Feature** - -This design includes a unique capability: persisting discovered argument documentation for complex programs. - -**The Problem:** Game engines and complex applications may have dozens of plugins, each with arguments. On first run, you don't know what all the arguments are until all plugins load. But you want to document them for users. - -**The Solution:** Generate and persist help text after first run: - -```zig -// First run of the program - plugins load and register args -const engine_args = try gArguments.parse(EngineArgs, .{ .module = "Engine" }); -const physics_args = try gArguments.parse(PhysicsArgs, .{ .module = "Physics" }); -const audio_args = try gArguments.parse(AudioArgs, .{ .module = "Audio" }); - -// At the end of initialization (or in a debug menu) -const help_text = try gArguments.getUsageAlloc(allocator); - -// Write to file for documentation -try std.fs.cwd().writeFile("arguments.txt", help_text); - -// Or even embed as a resource in the binary for --help display -``` - -**Usage patterns:** - -1. **Development:** Generate `arguments.txt` after full initialization -2. **CI/CD:** Run with `--generate-help` flag, commit generated docs -3. **Embedded:** Embed the help text as a `@embedFile()` resource in release builds -4. **Runtime:** Always support `--help` to show current help (may be partial if not all plugins loaded) - -```zig -// Check for help before any parsing -if (gArguments.isHelpRequested()) { - const help = comptime @embedFile("arguments.txt"); // Embedded from previous run - std.debug.print("{s}\n", .{help}); - return; -} -``` - -This approach is particularly valuable for: -- Game engines with plugin systems -- Large applications with conditional modules -- Tools that discover features at runtime -- Programs where full initialization is slow - -**This is a significant departure from traditional CLI parsing**, where help is always generated from a static schema. Here, help is discovered dynamically and can be persisted across runs. - -```zig -const std = @import("std"); -const zargs = @import("zargs"); - -// Global registry singleton -pub var gArguments: zargs.ArgumentRegistry = undefined; - -// Module 1: Engine -pub const EngineArgs = struct { - /// Enable vertical sync - vsync: bool = true, - - /// Display resolution - resolution: []const u8 = "1920x1080", - - /// Graphics API to use - graphics_api: enum { vulkan, opengl, metal } = .vulkan, - - /// Target frame rate - fps_target: u32 = 60, - - pub const meta = .{ - .vsync = .{ .long = "vsync" }, - .resolution = .{ - .short = 'r', - .long = "resolution", - .value_name = "WxH", - }, - .graphics_api = .{ - .long = "graphics-api", - .value_name = "API", - }, - .fps_target = .{ - .long = "fps", - .value_name = "N", - }, - }; -}; - -// Module 2: Physics -pub const PhysicsArgs = struct { - /// Enable SIMD optimizations - use_simd: bool = true, - - /// Physics substeps per frame - substeps: u32 = 4, - - /// Enabled physics layers - layers: []const []const u8 = &.{"default"}, - - pub const meta = .{ - .use_simd = .{ .long = "physics-simd" }, - .substeps = .{ - .long = "physics-substeps", - .value_name = "N", - }, - .layers = .{ - .long = "physics-layers", - .list = true, // Comma-separated - }, - }; -}; - -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - // Initialize global registry - gArguments = zargs.ArgumentRegistry.init(allocator); - defer gArguments.deinit(); - - // Register argument schemas (can happen anywhere in code) - gArguments.register(EngineArgs, .{ .module = "Engine" }); - gArguments.register(PhysicsArgs, .{ .module = "Physics" }); - - // Parse all registered arguments from command line - try gArguments.parseAll(); - - // Check for help request - if (gArguments.isHelpRequested()) { - const help = try gArguments.getUsageAlloc(allocator); - defer allocator.free(help); - std.debug.print("{s}\n", .{help}); - return; - } - - // Modules retrieve their parsed arguments - const engine_args = gArguments.get(EngineArgs); - const physics_args = gArguments.get(PhysicsArgs); - - // Use them with full type safety - std.debug.print("VSync: {}\n", .{engine_args.vsync}); - std.debug.print("Resolution: {s}\n", .{engine_args.resolution}); - std.debug.print("Graphics API: {s}\n", .{@tagName(engine_args.graphics_api)}); - std.debug.print("Physics SIMD: {}\n", .{physics_args.use_simd}); - std.debug.print("Substeps: {}\n", .{physics_args.substeps}); - - for (physics_args.layers) |layer| { - std.debug.print(" Layer: {s}\n", .{layer}); - } -} -``` - -### 🔧 Implementation Sketch - -```zig -pub const ArgumentRegistry = struct { - allocator: Allocator, - arena: std.heap.ArenaAllocator, - - // Store metadata for each unique argument (not per struct) - arguments: std.StringHashMap(ArgumentMetadata), - - // Track which modules registered which arguments - modules: std.StringHashMap(ModuleInfo), - - // Store parsed values by argument name - parsed_values: std.StringHashMap(ParsedValue), - - // Track parsed structs to avoid re-parsing - parsed_structs: std.StringHashMap(void), - - // Cache argv for lazy parsing - argv: ?[]const [:0]const u8 = null, - help_requested: bool = false, - - const ArgumentMetadata = struct { - name: []const u8, - type: ArgumentType, - default_value_str: []const u8, - short: ?u8, - long: []const u8, - help: []const u8, - value_name: []const u8, - is_list: bool, - - // Source location where first registered - source_location: std.builtin.SourceLocation, - - // Which modules use this argument - modules: std.ArrayList([]const u8), - }; - - const ModuleInfo = struct { - name: []const u8, - arguments: std.ArrayList([]const u8), // List of argument names - }; - - const ArgumentType = enum { - bool, - u8, u16, u32, u64, - i8, i16, i32, i64, - string, - string_list, - enum_type, - - pub fn fromZigType(comptime T: type) ArgumentType { - return switch (@typeInfo(T)) { - .Bool => .bool, - .Int => |int| if (int.signedness == .unsigned) - switch (int.bits) { - 8 => .u8, - 16 => .u16, - 32 => .u32, - 64 => .u64, - else => @compileError("Unsupported int size"), - } - else - switch (int.bits) { - 8 => .i8, - 16 => .i16, - 32 => .i32, - 64 => .i64, - else => @compileError("Unsupported int size"), - }, - .Pointer => |ptr| { - if (ptr.size == .Slice and ptr.child == u8) return .string; - // Handle []const []const u8 for string lists - if (ptr.size == .Slice and @typeInfo(ptr.child) == .Pointer) { - return .string_list; - } - @compileError("Unsupported pointer type"); - }, - .Enum => .enum_type, - .Optional => |opt| fromZigType(opt.child), - else => @compileError("Unsupported argument type: " ++ @typeName(T)), - }; - } - - pub fn matches(self: ArgumentType, other: ArgumentType) bool { - return self == other; - } - }; - - const ParsedValue = union(enum) { - bool_val: bool, - u8_val: u8, u16_val: u16, u32_val: u32, u64_val: u64, - i8_val: i8, i16_val: i16, i32_val: i32, i64_val: i64, - string_val: []const u8, - string_list_val: []const []const u8, - enum_val: []const u8, - }; - - pub fn init(allocator: Allocator) ArgumentRegistry { - return .{ - .allocator = allocator, - .arena = std.heap.ArenaAllocator.init(allocator), - .arguments = std.StringHashMap(ArgumentMetadata).init(allocator), - .modules = std.StringHashMap(ModuleInfo).init(allocator), - .parsed_values = std.StringHashMap(ParsedValue).init(allocator), - .parsed_structs = std.StringHashMap(void).init(allocator), - }; - } - - pub fn deinit(self: *ArgumentRegistry) void { - // Clean up module info - var module_iter = self.modules.valueIterator(); - while (module_iter.next()) |module| { - module.arguments.deinit(); - } - - // Clean up argument metadata - var arg_iter = self.arguments.valueIterator(); - while (arg_iter.next()) |arg| { - arg.modules.deinit(); - } - - self.arena.deinit(); - self.arguments.deinit(); - self.modules.deinit(); - self.parsed_values.deinit(); - self.parsed_structs.deinit(); - } - - pub fn isHelpRequested(self: *ArgumentRegistry) bool { - // Check argv on first call - if (self.argv == null) { - var args = std.process.argsAlloc(self.allocator) catch return false; - self.argv = args; - - for (args) |arg| { - if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { - self.help_requested = true; - break; - } - } - } - return self.help_requested; - } - - pub fn parse( - self: *ArgumentRegistry, - comptime T: type, - opts: ParseOptions, - ) !T { - const type_name = @typeName(T); - - // If already parsed this struct, just return values - if (self.parsed_structs.contains(type_name)) { - return self.reconstructStruct(T); - } - - // Mark as parsed - try self.parsed_structs.put(type_name, {}); - - // First time seeing this struct - register metadata - try self.registerMetadata(T, opts); - - // Parse argv for these arguments (only parse new ones) - try self.parseArgv(); - - // Construct and return the struct - return self.reconstructStruct(T); - } - - fn registerMetadata( - self: *ArgumentRegistry, - comptime T: type, - opts: ParseOptions, - ) !void { - const fields = @typeInfo(T).Struct.fields; - - // Get module info or create it - var module = try self.modules.getOrPut(opts.module); - if (!module.found_existing) { - module.value_ptr.* = .{ - .name = opts.module, - .arguments = std.ArrayList([]const u8).init(self.allocator), - }; - } - - inline for (fields) |field| { - // Get metadata for this field - const meta = if (@hasDecl(T, "meta")) - @field(T.meta, field.name) - else - .{}; - - const long_name = if (@hasField(@TypeOf(meta), "long")) - meta.long - else - field.name; - - const arg_type = ArgumentType.fromZigType(field.type); - - // Check if argument already exists - if (self.arguments.get(long_name)) |existing| { - // Check type compatibility - if (!existing.type.matches(arg_type)) { - std.log.err( - "Incompatible type for argument '--{s}':\n" ++ - " First defined as {s} in {s} at {s}:{}:{}\n" ++ - " Now defined as {s} in {s} at {s}:{}:{}\n", - .{ - long_name, - @tagName(existing.type), - existing.modules.items[0], - existing.source_location.file, - existing.source_location.line, - existing.source_location.column, - @tagName(arg_type), - opts.module, - opts.source.file, - opts.source.line, - opts.source.column, - }, - ); - return error.IncompatibleArgumentType; - } - - // Compatible collision - warn and add module - std.log.warn( - "Argument '--{s}' registered by multiple modules: {s}, {s}", - .{ long_name, existing.modules.items[0], opts.module }, - ); - - try existing.modules.append(opts.module); - } else { - // New argument - register it - const default_val = @as(field.type, field.default_value orelse unreachable); - const default_str = try formatDefaultValue(field.type, default_val, self.allocator); - - var modules_list = std.ArrayList([]const u8).init(self.allocator); - try modules_list.append(opts.module); - - try self.arguments.put(long_name, .{ - .name = field.name, - .type = arg_type, - .default_value_str = default_str, - .short = if (@hasField(@TypeOf(meta), "short")) meta.short else null, - .long = long_name, - .help = extractDocComment(T, field.name), - .value_name = if (@hasField(@TypeOf(meta), "value_name")) - meta.value_name - else - std.ascii.toUpperString(field.name), - .is_list = if (@hasField(@TypeOf(meta), "list")) meta.list else false, - .source_location = opts.source, - .modules = modules_list, - }); - } - - // Add to module's argument list - try module.value_ptr.arguments.append(long_name); - } - } - - fn parseArgv(self: *ArgumentRegistry) !void { - if (self.argv == null) { - self.argv = try std.process.argsAlloc(self.allocator); - } - - for (self.argv.?) |arg| { - if (std.mem.startsWith(u8, arg, "--")) { - try self.parseArg(arg[2..]); - } else if (std.mem.startsWith(u8, arg, "-") and arg.len == 2) { - try self.parseShortArg(arg[1]); - } - } - } - - fn reconstructStruct(self: *ArgumentRegistry, comptime T: type) T { - var result: T = undefined; - - inline for (@typeInfo(T).Struct.fields) |field| { - const meta = if (@hasDecl(T, "meta")) - @field(T.meta, field.name) - else - .{}; - - const long_name = if (@hasField(@TypeOf(meta), "long")) - meta.long - else - field.name; - - // Get parsed value or use default - if (self.parsed_values.get(long_name)) |parsed| { - @field(result, field.name) = convertParsedValue(field.type, parsed); - } else { - @field(result, field.name) = field.default_value orelse unreachable; - } - } - - return result; - } - - pub fn getUsageAlloc(self: *ArgumentRegistry, allocator: Allocator) ![]const u8 { - var buf = std.ArrayList(u8).init(allocator); - const writer = buf.writer(); - - try writer.writeAll("Usage: [OPTIONS]\n\n"); - try writer.writeAll("Options:\n"); - try writer.writeAll(" -h, --help Show this help message\n\n"); - - // Group by module - var module_iter = self.modules.iterator(); - while (module_iter.next()) |entry| { - const module = entry.value_ptr; - try writer.print("{s}:\n", .{module.name}); - - for (module.arguments.items) |arg_name| { - const arg = self.arguments.get(arg_name) orelse continue; - - try writer.writeAll(" "); - - if (arg.short) |s| { - try writer.print("-{c}, ", .{s}); - } else { - try writer.writeAll(" "); - } - - try writer.print("--{s}", .{arg.long}); - - if (arg.type != .bool) { - try writer.print(" <{s}>", .{arg.value_name}); - } - - // Padding - try writer.writeAll(" "); - - // Help text - try writer.print("{s}", .{arg.help}); - - // Default value - if (arg.default_value_str.len > 0) { - try writer.print(" [default: {s}]", .{arg.default_value_str}); - } - - try writer.writeAll("\n"); - } - - try writer.writeAll("\n"); - } - - return buf.toOwnedSlice(); - } - - const ParseOptions = struct { - module: []const u8, - source: std.builtin.SourceLocation, - }; - - // ... helper functions for parsing, type conversion, formatting, etc. -}; -``` - -## Comparison to Requirements - -| Requirement | ✅ Met | Notes | -|------------|--------|-------| -| Struct-based schema | ✅ | Clean type-driven definition | -| All args have defaults | ✅ | Enforced by design - no required args | -| No positionals | ✅ | Simplifies parsing significantly | -| List support (--arg=a,b,c) | ✅ | Built-in via metadata | -| Global registry | ✅ | `gArguments` singleton | -| Runtime registration | ✅ | Metadata added on first `parse()` call | -| Dynamic help generation | ✅ | `getUsageAlloc()` at any point | -| Plugin-friendly | ✅ | Perfect for game engines | -| Type safety | ✅ | Compile-time guarantees | -| Grouped help by module | ✅ | Excellent UX | -| Compatible collisions | ✅ | Same arg name OK if types match | -| Incompatible collision errors | ✅ | Different types = error with locations | -| Reserved --help | ✅ | Always mapped to boolean | -| Metadata storage | ✅ | No type erasure, just metadata | -| Source location tracking | ✅ | Via `@src()` for error messages | -| Help text persistence | ✅ | Generate and embed for future runs | -| Lazy parsing | ✅ | Parse on first encounter per struct | - -## Potential Extensions - -### 1. Conflict Detection -```zig -pub const meta = .{ - .config_file = .{ - .long = "config", - .conflicts_with = &.{"manual-mode"}, - }, -}; -``` - -### 2. Environment Variable Fallback -```zig -pub const meta = .{ - .api_key = .{ - .long = "api-key", - .env = "API_KEY", // Check env var if not provided - }, -}; -``` - -### 3. Value Validation -```zig -pub const meta = .{ - .threads = .{ - .long = "threads", - .validator = validateThreadCount, - }, -}; - -fn validateThreadCount(n: u32) !void { - if (n == 0 or n > 64) return error.InvalidThreadCount; -} -``` - -### 4. Subcommands (Future) -```zig -gArguments.registerCommand("build", BuildArgs, "Build the project"); -gArguments.registerCommand("test", TestArgs, "Run tests"); -``` - -### 5. Config File Integration -```zig -// Load from TOML/JSON -try gArguments.loadConfig("config.toml"); -// CLI args override config file values -try gArguments.parseAll(); -``` - -## Verdict - -**This is an excellent design!** 🎉 - -### Why it works: - -1. **Solves the core problem** - Scattered parsing + good documentation -2. **Plugin-friendly** - Perfect for game engine architecture -3. **Type-safe** - Full compile-time checking -4. **Clean API** - Simple to use, hard to misuse -5. **Zig-idiomatic** - Leverages comptime effectively -6. **Practical limitations** - No positionals/all defaults simplifies significantly - -### Recommended Next Steps: - -1. **Prototype the core registry** - Get basic register/parse/get working -2. **Implement help generation** - Critical for the value proposition -3. **Handle list parsing** - Comma-separated values -4. **Test with plugins** - Validate the use case -5. **Add documentation** - Examples for game engine integration -6. **Consider namespacing** - Resolve conflicts between modules - -This design hits a sweet spot between flexibility and structure. It's novel enough to be interesting but practical enough to be useful. The global registry pattern is somewhat unconventional in Zig, but justified by the use case. - -**Go for it!** 🚀 - -## Key Design Innovations - -This design differs significantly from traditional argument parsers in several ways: - -### 1. **Discovery-Based Help Generation** -Traditional parsers require all arguments to be defined upfront. This parser discovers arguments as modules load, enabling: -- Help text that grows as plugins initialize -- Documentation generation after first run -- Embedding help text as a resource for fast `--help` responses -- Perfect for plugin architectures where available arguments depend on runtime state - -### 2. **Compatible Collision System** -Most parsers either forbid argument name collisions or use namespacing. This parser: -- Allows multiple modules to define the same argument if types match -- Enables common flags like `--verbose` to be shared naturally -- Detects incompatible type collisions with detailed error messages including source locations -- Provides a middle ground between strict isolation and complete freedom - -### 3. **Metadata-Only Storage** -The registry doesn't store struct types or instances, only metadata: -- No type erasure needed -- Minimal memory overhead -- Comptime type checking at every `parse()` call site -- No runtime reflection required - -### 4. **Parse-on-Encounter Model** -Unlike two-phase parsers (register then parse) or upfront parsers: -- Each struct parsed independently when first encountered -- Argv parsed incrementally as new arguments discovered -- Already-parsed values reused for subsequent structs -- No coordination needed between modules - -### 5. **Enforced Defaults** -By requiring all arguments to have defaults: -- Eliminates initialization order dependencies -- Simplifies error handling (no "required argument missing" errors) -- Makes partial initialization viable (not all plugins need to load) -- Follows game engine conventions (config with fallbacks) - -## Comparison to Existing Parsers - -| Feature | zargs | clap (Rust) | argparse (Python) | Ad-hoc | -|---------|-------|-------------|-------------------|--------| -| Type-driven schema | ✅ | ✅ | ❌ | ❌ | -| Scattered parsing | ✅ | ❌ | ❌ | ✅ | -| Auto-generated help | ✅ | ✅ | ✅ | ❌ | -| Runtime registration | ✅ | ❌ | Partial | ✅ | -| Plugin-friendly | ✅ | ❌ | ❌ | ✅ | -| Compatible collisions | ✅ | ❌ | ❌ | N/A | -| Help persistence | ✅ | ❌ | ❌ | ❌ | -| No required args | ✅ | ❌ | ❌ | N/A | -| Source location tracking | ✅ | ❌ | ❌ | ❌ | -| Lazy metadata discovery | ✅ | ❌ | ❌ | ✅ | - -## When to Use This Design - -**Perfect for:** -- Game engines with plugin systems -- Applications with runtime-loaded modules -- Large codebases where arguments are scattered across many files -- Tools where full initialization is expensive -- Programs that need to document discovered features - -**Not ideal for:** -- Simple CLI tools with fixed arguments (overkill) -- Programs requiring positional arguments -- When you need strict argument isolation (no shared names) -- Applications requiring required/mandatory arguments -- When initialization order must be controlled - -## Novel Aspects Summary - -This design is genuinely novel in combining: -1. **Type-driven definitions** (like Rust clap derive) -2. **Runtime registration** (like ad-hoc parsers) -3. **Compatible collision handling** (unique to this design) -4. **Help text persistence** (unique to this design) -5. **Parse-on-encounter semantics** (unique to this design) -6. **Source location tracking** (rare in arg parsers) -7. **Metadata-only storage** (enables all of the above in Zig) - -The result is a parser that adapts to the program's actual runtime structure while maintaining type safety and generating comprehensive documentation. It's particularly well-suited to Zig's comptime capabilities and the needs of large, modular systems. diff --git a/lib/zargs/research/type_driven_example.md b/lib/zargs/research/type_driven_example.md deleted file mode 100644 index 27050b2..0000000 --- a/lib/zargs/research/type_driven_example.md +++ /dev/null @@ -1,647 +0,0 @@ -# Type-Driven Argument Parsing - -## Summary - -Type-driven parsing uses the type system and compile-time reflection/metaprogramming to automatically generate the argument parser from type definitions. You define a struct with fields representing your arguments, annotate them with metadata (via attributes, doc comments, or comptime declarations), and the parser is generated automatically. - -Think of it as: **Your types ARE the schema**. No separate parser configuration needed. - -## Core Concept - -``` -struct MyArgs { - @arg(...) field1: Type, - @arg(...) field2: Type, -} - -// Parser generated automatically at compile time -// from the struct definition -``` - -## Concrete Examples - -### Example 1: Rust with clap derive macros - -```rust -use clap::Parser; - -/// Simple program to greet a person -#[derive(Parser, Debug)] -#[command(name = "MyApp")] -#[command(author = "John Doe ")] -#[command(version = "1.0")] -#[command(about = "Does awesome things", long_about = None)] -struct Args { - /// Enable verbose output - #[arg(short, long)] - verbose: bool, - - /// Output file path - #[arg(short, long, value_name = "FILE")] - output: Option, - - /// Number of iterations - #[arg(short = 'n', long, default_value_t = 1)] - count: u32, - - /// Config file path (conflicts with output) - #[arg(short, long, value_name = "PATH", conflicts_with = "output")] - config: Option, - - /// Input files to process - #[arg(required = true)] - files: Vec, -} - -fn main() { - // Parse happens automatically, returns Args struct - let args = Args::parse(); - - // Use as regular struct fields - if args.verbose { - println!("Verbose mode enabled"); - } - - println!("Count: {}", args.count); - - if let Some(output) = &args.output { - println!("Output to: {}", output); - } - - for file in &args.files { - println!("Processing: {}", file); - } -} -``` - -When you run with `--help`: -``` -Does awesome things - -Usage: MyApp [OPTIONS] --files ... - -Arguments: - ... Input files to process - -Options: - -v, --verbose Enable verbose output - -o, --output Output file path - -n, --count Number of iterations [default: 1] - -c, --config Config file path - -h, --help Print help - -V, --version Print version -``` - -### Example 2: Hypothetical Zig with comptime reflection - -```zig -const std = @import("std"); -const zargs = @import("zargs"); - -const Args = struct { - /// Enable verbose output - verbose: bool = false, - - /// Output file path - output: ?[]const u8 = null, - - /// Number of iterations - count: u32 = 1, - - /// Config file path - config: ?[]const u8 = null, - - /// Input files to process - files: []const []const u8 = &.{}, - - // Metadata defined at comptime - pub const meta = .{ - .verbose = .{ - .short = 'v', - .long = "verbose", - }, - .output = .{ - .short = 'o', - .long = "output", - .value_name = "FILE", - }, - .count = .{ - .short = 'n', - .long = "count", - .value_name = "NUM", - }, - .config = .{ - .short = 'c', - .long = "config", - .value_name = "PATH", - .conflicts_with = &.{"output"}, - }, - .files = .{ - .positional = true, - .required = true, - }, - }; - - pub const about = "Does awesome things"; - pub const version = "1.0.0"; -}; - -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - // Parser generated at comptime from Args type - const args = try zargs.parse(Args, allocator); - defer args.deinit(); - - // Use as regular struct fields - if (args.verbose) { - std.debug.print("Verbose mode enabled\n", .{}); - } - - std.debug.print("Count: {}\n", .{args.count}); - - if (args.output) |output| { - std.debug.print("Output to: {s}\n", .{output}); - } - - for (args.files) |file| { - std.debug.print("Processing: {s}\n", .{file}); - } -} -``` - -### Example 3: Alternative Zig approach with field tags - -```zig -const std = @import("std"); -const zargs = @import("zargs"); - -const Args = struct { - verbose: bool = false, - output: ?[]const u8 = null, - count: u32 = 1, - config: ?[]const u8 = null, - files: []const []const u8 = &.{}, -}; - -// Metadata in separate comptime structure -const args_spec = zargs.Spec(Args, .{ - .about = "Does awesome things", - .version = "1.0.0", - .args = .{ - .verbose = .{ - .short = 'v', - .long = "verbose", - .help = "Enable verbose output", - }, - .output = .{ - .short = 'o', - .long = "output", - .help = "Output file path", - .value_name = "FILE", - }, - .count = .{ - .short = 'n', - .long = "count", - .help = "Number of iterations", - .value_name = "NUM", - }, - .config = .{ - .short = 'c', - .long = "config", - .help = "Config file path", - .value_name = "PATH", - .conflicts_with = &.{"output"}, - }, - .files = .{ - .positional = true, - .required = true, - .help = "Input files to process", - }, - }, -}); - -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - const args = try args_spec.parse(allocator); - defer args.deinit(); - - // Use normally... -} -``` - -### Example 4: Zig with doc comment parsing - -```zig -const std = @import("std"); -const zargs = @import("zargs"); - -const Args = struct { - /// Enable verbose output - /// Short: -v, Long: --verbose - verbose: bool = false, - - /// Output file path - /// Short: -o, Long: --output, Value: FILE - output: ?[]const u8 = null, - - /// Number of iterations - /// Short: -n, Long: --count, Value: NUM - count: u32 = 1, - - /// Config file path (conflicts with output) - /// Short: -c, Long: --config, Value: PATH - /// Conflicts: output - config: ?[]const u8 = null, - - /// Input files to process (required) - /// Positional: true - files: []const []const u8 = &.{}, -}; - -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - // Parser extracts metadata from doc comments at comptime - const args = try zargs.parseWithDocs(Args, allocator); - defer args.deinit(); -} -``` - -### Example 5: Haskell with optparse-applicative - -```haskell -{-# LANGUAGE RecordWildCards #-} -import Options.Applicative -import Data.Semigroup ((<>)) - -data Args = Args - { verbose :: Bool - , output :: Maybe String - , count :: Int - , config :: Maybe String - , files :: [String] - } deriving Show - --- Parser defined compositionally with applicative style -argsParser :: Parser Args -argsParser = Args - <$> switch - ( long "verbose" - <> short 'v' - <> help "Enable verbose output" ) - <*> optional (strOption - ( long "output" - <> short 'o' - <> metavar "FILE" - <> help "Output file path" )) - <*> option auto - ( long "count" - <> short 'n' - <> value 1 - <> showDefault - <> help "Number of iterations" ) - <*> optional (strOption - ( long "config" - <> short 'c' - <> metavar "PATH" - <> help "Config file path" )) - <*> some (argument str (metavar "FILES...")) - -main :: IO () -main = do - args <- execParser opts - -- Use the parsed Args - when (verbose args) $ putStrLn "Verbose mode" - print args - where - opts = info (argsParser <**> helper) - ( fullDesc - <> progDesc "Does awesome things" - <> header "myapp - a CLI tool" ) -``` - -### Example 6: TypeScript with ts-command-line-args - -```typescript -import { parse } from 'ts-command-line-args'; - -interface Args { - /** Enable verbose output */ - verbose: boolean; - - /** Output file path */ - output?: string; - - /** Number of iterations */ - count: number; - - /** Config file path */ - config?: string; - - /** Input files to process */ - files: string[]; -} - -// Metadata provided separately -const args = parse( - { - verbose: { - type: Boolean, - alias: 'v', - description: 'Enable verbose output', - defaultValue: false, - }, - output: { - type: String, - alias: 'o', - description: 'Output file path', - optional: true, - }, - count: { - type: Number, - alias: 'n', - description: 'Number of iterations', - defaultValue: 1, - }, - config: { - type: String, - alias: 'c', - description: 'Config file path', - optional: true, - }, - files: { - type: String, - multiple: true, - description: 'Input files to process', - }, - }, - { - helpArg: 'help', - headerContentSections: [ - { header: 'MyApp', content: 'Does awesome things' }, - ], - }, -); - -// Use with type safety -if (args.verbose) { - console.log('Verbose mode'); -} -console.log(`Count: ${args.count}`); -``` - -## Key Characteristics - -### Compile-Time Generation -The parser code is generated at compile time by reflecting on the type: -- Field names become argument names -- Field types determine parsing behavior -- Defaults from field initialization -- Metadata from attributes/annotations - -### Type Safety -Parsing directly produces a typed struct: -```zig -const args: Args = try parse(Args, allocator); -// args.count is u32, not a string or any -``` - -### Co-Located Documentation -Help text lives with the type definition: -- Doc comments become help text -- Annotations specify short/long forms -- Types imply value requirements - -### Zero Boilerplate (Ideally) -```zig -// Define struct -const Args = struct { ... }; - -// Parse - that's it! -const args = try parse(Args, allocator); -``` - -## How It Works (Zig Implementation) - -```zig -pub fn parse(comptime T: type, allocator: Allocator) !T { - // At comptime, reflect on T - const fields = @typeInfo(T).Struct.fields; - - var result: T = undefined; - - // For each field at comptime - inline for (fields) |field| { - // Get metadata if it exists - const meta = if (@hasDecl(T, "meta")) - @field(T.meta, field.name) - else - .{}; - - // Generate parser for this field - const value = try parseField( - field.type, - field.name, - meta, - allocator, - ); - - @field(result, field.name) = value; - } - - return result; -} -``` - -## Advantages - -1. **Minimal code** - Just define the struct -2. **Type safety** - Compiler enforces correctness -3. **DRY principle** - No duplicate schema definitions -4. **Automatic help** - Generated from types + metadata -5. **Refactoring-friendly** - Rename field = rename argument -6. **IDE support** - Autocomplete on result struct -7. **Compile-time validation** - Invalid configs = compile errors - -## Disadvantages - -1. **Requires strong metaprogramming** - Not all languages support this -2. **Less flexible** - Hard to add runtime-conditional arguments -3. **Learning curve** - Attribute syntax can be complex -4. **Debugging difficulty** - Generated code can be opaque -5. **Plugin unfriendly** - Hard for plugins to add arguments -6. **Compile time overhead** - More for compiler to process - -## When to Use - -- Static CLI tools with stable interfaces -- When you value type safety highly -- Languages with good compile-time reflection (Rust, Zig) -- When you want minimal boilerplate -- Single-binary applications (not plugin architectures) - -## Comparison to Other Styles - -| Feature | Type-Driven | Declarative | Ad-hoc | -|---------|-------------|-------------|---------| -| Boilerplate | ✅ Minimal | ⚠️ Moderate | ✅ Minimal | -| Type safety | ✅ Excellent | ⚠️ Good | ❌ Poor | -| Help generation | ✅ Automatic | ✅ Good | ❌ Poor | -| Flexibility | ❌ Limited | ⚠️ Moderate | ✅ High | -| Plugin support | ❌ Poor | ⚠️ Moderate | ✅ Excellent | -| Compile-time cost | ⚠️ Higher | ✅ Low | ✅ Very Low | -| Runtime cost | ✅ Minimal | ⚠️ Moderate | ✅ Minimal | - -## Zig-Specific Considerations - -### Leverage Comptime -Zig's comptime is perfect for type-driven parsing: -- `@typeInfo()` for reflection -- `@hasDecl()` for optional metadata -- `@field()` for generic field access -- `inline for` for compile-time iteration - -### Metadata Strategies - -**1. Separate meta struct:** -```zig -pub const meta = .{ - .verbose = .{ .short = 'v' }, -}; -``` - -**2. Doc comment parsing:** -```zig -/// Enable verbose output -/// @short v -/// @long verbose -verbose: bool, -``` - -**3. Field-level declarations:** -```zig -verbose: bool = false, -pub const verbose_short = 'v'; -pub const verbose_help = "Enable verbose output"; -``` - -### Type Mapping -Zig types naturally map to argument types: -- `bool` → flag (no value) -- `?T` → optional argument -- `u32`, `i32`, etc. → parsed integers -- `[]const u8` → string argument -- `[]const []const u8` → multiple values - -### Memory Management -Type-driven parsing needs to allocate for strings: -```zig -const Args = struct { - output: ?[]const u8, - - allocator: Allocator, - - pub fn deinit(self: Args) void { - if (self.output) |out| { - self.allocator.free(out); - } - } -}; -``` - -## Best Practices - -1. **Keep structs flat** - Nested structs complicate parsing -2. **Use meaningful defaults** - They document expected values -3. **Document thoroughly** - Doc comments become help text -4. **Validate in types** - Use enums for restricted values -5. **Consider optional fields** - Use `?T` for truly optional args -6. **Provide deinit** - If parser allocates, provide cleanup - -## Example: Complex Zig Type-Driven Parser - -```zig -const std = @import("std"); -const zargs = @import("zargs"); - -const LogLevel = enum { - debug, - info, - warn, - err, - - pub fn fromString(s: []const u8) !LogLevel { - return std.meta.stringToEnum(LogLevel, s) - orelse error.InvalidLogLevel; - } -}; - -const Args = struct { - /// Verbosity level - verbose: bool = false, - - /// Log level (debug, info, warn, err) - log_level: LogLevel = .info, - - /// Output directory - output_dir: []const u8 = "out", - - /// Input files (at least one required) - inputs: []const []const u8, - - /// Number of worker threads - threads: ?u32 = null, - - /// Enable experimental features - experimental: bool = false, - - allocator: Allocator, - - pub const meta = .{ - .verbose = .{ .short = 'v', .long = "verbose" }, - .log_level = .{ .short = 'l', .long = "log-level", .value_name = "LEVEL" }, - .output_dir = .{ .short = 'o', .long = "output", .value_name = "DIR" }, - .inputs = .{ .positional = true, .required = true }, - .threads = .{ .short = 'j', .long = "threads", .value_name = "N" }, - .experimental = .{ .long = "experimental" }, - }; - - pub const about = "Process input files and generate output"; - pub const version = "2.1.0"; - - pub fn deinit(self: Args) void { - self.allocator.free(self.output_dir); - for (self.inputs) |input| { - self.allocator.free(input); - } - self.allocator.free(self.inputs); - } -}; - -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - const args = try zargs.parse(Args, allocator); - defer args.deinit(); - - std.debug.print("Log level: {s}\n", .{@tagName(args.log_level)}); - std.debug.print("Output dir: {s}\n", .{args.output_dir}); - std.debug.print("Thread count: {?}\n", .{args.threads}); - - for (args.inputs) |input| { - std.debug.print("Processing: {s}\n", .{input}); - } -} -``` - -This combines the elegance of type-driven parsing with Zig's comptime power for a clean, type-safe CLI interface. diff --git a/lib/zargs/src/ArgumentRegistry.zig b/lib/zargs/src/ArgumentRegistry.zig index be749e6..3c8ebe4 100644 --- a/lib/zargs/src/ArgumentRegistry.zig +++ b/lib/zargs/src/ArgumentRegistry.zig @@ -1,6 +1,6 @@ const std = @import("std"); -const metadata = @import("metadata"); -const ParsedValue = @import("ArgumentType").ParsedValue; +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 @@ -21,13 +21,15 @@ pub const ArgumentRegistry = struct { /// Prevents duplicate registration registered_types: std.StringHashMap(void), - /// Cached argv for parsing - /// Owned by this registry - argv: ?[]const [:0]const u8 = null, + /// 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), @@ -39,6 +41,7 @@ pub const ArgumentRegistry = struct { /// 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), @@ -84,13 +87,8 @@ pub const ArgumentRegistry = struct { } self.parsed_values.deinit(); - // Free argv if we own it - if (self.argv) |args| { - for (args) |arg| { - self.allocator.free(arg); - } - self.allocator.free(args); - } + // 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 @@ -99,17 +97,35 @@ pub const ArgumentRegistry = struct { 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, {}); } - /// Check if help was requested - pub fn isHelpRequested(self: *const ArgumentRegistry) bool { - return self.help_requested; - } - /// 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| { @@ -148,12 +164,45 @@ pub const ArgumentRegistry = struct { 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 - /// Extracts all field metadata and registers each argument + /// 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, @@ -208,7 +257,7 @@ pub const ArgumentRegistry = struct { // 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 @@ -218,15 +267,15 @@ pub const ArgumentRegistry = struct { 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, {}); } @@ -251,10 +300,3 @@ pub const ArgumentRegistry = struct { return self.arguments.count(); } }; - -// Compile-time validation -comptime { - // Verify ArgumentRegistry can be created - const allocator = std.heap.page_allocator; - _ = ArgumentRegistry.init(allocator); -} diff --git a/lib/zargs/src/help.zig b/lib/zargs/src/help.zig index b6cc86f..89339a5 100644 --- a/lib/zargs/src/help.zig +++ b/lib/zargs/src/help.zig @@ -1,38 +1,38 @@ const std = @import("std"); -const metadata = @import("metadata"); -const ArgumentRegistry = @import("ArgumentRegistry").ArgumentRegistry; -const ArgumentType = @import("ArgumentType").ArgumentType; +const metadata = @import("metadata.zig"); +const ArgumentRegistry = @import("ArgumentRegistry.zig").ArgumentRegistry; +const ArgumentType = @import("ArgumentType.zig").ArgumentType; /// Generate help text from registered arguments pub fn generateHelpText( - registry: *const ArgumentRegistry, + registry: ArgumentRegistry, allocator: std.mem.Allocator, program_name: ?[]const u8, ) ![]const u8 { var buffer = std.ArrayListUnmanaged(u8){}; errdefer buffer.deinit(allocator); const writer = buffer.writer(allocator); - + // Write program name/header if (program_name) |name| { try writer.print("Usage: {s} [OPTIONS]\n\n", .{name}); } else { try writer.writeAll("Usage: [OPTIONS]\n\n"); } - + // Write description if available (TODO: add module_info support) - + // Collect all arguments for formatting var args_list = std.ArrayListUnmanaged(ArgumentInfo){}; defer args_list.deinit(allocator); - + var arg_iter = registry.arguments.iterator(); while (arg_iter.next()) |entry| { const arg_meta = entry.value_ptr; - + // Skip short flags (they'll be shown with their long form) if (entry.key_ptr.len == 1) continue; - + try args_list.append(allocator, .{ .long_name = arg_meta.arg_name, .short_char = arg_meta.short, @@ -43,11 +43,11 @@ pub fn generateHelpText( .enum_values = arg_meta.enum_values, }); } - + // Sort arguments alphabetically by long name const items = args_list.items; std.mem.sort(ArgumentInfo, items, {}, argumentLessThan); - + // Calculate maximum width for alignment var max_flags_width: usize = 0; for (items) |arg| { @@ -56,24 +56,24 @@ pub fn generateHelpText( max_flags_width = width; } } - + // Add padding const padding = 2; const total_width = max_flags_width + padding; - + // Write "Options:" header try writer.writeAll("Options:\n"); - + // Always show help first try writer.writeAll(" -h, --help"); try writePadding(writer, 12, total_width); try writer.writeAll("Show this help message\n"); - + // Write each argument for (items) |arg| { try writeArgumentHelp(writer, arg, total_width); } - + return buffer.toOwnedSlice(allocator); } @@ -96,20 +96,20 @@ fn argumentLessThan(_: void, a: ArgumentInfo, b: ArgumentInfo) bool { /// Calculate the width of the flags portion (e.g., "-v, --verbose") fn calculateFlagsWidth(arg: ArgumentInfo) usize { var width: usize = 2; // Leading " " - + if (arg.short_char) |_| { width += 4; // "-x, " } - + width += 2; // "--" width += arg.long_name.len; - + // Add value placeholder for non-boolean types if (arg.arg_type != .bool) { width += 1; // space width += getValuePlaceholder(arg.arg_type).len; } - + return width; } @@ -142,33 +142,33 @@ fn writeArgumentHelp(writer: anytype, arg: ArgumentInfo, total_width: usize) !vo // Write flags try writer.writeAll(" "); var current_width: usize = 2; - + if (arg.short_char) |short| { try writer.print("-{c}, ", .{short}); current_width += 4; } - + try writer.print("--{s}", .{arg.long_name}); current_width += 2 + arg.long_name.len; - + // Add value placeholder for non-boolean types if (arg.arg_type != .bool) { const placeholder = getValuePlaceholder(arg.arg_type); try writer.print(" {s}", .{placeholder}); current_width += 1 + placeholder.len; } - + // Write padding try writePadding(writer, current_width, total_width); - + // Write help text try writer.writeAll(arg.help_text); - + // Add default value if present if (arg.default_value) |default| { try writer.print(" [default: {s}]", .{default}); } - + // Add enum choices if present if (arg.enum_values) |values| { if (values.len > 0) { @@ -180,12 +180,12 @@ fn writeArgumentHelp(writer: anytype, arg: ArgumentInfo, total_width: usize) !vo try writer.writeByte(']'); } } - + // Add required marker if no default if (arg.required and arg.default_value == null) { try writer.writeAll(" (required)"); } - + try writer.writeByte('\n'); } diff --git a/lib/zargs/src/main.zig b/lib/zargs/src/main.zig index dfad987..bc4074c 100644 --- a/lib/zargs/src/main.zig +++ b/lib/zargs/src/main.zig @@ -8,96 +8,41 @@ pub const FieldMeta = @import("metadata.zig").FieldMeta; pub const ModuleInfo = @import("metadata.zig").ModuleInfo; pub const ArgumentRegistry = @import("ArgumentRegistry.zig").ArgumentRegistry; pub const generateHelpText = @import("help.zig").generateHelpText; -pub const parseArgv = @import("parsing.zig").parseArgv; -pub const populateStruct = @import("parsing.zig").populateStruct; -// Version information -pub const version = "0.1.0-dev"; +pub var gRegistry: ?ArgumentRegistry = null; -/// Parse command-line arguments into a struct -/// This is the main entry point for the library -/// -/// Example: -/// ```zig -/// const Config = struct { -/// verbose: bool = false, -/// output: []const u8 = "output.txt", -/// count: u32 = 10, -/// -/// pub const meta = .{ -/// .verbose = .{ .short = 'v', .help = "Enable verbose output" }, -/// .output = .{ .short = 'o', .help = "Output file path" }, -/// .count = .{ .short = 'c', .help = "Number of items" }, -/// }; -/// }; -/// -/// var gpa = std.heap.GeneralPurposeAllocator(.{}){}; -/// defer _ = gpa.deinit(); -/// -/// const config = try zargs.parse(Config, gpa.allocator(), std.os.argv); -/// ``` -pub fn parse( - comptime T: type, - allocator: std.mem.Allocator, - argv: []const [:0]const u8, -) !T { - var registry = ArgumentRegistry.init(allocator); - defer registry.deinit(); - - // Register the struct's metadata - try registry.registerMetadata(T, @typeName(T)); - - // Parse the arguments - try parseArgv(®istry, argv); - - // Check if help was requested - if (registry.isHelpRequested()) { - const program_name = if (argv.len > 0) argv[0] else null; - const help_text = try generateHelpText(®istry, allocator, program_name); - defer allocator.free(help_text); - - // Print help and return error - try std.io.getStdOut().writeAll(help_text); - return error.HelpRequested; +pub fn getUsageAlloc(allocator: std.mem.Allocator, programName: []const u8) ![]const u8 { + if (gRegistry == null) { + gRegistry = ArgumentRegistry.init(allocator); } - - // Populate and return the struct - return populateStruct(T, ®istry, allocator); + + return try generateHelpText(gRegistry.?, allocator, programName); } -/// Parse with a custom registry (for advanced use cases) -/// Allows multiple modules to register their arguments before parsing -pub fn parseWithRegistry( - comptime T: type, - registry: *ArgumentRegistry, - allocator: std.mem.Allocator, - argv: []const [:0]const u8, -) !T { - // Register the struct's metadata if not already done - if (!registry.isTypeRegistered(T)) { - try registry.registerMetadata(T, @typeName(T)); +pub fn parse(comptime T: type, allocator: std.mem.Allocator) !T { + if (gRegistry == null) { + gRegistry = ArgumentRegistry.init(allocator); } - - // Parse the arguments - try parseArgv(registry, argv); - - // Check if help was requested - if (registry.isHelpRequested()) { - const program_name = if (argv.len > 0) argv[0] else null; - const help_text = try generateHelpText(registry, allocator, program_name); - defer allocator.free(help_text); - - // Print help and return error - try std.io.getStdOut().writeAll(help_text); - return error.HelpRequested; + + const value = try gRegistry.?.populate(T, @typeName(T), allocator); + return value; +} + +pub fn isHelp(allocator: std.mem.Allocator) bool { + if (gRegistry == null) { + gRegistry = ArgumentRegistry.init(allocator); + } + gRegistry.?.scanForHelp(); + return gRegistry.?.help_requested; +} + +pub fn shutdown() void { + if (gRegistry) |*reg| { + reg.deinit(); } - - // Populate and return the struct - return populateStruct(T, registry, allocator); } test { // Reference all test files _ = @import("ArgumentType.zig"); } - diff --git a/lib/zargs/src/metadata.zig b/lib/zargs/src/metadata.zig index d6b7ce5..efbd210 100644 --- a/lib/zargs/src/metadata.zig +++ b/lib/zargs/src/metadata.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const ArgumentTypeModule = @import("ArgumentType"); +const ArgumentTypeModule = @import("ArgumentType.zig"); const ArgumentType = ArgumentTypeModule.ArgumentType; /// Metadata for a single command-line argument @@ -249,7 +249,11 @@ fn formatDefaultValue(comptime T: type, default_ptr: *const anyopaque) ?[]const } break :blk null; }, - .@"enum" => @tagName(value), + .@"enum" => blk: { + // Can't use @tagName at comptime with generic enum values + // Just return the enum type name for now + break :blk @typeName(ActualType); + }, else => null, }; } diff --git a/lib/zargs/src/parse.zig b/lib/zargs/src/parse.zig new file mode 100644 index 0000000..6a8a218 --- /dev/null +++ b/lib/zargs/src/parse.zig @@ -0,0 +1,354 @@ +const std = @import("std"); +const ArgumentType = @import("ArgumentType").ArgumentType; +const ParsedValue = @import("ArgumentType").ParsedValue; +const metadata = @import("metadata"); + +/// Global arena allocator for argument parsing +/// All parsed strings and allocations live here until program exit +var global_parse_arena: ?std.heap.ArenaAllocator = null; +var global_parse_arena_mutex: std.Thread.Mutex = .{}; + +/// Get or create the global parse arena +fn getParseArena(parent_allocator: std.mem.Allocator) !std.mem.Allocator { + global_parse_arena_mutex.lock(); + defer global_parse_arena_mutex.unlock(); + + if (global_parse_arena == null) { + global_parse_arena = std.heap.ArenaAllocator.init(parent_allocator); + } + + return global_parse_arena.?.allocator(); +} + +/// Temporary storage for parsed values during struct population +/// All allocations use the arena allocator and live until program exit +const ParsedArguments = struct { + arena: std.mem.Allocator, + values: std.StringHashMap(ParsedValue), + + pub fn init(arena: std.mem.Allocator) ParsedArguments { + return .{ + .arena = arena, + .values = std.StringHashMap(ParsedValue).init(arena), + }; + } + + pub fn deinit(self: *ParsedArguments) void { + // No need to free individual values - arena owns everything + self.values.deinit(); + } + + pub fn put(self: *ParsedArguments, name: []const u8, value: ParsedValue) !void { + // For repeated arguments (like lists), we don't need to free old values + // Arena will clean up everything eventually + try self.values.put(name, value); + } + + pub fn get(self: *const ParsedArguments, name: []const u8) ?ParsedValue { + return self.values.get(name); + } +}; + +/// Check if help was requested in argv +pub fn isHelpRequested(argv: []const [:0]const u8) bool { + for (argv[1..]) |arg| { + if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { + return true; + } + } + return false; +} + +/// Parse argv for a specific struct type +/// Only parses arguments that match the struct's fields +fn parseForStruct( + comptime T: type, + argv: []const [:0]const u8, + allocator: std.mem.Allocator, +) !ParsedArguments { + // Get the global arena for all parse allocations + const arena = try getParseArena(allocator); + var result = ParsedArguments.init(arena); + errdefer result.deinit(); + + const type_info = @typeInfo(T); + if (type_info != .@"struct") { + @compileError("parseForStruct requires a struct type"); + } + + // Build a comptime lookup table for quick matching + // Maps argument names (both long and short) to field info + // We use a simple struct to avoid the formatDefaultValue issue + const FieldInfo = struct { + arg_name: []const u8, + arg_type: ArgumentType, + short: ?u8, + }; + + comptime var field_lookup: std.StaticStringMap(FieldInfo) = blk: { + var entries: []const struct { []const u8, FieldInfo } = &.{}; + for (type_info.@"struct".fields) |field| { + // Extract just what we need for parsing + const field_meta = if (metadata.hasFieldMeta(T, field.name)) + metadata.getFieldMeta(T, field.name) + else + metadata.FieldMeta{}; + + const arg_name = if (field_meta.name) |custom| custom else field.name; + + const arg_type = ArgumentType.fromZigType(field.type); + const short = field_meta.short; + + const info = FieldInfo{ + .arg_name = arg_name, + .arg_type = arg_type, + .short = short, + }; + + // Add long form + entries = entries ++ &[_]struct { []const u8, FieldInfo }{ + .{ arg_name, info }, + }; + + // Add short form if present + if (short) |short_char| { + const short_str = &[_]u8{short_char}; + entries = entries ++ &[_]struct { []const u8, FieldInfo }{ + .{ short_str, info }, + }; + } + } + break :blk std.StaticStringMap(FieldInfo).initComptime(entries); + }; + + var i: usize = 1; // Skip program name + while (i < argv.len) : (i += 1) { + const arg = argv[i]; + + // Skip help flags (already handled by isHelpRequested) + if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { + continue; + } + + // Parse --flag, --flag=value formats + if (std.mem.startsWith(u8, arg, "--")) { + const long_arg = arg[2..]; + + // Check for --name=value format + if (std.mem.indexOf(u8, long_arg, "=")) |eq_idx| { + const name = long_arg[0..eq_idx]; + const value = long_arg[eq_idx + 1 ..]; + + // Only process if this struct recognizes the argument + if (field_lookup.get(name)) |field_info| { + try parseLongArgWithValue(&result, field_info.arg_name, field_info.arg_type, value, arena); + } + } else { + // --name format - might be boolean flag or take next arg as value + if (field_lookup.get(long_arg)) |field_info| { + if (field_info.arg_type == .bool) { + // Boolean flag - implicit true + const parsed = try ParsedValue.fromString(.bool, "true", arena); + try result.put(field_info.arg_name, parsed); + } else { + // Take next argument as value + if (i + 1 >= argv.len) return error.MissingArgumentValue; + i += 1; + const value = argv[i]; + try parseLongArgWithValue(&result, field_info.arg_name, field_info.arg_type, value, arena); + } + } + // Silently ignore unknown arguments (other modules may use them) + } + } + // Short form: -x or -x value + else if (std.mem.startsWith(u8, arg, "-") and arg.len == 2) { + const short_char = arg[1]; + const short_key = &[_]u8{short_char}; + + if (field_lookup.get(short_key)) |field_info| { + if (field_info.arg_type == .bool) { + // Boolean flag - implicit true + const parsed = try ParsedValue.fromString(.bool, "true", arena); + try result.put(field_info.arg_name, parsed); + } else { + // Take next argument as value + if (i + 1 >= argv.len) return error.MissingArgumentValue; + i += 1; + const value = argv[i]; + + const parsed = try ParsedValue.fromString(field_info.arg_type, value, arena); + try result.put(field_info.arg_name, parsed); + } + } + // Silently ignore unknown short flags + } + // Multi-flag short form: -abc (treat as -a -b -c) + else if (std.mem.startsWith(u8, arg, "-") and arg.len > 2) { + for (arg[1..]) |short_char| { + const short_key = &[_]u8{short_char}; + + if (field_lookup.get(short_key)) |field_info| { + // Multi-flag only works for boolean flags + if (field_info.arg_type != .bool) { + return error.InvalidArgumentFormat; + } + + const parsed = try ParsedValue.fromString(.bool, "true", arena); + try result.put(field_info.arg_name, parsed); + } + // Silently ignore unknown flags in multi-flag + } + } + // Ignore positional arguments (not supported by design) + } + + return result; +} + +/// Parse a long argument with a value +/// All allocations use the arena allocator +fn parseLongArgWithValue( + result: *ParsedArguments, + arg_name: []const u8, + arg_type: ArgumentType, + value: []const u8, + arena: std.mem.Allocator, +) !void { + // Handle list types - support both comma-separated and repeated arguments + if (arg_type == .string_list) { + // Check if we already have a value for this argument + const existing = result.get(arg_name); + + if (existing) |prev| { + // Append to existing list + var new_list = std.ArrayListUnmanaged([]const u8){}; + defer new_list.deinit(arena); + + // Add previous values (reuse the string pointers - arena owns them) + for (prev.string_list) |str| { + try new_list.append(arena, str); + } + + // Parse and add new values (comma-separated) + var iter = std.mem.splitSequence(u8, value, ","); + while (iter.next()) |item| { + const trimmed = std.mem.trim(u8, item, " \t"); + const duped = try arena.dupe(u8, trimmed); + try new_list.append(arena, duped); + } + + const final_list = try new_list.toOwnedSlice(arena); + + // No need to free old array - arena owns it + + // Put the new value + const parsed = ParsedValue{ .string_list = final_list }; + try result.values.put(arg_name, parsed); + } else { + // First occurrence - parse comma-separated values + var list = std.ArrayListUnmanaged([]const u8){}; + defer list.deinit(arena); + + var iter = std.mem.splitSequence(u8, value, ","); + while (iter.next()) |item| { + const trimmed = std.mem.trim(u8, item, " \t"); + const duped = try arena.dupe(u8, trimmed); + try list.append(arena, duped); + } + + const final_list = try list.toOwnedSlice(arena); + const parsed = ParsedValue{ .string_list = final_list }; + try result.put(arg_name, parsed); + } + } else if (arg_type == .enum_type) { + // For enum types, we need to store the string and let the populate function handle it + const duped_name = try arena.dupe(u8, value); + const parsed = ParsedValue{ .enum_type = .{ .name = duped_name, .value = 0 } }; + try result.put(arg_name, parsed); + } else { + // Non-list type - just parse + const parsed = try ParsedValue.fromString(arg_type, value, arena); + try result.put(arg_name, parsed); + } +} + +/// Populate struct from parsed arguments +/// Strings are owned by the global arena and live until program exit +fn populateFromParsed( + comptime T: type, + parsed: ParsedArguments, + allocator: std.mem.Allocator, +) !T { + _ = allocator; // Not used - arena owns all allocations + const type_info = @typeInfo(T); + var result: T = undefined; + + inline for (type_info.@"struct".fields) |field| { + // Extract arg_name from metadata + const field_meta = if (metadata.hasFieldMeta(T, field.name)) + metadata.getFieldMeta(T, field.name) + else + metadata.FieldMeta{}; + + const arg_name = if (field_meta.name) |custom| custom else field.name; + + if (parsed.get(arg_name)) |value| { + // Special handling for enum types + const field_info = @typeInfo(field.type); + const is_optional = field_info == .optional; + const ActualType = if (is_optional) field_info.optional.child else field.type; + const actual_info = @typeInfo(ActualType); + + if (actual_info == .@"enum") { + // Parse enum by name + const enum_name = value.enum_type.name; + inline for (actual_info.@"enum".fields) |enum_field| { + if (std.mem.eql(u8, enum_name, enum_field.name)) { + const enum_value = @field(ActualType, enum_field.name); + @field(result, field.name) = if (is_optional) enum_value else enum_value; + break; + } + } else { + return error.InvalidEnumValue; + } + } else { + // Convert to field type normally + @field(result, field.name) = value.toTypedValue(field.type); + } + } else { + // Use default value + if (field.default_value_ptr) |default_ptr| { + const value_ptr: *const field.type = @ptrCast(@alignCast(default_ptr)); + @field(result, field.name) = value_ptr.*; + } else { + // No default value and no parsed value + return error.MissingRequiredArgument; + } + } + } + + return result; +} + +/// Parse argv directly into a struct +/// This is the core parsing function - no registry needed +/// All string allocations live in a global arena until program exit +pub fn parse( + comptime T: type, + allocator: std.mem.Allocator, + argv: []const [:0]const u8, +) !T { + // Check for --help / -h (caller handles help display) + if (isHelpRequested(argv)) { + return error.HelpRequested; + } + + // Scan argv and parse matching arguments + // All allocations go into the global arena + var parsed = try parseForStruct(T, argv, allocator); + defer parsed.deinit(); // Only deinits the HashMap, not the arena + + // Populate struct with arena-allocated strings + return populateFromParsed(T, parsed, allocator); +} diff --git a/lib/zargs/src/parsing.zig b/lib/zargs/src/parsing.zig index bacc604..7987dd5 100644 --- a/lib/zargs/src/parsing.zig +++ b/lib/zargs/src/parsing.zig @@ -1,8 +1,8 @@ const std = @import("std"); -const ArgumentType = @import("ArgumentType").ArgumentType; -const ParsedValue = @import("ArgumentType").ParsedValue; -const metadata = @import("metadata"); -const ArgumentRegistry = @import("ArgumentRegistry").ArgumentRegistry; +const ArgumentType = @import("ArgumentType.zig").ArgumentType; +const ParsedValue = @import("ArgumentType.zig").ParsedValue; +const metadata = @import("metadata.zig"); +const ArgumentRegistry = @import("ArgumentRegistry.zig").ArgumentRegistry; /// Result of parsing a single argument pub const ParseResult = struct { @@ -11,6 +11,7 @@ pub const ParseResult = struct { }; /// Parse argv and populate the registry with parsed values +/// Ignores unknown arguments (they may belong to modules not yet loaded) pub fn parseArgv(registry: *ArgumentRegistry, argv: []const [:0]const u8) !void { var i: usize = 1; // Skip program name @@ -31,21 +32,38 @@ pub fn parseArgv(registry: *ArgumentRegistry, argv: []const [:0]const u8) !void if (std.mem.indexOf(u8, long_arg, "=")) |eq_idx| { const name = long_arg[0..eq_idx]; const value = long_arg[eq_idx + 1 ..]; - try parseLongArgWithValue(registry, name, value); + parseLongArgWithValue(registry, name, value) catch |err| { + // Ignore unknown arguments - they may belong to other modules + if (err == error.UnknownArgument) continue; + return err; + }; } else { // --name format - might be boolean flag or take next arg as value - const arg_meta = registry.getArgument(long_arg) orelse return error.UnknownArgument; + const arg_meta = registry.getArgument(long_arg) orelse { + // Unknown argument - skip it + continue; + }; if (arg_meta.arg_type == .bool) { // Boolean flag - implicit true - const parsed = try ParsedValue.fromString(.bool, "true", registry.allocator); - try registry.storeParsedValue(arg_meta.arg_name, parsed); + // Only store if not already parsed + if (registry.getParsedValue(arg_meta.arg_name) == null) { + const parsed = try ParsedValue.fromString(.bool, "true", registry.allocator); + try registry.storeParsedValue(arg_meta.arg_name, parsed); + } } else { // Take next argument as value - if (i + 1 >= argv.len) return error.MissingArgumentValue; + if (i + 1 >= argv.len) { + // No value provided - skip this argument + continue; + } i += 1; const value = argv[i]; - try parseLongArgWithValue(registry, long_arg, value); + parseLongArgWithValue(registry, long_arg, value) catch |err| { + // Ignore unknown arguments + if (err == error.UnknownArgument) continue; + return err; + }; } } } @@ -54,40 +72,56 @@ pub fn parseArgv(registry: *ArgumentRegistry, argv: []const [:0]const u8) !void const short_char = arg[1]; const short_key = &[_]u8{short_char}; - const arg_meta = registry.getArgument(short_key) orelse return error.UnknownArgument; + const arg_meta = registry.getArgument(short_key) orelse { + // Unknown argument - skip it + continue; + }; if (arg_meta.arg_type == .bool) { // Boolean flag - implicit true - const parsed = try ParsedValue.fromString(.bool, "true", registry.allocator); - try registry.storeParsedValue(arg_meta.arg_name, parsed); + // Only store if not already parsed + if (registry.getParsedValue(arg_meta.arg_name) == null) { + const parsed = try ParsedValue.fromString(.bool, "true", registry.allocator); + try registry.storeParsedValue(arg_meta.arg_name, parsed); + } } else { // Take next argument as value - if (i + 1 >= argv.len) return error.MissingArgumentValue; + if (i + 1 >= argv.len) { + // No value provided - skip this argument + continue; + } i += 1; const value = argv[i]; - const parsed = try ParsedValue.fromString(arg_meta.arg_type, value, registry.allocator); - try registry.storeParsedValue(arg_meta.arg_name, parsed); + // Use parseLongArgWithValue which handles all types including lists and enums + parseLongArgWithValue(registry, short_key, value) catch |err| { + // Ignore unknown arguments + if (err == error.UnknownArgument) continue; + return err; + }; } } // Multi-flag short form: -abc (treat as -a -b -c) else if (std.mem.startsWith(u8, arg, "-") and arg.len > 2) { for (arg[1..]) |short_char| { const short_key = &[_]u8{short_char}; - const arg_meta = registry.getArgument(short_key) orelse return error.UnknownArgument; + const arg_meta = registry.getArgument(short_key) orelse { + // Unknown argument - skip it + continue; + }; // Multi-flag only works for boolean flags if (arg_meta.arg_type != .bool) { - return error.InvalidArgumentFormat; + continue; } const parsed = try ParsedValue.fromString(.bool, "true", registry.allocator); try registry.storeParsedValue(arg_meta.arg_name, parsed); } } - // Positional arguments not supported + // Positional arguments not supported - just ignore them else { - return error.UnknownArgument; + continue; } } } @@ -96,11 +130,15 @@ pub fn parseArgv(registry: *ArgumentRegistry, argv: []const [:0]const u8) !void fn parseLongArgWithValue(registry: *ArgumentRegistry, name: []const u8, value: []const u8) !void { const arg_meta = registry.getArgument(name) orelse return error.UnknownArgument; + // For non-list types, skip if already parsed (happens in multi-module scenarios) + // For list types, we allow appending within the same parse pass + const existing = registry.getParsedValue(arg_meta.arg_name); + if (arg_meta.arg_type != .string_list and existing != null) { + return; + } + // Handle list types - support both comma-separated and repeated arguments if (arg_meta.arg_type == .string_list) { - // Check if we already have a value for this argument - const existing = registry.getParsedValue(arg_meta.arg_name); - if (existing) |prev| { // Append to existing list var new_list = std.ArrayListUnmanaged([]const u8){}; @@ -145,7 +183,6 @@ fn parseLongArgWithValue(registry: *ArgumentRegistry, name: []const u8, value: [ } } else if (arg_meta.arg_type == .enum_type) { // For enum types, we need to store the string and let the populate function handle it - // Store as a pseudo-enum value with the string name const duped_name = try registry.allocator.dupe(u8, value); const parsed = ParsedValue{ .enum_type = .{ .name = duped_name, .value = 0 } }; try registry.storeParsedValue(arg_meta.arg_name, parsed); diff --git a/lib/zargs/tests/test_errors.zig b/lib/zargs/tests/test_errors.zig deleted file mode 100644 index 0f7f9a5..0000000 --- a/lib/zargs/tests/test_errors.zig +++ /dev/null @@ -1,100 +0,0 @@ -const std = @import("std"); -const errors = @import("errors"); - -test "Error: all error types defined" { - // Verify all error types exist - const err_types = [_]errors.Error{ - error.IncompatibleArgumentType, - error.UnknownArgument, - error.InvalidValue, - error.InvalidIntegerValue, - error.InvalidBooleanValue, - error.InvalidEnumValue, - error.MissingArgumentValue, - error.OutOfMemory, - }; - - // If we can create all these, they're defined - try std.testing.expect(err_types.len == 8); -} - -test "ErrorContext: default initialization" { - const ctx = errors.ErrorContext{}; - try std.testing.expectEqual(@as(?[]const u8, null), ctx.argument_name); - try std.testing.expectEqual(@as(?[]const u8, null), ctx.invalid_value); - try std.testing.expectEqual(@as(?[]const u8, null), ctx.expected_type); - try std.testing.expectEqual(@as(?[]const u8, null), ctx.message); -} - -test "ErrorContext: with values" { - const ctx = errors.ErrorContext{ - .argument_name = "--verbose", - .invalid_value = "maybe", - .expected_type = "bool", - .message = "Invalid boolean value", - }; - - try std.testing.expectEqualStrings("--verbose", ctx.argument_name.?); - try std.testing.expectEqualStrings("maybe", ctx.invalid_value.?); - try std.testing.expectEqualStrings("bool", ctx.expected_type.?); - try std.testing.expectEqualStrings("Invalid boolean value", ctx.message.?); -} - -test "Result: ok value" { - const IntResult = errors.Result(u32); - const result = IntResult{ .ok = 42 }; - - try std.testing.expect(result.isOk()); - try std.testing.expect(!result.isErr()); - try std.testing.expectEqual(@as(u32, 42), result.unwrap()); -} - -test "Result: error value" { - const IntResult = errors.Result(u32); - const result = IntResult{ - .err = .{ - .error_type = error.InvalidIntegerValue, - .context = .{ - .argument_name = "--count", - .invalid_value = "abc", - }, - }, - }; - - try std.testing.expect(!result.isOk()); - try std.testing.expect(result.isErr()); - try std.testing.expectEqual(error.InvalidIntegerValue, result.err.error_type); - try std.testing.expectEqualStrings("--count", result.err.context.argument_name.?); -} - -test "Result: unwrapOr with ok" { - const IntResult = errors.Result(u32); - const result = IntResult{ .ok = 42 }; - const value = result.unwrapOr(100); - try std.testing.expectEqual(@as(u32, 42), value); -} - -test "Result: unwrapOr with error" { - const IntResult = errors.Result(u32); - const result = IntResult{ - .err = .{ - .error_type = error.InvalidValue, - .context = .{}, - }, - }; - const value = result.unwrapOr(100); - try std.testing.expectEqual(@as(u32, 100), value); -} - -test "Result: works with different types" { - { - const BoolResult = errors.Result(bool); - const result = BoolResult{ .ok = true }; - try std.testing.expect(result.unwrap()); - } - { - const StringResult = errors.Result([]const u8); - const result = StringResult{ .ok = "hello" }; - try std.testing.expectEqualStrings("hello", result.unwrap()); - } -} diff --git a/lib/zargs/tests/test_help.zig b/lib/zargs/tests/test_help.zig deleted file mode 100644 index 669fb65..0000000 --- a/lib/zargs/tests/test_help.zig +++ /dev/null @@ -1,293 +0,0 @@ -const std = @import("std"); -const help = @import("help"); -const ArgumentRegistry = @import("ArgumentRegistry").ArgumentRegistry; -const metadata = @import("metadata"); -const ArgumentType = @import("ArgumentType").ArgumentType; - -const SimpleConfig = struct { - verbose: bool = false, - output: []const u8 = "output.txt", - count: u32 = 10, - - pub const meta = .{ - .verbose = .{ .short = 'v', .help = "Enable verbose output" }, - .output = .{ .short = 'o', .help = "Output file path" }, - .count = .{ .short = 'c', .help = "Number of items to process" }, - }; -}; - -test "generate help text basic" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const help_text = try help.generateSimpleHelp(®istry, std.testing.allocator); - defer std.testing.allocator.free(help_text); - - // Should contain usage line - try std.testing.expect(std.mem.indexOf(u8, help_text, "Usage:") != null); - - // Should contain Options header - try std.testing.expect(std.mem.indexOf(u8, help_text, "Options:") != null); - - // Should contain help flag - try std.testing.expect(std.mem.indexOf(u8, help_text, "--help") != null); - try std.testing.expect(std.mem.indexOf(u8, help_text, "-h") != null); -} - -test "generate help text with all arguments" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const help_text = try help.generateSimpleHelp(®istry, std.testing.allocator); - defer std.testing.allocator.free(help_text); - - // Should contain all argument names - try std.testing.expect(std.mem.indexOf(u8, help_text, "--verbose") != null); - try std.testing.expect(std.mem.indexOf(u8, help_text, "--output") != null); - try std.testing.expect(std.mem.indexOf(u8, help_text, "--count") != null); - - // Should contain short flags - try std.testing.expect(std.mem.indexOf(u8, help_text, "-v") != null); - try std.testing.expect(std.mem.indexOf(u8, help_text, "-o") != null); - try std.testing.expect(std.mem.indexOf(u8, help_text, "-c") != null); -} - -test "generate help text with help descriptions" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const help_text = try help.generateSimpleHelp(®istry, std.testing.allocator); - defer std.testing.allocator.free(help_text); - - // Should contain help text for each argument - try std.testing.expect(std.mem.indexOf(u8, help_text, "Enable verbose output") != null); - try std.testing.expect(std.mem.indexOf(u8, help_text, "Output file path") != null); - try std.testing.expect(std.mem.indexOf(u8, help_text, "Number of items to process") != null); -} - -test "generate help text with default values" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const help_text = try help.generateSimpleHelp(®istry, std.testing.allocator); - defer std.testing.allocator.free(help_text); - - // Should show default values - try std.testing.expect(std.mem.indexOf(u8, help_text, "[default: false]") != null); - try std.testing.expect(std.mem.indexOf(u8, help_text, "[default: output.txt]") != null); - // Note: integer defaults are disabled, so count won't show default -} - -test "generate help text with value placeholders" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const help_text = try help.generateSimpleHelp(®istry, std.testing.allocator); - defer std.testing.allocator.free(help_text); - - // Boolean should not have placeholder - const verbose_line_start = std.mem.indexOf(u8, help_text, "-v, --verbose").?; - const verbose_line_end = std.mem.indexOfPos(u8, help_text, verbose_line_start, "\n").?; - const verbose_line = help_text[verbose_line_start..verbose_line_end]; - try std.testing.expect(std.mem.indexOf(u8, verbose_line, "<") == null); - - // String should have placeholder - try std.testing.expect(std.mem.indexOf(u8, help_text, "--output ") != null); - - // Number should have placeholder - try std.testing.expect(std.mem.indexOf(u8, help_text, "--count ") != null); -} - -test "generate help text with program name" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const help_text = try help.generateHelpText(®istry, std.testing.allocator, "myprogram"); - defer std.testing.allocator.free(help_text); - - // Should contain program name in usage line - try std.testing.expect(std.mem.indexOf(u8, help_text, "Usage: myprogram") != null); -} - -test "generate help text with enum choices" { - const Mode = enum { fast, slow, balanced }; - - const EnumConfig = struct { - mode: Mode = .balanced, - - pub const meta = .{ - .mode = .{ .short = 'm', .help = "Processing mode" }, - }; - }; - - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(EnumConfig, "test"); - - const help_text = try help.generateSimpleHelp(®istry, std.testing.allocator); - defer std.testing.allocator.free(help_text); - - // Should contain enum choices (if implemented) - // Note: enum values extraction is currently disabled due to comptime limitations - // This test documents the expected behavior -} - -test "generate help text alphabetical order" { - const UnorderedConfig = struct { - zebra: bool = false, - apple: bool = false, - middle: bool = false, - - pub const meta = .{ - .zebra = .{ .help = "Last" }, - .apple = .{ .help = "First" }, - .middle = .{ .help = "Middle" }, - }; - }; - - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(UnorderedConfig, "test"); - - const help_text = try help.generateSimpleHelp(®istry, std.testing.allocator); - defer std.testing.allocator.free(help_text); - - // Find positions of each argument - const apple_pos = std.mem.indexOf(u8, help_text, "--apple").?; - const middle_pos = std.mem.indexOf(u8, help_text, "--middle").?; - const zebra_pos = std.mem.indexOf(u8, help_text, "--zebra").?; - - // Should be in alphabetical order - try std.testing.expect(apple_pos < middle_pos); - try std.testing.expect(middle_pos < zebra_pos); -} - -test "generate help text with optional fields" { - const OptionalConfig = struct { - name: ?[]const u8 = null, - age: ?u32 = null, - - pub const meta = .{ - .name = .{ .help = "Optional name" }, - .age = .{ .help = "Optional age" }, - }; - }; - - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(OptionalConfig, "test"); - - const help_text = try help.generateSimpleHelp(®istry, std.testing.allocator); - defer std.testing.allocator.free(help_text); - - // Should contain optional fields - try std.testing.expect(std.mem.indexOf(u8, help_text, "--name") != null); - try std.testing.expect(std.mem.indexOf(u8, help_text, "--age") != null); - - // Optional fields should not be marked as required - try std.testing.expect(std.mem.indexOf(u8, help_text, "(required)") == null); -} - -test "generate help text with string list" { - const ListConfig = struct { - files: []const []const u8 = &[_][]const u8{}, - - pub const meta = .{ - .files = .{ .short = 'f', .help = "Input files" }, - }; - }; - - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(ListConfig, "test"); - - const help_text = try help.generateSimpleHelp(®istry, std.testing.allocator); - defer std.testing.allocator.free(help_text); - - // Should have placeholder for string list - try std.testing.expect(std.mem.indexOf(u8, help_text, "--files ") != null); -} - -test "generate help text alignment" { - const VaryingLengthConfig = struct { - a: bool = false, - very_long_argument_name: bool = false, - mid: bool = false, - - pub const meta = .{ - .a = .{ .help = "Short name" }, - .very_long_argument_name = .{ .help = "Long name" }, - .mid = .{ .help = "Medium name" }, - }; - }; - - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(VaryingLengthConfig, "test"); - - const help_text = try help.generateSimpleHelp(®istry, std.testing.allocator); - defer std.testing.allocator.free(help_text); - - // Parse lines and check that help text starts at a consistent column - var lines = std.mem.splitSequence(u8, help_text, "\n"); - var help_text_columns = std.ArrayListUnmanaged(usize){}; - defer help_text_columns.deinit(std.testing.allocator); - - while (lines.next()) |line| { - // Skip header lines - if (std.mem.indexOf(u8, line, "--") == null) continue; - - // Find where the help text starts (after the argument name) - if (std.mem.indexOf(u8, line, "Short name")) |pos| { - try help_text_columns.append(std.testing.allocator, pos); - } else if (std.mem.indexOf(u8, line, "Long name")) |pos| { - try help_text_columns.append(std.testing.allocator, pos); - } else if (std.mem.indexOf(u8, line, "Medium name")) |pos| { - try help_text_columns.append(std.testing.allocator, pos); - } - } - - // All help text should start at the same column (within reason) - if (help_text_columns.items.len >= 2) { - const first_col = help_text_columns.items[0]; - for (help_text_columns.items[1..]) |col| { - // Allow some variation due to spacing, but should be close - const diff = if (col > first_col) col - first_col else first_col - col; - try std.testing.expect(diff < 5); - } - } -} - -test "generate help with no arguments" { - const EmptyConfig = struct {}; - - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(EmptyConfig, "test"); - - const help_text = try help.generateSimpleHelp(®istry, std.testing.allocator); - defer std.testing.allocator.free(help_text); - - // Should still have basic structure - try std.testing.expect(std.mem.indexOf(u8, help_text, "Usage:") != null); - try std.testing.expect(std.mem.indexOf(u8, help_text, "Options:") != null); - try std.testing.expect(std.mem.indexOf(u8, help_text, "--help") != null); -} diff --git a/lib/zargs/tests/test_metadata.zig b/lib/zargs/tests/test_metadata.zig deleted file mode 100644 index 0c7e1fc..0000000 --- a/lib/zargs/tests/test_metadata.zig +++ /dev/null @@ -1,474 +0,0 @@ -const std = @import("std"); -const metadata = @import("metadata"); -const ArgumentTypeModule = @import("ArgumentType"); -const ArgumentType = ArgumentTypeModule.ArgumentType; - -test "ArgumentMetadata: basic initialization" { - const arg = metadata.ArgumentMetadata{ - .field_name = "verbose", - .arg_name = "verbose", - .arg_type = .bool, - }; - - try std.testing.expectEqualStrings("verbose", arg.field_name); - try std.testing.expectEqualStrings("verbose", arg.arg_name); - try std.testing.expectEqual(ArgumentType.bool, arg.arg_type); - try std.testing.expectEqual(@as(?u8, null), arg.short); - try std.testing.expectEqualStrings("", arg.help); - try std.testing.expectEqual(false, arg.required); -} - -test "ArgumentMetadata: with all fields" { - const arg = metadata.ArgumentMetadata{ - .field_name = "output_file", - .arg_name = "output-file", - .arg_type = .string, - .short = 'o', - .help = "Output file path", - .required = true, - .default_value = "output.txt", - .is_optional = false, - }; - - try std.testing.expectEqualStrings("output_file", arg.field_name); - try std.testing.expectEqualStrings("output-file", arg.arg_name); - try std.testing.expectEqual(ArgumentType.string, arg.arg_type); - try std.testing.expectEqual(@as(?u8, 'o'), arg.short); - try std.testing.expectEqualStrings("Output file path", arg.help); - try std.testing.expectEqual(true, arg.required); - try std.testing.expectEqualStrings("output.txt", arg.default_value.?); - try std.testing.expectEqual(false, arg.is_optional); -} - -test "ArgumentMetadata: enum with values" { - const enum_values = [_][]const u8{ "debug", "info", "warn", "error" }; - const arg = metadata.ArgumentMetadata{ - .field_name = "logLevel", - .arg_name = "log-level", - .arg_type = .enum_type, - .enum_values = &enum_values, - .default_value = "info", - }; - - try std.testing.expectEqual(ArgumentType.enum_type, arg.arg_type); - try std.testing.expectEqual(@as(usize, 4), arg.enum_values.len); - try std.testing.expectEqualStrings("debug", arg.enum_values[0]); - try std.testing.expectEqualStrings("error", arg.enum_values[3]); -} - -test "FieldMeta: default initialization" { - const meta = metadata.FieldMeta{}; - - try std.testing.expectEqual(@as(?[]const u8, null), meta.name); - try std.testing.expectEqual(@as(?u8, null), meta.short); - try std.testing.expectEqual(@as(?[]const u8, null), meta.help); - try std.testing.expectEqual(@as(?bool, null), meta.required); -} - -test "FieldMeta: with values" { - const meta = metadata.FieldMeta{ - .name = "custom-name", - .short = 'c', - .help = "Custom help text", - .required = true, - }; - - try std.testing.expectEqualStrings("custom-name", meta.name.?); - try std.testing.expectEqual(@as(u8, 'c'), meta.short.?); - try std.testing.expectEqualStrings("Custom help text", meta.help.?); - try std.testing.expectEqual(true, meta.required.?); -} - -test "ModuleInfo: basic initialization" { - const args = [_]metadata.ArgumentMetadata{}; - const info = metadata.ModuleInfo{ - .program_name = "myapp", - .arguments = &args, - }; - - try std.testing.expectEqualStrings("myapp", info.program_name); - try std.testing.expectEqualStrings("", info.description); - try std.testing.expectEqual(@as(usize, 0), info.arguments.len); - try std.testing.expectEqual(@as(?[]const u8, null), info.version); -} - -test "ModuleInfo: with full metadata" { - const args = [_]metadata.ArgumentMetadata{ - .{ - .field_name = "verbose", - .arg_name = "verbose", - .arg_type = .bool, - .short = 'v', - .help = "Enable verbose mode", - }, - }; - - const examples = [_][]const u8{ - "myapp --verbose", - "myapp -v --output file.txt", - }; - - const info = metadata.ModuleInfo{ - .program_name = "myapp", - .description = "A sample application", - .arguments = &args, - .version = "1.0.0", - .examples = &examples, - }; - - try std.testing.expectEqualStrings("myapp", info.program_name); - try std.testing.expectEqualStrings("A sample application", info.description); - try std.testing.expectEqual(@as(usize, 1), info.arguments.len); - try std.testing.expectEqualStrings("1.0.0", info.version.?); - try std.testing.expectEqual(@as(usize, 2), info.examples.len); - try std.testing.expectEqualStrings("myapp --verbose", info.examples[0]); -} - -test "hasMeta: struct without meta" { - const TestStruct = struct { - value: u32, - }; - - try std.testing.expect(!metadata.hasMeta(TestStruct)); -} - -test "hasMeta: struct with meta" { - const TestStruct = struct { - value: u32, - - pub const meta = .{ - .value = .{ .help = "A value" }, - }; - }; - - try std.testing.expect(metadata.hasMeta(TestStruct)); -} - -test "hasFieldMeta: field without meta" { - const TestStruct = struct { - value: u32, - other: bool, - - pub const meta = .{ - .value = .{ .help = "A value" }, - }; - }; - - try std.testing.expect(metadata.hasFieldMeta(TestStruct, "value")); - try std.testing.expect(!metadata.hasFieldMeta(TestStruct, "other")); -} - -test "getFieldMeta: field without meta returns default" { - const TestStruct = struct { - value: u32, - }; - - const meta = comptime metadata.getFieldMeta(TestStruct, "value"); - try std.testing.expectEqual(@as(?[]const u8, null), meta.name); - try std.testing.expectEqual(@as(?u8, null), meta.short); -} - -test "getFieldMeta: field with meta" { - const TestStruct = struct { - value: u32, - - pub const meta = .{ - .value = .{ - .name = "val", - .short = 'v', - .help = "A value", - .required = true, - }, - }; - }; - - const meta = comptime metadata.getFieldMeta(TestStruct, "value"); - try std.testing.expectEqualStrings("val", meta.name.?); - try std.testing.expectEqual(@as(u8, 'v'), meta.short.?); - try std.testing.expectEqualStrings("A value", meta.help.?); - try std.testing.expectEqual(true, meta.required.?); -} - -test "getFieldMeta: partial meta" { - const TestStruct = struct { - value: u32, - - pub const meta = .{ - .value = .{ - .help = "Just help text", - }, - }; - }; - - const meta = comptime metadata.getFieldMeta(TestStruct, "value"); - try std.testing.expectEqual(@as(?[]const u8, null), meta.name); - try std.testing.expectEqual(@as(?u8, null), meta.short); - try std.testing.expectEqualStrings("Just help text", meta.help.?); - try std.testing.expectEqual(@as(?bool, null), meta.required); -} - -test "hasModuleInfo: struct without module_info" { - const TestStruct = struct { - value: u32, - }; - - try std.testing.expect(!metadata.hasModuleInfo(TestStruct)); -} - -test "hasModuleInfo: struct with module_info" { - const TestStruct = struct { - value: u32, - - pub const module_info = .{ - .description = "Test program", - }; - }; - - try std.testing.expect(metadata.hasModuleInfo(TestStruct)); -} - -test "getModuleInfo: struct without module_info" { - const TestStruct = struct { - value: u32, - }; - - const info = comptime metadata.getModuleInfo(TestStruct, "test"); - try std.testing.expectEqualStrings("", info.description); - try std.testing.expectEqual(@as(?[]const u8, null), info.version); - try std.testing.expectEqual(@as(usize, 0), info.examples.len); -} - -test "getModuleInfo: struct with full module_info" { - const examples = [_][]const u8{ "example 1", "example 2" }; - - const TestStruct = struct { - value: u32, - - pub const module_info = .{ - .description = "A test program", - .version = "1.2.3", - .examples = &examples, - }; - }; - - const info = comptime metadata.getModuleInfo(TestStruct, "test"); - try std.testing.expectEqualStrings("A test program", info.description); - try std.testing.expectEqualStrings("1.2.3", info.version.?); - try std.testing.expectEqual(@as(usize, 2), info.examples.len); -} - -// ============================================================================ -// Metadata Extraction Tests -// ============================================================================ - -test "extractFieldMetadata: simple bool field" { - const TestStruct = struct { - verbose: bool, - }; - - const fields = @typeInfo(TestStruct).@"struct".fields; - const meta = comptime metadata.extractFieldMetadata(TestStruct, fields[0]); - - try std.testing.expectEqualStrings("verbose", meta.field_name); - try std.testing.expectEqualStrings("verbose", meta.arg_name); - try std.testing.expectEqual(ArgumentType.bool, meta.arg_type); - try std.testing.expectEqual(false, meta.is_optional); - try std.testing.expectEqual(true, meta.required); -} - -test "extractFieldMetadata: camelCase to kebab-case" { - const TestStruct = struct { - outputFile: []const u8, - }; - - const fields = @typeInfo(TestStruct).@"struct".fields; - const meta = comptime metadata.extractFieldMetadata(TestStruct, fields[0]); - - try std.testing.expectEqualStrings("outputFile", meta.field_name); - // TODO: Kebab-case conversion disabled for now - try std.testing.expectEqualStrings("outputFile", meta.arg_name); - try std.testing.expectEqual(ArgumentType.string, meta.arg_type); -} - -test "extractFieldMetadata: optional field" { - const TestStruct = struct { - count: ?u32, - }; - - const fields = @typeInfo(TestStruct).@"struct".fields; - const meta = comptime metadata.extractFieldMetadata(TestStruct, fields[0]); - - try std.testing.expectEqual(ArgumentType.u32, meta.arg_type); - try std.testing.expectEqual(true, meta.is_optional); - try std.testing.expectEqual(false, meta.required); -} - -test "extractFieldMetadata: with user metadata" { - const TestStruct = struct { - verbose: bool, - - pub const meta = .{ - .verbose = .{ - .name = "loud", - .short = 'l', - .help = "Be loud", - .required = true, - }, - }; - }; - - const fields = @typeInfo(TestStruct).@"struct".fields; - const meta = comptime metadata.extractFieldMetadata(TestStruct, fields[0]); - - try std.testing.expectEqualStrings("verbose", meta.field_name); - try std.testing.expectEqualStrings("loud", meta.arg_name); - try std.testing.expectEqual(@as(?u8, 'l'), meta.short); - try std.testing.expectEqualStrings("Be loud", meta.help); - try std.testing.expectEqual(true, meta.required); -} - -test "extractFieldMetadata: enum field" { - const LogLevel = enum { debug, info, warn, @"error" }; - - const TestStruct = struct { - logLevel: LogLevel, - }; - - const fields = @typeInfo(TestStruct).@"struct".fields; - const meta = comptime metadata.extractFieldMetadata(TestStruct, fields[0]); - - try std.testing.expectEqual(ArgumentType.enum_type, meta.arg_type); - // TODO: Re-enable when enum value extraction is fixed - // try std.testing.expectEqual(@as(usize, 4), meta.enum_values.len); - // try std.testing.expectEqualStrings("debug", meta.enum_values[0]); - // try std.testing.expectEqualStrings("info", meta.enum_values[1]); - // try std.testing.expectEqualStrings("warn", meta.enum_values[2]); - // try std.testing.expectEqualStrings("error", meta.enum_values[3]); -} - -test "extractFieldMetadata: with default value bool" { - const TestStruct = struct { - verbose: bool = false, - }; - - const fields = @typeInfo(TestStruct).@"struct".fields; - const meta = comptime metadata.extractFieldMetadata(TestStruct, fields[0]); - - try std.testing.expectEqualStrings("false", meta.default_value.?); -} - -test "extractFieldMetadata: with default value int" { - const TestStruct = struct { - count: u32 = 0, - }; - - const fields = @typeInfo(TestStruct).@"struct".fields; - const meta = comptime metadata.extractFieldMetadata(TestStruct, fields[0]); - - // TODO: Integer default value formatting is disabled due to comptime limitations - try std.testing.expect(meta.default_value == null); -} - -test "extractFieldMetadata: with default value string" { - const TestStruct = struct { - name: []const u8 = "default", - }; - - const fields = @typeInfo(TestStruct).@"struct".fields; - const meta = comptime metadata.extractFieldMetadata(TestStruct, fields[0]); - - try std.testing.expectEqualStrings("default", meta.default_value.?); -} - -test "extractAllFieldMetadata: multiple fields" { - const TestStruct = struct { - verbose: bool, - count: u32, - output: []const u8, - }; - - const all_meta = comptime metadata.extractAllFieldMetadata(TestStruct); - - try std.testing.expectEqual(@as(usize, 3), all_meta.len); - try std.testing.expectEqualStrings("verbose", all_meta[0].field_name); - try std.testing.expectEqualStrings("count", all_meta[1].field_name); - try std.testing.expectEqualStrings("output", all_meta[2].field_name); -} - -test "extractAllFieldMetadata: with mixed metadata" { - const TestStruct = struct { - verbose: bool, - count: ?u32, - output: []const u8 = "out.txt", - - pub const meta = .{ - .verbose = .{ - .short = 'v', - .help = "Verbose output", - }, - .count = .{ - .help = "Number of items", - }, - }; - }; - - const all_meta = comptime metadata.extractAllFieldMetadata(TestStruct); - - try std.testing.expectEqual(@as(usize, 3), all_meta.len); - - // verbose - try std.testing.expectEqual(@as(?u8, 'v'), all_meta[0].short); - try std.testing.expectEqualStrings("Verbose output", all_meta[0].help); - try std.testing.expectEqual(true, all_meta[0].required); - - // count - try std.testing.expectEqual(@as(?u8, null), all_meta[1].short); - try std.testing.expectEqualStrings("Number of items", all_meta[1].help); - try std.testing.expectEqual(false, all_meta[1].required); // Optional - - // output - try std.testing.expectEqualStrings("out.txt", all_meta[2].default_value.?); -} - -test "buildModuleInfo: complete struct" { - const examples = [_][]const u8{"myapp --verbose"}; - - const TestStruct = struct { - verbose: bool, - count: u32 = 10, - - pub const module_info = .{ - .description = "Test application", - .version = "1.0.0", - .examples = &examples, - }; - - pub const meta = .{ - .verbose = .{ - .short = 'v', - .help = "Verbose mode", - }, - .count = .{ - .help = "Item count", - }, - }; - }; - - const info = comptime metadata.buildModuleInfo(TestStruct, "myapp"); - - try std.testing.expectEqualStrings("myapp", info.program_name); - try std.testing.expectEqualStrings("Test application", info.description); - try std.testing.expectEqualStrings("1.0.0", info.version.?); - try std.testing.expectEqual(@as(usize, 1), info.examples.len); - try std.testing.expectEqual(@as(usize, 2), info.arguments.len); - - // Check verbose argument - try std.testing.expectEqualStrings("verbose", info.arguments[0].field_name); - try std.testing.expectEqual(@as(?u8, 'v'), info.arguments[0].short); - try std.testing.expectEqualStrings("Verbose mode", info.arguments[0].help); - - // Check count argument - try std.testing.expectEqualStrings("count", info.arguments[1].field_name); - // TODO: Integer default value formatting is disabled due to comptime limitations - try std.testing.expect(info.arguments[1].default_value == null); -} diff --git a/lib/zargs/tests/test_parsed_value.zig b/lib/zargs/tests/test_parsed_value.zig deleted file mode 100644 index df49a63..0000000 --- a/lib/zargs/tests/test_parsed_value.zig +++ /dev/null @@ -1,177 +0,0 @@ -const std = @import("std"); -const ArgumentType = @import("ArgumentType"); -const ParsedValue = ArgumentType.ParsedValue; - -test "ParsedValue: parse boolean true variants" { - const test_cases = [_][]const u8{ "true", "TRUE", "True", "1", "yes", "YES", "on", "ON" }; - for (test_cases) |str| { - const parsed = try ParsedValue.fromString(.bool, str, std.testing.allocator); - try std.testing.expectEqual(true, parsed.bool); - } -} - -test "ParsedValue: parse boolean false variants" { - const test_cases = [_][]const u8{ "false", "FALSE", "False", "0", "no", "NO", "off", "OFF" }; - for (test_cases) |str| { - const parsed = try ParsedValue.fromString(.bool, str, std.testing.allocator); - try std.testing.expectEqual(false, parsed.bool); - } -} - -test "ParsedValue: parse boolean invalid" { - const result = ParsedValue.fromString(.bool, "maybe", std.testing.allocator); - try std.testing.expectError(error.InvalidValue, result); -} - -test "ParsedValue: parse unsigned integers" { - const parsed_u8 = try ParsedValue.fromString(.u8, "255", std.testing.allocator); - try std.testing.expectEqual(@as(u8, 255), parsed_u8.u8); - - const parsed_u16 = try ParsedValue.fromString(.u16, "65535", std.testing.allocator); - try std.testing.expectEqual(@as(u16, 65535), parsed_u16.u16); - - const parsed_u32 = try ParsedValue.fromString(.u32, "4294967295", std.testing.allocator); - try std.testing.expectEqual(@as(u32, 4294967295), parsed_u32.u32); - - const parsed_u64 = try ParsedValue.fromString(.u64, "18446744073709551615", std.testing.allocator); - try std.testing.expectEqual(@as(u64, 18446744073709551615), parsed_u64.u64); -} - -test "ParsedValue: parse signed integers" { - const parsed_i8 = try ParsedValue.fromString(.i8, "-128", std.testing.allocator); - try std.testing.expectEqual(@as(i8, -128), parsed_i8.i8); - - const parsed_i16 = try ParsedValue.fromString(.i16, "-32768", std.testing.allocator); - try std.testing.expectEqual(@as(i16, -32768), parsed_i16.i16); - - const parsed_i32 = try ParsedValue.fromString(.i32, "-2147483648", std.testing.allocator); - try std.testing.expectEqual(@as(i32, -2147483648), parsed_i32.i32); - - const parsed_i64 = try ParsedValue.fromString(.i64, "9223372036854775807", std.testing.allocator); - try std.testing.expectEqual(@as(i64, 9223372036854775807), parsed_i64.i64); -} - -test "ParsedValue: parse integers with hex prefix" { - const parsed = try ParsedValue.fromString(.u32, "0xFF", std.testing.allocator); - try std.testing.expectEqual(@as(u32, 255), parsed.u32); -} - -test "ParsedValue: parse integers with binary prefix" { - const parsed = try ParsedValue.fromString(.u8, "0b11111111", std.testing.allocator); - try std.testing.expectEqual(@as(u8, 255), parsed.u8); -} - -test "ParsedValue: parse integer overflow" { - const result = ParsedValue.fromString(.u8, "256", std.testing.allocator); - try std.testing.expectError(error.Overflow, result); -} - -test "ParsedValue: parse integer invalid" { - const result = ParsedValue.fromString(.i32, "not a number", std.testing.allocator); - try std.testing.expectError(error.InvalidCharacter, result); -} - -test "ParsedValue: parse string" { - const parsed = try ParsedValue.fromString(.string, "hello world", std.testing.allocator); - defer std.testing.allocator.free(parsed.string); - - try std.testing.expectEqualStrings("hello world", parsed.string); -} - -test "ParsedValue: parse empty string" { - const parsed = try ParsedValue.fromString(.string, "", std.testing.allocator); - defer std.testing.allocator.free(parsed.string); - - try std.testing.expectEqualStrings("", parsed.string); -} - -test "ParsedValue: parse enum" { - const Color = enum { red, green, blue }; - - const parsed = try ParsedValue.parseEnum(Color, "green", std.testing.allocator); - defer std.testing.allocator.free(parsed.enum_type.name); - - try std.testing.expectEqualStrings("green", parsed.enum_type.name); - try std.testing.expectEqual(@as(usize, 1), parsed.enum_type.value); -} - -test "ParsedValue: parse enum invalid" { - const Color = enum { red, green, blue }; - - const result = ParsedValue.parseEnum(Color, "yellow", std.testing.allocator); - try std.testing.expectError(error.InvalidValue, result); -} - -test "ParsedValue: toTypedValue bool" { - const parsed = ParsedValue{ .bool = true }; - const value = parsed.toTypedValue(bool); - try std.testing.expectEqual(true, value); -} - -test "ParsedValue: toTypedValue optional bool" { - const parsed = ParsedValue{ .bool = false }; - const value = parsed.toTypedValue(?bool); - try std.testing.expectEqual(@as(?bool, false), value); -} - -test "ParsedValue: toTypedValue integers" { - { - const parsed = ParsedValue{ .u32 = 42 }; - const value = parsed.toTypedValue(u32); - try std.testing.expectEqual(@as(u32, 42), value); - } - { - const parsed = ParsedValue{ .i64 = -999 }; - const value = parsed.toTypedValue(i64); - try std.testing.expectEqual(@as(i64, -999), value); - } -} - -test "ParsedValue: toTypedValue string" { - const parsed = ParsedValue{ .string = "test" }; - const value = parsed.toTypedValue([]const u8); - try std.testing.expectEqualStrings("test", value); -} - -test "ParsedValue: toTypedValue enum" { - const Color = enum { red, green, blue }; - - const parsed = ParsedValue{ - .enum_type = .{ - .name = "blue", - .value = 2, - }, - }; - const value = parsed.toTypedValue(Color); - try std.testing.expectEqual(Color.blue, value); -} - -test "ParsedValue: round-trip bool" { - const parsed = try ParsedValue.fromString(.bool, "true", std.testing.allocator); - const value = parsed.toTypedValue(bool); - try std.testing.expectEqual(true, value); -} - -test "ParsedValue: round-trip integer" { - const parsed = try ParsedValue.fromString(.u32, "12345", std.testing.allocator); - const value = parsed.toTypedValue(u32); - try std.testing.expectEqual(@as(u32, 12345), value); -} - -test "ParsedValue: round-trip string" { - const parsed = try ParsedValue.fromString(.string, "hello", std.testing.allocator); - defer std.testing.allocator.free(parsed.string); - - const value = parsed.toTypedValue([]const u8); - try std.testing.expectEqualStrings("hello", value); -} - -test "ParsedValue: round-trip enum" { - const LogLevel = enum { debug, info, warn, @"error" }; - - const parsed = try ParsedValue.parseEnum(LogLevel, "warn", std.testing.allocator); - defer std.testing.allocator.free(parsed.enum_type.name); - - const value = parsed.toTypedValue(LogLevel); - try std.testing.expectEqual(LogLevel.warn, value); -} diff --git a/lib/zargs/tests/test_parsing.zig b/lib/zargs/tests/test_parsing.zig deleted file mode 100644 index e7ed667..0000000 --- a/lib/zargs/tests/test_parsing.zig +++ /dev/null @@ -1,358 +0,0 @@ -const std = @import("std"); -const parsing = @import("parsing"); -const ArgumentRegistry = @import("ArgumentRegistry").ArgumentRegistry; -const metadata = @import("metadata"); -const ArgumentType = @import("ArgumentType").ArgumentType; - -// Test struct for parsing -const SimpleConfig = struct { - verbose: bool = false, - output: []const u8 = "default.txt", - count: u32 = 10, - - pub const meta = .{ - .verbose = .{ .short = 'v', .help = "Verbose output" }, - .output = .{ .short = 'o', .help = "Output file" }, - .count = .{ .short = 'c', .help = "Item count" }, - }; -}; - -test "parse long boolean flag" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const argv = &[_][:0]const u8{ "program", "--verbose" }; - try parsing.parseArgv(®istry, argv); - - const value = registry.getParsedValue("verbose"); - try std.testing.expect(value != null); - try std.testing.expectEqual(true, value.?.bool); -} - -test "parse short boolean flag" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const argv = &[_][:0]const u8{ "program", "-v" }; - try parsing.parseArgv(®istry, argv); - - const value = registry.getParsedValue("verbose"); - try std.testing.expect(value != null); - try std.testing.expectEqual(true, value.?.bool); -} - -test "parse long flag with equals value" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const argv = &[_][:0]const u8{ "program", "--output=myfile.txt" }; - try parsing.parseArgv(®istry, argv); - - const value = registry.getParsedValue("output"); - try std.testing.expect(value != null); - try std.testing.expectEqualStrings("myfile.txt", value.?.string); -} - -test "parse long flag with space-separated value" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const argv = &[_][:0]const u8{ "program", "--output", "myfile.txt" }; - try parsing.parseArgv(®istry, argv); - - const value = registry.getParsedValue("output"); - try std.testing.expect(value != null); - try std.testing.expectEqualStrings("myfile.txt", value.?.string); -} - -test "parse short flag with value" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const argv = &[_][:0]const u8{ "program", "-o", "myfile.txt" }; - try parsing.parseArgv(®istry, argv); - - const value = registry.getParsedValue("output"); - try std.testing.expect(value != null); - try std.testing.expectEqualStrings("myfile.txt", value.?.string); -} - -test "parse integer value" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const argv = &[_][:0]const u8{ "program", "--count=42" }; - try parsing.parseArgv(®istry, argv); - - const value = registry.getParsedValue("count"); - try std.testing.expect(value != null); - try std.testing.expectEqual(@as(u32, 42), value.?.u32); -} - -test "parse multiple arguments" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const argv = &[_][:0]const u8{ "program", "-v", "--output", "test.txt", "--count=99" }; - try parsing.parseArgv(®istry, argv); - - const verbose = registry.getParsedValue("verbose"); - const output = registry.getParsedValue("output"); - const count = registry.getParsedValue("count"); - - try std.testing.expect(verbose != null); - try std.testing.expectEqual(true, verbose.?.bool); - - try std.testing.expect(output != null); - try std.testing.expectEqualStrings("test.txt", output.?.string); - - try std.testing.expect(count != null); - try std.testing.expectEqual(@as(u32, 99), count.?.u32); -} - -test "parse multi-flag short form" { - const MultiFlag = struct { - verbose: bool = false, - debug: bool = false, - quiet: bool = false, - - pub const meta = .{ - .verbose = .{ .short = 'v' }, - .debug = .{ .short = 'd' }, - .quiet = .{ .short = 'q' }, - }; - }; - - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(MultiFlag, "test"); - - const argv = &[_][:0]const u8{ "program", "-vdq" }; - try parsing.parseArgv(®istry, argv); - - const verbose = registry.getParsedValue("verbose"); - const debug = registry.getParsedValue("debug"); - const quiet = registry.getParsedValue("quiet"); - - try std.testing.expect(verbose != null); - try std.testing.expectEqual(true, verbose.?.bool); - try std.testing.expect(debug != null); - try std.testing.expectEqual(true, debug.?.bool); - try std.testing.expect(quiet != null); - try std.testing.expectEqual(true, quiet.?.bool); -} - -test "parse help flag" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const argv = &[_][:0]const u8{ "program", "--help" }; - try parsing.parseArgv(®istry, argv); - - try std.testing.expect(registry.isHelpRequested()); -} - -test "parse short help flag" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const argv = &[_][:0]const u8{ "program", "-h" }; - try parsing.parseArgv(®istry, argv); - - try std.testing.expect(registry.isHelpRequested()); -} - -test "unknown argument error" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const argv = &[_][:0]const u8{ "program", "--unknown" }; - const result = parsing.parseArgv(®istry, argv); - - try std.testing.expectError(error.UnknownArgument, result); -} - -test "missing value error" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const argv = &[_][:0]const u8{ "program", "--output" }; - const result = parsing.parseArgv(®istry, argv); - - try std.testing.expectError(error.MissingArgumentValue, result); -} - -test "populate struct with defaults" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const argv = &[_][:0]const u8{"program"}; - try parsing.parseArgv(®istry, argv); - - const config = try parsing.populateStruct(SimpleConfig, ®istry, std.testing.allocator); - - try std.testing.expectEqual(false, config.verbose); - try std.testing.expectEqualStrings("default.txt", config.output); - try std.testing.expectEqual(@as(u32, 10), config.count); -} - -test "populate struct with parsed values" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const argv = &[_][:0]const u8{ "program", "-v", "--output=result.txt", "--count=5" }; - try parsing.parseArgv(®istry, argv); - - const config = try parsing.populateStruct(SimpleConfig, ®istry, std.testing.allocator); - - try std.testing.expectEqual(true, config.verbose); - try std.testing.expectEqualStrings("result.txt", config.output); - try std.testing.expectEqual(@as(u32, 5), config.count); -} - -test "populate struct with mixed defaults and values" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(SimpleConfig, "test"); - - const argv = &[_][:0]const u8{ "program", "-v" }; - try parsing.parseArgv(®istry, argv); - - const config = try parsing.populateStruct(SimpleConfig, ®istry, std.testing.allocator); - - try std.testing.expectEqual(true, config.verbose); - try std.testing.expectEqualStrings("default.txt", config.output); - try std.testing.expectEqual(@as(u32, 10), config.count); -} - -test "parse enum values" { - const Mode = enum { fast, slow, medium }; - - const EnumConfig = struct { - mode: Mode = .medium, - - pub const meta = .{ - .mode = .{ .help = "Processing mode" }, - }; - }; - - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(EnumConfig, "test"); - - const argv = &[_][:0]const u8{ "program", "--mode=fast" }; - try parsing.parseArgv(®istry, argv); - - const config = try parsing.populateStruct(EnumConfig, ®istry, std.testing.allocator); - - try std.testing.expectEqual(Mode.fast, config.mode); -} - -test "parse optional types" { - const OptionalConfig = struct { - name: ?[]const u8 = null, - age: ?u32 = null, - - pub const meta = .{ - .name = .{ .help = "Optional name" }, - .age = .{ .help = "Optional age" }, - }; - }; - - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(OptionalConfig, "test"); - - // Test with values - { - const argv = &[_][:0]const u8{ "program", "--name=Alice", "--age=30" }; - try parsing.parseArgv(®istry, argv); - - const config = try parsing.populateStruct(OptionalConfig, ®istry, std.testing.allocator); - - try std.testing.expect(config.name != null); - try std.testing.expectEqualStrings("Alice", config.name.?); - try std.testing.expect(config.age != null); - try std.testing.expectEqual(@as(u32, 30), config.age.?); - } -} - -test "parse string list with comma separation" { - const ListConfig = struct { - files: []const []const u8 = &[_][]const u8{}, - - pub const meta = .{ - .files = .{ .help = "List of files" }, - }; - }; - - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(ListConfig, "test"); - - const argv = &[_][:0]const u8{ "program", "--files=a.txt,b.txt,c.txt" }; - try parsing.parseArgv(®istry, argv); - - const value = registry.getParsedValue("files"); - try std.testing.expect(value != null); - try std.testing.expectEqual(@as(usize, 3), value.?.string_list.len); - try std.testing.expectEqualStrings("a.txt", value.?.string_list[0]); - try std.testing.expectEqualStrings("b.txt", value.?.string_list[1]); - try std.testing.expectEqualStrings("c.txt", value.?.string_list[2]); -} - -test "parse string list with repeated arguments" { - const ListConfig = struct { - files: []const []const u8 = &[_][]const u8{}, - - pub const meta = .{ - .files = .{ .help = "List of files" }, - }; - }; - - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.registerMetadata(ListConfig, "test"); - - const argv = &[_][:0]const u8{ "program", "--files=a.txt", "--files=b.txt", "--files=c.txt" }; - try parsing.parseArgv(®istry, argv); - - const value = registry.getParsedValue("files"); - try std.testing.expect(value != null); - try std.testing.expectEqual(@as(usize, 3), value.?.string_list.len); - try std.testing.expectEqualStrings("a.txt", value.?.string_list[0]); - try std.testing.expectEqualStrings("b.txt", value.?.string_list[1]); - try std.testing.expectEqualStrings("c.txt", value.?.string_list[2]); -} diff --git a/lib/zargs/tests/test_registry.zig b/lib/zargs/tests/test_registry.zig deleted file mode 100644 index 304d22f..0000000 --- a/lib/zargs/tests/test_registry.zig +++ /dev/null @@ -1,496 +0,0 @@ -const std = @import("std"); -const RegistryModule = @import("ArgumentRegistry"); -const ArgumentRegistry = RegistryModule.ArgumentRegistry; -const metadata = @import("metadata"); -const ArgumentTypeModule = @import("ArgumentType"); -const ArgumentType = ArgumentTypeModule.ArgumentType; -const ParsedValue = ArgumentTypeModule.ParsedValue; - -test "ArgumentRegistry: init and deinit" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - // Registry should be initialized with empty maps - try std.testing.expectEqual(@as(usize, 0), registry.arguments.count()); - try std.testing.expectEqual(@as(usize, 0), registry.modules_by_arg.count()); - try std.testing.expectEqual(@as(usize, 0), registry.registered_types.count()); - try std.testing.expectEqual(@as(usize, 0), registry.parsed_values.count()); -} - -test "ArgumentRegistry: deinit cleans up memory" { - var registry = ArgumentRegistry.init(std.testing.allocator); - - // Add some data - try registry.registered_types.put("TestType", {}); - - var list = std.ArrayListUnmanaged([]const u8){}; - try list.append(std.testing.allocator, "module1"); - try registry.modules_by_arg.put("test-arg", list); - - // This should not leak - registry.deinit(); -} - -test "ArgumentRegistry: isTypeRegistered" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - const TestStruct = struct { value: u32 }; - const OtherStruct = struct { other: bool }; - - try std.testing.expect(!registry.isTypeRegistered(TestStruct)); - try std.testing.expect(!registry.isTypeRegistered(OtherStruct)); -} - -test "ArgumentRegistry: markTypeRegistered" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - const TestStruct = struct { value: u32 }; - - try std.testing.expect(!registry.isTypeRegistered(TestStruct)); - - try registry.markTypeRegistered(TestStruct); - - try std.testing.expect(registry.isTypeRegistered(TestStruct)); -} - -test "ArgumentRegistry: markTypeRegistered multiple types" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - const TestStruct1 = struct { value: u32 }; - const TestStruct2 = struct { other: bool }; - - try registry.markTypeRegistered(TestStruct1); - try registry.markTypeRegistered(TestStruct2); - - try std.testing.expect(registry.isTypeRegistered(TestStruct1)); - try std.testing.expect(registry.isTypeRegistered(TestStruct2)); -} - -test "ArgumentRegistry: markTypeRegistered idempotent" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - const TestStruct = struct { value: u32 }; - - try registry.markTypeRegistered(TestStruct); - try registry.markTypeRegistered(TestStruct); // Should not error - - try std.testing.expect(registry.isTypeRegistered(TestStruct)); -} - -test "ArgumentRegistry: isHelpRequested default false" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try std.testing.expectEqual(false, registry.isHelpRequested()); -} - -test "ArgumentRegistry: isHelpRequested can be set" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - registry.help_requested = true; - try std.testing.expectEqual(true, registry.isHelpRequested()); -} - -test "ArgumentRegistry: getArgument with empty registry" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try std.testing.expectEqual(@as(?*const metadata.ArgumentMetadata, null), registry.getArgument("verbose")); -} - -test "ArgumentRegistry: getArgument after insertion" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - const arg_meta = metadata.ArgumentMetadata{ - .field_name = "verbose", - .arg_name = "verbose", - .arg_type = .bool, - .help = "Verbose output", - }; - - try registry.arguments.put("verbose", arg_meta); - - const found = registry.getArgument("verbose"); - try std.testing.expect(found != null); - try std.testing.expectEqualStrings("verbose", found.?.field_name); - try std.testing.expectEqualStrings("Verbose output", found.?.help); -} - -test "ArgumentRegistry: getModulesForArg empty" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try std.testing.expectEqual(@as(?std.ArrayListUnmanaged([]const u8), null), registry.getModulesForArg("test")); -} - -test "ArgumentRegistry: getModulesForArg with modules" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - var list = std.ArrayListUnmanaged([]const u8){}; - try list.append(std.testing.allocator, "module1"); - try list.append(std.testing.allocator, "module2"); - try registry.modules_by_arg.put("verbose", list); - - const found = registry.getModulesForArg("verbose"); - try std.testing.expect(found != null); - try std.testing.expectEqual(@as(usize, 2), found.?.items.len); -} - -test "ArgumentRegistry: getParsedValue empty" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try std.testing.expectEqual(@as(?ParsedValue, null), registry.getParsedValue("verbose")); -} - -test "ArgumentRegistry: storeParsedValue and retrieve" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - const value = ParsedValue{ .bool = true }; - try registry.storeParsedValue("verbose", value); - - const found = registry.getParsedValue("verbose"); - try std.testing.expect(found != null); - try std.testing.expectEqual(true, found.?.bool); -} - -test "ArgumentRegistry: storeParsedValue multiple values" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.storeParsedValue("verbose", .{ .bool = true }); - try registry.storeParsedValue("count", .{ .u32 = 42 }); - - const verbose = registry.getParsedValue("verbose"); - const count = registry.getParsedValue("count"); - - try std.testing.expect(verbose != null); - try std.testing.expect(count != null); - try std.testing.expectEqual(true, verbose.?.bool); - try std.testing.expectEqual(@as(u32, 42), count.?.u32); -} - -test "ArgumentRegistry: storeParsedValue overwrites" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try registry.storeParsedValue("count", .{ .u32 = 10 }); - try registry.storeParsedValue("count", .{ .u32 = 20 }); - - const found = registry.getParsedValue("count"); - try std.testing.expectEqual(@as(u32, 20), found.?.u32); -} - -test "ArgumentRegistry: deinit frees parsed string values" { - var registry = ArgumentRegistry.init(std.testing.allocator); - - const str = try std.testing.allocator.dupe(u8, "test string"); - const value = ParsedValue{ .string = str }; - try registry.storeParsedValue("name", value); - - // deinit should free the string - registry.deinit(); -} - -test "ArgumentRegistry: deinit frees parsed enum values" { - var registry = ArgumentRegistry.init(std.testing.allocator); - - const name = try std.testing.allocator.dupe(u8, "debug"); - const value = ParsedValue{ - .enum_type = .{ - .name = name, - .value = 0, - }, - }; - try registry.storeParsedValue("log-level", value); - - // deinit should free the enum name - registry.deinit(); -} - -test "ArgumentRegistry: multiple operations" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - const TestStruct = struct { verbose: bool, count: u32 }; - - // Mark type as registered - try registry.markTypeRegistered(TestStruct); - try std.testing.expect(registry.isTypeRegistered(TestStruct)); - - // Store some metadata - const arg_meta = metadata.ArgumentMetadata{ - .field_name = "verbose", - .arg_name = "verbose", - .arg_type = .bool, - }; - try registry.arguments.put("verbose", arg_meta); - - // Store a module list - var list = std.ArrayListUnmanaged([]const u8){}; - try list.append(std.testing.allocator, "TestModule"); - try registry.modules_by_arg.put("verbose", list); - - // Store a parsed value - try registry.storeParsedValue("verbose", .{ .bool = true }); - - // Verify everything - try std.testing.expect(registry.getArgument("verbose") != null); - try std.testing.expect(registry.getModulesForArg("verbose") != null); - try std.testing.expect(registry.getParsedValue("verbose") != null); -} - -// ============================================================================ -// Registration Tests -// ============================================================================ - -test "ArgumentRegistry: registerMetadata simple struct" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - const TestStruct = struct { - verbose: bool, - count: u32, - }; - - try registry.registerMetadata(TestStruct, "TestModule"); - - // Should have registered both arguments - try std.testing.expect(registry.hasArgument("verbose")); - try std.testing.expect(registry.hasArgument("count")); - try std.testing.expectEqual(@as(usize, 2), registry.argumentCount()); -} - -test "ArgumentRegistry: registerMetadata with short flags" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - const TestStruct = struct { - verbose: bool, - - pub const meta = .{ - .verbose = .{ - .short = 'v', - .help = "Verbose output", - }, - }; - }; - - try registry.registerMetadata(TestStruct, "TestModule"); - - // Should have registered both long and short forms - try std.testing.expect(registry.hasArgument("verbose")); - try std.testing.expect(registry.hasArgument("v")); -} - -test "ArgumentRegistry: registerMetadata with camelCase" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - const TestStruct = struct { - outputFile: []const u8, - }; - - try registry.registerMetadata(TestStruct, "TestModule"); - - // TODO: Field names aren't converted to kebab-case yet, using direct name - try std.testing.expect(registry.hasArgument("outputFile")); - try std.testing.expect(!registry.hasArgument("output-file")); -} - -test "ArgumentRegistry: registerMetadata skips duplicate type" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - const TestStruct = struct { - verbose: bool, - }; - - try registry.registerMetadata(TestStruct, "Module1"); - try registry.registerMetadata(TestStruct, "Module2"); // Should skip - - // Should only have one instance - try std.testing.expectEqual(@as(usize, 1), registry.argumentCount()); -} - -test "ArgumentRegistry: registerMetadata compatible collision" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - const Module1 = struct { - verbose: bool, - }; - - const Module2 = struct { - verbose: bool, - }; - - try registry.registerMetadata(Module1, "Module1"); - try registry.registerMetadata(Module2, "Module2"); - - // Both should register successfully (compatible types) - const arg = registry.getArgument("verbose"); - try std.testing.expect(arg != null); - try std.testing.expectEqual(ArgumentType.bool, arg.?.arg_type); - - // Both modules should be listed - const modules = registry.getModulesForArg("verbose"); - try std.testing.expect(modules != null); - try std.testing.expectEqual(@as(usize, 2), modules.?.items.len); -} - -test "ArgumentRegistry: registerMetadata incompatible collision" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - const Module1 = struct { - verbose: bool, - }; - - const Module2 = struct { - verbose: u32, // Different type! - }; - - try registry.registerMetadata(Module1, "Module1"); - - // Should fail with incompatible type error - try std.testing.expectError( - error.IncompatibleArgumentType, - registry.registerMetadata(Module2, "Module2") - ); -} - -test "ArgumentRegistry: registerMetadata short flag collision compatible" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - const Module1 = struct { - verbose: bool, - pub const meta = .{ - .verbose = .{ .short = 'v' }, - }; - }; - - const Module2 = struct { - validate: bool, - pub const meta = .{ - .validate = .{ .short = 'v' }, - }; - }; - - try registry.registerMetadata(Module1, "Module1"); - try registry.registerMetadata(Module2, "Module2"); - - // Both should work (same type) - try std.testing.expect(registry.hasArgument("verbose")); - try std.testing.expect(registry.hasArgument("validate")); - try std.testing.expect(registry.hasArgument("v")); -} - -test "ArgumentRegistry: registerMetadata short flag collision incompatible" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - const Module1 = struct { - verbose: bool, - pub const meta = .{ - .verbose = .{ .short = 'v' }, - }; - }; - - const Module2 = struct { - value: u32, - pub const meta = .{ - .value = .{ .short = 'v' }, - }; - }; - - try registry.registerMetadata(Module1, "Module1"); - - // Should fail due to incompatible short flag - try std.testing.expectError( - error.IncompatibleArgumentType, - registry.registerMetadata(Module2, "Module2") - ); -} - -test "ArgumentRegistry: registerMetadata with optional fields" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - const TestStruct = struct { - verbose: bool, - count: ?u32, - }; - - try registry.registerMetadata(TestStruct, "TestModule"); - - // Both should be registered - const verbose_arg = registry.getArgument("verbose"); - const count_arg = registry.getArgument("count"); - - try std.testing.expect(verbose_arg != null); - try std.testing.expect(count_arg != null); - - // verbose is required (non-optional) - try std.testing.expectEqual(true, verbose_arg.?.required); - - // count is not required (optional) - try std.testing.expectEqual(false, count_arg.?.required); -} - -test "ArgumentRegistry: registerMetadata with enum" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - const LogLevel = enum { debug, info, warn, @"error" }; - - const TestStruct = struct { - logLevel: LogLevel, - }; - - try registry.registerMetadata(TestStruct, "TestModule"); - - // TODO: Field names aren't converted to kebab-case yet, using direct name - const arg = registry.getArgument("logLevel"); - try std.testing.expect(arg != null); - try std.testing.expectEqual(ArgumentType.enum_type, arg.?.arg_type); - // TODO: Re-enable when enum value extraction is fixed - // try std.testing.expectEqual(@as(usize, 4), arg.?.enum_values.len); -} - -test "ArgumentRegistry: hasArgument" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try std.testing.expect(!registry.hasArgument("verbose")); - - const TestStruct = struct { verbose: bool }; - try registry.registerMetadata(TestStruct, "Module"); - - try std.testing.expect(registry.hasArgument("verbose")); -} - -test "ArgumentRegistry: argumentCount" { - var registry = ArgumentRegistry.init(std.testing.allocator); - defer registry.deinit(); - - try std.testing.expectEqual(@as(usize, 0), registry.argumentCount()); - - const TestStruct = struct { - verbose: bool, - count: u32, - output: []const u8, - }; - try registry.registerMetadata(TestStruct, "Module"); - - try std.testing.expectEqual(@as(usize, 3), registry.argumentCount()); -} diff --git a/lib/zargs/tests/test_utils.zig b/lib/zargs/tests/test_utils.zig deleted file mode 100644 index 071e357..0000000 --- a/lib/zargs/tests/test_utils.zig +++ /dev/null @@ -1,48 +0,0 @@ -const std = @import("std"); -const utils = @import("utils"); - -test "toKebabCase: basic camelCase" { - const result = comptime utils.toKebabCase("verboseMode"); - try std.testing.expectEqualStrings("verbose-mode", result); -} - -test "toKebabCase: snake_case" { - const result = comptime utils.toKebabCase("output_file"); - try std.testing.expectEqualStrings("output-file", result); -} - -test "toKebabCase: uppercase acronym" { - const result = comptime utils.toKebabCase("HTTPServer"); - try std.testing.expectEqualStrings("http-server", result); -} - -test "toKebabCase: mixed formats" { - const result = comptime utils.toKebabCase("parse_XMLFile"); - try std.testing.expectEqualStrings("parse-xml-file", result); -} - -test "toKebabCase: single word" { - const result = comptime utils.toKebabCase("verbose"); - try std.testing.expectEqualStrings("verbose", result); -} - -test "toKebabCase: already kebab-case" { - const result = comptime utils.toKebabCase("log-level"); - try std.testing.expectEqualStrings("log-level", result); -} - -test "toKebabCase: empty string" { - const result = comptime utils.toKebabCase(""); - try std.testing.expectEqualStrings("", result); -} - -test "toKebabCase: complex examples" { - { - const result = comptime utils.toKebabCase("maxConnectionsPerHost"); - try std.testing.expectEqualStrings("max-connections-per-host", result); - } - { - const result = comptime utils.toKebabCase("enableHTTPSRedirect"); - try std.testing.expectEqualStrings("enable-https-redirect", result); - } -} diff --git a/lib/zargs/tests/type_test.zig b/lib/zargs/tests/type_test.zig deleted file mode 100644 index 0854084..0000000 --- a/lib/zargs/tests/type_test.zig +++ /dev/null @@ -1,57 +0,0 @@ -const std = @import("std"); -const testing = std.testing; -const zargs = @import("zargs"); -const ArgumentType = zargs.ArgumentType; - -test "ArgumentType.fromZigType - bool" { - const t = ArgumentType.fromZigType(bool); - try testing.expectEqual(ArgumentType.bool, t); -} - -test "ArgumentType.fromZigType - unsigned integers" { - try testing.expectEqual(ArgumentType.u8, ArgumentType.fromZigType(u8)); - try testing.expectEqual(ArgumentType.u16, ArgumentType.fromZigType(u16)); - try testing.expectEqual(ArgumentType.u32, ArgumentType.fromZigType(u32)); - try testing.expectEqual(ArgumentType.u64, ArgumentType.fromZigType(u64)); -} - -test "ArgumentType.fromZigType - signed integers" { - try testing.expectEqual(ArgumentType.i8, ArgumentType.fromZigType(i8)); - try testing.expectEqual(ArgumentType.i16, ArgumentType.fromZigType(i16)); - try testing.expectEqual(ArgumentType.i32, ArgumentType.fromZigType(i32)); - try testing.expectEqual(ArgumentType.i64, ArgumentType.fromZigType(i64)); -} - -test "ArgumentType.fromZigType - string" { - const t = ArgumentType.fromZigType([]const u8); - try testing.expectEqual(ArgumentType.string, t); -} - -test "ArgumentType.fromZigType - string list" { - const t = ArgumentType.fromZigType([]const []const u8); - try testing.expectEqual(ArgumentType.string_list, t); -} - -test "ArgumentType.fromZigType - enum" { - const TestEnum = enum { foo, bar }; - const t = ArgumentType.fromZigType(TestEnum); - try testing.expectEqual(ArgumentType.enum_type, t); -} - -test "ArgumentType.fromZigType - optional unwraps" { - try testing.expectEqual(ArgumentType.u32, ArgumentType.fromZigType(?u32)); - try testing.expectEqual(ArgumentType.bool, ArgumentType.fromZigType(?bool)); - try testing.expectEqual(ArgumentType.string, ArgumentType.fromZigType(?[]const u8)); -} - -test "ArgumentType.matches - same types match" { - try testing.expect(ArgumentType.u32.matches(ArgumentType.u32)); - try testing.expect(ArgumentType.bool.matches(ArgumentType.bool)); - try testing.expect(ArgumentType.string.matches(ArgumentType.string)); -} - -test "ArgumentType.matches - different types don't match" { - try testing.expect(!ArgumentType.u32.matches(ArgumentType.bool)); - try testing.expect(!ArgumentType.i32.matches(ArgumentType.u32)); - try testing.expect(!ArgumentType.string.matches(ArgumentType.string_list)); -} diff --git a/lib/zargs/todo/QUICK_START.md b/lib/zargs/todo/QUICK_START.md deleted file mode 100644 index 32133d7..0000000 --- a/lib/zargs/todo/QUICK_START.md +++ /dev/null @@ -1,399 +0,0 @@ -# Implementation Quick Start - -## Day 1 Morning: Setup - -### 1. Create Directory Structure (5 minutes) -```bash -cd /home/sear/Backlog/lib/zargs -mkdir -p src tests examples -``` - -### 2. Create Initial Files (5 minutes) -```bash -touch src/main.zig -touch src/ArgumentType.zig -touch src/errors.zig -touch tests/type_test.zig -touch build.zig -``` - -### 3. Setup build.zig (15 minutes) -```zig -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - // Library module - const zargs = b.addModule("zargs", .{ - .root_source_file = b.path("src/main.zig"), - }); - - // Tests - const tests = b.addTest(.{ - .root_source_file = b.path("tests/type_test.zig"), - .target = target, - .optimize = optimize, - }); - tests.root_module.addImport("zargs", zargs); - - const run_tests = b.addRunArtifact(tests); - const test_step = b.step("test", "Run tests"); - test_step.dependOn(&run_tests.step); -} -``` - -### 4. Verify Setup (2 minutes) -```bash -zig build test -# Should compile (no tests yet) -``` - ---- - -## Day 1 Afternoon: ArgumentType (Phase 1.1) - -### Step 1: Write Test First (30 minutes) -**File:** `tests/type_test.zig` - -```zig -const std = @import("std"); -const testing = std.testing; -const ArgumentType = @import("ArgumentType.zig").ArgumentType; - -test "ArgumentType.fromZigType - bool" { - const t = ArgumentType.fromZigType(bool); - try testing.expectEqual(.bool, t); -} - -test "ArgumentType.fromZigType - u32" { - const t = ArgumentType.fromZigType(u32); - try testing.expectEqual(.u32, t); -} - -test "ArgumentType.fromZigType - string" { - const t = ArgumentType.fromZigType([]const u8); - try testing.expectEqual(.string, t); -} - -test "ArgumentType.fromZigType - optional unwraps" { - const t = ArgumentType.fromZigType(?u32); - try testing.expectEqual(.u32, t); -} - -test "ArgumentType.matches - same types match" { - const t1 = ArgumentType.u32; - const t2 = ArgumentType.u32; - try testing.expect(t1.matches(t2)); -} - -test "ArgumentType.matches - different types don't match" { - const t1 = ArgumentType.u32; - const t2 = ArgumentType.bool; - try testing.expect(!t1.matches(t2)); -} -``` - -### Step 2: Implement ArgumentType (1.5 hours) -**File:** `src/ArgumentType.zig` - -```zig -const std = @import("std"); - -pub const ArgumentType = enum { - bool, - u8, u16, u32, u64, - i8, i16, i32, i64, - string, - string_list, - enum_type, - - /// Convert a Zig type to ArgumentType at compile time - pub fn fromZigType(comptime T: type) ArgumentType { - const info = @typeInfo(T); - - return switch (info) { - .Bool => .bool, - - .Int => |int| { - if (int.signedness == .unsigned) { - return switch (int.bits) { - 8 => .u8, - 16 => .u16, - 32 => .u32, - 64 => .u64, - else => @compileError("Unsupported unsigned int size: " ++ - @typeName(T)), - }; - } else { - return switch (int.bits) { - 8 => .i8, - 16 => .i16, - 32 => .i32, - 64 => .i64, - else => @compileError("Unsupported signed int size: " ++ - @typeName(T)), - }; - } - }, - - .Pointer => |ptr| { - if (ptr.size == .Slice) { - if (ptr.child == u8) return .string; - - // Check for []const []const u8 (string list) - const child_info = @typeInfo(ptr.child); - if (child_info == .Pointer) { - const inner_ptr = child_info.Pointer; - if (inner_ptr.size == .Slice and inner_ptr.child == u8) { - return .string_list; - } - } - } - - @compileError("Unsupported pointer type: " ++ @typeName(T)); - }, - - .Enum => .enum_type, - - .Optional => |opt| fromZigType(opt.child), - - else => @compileError("Unsupported argument type: " ++ @typeName(T)), - }; - } - - /// Check if two ArgumentTypes are compatible - pub fn matches(self: ArgumentType, other: ArgumentType) bool { - return self == other; - } -}; - -// Compile-time tests -comptime { - _ = ArgumentType.fromZigType(bool); - _ = ArgumentType.fromZigType(u32); - _ = ArgumentType.fromZigType([]const u8); - _ = ArgumentType.fromZigType(?u32); -} -``` - -### Step 3: Run Tests (5 minutes) -```bash -zig build test -# Should pass all tests -``` - -### Step 4: Update build.zig for ArgumentType (5 minutes) -Add ArgumentType to the tests: -```zig -tests.root_module.addAnonymousImport("ArgumentType", .{ - .root_source_file = b.path("src/ArgumentType.zig"), -}); -``` - ---- - -## Day 1 Success Criteria ✓ - -At end of Day 1, you should have: -- [ ] Project structure created -- [ ] build.zig working -- [ ] ArgumentType fully implemented -- [ ] All type detection tests passing -- [ ] Comptime tests verifying common types - -**Progress:** ~10% complete, on track! - ---- - -## Day 2 Morning: ParsedValue (Phase 1.2) - -### Step 1: Write Tests -Add to `tests/type_test.zig`: - -```zig -const ParsedValue = @import("ArgumentType.zig").ParsedValue; - -test "ParsedValue.fromString - bool true" { - const allocator = testing.allocator; - const pv = try ParsedValue.fromString(.bool, "true", allocator); - defer pv.deinit(allocator); - try testing.expectEqual(true, pv.bool_val); -} - -test "ParsedValue.fromString - u32" { - const allocator = testing.allocator; - const pv = try ParsedValue.fromString(.u32, "42", allocator); - defer pv.deinit(allocator); - try testing.expectEqual(@as(u32, 42), pv.u32_val); -} - -test "ParsedValue.toTypedValue - u32" { - const allocator = testing.allocator; - const pv = try ParsedValue.fromString(.u32, "42", allocator); - defer pv.deinit(allocator); - const val = pv.toTypedValue(u32); - try testing.expectEqual(@as(u32, 42), val); -} -``` - -### Step 2: Implement ParsedValue -Add to `src/ArgumentType.zig`: - -```zig -pub const ParsedValue = union(ArgumentType) { - bool: bool, - u8: u8, u16: u16, u32: u32, u64: u64, - i8: i8, i16: i16, i32: i32, i64: i64, - string: []const u8, - string_list: []const []const u8, - enum_type: []const u8, - - pub fn fromString( - arg_type: ArgumentType, - s: []const u8, - allocator: std.mem.Allocator, - ) !ParsedValue { - return switch (arg_type) { - .bool => .{ .bool = try parseBool(s) }, - .u8 => .{ .u8 = try std.fmt.parseInt(u8, s, 10) }, - .u16 => .{ .u16 = try std.fmt.parseInt(u16, s, 10) }, - .u32 => .{ .u32 = try std.fmt.parseInt(u32, s, 10) }, - .u64 => .{ .u64 = try std.fmt.parseInt(u64, s, 10) }, - .i8 => .{ .i8 = try std.fmt.parseInt(i8, s, 10) }, - .i16 => .{ .i16 = try std.fmt.parseInt(i16, s, 10) }, - .i32 => .{ .i32 = try std.fmt.parseInt(i32, s, 10) }, - .i64 => .{ .i64 = try std.fmt.parseInt(i64, s, 10) }, - .string => .{ .string = try allocator.dupe(u8, s) }, - .string_list => .{ .string_list = try parseList(s, allocator) }, - .enum_type => .{ .enum_type = try allocator.dupe(u8, s) }, - }; - } - - pub fn toTypedValue(self: ParsedValue, comptime T: type) T { - const arg_type = ArgumentType.fromZigType(T); - return switch (arg_type) { - .bool => self.bool, - .u8 => self.u8, - .u16 => self.u16, - .u32 => self.u32, - .u64 => self.u64, - .i8 => self.i8, - .i16 => self.i16, - .i32 => self.i32, - .i64 => self.i64, - .string => self.string, - .string_list => self.string_list, - .enum_type => { - // For enums, need to convert string to enum at runtime - // This is a placeholder - full implementation in Phase 4 - @compileError("Enum conversion not yet implemented"); - }, - }; - } - - pub fn deinit(self: ParsedValue, allocator: std.mem.Allocator) void { - switch (self) { - .string => |s| allocator.free(s), - .string_list => |list| { - for (list) |item| allocator.free(item); - allocator.free(list); - }, - .enum_type => |s| allocator.free(s), - else => {}, - } - } -}; - -fn parseBool(s: []const u8) !bool { - if (std.mem.eql(u8, s, "true") or std.mem.eql(u8, s, "1") or - std.mem.eql(u8, s, "yes")) { - return true; - } else if (std.mem.eql(u8, s, "false") or std.mem.eql(u8, s, "0") or - std.mem.eql(u8, s, "no")) { - return false; - } - return error.InvalidBooleanValue; -} - -fn parseList(s: []const u8, allocator: std.mem.Allocator) ![]const []const u8 { - var list = std.ArrayList([]const u8).init(allocator); - errdefer { - for (list.items) |item| allocator.free(item); - list.deinit(); - } - - var iter = std.mem.splitScalar(u8, s, ','); - while (iter.next()) |item| { - const trimmed = std.mem.trim(u8, item, " \t"); - try list.append(try allocator.dupe(u8, trimmed)); - } - - return try list.toOwnedSlice(); -} -``` - -### Step 3: Run Tests -```bash -zig build test -``` - ---- - -## Momentum Tips - -### Keep Moving Forward: -1. **If stuck > 30 minutes:** Skip to next task, come back later -2. **If test fails:** Debug immediately, don't move on -3. **If design unclear:** Implement simplest version, refactor later -4. **Commit often:** After each green test - -### Daily Review (15 minutes EOD): -- What did I accomplish? -- What's blocking me? -- What's tomorrow's priority? - -### Weekly Review (30 minutes Friday): -- Am I on schedule? -- Do I need to adjust the plan? -- What did I learn? - ---- - -## Common Issues and Solutions - -### Issue: Comptime too complex -**Solution:** Move to runtime, optimize later - -### Issue: Memory leaks in tests -**Solution:** Add `defer` immediately after allocation - -### Issue: Type conversion not working -**Solution:** Check ArgumentType.fromZigType() logic - -### Issue: Tests not compiling -**Solution:** Check imports and build.zig configuration - ---- - -## Morale Boosters - -- ✅ Each passing test is progress! -- ✅ Small commits compound into big features -- ✅ Taking breaks prevents burnout -- ✅ Asking for help is strength, not weakness -- ✅ Perfect is the enemy of done - ship it! - -**You've got this!** 💪 - ---- - -## Contact/Support - -- Review design docs in `research/` when unsure -- Check `todo/implementation_plan_v2.md` for detailed steps -- Run `zig build test` frequently -- Trust the process - you planned well! - -**START WITH DAY 1 MORNING. BUILD INCREMENTALLY. TEST EVERYTHING.** 🚀 diff --git a/lib/zargs/todo/READINESS_CHECKLIST.md b/lib/zargs/todo/READINESS_CHECKLIST.md deleted file mode 100644 index dd4d562..0000000 --- a/lib/zargs/todo/READINESS_CHECKLIST.md +++ /dev/null @@ -1,236 +0,0 @@ -# Implementation Readiness Checklist - -## Design Completeness ✅ - -- [x] Core architecture defined -- [x] All requirements documented -- [x] Edge cases considered -- [x] Memory model defined -- [x] Error handling strategy defined -- [x] Testing strategy defined -- [x] Build system planned - -## Plan Quality ✅ - -- [x] Broken into manageable phases -- [x] Each phase has clear deliverables -- [x] Dependencies between phases identified -- [x] Estimated timeline reasonable (5 weeks) -- [x] Test-driven development emphasized -- [x] Go/no-go decision points defined -- [x] Success criteria defined - -## Technical Clarity ✅ - -- [x] Type system design complete -- [x] Metadata extraction approach clear -- [x] Parsing strategy defined -- [x] Help generation approach clear -- [x] Memory ownership model documented -- [x] String handling strategy defined -- [x] Collision detection logic specified - -## Risk Management ✅ - -- [x] Risks identified and prioritized -- [x] Mitigation strategies defined -- [x] Critical path identified -- [x] Incremental approach enables early feedback -- [x] Open questions documented (deferred to v2) - -## Missing Items ❌ → ✅ - -- [x] String handling strategy (ADDED in v2) -- [x] Error types definition (ADDED in v2) -- [x] kebab-case conversion (ADDED in v2) -- [x] List parsing details (CLARIFIED in v2) -- [x] argv ownership (CLARIFIED in v2) -- [x] Optional field handling (CLARIFIED in v2) - -## Confidence Assessment - -**Implementation Plan v2 Confidence: 95%** - -### Strong Points: -1. ✅ Comprehensive phase breakdown -2. ✅ TDD approach integrated throughout -3. ✅ Memory model clearly defined -4. ✅ All edge cases considered -5. ✅ Realistic timeline with buffers -6. ✅ Clear success criteria - -### Remaining Unknowns (acceptable): -1. ⚠️ Exact comptime complexity - will discover during implementation -2. ⚠️ Performance characteristics - will measure during Phase 10 -3. ⚠️ Integration friction - will discover during Phase 9 - -### Mitigation for Unknowns: -- Build incrementally -- Test each phase thoroughly before proceeding -- Go/no-go decision points allow course correction -- Arena allocator simplifies memory management -- Focus on simple, working implementation first - -## Recommendation: **PROCEED WITH IMPLEMENTATION** ✅ - -The plan is: -- **Complete** - All requirements covered -- **Realistic** - Timeline accounts for complexity -- **Testable** - TDD approach throughout -- **Safe** - Memory model clear, error handling defined -- **Flexible** - Decision points allow adjustments - -## Next Steps - -1. **Immediate:** Create directory structure - ``` - mkdir -p src tests examples - touch src/main.zig - ``` - -2. **Day 1:** Start Phase 1.1 - ArgumentType implementation - - Write tests first - - Implement enum - - Implement fromZigType() - - Verify all types handled - -3. **Daily:** Follow TDD workflow - - Test → Implement → Refactor → Commit - -4. **Weekly:** Review progress - - Are we on track? - - Any design changes needed? - - Update plan if necessary - -## Final Sanity Checks - -- [ ] Can we implement ArgumentType in 1 day? **YES** - straightforward enum -- [ ] Can we extract metadata at comptime? **YES** - @typeInfo is powerful -- [ ] Can we handle string ownership? **YES** - arena allocator -- [ ] Can we detect type collisions? **YES** - string comparison + type check -- [ ] Can we format help text? **YES** - string formatting is well-understood -- [ ] Will it integrate with Backlog? **YES** - designed for this use case -- [ ] Is 5 weeks reasonable? **YES** - ~25 working days, includes buffer - -**All checks passed. Ready to build! 🎯** - ---- - -## Implementation Priorities (if time pressure) - -### Must-Have (Core MVP): -1. Type system (ArgumentType, ParsedValue) -2. Metadata extraction (basic, no doc comments) -3. Argument parsing (long-form only) -4. Struct reconstruction -5. Basic help generation -6. Collision detection (error on any collision) - -### Should-Have (Full v1): -7. Short-form arguments (-s) -8. List support (comma-separated) -9. Compatible collision handling (with warnings) -10. Pretty help formatting -11. Comprehensive tests -12. Documentation - -### Nice-to-Have (Polish): -13. Help text persistence example -14. Performance optimization -15. Help text alignment -16. Doc comment extraction -17. Multiple list syntax support - -This allows shipping a working MVP in ~3 weeks if needed, with polish taking remaining time. - ---- - -## Blockers Assessment - -**Technical Blockers:** None identified -- All features use standard Zig capabilities -- No external dependencies -- No unproven techniques - -**Resource Blockers:** None -- Single developer project -- No external dependencies -- No hardware requirements - -**Knowledge Gaps:** Minor -- Zig comptime specifics - will learn during implementation -- Backlog engine integration - will discover during Phase 9 -- Both are learning opportunities, not blockers - ---- - -## Comparison to Existing Solutions - -| Feature | zargs | clap | argparse | -|---------|-------|------|----------| -| Scattered parsing | ✅ | ❌ | ❌ | -| Good help | ✅ | ✅ | ✅ | -| Plugin support | ✅ | ❌ | Partial | -| Type-driven | ✅ | ✅ | ❌ | -| Compatible collisions | ✅ | ❌ | ❌ | -| Help persistence | ✅ | ❌ | ❌ | - -**Unique value proposition confirmed:** Combines scattered parsing with comprehensive documentation. - ---- - -## Final Sign-Off - -**Plan Status:** ✅ APPROVED FOR IMPLEMENTATION - -**Review Date:** 2026-01-22 -**Reviewer:** Implementation Planning Team -**Next Review:** After Phase 1 completion (Day 3) - -**Signature:** Ready to proceed 🚀 - ---- - -## Quick Reference Card - -### Key Files to Create: -- `src/ArgumentType.zig` - Type system -- `src/ArgumentRegistry.zig` - Core registry -- `src/metadata.zig` - Metadata extraction -- `src/parsing.zig` - Argument parsing -- `src/help.zig` - Help generation -- `src/utils.zig` - Utilities (kebab-case, etc.) -- `src/errors.zig` - Error types -- `src/main.zig` - Public API - -### Key Commands: -- `zig build test` - Run tests -- `zig build run-simple` - Run simple example -- `zig build` - Build library - -### Key Patterns: -```zig -// Define args struct -const Args = struct { - field: type = default, - pub const meta = .{ ... }; -}; - -// Parse args -const args = try gArguments.parse(Args, .{ - .module = "MyModule", - .source = @src(), -}); - -// Generate help -const help = try gArguments.getUsageAlloc(allocator); -``` - -### Key Principles: -1. Test-driven development -2. Comptime where possible -3. Arena for strings -4. Clear ownership -5. Incremental progress - -**LET'S BUILD IT!** 🏗️ diff --git a/lib/zargs/todo/README.md b/lib/zargs/todo/README.md deleted file mode 100644 index bfcb183..0000000 --- a/lib/zargs/todo/README.md +++ /dev/null @@ -1,225 +0,0 @@ -# Implementation Plan Summary - -## Overview - -This directory contains the complete implementation plan for **zargs**, a novel argument parser for Zig designed for game engines and plugin architectures. - -## Documents - -### 📋 Core Planning -- **`implementation_plan.md`** - Original detailed plan (v1) -- **`implementation_plan_v2.md`** - Refined plan with improvements ⭐ **PRIMARY REFERENCE** -- **`review_iteration1.md`** - Issues found and improvements made - -### ✅ Readiness Assessment -- **`READINESS_CHECKLIST.md`** - Final confidence assessment and sign-off -- **Verdict:** ✅ **APPROVED FOR IMPLEMENTATION** (95% confidence) - -### 🚀 Getting Started -- **`QUICK_START.md`** - Day-by-day guide to begin implementation ⭐ **START HERE** - -## Quick Reference - -### Timeline -- **Total Duration:** 5 weeks (25 working days) -- **Phase 1-2:** Foundation (Week 1) -- **Phase 3-4:** Core implementation (Week 2-3) -- **Phase 5-7:** Polish and testing (Week 3-4) -- **Phase 8-10:** Documentation and release (Week 5) - -### Key Phases -1. **Type System** - ArgumentType, ParsedValue, error types -2. **Metadata** - Comptime extraction from structs -3. **Registry** - Core global registry with collision detection -4. **Parsing** - Argv parsing and struct reconstruction -5. **Help** - Generate comprehensive help text -6. **API** - Public exports and documentation -7. **Testing** - Comprehensive test suite -8. **Examples** - Demonstrate all features -9. **Build** - Integration with Backlog engine -10. **Polish** - Final quality pass - -### Success Criteria -- ✅ All tests pass (100% coverage target) -- ✅ Zero memory leaks -- ✅ All examples work -- ✅ Collision detection functional -- ✅ Help generation readable -- ✅ Integration with Backlog successful - -## Design Philosophy - -### Core Innovation -**Discovery-Based Documentation:** Arguments are discovered as modules load, enabling: -- Help text that grows with plugin initialization -- Documentation generation after first run -- Embedded help for fast `--help` responses -- Perfect for plugin architectures - -### Key Design Decisions -1. **Struct-based schema** - Type-driven argument definition -2. **All args have defaults** - No required arguments -3. **No positional arguments** - Simplifies parsing -4. **Compatible collisions** - Same name OK if types match -5. **Global registry** - Central metadata accumulation -6. **Parse-on-encounter** - Lazy registration and parsing -7. **Help persistence** - Generate once, embed forever - -## Technical Approach - -### Memory Model -- **Arena allocator** for all dynamic strings -- **Comptime strings** used directly (no duplication) -- **Registry owns** argv and parsed values -- **Clear lifetime:** Valid until registry.deinit() - -### Type System -- **ArgumentType enum** maps Zig types to argument types -- **ParsedValue union** stores parsed values -- **Comptime detection** via `@typeInfo()` -- **Optional support** via unwrapping `?T` - -### Collision Handling -- **Compatible:** Warn, allow multiple modules to define -- **Incompatible:** Error with source locations -- **Reserved:** `--help` always boolean - -## Development Process - -### Test-Driven Development -1. Write failing test -2. Implement minimum -3. Refactor -4. Commit - -### Daily Workflow -1. Review plan -2. Write tests first -3. Implement feature -4. Verify no leaks -5. Update docs -6. Commit - -### Go/No-Go Points -- **After Phase 1:** Type system working? -- **After Phase 2:** Metadata extraction working? -- **After Phase 4:** Full parse cycle working? -- **After Phase 7:** All tests passing? - -## Getting Started - -### Prerequisites -- Zig 0.14 -- No external dependencies - -### First Steps -1. Read `QUICK_START.md` -2. Create directory structure -3. Setup `build.zig` -4. Begin Phase 1.1: ArgumentType implementation -5. Follow TDD workflow - -### Day 1 Goal -- ✅ ArgumentType enum complete -- ✅ Type detection working -- ✅ All tests passing - -## Resources - -### Design Documents -- `../research/design.md` - Full design analysis -- `../research/hybrid_design.md` - Final design specification -- `../research/type_driven_example.md` - Type-driven patterns -- `../research/builder_pattern_example.md` - Builder comparison - -### Examples (to be created) -- `../examples/simple.zig` - Basic usage -- `../examples/game_engine.zig` - Multi-module scenario -- `../examples/persistence.zig` - Help text persistence - -### Tests (to be created) -- `../tests/type_test.zig` - Type system tests -- `../tests/collision_test.zig` - Collision detection -- `../tests/parsing_test.zig` - Argument parsing -- `../tests/help_test.zig` - Help generation - -## Confidence Assessment - -### Strengths -- ✅ Comprehensive planning -- ✅ Clear phase breakdown -- ✅ TDD approach -- ✅ Memory model defined -- ✅ All edge cases considered -- ✅ Realistic timeline - -### Risks (Mitigated) -- ⚠️ Comptime complexity → Build incrementally -- ⚠️ Memory leaks → Arena + testing -- ⚠️ Integration friction → Test early - -### Final Verdict -**95% confidence. Ready to implement!** 🎯 - -## Unique Value Proposition - -zargs combines: -1. **Scattered parsing** (like ad-hoc parsers) -2. **Good documentation** (like argparse) -3. **Type safety** (like Rust clap) -4. **Compatible collisions** (unique!) -5. **Help persistence** (unique!) -6. **Discovery-based docs** (unique!) - -**No other argument parser does this!** - -## Project Goals - -### Primary Goal -Create an argument parser optimized for game engines with plugin architectures, where: -- Arguments are scattered across many modules -- Not all modules may load in every run -- Comprehensive documentation is still needed -- Type safety is non-negotiable - -### Secondary Goals -- Zero external dependencies -- Minimal runtime overhead -- Clear error messages -- Excellent documentation -- Pleasant developer experience - -## Next Action - -**👉 Start here:** Read `QUICK_START.md` and begin Day 1! - ---- - -## Plan Status - -| Document | Status | Confidence | -|----------|--------|------------| -| implementation_plan.md | ✅ Complete | 85% | -| review_iteration1.md | ✅ Complete | - | -| implementation_plan_v2.md | ✅ Complete | 95% | -| READINESS_CHECKLIST.md | ✅ Approved | 95% | -| QUICK_START.md | ✅ Complete | - | - -**Overall Readiness: ✅ APPROVED FOR IMPLEMENTATION** - ---- - -## Contacts - -- Design Questions: See `research/` directory -- Implementation Questions: See `implementation_plan_v2.md` -- Getting Started Questions: See `QUICK_START.md` -- Daily Progress: Follow TDD workflow in plan - ---- - -**Built with confidence. Ready to ship.** 🚀 - -*"First, make it work. Then, make it fast. Then, make it beautiful."* - -**Let's build something novel!** 💡 diff --git a/lib/zargs/todo/TIMELINE.txt b/lib/zargs/todo/TIMELINE.txt deleted file mode 100644 index 32ee53b..0000000 --- a/lib/zargs/todo/TIMELINE.txt +++ /dev/null @@ -1,146 +0,0 @@ -╔══════════════════════════════════════════════════════════════════════════════╗ -║ ZARGS IMPLEMENTATION TIMELINE ║ -║ 5 Weeks / 25 Days ║ -╚══════════════════════════════════════════════════════════════════════════════╝ - -WEEK 1: FOUNDATION -┌──────────────────────────────────────────────────────────────────────────────┐ -│ DAY 1-3: Type System │ -│ [====] ArgumentType enum & fromZigType() │ -│ [====] ParsedValue union & conversions │ -│ [====] String utilities (kebab-case) │ -│ [====] Error type definitions │ -│ ✓ Milestone: Type detection working, all tests pass │ -├──────────────────────────────────────────────────────────────────────────────┤ -│ DAY 4-5: Metadata System │ -│ [====] Metadata structures │ -│ [====] Comptime metadata extraction │ -│ [====] Default value formatting │ -│ ✓ Milestone: Can extract metadata from any struct │ -└──────────────────────────────────────────────────────────────────────────────┘ - -WEEK 2: CORE IMPLEMENTATION -┌──────────────────────────────────────────────────────────────────────────────┐ -│ DAY 6-9: ArgumentRegistry │ -│ [====] Registry structure & init/deinit │ -│ [====] argv caching & help detection │ -│ [====] Metadata registration │ -│ [====] Collision detection logic │ -│ [====] Struct tracking │ -│ ✓ Milestone: Registry manages metadata correctly │ -├──────────────────────────────────────────────────────────────────────────────┤ -│ DAY 10: Start Parsing │ -│ [====] Argv parsing infrastructure │ -│ ✓ Milestone: Can iterate argv and dispatch │ -└──────────────────────────────────────────────────────────────────────────────┘ - -WEEK 3: PARSING & HELP -┌──────────────────────────────────────────────────────────────────────────────┐ -│ DAY 11-14: Complete Parsing │ -│ [====] Value parsing (all types) │ -│ [====] List parsing (comma-separated) │ -│ [====] Struct reconstruction │ -│ [====] Main parse() function │ -│ ✓ Milestone: End-to-end parsing works! │ -├──────────────────────────────────────────────────────────────────────────────┤ -│ DAY 15-16: Help Generation │ -│ [====] Help text formatting │ -│ [====] Module grouping & alignment │ -│ ✓ Milestone: Professional help output │ -└──────────────────────────────────────────────────────────────────────────────┘ - -WEEK 4: API & TESTING -┌──────────────────────────────────────────────────────────────────────────────┐ -│ DAY 17: Public API │ -│ [====] Module exports │ -│ [====] API documentation │ -│ ✓ Milestone: Clean public interface │ -├──────────────────────────────────────────────────────────────────────────────┤ -│ DAY 18-21: Comprehensive Testing │ -│ [====] Unit tests (100% coverage) │ -│ [====] Integration tests │ -│ [====] Memory leak tests │ -│ ✓ Milestone: Production-ready quality │ -└──────────────────────────────────────────────────────────────────────────────┘ - -WEEK 5: POLISH & RELEASE -┌──────────────────────────────────────────────────────────────────────────────┐ -│ DAY 22-24: Examples & Documentation │ -│ [====] Simple example │ -│ [====] Game engine example │ -│ [====] Persistence example │ -│ [====] README & API docs │ -│ ✓ Milestone: Complete documentation │ -├──────────────────────────────────────────────────────────────────────────────┤ -│ DAY 25: Build & Polish │ -│ [====] Build system integration │ -│ [====] Backlog engine integration │ -│ [====] Final review & fixes │ -│ ✓ Milestone: ✅ SHIPPED! │ -└──────────────────────────────────────────────────────────────────────────────┘ - -═══════════════════════════════════════════════════════════════════════════════ - PROGRESS TRACKING -═══════════════════════════════════════════════════════════════════════════════ - -Phase 1: Type System [ ] [ ] [ ] Days 1-3 -Phase 2: Metadata [ ] [ ] Days 4-5 -Phase 3: Registry [ ] [ ] [ ] [ ] Days 6-9 -Phase 4: Parsing [ ] [ ] [ ] [ ] [ ] Days 10-14 -Phase 5: Help [ ] [ ] Days 15-16 -Phase 6: API [ ] Day 17 -Phase 7: Testing [ ] [ ] [ ] [ ] Days 18-21 -Phase 8: Examples & Docs [ ] [ ] [ ] Days 22-24 -Phase 9-10: Build & Polish [ ] Day 25 - -Current Day: __ / 25 -Current Phase: ___________ -On Schedule: [ ] YES [ ] NO [ ] AHEAD - -═══════════════════════════════════════════════════════════════════════════════ - CRITICAL CHECKPOINTS -═══════════════════════════════════════════════════════════════════════════════ - -✓ Day 3: Type system complete and tested? [ ] YES [ ] NO -✓ Day 5: Metadata extraction working? [ ] YES [ ] NO -✓ Day 9: Registry managing data correctly? [ ] YES [ ] NO -✓ Day 14: Full parse cycle working? [ ] YES [ ] NO -✓ Day 21: All tests passing, no leaks? [ ] YES [ ] NO -✓ Day 25: Ready to ship? [ ] YES [ ] NO - -═══════════════════════════════════════════════════════════════════════════════ - DAILY CHECKLIST -═══════════════════════════════════════════════════════════════════════════════ - -Each day: - [ ] Review plan for today - [ ] Write tests first (TDD) - [ ] Implement feature - [ ] Verify tests pass - [ ] Check for memory leaks - [ ] Update documentation - [ ] Commit with clear message - [ ] Update progress tracker above - -═══════════════════════════════════════════════════════════════════════════════ - SUCCESS METRICS -═══════════════════════════════════════════════════════════════════════════════ - -By Day 25: - [ ] All unit tests pass - [ ] All integration tests pass - [ ] Zero memory leaks detected - [ ] All examples compile and run - [ ] Documentation complete - [ ] Integration with Backlog successful - [ ] Collision detection works - [ ] Help generation readable - [ ] Help persistence demonstrated - -═══════════════════════════════════════════════════════════════════════════════ - - YOU'VE GOT A SOLID PLAN. NOW EXECUTE IT! 💪 - - "The best way to predict the future is to implement it." - -═══════════════════════════════════════════════════════════════════════════════ diff --git a/lib/zargs/todo/implementation_plan.md b/lib/zargs/todo/implementation_plan.md deleted file mode 100644 index 5384efb..0000000 --- a/lib/zargs/todo/implementation_plan.md +++ /dev/null @@ -1,715 +0,0 @@ -# zargs Implementation Plan - -## Project Structure - -``` -lib/zargs/ -├── src/ -│ ├── main.zig # Public API exports -│ ├── ArgumentRegistry.zig # Core registry implementation -│ ├── ArgumentType.zig # Type system and conversions -│ ├── parsing.zig # Argv parsing logic -│ ├── help.zig # Help text generation -│ └── metadata.zig # Metadata extraction from structs -├── tests/ -│ ├── basic_test.zig # Basic functionality -│ ├── collision_test.zig # Type collision detection -│ ├── parsing_test.zig # Argument parsing -│ └── help_test.zig # Help generation -├── examples/ -│ ├── simple.zig # Minimal example -│ ├── game_engine.zig # Multi-module game engine example -│ └── persistence.zig # Help text persistence example -├── research/ # Design documents (existing) -├── todo/ # Implementation tracking (current) -└── build.zig # Build configuration -``` - -## Phase 1: Core Type System (Week 1) - -### 1.1 ArgumentType Implementation -**File:** `src/ArgumentType.zig` - -**Tasks:** -- [ ] Define `ArgumentType` enum with all supported types - - [ ] `bool`, `u8`, `u16`, `u32`, `u64` - - [ ] `i8`, `i16`, `i32`, `i64` - - [ ] `string` ([]const u8) - - [ ] `string_list` ([]const []const u8) - - [ ] `enum_type` (for Zig enums) -- [ ] Implement `fromZigType(comptime T: type)` function - - [ ] Handle `bool` - - [ ] Handle integers with proper signedness/width detection - - [ ] Handle string slices - - [ ] Handle string list slices - - [ ] Handle enums - - [ ] Handle `?T` (optional) by unwrapping - - [ ] Provide clear compile errors for unsupported types -- [ ] Implement `matches(self, other)` for type compatibility -- [ ] Add unit tests for type detection - -**Acceptance Criteria:** -- All Zig primitive types correctly map to ArgumentType -- Optional types unwrap correctly -- Clear compile errors for unsupported types (structs, unions, etc.) -- Type compatibility checker works correctly - -**Estimated Time:** 1-2 days - ---- - -### 1.2 ParsedValue Union -**File:** `src/ArgumentType.zig` (same file) - -**Tasks:** -- [ ] Define `ParsedValue` tagged union -- [ ] Implement conversion functions: - - [ ] `fromString(arg_type: ArgumentType, s: []const u8, allocator: Allocator) !ParsedValue` - - [ ] `toTypedValue(comptime T: type, parsed: ParsedValue) T` -- [ ] Handle list parsing (comma-separated values) -- [ ] Handle enum parsing (string to enum value) -- [ ] Add unit tests for value conversions - -**Acceptance Criteria:** -- String to typed value conversion works for all types -- Lists properly split on commas -- Enums parse from string names -- Error handling for invalid values - -**Estimated Time:** 1 day - ---- - -## Phase 2: Metadata System (Week 1) - -### 2.1 Metadata Structures -**File:** `src/metadata.zig` - -**Tasks:** -- [ ] Define `ArgumentMetadata` struct - - [ ] name, type, default_value_str - - [ ] short, long, help, value_name - - [ ] is_list flag - - [ ] source_location - - [ ] modules list (ArrayList) -- [ ] Define `ModuleInfo` struct - - [ ] name - - [ ] arguments list (ArrayList) -- [ ] Define `FieldMetadata` struct (for comptime extraction) - -**Acceptance Criteria:** -- Structures compile and are well-documented -- Memory management strategy clear - -**Estimated Time:** 0.5 days - ---- - -### 2.2 Metadata Extraction -**File:** `src/metadata.zig` - -**Tasks:** -- [ ] Implement `extractFieldMetadata(comptime T: type, comptime field_name: []const u8)` - - [ ] Get `meta` decl if exists - - [ ] Extract short/long/help/value_name from meta - - [ ] Generate defaults if meta missing - - [ ] Convert field name to kebab-case for long form -- [ ] Implement `extractDocComment(comptime T: type, comptime field_name: []const u8) []const u8` - - [ ] Use doc comments as help text (if available in future Zig) - - [ ] Fallback to empty string for now -- [ ] Implement `formatDefaultValue(comptime T: type, value: T, allocator: Allocator) ![]const u8` - - [ ] Format bool as "true"/"false" - - [ ] Format integers as strings - - [ ] Format strings as-is - - [ ] Format enums as tag names - - [ ] Format lists as comma-separated - -**Acceptance Criteria:** -- Can extract metadata from any valid struct -- Default values formatted correctly -- Missing meta declarations handled gracefully - -**Estimated Time:** 1-2 days - ---- - -## Phase 3: Core Registry (Week 2) - -### 3.1 ArgumentRegistry Basic Structure -**File:** `src/ArgumentRegistry.zig` - -**Tasks:** -- [ ] Define `ArgumentRegistry` struct with fields: - - [ ] allocator, arena - - [ ] arguments (StringHashMap) - - [ ] modules (StringHashMap) - - [ ] parsed_values (StringHashMap) - - [ ] parsed_structs (StringHashMap) - - [ ] argv cache - - [ ] help_requested flag -- [ ] Implement `init(allocator: Allocator) ArgumentRegistry` -- [ ] Implement `deinit(self: *ArgumentRegistry) void` - - [ ] Clean up all ArrayLists in modules - - [ ] Clean up all ArrayLists in arguments - - [ ] Deinit hashmaps - - [ ] Deinit arena - -**Acceptance Criteria:** -- Registry initializes correctly -- No memory leaks (test with MemoryLeakDetector) -- All resources cleaned up properly - -**Estimated Time:** 1 day - ---- - -### 3.2 Help Request Detection -**File:** `src/ArgumentRegistry.zig` - -**Tasks:** -- [ ] Implement `isHelpRequested(self: *ArgumentRegistry) bool` - - [ ] Cache argv on first call - - [ ] Scan for "--help" or "-h" - - [ ] Set help_requested flag - - [ ] Return cached result on subsequent calls - -**Acceptance Criteria:** -- Help detection works before any parsing -- Argv cached for later use -- No performance issues with repeated calls - -**Estimated Time:** 0.5 days - ---- - -### 3.3 Metadata Registration -**File:** `src/ArgumentRegistry.zig` - -**Tasks:** -- [ ] Implement `registerMetadata(self: *ArgumentRegistry, comptime T: type, opts: ParseOptions) !void` - - [ ] Get or create module entry - - [ ] Iterate over struct fields (comptime) - - [ ] Extract metadata for each field - - [ ] Check for existing arguments (collision detection) - - [ ] Error on incompatible type collisions with source locations - - [ ] Warn on compatible type collisions - - [ ] Add argument to module's list - - [ ] Store ArgumentMetadata in registry - -**Acceptance Criteria:** -- Metadata correctly extracted from structs -- Compatible collisions allowed with warnings -- Incompatible collisions rejected with clear error messages -- Source locations captured and displayed in errors - -**Estimated Time:** 2 days - ---- - -### 3.4 Struct Already Parsed Check -**File:** `src/ArgumentRegistry.zig` - -**Tasks:** -- [ ] Implement struct tracking in `parsed_structs` hashmap -- [ ] Use `@typeName(T)` as key -- [ ] Skip re-registration if already seen - -**Acceptance Criteria:** -- Calling `parse()` twice with same struct is efficient -- No duplicate metadata registration - -**Estimated Time:** 0.5 days - ---- - -## Phase 4: Argument Parsing (Week 2-3) - -### 4.1 Argv Parsing Infrastructure -**File:** `src/parsing.zig` - -**Tasks:** -- [ ] Implement `parseArgv(self: *ArgumentRegistry) !void` - - [ ] Get argv via `std.process.argsAlloc()` if not cached - - [ ] Skip program name - - [ ] Iterate over arguments - - [ ] Dispatch to appropriate parser -- [ ] Implement `parseArg(self: *ArgumentRegistry, arg: []const u8) !void` - - [ ] Handle `--long-name=value` format - - [ ] Handle `--long-name value` format (next arg) - - [ ] Handle `--flag` (boolean) format - - [ ] Look up argument metadata - - [ ] Parse value according to type - - [ ] Store in parsed_values -- [ ] Implement `parseShortArg(self: *ArgumentRegistry, short: u8) !void` - - [ ] Look up by short character - - [ ] Handle value if required - - [ ] Handle flag if boolean - -**Acceptance Criteria:** -- All argument formats parsed correctly -- Unknown arguments produce clear errors -- Values parsed according to type -- Boolean flags don't require values - -**Estimated Time:** 2 days - ---- - -### 4.2 Value Parsing -**File:** `src/parsing.zig` - -**Tasks:** -- [ ] Implement integer parsing with error handling -- [ ] Implement boolean parsing ("true"/"false", "1"/"0") -- [ ] Implement string parsing (already a string) -- [ ] Implement list parsing (split on comma) -- [ ] Implement enum parsing (string to enum tag) -- [ ] Handle parsing errors with useful messages - -**Acceptance Criteria:** -- All types parse correctly from strings -- Clear errors for invalid values -- Edge cases handled (empty strings, invalid numbers, etc.) - -**Estimated Time:** 1 day - ---- - -### 4.3 Struct Reconstruction -**File:** `src/ArgumentRegistry.zig` - -**Tasks:** -- [ ] Implement `reconstructStruct(self: *ArgumentRegistry, comptime T: type) T` - - [ ] Create uninitialized struct - - [ ] Iterate over fields (comptime) - - [ ] Look up parsed value by long name - - [ ] Convert ParsedValue to field type - - [ ] Fall back to default if not parsed - - [ ] Return completed struct - -**Acceptance Criteria:** -- Structs correctly populated with parsed values -- Defaults used when arguments not provided -- Type conversions work correctly -- All fields properly initialized - -**Estimated Time:** 1 day - ---- - -### 4.4 Main parse() Function -**File:** `src/ArgumentRegistry.zig` - -**Tasks:** -- [ ] Implement `parse(self: *ArgumentRegistry, comptime T: type, opts: ParseOptions) !T` - - [ ] Check if already parsed (use parsed_structs) - - [ ] If not, register metadata - - [ ] Parse argv (only new arguments) - - [ ] Reconstruct and return struct - - [ ] Mark struct as parsed - -**Acceptance Criteria:** -- Complete parse flow works end-to-end -- Lazy parsing only processes new arguments -- Subsequent calls return cached results efficiently - -**Estimated Time:** 1 day - ---- - -## Phase 5: Help Generation (Week 3) - -### 5.1 Help Text Formatting -**File:** `src/help.zig` - -**Tasks:** -- [ ] Implement `getUsageAlloc(self: *ArgumentRegistry, allocator: Allocator) ![]const u8` - - [ ] Write header ("Usage: [OPTIONS]") - - [ ] Write global options (--help) - - [ ] Group arguments by module - - [ ] Format each argument: - - [ ] `-s, --long-name ` - - [ ] Help text - - [ ] Default value - - [ ] Return allocated string - -**Acceptance Criteria:** -- Help text is well-formatted and readable -- Arguments grouped by module -- Defaults shown for all arguments -- Short and long forms displayed correctly - -**Estimated Time:** 1 day - ---- - -### 5.2 Help Text Alignment -**File:** `src/help.zig` - -**Tasks:** -- [ ] Calculate maximum width of argument specifications -- [ ] Align help text in columns -- [ ] Handle line wrapping for long help text -- [ ] Ensure consistent spacing - -**Acceptance Criteria:** -- Help text looks professional -- Columns aligned nicely -- Readable on standard terminal widths - -**Estimated Time:** 0.5 days - ---- - -## Phase 6: Public API (Week 3) - -### 6.1 Main Module Exports -**File:** `src/main.zig` - -**Tasks:** -- [ ] Export `ArgumentRegistry` -- [ ] Export `ArgumentType` -- [ ] Export `ParsedValue` -- [ ] Export helper types (ParseOptions, etc.) -- [ ] Add top-level documentation -- [ ] Define version constant - -**Acceptance Criteria:** -- All public types accessible -- API is clean and well-documented -- Version information available - -**Estimated Time:** 0.5 days - ---- - -### 6.2 Global Registry Helper -**File:** `src/main.zig` - -**Tasks:** -- [ ] Consider providing helper to initialize global registry -- [ ] Document pattern for global usage -- [ ] Provide example code - -**Acceptance Criteria:** -- Clear guidance on using global singleton -- Thread safety considerations documented - -**Estimated Time:** 0.5 days - ---- - -## Phase 7: Testing (Week 4) - -### 7.1 Unit Tests -**Files:** `tests/*.zig` - -**Tasks:** -- [ ] Test type detection and conversion -- [ ] Test metadata extraction -- [ ] Test argument parsing (all formats) -- [ ] Test collision detection (compatible and incompatible) -- [ ] Test help generation -- [ ] Test struct reconstruction -- [ ] Test list parsing -- [ ] Test enum parsing -- [ ] Test error conditions - -**Acceptance Criteria:** -- 100% code coverage of core logic -- All edge cases tested -- Clear test names and documentation - -**Estimated Time:** 2 days - ---- - -### 7.2 Integration Tests -**Files:** `tests/*.zig` - -**Tasks:** -- [ ] Test full parse cycle with multiple structs -- [ ] Test module registration order independence -- [ ] Test argv caching behavior -- [ ] Test help request before parsing -- [ ] Test help text persistence workflow - -**Acceptance Criteria:** -- End-to-end workflows tested -- Multiple modules interacting correctly -- Real-world scenarios covered - -**Estimated Time:** 1 day - ---- - -### 7.3 Memory Leak Testing -**Files:** `tests/*.zig` - -**Tasks:** -- [ ] Wrap all tests with memory leak detection -- [ ] Test cleanup paths (deinit) -- [ ] Test error paths (proper cleanup on errors) -- [ ] Verify arena allocator usage - -**Acceptance Criteria:** -- Zero memory leaks in all tests -- All allocations properly freed - -**Estimated Time:** 0.5 days - ---- - -## Phase 8: Examples and Documentation (Week 4) - -### 8.1 Simple Example -**File:** `examples/simple.zig` - -**Tasks:** -- [ ] Single struct with basic types -- [ ] Parse and print values -- [ ] Show help usage -- [ ] Document every step - -**Acceptance Criteria:** -- Works as minimal starting point -- Clear and easy to understand - -**Estimated Time:** 0.5 days - ---- - -### 8.2 Game Engine Example -**File:** `examples/game_engine.zig` - -**Tasks:** -- [ ] Multiple modules (Engine, Physics, Audio, Renderer) -- [ ] Each module has its own Args struct -- [ ] Show scattered parsing pattern -- [ ] Generate help text -- [ ] Demonstrate compatible collisions - -**Acceptance Criteria:** -- Realistic game engine scenario -- Shows plugin architecture usage -- Help text properly grouped - -**Estimated Time:** 1 day - ---- - -### 8.3 Persistence Example -**File:** `examples/persistence.zig` - -**Tasks:** -- [ ] Generate help text after parsing -- [ ] Write to file -- [ ] Show embedding with @embedFile -- [ ] Fast --help response - -**Acceptance Criteria:** -- Demonstrates novel persistence feature -- Shows workflow for production usage - -**Estimated Time:** 0.5 days - ---- - -### 8.4 README and API Documentation -**Files:** `README.md`, doc comments - -**Tasks:** -- [ ] Write comprehensive README - - [ ] What is zargs? - - [ ] Why use it? - - [ ] Quick start guide - - [ ] Design philosophy - - [ ] Comparison to alternatives -- [ ] Document all public APIs with doc comments -- [ ] Add usage examples to doc comments -- [ ] Document design decisions - -**Acceptance Criteria:** -- README is compelling and informative -- All public APIs documented -- Examples included in docs - -**Estimated Time:** 1 day - ---- - -## Phase 9: Build System (Week 4) - -### 9.1 Build.zig Setup -**File:** `build.zig` - -**Tasks:** -- [ ] Define library module -- [ ] Add test step -- [ ] Add example build steps -- [ ] Add install step -- [ ] Configure for Zig 0.14 - -**Acceptance Criteria:** -- `zig build` compiles library -- `zig build test` runs all tests -- `zig build run-simple` runs simple example -- Works with Zig 0.14 - -**Estimated Time:** 0.5 days - ---- - -### 9.2 Integration with Backlog Engine -**File:** Integration into main project - -**Tasks:** -- [ ] Import as lib/zargs module -- [ ] Make available to engine modules -- [ ] Test with actual engine code -- [ ] Document engine-specific patterns - -**Acceptance Criteria:** -- Engine can use zargs -- Works with existing build system - -**Estimated Time:** 0.5 days - ---- - -## Phase 10: Polish and Release (Week 5) - -### 10.1 Error Messages -**Tasks:** -- [ ] Review all error messages -- [ ] Ensure helpful and actionable -- [ ] Include context (argument name, module, source location) -- [ ] Format consistently - -**Acceptance Criteria:** -- User-friendly error messages -- Easy to debug issues - -**Estimated Time:** 0.5 days - ---- - -### 10.2 Performance Testing -**Tasks:** -- [ ] Benchmark parsing overhead -- [ ] Benchmark help generation -- [ ] Profile memory usage -- [ ] Optimize hot paths if needed - -**Acceptance Criteria:** -- Parsing overhead negligible -- Help generation fast -- Memory usage reasonable - -**Estimated Time:** 1 day - ---- - -### 10.3 Edge Cases -**Tasks:** -- [ ] Test with empty argv -- [ ] Test with no arguments defined -- [ ] Test with only --help -- [ ] Test with very long argument lists -- [ ] Test with unicode in arguments -- [ ] Test with special characters - -**Acceptance Criteria:** -- No crashes on edge cases -- Reasonable behavior - -**Estimated Time:** 0.5 days - ---- - -### 10.4 Final Review -**Tasks:** -- [ ] Code review entire implementation -- [ ] Check for TODOs -- [ ] Verify all tests pass -- [ ] Run formatter -- [ ] Check for memory leaks -- [ ] Update documentation - -**Acceptance Criteria:** -- Code is production-ready -- No known issues - -**Estimated Time:** 1 day - ---- - -## Timeline Summary - -| Phase | Duration | Milestone | -|-------|----------|-----------| -| 1. Core Type System | 2-3 days | Type detection working | -| 2. Metadata System | 1.5-2.5 days | Metadata extraction working | -| 3. Core Registry | 4 days | Registry structure complete | -| 4. Argument Parsing | 5 days | End-to-end parsing working | -| 5. Help Generation | 1.5 days | Help text generation working | -| 6. Public API | 1 day | API finalized | -| 7. Testing | 3.5 days | Full test coverage | -| 8. Examples & Docs | 3 days | Documentation complete | -| 9. Build System | 1 day | Build integration complete | -| 10. Polish & Release | 3 days | Production ready | - -**Total Estimated Time:** ~25 days (5 weeks) - -## Success Criteria - -- [ ] All unit tests pass -- [ ] All integration tests pass -- [ ] Zero memory leaks -- [ ] All examples run correctly -- [ ] Documentation complete and clear -- [ ] Can parse arguments from multiple modules -- [ ] Compatible collisions work -- [ ] Incompatible collisions error appropriately -- [ ] Help text generation works -- [ ] Help text persistence workflow demonstrated -- [ ] Integration with Backlog engine successful - -## Risks and Mitigations - -| Risk | Impact | Mitigation | -|------|--------|------------| -| Comptime complexity too high | High | Start simple, iterate; use runtime where needed | -| Memory management issues | High | Test early with leak detection; use arena allocator | -| Type system edge cases | Medium | Comprehensive type testing; clear error messages | -| Help text formatting tricky | Low | Reference existing tools; iterate on format | -| Integration issues | Medium | Test integration early in Phase 9 | - -## Open Questions - -1. Should we support positional arguments in v2? (deferred to v1 feedback) -2. Should we support config file loading? (separate feature, later) -3. Should we support environment variable fallback? (separate feature, later) -4. What about shell completion generation? (v2 feature) -5. How to handle argument value validation? (v2 feature - validators) - -## Dependencies - -- Zig 0.14 -- No external dependencies (pure std lib) - -## Testing Strategy - -1. **Unit tests** - Test individual components in isolation -2. **Integration tests** - Test component interactions -3. **Example tests** - Ensure examples compile and run -4. **Memory tests** - Verify no leaks with GeneralPurposeAllocator -5. **Manual testing** - Test with Backlog engine integration - -## Notes - -- Keep implementation simple and focused on core use case -- Prioritize game engine / plugin architecture scenario -- Document design decisions and tradeoffs -- Write tests alongside implementation (TDD where appropriate) -- Get feedback early from engine integration diff --git a/lib/zargs/todo/implementation_plan_v2.md b/lib/zargs/todo/implementation_plan_v2.md deleted file mode 100644 index 423454d..0000000 --- a/lib/zargs/todo/implementation_plan_v2.md +++ /dev/null @@ -1,486 +0,0 @@ -# Implementation Plan v2 - Refined - -## Critical Changes from v1 - -1. **Add string handling strategy early (Phase 1.3)** -2. **Define error types upfront (Phase 1.4)** -3. **Emphasize test-driven development throughout** -4. **Clarify memory ownership at every step** -5. **Add missing helpers (kebab-case conversion, etc.)** - ---- - -## Phase 1: Foundation (Week 1: Days 1-3) - -### 1.1 ArgumentType Enum -**File:** `src/ArgumentType.zig` -**Duration:** 1 day - -- [ ] Define `ArgumentType` enum -- [ ] Implement `fromZigType(comptime T: type) ArgumentType` -- [ ] Implement `matches(self, other) bool` -- [ ] **TESTS:** Type detection for all supported types - -**Key Decision:** Support `?T` by unwrapping to underlying type - ---- - -### 1.2 ParsedValue Union -**File:** `src/ArgumentType.zig` -**Duration:** 1 day - -- [ ] Define `ParsedValue` tagged union -- [ ] Implement `fromString(type, string, allocator) !ParsedValue` -- [ ] Implement `toTypedValue(comptime T: type, parsed) T` -- [ ] **TESTS:** Conversions for all types, error cases - -**Key Decision:** Allocate strings into caller-provided arena - ---- - -### 1.3 String Handling Strategy -**File:** `src/utils.zig` -**Duration:** 0.5 days - -- [ ] Implement `toKebabCase(comptime name: []const u8) []const u8` - - Convert camelCase/snake_case to kebab-case - - Comptime function, returns comptime string -- [ ] Document string ownership model: - - Arena owns all parsed strings - - Comptime strings (field names, literals) not duplicated - - Runtime strings (argv) duplicated into arena -- [ ] **TESTS:** kebab-case conversion edge cases - -**Key Decision:** Use arena allocator for all dynamic strings - ---- - -### 1.4 Error Type Definitions -**File:** `src/errors.zig` -**Duration:** 0.5 days - -- [ ] Define comprehensive error set: -```zig -pub const Error = error{ - IncompatibleArgumentType, - UnknownArgument, - InvalidValue, - InvalidIntegerValue, - InvalidBooleanValue, - InvalidEnumValue, - MissingArgumentValue, - OutOfMemory, -}; -``` -- [ ] Document when each error occurs -- [ ] Consider error payloads for context - -**Key Decision:** Separate error type allows clear API contracts - ---- - -## Phase 2: Metadata Extraction (Week 1: Days 4-5) - -### 2.1 Metadata Structures -**File:** `src/metadata.zig` -**Duration:** 0.5 days - -- [ ] Define `ArgumentMetadata` struct -- [ ] Define `ModuleInfo` struct -- [ ] Define `FieldMeta` (what goes in `pub const meta = .{...}`) -- [ ] Document structure ownership - ---- - -### 2.2 Comptime Metadata Extraction -**File:** `src/metadata.zig` -**Duration:** 1.5 days - -- [ ] `extractFieldMetadata(comptime T: type, comptime field: Field) FieldMeta` - - Get `T.meta.field_name` if exists - - Generate defaults for missing fields - - Convert field name to kebab-case - - Extract default value -- [ ] `extractDocComment(comptime T: type, comptime field_name: []const u8) []const u8` - - Return empty for now (future: parse doc comments) -- [ ] `formatDefaultValue(comptime T: type, value: T, allocator) ![]const u8` - - Format bool, int, string, enum, list -- [ ] **TESTS:** Metadata extraction with various struct configurations - -**Key Decision:** All metadata extraction is comptime - ---- - -## Phase 3: Core Registry (Week 2: Days 6-9) - -### 3.1 Registry Structure -**File:** `src/ArgumentRegistry.zig` -**Duration:** 1 day - -- [ ] Define struct with all fields -- [ ] Implement `init(allocator) ArgumentRegistry` -- [ ] Implement `deinit()` -- [ ] **TESTS:** Init/deinit, memory leak detection - -**Key Decision:** Use StringHashMap for O(1) lookups - ---- - -### 3.2 argv Caching -**File:** `src/ArgumentRegistry.zig` -**Duration:** 0.5 days - -- [ ] Cache argv on first access -- [ ] Implement `isHelpRequested() bool` -- [ ] **TESTS:** Help detection, caching behavior - -**Key Decision:** Registry owns argv memory - ---- - -### 3.3 Metadata Registration with Collision Detection -**File:** `src/ArgumentRegistry.zig` -**Duration:** 2 days - -- [ ] `registerMetadata(comptime T: type, opts: ParseOptions) !void` - - Create/get module entry - - For each field: - - Extract metadata - - Check for existing argument - - If exists and types match: warn, add module - - If exists and types differ: error with locations - - If new: store metadata -- [ ] Implement collision detection logic -- [ ] Format error messages with source locations -- [ ] **TESTS:** Compatible collisions, incompatible collisions, error messages - -**Key Decision:** Source locations captured via `@src()`, stored as-is (compile-time strings) - ---- - -### 3.4 Struct Tracking -**File:** `src/ArgumentRegistry.zig` -**Duration:** 0.5 days - -- [ ] Track parsed structs by type name -- [ ] Skip re-registration if already parsed -- [ ] **TESTS:** Multiple parse calls with same struct - ---- - -## Phase 4: Argument Parsing (Week 2-3: Days 10-14) - -### 4.1 Argv Parsing Infrastructure -**File:** `src/parsing.zig` -**Duration:** 1.5 days - -- [ ] `parseArgv() !void` - - Iterate cached argv - - Dispatch to appropriate parser -- [ ] `parseArg(arg: []const u8) !void` - - Handle `--long=value` - - Handle `--long value` - - Handle `--flag` (bool) -- [ ] `parseShortArg(short: u8) !void` - - Look up by short name - - Handle value/flag -- [ ] **TESTS:** All argument formats, unknown arguments - -**Key Decision:** Duplicate parsed strings into arena - ---- - -### 4.2 Value Parsing with List Support -**File:** `src/parsing.zig` -**Duration:** 1.5 days - -- [ ] Parse integers with range checking -- [ ] Parse booleans (true/false, 1/0, yes/no) -- [ ] Parse strings (already strings, but duplicate) -- [ ] Parse lists: - - Split on comma - - Also support repeated args: `--list=a --list=b` - - Accumulate into single list -- [ ] Parse enums (stringToEnum) -- [ ] **TESTS:** All types, edge cases, error conditions - -**Key Decision:** Support both comma-separated and repeated arguments for lists - ---- - -### 4.3 Struct Reconstruction with Type Safety -**File:** `src/ArgumentRegistry.zig` -**Duration:** 1 day - -- [ ] `reconstructStruct(comptime T: type) T` - - For each field: - - Get parsed value by long name - - Convert to field type with comptime assertions - - Fall back to default if not provided - - Handle `?T` (optional) types -- [ ] Runtime type checking for safety -- [ ] **TESTS:** Struct reconstruction, optional fields, defaults - -**Key Decision:** Comptime type checks prevent runtime type errors - ---- - -### 4.4 Main parse() Integration -**File:** `src/ArgumentRegistry.zig` -**Duration:** 1 day - -- [ ] `parse(comptime T: type, opts: ParseOptions) !T` - - Check parsed_structs - - If new: registerMetadata, parseArgv - - reconstructStruct and return - - Mark as parsed -- [ ] **TESTS:** Full end-to-end parsing, multiple structs - -**Key Decision:** Single function handles everything - ---- - -## Phase 5: Help Generation (Week 3: Days 15-16) - -### 5.1 Help Text Generation -**File:** `src/help.zig` -**Duration:** 1 day - -- [ ] `getUsageAlloc(allocator) ![]const u8` - - Write header - - Write global options (--help) - - For each module: - - Write module name - - For each argument: - - Format `-s, --long Help text [default: X]` - - Calculate alignment for readability -- [ ] **TESTS:** Help text format, alignment, grouping - -**Key Decision:** Generate fresh each time (acceptable performance) - ---- - -## Phase 6: Public API (Week 3-4: Day 17) - -### 6.1 Module Exports and Documentation -**File:** `src/main.zig` -**Duration:** 1 day - -- [ ] Export all public types -- [ ] Add top-level module documentation -- [ ] Define version constant -- [ ] Document global registry pattern -- [ ] **TESTS:** Ensure exports are accessible - ---- - -## Phase 7: Comprehensive Testing (Week 4: Days 18-21) - -### 7.1 Unit Test Coverage -**Duration:** 2 days - -- [ ] Achieve 100% coverage of: - - Type detection and conversion - - Metadata extraction - - Collision detection - - Parsing logic - - Struct reconstruction - - Help generation -- [ ] Test error paths -- [ ] Test edge cases - ---- - -### 7.2 Integration Tests -**Duration:** 1 day - -- [ ] Multi-module scenarios -- [ ] Parse order independence -- [ ] Help text workflow -- [ ] Persistence workflow - ---- - -### 7.3 Memory and Safety Tests -**Duration:** 1 day - -- [ ] Memory leak detection on all tests -- [ ] Test cleanup on error paths -- [ ] Arena allocator correctness -- [ ] Stress tests (many arguments, large values) - ---- - -## Phase 8: Examples and Documentation (Week 5: Days 22-24) - -### 8.1 Examples -**Duration:** 2 days - -- [ ] `examples/simple.zig` - Basic usage -- [ ] `examples/game_engine.zig` - Multi-module -- [ ] `examples/persistence.zig` - Help text persistence -- [ ] Ensure all examples compile and run - ---- - -### 8.2 Documentation -**Duration:** 1 day - -- [ ] Write comprehensive README -- [ ] Document all public APIs -- [ ] Add usage examples to doc comments -- [ ] Document design decisions and tradeoffs - ---- - -## Phase 9: Build and Integration (Week 5: Day 25) - -### 9.1 Build System -**Duration:** 0.5 days - -- [ ] Configure build.zig -- [ ] Test, example, and install steps -- [ ] Verify Zig 0.14 compatibility - ---- - -### 9.2 Engine Integration -**Duration:** 0.5 days - -- [ ] Import into Backlog engine -- [ ] Test with actual engine modules -- [ ] Document engine-specific usage - ---- - -## Phase 10: Polish (Week 5: Day 25) - -### 10.1 Final Review -**Duration:** 0.5 days - -- [ ] Review all error messages -- [ ] Run formatter -- [ ] Check for TODOs -- [ ] Verify no memory leaks -- [ ] Performance check - ---- - -## Daily Checklist Template - -For each day of implementation: - -- [ ] Write tests FIRST for new functionality -- [ ] Implement feature -- [ ] Ensure tests pass -- [ ] Check for memory leaks -- [ ] Update documentation -- [ ] Commit with clear message - ---- - -## Test-Driven Development Workflow - -1. **Write failing test** - Define expected behavior -2. **Implement minimum** - Make test pass -3. **Refactor** - Improve code quality -4. **Repeat** - Next feature - ---- - -## Memory Ownership Rules - -### Simple Rules: -1. **Registry owns:** argv, all parsed strings (via arena) -2. **Caller owns:** allocator passed to registry -3. **Comptime owns:** field names, type names, meta strings -4. **Return values:** Structs contain pointers into registry arena - - Valid until registry.deinit() - - Document this lifetime requirement - -### Rule of Thumb: -- If it comes from argv → duplicate into arena -- If it's comptime → use as-is -- If it's dynamically formatted → allocate from arena - ---- - -## Success Metrics - -- [ ] All tests pass (100% coverage target) -- [ ] Zero memory leaks detected -- [ ] All examples compile and run -- [ ] Documentation complete and clear -- [ ] Integration with Backlog engine successful -- [ ] Collision detection works correctly -- [ ] Help generation produces readable output -- [ ] Can demonstrate persistence workflow - ---- - -## Open Questions Resolved - -1. **Positional arguments?** No, deferred to v2 -2. **Config files?** No, separate feature -3. **Environment variables?** No, separate feature -4. **Shell completion?** No, v2 feature -5. **Validators?** No, v2 feature - -All features deferred to maintain focus on core use case. - ---- - -## Confidence Level: 95% - -**Why higher:** -- Addressed string handling explicitly -- Clarified memory ownership model -- Emphasized TDD approach -- Defined error types upfront -- Covered missing utility functions - -**Remaining concerns:** -- Comptime complexity (will discover during Phase 2) -- Edge cases in parsing (will catch with comprehensive tests) - -**Mitigation:** -- Build incrementally -- Test each component in isolation -- Integration test early (Phase 7) - ---- - -## Go/No-Go Decision Points - -### After Phase 1 (Day 3): -**Check:** Type system working correctly? -- If yes: proceed -- If no: revisit type design - -### After Phase 2 (Day 5): -**Check:** Metadata extraction compiling and working? -- If yes: proceed -- If no: simplify metadata approach - -### After Phase 4 (Day 14): -**Check:** Full parse cycle working end-to-end? -- If yes: proceed to polish -- If no: debug integration issues - -### After Phase 7 (Day 21): -**Check:** All tests passing, no leaks? -- If yes: ready for production -- If no: fix issues before release - ---- - -## Implementation Notes - -- Keep each file under 500 lines -- Prefer clarity over cleverness -- Document all comptime behavior -- Write tests for every public function -- Use meaningful error messages -- Follow Zig style guide - -**Ready to implement!** 🚀 diff --git a/lib/zargs/todo/review_iteration1.md b/lib/zargs/todo/review_iteration1.md deleted file mode 100644 index 947f4eb..0000000 --- a/lib/zargs/todo/review_iteration1.md +++ /dev/null @@ -1,227 +0,0 @@ -# Implementation Plan Review - Iteration 1 - -## Issues Found & Improvements - -### 1. Missing Critical Component: String Interning/Storage -**Problem:** The plan doesn't address how we store string keys and values efficiently. - -**Impact:** High - affects memory management and performance - -**Solution:** Add Phase 1.3 for string storage strategy -- Use arena allocator for all strings -- Duplicate keys for hashmaps -- Clear ownership model - ---- - -### 2. Incomplete Error Handling Strategy -**Problem:** Error types not defined upfront - -**Impact:** Medium - will cause refactoring later - -**Solution:** Add to Phase 1: -- Define error set in ArgumentType.zig -- `error{ IncompatibleArgumentType, UnknownArgument, InvalidValue, ... }` -- Document error semantics - ---- - -### 3. Missing: Argument Name Conversion Logic -**Problem:** Need to convert field_name -> kebab-case for --long-name - -**Impact:** Medium - affects usability - -**Solution:** Add to Phase 2.2: -- Implement `toKebabCase(comptime name: []const u8) []const u8` -- Handle common patterns (fooBar -> foo-bar) - ---- - -### 4. List Parsing Details Unclear -**Problem:** How do we handle repeated arguments? `--files=a.txt --files=b.txt` - -**Impact:** Medium - affects API design - -**Solution:** Clarify in Phase 4.2: -- Support both comma-separated AND repeated args -- Accumulate into list -- Document precedence - ---- - -### 5. Collision Warning Implementation Missing -**Problem:** Plan says "warn" but doesn't specify how - -**Impact:** Low - but affects UX - -**Solution:** Add to Phase 3.3: -- Use `std.log.warn()` for compatible collisions -- Ensure warnings only shown once per argument -- Consider quiet mode for production - ---- - -### 6. Type Conversion Safety -**Problem:** What if ParsedValue type doesn't match field type? - -**Impact:** High - affects correctness - -**Solution:** Add to Phase 4.3: -- Assert type compatibility at comptime -- Runtime check for dynamic cases -- Clear error if mismatch - ---- - -### 7. Testing Order -**Problem:** Testing in Phase 7 means no tests until week 4 - -**Impact:** High - integration issues caught late - -**Solution:** Reorder: -- Write tests alongside implementation -- Test-driven development for core components -- Phase 7 becomes "comprehensive test suite" - ---- - -### 8. Source Location Storage -**Problem:** `std.builtin.SourceLocation` contains `file: []const u8` - who owns this? - -**Impact:** Medium - potential memory issue - -**Solution:** Add to Phase 3.3: -- SourceLocation strings are compile-time constants -- No need to duplicate -- Document this invariant - ---- - -### 9. argv Ownership -**Problem:** Who owns the argv strings? How long are they valid? - -**Impact:** High - potential use-after-free - -**Solution:** Add to Phase 4.1: -- `argsAlloc()` allocates - we own it -- Store in registry, free in deinit -- All parsed strings must be duplicated into arena - ---- - -### 10. Help Text Performance -**Problem:** Generating help text every time could be slow - -**Impact:** Low - help is infrequent - -**Solution:** Note in Phase 5.1: -- Acceptable to regenerate each time -- Could add caching later if needed - ---- - -### 11. Module Name Storage -**Problem:** Module names in ParseOptions - are they string literals? - -**Impact:** Medium - affects API - -**Solution:** Clarify in Phase 3.3: -- Expect compile-time string literals -- Document that runtime strings need to be stable -- Consider copying to arena for safety - ---- - -### 12. Optional Field Handling -**Problem:** How do we handle `?T` fields - always optional arguments? - -**Impact:** Medium - affects API semantics - -**Solution:** Add to Phase 4.3: -- `?T` means argument is optional -- `nil` if not provided -- Non-optional fields must have defaults (already required) - ---- - -## Revised Phases - -### New Phase Order: - -**Week 1:** -- Phase 1: Core Type System + Error Types (3 days) -- Phase 2: Metadata System + String Handling (2 days) - -**Week 2:** -- Phase 3: Core Registry (4 days) -- Start Phase 4: Argument Parsing (1 day) - -**Week 3:** -- Finish Phase 4: Argument Parsing (4 days) -- Phase 5: Help Generation (1 day) - -**Week 4:** -- Phase 6: Public API (1 day) -- Phase 7: Comprehensive Testing (4 days) - -**Week 5:** -- Phase 8: Examples & Docs (3 days) -- Phase 9: Build System (1 day) -- Phase 10: Polish (1 day) - ---- - -## Critical Path Items - -1. **Type System** - Everything depends on this -2. **Metadata Extraction** - Needed for registration -3. **Argument Parsing** - Core functionality -4. **Struct Reconstruction** - Completes the cycle -5. **Help Generation** - Key differentiator - -These must work before moving forward. - ---- - -## Risk Assessment Updates - -### High Risk Items: -1. **Comptime metadata extraction** - Most complex part - - Mitigation: Build iteratively, test each type - -2. **Memory management** - Easy to leak - - Mitigation: Arena for most things, test early - -3. **Type conversion safety** - Runtime bugs possible - - Mitigation: Comptime checks where possible - -### Medium Risk Items: -1. **String ownership** - Confusing - - Mitigation: Clear documentation, ownership model - -2. **Collision detection** - Edge cases - - Mitigation: Comprehensive tests - -### Low Risk Items: -1. **Help formatting** - Mostly cosmetic -2. **Build integration** - Well-understood - ---- - -## Confidence Level: 85% - -**Strengths:** -- Clear phase breakdown -- Reasonable timeline -- Covers all requirements -- Identified most risks - -**Concerns:** -- Comptime complexity might be underestimated -- String handling needs more thought -- Test-driven approach should be emphasized more - -**Recommendation:** -- Address string handling first (Phase 1.3) -- Write tests alongside implementation -- Build simplest possible version first, then iterate