aw_test/
client.rs

1use reqwest::header::HeaderMap;
2use std::{collections::HashMap, str::FromStr};
3use std::path::PathBuf;
4use crate::services::AppwriteException;
5
6#[derive(Clone)]
7pub struct Client {
8    endpoint: url::Url,
9    headers: HeaderMap,
10    client: reqwest::blocking::Client,
11}
12
13pub static CHUNK_SIZE: u64 = 5*1024*1024; // 5MB
14
15#[derive(Clone, Deserialize, Serialize, Debug)]
16#[serde(untagged)]
17pub enum ParamType {
18    Bool(bool),
19    Number(i64),
20    String(String),
21    Array(Vec<ParamType>),
22    FilePath(PathBuf),
23    Object(HashMap<String, ParamType>),
24    Float(f64),
25    StreamData(Vec<u8>, String),
26    OptionalBool(Option<bool>),
27    OptionalNumber(Option<i64>),
28    OptionalArray(Option<Vec<ParamType>>),
29    OptionalFilePath(Option<PathBuf>),
30    OptionalObject(Option<HashMap<String, ParamType>>),
31    OptionalFloat(Option<f64>),
32    DocumentList(crate::models::DocumentList),
33    CollectionList(crate::models::CollectionList),
34    IndexList(crate::models::IndexList),
35    UserList(crate::models::UserList),
36    SessionList(crate::models::SessionList),
37    LogList(crate::models::LogList),
38    FileList(crate::models::FileList),
39    BucketList(crate::models::BucketList),
40    TeamList(crate::models::TeamList),
41    MembershipList(crate::models::MembershipList),
42    FunctionList(crate::models::FunctionList),
43    RuntimeList(crate::models::RuntimeList),
44    DeploymentList(crate::models::DeploymentList),
45    ExecutionList(crate::models::ExecutionList),
46    ProjectList(crate::models::ProjectList),
47    WebhookList(crate::models::WebhookList),
48    KeyList(crate::models::KeyList),
49    PlatformList(crate::models::PlatformList),
50    DomainList(crate::models::DomainList),
51    CountryList(crate::models::CountryList),
52    ContinentList(crate::models::ContinentList),
53    LanguageList(crate::models::LanguageList),
54    CurrencyList(crate::models::CurrencyList),
55    PhoneList(crate::models::PhoneList),
56    MetricList(crate::models::MetricList),
57    Collection(crate::models::Collection),
58    AttributeList(crate::models::AttributeList),
59    AttributeString(crate::models::AttributeString),
60    AttributeInteger(crate::models::AttributeInteger),
61    AttributeFloat(crate::models::AttributeFloat),
62    AttributeBoolean(crate::models::AttributeBoolean),
63    AttributeEmail(crate::models::AttributeEmail),
64    AttributeEnum(crate::models::AttributeEnum),
65    AttributeIp(crate::models::AttributeIp),
66    AttributeUrl(crate::models::AttributeUrl),
67    Index(crate::models::Index),
68    Document(crate::models::Document),
69    Log(crate::models::Log),
70    User(crate::models::User),
71    Preferences(crate::models::Preferences),
72    Session(crate::models::Session),
73    Token(crate::models::Token),
74    Jwt(crate::models::Jwt),
75    Locale(crate::models::Locale),
76    File(crate::models::File),
77    Bucket(crate::models::Bucket),
78    Team(crate::models::Team),
79    Membership(crate::models::Membership),
80    Function(crate::models::Function),
81    Runtime(crate::models::Runtime),
82    Deployment(crate::models::Deployment),
83    Execution(crate::models::Execution),
84    Project(crate::models::Project),
85    Webhook(crate::models::Webhook),
86    Key(crate::models::Key),
87    Domain(crate::models::Domain),
88    Platform(crate::models::Platform),
89    Country(crate::models::Country),
90    Continent(crate::models::Continent),
91    Language(crate::models::Language),
92    Currency(crate::models::Currency),
93    Phone(crate::models::Phone),
94    HealthAntivirus(crate::models::HealthAntivirus),
95    HealthQueue(crate::models::HealthQueue),
96    HealthStatus(crate::models::HealthStatus),
97    HealthTime(crate::models::HealthTime),
98    Metric(crate::models::Metric),
99    UsageDatabase(crate::models::UsageDatabase),
100    UsageCollection(crate::models::UsageCollection),
101    UsageUsers(crate::models::UsageUsers),
102    UsageStorage(crate::models::UsageStorage),
103    UsageBuckets(crate::models::UsageBuckets),
104    UsageFunctions(crate::models::UsageFunctions),
105    UsageProject(crate::models::UsageProject),
106    }
107
108// Converts optionals into normal ParamTypes
109fn handleOptional(param: ParamType) -> Option<ParamType> {
110    match param {
111        ParamType::OptionalBool(value) => match value {
112            Some(data) => Some(ParamType::Bool(data)),
113            None => None
114        }
115        ParamType::OptionalNumber(value) => match value {
116            Some(data) => Some(ParamType::Number(data)),
117            None => None
118        }
119        ParamType::OptionalArray(value) => match value {
120            Some(data) => Some(ParamType::Array(data)),
121            None => None
122        }
123        ParamType::OptionalFilePath(value) => match value {
124            Some(data) => Some(ParamType::FilePath(data)),
125            None => None
126        }
127        ParamType::OptionalObject(value) => match value {
128            Some(data) => Some(ParamType::Object(data)),
129            None => None
130        }
131        ParamType::OptionalFloat(value) => match value {
132            Some(data) => Some(ParamType::Float(data)),
133            None => None
134        }
135        _ => Some(param)
136    }
137}
138
139/// Example
140/// ```rust
141/// let mut client = appwrite::client::Client::new();
142/// 
143/// client.set_endpoint("Your Endpoint URL");
144/// client.set_project("Your Project ID");
145/// client.set_key("Your API Key");
146/// 
147/// // Create a user as a example
148/// let userService = appwrite::services::Users::new(&client);
149/// let response = userService.create("amadeus@example.com", "supersecurepassword", "Wolfgang Amadeus Mozart");
150/// 
151/// println!("{}", response.text().unwrap()); // Here you can also check the status code to see success
152/// ```
153impl Client {
154    pub fn new() -> Self {
155        let mut new_headers = HeaderMap::new();
156
157        new_headers.insert("x-sdk-version", "appwrite:rust:0.0.2".parse().unwrap());
158        new_headers.insert("user-agent", format!("{}-rust-{}", std::env::consts::OS, "0.0.2").parse().unwrap());
159        new_headers.insert("X-Appwrite-Response-Format", "0.7.0".parse().unwrap());
160
161        Self {
162            endpoint: "https://HOSTNAME/v1".parse().unwrap(),
163            headers: new_headers,
164            client: reqwest::blocking::Client::builder()
165            .build().unwrap(),
166        }
167    }
168
169    pub fn add_header(&mut self, key: String, value: String) {
170        self.headers.append(
171            reqwest::header::HeaderName::from_str(&key).unwrap(),
172            (&value.to_lowercase()).parse().unwrap(),
173        );
174    }
175
176    pub fn add_self_signed(&mut self, value: bool) {
177      self.client = reqwest::blocking::Client::builder().danger_accept_invalid_certs(value).build().unwrap();
178    }
179
180    /// Sets Your project ID
181    pub fn set_project(&mut self, value: &str) {
182        self.add_header("X-Appwrite-Project".to_string(), value.to_string())
183    }
184
185    /// Sets Your secret API key
186    pub fn set_key(&mut self, value: &str) {
187        self.add_header("X-Appwrite-Key".to_string(), value.to_string())
188    }
189
190    /// Sets Your secret JSON Web Token
191    pub fn set_jwt(&mut self, value: &str) {
192        self.add_header("X-Appwrite-JWT".to_string(), value.to_string())
193    }
194
195    pub fn set_locale(&mut self, value: &str) {
196        self.add_header("X-Appwrite-Locale".to_string(), value.to_string())
197    }
198
199    pub fn set_mode(&mut self, value: &str) {
200        self.add_header("X-Appwrite-Mode".to_string(), value.to_string())
201    }
202
203
204    pub fn set_endpoint(&mut self, endpoint: &str) {
205        self.endpoint = endpoint.parse().unwrap()
206    }
207
208    pub fn call(
209        self,
210        method: &str,
211        path: &str,
212        headers: Option<HashMap<String, String>>,
213        params: Option<HashMap<String, ParamType>>,
214    ) -> Result<reqwest::blocking::Response, AppwriteException> {
215        // If we have headers in the function call we combine them with the client headers.
216
217        let mut content_type: String = "application/json".to_string();
218
219        let request_headers: HeaderMap = match headers {
220            Some(data) => {
221                let mut headers = self.headers.clone();
222
223                for (k, v) in data {
224                  if k == "content-type" {
225                    content_type = v.to_string()
226                  } else {
227                    headers.append(
228                        reqwest::header::HeaderName::from_lowercase(k.as_bytes()).unwrap(),
229                        (&v.to_lowercase()).parse().unwrap(),
230                    );
231                  }
232                }
233
234                headers
235            }
236            None => self.headers.clone(),
237        };
238
239        // Now start building request with reqwest
240        let method_type = match method {
241            "GET" => reqwest::Method::GET,
242            "POST" => reqwest::Method::POST,
243            "OPTIONS" => reqwest::Method::OPTIONS,
244            "PUT" => reqwest::Method::PUT,
245            "DELETE" => reqwest::Method::DELETE,
246            "HEAD" => reqwest::Method::HEAD,
247            "PATCH" => reqwest::Method::PATCH,
248            _ => reqwest::Method::GET,
249        };
250
251        let mut request = self
252            .client
253            .request(method_type, self.endpoint.join(&format!("{}{}", "v1", path)).unwrap());
254
255        match params {
256            Some(data) => {
257                let flattened_data = flatten(FlattenType::Normal(data.clone()), None);
258
259                // Handle Optional Values
260                // Remove all optionals that result in None
261                // Turn all Optional____ into their non optional equivilants.
262                let mut buffer: Vec<(String, ParamType)> = Vec::new();
263                for (k, v) in flattened_data {
264                    match handleOptional(v) {
265                        Some(data) => buffer.push((k, data)),
266                        None => {}
267                    }
268                }
269                let flattened_data = buffer;
270
271                // First flatten the data and feed it into a FormData
272                if content_type.starts_with("multipart/form-data") {
273                    let mut form = reqwest::blocking::multipart::Form::new();
274
275                    for (k, v) in flattened_data.clone() {
276                        match v {
277                            ParamType::Bool(data) => {
278                                form = form.text(k, data.to_string());
279                            }
280                            ParamType::String(data) => form = form.text(k, data),
281                            ParamType::FilePath(data) => form = form.file(k, data).unwrap(),
282                            ParamType::Number(data) => form = form.text(k, data.to_string()),
283                            ParamType::Float(data) => form = form.text(k, data.to_string()),
284                            ParamType::StreamData(data, filename) => form = form.part(k, reqwest::blocking::multipart::Part::bytes(data).file_name(filename)),
285                            // This shouldn't be possible due to the flatten function, so we won't handle this for now
286                            ParamType::Array(_data) => {
287                                //todo: Feed this back into a flatten function if needed
288                            },
289                            ParamType::Object(_data) => {
290                              // Same for this
291                            },
292                            _ => {}
293                        }
294                    }
295                    request = request.multipart(form);
296                }
297
298                if content_type.starts_with("application/json") && method != "GET" {
299                    request = request.json(&data);
300                }
301
302                if method == "GET" {
303                    request = request.query(&queryize_data(flatten(FlattenType::Normal(data), None)));
304                }
305            }
306            None => {}
307        }
308
309        request = request.headers(request_headers);
310
311        match request.send() {
312            Ok(data) => {
313                if data.status().is_success() {
314                    Ok(data)    
315                } else {
316                    let dataString = match data.text() {
317                      Ok(data) => {data},
318                      Err(err) => {
319                        // Last Resort. Called if string isn't even readable text.
320                        return Err(AppwriteException::new(format!("A error occoured. ERR: {}, This could either be a connection error or an internal Appwrite error. Please check your Appwrite instance logs. ", err), 0, "".to_string()))
321                      }
322                    };
323
324                    // Format error
325                    Err(match serde_json::from_str(&dataString) {
326                        Ok(data) => data,
327                        Err(_err) => {
328                            AppwriteException::new(format!("{}", dataString), 0, "".to_string())
329                        }
330                    })
331                }
332            },
333            Err(err) => {
334                // Throw Appwrite Exception
335                Err(AppwriteException::new(format!("{}", err), 0, "".to_string()))
336             },
337        }
338    }
339}
340
341enum FlattenType {
342    Normal(HashMap<String, ParamType>),
343    Nested(Vec<ParamType>),
344}
345
346fn queryize_data(data: Vec<(String, ParamType)>) -> Vec<(String, String)> {
347    let mut output: Vec<(String, String)> = Default::default();
348
349    for (k, v) in data {
350        match v {
351            ParamType::Bool(value) => output.push((k, value.to_string())),
352            ParamType::String(value) => output.push((k, value)),
353            ParamType::Number(value) => output.push((k, value.to_string())),
354            _ => {}
355        }
356    }
357
358    output
359}
360
361fn flatten(data: FlattenType, prefix: Option<String>) -> Vec<(String, ParamType)> {
362    let mut output: Vec<(String, ParamType)> = Default::default();
363
364    match data {
365        FlattenType::Normal(data) => {
366            for (k, v) in data {
367                let final_key = match &prefix {
368                    Some(current_prefix) => format!("{}[{}]", current_prefix, k),
369                    None => k,
370                };
371
372                match v {
373                    ParamType::Array(value) => {
374                        output.append(&mut flatten(FlattenType::Nested(value), Some(final_key)));
375                    }
376                    ParamType::Object(value) => {
377                        output.extend(flatten(FlattenType::Normal(value), Some(final_key)).into_iter())
378                    },
379                    value => {
380                        output.push((final_key, value));
381                    }
382                }
383            }
384        }
385
386        FlattenType::Nested(data) => {
387            for (k, v) in data.iter().enumerate() {
388                let final_key = match &prefix {
389                    Some(current_prefix) => format!("{}[{}]", current_prefix, k),
390                    None => k.to_string(),
391                };
392
393                match v {
394                    ParamType::Array(value) => {
395                        flatten(FlattenType::Nested(value.to_owned()), Some(final_key))
396                            .append(&mut output);
397                    }
398                    value => {
399                        output.push((final_key, value.to_owned()));
400                    }
401                }
402            }
403        }
404    }
405
406    output
407}