gel_protocol/
annotations.rs

1#[cfg(feature = "with-serde")]
2use crate::encoding::Annotations;
3
4/// CommandDataDescription1 may contain "warnings" annotations, whose value is
5/// a JSON array of this [Warning] type.
6#[derive(Debug, Clone, PartialEq, Eq)]
7#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))]
8pub struct Warning {
9    /// User-friendly explanation of the problem
10    pub message: String,
11
12    /// Name of the Python exception class
13    pub r#type: String,
14
15    /// Machine-friendly exception id
16    pub code: u64,
17
18    /// Name of the source file that caused the warning.
19    #[cfg_attr(feature = "with-serde", serde(default))]
20    pub filename: Option<String>,
21
22    /// Additional user-friendly info
23    #[cfg_attr(feature = "with-serde", serde(default))]
24    pub hint: Option<String>,
25
26    /// Developer-friendly explanation of why this problem occured
27    #[cfg_attr(feature = "with-serde", serde(default))]
28    pub details: Option<String>,
29
30    /// Inclusive 0-based position within the source
31    #[cfg_attr(
32        feature = "with-serde",
33        serde(deserialize_with = "deserialize_usize_from_str", default)
34    )]
35    pub start: Option<usize>,
36
37    /// Exclusive 0-based position within the source
38    #[cfg_attr(
39        feature = "with-serde",
40        serde(deserialize_with = "deserialize_usize_from_str", default)
41    )]
42    pub end: Option<usize>,
43
44    /// 1-based index of the line of the start
45    #[cfg_attr(
46        feature = "with-serde",
47        serde(deserialize_with = "deserialize_usize_from_str", default)
48    )]
49    pub line: Option<usize>,
50
51    /// 1-based index of the column of the start
52    #[cfg_attr(
53        feature = "with-serde",
54        serde(deserialize_with = "deserialize_usize_from_str", default)
55    )]
56    pub col: Option<usize>,
57}
58
59impl std::fmt::Display for Warning {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        let Warning {
62            filename,
63            line,
64            col,
65            r#type,
66            message,
67            ..
68        } = self;
69        let filename = filename
70            .as_ref()
71            .map(|f| format!("{f}:"))
72            .unwrap_or_default();
73        let line = (*line).unwrap_or(1);
74        let col = (*col).unwrap_or(1);
75
76        write!(f, "{type} at {filename}{line}:{col} {message}")
77    }
78}
79
80#[cfg(feature = "with-serde")]
81pub fn decode_warnings(annotations: &Annotations) -> Result<Vec<Warning>, gel_errors::Error> {
82    use gel_errors::{ErrorKind, ProtocolEncodingError};
83
84    const ANN_NAME: &str = "warnings";
85
86    if let Some(warnings) = annotations.get(ANN_NAME) {
87        serde_json::from_str::<Vec<_>>(warnings).map_err(|e| {
88            ProtocolEncodingError::with_source(e)
89                .context("Invalid JSON while decoding 'warnings' annotation")
90        })
91    } else {
92        Ok(vec![])
93    }
94}
95
96#[cfg(feature = "with-serde")]
97fn deserialize_usize_from_str<'de, D: serde::Deserializer<'de>>(
98    deserializer: D,
99) -> Result<Option<usize>, D::Error> {
100    use serde::Deserialize;
101
102    #[derive(Deserialize)]
103    #[serde(untagged)]
104    enum StringOrInt {
105        String(String),
106        Number(usize),
107    }
108
109    Option::<StringOrInt>::deserialize(deserializer)?
110        .map(|x| match x {
111            StringOrInt::String(s) => s.parse::<usize>().map_err(serde::de::Error::custom),
112            StringOrInt::Number(i) => Ok(i),
113        })
114        .transpose()
115}
116
117#[test]
118#[cfg(feature = "with-serde")]
119fn deserialize_warning() {
120    let a: Warning =
121        serde_json::from_str(r#"{"message": "a", "type": "WarningException", "code": 1}"#).unwrap();
122    assert_eq!(
123        a,
124        Warning {
125            message: "a".to_string(),
126            r#type: "WarningException".to_string(),
127            code: 1,
128            filename: None,
129            hint: None,
130            details: None,
131            start: None,
132            end: None,
133            line: None,
134            col: None
135        }
136    );
137
138    let a: Warning = serde_json::from_str(
139        r#"{"message": "a", "type": "WarningException", "code": 1, "start": null}"#,
140    )
141    .unwrap();
142    assert_eq!(
143        a,
144        Warning {
145            message: "a".to_string(),
146            r#type: "WarningException".to_string(),
147            code: 1,
148            filename: None,
149            hint: None,
150            details: None,
151            start: None,
152            end: None,
153            line: None,
154            col: None
155        }
156    );
157
158    let a: Warning = serde_json::from_str(
159        r#"{"message": "a", "type": "WarningException", "code": 1, "start": 23}"#,
160    )
161    .unwrap();
162    assert_eq!(
163        a,
164        Warning {
165            message: "a".to_string(),
166            r#type: "WarningException".to_string(),
167            code: 1,
168            filename: None,
169            hint: None,
170            details: None,
171            start: Some(23),
172            end: None,
173            line: None,
174            col: None
175        }
176    );
177
178    let a: Warning = serde_json::from_str(
179        r#"{"message": "a", "type": "WarningException", "code": 1, "start": "23"}"#,
180    )
181    .unwrap();
182    assert_eq!(
183        a,
184        Warning {
185            message: "a".to_string(),
186            r#type: "WarningException".to_string(),
187            code: 1,
188            filename: None,
189            hint: None,
190            details: None,
191            start: Some(23),
192            end: None,
193            line: None,
194            col: None
195        }
196    );
197}