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};
14use std::io::{self, Write};
15
16/// Version 1 side-effect-free query request.
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct QueryRequest {
19    /// Declared application or Runtime query capability.
20    pub query_name: String,
21    /// Caller-assigned request identity.
22    pub query_id: String,
23    /// Structured application-owned query payload.
24    pub payload: serde_json::Value,
25}
26
27/// Version 1 controlled query response.
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct QueryResponse {
30    /// Whether query execution succeeded.
31    pub ok: bool,
32    /// Controlled rejection detail, when present.
33    pub message: Option<String>,
34    /// Structured application-owned response payload.
35    pub payload: serde_json::Value,
36}
37
38/// Validation failures defined by the query V1 contract.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum QueryRequestValidationError {
41    /// The query name is empty.
42    EmptyQueryName,
43    /// The query identifier is empty.
44    EmptyQueryId,
45    /// The query name is malformed.
46    InvalidQueryName,
47    /// The query identifier is malformed.
48    InvalidQueryId,
49    /// The serialized payload exceeds the configured request bound.
50    PayloadTooLarge,
51}
52
53impl QueryRequest {
54    /// Validates identifiers and the serialized payload bound.
55    pub fn validate(&self, max_payload_bytes: usize) -> Result<(), QueryRequestValidationError> {
56        if self.query_name.trim().is_empty() {
57            return Err(QueryRequestValidationError::EmptyQueryName);
58        }
59        if self.query_id.trim().is_empty() {
60            return Err(QueryRequestValidationError::EmptyQueryId);
61        }
62        if self.query_name.len() > 128 || !is_valid_token(&self.query_name) {
63            return Err(QueryRequestValidationError::InvalidQueryName);
64        }
65        if self.query_id.len() > 128 || !is_valid_token(&self.query_id) {
66            return Err(QueryRequestValidationError::InvalidQueryId);
67        }
68        if !payload_fits(&self.payload, max_payload_bytes) {
69            return Err(QueryRequestValidationError::PayloadTooLarge);
70        }
71        Ok(())
72    }
73
74    /// Serializes the structured payload to JSON bytes.
75    pub fn payload_bytes(&self) -> Vec<u8> {
76        serde_json::to_vec(&self.payload).unwrap_or_default()
77    }
78}
79
80struct LimitedJsonCounter {
81    remaining: usize,
82    exceeded: bool,
83}
84
85impl LimitedJsonCounter {
86    const fn new(limit: usize) -> Self {
87        Self {
88            remaining: limit,
89            exceeded: false,
90        }
91    }
92}
93
94impl Write for LimitedJsonCounter {
95    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
96        if bytes.len() > self.remaining {
97            self.exceeded = true;
98            return Err(io::Error::other("query payload exceeds configured limit"));
99        }
100        self.remaining -= bytes.len();
101        Ok(bytes.len())
102    }
103
104    fn flush(&mut self) -> io::Result<()> {
105        Ok(())
106    }
107}
108
109fn payload_fits(payload: &serde_json::Value, limit: usize) -> bool {
110    let mut counter = LimitedJsonCounter::new(limit);
111    let result = serde_json::to_writer(&mut counter, payload);
112    debug_assert!(result.is_ok() || counter.exceeded);
113    !counter.exceeded
114}
115
116impl QueryResponse {
117    /// Creates a successful response with a structured payload.
118    pub fn ok(payload: serde_json::Value) -> Self {
119        Self {
120            ok: true,
121            message: None,
122            payload,
123        }
124    }
125
126    /// Creates a controlled rejected response.
127    pub fn rejected(message: impl Into<String>) -> Self {
128        Self {
129            ok: false,
130            message: Some(message.into()),
131            payload: serde_json::Value::Object(serde_json::Map::new()),
132        }
133    }
134}
135
136fn is_valid_token(value: &str) -> bool {
137    value
138        .bytes()
139        .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b':' | b'-'))
140}
141
142#[cfg(test)]
143#[path = "query_contract_tests.rs"]
144mod tests;