use serde::{Deserialize, Serialize};
use std::io::{self, Write};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QueryRequest {
pub query_name: String,
pub query_id: String,
pub payload: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QueryResponse {
pub ok: bool,
pub message: Option<String>,
pub payload: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QueryRequestValidationError {
EmptyQueryName,
EmptyQueryId,
InvalidQueryName,
InvalidQueryId,
PayloadTooLarge,
}
impl QueryRequest {
pub fn validate(&self, max_payload_bytes: usize) -> Result<(), QueryRequestValidationError> {
if self.query_name.trim().is_empty() {
return Err(QueryRequestValidationError::EmptyQueryName);
}
if self.query_id.trim().is_empty() {
return Err(QueryRequestValidationError::EmptyQueryId);
}
if self.query_name.len() > 128 || !is_valid_token(&self.query_name) {
return Err(QueryRequestValidationError::InvalidQueryName);
}
if self.query_id.len() > 128 || !is_valid_token(&self.query_id) {
return Err(QueryRequestValidationError::InvalidQueryId);
}
if !payload_fits(&self.payload, max_payload_bytes) {
return Err(QueryRequestValidationError::PayloadTooLarge);
}
Ok(())
}
pub fn payload_bytes(&self) -> Vec<u8> {
serde_json::to_vec(&self.payload).unwrap_or_default()
}
}
struct LimitedJsonCounter {
remaining: usize,
exceeded: bool,
}
impl LimitedJsonCounter {
const fn new(limit: usize) -> Self {
Self {
remaining: limit,
exceeded: false,
}
}
}
impl Write for LimitedJsonCounter {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
if bytes.len() > self.remaining {
self.exceeded = true;
return Err(io::Error::other("query payload exceeds configured limit"));
}
self.remaining -= bytes.len();
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn payload_fits(payload: &serde_json::Value, limit: usize) -> bool {
let mut counter = LimitedJsonCounter::new(limit);
let result = serde_json::to_writer(&mut counter, payload);
debug_assert!(result.is_ok() || counter.exceeded);
!counter.exceeded
}
impl QueryResponse {
pub fn ok(payload: serde_json::Value) -> Self {
Self {
ok: true,
message: None,
payload,
}
}
pub fn rejected(message: impl Into<String>) -> Self {
Self {
ok: false,
message: Some(message.into()),
payload: serde_json::Value::Object(serde_json::Map::new()),
}
}
}
fn is_valid_token(value: &str) -> bool {
value
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b':' | b'-'))
}
#[cfg(test)]
#[path = "query_contract_tests.rs"]
mod tests;