cargo-credential 0.4.10

A library to assist writing Cargo credential helpers.
Documentation
use serde::{Deserialize, Serialize};
use std::error::Error as StdError;
use thiserror::Error as ThisError;

/// Credential provider error type.
///
/// `UrlNotSupported` and `NotFound` errors both cause Cargo
/// to attempt another provider, if one is available. The other
/// variants are fatal.
///
/// Note: Do not add a tuple variant, as it cannot be serialized.
#[derive(Serialize, Deserialize, ThisError, Debug)]
#[serde(rename_all = "kebab-case", tag = "kind")]
#[non_exhaustive]
pub enum Error {
    /// Registry URL is not supported. This should be used if
    /// the provider only works for some registries. Cargo will
    /// try another provider, if available
    #[error("registry not supported")]
    UrlNotSupported,

    /// Credentials could not be found. Cargo will try another
    /// provider, if available
    #[error("credential not found")]
    NotFound,

    /// The provider doesn't support this operation, such as
    /// a provider that can't support 'login' / 'logout'
    #[error("requested operation not supported")]
    OperationNotSupported,

    /// The provider failed to perform the operation. Other
    /// providers will not be attempted
    #[error(transparent)]
    #[serde(with = "error_serialize")]
    Other(Box<dyn StdError + Sync + Send>),

    /// A new variant was added to this enum since Cargo was built
    #[error("unknown error kind; try updating Cargo?")]
    #[serde(other)]
    Unknown,
}

impl From<String> for Error {
    fn from(message: String) -> Self {
        Box::new(StringTypedError {
            message,
            source: None,
        })
        .into()
    }
}

impl From<&str> for Error {
    fn from(err: &str) -> Self {
        err.to_string().into()
    }
}

impl From<anyhow::Error> for Error {
    fn from(value: anyhow::Error) -> Self {
        let mut prev = None;
        for e in value.chain().rev() {
            prev = Some(Box::new(StringTypedError {
                message: e.to_string(),
                source: prev,
            }));
        }
        Error::Other(prev.unwrap())
    }
}

impl<T: StdError + Send + Sync + 'static> From<Box<T>> for Error {
    fn from(value: Box<T>) -> Self {
        Error::Other(value)
    }
}

/// String-based error type with an optional source
#[derive(Debug)]
struct StringTypedError {
    message: String,
    source: Option<Box<StringTypedError>>,
}

impl StdError for StringTypedError {
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        self.source.as_ref().map(|err| err as &dyn StdError)
    }
}

impl std::fmt::Display for StringTypedError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.message.fmt(f)
    }
}

/// Serializer / deserializer for any boxed error.
/// The string representation of the error, and its `source` chain can roundtrip across
/// the serialization. The actual types are lost (downcast will not work).
mod error_serialize {
    use std::error::Error as StdError;
    use std::ops::Deref;

    use serde::{Deserialize, Deserializer, Serializer, ser::SerializeStruct};

    use crate::error::StringTypedError;

    pub fn serialize<S>(
        e: &Box<dyn StdError + Send + Sync>,
        serializer: S,
    ) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut state = serializer.serialize_struct("StringTypedError", 2)?;
        state.serialize_field("message", &format!("{}", e))?;

        // Serialize the source error chain recursively
        let mut current_source: &dyn StdError = e.deref();
        let mut sources = Vec::new();
        while let Some(err) = current_source.source() {
            sources.push(err.to_string());
            current_source = err;
        }
        state.serialize_field("caused-by", &sources)?;
        state.end()
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<Box<dyn StdError + Sync + Send>, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        #[serde(rename_all = "kebab-case")]
        struct ErrorData {
            message: String,
            caused_by: Option<Vec<String>>,
        }
        let data = ErrorData::deserialize(deserializer)?;
        let mut prev = None;
        if let Some(source) = data.caused_by {
            for e in source.into_iter().rev() {
                prev = Some(Box::new(StringTypedError {
                    message: e,
                    source: prev,
                }));
            }
        }
        let e = Box::new(StringTypedError {
            message: data.message,
            source: prev,
        });
        Ok(e)
    }
}

#[cfg(test)]
mod tests {
    use super::Error;

    #[test]
    pub fn unknown_kind() {
        let json = r#"{
            "kind": "unexpected-kind",
            "unexpected-content": "test"
          }"#;
        let e: Error = serde_json::from_str(&json).unwrap();
        assert!(matches!(e, Error::Unknown));
    }

    #[test]
    pub fn roundtrip() {
        // Construct an error with context
        let e = anyhow::anyhow!("E1").context("E2").context("E3");
        // Convert to a string with contexts.
        let s1 = format!("{:?}", e);
        // Convert the error into an `Error`
        let e: Error = e.into();
        // Convert that error into JSON
        let json = serde_json::to_string_pretty(&e).unwrap();
        // Convert that error back to anyhow
        let e: anyhow::Error = e.into();
        let s2 = format!("{:?}", e);
        assert_eq!(s1, s2);

        // Convert the error back from JSON
        let e: Error = serde_json::from_str(&json).unwrap();
        // Convert to back to anyhow
        let e: anyhow::Error = e.into();
        let s3 = format!("{:?}", e);
        assert_eq!(s2, s3);

        assert_eq!(
            r#"{
  "kind": "other",
  "message": "E3",
  "caused-by": [
    "E2",
    "E1"
  ]
}"#,
            json
        );
    }
}