axone-objectarium 6.0.0

A Smart Contract which enables the storage of arbitrary unstructured Objects.
Documentation
use crate::cursor::AsCursor;
use crate::msg::{Cursor, PageInfo};
use crate::state::Pagination;
use cosmwasm_std::{StdError, StdResult};
use cw_storage_plus::{Bound, PrimaryKey};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::marker::PhantomData;

#[derive(Clone)]
pub struct PaginationHandler<'a, T, PK>
where
    T: Serialize + DeserializeOwned,
    PK: PrimaryKey<'a>,
{
    max_page_size: u32,
    default_page_size: u32,

    _data_type: PhantomData<T>,
    _pk_type: PhantomData<PK>,
    _lifetime: PhantomData<&'a ()>,
}

impl<'a, T, PK> From<Pagination> for PaginationHandler<'a, T, PK>
where
    T: Serialize + DeserializeOwned,
    PK: PrimaryKey<'a>,
{
    fn from(value: Pagination) -> Self {
        PaginationHandler::new(value.max_page_size, value.default_page_size)
    }
}

pub trait QueryPage<'a, T, PK> {
    fn query_page<I>(
        self,
        iter_fn: I,
        after: Option<Cursor>,
        first: Option<u32>,
    ) -> StdResult<(Vec<T>, PageInfo)>
    where
        I: FnOnce(Option<Bound<'_, PK>>) -> Box<dyn Iterator<Item = StdResult<(PK, T)>> + 'a>;
}

