storm-config 0.28.176

A crate containing the configuration structure and utilities used by Storm Software monorepos.
Documentation
use std::error::Error;
use std::{fmt, result};

use serde::{de, ser};

// #[derive(Error, Debug)]
// pub enum StormConfigError {
//   #[error("Unable to locate the configuration file - {0}")]
//   SpecificConfigFileNotFound(String),
//   #[error("Unable to locate the package.json file - {0}")]
//   PackageJsonNotFound(String),
// }

#[derive(Debug)]
pub enum Unexpected {
  Bool(bool),
  I64(i64),
  I128(i128),
  U64(u64),
  U128(u128),
  Float(f64),
  Str(String),
  Unit,
  Seq,
  Map,
}

impl fmt::Display for Unexpected {
  fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> {
    match *self {
      Unexpected::Bool(b) => write!(f, "boolean `{}`", b),
      Unexpected::I64(i) => write!(f, "64-bit integer `{}`", i),
      Unexpected::I128(i) => write!(f, "128-bit integer `{}`", i),
      Unexpected::U64(i) => write!(f, "64-bit unsigned integer `{}`", i),
      Unexpected::U128(i) => write!(f, "128-bit unsigned integer `{}`", i),
      Unexpected::Float(v) => write!(f, "floating point `{}`", v),
      Unexpected::Str(ref s) => write!(f, "string {:?}", s),
      Unexpected::Unit => write!(f, "unit value"),
      Unexpected::Seq => write!(f, "sequence"),
      Unexpected::Map => write!(f, "map"),
    }
  }
}

/// Represents all possible errors that can occur when working with
/// configuration.
pub enum ConfigError {
  /// Configuration is frozen and no further mutations can be made.
  Frozen,

  /// Configuration property was not found
  NotFound(String),

  /// Configuration path could not be parsed.
  PathParse(nom::error::ErrorKind),

  /// Configuration could not be parsed from file.
  FileParse {
    /// The URI used to access the file (if not loaded from a string).
    /// Example: `/path/to/config.json`
    uri: Option<String>,

    /// The captured error from attempting to parse the file in its desired format.
    /// This is the actual error object from the library used for the parsing.
    cause: Box<dyn Error + Send + Sync>,
  },

  /// Value could not be converted into the requested type.
  Type {
    /// The URI that references the source that the value came from.
    /// Example: `/path/to/config.json` or `Environment` or `etcd://localhost`
    // TODO: Why is this called Origin but FileParse has a uri field?
    origin: Option<String>,

    /// What we found when parsing the value
    unexpected: Unexpected,

    /// What was expected when parsing the value
    expected: &'static str,

    /// The key in the configuration hash of this value (if available where the
    /// error is generated).
    key: Option<String>,
  },

  /// Custom message
  Message(String),

  /// Unadorned error from a foreign origin.
  Foreign(Box<dyn Error + Send + Sync>),

  /// Invalid format used in an extend directive.
  InvalidExtendFormat,

  /// Error occurred while merging configurations.
  MergeFailure,
}

impl ConfigError {
  // FIXME: pub(crate)
  #[doc(hidden)]
  pub fn invalid_type(
    origin: Option<String>,
    unexpected: Unexpected,
    expected: &'static str,
  ) -> Self {
    Self::Type { origin, unexpected, expected, key: None }
  }

  // Have a proper error fire if the root of a file is ever not a Table
  // TODO: for now only json5 checked, need to finish others
  #[doc(hidden)]
  pub fn invalid_root(origin: Option<&String>, unexpected: Unexpected) -> Box<Self> {
    Box::new(Self::Type { origin: origin.cloned(), unexpected, expected: "a map", key: None })
  }

  // FIXME: pub(crate)
  #[doc(hidden)]
  #[must_use]
  pub fn extend_with_key(self, key: &str) -> Self {
    match self {
      Self::Type { origin, unexpected, expected, .. } => {
        Self::Type { origin, unexpected, expected, key: Some(key.into()) }
      }

      _ => self,
    }
  }

  #[must_use]
  fn prepend(self, segment: &str, add_dot: bool) -> Self {
    let concat = |key: Option<String>| {
      let key = key.unwrap_or_default();
      let dot = if add_dot && key.as_bytes().first().unwrap_or(&b'[') != &b'[' { "." } else { "" };
      format!("{}{}{}", segment, dot, key)
    };
    match self {
      Self::Type { origin, unexpected, expected, key } => {
        Self::Type { origin, unexpected, expected, key: Some(concat(key)) }
      }
      Self::NotFound(key) => Self::NotFound(concat(Some(key))),
      _ => self,
    }
  }

  #[must_use]
  pub(crate) fn prepend_key(self, key: &str) -> Self {
    self.prepend(key, true)
  }

  #[must_use]
  pub(crate) fn prepend_index(self, idx: usize) -> Self {
    self.prepend(&format!("[{}]", idx), false)
  }
}

/// Alias for a `Result` with the error type set to `ConfigError`.
pub type Result<T> = result::Result<T, ConfigError>;

// Forward Debug to Display for readable panic! messages
impl fmt::Debug for ConfigError {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    write!(f, "{}", *self)
  }
}

impl fmt::Display for ConfigError {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    match *self {
      ConfigError::Frozen => write!(f, "configuration is frozen"),

      ConfigError::PathParse(ref kind) => write!(f, "{}", kind.description()),

      ConfigError::Message(ref s) => write!(f, "{}", s),

      ConfigError::Foreign(ref cause) => write!(f, "{}", cause),

      ConfigError::NotFound(ref key) => {
        write!(f, "configuration property {:?} not found", key)
      }

      ConfigError::Type { ref origin, ref unexpected, expected, ref key } => {
        write!(f, "invalid type: {}, expected {}", unexpected, expected)?;

        if let Some(ref key) = *key {
          write!(f, " for key `{}`", key)?;
        }

        if let Some(ref origin) = *origin {
          write!(f, " in {}", origin)?;
        }

        Ok(())
      }

      ConfigError::FileParse { ref cause, ref uri } => {
        write!(f, "{}", cause)?;

        if let Some(ref uri) = *uri {
          write!(f, " in {}", uri)?;
        }

        Ok(())
      }

      ConfigError::InvalidExtendFormat => {
        write!(f, "invalid format used in an extend directive")
      }

      ConfigError::MergeFailure => {
        write!(f, "error occurred while merging configurations")
      }
    }
  }
}

impl Error for ConfigError {}

impl de::Error for ConfigError {
  fn custom<T: fmt::Display>(msg: T) -> Self {
    Self::Message(msg.to_string())
  }
}

impl ser::Error for ConfigError {
  fn custom<T: fmt::Display>(msg: T) -> Self {
    Self::Message(msg.to_string())
  }
}