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
use std::{
    collections::HashMap,
    fs::File,
    io::BufReader,
    path::{Path, PathBuf},
};

use directories::ProjectDirs;
use uuid::Uuid;

use crate::Property;

#[derive(thiserror::Error, Debug)]
pub enum ConfigError {
    #[error("Couldn't compute config location")]
    UnknownLocation,

    #[error(transparent)]
    Io(#[from] std::io::Error),

    #[error(transparent)]
    Serde(#[from] serde_json::Error),
}

// NOTE: all the `rename` clauses are to avoid a potential catastrophe :)
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Config {
    #[serde(rename = "analytics_enabled")]
    pub analytics_enabled: bool,

    // NOTE: not a UUID on purpose, it is sometimes useful to use handcrafted IDs.
    #[serde(rename = "analytics_id")]
    pub analytics_id: String,

    /// A unique ID for this session.
    #[serde(skip, default = "::uuid::Uuid::new_v4")]
    pub session_id: Uuid,

    /// Opt-in meta-data you can set via `rerun analytics`.
    ///
    /// For instance Rerun employees are encouraged to set `rerun analytics email`.
    /// For real users, this is always empty.
    #[serde(rename = "metadata", default)]
    pub opt_in_metadata: HashMap<String, Property>,

    /// The path of the config file.
    #[serde(rename = "config_file_path")]
    pub config_file_path: PathBuf,

    /// The directory where pending data is stored.
    #[serde(rename = "data_dir_path")]
    pub data_dir_path: PathBuf,

    /// Is this the first time the user runs the app?
    ///
    /// This is determined based on whether the analytics config already exists on disk.
    #[serde(skip)]
    is_first_run: bool,
}

impl Config {
    pub fn new() -> Result<Self, ConfigError> {
        let dirs = Self::project_dirs()?;
        let config_path = dirs.config_dir().join("analytics.json");
        let data_path = dirs.data_local_dir().join("analytics");
        Ok(Self {
            analytics_id: Uuid::new_v4().to_string(),
            analytics_enabled: true,
            opt_in_metadata: Default::default(),
            session_id: Uuid::new_v4(),
            is_first_run: true,
            config_file_path: config_path,
            data_dir_path: data_path,
        })
    }

    pub fn load_or_default() -> Result<Self, ConfigError> {
        match Self::load()? {
            Some(config) => Ok(config),
            None => Self::new(),
        }
    }

    pub fn load() -> Result<Option<Self>, ConfigError> {
        let dirs = Self::project_dirs()?;
        let config_path = dirs.config_dir().join("analytics.json");
        match File::open(config_path) {
            Ok(file) => {
                let reader = BufReader::new(file);
                let config = serde_json::from_reader(reader)?;
                Ok(Some(config))
            }
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
            Err(err) => Err(ConfigError::Io(err)),
        }
    }

    pub fn save(&self) -> Result<(), ConfigError> {
        // create data directory
        std::fs::create_dir_all(self.data_dir())?;

        // create config file
        std::fs::create_dir_all(self.config_dir())?;
        let file = File::create(self.config_file())?;
        serde_json::to_writer(file, self).map_err(Into::into)
    }

    pub fn config_dir(&self) -> &Path {
        self.config_file_path
            .parent()
            .expect("config file has no parent")
    }

    pub fn config_file(&self) -> &Path {
        &self.config_file_path
    }

    pub fn data_dir(&self) -> &Path {
        &self.data_dir_path
    }

    pub fn is_first_run(&self) -> bool {
        self.is_first_run
    }

    fn project_dirs() -> Result<ProjectDirs, ConfigError> {
        ProjectDirs::from("", "", "rerun").ok_or(ConfigError::UnknownLocation)
    }
}