Skip to content

Declare a flag-backed setting once, instead of on both a Cli field and a Config field #1608

Closed

Description

@kixelated

Problem

A setting that is also a flag has to be declared twice today: once on the #[derive(Cli)]/#[derive(Args)] field the program parses, and once on a #[derive(Config)] field that builds SETTINGS_REGISTRY. A struct can't derive both, since each derive refuses the other's keys (usage::Config does not understand `long` ). Registry::drift then checks that the two copies agree.

// What the binary parses.
#[derive(usage_rs::Args)]
pub struct ListenTls {
    /// Load the given certificate from disk.
    #[usage(long = "listen-tls-cert", env = "MOQ_LISTEN_TLS_CERT", setting = "listen.tls.cert")]
    pub cert: Vec<PathBuf>,
}

// The same setting again, for the registry.
#[derive(usage_rs::Config)]
#[usage(prefix = "listen.tls")]
struct ListenTlsSettings {
    #[usage(env = "MOQ_LISTEN_TLS_CERT", cli("--listen-tls-cert"), parse = "list_by_comma")]
    cert: Option<Vec<String>>,
}

Everything the second declaration says is already on the first: the key (setting), the env names (env, deprecated_env), the flags (long/short), the help (the doc comment), and the type (the field). The drift test catches the copies disagreeing, but a CLI that merges every flag into its settings still writes and reviews every flag twice. That is the "second description of every setting" the Config derive was introduced to remove.

Proposal

Let one declaration serve both. Either shape would do:

  1. The parse derives emit registry entries. A Cli/Args field carrying setting = "key" contributes a prop: the type comes from the field (Option<T> → optional, Vec<T> → list, with value_delimiter, or an explicit parse, deciding how a string becomes a list), env/deprecated_env and help come from the same attributes, and cli(...) comes from long/short. A root with #[usage(settings)] exposes the joined registry, composed through Props/concat_props like flattened Config groups are today. Settings with no flag (file-only ones) stay in a #[derive(Config)] struct that is joined in.
  2. One struct may derive both. Each derive ignores the other's keys instead of refusing them, env/deprecated_env/doc comments are shared, and Config infers cli(...) from long/short.

Either way, generating usage-config items can stay behind the existing #[usage(settings)] / config gates, so a CLI without settings still never mentions usage-config. That keeps the layering from #896, where only the root names the config crate. Source presence, precedence, deprecated-name handling and provenance (explain) would stay as they are; only the declaration moves.

Use case

moq (a media relay and CLI tools on usage-rs 7.1) merges every connection, TLS, QUIC and logging flag with a TOML file. Its two registries, moq-tokio/src/settings.rs and moq-relay/src/settings.rs, are 646 lines restating about 110 settings (env name, flag name, list parsing) already declared on the parsed structs, plus a drift test. With either shape, those files and the test go away and CLI behavior stays the same.


AI-assisted — Tool: Claude Code; model: anthropic/claude-opus-5-5; version: 2.1.316.

(Written by Opus 5.5)

Activity

  1. added a commit that references this issue on Nov 3, 2026
    af36eb3
  2. github-actions commented on Nov 3, 2026

    @github-actions
    Contributor

    Fixed in v7.2.0 by #1610.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions