Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ that may never merge. They are not releases and are not listed here.
set in. One setting today: `update-check`, which `mapbox config set
update-check off` turns off for good, mirroring `MAPBOX_NO_UPDATE_CHECK`.

- `mapbox config list`/`unset`, alongside `get`/`set`. `list` reports every
setting in one call rather than one key at a time; `unset` clears a
setting back to "never set" rather than writing its current default value
explicitly — the difference that lets a later default change reach a
cleared key but not one a caller pinned to the old default on purpose.

- The README now documents installing without the install script: the
archives are plain HTTP downloads, `manifest.json` lists every target with
its checksum, and the commands to verify and extract one are written out.
Expand Down
84 changes: 83 additions & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ nests, and is typed `mapbox styles draft get`.
**[Uninstall](#uninstall)** — [uninstall](#mapbox-uninstall)

**[Config](#config)** — [config.get](#mapbox-config-get) ·
[config.set](#mapbox-config-set)
[config.set](#mapbox-config-set) · [config.list](#mapbox-config-list) ·
[config.unset](#mapbox-config-unset)

**[Usage](#usage)** — [usage](#mapbox-usage)

Expand Down Expand Up @@ -3196,6 +3197,87 @@ update-check set to off.
</td></tr>
</table>

### `mapbox config list`

Lists every setting and its current value — `get` answers one key at a
time, this answers all of them in one call, falling back to each one's
default the same way `get` does.

#### Parameters

None.

#### Examples

```sh
mapbox config list
```

#### Outputs

<table>
<tr><th width="50%"><code>text</code></th><th width="50%"><code>json</code></th></tr>
<tr><td>

```
update-check on
```

</td><td>

```json
[
{
"key": "update-check",
"value": true
}
]
```

</td></tr>
</table>

### `mapbox config unset`

Clears a setting back to its default, rather than setting it to that
default value explicitly. The difference matters the next time this CLI
changes what a setting's default is: a cleared key picks up the new
default, a key explicitly set to the old default value does not.

#### Parameters

| Parameter | Effect |
| --- | --- |
| `<key>` | Which setting to clear. Only `update-check` exists today. |

#### Examples

```sh
mapbox config unset update-check
```

#### Outputs

<table>
<tr><th width="50%"><code>text</code></th><th width="50%"><code>json</code></th></tr>
<tr><td>

```
update-check cleared, now on (default).
```

</td><td>

```json
{
"key": "update-check",
"value": true
}
```

</td></tr>
</table>

---

## Usage
Expand Down
125 changes: 110 additions & 15 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@
//! file beside the credentials, written through the same
//! [`crate::auth::write_private`] so it gets the same `0600` treatment.
//!
//! One setting today — `update-check` — with room for more: `get`/`set` take
//! a `key`, restricted by clap to [`KEYS`], so adding a second setting is a
//! new key and a new match arm rather than a new pair of subcommands.
//! One setting today — `update-check` — with room for more: `get`/`set`/
//! `unset` take a `key`, restricted by clap to [`KEYS`], so adding a second
//! setting is a new key and a new match arm rather than a new subcommand.
//! `list` needs no key at all: it walks [`KEYS`] and reports every setting's
//! current value in one call, which `get` cannot — the whole reason it
//! exists alongside `get`/`set` rather than waiting for a second setting to
//! make the gap visible.

use std::path::PathBuf;

use anyhow::{Context, Result};
use clap::builder::PossibleValuesParser;
use clap::{Arg, ArgMatches, Command};
use serde::{Deserialize, Serialize};
use serde_json::json;
use serde_json::{json, Value};

use crate::auth;
use crate::output::{self, Mode};
Expand Down Expand Up @@ -87,6 +91,28 @@ fn on_off(enabled: bool) -> &'static str {
}
}

/// `key`'s current value in `config`, resolved to its default the same way
/// every getter here does. Shared by [`get`] and [`list`] so the two cannot
/// answer a key differently.
fn resolve(config: &Config, key: &str) -> bool {
match key {
UPDATE_CHECK_KEY => update_check_setting(config),
_ => unreachable!("clap's value_parser restricts `key` to {KEYS:?}"),
}
}

/// Clears `key` back to "never set" in `config`, in place. The counterpart
/// to `set`'s `Some(enabled)` — distinct from setting a key to its default
/// value, which [`resolve`] would read identically but which
/// `serde(skip_serializing_if)` would not write identically: a later default
/// change reaches only a key that was actually cleared.
fn clear(config: &mut Config, key: &str) {
match key {
UPDATE_CHECK_KEY => config.update_check = None,
_ => unreachable!("clap's value_parser restricts `key` to {KEYS:?}"),
}
}

pub fn command() -> Command {
let key_arg = || {
Arg::new("key")
Expand All @@ -113,23 +139,30 @@ pub fn command() -> Command {
.arg(key_arg())
.arg(Arg::new("value").required(true).value_parser([ON, OFF])),
)
.subcommand(Command::new("list").about("List every setting and its current value"))
.subcommand(
Command::new("unset")
.about("Clear a setting back to its default")
.long_about(
"Clear a setting back to its default, rather than setting it to that \
default value explicitly — the difference matters the next time this \
CLI changes what the default is: a cleared key picks up the new default, \
a key explicitly set to the old default value does not.",
)
.arg(key_arg()),
)
}

pub fn get(matches: &ArgMatches, mode: Mode) -> Result<()> {
let key = matches.get_one::<String>("key").expect("required");
let config = read_config();
let enabled = resolve(&config, key);

match key.as_str() {
UPDATE_CHECK_KEY => {
let enabled = update_check_setting(&config);
output::emit(
mode,
on_off(enabled),
json!({ "key": key, "value": enabled }),
)
}
_ => unreachable!("clap's value_parser restricts `key` to {KEYS:?}"),
}
output::emit(
mode,
on_off(enabled),
json!({ "key": key, "value": enabled }),
)
}

pub fn set(matches: &ArgMatches, mode: Mode) -> Result<()> {
Expand All @@ -151,6 +184,37 @@ pub fn set(matches: &ArgMatches, mode: Mode) -> Result<()> {
)
}

pub fn list(mode: Mode) -> Result<()> {
let config = read_config();
let entries: Vec<Value> = KEYS
.iter()
.map(|key| json!({ "key": key, "value": resolve(&config, key) }))
.collect();

let text = KEYS
.iter()
.map(|key| format!("{key}\t{}", on_off(resolve(&config, key))))
.collect::<Vec<_>>()
.join("\n");

output::emit(mode, &text, Value::Array(entries))
}

pub fn unset(matches: &ArgMatches, mode: Mode) -> Result<()> {
let key = matches.get_one::<String>("key").expect("required");

let mut config = read_config();
clear(&mut config, key);
write_config(&config)?;

let enabled = resolve(&config, key);
output::emit(
mode,
&format!("{key} cleared, now {} (default).", on_off(enabled)),
json!({ "key": key, "value": enabled }),
)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -181,4 +245,35 @@ mod tests {
assert_eq!(on_off(true), ON);
assert_eq!(on_off(false), OFF);
}

#[test]
fn resolve_matches_update_check_setting_at_every_state() {
for update_check in [None, Some(true), Some(false)] {
let config = Config { update_check };
assert_eq!(
resolve(&config, UPDATE_CHECK_KEY),
update_check_setting(&config)
);
}
}

/// `clear` and `set` leave different bytes on disk even when they leave
/// the same *value*: this is the difference `unset` exists to offer, so
/// it is worth pinning rather than only exercising through `resolve`,
/// which cannot tell the two states apart by design.
#[test]
fn clear_removes_the_key_rather_than_writing_the_default() {
let mut explicit_default = Config {
update_check: Some(true),
};
clear(&mut explicit_default, UPDATE_CHECK_KEY);
assert_eq!(explicit_default, Config::default());
assert_eq!(explicit_default.update_check, None);

let mut explicit_off = Config {
update_check: Some(false),
};
clear(&mut explicit_off, UPDATE_CHECK_KEY);
assert_eq!(explicit_off.update_check, None);
}
}
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,8 @@ fn run(app: &Command, specs: &[ServiceSpec], matches: &ArgMatches, mode: Mode) -
Some((config::COMMAND, config_matches)) => match config_matches.subcommand() {
Some(("get", get_matches)) => config::get(get_matches, mode)?,
Some(("set", set_matches)) => config::set(set_matches, mode)?,
Some(("list", _)) => config::list(mode)?,
Some(("unset", unset_matches)) => config::unset(unset_matches, mode)?,
_ => unreachable!("`config` sets subcommand_required(true)"),
},
// Token resolution mirrors the service arm below, minus path
Expand Down
64 changes: 64 additions & 0 deletions tests/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,70 @@ fn an_unknown_key_or_value_is_a_usage_error_not_a_panic() {
);
}

#[test]
fn list_reports_every_setting_including_an_unset_one() {
let home = scratch("list");

// Nothing set yet: list still names the one known key, at its default.
let empty = command(&home)
.args(["-o", "json", "config", "list"])
.output()
.expect("run mapbox config list");
assert!(empty.status.success());
assert_eq!(stdout(&empty), r#"[{"key":"update-check","value":true}]"#);

let set = command(&home)
.args(["config", "set", "update-check", "off"])
.output()
.expect("run mapbox config set");
assert!(set.status.success());

let after = command(&home)
.args(["-o", "json", "config", "list"])
.output()
.expect("run mapbox config list");
assert!(after.status.success());
assert_eq!(stdout(&after), r#"[{"key":"update-check","value":false}]"#);

let text = command(&home)
.args(["-o", "text", "config", "list"])
.output()
.expect("run mapbox config list");
assert!(text.status.success());
assert_eq!(stdout(&text), "update-check\toff");
}

#[test]
fn unset_clears_the_key_rather_than_writing_the_default() {
let home = scratch("unset");
let path = config_dir(&home).join("config.json");

// Set to `on` explicitly — the default value, but written, not absent.
let set = command(&home)
.args(["config", "set", "update-check", "on"])
.output()
.expect("run mapbox config set");
assert!(set.status.success());
let written = std::fs::read_to_string(&path).expect("config.json exists");
assert!(
written.contains("update_check"),
"an explicit `on` should still be written: {written:?}"
);

let unset = command(&home)
.args(["-o", "json", "config", "unset", "update-check"])
.output()
.expect("run mapbox config unset");
assert!(unset.status.success());
assert_eq!(stdout(&unset), r#"{"key":"update-check","value":true}"#);

let cleared = std::fs::read_to_string(&path).expect("config.json still exists");
assert_eq!(
cleared, "{}",
"unset should remove the key, not merely write back its default"
);
}

#[cfg(unix)]
#[test]
fn the_config_file_is_written_private() {
Expand Down
Loading