Skip to main content

kontext_dev_sdk/
errors.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::KontextDevError;
6
7#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
8#[serde(rename_all = "camelCase")]
9pub struct KontextError {
10    pub code: String,
11    pub message: String,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub status_code: Option<u16>,
14    pub docs_url: String,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub request_id: Option<String>,
17    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
18    pub meta: HashMap<String, serde_json::Value>,
19}
20
21impl std::fmt::Display for KontextError {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        write!(f, "[{}] {}", self.code, self.message)
24    }
25}
26
27impl std::error::Error for KontextError {}
28
29impl KontextError {
30    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
31        let code = code.into();
32        Self {
33            docs_url: format!("https://docs.kontext.dev/errors/{code}"),
34            code,
35            message: message.into(),
36            status_code: None,
37            request_id: None,
38            meta: HashMap::new(),
39        }
40    }
41
42    pub fn with_status(mut self, status_code: u16) -> Self {
43        self.status_code = Some(status_code);
44        self
45    }
46
47    pub fn with_request_id(mut self, request_id: Option<String>) -> Self {
48        self.request_id = request_id;
49        self
50    }
51
52    pub fn with_meta(
53        mut self,
54        meta: impl IntoIterator<Item = (String, serde_json::Value)>,
55    ) -> Self {
56        self.meta.extend(meta);
57        self
58    }
59}
60
61#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
62pub struct AuthorizationRequiredError(pub KontextError);
63
64#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
65pub struct IntegrationConnectionRequiredError(pub KontextError);
66
67#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
68pub struct OAuthError(pub KontextError);
69
70#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
71pub struct ConfigError(pub KontextError);
72
73#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
74pub struct NetworkError(pub KontextError);
75
76#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
77pub struct HttpError(pub KontextError);
78
79impl AuthorizationRequiredError {
80    pub fn new(message: impl Into<String>) -> Self {
81        Self(KontextError::new("kontext_authorization_required", message.into()).with_status(401))
82    }
83}
84
85impl IntegrationConnectionRequiredError {
86    pub fn new(
87        integration_id: impl Into<String>,
88        message: impl Into<String>,
89        connect_url: Option<String>,
90    ) -> Self {
91        let mut meta = vec![(
92            "integrationId".to_string(),
93            serde_json::Value::String(integration_id.into()),
94        )];
95
96        if let Some(connect_url) = connect_url {
97            meta.push((
98                "connectUrl".to_string(),
99                serde_json::Value::String(connect_url),
100            ));
101        }
102
103        Self(
104            KontextError::new("kontext_integration_connection_required", message.into())
105                .with_status(403)
106                .with_meta(meta),
107        )
108    }
109}
110
111impl OAuthError {
112    pub fn new(message: impl Into<String>, code: impl Into<String>) -> Self {
113        Self(KontextError::new(code.into(), message.into()).with_status(400))
114    }
115}
116
117impl ConfigError {
118    pub fn new(message: impl Into<String>, code: impl Into<String>) -> Self {
119        Self(KontextError::new(code.into(), message.into()))
120    }
121}
122
123impl NetworkError {
124    pub fn new(message: impl Into<String>) -> Self {
125        Self(KontextError::new("kontext_network_error", message.into()))
126    }
127}
128
129impl HttpError {
130    pub fn new(
131        message: impl Into<String>,
132        status_code: u16,
133        request_id: Option<String>,
134        code: Option<String>,
135    ) -> Self {
136        Self(
137            KontextError::new(
138                code.unwrap_or_else(|| format!("kontext_http_{status_code}")),
139                message.into(),
140            )
141            .with_status(status_code)
142            .with_request_id(request_id),
143        )
144    }
145}
146
147pub fn parse_http_error(
148    status_code: u16,
149    message: impl Into<String>,
150    request_id: Option<String>,
151) -> HttpError {
152    HttpError::new(message, status_code, request_id, None)
153}
154
155pub fn is_network_error(error: &KontextError) -> bool {
156    error.code == "kontext_network_error"
157}
158
159pub fn is_unauthorized_error(error: &KontextError) -> bool {
160    error.status_code == Some(401) || error.code == "kontext_authorization_required"
161}
162
163impl From<KontextDevError> for KontextError {
164    fn from(value: KontextDevError) -> Self {
165        match value {
166            KontextDevError::Core(err) => KontextError::new("kontext_core_error", err.to_string()),
167            KontextDevError::InvalidUrl { .. } => {
168                KontextError::new("kontext_invalid_url", value.to_string())
169            }
170            KontextDevError::BrowserOpenFailed => {
171                KontextError::new("kontext_browser_open_failed", value.to_string())
172            }
173            KontextDevError::OAuthCallbackTimeout { .. }
174            | KontextDevError::OAuthCallbackCancelled
175            | KontextDevError::MissingAuthorizationCode
176            | KontextDevError::OAuthCallbackError { .. }
177            | KontextDevError::InvalidOAuthState => {
178                KontextError::new("kontext_oauth_error", value.to_string()).with_status(400)
179            }
180            KontextDevError::TokenRequest { .. } => {
181                KontextError::new("kontext_token_request_failed", value.to_string())
182                    .with_status(401)
183            }
184            KontextDevError::TokenExchange { .. } => {
185                KontextError::new("kontext_token_exchange_failed", value.to_string())
186                    .with_status(401)
187            }
188            KontextDevError::ConnectSession { .. } => {
189                KontextError::new("kontext_connect_session_failed", value.to_string())
190                    .with_status(500)
191            }
192            KontextDevError::IntegrationOAuthInit { .. } => {
193                KontextError::new("kontext_integration_oauth_init_failed", value.to_string())
194                    .with_status(500)
195            }
196            KontextDevError::TokenCacheRead { .. }
197            | KontextDevError::TokenCacheWrite { .. }
198            | KontextDevError::TokenCacheDeserialize { .. }
199            | KontextDevError::TokenCacheSerialize { .. } => {
200                KontextError::new("kontext_token_cache_error", value.to_string())
201            }
202            KontextDevError::EmptyAccessToken => {
203                KontextError::new("kontext_empty_access_token", value.to_string())
204            }
205            KontextDevError::MissingIntegrationUiUrl => {
206                KontextError::new("kontext_missing_integration_ui_url", value.to_string())
207            }
208        }
209    }
210}
211
212#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
213#[serde(rename_all = "camelCase")]
214pub struct ElicitationEntry {
215    pub integration_id: String,
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub integration_name: Option<String>,
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub url: Option<String>,
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub message: Option<String>,
222}