onetaskgraph 0.2.18

One interface over the ticketing systems your work lives in.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
//! The command line, and the configuration layer it contributes.
//!
//! Flags are the highest of the three layers, and every setting is reachable here —
//! including every field of every named source, through `--set`. That is what makes
//! the product scriptable: a caller who can name a setting in a document can name the
//! same setting on the command line, at the same dotted path.

use std::num::NonZeroU32;

use clap::{Args, Parser, Subcommand, ValueEnum};
use onetaskgraph_core::config::{Layer, Origin, Setting, SettingPath, value_from_text};
use onetaskgraph_core::{OutputFormat, PluginKind, SearchKind};
use onetaskgraph_plugin_api::{Direction, StatusCategory, TextFields};
use serde_json::Value;

/// One interface over the ticketing systems your work lives in.
///
/// Exit codes: `0` on success, `1` when a command failed while running, `2` when the
/// invocation itself was wrong (clap's own code for that), `4` when a query succeeded
/// for some sources and failed for others without `--allow-partial`. `0` means success
/// and nothing else: a run that reached no source, or lost one, never exits `0` unless
/// you asked for a partial answer.
#[derive(Debug, Parser)]
// `bin_name` is pinned rather than left to clap, which takes it from argv[0] — and on
// Windows argv[0] is `onetaskgraph.exe`, so the usage line would name a different command
// there than the one this declares and than the one the documentation tells a user to type.
#[command(name = "onetaskgraph", bin_name = "onetaskgraph", version)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Command,

    #[command(flatten)]
    pub overrides: Overrides,
}

/// The verbs this binary answers.
#[derive(Debug, Subcommand)]
pub enum Command {
    /// Host one compiled-in source over the stdio plugin protocol.
    #[command(hide = true)]
    PluginServe {
        /// The compiled-in plugin kind to host.
        source: PluginKind,
    },

    /// Print the JSON Schema bundle the contract types generate.
    ///
    /// Both SDKs are generated from this document, so it is emitted from the
    /// running binary rather than committed: the schema and the types that
    /// serialise cannot drift when they are the same types.
    Schema,

    /// Work with the configuration this command reads.
    Config {
        #[command(subcommand)]
        command: ConfigCommand,
    },

    /// Work with the sources this configuration names.
    Sources {
        #[command(subcommand)]
        command: SourcesCommand,
    },

    /// List, show and walk tasks.
    Task {
        #[command(subcommand)]
        command: TaskCommand,
    },

    /// List, show and walk projects.
    Project {
        #[command(subcommand)]
        command: ProjectCommand,
    },

    /// List, show and copy documents.
    ///
    /// A document is not work: it has no status and takes part in no dependency graph, so
    /// this group carries no `--status` filter and no `deps` verb.
    Document {
        #[command(subcommand)]
        command: DocumentCommand,
    },

    /// List the labels the sources know.
    Label {
        #[command(subcommand)]
        command: LabelCommand,
    },

    /// Search tasks, projects, or both.
    Search(SearchArgs),
}

/// What `onetaskgraph sources` can do.
#[derive(Debug, Subcommand)]
pub enum SourcesCommand {
    /// List every configured source, its plugin, and what it declares it can do.
    ///
    /// A source that could not be built is listed too, with the reason — one broken
    /// credential is a source you can see is broken, not a command that stops working.
    List,
}

/// What `onetaskgraph task` can do.
#[derive(Debug, Subcommand)]
pub enum TaskCommand {
    /// List tasks across the selected sources.
    List(TaskListArgs),
    /// Show one task by its qualified id, `<source>:<native-id>`.
    Show(ShowArgs),
    /// Walk one task's dependency edges.
    Deps(DependencyArgs),
    /// Copy tasks into another configured source, by qualified id.
    Copy(TaskCopyArgs),
}

/// What `onetaskgraph project` can do.
#[derive(Debug, Subcommand)]
pub enum ProjectCommand {
    /// List projects across the selected sources.
    List(ProjectListArgs),
    /// Show one project by its qualified id, `<source>:<native-id>`.
    Show(ShowArgs),
    /// Walk one project's dependency edges.
    Deps(DependencyArgs),
    /// Copy one project, and the tasks in it, into another configured source.
    Copy(ProjectCopyArgs),
}

/// What `onetaskgraph document` can do.
///
/// No `deps`: a document takes part in no dependency graph, so there is nothing for a
/// dependency verb here to walk.
#[derive(Debug, Subcommand)]
pub enum DocumentCommand {
    /// List documents across the selected sources.
    List(DocumentListArgs),
    /// Show one document by its qualified id, `<source>:<native-id>`.
    Show(ShowArgs),
    /// Copy documents into another configured source, by qualified id.
    Copy(DocumentCopyArgs),
}

