1#![warn(missing_debug_implementations)]
22
23use std::fmt;
24use std::time::Duration;
25
26use reqwest::{IntoUrl, Url};
27use serde::de::DeserializeOwned;
28use serde::{Deserialize, Serialize};
29
30#[derive(Debug)]
32pub struct Client {
33 inner: reqwest::Client,
34 url: Url,
35 session_id: Option<String>,
36}
37
38impl Client {
39 pub fn new<U>(url: U) -> Result<Self, Error>
45 where
46 U: IntoUrl,
47 {
48 let mut url = url.into_url()?;
49 if url.path() != "/" {
50 return Err(Error::InvalidUrl("base URL cannot have path".into()));
51 }
52 assert!(!url.cannot_be_a_base());
53 url.path_segments_mut()
54 .expect("cannot-be-a-base checked to be false")
55 .push("api");
56 Ok(Client {
57 inner: reqwest::Client::new(),
58 url,
59 session_id: None,
60 })
61 }
62
63 pub fn set_session_id(&mut self, session_id: String) {
65 self.session_id = Some(session_id);
66 }
67
68 pub async fn session_properties(&self) -> Result<SessionPropertiesResponse, reqwest::Error> {
72 let url = self.api_url(&["session", "properties"]);
73 self.send_request(self.inner.get(url)).await
74 }
75
76 pub async fn login(&self, request: &LoginRequest) -> Result<LoginResponse, reqwest::Error> {
83 let url = self.api_url(&["session"]);
84 self.send_request(self.inner.post(url).json(request)).await
85 }
86
87 pub async fn setup(&self, request: &SetupRequest) -> Result<LoginResponse, reqwest::Error> {
97 let url = self.api_url(&["setup"]);
98 self.send_request(self.inner.post(url).json(request)).await
99 }
100
101 pub async fn databases(&self) -> Result<Vec<Database>, reqwest::Error> {
105 let url = self.api_url(&["database"]);
106 let res: ListWrapper<_> = self.send_request(self.inner.get(url)).await?;
107 Ok(res.data)
108 }
109
110 pub async fn database_metadata(&self, id: usize) -> Result<DatabaseMetadata, reqwest::Error> {
114 let url = self.api_url(&["database", &id.to_string(), "metadata"]);
115 self.send_request(self.inner.get(url)).await
116 }
117
118 fn api_url(&self, endpoint: &[&str]) -> Url {
119 let mut url = self.url.clone();
120 url.path_segments_mut()
121 .expect("url validated on construction")
122 .extend(endpoint);
123 url
124 }
125
126 async fn send_request<T>(&self, mut req: reqwest::RequestBuilder) -> Result<T, reqwest::Error>
127 where
128 T: DeserializeOwned,
129 {
130 req = req.timeout(Duration::from_secs(5));
131 if let Some(session_id) = &self.session_id {
132 req = req.header("X-Metabase-Session", session_id);
133 }
134 let res = req.send().await?.error_for_status()?;
135 res.json().await
136 }
137}
138
139#[derive(Debug)]
141pub enum Error {
142 InvalidUrl(String),
144 Transport(reqwest::Error),
146}
147
148impl From<reqwest::Error> for Error {
149 fn from(e: reqwest::Error) -> Error {
150 Error::Transport(e)
151 }
152}
153
154impl std::error::Error for Error {
155 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
156 match self {
157 Error::InvalidUrl(_) => None,
158 Error::Transport(e) => Some(e),
159 }
160 }
161}
162
163impl fmt::Display for Error {
164 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
165 match self {
166 Error::InvalidUrl(msg) => write!(f, "invalid url: {}", msg),
167 Error::Transport(e) => write!(f, "transport: {}", e),
168 }
169 }
170}
171
172#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
173struct ListWrapper<T> {
174 data: Vec<T>,
175}
176
177#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
179#[serde(rename_all = "kebab-case")]
180pub struct SessionPropertiesResponse {
181 pub setup_token: Option<String>,
182}
183
184#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
186pub struct SetupRequest {
187 pub allow_tracking: bool,
188 pub database: SetupDatabase,
189 pub token: String,
190 pub prefs: SetupPrefs,
191 pub user: SetupUser,
192}
193
194#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
196pub struct SetupDatabase {
197 pub engine: String,
198 pub name: String,
199 pub details: SetupDatabaseDetails,
200}
201
202#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
204pub struct SetupDatabaseDetails {
205 pub host: String,
206 pub port: usize,
207 pub dbname: String,
208 pub user: String,
209}
210
211#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
213pub struct SetupPrefs {
214 pub site_name: String,
215}
216
217#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
219pub struct SetupUser {
220 pub email: String,
221 pub first_name: String,
222 pub last_name: String,
223 pub password: String,
224 pub site_name: String,
225}
226
227#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
229pub struct LoginRequest {
230 pub username: String,
231 pub password: String,
232}
233
234#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
236pub struct LoginResponse {
237 pub id: String,
238}
239
240#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
242pub struct Database {
243 pub name: String,
244 pub id: usize,
245}
246
247#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
249pub struct DatabaseMetadata {
250 pub tables: Vec<Table>,
251}
252
253#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
255pub struct Table {
256 pub name: String,
257 pub schema: String,
258 pub fields: Vec<TableField>,
259}
260
261#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
263pub struct TableField {
264 pub name: String,
265 pub database_type: String,
266 pub base_type: String,
267 pub special_type: Option<String>,
268}