repo_description: 🥤Docker container updates made easy
repo_topics: cli container docker docker-container docker-image dockerhub react rust self-hosted server tailwindcss terminal update update-checker updates
code: use rustc_hash::FxHashMap;
  use serde::Deserialize;
  use serde::Deserializer;
  use std::env;
  use std::mem;
  use std::path::PathBuf;
  
  use crate::error;
  
  // We can't assign `a` to `b` in the loop in `Config::load`, so we'll have to use swap. It looks ugly so now we have a macro for it.
  macro_rules! swap {
      ($a:expr, $b:expr) => {
          mem::swap(&mut $a, &mut $b)
      };
  }
  
  #[derive(Clone, Deserialize)]
  #[serde(rename_all = "snake_case")]
  pub enum Theme {
      Default,
      Blue,
  }
  
  impl Default for Theme {
      fn default() -> Self {
          Self::Default
      }
  }
  
  #[derive(Clone, Deserialize)]
  #[serde(rename_all = "snake_case")]
  pub enum UpdateType {
      None,
      Major,
      Minor,
      Patch,
  }
  
  impl Default for UpdateType {
      fn default() -> Self {
          Self::None
      }
  }
  
  #[derive(Clone, Deserialize, Default)]
  #[serde(deny_unknown_fields)]
  #[serde(default)]
  pub struct RegistryConfig {
      pub authentication: Option<String>,
      pub insecure: bool,
      pub ignore: bool,
  }
  
  #[derive(Clone, Deserialize, Default)]
  #[serde(default)]
  pub struct ImageConfig {
      pub extra: Vec<String>,
      pub exclude: Vec<String>,
  }
  
  #[derive(Clone, Deserialize)]
  #[serde(default)]
  pub struct Config {
      version: u8,
      pub agent: bool,
      pub ignore_update_type: UpdateType,
      pub images: ImageConfig,
      #[serde(deserialize_with = "empty_as_none")]
      pub refresh_interval: Option<String>,
      pub registries: FxHashMap<String, RegistryConfig>,
      pub servers: FxHashMap<String, String>,
      pub socket: Option<String>,
      pub theme: Theme,
  }
  
  impl Config {
      pub fn new() -> Self {
          Self {
              version: 3,
              agent: false,
              ignore_update_type: UpdateType::default(),
              images: ImageConfig::default(),
              refresh_interval: None,
              registries: FxHashMap::default(),
              servers: FxHashMap::default(),
              socket: None,
              theme: Theme::Default,
          }
      }
  
      /// Loads file and env config and merges them
      pub fn load(&mut self, path: Option<PathBuf>) -> Self {
          let mut config = self.load_file(path);
  
          // Get environment variables with CUP_ prefix
          let env_vars: FxHashMap<String, String> =
              env::vars().filter(|(k, _)| k.starts_with("CUP_")).collect();
  
          if !env_vars.is_empty() {
              if let Ok(mut cfg) = envy::prefixed("CUP_").from_env::<Config>() {
                  // If we have environment variables, override config.json options
                  for (key, _) in env_vars {
                      match key.as_str() {
                          "CUP_AGENT" => config.agent = cfg.agent,
                          #[rustfmt::skip]
                          "CUP_IGNORE_UPDATE_TYPE" => swap!(config.ignore_update_type, cfg.ignore_update_type),
                          #[rustfmt::skip]
                          "CUP_REFRESH_INTERVAL" => swap!(config.refresh_interval, cfg.refresh_interval),
                          "CUP_SOCKET" => swap!(config.socket, cfg.socket),
                          "CUP_THEME" => swap!(config.theme, cfg.theme),
                          // The syntax for these is slightly more complicated, not sure if they should be enabled or not. Let's stick to simple types for now.
                          // "CUP_IMAGES" => swap!(config.images, cfg.images),
                          // "CUP_REGISTRIES" => swap!(config.registries, cfg.registries),
                          // "CUP_SERVERS" => swap!(config.servers, cfg.servers),
                          _ => (), // Maybe print a warning if other CUP_ variables are detected
                      }
                  }
              }
          }
  
          config
      }
  
      /// Reads the config from the file path provided and returns the parsed result.
      fn load_file(&self, path: Option<PathBuf>) -> Self {
          let raw_config = match &path {
              Some(path) => std::fs::read_to_string(path),
              None => return Self::new(), // Empty config
          };
          if raw_config.is_err() {
              error!(
                  "Failed to read config file from {}. Are you sure the file exists?",
                  &path.unwrap().to_str().unwrap()
              )
          };
          self.parse(&raw_config.unwrap()) // We can safely unwrap here
      }
      /// Parses and validates the config.
      fn parse(&self, raw_config: &str) -> Self {
          let config: Self = match serde_json::from_str(raw_config) {
              Ok(config) => config,
              Err(e) => error!("Unexpected error occured while parsing config: {}", e),
          };
          if config.version != 3 {
              error!("You are trying to run Cup with an incompatible config file! Please migrate your config file to the version 3, or if you have already done so, add a `version` key with the value `3`.")
          }
          config
      }
  }
  
  impl Default for Config {
      fn default() -> Self {
          Self::new()
      }
  }
  
  fn empty_as_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
  where
      D: Deserializer<'de>,
  {
      let s = String::deserialize(deserializer)?;
      if s.is_empty() {
          Ok(None)
      } else {
          Ok(Some(s))
      }
  }
  