/// What `onetaskgraph label` can do.
#[derive(Debug, Subcommand)]
pub enum LabelCommand {
    /// List every label the selected sources know.
    List(LabelListArgs),
}

/// Which sources a query addresses.
#[derive(Debug, Args)]
pub struct SelectionArgs {
    /// Address this source. Repeat for several; omit for the configured selection.
    ///
    /// llmlint: ignore[invalid_states_unrepresentable] — a `SourceName` here would move
    /// the refusal into clap, which reports it as an invalid *invocation* (exit 2) under
    /// clap's own wording. A name that cannot be a source name and one that names no
    /// configured source are the same typo to the user, and both owe the same next
    /// action; `selection` in `main` converts through `SourceName::new` immediately and
    /// attaches it, at the exit code the documented table gives that mistake.
    #[arg(long = "source", value_name = "S")]
    pub source: Vec<String>,
}

/// The filters the list verbs share.
#[derive(Debug, Args)]
pub struct FilterArgs {
    /// Keep items carrying this label. Repeat to require several at once.
    #[arg(long = "label", value_name = "L")]
    pub label: Vec<String>,

    /// Drop items carrying this label. Repeat for several.
    #[arg(long = "not-label", value_name = "L")]
    pub not_label: Vec<String>,

    /// Keep items in this status category. Repeat for several.
    #[arg(long = "status", value_name = "S")]
    pub status: Vec<StatusArg>,

    /// Keep items matching this text.
    #[arg(long, value_name = "TEXT")]
    pub search: Option<String>,

    /// Which fields --search looks in.
    #[arg(long = "in", value_name = "FIELDS", default_value = "both")]
    pub fields: FieldsArg,
}

/// The filters a document list carries.
///
/// [`FilterArgs`] without `--status`, and its own type rather than a shared one for the
/// reason [`DocumentFilters`](onetaskgraph_core::DocumentFilters) is: a document has no
/// status, so a status flag here could only be accepted and ignored.
#[derive(Debug, Args)]
pub struct DocumentFilterArgs {
    /// Keep documents carrying this label. Repeat to require several at once.
    #[arg(long = "label", value_name = "L")]
    pub label: Vec<String>,

    /// Drop documents carrying this label. Repeat for several.
    #[arg(long = "not-label", value_name = "L")]
    pub not_label: Vec<String>,

    /// Keep documents matching this text.
    #[arg(long, value_name = "TEXT")]
    pub search: Option<String>,

    /// Which fields --search looks in.
    #[arg(long = "in", value_name = "FIELDS", default_value = "both")]
    pub fields: FieldsArg,
}

/// How much of a result set to return, and how much to say about it.
#[derive(Debug, Args)]
pub struct PageArgs {
    /// How many items this page holds. Defaults to the `page_size` setting.
    ///
    /// A `NonZeroU32` rather than a range-checked `u32`: a page of no rows is not a page,
    /// and typing it should be refused where it was typed rather than carried inwards as
    /// a number some later layer has to remember to check.
    #[arg(long, value_name = "N")]
    pub limit: Option<NonZeroU32>,

    /// Resume from a token a previous page reported.
    ///
    /// llmlint: ignore[invalid_states_unrepresentable] — a `PageToken` here would only
    /// move the *encoding* check to parse time, and a token is refused for three reasons
    /// beyond its encoding — a configuration it cannot address, a query it did not come
    /// from, a stream it does not resume — none of which is decidable before the
    /// configuration is loaded. Splitting one mistake ("I pasted the wrong token") across
    /// clap's exit 2 and the run's exit 1 is what that would buy.
    #[arg(long = "page", value_name = "TOKEN")]
    pub page: Option<String>,

    /// Report what each source was asked and what the engine did itself.
    #[arg(long)]
    pub explain: bool,

    /// Accept an answer some sources could not contribute to, and exit 0.
    #[arg(long = "allow-partial")]
    pub allow_partial: bool,
}

/// `onetaskgraph task list`.
#[derive(Debug, Args)]
pub struct TaskListArgs {
    #[command(flatten)]
    pub selection: SelectionArgs,

    #[command(flatten)]
    pub filters: FilterArgs,

    /// Keep tasks in this project, qualified (`work:PROJ-1`) or by native id.
    ///
    /// A qualified id names one project of one source, so it narrows the query to that
    /// source. A bare id is asked of every selected source.
    #[arg(long, value_name = "P", conflicts_with = "no_project")]
    pub project: Option<String>,

