oparl-types 0.7.1

Type definitions for the OParl protocol
Documentation
// SPDX-FileCopyrightText: Politik im Blick developers
// SPDX-FileCopyrightText: Wolfgang Silbermayr <wolfgang@silbermayr.at>
//
// SPDX-License-Identifier: AGPL-3.0-or-later OR EUPL-1.2

use std::str::FromStr;

use snafu::{ResultExt as _, Snafu, ensure};
use url::PathSegmentsMut;

#[derive(
    Debug,
    Clone,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    derive_more::AsRef,
    derive_more::Display,
    derive_more::Into,
    serde::Serialize,
    serde::Deserialize,
)]
pub struct Url(url::Url);

#[derive(Debug, Snafu)]
pub enum TryFromUrlError {
    #[snafu(display("Failed to parse URL: {source}"))]
    Parse { source: url::ParseError },

    #[snafu(display("OParl urls must be usable as a base"))]
    CannotBeABase,
}

impl TryFrom<url::Url> for Url {
    type Error = TryFromUrlError;

    fn try_from(value: url::Url) -> Result<Self, Self::Error> {
        ensure!(!value.cannot_be_a_base(), CannotBeABaseSnafu);
        Ok(Self(value))
    }
}

impl FromStr for Url {
    type Err = TryFromUrlError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let url: url::Url = s.parse().context(ParseSnafu)?;
        url.try_into()
    }
}

impl Url {
    pub fn as_str(&self) -> &str {
        self.0.as_str()
    }

    pub fn path_segments_mut(&mut self) -> PathSegmentsMut<'_> {
        self.0.path_segments_mut().unwrap()
    }

    pub fn set_query(&mut self, query: Option<&str>) {
        self.0.set_query(query)
    }
}

impl TryFrom<String> for Url {
    type Error = TryFromUrlError;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        value.parse()
    }
}

#[cfg(feature = "sea-orm")]
mod sea_orm_impls {
    use sea_orm::{
        ActiveValue, ColumnType, DbErr, IntoActiveValue, TryGetError, TryGetable, Value,
        sea_query::{ArrayType, Nullable, ValueType, ValueTypeErr},
    };

    use super::Url;

    impl From<Url> for Value {
        fn from(value: Url) -> Self {
            value.as_str().into()
        }
    }

    impl TryGetable for Url {
        fn try_get_by<I: sea_orm::ColIdx>(
            res: &sea_orm::QueryResult,
            index: I,
        ) -> Result<Self, TryGetError> {
            let s = <String as TryGetable>::try_get_by(res, index)?;
            s.try_into().map_err(|e| {
                DbErr::TryIntoErr {
                    from: "String",
                    into: "Url",
                    source: Box::new(e),
                }
                .into()
            })
        }
    }

    impl ValueType for Url {
        fn try_from(v: Value) -> Result<Self, ValueTypeErr> {
            let s = <String as ValueType>::try_from(v)?;
            s.try_into().map_err(|_| ValueTypeErr)
        }

        fn type_name() -> String {
            stringify!(String).to_owned()
        }

        fn array_type() -> ArrayType {
            <String as ValueType>::array_type()
        }

        fn column_type() -> ColumnType {
            <String as ValueType>::column_type()
        }
    }

    impl Nullable for Url {
        fn null() -> Value {
            Value::String(None)
        }
    }

    impl IntoActiveValue<Url> for Url {
        fn into_active_value(self) -> ActiveValue<Url> {
            ActiveValue::Set(self)
        }
    }
}

#[cfg(test)]
mod tests {
    use pretty_assertions::assert_eq;

    use super::Url;

    #[test]
    fn from_str() {
        assert_eq!(
            Url("https://abc.example.com/qwert".parse().unwrap()),
            "https://abc.example.com/qwert"
                .parse()
                .expect("value must be a parseable url")
        );
    }
}

#[cfg(test)]
mod serde_tests {
    use pretty_assertions::assert_eq;
    use serde_json::json;

    use super::Url;

    #[test]
    fn serialize() {
        assert_eq!(
            json!("https://abc.example.com/qwert".parse::<Url>().unwrap()),
            json!("https://abc.example.com/qwert")
        );
    }

    #[test]
    fn deserialize_good() {
        let deserialized: Url = serde_json::from_value(json!("https://abc.example.com/qwert"))
            .expect("value must be deserializable as Url");
        assert_eq!(
            deserialized,
            "https://abc.example.com/qwert".parse::<Url>().unwrap()
        );
    }

    #[test]
    fn deserialize_bad() {
        assert!(serde_json::from_value::<Url>(json!([])).is_err());
        assert!(serde_json::from_value::<Url>(json!({})).is_err());
        assert!(serde_json::from_value::<Url>(json!("hello")).is_err());
        assert!(serde_json::from_value::<Url>(json!(true)).is_err());
        assert!(serde_json::from_value::<Url>(json!(123)).is_err());
    }
}