use std::fmt;
use serde::{Deserialize, Serialize};
use crate::{
client::{Error, Result},
types::block::address::Bech32Address,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryParameters(Vec<QueryParameter>);
impl QueryParameters {
#[must_use]
pub fn new(query_parameters: impl Into<Vec<QueryParameter>>) -> Self {
let mut query_parameters = query_parameters.into();
query_parameters.sort_unstable_by_key(QueryParameter::kind);
query_parameters.dedup_by_key(|qp| qp.kind());
Self(query_parameters)
}
pub fn empty() -> Self {
Self(Vec::new())
}
pub fn replace(&mut self, query_parameter: QueryParameter) {
match self
.0
.binary_search_by_key(&query_parameter.kind(), QueryParameter::kind)
{
Ok(pos) => self.0[pos] = query_parameter,
Err(pos) => self.0.insert(pos, query_parameter),
}
}
pub(crate) fn contains(&self, kind: u8) -> bool {
self.0.iter().any(|q| q.kind() == kind)
}
#[cfg(test)]
pub(crate) fn any<F: Fn(&QueryParameter) -> bool>(&self, f: F) -> bool {
self.0.iter().any(f)
}
pub fn to_query_string(&self) -> Option<String> {
if self.0.is_empty() {
None
} else {
Some(
self.0
.iter()
.map(QueryParameter::to_query_string)
.collect::<Vec<String>>()
.join("&"),
)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum QueryParameter {
Address(Bech32Address),
AliasAddress(Bech32Address),
CreatedAfter(u32),
CreatedBefore(u32),
Cursor(String),
ExpirationReturnAddress(Bech32Address),
ExpiresAfter(u32),
ExpiresBefore(u32),
Governor(Bech32Address),
HasExpiration(bool),
HasNativeTokens(bool),
HasStorageDepositReturn(bool),
HasTimelock(bool),
Issuer(Bech32Address),
MaxNativeTokenCount(u32),
MinNativeTokenCount(u32),
PageSize(usize),
Sender(Bech32Address),
StateController(Bech32Address),
StorageDepositReturnAddress(Bech32Address),
Tag(String),
TimelockedAfter(u32),
TimelockedBefore(u32),
}
impl QueryParameter {
fn to_query_string(&self) -> String {
match self {
Self::Address(v) => format!("address={v}"),
Self::AliasAddress(v) => format!("aliasAddress={v}"),
Self::CreatedAfter(v) => format!("createdAfter={v}"),
Self::CreatedBefore(v) => format!("createdBefore={v}"),
Self::Cursor(v) => format!("cursor={v}"),
Self::ExpirationReturnAddress(v) => format!("expirationReturnAddress={v}"),
Self::ExpiresAfter(v) => format!("expiresAfter={v}"),
Self::ExpiresBefore(v) => format!("expiresBefore={v}"),
Self::Governor(v) => format!("governor={v}"),
Self::HasExpiration(v) => format!("hasExpiration={v}"),
Self::HasNativeTokens(v) => format!("hasNativeTokens={v}"),
Self::HasStorageDepositReturn(v) => format!("hasStorageDepositReturn={v}"),
Self::HasTimelock(v) => format!("hasTimelock={v}"),
Self::Issuer(v) => format!("issuer={v}"),
Self::MaxNativeTokenCount(v) => format!("maxNativeTokenCount={v}"),
Self::MinNativeTokenCount(v) => format!("minNativeTokenCount={v}"),
Self::PageSize(v) => format!("pageSize={v}"),
Self::Sender(v) => format!("sender={v}"),
Self::StateController(v) => format!("stateController={v}"),
Self::StorageDepositReturnAddress(v) => format!("storageDepositReturnAddress={v}"),
Self::Tag(v) => format!("tag={v}"),
Self::TimelockedAfter(v) => format!("timelockedAfter={v}"),
Self::TimelockedBefore(v) => format!("timelockedBefore={v}"),
}
}
pub(crate) fn kind(&self) -> u8 {
match self {
Self::Address(_) => 0,
Self::AliasAddress(_) => 1,
Self::CreatedAfter(_) => 2,
Self::CreatedBefore(_) => 3,
Self::Cursor(_) => 4,
Self::ExpirationReturnAddress(_) => 5,
Self::ExpiresAfter(_) => 6,
Self::ExpiresBefore(_) => 7,
Self::Governor(_) => 8,
Self::HasExpiration(_) => 9,
Self::HasNativeTokens(_) => 10,
Self::HasStorageDepositReturn(_) => 11,
Self::HasTimelock(_) => 12,
Self::Issuer(_) => 13,
Self::MaxNativeTokenCount(_) => 14,
Self::MinNativeTokenCount(_) => 15,
Self::PageSize(_) => 16,
Self::Sender(_) => 17,
Self::StateController(_) => 18,
Self::StorageDepositReturnAddress(_) => 19,
Self::Tag(_) => 20,
Self::TimelockedAfter(_) => 21,
Self::TimelockedBefore(_) => 22,
}
}
}
impl fmt::Display for QueryParameter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_query_string())
}
}
macro_rules! verify_query_parameters {
($query_parameters:ident, $first:path $(, $rest:path)*) => {
if let Some(qp) = $query_parameters.iter().find(|qp| {
!matches!(qp, $first(_) $(| $rest(_))*)
}) {
Err(Error::UnsupportedQueryParameter(qp.clone()))
} else {
Ok(())
}
};
}
pub(crate) fn verify_query_parameters_basic_outputs(query_parameters: Vec<QueryParameter>) -> Result<QueryParameters> {
verify_query_parameters!(
query_parameters,
QueryParameter::Address,
QueryParameter::HasNativeTokens,
QueryParameter::MinNativeTokenCount,
QueryParameter::MaxNativeTokenCount,
QueryParameter::HasStorageDepositReturn,
QueryParameter::StorageDepositReturnAddress,
QueryParameter::HasTimelock,
QueryParameter::TimelockedBefore,
QueryParameter::TimelockedAfter,
QueryParameter::HasExpiration,
QueryParameter::ExpiresBefore,
QueryParameter::ExpiresAfter,
QueryParameter::ExpirationReturnAddress,
QueryParameter::Sender,
QueryParameter::Tag,
QueryParameter::CreatedBefore,
QueryParameter::CreatedAfter,
QueryParameter::PageSize,
QueryParameter::Cursor
)?;
Ok(QueryParameters::new(query_parameters))
}
pub(crate) fn verify_query_parameters_alias_outputs(query_parameters: Vec<QueryParameter>) -> Result<QueryParameters> {
verify_query_parameters!(
query_parameters,
QueryParameter::StateController,
QueryParameter::Governor,
QueryParameter::Issuer,
QueryParameter::Sender,
QueryParameter::HasNativeTokens,
QueryParameter::MinNativeTokenCount,
QueryParameter::MaxNativeTokenCount,
QueryParameter::CreatedBefore,
QueryParameter::CreatedAfter,
QueryParameter::PageSize,
QueryParameter::Cursor
)?;
Ok(QueryParameters::new(query_parameters))
}
pub(crate) fn verify_query_parameters_foundry_outputs(
query_parameters: Vec<QueryParameter>,
) -> Result<QueryParameters> {
verify_query_parameters!(
query_parameters,
QueryParameter::AliasAddress,
QueryParameter::HasNativeTokens,
QueryParameter::MinNativeTokenCount,
QueryParameter::MaxNativeTokenCount,
QueryParameter::CreatedBefore,
QueryParameter::CreatedAfter,
QueryParameter::PageSize,
QueryParameter::Cursor
)?;
Ok(QueryParameters::new(query_parameters))
}
pub(crate) fn verify_query_parameters_nft_outputs(query_parameters: Vec<QueryParameter>) -> Result<QueryParameters> {
verify_query_parameters!(
query_parameters,
QueryParameter::Address,
QueryParameter::HasNativeTokens,
QueryParameter::MinNativeTokenCount,
QueryParameter::MaxNativeTokenCount,
QueryParameter::HasStorageDepositReturn,
QueryParameter::StorageDepositReturnAddress,
QueryParameter::HasTimelock,
QueryParameter::TimelockedBefore,
QueryParameter::TimelockedAfter,
QueryParameter::HasExpiration,
QueryParameter::ExpiresBefore,
QueryParameter::ExpiresAfter,
QueryParameter::ExpirationReturnAddress,
QueryParameter::Issuer,
QueryParameter::Sender,
QueryParameter::Tag,
QueryParameter::CreatedBefore,
QueryParameter::CreatedAfter,
QueryParameter::PageSize,
QueryParameter::Cursor
)?;
Ok(QueryParameters::new(query_parameters))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn query_parameter() {
let address1 = QueryParameter::Address(
Bech32Address::try_from_str("atoi1qzt0nhsf38nh6rs4p6zs5knqp6psgha9wsv74uajqgjmwc75ugupx3y7x0r").unwrap(),
);
let address2 = QueryParameter::Address(
Bech32Address::try_from_str("atoi1qzt0nhsf38nh6rs4p6zs5knqp6psgha9wsv74uajqgjmwc75ugupx3y7x0r").unwrap(),
);
let address3 = QueryParameter::Address(
Bech32Address::try_from_str("atoi1qprxpfvaz2peggq6f8k9cj8zfsxuw69e4nszjyv5kuf8yt70t2847shpjak").unwrap(),
);
let state_controller = QueryParameter::StateController(
Bech32Address::try_from_str("atoi1qzt0nhsf38nh6rs4p6zs5knqp6psgha9wsv74uajqgjmwc75ugupx3y7x0r").unwrap(),
);
let mut query_parameters = QueryParameters::new([address1, address2, state_controller]);
assert!(query_parameters.0.len() == 2);
query_parameters.replace(address3);
assert!(query_parameters.0.len() == 2);
assert!(query_parameters.any(|param| matches!(param, QueryParameter::Address(_))));
assert!(!query_parameters.any(|param| matches!(param, QueryParameter::Cursor(_))));
}
}