    /// Keep only tasks belonging to no project at all.
    #[arg(long = "no-project")]
    pub no_project: bool,

    #[command(flatten)]
    pub paging: PageArgs,
}

/// `onetaskgraph project list`.
#[derive(Debug, Args)]
pub struct ProjectListArgs {
    #[command(flatten)]
    pub selection: SelectionArgs,

    #[command(flatten)]
    pub filters: FilterArgs,

    #[command(flatten)]
    pub paging: PageArgs,
}

/// `onetaskgraph document list`.
#[derive(Debug, Args)]
pub struct DocumentListArgs {
    #[command(flatten)]
    pub selection: SelectionArgs,

    #[command(flatten)]
    pub filters: DocumentFilterArgs,

    /// Keep documents in this project, qualified (`work:PROJ-1`) or by native id.
    ///
    /// A qualified id names one project of one source, so it narrows the query to that
    /// source. A bare id is asked of every selected source.
    #[arg(long, value_name = "P", conflicts_with = "no_project")]
    pub project: Option<String>,

    /// Keep only documents belonging to no project at all.
    #[arg(long = "no-project")]
    pub no_project: bool,

    #[command(flatten)]
    pub paging: PageArgs,
}

/// `onetaskgraph label list`.
#[derive(Debug, Args)]
pub struct LabelListArgs {
    #[command(flatten)]
    pub selection: SelectionArgs,

    #[command(flatten)]
    pub paging: PageArgs,
}

/// `onetaskgraph task show` and `onetaskgraph project show`.
#[derive(Debug, Args)]
pub struct ShowArgs {
    /// The qualified id, `<source>:<native-id>`.
    ///
    /// llmlint: ignore[invalid_states_unrepresentable] — a `GlobalId` here would refuse an
    /// unqualified id as a bad invocation, under clap's wording. `qualified` in `main`
    /// converts through `GlobalId::from_str` immediately and says what a qualified id is
    /// and where to read the configured names, which is the answer a user typing `T-1`
    /// needs and the one this repository's failure journeys assert on.
    #[arg(value_name = "ID")]
    pub id: String,

    /// Report what the source was asked.
    #[arg(long)]
    pub explain: bool,

    /// Accept an answer the source could not contribute to, and exit 0.
    #[arg(long = "allow-partial")]
    pub allow_partial: bool,
}

/// `onetaskgraph task deps` and `onetaskgraph project deps`.
#[derive(Debug, Args)]
pub struct DependencyArgs {
    /// The qualified id, `<source>:<native-id>`.
    ///
    /// llmlint: ignore[invalid_states_unrepresentable] — a `GlobalId` here would refuse an
    /// unqualified id as a bad invocation, under clap's wording. `qualified` in `main`
    /// converts through `GlobalId::from_str` immediately and says what a qualified id is
    /// and where to read the configured names, which is the answer a user typing `T-1`
    /// needs and the one this repository's failure journeys assert on.
    #[arg(value_name = "ID")]
    pub id: String,

    /// Which way to walk. Reverse is emulated for a forward-only source.
    #[arg(long, value_name = "DIRECTION", default_value = "depends-on")]
    pub direction: DirectionArg,

    #[command(flatten)]
    pub paging: PageArgs,
}

/// The arguments both copy verbs share.
///
/// A copy is one write into one destination, so there is no paging, no `--explain` and no
/// `--allow-partial` here: a partial write is not an answer a caller could act on.
#[derive(Debug, Args)]
pub struct CopyArgs {
    /// The configured source to copy into. A source name, never a qualified id.
    ///
    /// llmlint: ignore[invalid_states_unrepresentable] — a `SourceName` here would move
    /// the refusal into clap, which reports it as an invalid *invocation* (exit 2) under
    /// clap's own wording, for the reason recorded on `SelectionArgs::source`. A name
    /// that cannot be a source name and one that names no configured source are the same
    /// typo, and both owe the same next action.
    #[arg(long, value_name = "SOURCE")]
    pub to: String,

    /// Re-establish a lost correspondence by matching on `title` or on a metadata key.
    #[arg(long = "match-by", value_name = "KEY")]
    pub match_by: Option<String>,

    /// Create a new destination item when a recorded origin names nothing there.
    #[arg(long)]
    pub recreate: bool,

    /// Perform every read, write nothing, and report what would have happened.
    #[arg(long = "dry-run")]
    pub dry_run: bool,
}

