Skip to main content

appcore_api/
query_contract.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: query_contract.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/06/01 13:57:57 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/07/23 13:45:20 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11//! Shared query request/response API contract for transports.
12
13use serde::{Deserialize, Serialize};
14
15/// Version 1 side-effect-free query request.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct QueryRequest {
18    /// Declared application or Runtime query capability.
19    pub query_name: String,
20    /// Caller-assigned request identity.
21    pub query_id: String,
22    /// Structured application-owned query payload.
23    pub payload: serde_json::Value,
24}
25
26/// Version 1 controlled query response.
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct QueryResponse {
29    /// Whether query execution succeeded.
30    pub ok: bool,
31    /// Controlled rejection detail, when present.
32    pub message: Option<String>,
33    /// Structured application-owned response payload.
34    pub payload: serde_json::Value,
35}
36
37/// Validation failures defined by the query V1 contract.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum QueryRequestValidationError {
40    /// The query name is empty.
41    EmptyQueryName,
42    /// The query identifier is empty.
43    EmptyQueryId,
44    /// The query name is malformed.
45    InvalidQueryName,
46    /// The query identifier is malformed.
47    InvalidQueryId,
48    /// The serialized payload exceeds the configured request bound.
49    PayloadTooLarge,
50}
51
52impl QueryRequest {
53    /// Validates identifiers and the serialized payload bound.
54    pub fn validate(&self, max_payload_bytes: usize) -> Result<(), QueryRequestValidationError> {
55        if self.query_name.trim().is_empty() {
56            return Err(QueryRequestValidationError::EmptyQueryName);
57        }
58        if self.query_id.trim().is_empty() {
59            return Err(QueryRequestValidationError::EmptyQueryId);
60        }
61        if self.query_name.len() > 128 || !is_valid_token(&self.query_name) {
62            return Err(QueryRequestValidationError::InvalidQueryName);
63        }
64        if self.query_id.len() > 128 || !is_valid_token(&self.query_id) {
65            return Err(QueryRequestValidationError::InvalidQueryId);
66        }
67        if self.payload_bytes().len() > max_payload_bytes {
68            return Err(QueryRequestValidationError::PayloadTooLarge);
69        }
70        Ok(())
71    }
72
73    /// Serializes the structured payload to JSON bytes.
74    pub fn payload_bytes(&self) -> Vec<u8> {
75        serde_json::to_vec(&self.payload).unwrap_or_default()
76    }
77}
78
79impl QueryResponse {
80    /// Creates a successful response with a structured payload.
81    pub fn ok(payload: serde_json::Value) -> Self {
82        Self {
83            ok: true,
84            message: None,
85            payload,
86        }
87    }
88
89    /// Creates a controlled rejected response.
90    pub fn rejected(message: impl Into<String>) -> Self {
91        Self {
92            ok: false,
93            message: Some(message.into()),
94            payload: serde_json::Value::Object(serde_json::Map::new()),
95        }
96    }
97}
98
99fn is_valid_token(value: &str) -> bool {
100    value
101        .bytes()
102        .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b':' | b'-'))
103}
104
105#[cfg(test)]
106#[path = "query_contract_tests.rs"]
107mod tests;