teaql_core/
request_intent.rs1#[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#[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#[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}