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}