@@ -19,8 +19,8 @@ use std::path::PathBuf;
1919use anyhow:: { Context , Result } ;
2020use clap:: builder:: PossibleValuesParser ;
2121use clap:: { Arg , ArgMatches , Command } ;
22- use serde:: { Deserialize , Serialize } ;
23- use serde_json:: { json, Value } ;
22+ use serde:: Serialize ;
23+ use serde_json:: { json, Map , Value } ;
2424
2525use crate :: auth;
2626use crate :: output:: { self , CliError , Mode } ;
@@ -43,7 +43,7 @@ const OFF: &str = "off";
4343/// default is `on` — distinct from `Some(true)`, which is someone turning it
4444/// back on after having turned it off, but read identically by
4545/// [`update_check_setting`].
46- #[ derive( Debug , Default , Clone , PartialEq , Eq , Serialize , Deserialize ) ]
46+ #[ derive( Debug , Default , Clone , PartialEq , Eq , Serialize ) ]
4747struct Config {
4848 #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
4949 update_check : Option < bool > ,
@@ -53,6 +53,33 @@ struct Config {
5353 log : Option < bool > ,
5454 #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
5555 telemetry : Option < bool > ,
56+ /// Keys this version doesn't know, kept so that writing the file doesn't
57+ /// erase a setting a newer version saved.
58+ #[ serde( flatten) ]
59+ rest : Map < String , Value > ,
60+ }
61+
62+ impl Config {
63+ /// Reads each key on its own, so one malformed value resets only that
64+ /// key. Parsed as a whole, a single bad value would reset every key,
65+ /// `telemetry` included, and an opt-out would quietly turn back on.
66+ fn parse ( text : & str ) -> Config {
67+ let Ok ( Value :: Object ( mut rest) ) = serde_json:: from_str ( text) else {
68+ return Config :: default ( ) ;
69+ } ;
70+ let mut flag = |key : & str | rest. remove ( key) . and_then ( |value| value. as_bool ( ) ) ;
71+ let update_check = flag ( "update_check" ) ;
72+ let history = flag ( "history" ) ;
73+ let log = flag ( "log" ) ;
74+ let telemetry = flag ( "telemetry" ) ;
75+ Config {
76+ update_check,
77+ history,
78+ log,
79+ telemetry,
80+ rest,
81+ }
82+ }
5683}
5784
5885fn config_path ( ) -> Option < PathBuf > {
@@ -67,7 +94,7 @@ fn config_path() -> Option<PathBuf> {
6794fn read_config ( ) -> Config {
6895 config_path ( )
6996 . and_then ( |path| std:: fs:: read_to_string ( path) . ok ( ) )
70- . and_then ( |text| serde_json :: from_str ( & text) . ok ( ) )
97+ . map ( |text| Config :: parse ( & text) )
7198 . unwrap_or_default ( )
7299}
73100
@@ -105,8 +132,8 @@ pub fn log_enabled() -> bool {
105132 read_config ( ) . log . unwrap_or ( false )
106133}
107134
108- /// Whether the run's telemetry event may be recorded , per the persisted
109- /// setting. [`crate::telemetry_event`] checks it alongside `MAPBOX_CLI_NO_TELEMETRY`.
135+ /// Whether telemetry is on , per the persisted setting. Read through
136+ /// [`crate::telemetry::telemetry_allowed`], alongside `MAPBOX_CLI_NO_TELEMETRY`.
110137pub fn telemetry_enabled ( ) -> bool {
111138 read_config ( ) . telemetry . unwrap_or ( true )
112139}
@@ -285,15 +312,32 @@ mod tests {
285312 } ;
286313 let text = serde_json:: to_string ( & off) . expect ( "serialize" ) ;
287314 assert_eq ! ( text, r#"{"update_check":false}"# ) ;
288- let read: Config = serde_json:: from_str ( & text) . expect ( "deserialize" ) ;
289- assert_eq ! ( read, off) ;
315+ assert_eq ! ( Config :: parse( & text) , off) ;
290316
291317 // A file from before this key existed, or one with nothing set yet.
292- let empty: Config = serde_json :: from_str ( "{}" ) . expect ( "an empty object ") ;
318+ let empty = Config :: parse ( "{}" ) ;
293319 assert_eq ! ( empty. update_check, None ) ;
294320 assert ! ( update_check_setting( & empty) ) ;
295321 }
296322
323+ #[ test]
324+ fn a_malformed_value_resets_only_its_own_key ( ) {
325+ let config = Config :: parse ( r#"{"telemetry":false,"history":"off"}"# ) ;
326+ assert_eq ! ( config. telemetry, Some ( false ) ) ;
327+ assert_eq ! ( config. history, None ) ;
328+ assert_eq ! ( Config :: parse( "not json" ) , Config :: default ( ) ) ;
329+ }
330+
331+ #[ test]
332+ fn keys_from_a_newer_version_survive_a_write ( ) {
333+ let mut config = Config :: parse ( r#"{"history":true,"future_key":[1]}"# ) ;
334+ config. history = Some ( false ) ;
335+ assert_eq ! (
336+ serde_json:: to_string( & config) . expect( "serialize" ) ,
337+ r#"{"history":false,"future_key":[1]}"#
338+ ) ;
339+ }
340+
297341 #[ test]
298342 fn on_and_off_round_trip_through_on_off ( ) {
299343 assert_eq ! ( on_off( true ) , ON ) ;
0 commit comments