Skip to main content

teaql_core/
request_intent.rs

1//! Request-owned intent. No Context, trace frame or logging option can supply it.
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum RequestKind {
5    Query,
6    Mutation,
7}
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct RequestIntentError {
11    pub request_kind: RequestKind,
12    pub field: &'static str,
13}
14
15impl RequestIntentError {
16    pub fn code(&self) -> &'static str {
17        match self.field {
18            "purpose" => "QUERY_PURPOSE_REQUIRED",
19            _ => "REQUEST_COMMENT_REQUIRED",
20        }
21    }
22}
23
24impl std::fmt::Display for RequestIntentError {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        write!(
27            f,
28            "{}: {:?} Request requires a non-blank {}; supply it at the request entry point",
29            self.code(),
30            self.request_kind,
31            self.field,
32        )
33    }
34}
35
36impl std::error::Error for RequestIntentError {}
37
38fn required<'a>(
39    value: Option<&'a str>,
40    request_kind: RequestKind,
41    field: &'static str,
42) -> Result<&'a str, RequestIntentError> {
43    value
44        .filter(|text| !text.trim().is_empty())
45        .ok_or(RequestIntentError {
46            request_kind,
47            field,
48        })
49}
50
51/// Validated query intent. Private fields and no Default prevent blank envelopes.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct QueryIntent {
54    comment: String,
55    purpose: String,
56}
57
58impl QueryIntent {
59    pub fn new(
60        comment: impl Into<String>,
61        purpose: impl Into<String>,
62    ) -> Result<Self, RequestIntentError> {
63        let comment = comment.into();
64        let purpose = purpose.into();
65        required(Some(&comment), RequestKind::Query, "comment")?;
66        required(Some(&purpose), RequestKind::Query, "purpose")?;
67        Ok(Self { comment, purpose })
68    }
69
70    pub fn from_optional(
71        comment: Option<&str>,
72        purpose: Option<&str>,
73    ) -> Result<Self, RequestIntentError> {
74        Self::new(
75            required(comment, RequestKind::Query, "comment")?,
76            required(purpose, RequestKind::Query, "purpose")?,
77        )
78    }
79
80    pub fn comment(&self) -> &str {
81        &self.comment
82    }
83    pub fn purpose(&self) -> &str {
84        &self.purpose
85    }
86}
87
88/// Validated root mutation reason. Local descendant reasons remain optional.
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct MutationIntent {
91    comment: String,
92}
93
94impl MutationIntent {
95    pub fn new(comment: impl Into<String>) -> Result<Self, RequestIntentError> {
96        let comment = comment.into();
97        required(Some(&comment), RequestKind::Mutation, "comment")?;
98        Ok(Self { comment })
99    }
100
101    pub fn from_optional(comment: Option<&str>) -> Result<Self, RequestIntentError> {
102        Self::new(required(comment, RequestKind::Mutation, "comment")?)
103    }
104
105    pub fn comment(&self) -> &str {
106        &self.comment
107    }
108
109    pub(crate) fn into_comment(self) -> String {
110        self.comment
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn missing_empty_ascii_and_unicode_whitespace_are_rejected() {
120        for value in [None, Some(""), Some(" \t\r\n"), Some("\u{2003}")] {
121            let query = QueryIntent::from_optional(value, Some("render details")).unwrap_err();
122            let mutation = MutationIntent::from_optional(value).unwrap_err();
123            for error in [query, mutation] {
124                assert_eq!(error.code(), "REQUEST_COMMENT_REQUIRED");
125                assert_eq!(error.field, "comment");
126            }
127            let purpose = QueryIntent::from_optional(Some("load details"), value).unwrap_err();
128            assert_eq!(purpose.code(), "QUERY_PURPOSE_REQUIRED");
129            assert_eq!(purpose.field, "purpose");
130        }
131    }
132
133    #[test]
134    fn intent_is_preserved_without_trimming_or_duplicating_fields() {
135        let query = QueryIntent::new(" what: load details ", "why: render details").unwrap();
136        assert_eq!(query.comment(), " what: load details ");
137        assert_eq!(query.purpose(), "why: render details");
138        assert_eq!(
139            MutationIntent::new("submit order").unwrap().comment(),
140            "submit order"
141        );
142    }
143}