impl<'a, T, PK> QueryPage<'a, T, PK> for PaginationHandler<'a, T, PK>
where
    T: Serialize + DeserializeOwned + AsCursor<PK> + Clone,
    PK: PrimaryKey<'a>,
{
    fn query_page<I>(
        self,
        iter_fn: I,
        after: Option<Cursor>,
        first: Option<u32>,
    ) -> StdResult<(Vec<T>, PageInfo)>
    where
        I: FnOnce(Option<Bound<'_, PK>>) -> Box<dyn Iterator<Item = StdResult<(PK, T)>> + 'a>,
    {
        self.query_page_cursor_fn(
            iter_fn,
            |c| T::decode_cursor(c),
            AsCursor::encode_cursor,
            after,
            first,
        )
    }
}

impl<'a, T, PK> PaginationHandler<'a, T, PK>
where
    T: Serialize + DeserializeOwned,
    PK: PrimaryKey<'a>,
{
    pub const fn new(max_page_size: u32, default_page_size: u32) -> Self {
        PaginationHandler {
            max_page_size,
            default_page_size,
            _data_type: PhantomData,
            _pk_type: PhantomData,
            _lifetime: PhantomData,
        }
    }

    pub fn query_page_cursor_fn<I, CD, CE>(
        self,
        iter_fn: I,
        cursor_dec_fn: CD,
        cursor_enc_fn: CE,
        after: Option<Cursor>,
        first: Option<u32>,
    ) -> StdResult<(Vec<T>, PageInfo)>
    where
        I: FnOnce(Option<Bound<'_, PK>>) -> Box<dyn Iterator<Item = StdResult<(PK, T)>> + 'a>,
        CD: FnOnce(Cursor) -> StdResult<PK>,
        CE: FnOnce(&T) -> Cursor,
    {
        let min_bound = match after {
            Some(cursor) => Some(Bound::exclusive(cursor_dec_fn(cursor)?)),
            _ => None,
        };
        let page_size = self.compute_page_size(first)?;
        let mut items: Vec<T> = iter_fn(min_bound)
            .take(page_size + 1)
            .map(|res: StdResult<(PK, T)>| res.map(|(_, item)| item))
            .collect::<StdResult<Vec<T>>>()?;

        let has_next_page = items.len() > page_size;
        if has_next_page {
            items.pop();
        }

        let cursor = items.last().map_or_else(String::new, cursor_enc_fn);

        Ok((
            items,
            PageInfo {
                has_next_page,
                cursor,
            },
        ))
    }

    fn compute_page_size(self, first: Option<u32>) -> StdResult<usize> {
        match first {
            Some(req) => {
                if req > self.max_page_size {
                    return Err(StdError::generic_err(
                        "Requested page size exceed maximum allowed",
                    ));
                }
                Ok(req)
            }
            _ => Ok(self.default_page_size),
        }
        .map(|size| size as usize)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::slice::Iter;

    struct TestIter<'a> {
        sub_iter: Iter<'a, i32>,
        shall_err: bool,
    }

    impl<'a> TestIter<'a> {
        fn map_to_result(&mut self, val: i32) -> StdResult<(i32, i32)> {
            if self.shall_err {
                return Err(StdError::generic_err("iter error".to_string()));
            }
            Ok((val, val))
        }
    }

    impl<'a> Iterator for TestIter<'a> {
        type Item = StdResult<(i32, i32)>;

        fn next(&mut self) -> Option<Self::Item> {
            match self.sub_iter.next() {
                Some(&x) => Some(self.map_to_result(x)),
                _ => None,
            }
        }
    }

    #[test]
    fn query_page() {
        let data = &[1, 2, 3, 4, 5];
        let handler: PaginationHandler<i32, i32> = Pagination {
            max_page_size: 3,
            default_page_size: 2,
        }
        .into();

        let iter_fn = |min_bound: Option<Bound<i32>>| match min_bound {
            Some(Bound::Exclusive((b, ..))) => Box::new(TestIter {
                sub_iter: data[b as usize..].iter(),
                shall_err: false,
            })
                as Box<dyn Iterator<Item = StdResult<(i32, i32)>>>,
            _ => Box::new(TestIter {
                sub_iter: data.iter(),
                shall_err: false,
            }),
        };
        let cursor_dec_fn =
            |cursor: Cursor| cursor.parse::<i32>().map_err(|_| StdError::generic_err(""));
        let cursor_enc_fn = |pk: &i32| pk.to_string();

        let res = handler
            .clone()
            .query_page_cursor_fn(
                |_: Option<Bound<i32>>| {
                    Box::new(TestIter {
                        sub_iter: (&[] as &[i32]).iter(),
                        shall_err: true,
                    })
                },
                cursor_dec_fn,
                cursor_enc_fn,
                None,
                None,
            )
            .unwrap();
        assert_eq!(res.0, Vec::<i32>::new());
        assert_eq!(
            res.1,
            PageInfo {
                has_next_page: false,
                cursor: "".to_string(),
            }
        );

        let cases = vec![
            (
                None,
                None,
                vec![1, 2],
                PageInfo {
                    has_next_page: true,
                    cursor: "2".to_string(),
                },
            ),
            (
                None,
                Some(1),
                vec![1],
                PageInfo {
                    has_next_page: true,
                    cursor: "1".to_string(),
                },
            ),
            (
                None,
                Some(3),
                vec![1, 2, 3],
                PageInfo {
                    has_next_page: true,
                    cursor: "3".to_string(),
                },
            ),
            (
                Some("1".to_string()),
                None,
                vec![2, 3],
                PageInfo {
                    has_next_page: true,
                    cursor: "3".to_string(),
                },
            ),
            (
                Some("2".to_string()),
                Some(3),
                vec![3, 4, 5],
                PageInfo {
                    has_next_page: false,
                    cursor: "5".to_string(),
                },
            ),
            (
                Some("3".to_string()),
                Some(3),
                vec![4, 5],
                PageInfo {
                    has_next_page: false,
                    cursor: "5".to_string(),
                },
            ),
        ];

        for case in cases {
            let res = handler
                .clone()
                .query_page_cursor_fn(iter_fn, cursor_dec_fn, cursor_enc_fn, case.0, case.1)
                .unwrap();
            assert_eq!(res.0, case.2);
            assert_eq!(res.1, case.3);
        }
    }

    #[test]
    fn query_page_err() {
        let data = &[1, 2, 3, 4, 5];
        let handler: PaginationHandler<i32, i32> = Pagination {
            max_page_size: 3,
            default_page_size: 2,
        }
        .into();

        let iter_fn = |_: Option<Bound<i32>>| {
            Box::new(TestIter {
                sub_iter: data.iter(),
                shall_err: false,
            }) as Box<dyn Iterator<Item = StdResult<(i32, i32)>>>
        };
        let cursor_dec_fn =
            |cursor: Cursor| cursor.parse::<i32>().map_err(|_| StdError::generic_err(""));
        let cursor_enc_fn = |pk: &i32| pk.to_string();

        let res = handler.clone().query_page_cursor_fn(
            |_: Option<Bound<i32>>| {
                Box::new(TestIter {
                    sub_iter: data.iter(),
                    shall_err: true,
                })
            },
            cursor_dec_fn,
            cursor_enc_fn,
            None,
            None,
        );
        assert_eq!(res, Err(StdError::generic_err("iter error".to_string())));

        let res = handler.clone().query_page_cursor_fn(
            iter_fn,
            cursor_dec_fn,
            cursor_enc_fn,
            None,
            Some(4),
        );
        assert_eq!(
            res,
            Err(StdError::generic_err(
                "Requested page size exceed maximum allowed".to_string()
            ))
        );

        let res = handler.clone().query_page_cursor_fn(
            iter_fn,
            |_| Err(StdError::generic_err("cursor decode error")),
            cursor_enc_fn,
            Some("1".to_string()),
            None,
        );
        assert_eq!(
            res,
            Err(StdError::generic_err("cursor decode error".to_string()))
        );
    }
}