A Rust wrapper around PocketBase Rest API's REST API.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 495 lines 17 kB view raw
1//! `pocketbase-rs` is a Rust wrapper around `PocketBase`'s REST API. 2//! 3//! # Usage 4//! 5//! ```rust,ignore 6//! use std::error::Error; 7//! 8//! use pocketbase_rs::{PocketBase, Collection, RequestError}; 9//! use serde::Deserialize; 10//! 11//! #[derive(Default, Deserialize, Clone)] 12//! struct Article { 13//! title: String, 14//! content: String, 15//! } 16//! 17//! #[tokio::main] 18//! async fn main() -> Result<(), Box<dyn Error>> { 19//! let mut pb = PocketBase::new("http://localhost:8090"); 20//! 21//! let auth_data = pb 22//! .collection("users") 23//! .auth_with_password("YOUR_EMAIL_OR_USERNAME", "YOUR_PASSWORD") 24//! .await?; 25//! 26//! let article: Article = pb 27//! .collection("articles") 28//! .get_one::<Article>("record_id_123") 29//! .call() 30//! .await?; 31//! 32//! println!("Article Title: {}", article.title); 33//! 34//! Ok(()) 35//! } 36//! ``` 37 38#![deny(missing_docs)] 39#![warn(clippy::nursery)] 40#![warn(clippy::pedantic)] 41#![allow(clippy::missing_errors_doc)] 42#![allow(clippy::module_name_repetitions)] 43#![allow(dead_code)] 44 45pub use error::*; 46pub use records::auth::{AuthStore, AuthStoreRecord}; 47use reqwest::RequestBuilder; 48pub use reqwest::multipart::{Form, Part}; 49use serde::{Deserialize, Serialize}; 50 51pub mod error; 52pub(crate) mod records; 53 54/// Represents a specific collection in a `PocketBase` database. 55/// 56/// The `Collection` struct provides an interface for interacting with a specific collection 57/// within a `PocketBase` instance. Instances of this struct are created using the 58/// [`PocketBase::collection`] method. All operations on the target collection, such as retrieving, 59/// creating, updating, or deleting records, are accessible through methods implemented on 60/// this struct. 61/// 62/// # Fields 63/// - `client`: A mutable reference to the `PocketBase` client instance. 64/// This allows the `Collection` to send requests to `PocketBase`. 65/// - `name`: The name of the collection being interacted with. 66pub struct Collection<'a> { 67 pub(crate) client: &'a mut PocketBase, 68 pub(crate) name: &'a str, 69} 70 71impl PocketBase { 72 /// Creates a new [`Collection`] instance for the specified collection name. 73 /// 74 /// This method provides access to operations related to a specific collection in `PocketBase`. 75 /// Most interactions with the `PocketBase` API are performed through the [`Collection`] instance returned 76 /// by this method. 77 /// 78 /// # Arguments 79 /// * `collection_name` - The name of the collection to interact with, provided as a static string. 80 /// 81 /// # Returns 82 /// A [`Collection`] instance configured for the specified collection. 83 /// 84 /// # Example 85 /// ```rust,ignore 86 /// let mut pb = PocketBase::new("http://localhost:8090"); 87 /// 88 /// pb.collection("users") 89 /// .auth_with_password("YOUR_EMAIL_OR_USERNAME", "YOUR_PASSWORD") 90 /// .await?; 91 /// 92 /// let article = pb 93 /// .collection("articles") 94 /// .get_first_list_item::<Article>() 95 /// .filter("language='en'") 96 /// .call() 97 /// .await?; 98 /// ``` 99 /// 100 /// # Panics 101 /// 102 /// This method will panic if the collection name is empty or contains invalid characters. 103 pub fn collection(&mut self, collection_name: &'static str) -> Collection { 104 // Validate collection name 105 assert!( 106 !collection_name.is_empty(), 107 "Collection name cannot be empty" 108 ); 109 110 // Collection names should only contain alphanumeric characters and underscores 111 assert!( 112 collection_name 113 .chars() 114 .all(|c| c.is_alphanumeric() || c == '_'), 115 "Collection name contains invalid characters. Only alphanumeric characters and underscores are allowed" 116 ); 117 118 Collection { 119 client: self, 120 name: collection_name, 121 } 122 } 123} 124 125/// Represents a paginated list of records retrieved from a `PocketBase` collection. 126/// 127/// The `RecordList` struct encapsulates the results of a paginated query to a collection. 128/// It contains metadata about the pagination state (such as the current page, total items, 129/// and total pages) as well as the records themselves. 130/// 131/// This struct is typically returned by methods that fetch a list of records from a 132/// collection, such as [`Collection::get_list`]. 133/// 134/// # Type Parameters 135/// - `T`: The type of the records contained in the `items` list. This is typically a 136/// deserialized struct that matches the schema of the records in the collection. 137/// 138/// # Fields 139/// - `page`: The current page number (starting from 1). 140/// - `per_page`: The maximum number of records returned per page (default is 30). 141/// - `total_items`: The total number of records in the collection that match the query. 142/// - `total_pages`: The total number of pages available for the query. 143/// - `items`: A vector containing the records for the current page. 144#[derive(Debug, Clone, Deserialize)] 145#[serde(rename_all = "camelCase")] 146pub struct RecordList<T> { 147 /// The page (aka. offset) of the paginated list *(default to 1)*. 148 pub page: i32, 149 /// The max returned records per page *(default to 30)*. 150 pub per_page: i32, 151 /// The total amount of records found in the collection. 152 pub total_items: i32, 153 /// The total amount of pages found in the collection. 154 pub total_pages: i32, 155 /// A list of all records for the given page. 156 pub items: Vec<T>, 157} 158 159/// Response structure for API errors from `PocketBase`. 160#[derive(Deserialize, Debug)] 161pub(crate) struct ErrorResponse { 162 /// HTTP status code 163 pub code: u16, 164 /// Error message from the server 165 pub message: String, 166 /// Additional error data, if any 167 pub data: Option<serde_json::Value>, 168} 169 170/// A `PocketBase` client for sending requests to a `PocketBase` instance. 171/// 172/// The `Debug` implementation for this struct redacts sensitive authentication data 173/// to prevent accidental exposure in logs. 174/// 175/// # Example 176/// ```rust,ignore 177/// use std::error::Error; 178/// use pocketbase_rs::PocketBase; 179/// use serde::Deserialize; 180/// 181/// #[derive(Deserialize)] 182/// struct Article { 183/// id: String, 184/// title: String, 185/// } 186/// 187/// #[tokio::main] 188/// async fn main() -> Result<(), Box<dyn Error>> { 189/// let mut pb = PocketBase::new("http://localhost:8090"); 190/// 191/// pb.collection("users") 192/// .auth_with_password("YOUR_EMAIL_OR_USERNAME", "YOUR_PASSWORD") 193/// .await?; 194/// 195/// let article = pb 196/// .collection("articles") 197/// .get_one::<Article>("record_id") 198/// .call() 199/// .await?; 200/// 201/// println!("Article: {:?}", article); 202/// 203/// Ok(()) 204/// } 205/// ``` 206#[derive(Clone)] 207pub struct PocketBase { 208 pub(crate) base_url: String, 209 pub(crate) auth_store: Option<AuthStore>, 210 pub(crate) reqwest_client: reqwest::Client, 211} 212 213impl std::fmt::Debug for PocketBase { 214 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 215 f.debug_struct("PocketBase") 216 .field("base_url", &self.base_url) 217 .field( 218 "auth_store", 219 &self.auth_store.as_ref().map(|_| "***REDACTED***"), 220 ) 221 .field("reqwest_client", &"Client") 222 .finish() 223 } 224} 225 226impl PocketBase { 227 /// Creates a new instance of the `PocketBase` client. 228 /// 229 /// # Example 230 /// ```rust 231 /// let pb = PocketBase::new("http://localhost:8090"); 232 /// // Use the client for further operations like authentication or fetching records 233 /// ``` 234 /// # Panics 235 /// 236 /// This method will panic if the provided `base_url` is not a valid URL. 237 #[must_use] 238 pub fn new(base_url: &str) -> Self { 239 // Validate URL format 240 let trimmed_url = base_url.trim_end_matches('/'); 241 assert!( 242 trimmed_url.starts_with("http://") || trimmed_url.starts_with("https://"), 243 "Invalid base_url: must start with http:// or https://" 244 ); 245 246 // Create client with sensible defaults 247 let client = reqwest::Client::builder() 248 .timeout(std::time::Duration::from_secs(30)) 249 .connect_timeout(std::time::Duration::from_secs(10)) 250 .build() 251 .expect("Failed to create HTTP client"); 252 253 Self { 254 base_url: trimmed_url.to_string(), 255 auth_store: None, 256 reqwest_client: client, 257 } 258 } 259 260 /// Creates a new `PocketBase` client with a custom reqwest client. 261 /// 262 /// # Example 263 /// ```rust 264 /// use std::time::Duration; 265 /// 266 /// let reqwest_client = reqwest::Client::builder() 267 /// .timeout(Duration::from_secs(60)) 268 /// .build() 269 /// .expect("Failed to build client"); 270 /// 271 /// let pb = PocketBase::new_with_client("http://localhost:8090", reqwest_client); 272 /// ``` 273 /// 274 /// # Panics 275 /// 276 /// This method will panic if the provided `base_url` is not a valid URL. 277 #[must_use] 278 pub fn new_with_client(base_url: &str, client: reqwest::Client) -> Self { 279 // Validate URL format 280 let trimmed_url = base_url.trim_end_matches('/'); 281 assert!( 282 trimmed_url.starts_with("http://") || trimmed_url.starts_with("https://"), 283 "Invalid base_url: must start with http:// or https://" 284 ); 285 286 Self { 287 base_url: trimmed_url.to_string(), 288 auth_store: None, 289 reqwest_client: client, 290 } 291 } 292 293 /// Retrieves the current auth store, if available. 294 /// 295 /// # Example 296 /// ```rust,ignore 297 /// let pb = PocketBase::new("http://localhost:8090"); 298 /// 299 /// // ... 300 /// 301 /// if let Some(auth_store) = pb.auth_store() { 302 /// println!("Authenticated with token: {}", auth_store.token); 303 /// } else { 304 /// println!("Not authenticated"); 305 /// } 306 /// ``` 307 #[must_use] 308 pub fn auth_store(&self) -> Option<AuthStore> { 309 self.auth_store.clone() 310 } 311 312 /// Retrieves the current authentication token, if available. 313 /// 314 /// # Example 315 /// ```rust,ignore 316 /// let pb = PocketBase::new("http://localhost:8090"); 317 /// 318 /// // ... 319 /// 320 /// if let Some(token) = pb.token() { 321 /// println!("Authenticated with token: {}", token); 322 /// } else { 323 /// println!("Not authenticated"); 324 /// } 325 /// ``` 326 #[must_use] 327 pub fn token(&self) -> Option<String> { 328 self.auth_store 329 .as_ref() 330 .map(|auth_store| auth_store.token.clone()) 331 } 332 333 /// Returns the base URL of the `PocketBase` server. 334 /// 335 /// # Example 336 /// ```rust,ignore 337 /// let pb = PocketBase::new("http://localhost:8090"); 338 /// assert_eq!(pb.base_url(), "http://localhost:8090".to_string()); 339 /// ``` 340 #[must_use] 341 pub fn base_url(&self) -> String { 342 self.base_url.clone() 343 } 344 345 pub(crate) fn update_auth_store(&mut self, new_auth_store: AuthStore) { 346 self.auth_store = Some(new_auth_store); 347 } 348} 349 350impl PocketBase { 351 /// Adds an authorization token to the request, if available. 352 /// 353 /// This method attaches a bearer authentication token to the provided `RequestBuilder` 354 /// if the client is currently authenticated. If no token is available, the request is 355 /// returned unchanged. 356 /// 357 /// # Arguments 358 /// * `request_builder` - A `reqwest::RequestBuilder` to which the token will be added. 359 /// 360 /// # Returns 361 /// A `reqwest::RequestBuilder` with the authorization token, if applicable. 362 pub(crate) fn with_authorization_token( 363 &self, 364 request_builder: reqwest::RequestBuilder, 365 ) -> reqwest::RequestBuilder { 366 if let Some(auth_store) = self.auth_store() { 367 request_builder.bearer_auth(auth_store.token) 368 } else { 369 request_builder 370 } 371 } 372 373 /// Creates a POST request builder for the specified endpoint. 374 /// 375 /// This method initializes a `POST` request to the given endpoint and adds 376 /// an authorization token if available. 377 /// 378 /// # Arguments 379 /// * `endpoint` - The API endpoint to send the `POST` request to. 380 /// 381 /// # Returns 382 /// A `reqwest::RequestBuilder` for the `POST` request. 383 pub(crate) fn request_post(&self, endpoint: &str) -> RequestBuilder { 384 let request_builder = self.reqwest_client.post(endpoint); 385 self.with_authorization_token(request_builder) 386 } 387 388 /// Creates a PATCH request builder with JSON body for the specified endpoint. 389 /// 390 /// This method initializes a `PATCH` request to the given endpoint with a JSON body, 391 /// and adds an authorization token if available. 392 /// 393 /// # Arguments 394 /// * `endpoint` - The API endpoint to send the `PATCH` request to. 395 /// * `params` - A reference to a serializable type to use as the JSON body of the request. 396 /// 397 /// # Returns 398 /// A `reqwest::RequestBuilder` for the `PATCH` request. 399 pub(crate) fn request_patch_json<T: Default + Serialize + Clone + Send>( 400 &self, 401 endpoint: &str, 402 params: &T, 403 ) -> RequestBuilder { 404 let request_builder = self.reqwest_client.patch(endpoint).json(&params); 405 self.with_authorization_token(request_builder) 406 } 407 408 /// Creates a POST request builder with JSON body for the specified endpoint. 409 /// 410 /// This method initializes a `POST` request to the given endpoint with a JSON body, 411 /// and adds an authorization token if available. 412 /// 413 /// # Arguments 414 /// * `endpoint` - The API endpoint to send the `POST` request to. 415 /// * `params` - A reference to a serializable type to use as the JSON body of the request. 416 /// 417 /// # Returns 418 /// A `reqwest::RequestBuilder` for the `POST` request. 419 pub(crate) fn request_post_json<T: Default + Serialize + Clone + Send>( 420 &self, 421 endpoint: &str, 422 params: &T, 423 ) -> RequestBuilder { 424 let request_builder = self.reqwest_client.post(endpoint).json(&params); 425 self.with_authorization_token(request_builder) 426 } 427 428 /// Creates a POST request builder with a form body for the specified endpoint. 429 /// 430 /// This method initializes a `POST` request to the given endpoint with a multipart form body, 431 /// and adds an authorization token if available. 432 /// 433 /// # Arguments 434 /// * `endpoint` - The API endpoint to send the `POST` request to. 435 /// * `form` - A `reqwest::multipart::Form` representing the form data for the request. 436 /// 437 /// # Returns 438 /// A `reqwest::RequestBuilder` for the `POST` request. 439 pub(crate) fn request_post_form(&self, endpoint: &str, form: Form) -> RequestBuilder { 440 let request_builder = self.reqwest_client.post(endpoint).multipart(form); 441 self.with_authorization_token(request_builder) 442 } 443 444 /// Creates a GET request builder for the specified endpoint. 445 /// 446 /// This method initializes a `GET` request to the given endpoint, adds an `Accept` header 447 /// for JSON responses, attaches query parameters if provided, and adds an authorization 448 /// token if available. 449 /// 450 /// # Arguments 451 /// * `endpoint` - The API endpoint to send the `GET` request to. 452 /// * `params` - An optional vector of key-value pairs to include as query parameters. 453 /// 454 /// # Returns 455 /// A `reqwest::RequestBuilder` for the `GET` request. 456 pub(crate) fn request_get( 457 &self, 458 endpoint: &str, 459 params: Option<Vec<(&str, &str)>>, 460 ) -> RequestBuilder { 461 let mut request_builder = self 462 .reqwest_client 463 .get(endpoint) 464 .header("Accept", "application/json"); 465 466 if let Some(params) = params { 467 request_builder = request_builder.query(&params); 468 } 469 470 self.with_authorization_token(request_builder) 471 } 472 473 /// Creates a DELETE request builder for the specified endpoint. 474 /// 475 /// This method initializes a `DELETE` request to the given endpoint and adds 476 /// an authorization token if available. 477 /// 478 /// # Arguments 479 /// * `endpoint` - The API endpoint to send the `DELETE` request to. 480 /// 481 /// # Returns 482 /// A `reqwest::RequestBuilder` for the `DELETE` request. 483 /// 484 /// # Example 485 /// ```rust,ignore 486 /// let pb = PocketBase::new("http://localhost:8090"); 487 /// 488 /// let request = pb.request_delete("http://localhost:8090/api/collections/articles/record_id"); 489 /// ``` 490 pub(crate) fn request_delete(&self, endpoint: &str) -> RequestBuilder { 491 let request_builder = self.reqwest_client.delete(endpoint); 492 493 self.with_authorization_token(request_builder) 494 } 495}