use std::fmt;
use std::str::FromStr;
use cid::Cid as MultiformatsCid;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Cid {
canonical: String,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum CidError {
#[error("CID is empty")]
Empty,
#[error("invalid CID {input:?}: {reason}")]
Invalid {
input: String,
reason: String,
},
}
impl Cid {
pub fn parse(input: impl Into<String>) -> Result<Self, CidError> {
let canonical: String = input.into();
if canonical.is_empty() {
return Err(CidError::Empty);
}
MultiformatsCid::try_from(canonical.as_str()).map_err(|e| CidError::Invalid {
input: canonical.clone(),
reason: e.to_string(),
})?;
Ok(Self { canonical })
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.canonical
}
pub fn to_multiformats(&self) -> Result<MultiformatsCid, cid::Error> {
MultiformatsCid::try_from(self.canonical.as_str())
}
}
impl fmt::Display for Cid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.canonical)
}
}
impl FromStr for Cid {
type Err = CidError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl AsRef<str> for Cid {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl std::ops::Deref for Cid {
type Target = str;
fn deref(&self) -> &str {
&self.canonical
}
}
impl std::borrow::Borrow<str> for Cid {
fn borrow(&self) -> &str {
&self.canonical
}
}
impl Serialize for Cid {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.canonical.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Cid {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::parse(s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
const VALID_CID_V1_B32: &str = "bafyreidkacrnonh3pkjnntp7ujnkfeudaodpqzxa67mtbmovd5sayuctfm";
const VALID_CID_V0_B58: &str = "QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB";
#[test]
fn parses_v1_base32() {
let c = Cid::parse(VALID_CID_V1_B32).unwrap();
assert_eq!(c.as_str(), VALID_CID_V1_B32);
}
#[test]
fn parses_v0_base58() {
let c = Cid::parse(VALID_CID_V0_B58).unwrap();
assert_eq!(c.as_str(), VALID_CID_V0_B58);
}
#[test]
fn rejects_empty() {
assert_eq!(Cid::parse(""), Err(CidError::Empty));
}
#[test]
fn rejects_garbage() {
let result = Cid::parse("not a cid");
assert!(matches!(result, Err(CidError::Invalid { .. })));
}
#[test]
fn round_trips_canonical_form() {
let c = Cid::parse(VALID_CID_V1_B32).unwrap();
let json = serde_json::to_string(&c).unwrap();
assert_eq!(json, format!("\"{VALID_CID_V1_B32}\""));
let back: Cid = serde_json::from_str(&json).unwrap();
assert_eq!(back, c);
}
#[test]
fn from_str_works() {
let c: Cid = VALID_CID_V1_B32.parse().unwrap();
assert_eq!(c.as_str(), VALID_CID_V1_B32);
}
#[test]
fn to_multiformats_round_trips_codec() {
let c = Cid::parse(VALID_CID_V1_B32).unwrap();
let mf = c.to_multiformats().unwrap();
assert_eq!(mf.codec(), 0x71);
}
}