use std::fmt::Display;
use serde_json::Value;
use crate::{
errors::{RpcError, RpcErrorCode},
mempool::MEMPOOL_HEIGHT,
};
const FILTER_INCLUDE_TOKENS: &str = "include_tokens";
const FILTER_EXCLUDE_TOKENS: &str = "exclude_tokens";
const FILTER_TOKENS_ONLY: &str = "tokens_only";
const FILTER_FROM_HEIGHT: &str = "from_height";
const FILTER_TO_HEIGHT: &str = "to_height";
const FILTER_LIMIT: &str = "limit";
const FILTER_OFFSET: &str = "offset";
const NO_TO_FILTER: i64 = -1;
const NO_FROM_FILTER: i64 = 0;
const NO_LIMIT: i64 = 0;
const NO_OFFSET: i64 = 0;
#[derive(Debug, Clone, Copy)]
pub struct QueryFilter {
pub exclude_tokens: bool,
pub token_only: bool,
pub from_height: i64,
pub to_height: i64,
pub offset: i64,
pub limit: i64,
}
impl QueryFilter {
pub fn filter_token_only() -> Self {
Self {
token_only: true,
..Self::default()
}
}
pub fn filter_exclude_tokens() -> Self {
Self {
exclude_tokens: true,
..Self::default()
}
}
pub fn height_within<T: Into<i64>>(&self, height: T) -> bool {
let height = height.into();
let height = if height == MEMPOOL_HEIGHT as i64 {
0
} else {
height
};
if height == 0 {
return self.to_height == NO_TO_FILTER;
}
debug_assert!(height != 0);
if self.to_height == NO_TO_FILTER {
height >= self.from_height
} else if self.from_height == NO_FROM_FILTER {
height < self.to_height
} else {
height >= self.from_height && height < self.to_height
}
}
pub fn from_param(param: Option<&Value>) -> Result<Self, RpcError> {
Self::from_param_custom_default(param, QueryFilter::default())
}
pub fn from_param_custom_default(
param: Option<&Value>,
mut default: QueryFilter,
) -> Result<Self, RpcError> {
if param.is_none() || param == Some(&Value::Null) {
return Ok(default);
}
let param = param.unwrap();
match param.as_str() {
Some(FILTER_INCLUDE_TOKENS) => return Ok(default),
Some(FILTER_EXCLUDE_TOKENS) => {
default.exclude_tokens = true;
return Ok(default);
}
Some(FILTER_TOKENS_ONLY) => {
default.token_only = true;
return Ok(default);
}
Some(other_str) => {
return Err(RpcError {
code: RpcErrorCode::InvalidParams,
msg: format!("Invalid parameter {} for filter", other_str),
})
}
None => { }
};
let param = match param.as_object() {
Some(o) => o,
None => {
return Err(RpcError {
code: RpcErrorCode::InvalidParams,
msg: "Invalid query filter parameter. Needs to be an object or string"
.to_string(),
})
}
};
let parse_bool = |what| -> Result<Option<bool>, RpcError> {
match param.get(what) {
Some(p) if p.is_null() => Ok(None),
Some(p) => match p.as_bool() {
Some(b) => Ok(Some(b)),
None => Err(RpcError {
code: RpcErrorCode::InvalidParams,
msg: format!("Filter field {} must be a boolean", what),
}),
},
None => Ok(None),
}
};
let parse_int = |what| -> Result<Option<i64>, RpcError> {
match param.get(what) {
Some(p) if p.is_null() => Ok(None),
Some(p) => match p.as_i64() {
Some(i) => Ok(Some(i)),
None => Err(RpcError {
code: RpcErrorCode::InvalidParams,
msg: format!("Filter field {} must be an integer", what),
}),
},
None => Ok(None),
}
};
if let Some(opt) = parse_bool(FILTER_TOKENS_ONLY)? {
default.token_only = opt;
}
if let Some(opt) = parse_bool(FILTER_EXCLUDE_TOKENS)? {
default.exclude_tokens = opt;
}
if let Some(opt) = parse_int(FILTER_FROM_HEIGHT)? {
default.from_height = opt;
}
if let Some(opt) = parse_int(FILTER_TO_HEIGHT)? {
default.to_height = opt;
}
if let Some(opt) = parse_int(FILTER_OFFSET)? {
default.offset = opt;
}
if let Some(opt) = parse_int(FILTER_LIMIT)? {
default.limit = opt
}
if default.to_height != NO_TO_FILTER && default.from_height > default.to_height {
return Err(RpcError {
code: RpcErrorCode::InvalidParams,
msg: format!(
"from_height cannot be larger than to_height ({} > {})",
default.from_height, default.to_height
),
});
}
if default.exclude_tokens && default.token_only {
return Err(RpcError {
code: RpcErrorCode::InvalidParams,
msg: "'exclude_tokens' and 'token_only' are mutually exclusive".to_string(),
});
}
if default.from_height < 0 {
return Err(RpcError {
code: RpcErrorCode::InvalidParams,
msg: "from_height cannot be negative".to_string(),
});
}
if default.to_height < -1 {
return Err(RpcError {
code: RpcErrorCode::InvalidParams,
msg: "to_height must be positive number or -1 (include mempool)".to_string(),
});
}
if default.offset < 0 {
return Err(RpcError {
code: RpcErrorCode::InvalidParams,
msg: "offset cannot be negative".to_string(),
});
}
if default.limit < 0 {
return Err(RpcError {
code: RpcErrorCode::InvalidParams,
msg: "limit cannot be negative".to_string(),
});
}
Ok(default)
}
pub(crate) fn filter_limit_offset<T>(&self, results: &mut Vec<T>) {
if self.offset != NO_OFFSET {
let offset = std::cmp::min(self.offset as usize, results.len());
let _ = results.drain(0..offset);
}
if self.limit != NO_LIMIT {
results.truncate(self.limit as usize);
}
}
pub(crate) fn has_height_filter(&self) -> bool {
!(self.from_height == NO_FROM_FILTER && self.to_height == NO_TO_FILTER)
}
}
impl Default for QueryFilter {
fn default() -> Self {
Self {
exclude_tokens: false,
token_only: false,
from_height: NO_FROM_FILTER,
to_height: NO_TO_FILTER,
limit: NO_LIMIT,
offset: NO_OFFSET,
}
}
}
impl Display for QueryFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"QueryFilter {{ exclude_tokens: {}, token_only: {}, from_height: {}, to_height: {} }}",
self.exclude_tokens, self.token_only, self.from_height, self.to_height
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_height_within_mempool_with_from_height_set() {
let filter = QueryFilter {
from_height: 5,
to_height: NO_TO_FILTER,
..QueryFilter::default()
};
assert!(filter.height_within(0)); assert!(filter.height_within(5)); assert!(filter.height_within(10)); }
#[test]
fn test_height_within_regular_range() {
let filter = QueryFilter {
from_height: 10,
to_height: 20,
..QueryFilter::default()
};
assert!(filter.height_within(15)); assert!(filter.height_within(10)); assert!(!filter.height_within(20)); assert!(!filter.height_within(5)); assert!(!filter.height_within(25)); }
#[test]
fn test_height_within_no_to_filter() {
let filter = QueryFilter {
from_height: 10,
to_height: NO_TO_FILTER,
..QueryFilter::default()
};
assert!(filter.height_within(10)); assert!(filter.height_within(15)); assert!(filter.height_within(100)); assert!(!filter.height_within(5)); }
#[test]
fn test_height_within_mempool_case() {
let filter = QueryFilter {
from_height: 0,
to_height: NO_TO_FILTER,
..QueryFilter::default()
};
assert!(filter.height_within(0)); }
#[test]
fn test_from_param_custom_default_with_null() {
let default = QueryFilter {
limit: 100,
..QueryFilter::default()
};
let result = QueryFilter::from_param_custom_default(Some(&Value::Null), default.clone());
assert!(result.is_ok());
assert_eq!(result.unwrap().limit, 100);
}
#[test]
fn test_from_param_custom_default_with_none() {
let default = QueryFilter {
limit: 100,
..QueryFilter::default()
};
let result = QueryFilter::from_param_custom_default(None, default.clone());
assert!(result.is_ok());
assert_eq!(result.unwrap().limit, 100);
}
}