Backlog/lib/zargs/research/builder_pattern_example.md

282 lines
8.0 KiB
Markdown

# 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::<u32>().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 <cxxopts.hpp>
#include <iostream>
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<std::string>())
("n,count", "Number of iterations",
cxxopts::value<int>()->default_value("1"))
("c,config", "Config file path",
cxxopts::value<std::string>())
("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<bool>();
std::string output = result["output"].as<std::string>();
int count = result["count"].as<int>();
}
```
## 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.