depo-api 0.15.0

API for the Blockchain Commons Depository ('depo') server.
Documentation
use std::collections::{HashMap, HashSet};

use bc_envelope::prelude::*;
use gstp::prelude::*;

use crate::{
    Error, GET_SHARES_FUNCTION, RECEIPT_PARAM, RECEIPT_PARAM_NAME, Result,
    receipt::Receipt,
    util::{Abbrev, FlankedFunction},
};

//
// Request
//

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GetShares(HashSet<Receipt>);

impl GetShares {
    pub fn new<I, T>(iterable: I) -> Self
    where
        I: IntoIterator<Item = T>,
        T: Clone + Into<Receipt>,
    {
        Self(
            iterable
                .into_iter()
                .map(|item| item.clone().into())
                .collect(),
        )
    }

    pub fn new_all_shares() -> Self { Self(HashSet::new()) }

    pub fn receipts(&self) -> &HashSet<Receipt> { &self.0 }
}

impl From<GetShares> for Expression {
    fn from(value: GetShares) -> Self {
        let mut expression = Expression::new(GET_SHARES_FUNCTION);
        for receipt in value.0.into_iter() {
            expression = expression.with_parameter(RECEIPT_PARAM, receipt);
        }
        expression
    }
}

impl TryFrom<Expression> for GetShares {
    type Error = Error;

    fn try_from(expression: Expression) -> Result<Self> {
        let receipts = expression
            .objects_for_parameter(RECEIPT_PARAM)
            .into_iter()
            .map(|parameter| {
                parameter.try_into().map_err(|e| Error::InvalidParameter {
                    parameter: RECEIPT_PARAM_NAME.to_string(),
                    message: format!("failed to convert to Receipt: {}", e),
                })
            })
            .collect::<Result<HashSet<Receipt>>>()?;
        Ok(Self(receipts))
    }
}

impl std::fmt::Display for GetShares {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_fmt(format_args!(
            "{} {}",
            "getShares".flanked_function(),
            self.receipts().abbrev()
        ))
    }
}

//
// Response
//

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GetSharesResult(HashMap<Receipt, ByteString>);

impl GetSharesResult {
    pub fn new(receipt_to_data: HashMap<Receipt, ByteString>) -> Self {
        Self(receipt_to_data)
    }

    pub fn receipt_to_data(&self) -> &HashMap<Receipt, ByteString> { &self.0 }

    pub fn data_for_receipt(&self, receipt: &Receipt) -> Option<&ByteString> {
        self.0.get(receipt)
    }
}

impl From<GetSharesResult> for Envelope {
    fn from(value: GetSharesResult) -> Self {
        let mut result = known_values::OK_VALUE.to_envelope();
        for (receipt, data) in value.0 {
            result = result.add_assertion(receipt, data);
        }
        result
    }
}

impl TryFrom<Envelope> for GetSharesResult {
    type Error = Error;

    fn try_from(envelope: Envelope) -> Result<Self> {
        let mut receipt_to_data = HashMap::new();
        for assertion in envelope.assertions() {
            let receipt =
                Receipt::try_from(assertion.try_predicate().map_err(|e| {
                    Error::InvalidEnvelope {
                        message: format!(
                            "failed to extract assertion predicate: {}",
                            e
                        ),
                    }
                })?)
                .map_err(|e| Error::InvalidParameter {
                    parameter: RECEIPT_PARAM_NAME.to_string(),
                    message: format!("invalid receipt in assertion: {}", e),
                })?;
            let object =
                assertion.try_object().map_err(|e| Error::InvalidEnvelope {
                    message: format!(
                        "failed to extract assertion object: {}",
                        e
                    ),
                })?;
            let data = ByteString::try_from(object).map_err(|e| {
                Error::InvalidEnvelope {
                    message: format!(
                        "failed to convert object to ByteString: {}",
                        e
                    ),
                }
            })?;
            receipt_to_data.insert(receipt, data);
        }
        Ok(Self::new(receipt_to_data))
    }
}

impl TryFrom<SealedResponse> for GetSharesResult {
    type Error = Error;

    fn try_from(response: SealedResponse) -> Result<Self> {
        response.result()?.clone().try_into()
    }
}

impl std::fmt::Display for GetSharesResult {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let abbrevable: HashMap<Receipt, ByteString> = self
            .receipt_to_data()
            .iter()
            .map(|(k, v)| (k.clone(), v.clone()))
            .collect();
        f.write_fmt(format_args!(
            "{} OK {}",
            "getShares".flanked_function(),
            abbrevable.abbrev()
        ))
    }
}

#[cfg(test)]
mod tests {
    use bc_components::XID;
    use indoc::indoc;

    use super::*;

    fn user_id() -> XID {
        XID::from_data_ref(hex_literal::hex!(
            "8712dfac3d0ebfa910736b2a9ee39d4b68f64222a77bcc0074f3f5f1c9216d30"
        ))
        .unwrap()
    }

    fn data_1() -> ByteString { b"data_1".to_vec().into() }

    fn receipt_1() -> Receipt { Receipt::new(user_id(), data_1()) }

    fn data_2() -> ByteString { b"data_2".to_vec().into() }

    fn receipt_2() -> Receipt { Receipt::new(user_id(), data_2()) }

    #[test]
    fn test_request() {
        bc_envelope::register_tags();

        let receipts = vec![receipt_1(), receipt_2()];

        let request = GetShares::new(receipts);
        let expression: Expression = request.clone().into();
        let request_envelope = expression.to_envelope();
        // println!("{}", request_envelope.format());
        #[rustfmt::skip]
        assert_eq!(request_envelope.format(), indoc! {r#"
            «"getShares"» [
                ❰"receipt"❱: Bytes(32) [
                    'isA': "Receipt"
                ]
                ❰"receipt"❱: Bytes(32) [
                    'isA': "Receipt"
                ]
            ]
        "#}.trim());
        let decoded_expression =
            Expression::try_from(request_envelope).unwrap();
        let decoded = GetShares::try_from(decoded_expression).unwrap();
        assert_eq!(request, decoded);
    }

    #[test]
    fn test_response() {
        bc_envelope::register_tags();

        let receipts_to_data =
            vec![(receipt_1(), data_1()), (receipt_2(), data_2())]
                .into_iter()
                .collect();
        let response = GetSharesResult::new(receipts_to_data);
        let response_envelope = response.to_envelope();
        // println!("{}", response_envelope.format());
        #[rustfmt::skip]
        assert_eq!(response_envelope.format(), indoc! {r#"
            'OK' [
                Bytes(32) [
                    'isA': "Receipt"
                ]
                : Bytes(6)
                Bytes(32) [
                    'isA': "Receipt"
                ]
                : Bytes(6)
            ]
        "#}.trim());
        let decoded = GetSharesResult::try_from(response_envelope).unwrap();
        assert_eq!(response, decoded);
    }
}