zig-clapでストリーミングパース時のヘルプ表示支援

## はじめに

`zig-clap`[^1] では、引数の構成情報(`clap.Patam(Id)`を使用して、ヘルプ出力をする`clap.help`関数が用意されている。
[^1]: https://github.com/Hejsil/zig-clap

もともとが、ヘルプの最終出力となる文字列から引数の構成情報を抽出し、再度ヘルプ出力するような使い方が想定されているように見える。

そのため、静的に構成した引数情報をストリームパーザ(clap.streaming.Parser)用に用意した定義情報渡す場合には少し注意が必要となる。

* `help`関数は、CLI引数の説明文を取得するため、Id型に`description(self: Id)`が存在することを要請している
* `help`関数は、CLI引数が価を受ける場合の代替文字列を取得するため、Id型に`value(self: Id)`が存在することを要請している
    * `---foo=VALUE`のVALUEの文字列のこと

したがって、`clap`用のId型を定義する際、以下のような記述を求められる

```zig
pub const SomeId = enum {
    a,
    b,
    c,

    pub fn description(self: SomeId) []const u8 {
        return switch (self) {
            .a => "CLI引数aの説明文",
            .b => "CLI引数bの説明文",
            .c => "CLI引数cの説明文",
        };
    }
    pub fn value(self: SomeId) []const u8 {
        return switch (self) {
            .a => "CLI引数a用",
            .b => "CLI引数b用",
            .c => "", // フラグ引数だと不要だから空文字列
        };
    }
}
```

毎度、`description`関数と`value`関数を書くのは面倒極まりない。
そこで手抜き方法を考えてみた。

## 方法

まず、説明文やvalue名をルックアップテーブルで引けるよう`std.StaticStringMap`に移す。

```zig
const Dscriptions = std.StaticStringMap(struct {desc: []const u8, val: []const u8}).init(.{
    .{@tagName(.a), .{.desc="CLI引数aの説明文", .val="CLI引数a用"}},
    .{@tagName(.b), .{.desc="CLI引数bの説明文", .val="CLI引数b用"}},
    .{@tagName(.c), .{.desc="CLI引数cの説明文", .val=""}},
});
```

ここではキーを`EnumLiteralの文字列化しているが、`std.meta.EnumField`を受けるラッパーを用意すると、よりｶﾀｰﾝｾﾞﾝ (型安全)となる。

上記のId型の定義は、以下のようになる

```zig
pub const SomeId = enum {
    a,
    b,
    c,

    pub fn description(self: SomeId) []const u8 {
        return Descriptions.get(@tagName(self)).?.desc;
    }
    pub fn value(self: SomeId) []const u8 {
        return Descriptions.get(@tagName(self)).?.val;
    }
}
```

さらに、毎度定型文な`description`や`value`を書くのもめんどくさい。

この部分をヘルパ型として切り出し、`usingnamespace`で`mixin`することでめんどさを多少緩和できる。

```zig
const DescMap = std.StaticStringMap(struct {desc: []const u8, val: []const u8});
fn DescriptionHelper(comptime descs: DescMap) type {
    return struct {
        pub fn description(self: SomeId) []const u8 {
            return descs.get(@tagName(self)).?.desc;
        }
        pub fn value(self: SomeId) []const u8 {
            return descs.get(@tagName(self)).?.val;
        }
    };
}
```

これを使うと、Id定義は以下のように幾分見通しがよくなる。

```zig
pub const SomeId = enum {
    a,
    b,
    c,

    pub usingnamespace DescriptionHelper(Descriptions);
}
```

## さらに一歩進んで

`clap.help`のプロトタイプは、以下となっている。

```zig
pub fn help(
    writer: anytype,
    comptime Id: type,
    params: []const Param(Id),
    opt: HelpOptions)
```

出力先の`writer`はしょうがないとして、`Id`と`params`はなんとかまとめたいのが人情である。

どうせId定義は`clap`専用なので、`params`の定義をIdに含めてしまっても別に構わんのだろ？ってことで`Id`を以下のように定義する。

```zig
pub const SomeId = enum {
    a,
    b,
    c,

    pub Decls: []const clap.Param(@This()) = &.{
        .{.id = .a, ...}.
        ...
    };
    pub usingnamespace DescriptionHelper(Descriptions);
}
```

そうした上で、以下のようなラッパ関数を用意する。

```zig
pub fn help(writer: anytype, comptime Id: type, options: clap.HelpOptions) !void {
    return clap.help(writer, Id, Id.Decls, options);
}
```

ヘルプオプションを決め打ち可能であれば、引数から落とすことでもっと単純化できる。

## おまけ

ローカライズやらなんやらで、Id定義に説明文を直接持たせたくない場合、以下のようにする。

```zig
pub fn SomeId(comptime descs: DescMap) type {
    enum {
        a,
        b,
        c,
    
        pub Decls: []const clap.Param(@This()) = &.{
            .{.id = .a, ...}.
            ...
        };
        pub usingnamespace DescriptionHelper(descs);
    };
}
```

`clap.help`の呼び出して、解決させる。

```zig
try help(writer, Id(desc_i18n)); // desc_i18nはローカライズされた説明文
```

ストリームパース時は説明文は不要なため、以下のようにすればよいでしょう。

```zig
var parser = clap.streaming.Clap(Id(.{}), std.process.ArgIterator){
    .params = ArgId(.{}).Decls,
    .iter = iter,
};
```