quilt-rs 0.5.6

Rust library for accessing Quilt data packages.
Documentation
use std::collections::HashMap;

use serde::{Deserialize, Serialize};
use url::{Url, form_urlencoded};

const LATEST_TAG: &str = "latest";

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "_tag", content = "value")]
pub enum RevisionPointer {
    Hash(String),
    Tag(String),
}

impl Default for RevisionPointer {
    fn default() -> Self {
        Self::Tag(String::from(LATEST_TAG))
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct S3PackageURI {
    pub bucket: String,
    pub namespace: String,
    pub revision: RevisionPointer,
    pub path: Option<String>,
}

impl TryFrom<&str> for S3PackageURI {
    type Error = String;

    fn try_from(input: &str) -> Result<Self, Self::Error> {
        let parsed_url = Url::parse(input).map_err(|err| err.to_string())?;
        if parsed_url.scheme() != "quilt+s3" {
            return Err("invalid scheme".into());
        }

        let fragment = parsed_url.fragment().ok_or("missing fragment")?;
        let mut params: HashMap<_, _> = form_urlencoded::parse(fragment.as_bytes())
            .into_owned()
            .collect();

        let pkg_spec = params
            .remove("package")
            .ok_or("fragment must contain package")?;

        let (namespace, revision) = match pkg_spec.split_once('@') {
            Some((namespace, top_hash)) => (
                namespace.to_string(),
                RevisionPointer::Hash(top_hash.into()),
            ),
            None => (pkg_spec, RevisionPointer::default()),
        };

        let path = params.remove("path");

        if !params.is_empty() {
            return Err(format!("unexpected fragment params: {:?}", params));
        }

        let bucket = parsed_url.host_str().ok_or("missing bucket")?.to_string();

        Ok(Self {
            bucket,
            namespace,
            path,
            revision,
        })
    }
}