/// `onetaskgraph task copy`.
#[derive(Debug, Args)]
pub struct TaskCopyArgs {
    /// The qualified ids to copy, `<source>:<native-id>`.
    ///
    /// llmlint: ignore[invalid_states_unrepresentable] — a `GlobalId` here would refuse an
    /// unqualified id as a bad invocation, under clap's wording, for the reason recorded
    /// on `ShowArgs::id`. `qualified` in `main` converts through `GlobalId::from_str` and
    /// says what a qualified id is and where to read the configured names.
    #[arg(value_name = "ID", required = true)]
    pub id: Vec<String>,

    #[command(flatten)]
    pub copy: CopyArgs,
}

/// `onetaskgraph project copy`.
#[derive(Debug, Args)]
pub struct ProjectCopyArgs {
    /// The qualified id to copy, `<source>:<native-id>`.
    ///
    /// llmlint: ignore[invalid_states_unrepresentable] — as `TaskCopyArgs::id`.
    #[arg(value_name = "ID")]
    pub id: String,

    /// Copy the project alone, leaving the tasks in it where they are.
    #[arg(long = "no-tasks")]
    pub no_tasks: bool,

    #[command(flatten)]
    pub copy: CopyArgs,
}

/// `onetaskgraph document copy`.
#[derive(Debug, Args)]
pub struct DocumentCopyArgs {
    /// The qualified ids to copy, `<source>:<native-id>`.
    ///
    /// llmlint: ignore[invalid_states_unrepresentable] — as `TaskCopyArgs::id`.
    #[arg(value_name = "ID", required = true)]
    pub id: Vec<String>,

    #[command(flatten)]
    pub copy: CopyArgs,
}

/// `onetaskgraph search`.
#[derive(Debug, Args)]
pub struct SearchArgs {
    /// What to look for.
    #[arg(value_name = "TEXT")]
    pub text: String,

    /// Which fields to look in.
    #[arg(long = "in", value_name = "FIELDS", default_value = "both")]
    pub fields: FieldsArg,

    /// Which entities to search.
    #[arg(long, value_name = "KIND", default_value = "both")]
    pub kind: KindArg,

    #[command(flatten)]
    pub selection: SelectionArgs,

    #[command(flatten)]
    pub paging: PageArgs,
}

/// A status category, as the command line spells it.
///
/// A command-line mirror of [`StatusCategory`] rather than that type itself, for the
/// reason [`Format`] carries: deriving clap's `ValueEnum` on a contract type would put
/// clap into the plugin contract's dependencies for the sake of one flag.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum StatusArg {
    /// Written down but not yet committed to as work.
    Draft,
    /// Known about, not yet queued.
    Backlog,
    /// Queued, not yet started.
    Todo,
    /// Being worked on.
    InProgress,
    /// Finished.
    Done,
    /// Abandoned.
    Cancelled,
    /// The source reported a status this vocabulary cannot place.
    Unknown,
}

impl StatusArg {
    /// The contract's own category.
    #[must_use]
    pub fn category(self) -> StatusCategory {
        match self {
            Self::Draft => StatusCategory::Draft,
            Self::Backlog => StatusCategory::Backlog,
            Self::Todo => StatusCategory::Todo,
            Self::InProgress => StatusCategory::InProgress,
            Self::Done => StatusCategory::Done,
            Self::Cancelled => StatusCategory::Cancelled,
            Self::Unknown => StatusCategory::Unknown,
        }
    }
}

/// Which fields a search covers, as the command line spells it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum FieldsArg {
    /// Titles only.
    Title,
    /// Bodies only.
    Content,
    /// Either one matching is a match.
    Both,
}

impl FieldsArg {
    /// The contract's own field selector.
    #[must_use]
    pub fn fields(self) -> TextFields {
        match self {
            Self::Title => TextFields::Title,
            Self::Content => TextFields::Content,
            Self::Both => TextFields::TitleOrContent,
        }
    }
}

/// Which way a dependency walk goes, as the command line spells it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum DirectionArg {
    /// What this item depends on.
    DependsOn,
    /// What depends on this item.
    DependedOnBy,
}

impl DirectionArg {
    /// The contract's own direction.
    #[must_use]
    pub fn direction(self) -> Direction {
        match self {
            Self::DependsOn => Direction::DependsOn,
            Self::DependedOnBy => Direction::DependedOnBy,
        }
    }
}

/// Which entities a search covers, as the command line spells it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum KindArg {
    /// Tasks only.
    Task,
    /// Projects only.
    Project,
    /// Both, interleaved.
    Both,
}

impl KindArg {
    /// The engine's own search scope.
    #[must_use]
    pub fn kind(self) -> SearchKind {
        match self {
            Self::Task => SearchKind::Tasks,
            Self::Project => SearchKind::Projects,
            Self::Both => SearchKind::Both,
        }
    }
}

