beaker 0.1.8

CosmWasm swiss-army knife configured for Osmosis by default, but trivial to make it work for other CosmWasm enabled chain.
Documentation
use std::{str::FromStr, vec};

use anyhow::Context;
use cosmrs::Coin;
use regex::Regex;

#[derive(Debug)]
pub struct CoinFromStr {
    inner: Coin,
}

impl CoinFromStr {
    pub fn inner(&self) -> &Coin {
        &self.inner
    }
}

impl FromStr for CoinFromStr {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let re = Regex::new(r"^(\d+)(.+)$").unwrap();
        let caps = re
            .captures(s)
            .with_context(|| format!("Unable to parse `{s}` as Coin."))?;

        let c = Coin {
            amount: caps
                .get(1)
                .with_context(|| format!("`{s}` does not contain valid amount"))?
                .as_str()
                .parse()
                .unwrap(),
            denom: caps
                .get(2)
                .with_context(|| format!("`{s}` does not contain valid denom"))?
                .as_str()
                .parse()
                .unwrap(),
        };

        Ok(CoinFromStr { inner: c })
    }
}

pub struct Coins(Vec<Coin>);

impl FromStr for Coins {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let coins: Vec<Coin> = s
            .split(',')
            .map(|s| CoinFromStr::from_str(s).map(|c| c.inner().to_owned()))
            .collect::<Result<Vec<Coin>, anyhow::Error>>()?;

        Ok(Coins(coins))
    }
}

impl TryFrom<Option<&str>> for Coins {
    type Error = anyhow::Error;
    fn try_from(os: Option<&str>) -> Result<Self, Self::Error> {
        match os {
            Some(s) => s.parse(),
            None => Ok(Coins(vec![])),
        }
    }
}

impl From<Coins> for Vec<Coin> {
    fn from(coins: Coins) -> Self {
        let Coins(v) = coins;
        v
    }
}

impl From<Coins> for Vec<cosmos_sdk_proto::cosmos::base::v1beta1::Coin> {
    fn from(coins: Coins) -> Self {
        let Coins(v) = coins;
        v.iter().map(|c| c.into()).collect()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use anyhow::anyhow;

    #[test]
    fn test_coin_from_str_with_correct_denom() {
        let c: CoinFromStr = "1000uosmo".parse().unwrap();
        assert_eq!(
            c.inner,
            Coin {
                amount: 1000u64.into(),
                denom: "uosmo".parse().unwrap()
            }
        )
    }

    #[test]
    fn test_coin_from_str_with_incorrect_denom() {
        let e = "uosmo1000".parse::<CoinFromStr>().unwrap_err();

        assert_eq!(
            e.to_string(),
            anyhow!("Unable to parse `uosmo1000` as Coin.").to_string()
        );
    }
}