/// What `onetaskgraph config` can do.
#[derive(Debug, Subcommand)]
pub enum ConfigCommand {
    /// Show every setting, with the layer its value came from.
    ///
    /// The layer is named exactly: which document, which environment variable, or
    /// which flag. Rendered as JSON when `output` is `json` — which `--json` sets.
    Show,
}

/// The command-line layer: any setting, at the top of the stack.
#[derive(Debug, Args)]
pub struct Overrides {
    /// Set any setting: --set sources.work.config.root=/tmp/tasks
    ///
    /// The path is the same dotted path a document uses and the same one the
    /// `ONETASKGRAPH_` variables encode, so one name works at all three layers.
    #[arg(long = "set", value_name = "PATH=VALUE", global = true)]
    pub set: Vec<String>,

    /// How many items one page holds.
    ///
    /// Refused at zero here rather than at load: this flag parses on every verb, so a
    /// value only the configuration loader would have caught is one the verbs that do
    /// not load a configuration would accept in silence.
    #[arg(
        long,
        value_name = "N",
        global = true,
        value_parser = clap::value_parser!(u32).range(1..)
    )]
    pub page_size: Option<u32>,

    /// Which sources answer when a command names none.
    #[arg(long, value_name = "NAMES", value_delimiter = ',', global = true)]
    pub default_sources: Option<Vec<String>>,

    /// How output is rendered.
    #[arg(long, value_name = "FORMAT", global = true, conflicts_with = "json")]
    pub output: Option<Format>,

    /// Shorthand for --output json.
    #[arg(long, global = true)]
    pub json: bool,
}

impl Overrides {
    /// These flags as one configuration layer.
    ///
    /// # Errors
    ///
    /// Returns a message naming the flag when a `--set` argument is not
    /// `PATH=VALUE`, or its path addresses nothing.
    pub fn layer(&self) -> Result<Layer, String> {
        let mut settings = Vec::new();

        if let Some(page_size) = self.page_size {
            settings.push(at("page_size", Value::from(page_size), "--page-size"));
        }
        if let Some(names) = &self.default_sources {
            let names: Vec<Value> = names.iter().map(|name| Value::from(name.clone())).collect();
            settings.push(at(
                "default_sources",
                Value::Array(names),
                "--default-sources",
            ));
        }
        if let Some(format) = self.output {
            settings.push(at("output", format.setting(), "--output"));
        }
        if self.json {
            settings.push(at("output", Value::from("json"), "--json"));
        }

        // Last, so `--set output=text` beats `--json`: the general form is the more
        // specific instruction, and a caller who spells a path out means it.
        for assignment in &self.set {
            settings.push(assignment_setting(assignment)?);
        }

        Ok(Layer::new(settings))
    }
}

/// How output is rendered, as the command line accepts it.
///
/// A command-line mirror of [`OutputFormat`] rather than that type itself, because
/// deriving clap's `ValueEnum` on it would put clap into the engine's dependencies for
/// the sake of one flag. The two cannot disagree about *spelling*: [`Format::setting`]
/// produces its value by serialising the `OutputFormat` it stands for, so what reaches
/// the configuration is whatever the engine's own type writes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum Format {
    /// For a person reading a terminal.
    Text,
    /// For a program.
    Json,
}

impl Format {
    /// This format as the `output` setting's value.
    #[must_use]
    pub fn setting(self) -> Value {
        let format = match self {
            Self::Text => OutputFormat::Text,
            Self::Json => OutputFormat::Json,
        };
        serde_json::to_value(format).expect("an output format renders as JSON")
    }
}

/// One setting at a path this binary spells itself.
fn at(key: &str, value: Value, flag: &str) -> Setting {
    Setting {
        key: SettingPath::parse(key).expect("a path this binary spells has no empty segment"),
        value,
        origin: Origin::Flag {
            flag: flag.to_owned(),
        },
    }
}

/// One `--set PATH=VALUE` argument.
fn assignment_setting(assignment: &str) -> Result<Setting, String> {
    let Some((path, value)) = assignment.split_once('=') else {
        return Err(format!(
            "--set {assignment}: that is not an assignment\n\
             next: write it as --set PATH=VALUE, for example --set page_size=10."
        ));
    };
    Ok(Setting {
        key: SettingPath::parse(path).map_err(|error| format!("--set {error}"))?,
        value: value_from_text(value),
        origin: Origin::Flag {
            flag: format!("--set {path}"),
        },
    })
}