Repository: emreyalvac/firebase-rs Branch: master Commit: ef20b98c626f Files: 11 Total size: 24.3 KB Directory structure: gitextract_6kyzg2xz/ ├── .cargo/ │ └── config ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md └── src/ ├── constants.rs ├── errors.rs ├── lib.rs ├── params.rs ├── sse.rs └── utils.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cargo/config ================================================ [alias] t = "test -- --nocapture" ================================================ FILE: .gitignore ================================================ /target .idea ================================================ FILE: Cargo.toml ================================================ [package] name = "firebase-rs" edition = "2021" version = "2.2.5" description = "Rust based Firebase library" readme = "README.md" repository = "https://github.com/emreyalvac/firebase-rs" documentation = "https://docs.rs/firebase-rs/2.2.5/firebase_rs/" license = "MIT" authors = ["Emre YALVAÇ "] exclude = ["examples/*", "tests/*"] keywords = ["firebase", "rest", "api", "web", "database"] [dependencies] url = "2.2.3" reqwest = { version = "0.12.12", features = ["json"] } serde_json = "1.0.137" serde = { version = "1.0.217", features = ["derive"] } itertools = "0.14.0" eventsource-client = "0.13.0" futures-util = "0.3.31" [dev-dependencies] tokio = { version = "1.43.0", features = ["rt", "macros", "rt-multi-thread"] } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Emre YALVAÇ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # firebase-rs [![Crates.io](https://img.shields.io/crates/v/firebase-rs.svg)](https://crates.io/crates/firebase-rs) [![docs.rs](https://docs.rs/firebase-rs/badge.svg)](https://docs.rs/firebase-rs) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) Rust based Firebase library. --- ![firebase](https://firebase.google.com/downloads/brand-guidelines/SVG/logo-logomark.svg 'Firebase') # Full Documentation [Documentation](https://docs.rs/firebase-rs/2.2.5/firebase_rs/) # Features - Server-Sent Events (a.k.a stream) (https://github.com/emreyalvac/firebase-rs#read-data-as-stream) - Generic Payload # How to use ### Load library ````rust use firebase_rs::*; ```` ### Without Auth ````rust let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap(); ```` ### With Auth ````rust let firebase = Firebase::auth("https://myfirebase.firebaseio.com", "AUTH_KEY").unwrap(); ```` --- ### At usage for nested objects ````rust let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().at("users").at("USER_ID").at(...); ```` --- ### Read Data as Stream ### With Real Time Events ````rust let firebase = Firebase::new("https://myfirebase.firebaseio.com").at("users").unwrap(); let stream = firebase.with_realtime_events().unwrap(); stream .listen( | event_type, data| { println ! ("Type: {:?} Data: {:?}", event_type, data); }, | err| println!("{:?}", err), false).await; ```` ### Read Data #### Read data as string ````rust let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().at("users"); let users = firebase.get_as_string().await; ```` #### Read data with generic type (All) ````rust #[derive(Serialize, Deserialize, Debug)] struct User { name: String } let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().at("users"); let user = firebase.get::> ().await; ```` #### Read data with generic type (Single record) ````rust #[derive(Serialize, Deserialize, Debug)] struct User { name: String } let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().at("users").at("USER_ID"); let user = firebase.get::().await; ```` --- ### Set Data with Custom Key ````rust #[derive(Serialize, Deserialize, Debug)] struct User { name: String } let user = User { name: String::default () }; let mut firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().at("users"); firebase.set_with_key("myKey", &user).await; ```` _Output_ ```json { "users": { "myKey": { "name": "" } } } ``` ### Set Data ````rust #[derive(Serialize, Deserialize, Debug)] struct User { name: String } let user = User { name: String::default () }; let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().at("users"); firebase.set(&user).await; ```` _Output_ ```json { "users": { "-OC9mYIUIdY3JygkmsFQ": { "name": "" } } } ``` --- ### Update Data ````rust #[derive(Serialize, Deserialize, Debug)] struct User { name: String } let user = User { name: String::default () }; let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().at("users").at("USER_ID"); firebase.update( &user).await; ```` --- ### With Params ````rust let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().with_params().start_at(1).order_by("name").equal_to(5).finish(); let result = firebase.get().await; ```` ## Contributors Thanks goes to these wonderful people ✨ ================================================ FILE: src/constants.rs ================================================ pub const AUTH: &str = "auth"; pub const ORDER_BY: &str = "orderBy"; pub const LIMIT_TO_FIRST: &str = "limitToFirst"; pub const LIMIT_TO_LAST: &str = "limitToLast"; pub const START_AT: &str = "startAt"; pub const END_AT: &str = "endAt"; pub const EQUAL_TO: &str = "equalTo"; pub const SHALLOW: &str = "shallow"; pub const FORMAT: &str = "format"; pub const EXPORT: &str = "export"; #[derive(Debug, PartialEq)] pub enum Method { GET, POST, DELETE, PATCH, PUT, } #[derive(Debug)] pub struct Response { pub data: String, } impl Response { pub fn new() -> Self { Self { data: String::default(), } } } ================================================ FILE: src/errors.rs ================================================ use std::{ error::Error, fmt::{Display, Formatter}, }; pub type UrlParseResult = Result; #[derive(Debug)] #[non_exhaustive] pub enum UrlParseError { NoPath, NotHttps, Parser(url::ParseError), } impl Error for UrlParseError {} impl Display for UrlParseError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { UrlParseError::NoPath => write!(f, "URL path is missing."), UrlParseError::NotHttps => write!(f, "The URL protocol should be https."), UrlParseError::Parser(e) => write!(f, "Error while parsing the URL: {}", e), } } } pub type RequestResult = Result; #[derive(Debug)] #[non_exhaustive] pub enum RequestError { NotJSON, NoUTF8, NetworkError, SerializeError(serde_json::Error), DeserializeError(serde_json::Error), NotFoundOrNullBody, Unauthorized, } impl Error for RequestError {} impl Display for RequestError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { RequestError::NotJSON => write!(f, "Invalid JSON"), RequestError::NoUTF8 => write!(f, "Utf8 error"), RequestError::NetworkError => write!(f, "Network error"), RequestError::SerializeError(e) => write!(f, "Failed to serialize request: {}", e), RequestError::DeserializeError(e) => write!(f, "Failed to deserialize response: {}", e), RequestError::NotFoundOrNullBody => write!(f, "Body is null or record is not found"), RequestError::Unauthorized => write!(f, "Unauthorized"), } } } #[derive(Debug)] #[non_exhaustive] pub enum ServerEventError { ConnectionError, } impl Error for ServerEventError {} impl Display for ServerEventError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { ServerEventError::ConnectionError => write!(f, "Connection error for server events"), } } } ================================================ FILE: src/lib.rs ================================================ use constants::{Method, Response, AUTH}; use errors::{RequestResult, UrlParseResult}; use params::Params; use reqwest::StatusCode; use serde::de::DeserializeOwned; use serde::Serialize; use serde_json::Value; use std::fmt::Debug; use url::Url; use utils::check_uri; use crate::sse::ServerEvents; pub use errors::{RequestError, ServerEventError, UrlParseError}; mod constants; mod errors; mod params; mod sse; mod utils; #[derive(Debug)] pub struct Firebase { uri: Url, } impl Firebase { /// ```rust /// use firebase_rs::Firebase; /// /// let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap(); /// ``` pub fn new(uri: &str) -> UrlParseResult where Self: Sized, { match check_uri(&uri) { Ok(uri) => Ok(Self { uri }), Err(err) => Err(err), } } /// ```rust /// const URI: &str = "..."; /// const AUTH_KEY: &str = "..."; /// /// use firebase_rs::Firebase; /// /// let firebase = Firebase::auth("https://myfirebase.firebaseio.com", AUTH_KEY).unwrap(); /// ``` pub fn auth(uri: &str, auth_key: &str) -> UrlParseResult where Self: Sized, { match check_uri(&uri) { Ok(mut uri) => { uri.set_query(Some(&format!("{}={}", AUTH, auth_key))); Ok(Self { uri }) } Err(err) => Err(err), } } /// ```rust /// use firebase_rs::Firebase; /// /// # async fn run() { /// let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().with_params().start_at(1).order_by("name").equal_to(5).finish(); /// let result = firebase.get::().await; /// # } /// ``` pub fn with_params(&self) -> Params { let uri = self.uri.clone(); Params::new(uri) } /// To use simple interface with synchronous callbacks, pair with `.listen()`: /// ```rust /// use firebase_rs::Firebase; /// # async fn run() { /// let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().at("users"); /// let stream = firebase.with_realtime_events().unwrap(); /// stream.listen(|event_type, data| { /// println!("{:?} {:?}" ,event_type, data); /// }, |err| println!("{:?}" ,err), false).await; /// # } /// ``` /// /// To use streaming interface for async code, pair with `.stream()`: /// ```rust /// use firebase_rs::Firebase; /// use futures_util::StreamExt; /// /// # async fn run() { /// let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().at("users"); /// let stream = firebase.with_realtime_events() /// .unwrap() /// .stream(true); /// stream.for_each(|event| { /// match event { /// Ok((event_type, maybe_data)) => println!("{:?} {:?}" ,event_type, maybe_data), /// Err(err) => println!("{:?}" ,err), /// } /// futures_util::future::ready(()) /// }).await; /// # } /// ``` pub fn with_realtime_events(&self) -> Option { ServerEvents::new(self.uri.as_str()) } /// ```rust /// use firebase_rs::Firebase; /// /// let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().at("users").at("USER_ID").at("f69111a8a5258c15286d3d0bd4688c55"); /// ``` pub fn at(&self, path: &str) -> Self { let uri = self.build_uri(path); Firebase { uri } } fn build_uri(&self, path: &str) -> Url { let mut new_path = String::new(); if let Some(segments) = self.uri.path_segments() { for segment in segments { let clean_segment = segment.trim_end_matches(".json"); new_path.push_str(clean_segment); new_path.push('/'); } } new_path.push_str(path); let final_path = if new_path.ends_with(".json") { new_path.trim_end_matches(".json").to_string() } else { new_path }; let mut uri = self.uri.clone(); uri.set_path(&format!("{}.json", final_path)); uri } /// ```rust /// use firebase_rs::Firebase; /// /// let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().at("users"); /// let uri = firebase.get_uri(); /// ``` pub fn get_uri(&self) -> String { self.uri.to_string() } async fn request(&self, method: Method, data: Option) -> RequestResult { let client = reqwest::Client::new(); let request = match method { Method::GET => client.get(self.uri.to_string()).send().await, Method::PUT | Method::PATCH | Method::POST => { if data.is_none() { return Err(RequestError::NotFoundOrNullBody); } let builder = if method == Method::PUT { client.put(self.uri.to_string()) } else if method == Method::POST { client.post(self.uri.to_string()) } else { client.patch(self.uri.to_string()) }; builder.json(&data).send().await } Method::DELETE => client.delete(self.uri.to_string()).send().await, }; match request { Ok(response) => match response.status() { StatusCode::OK | StatusCode::NO_CONTENT => { let response_text = response.text().await.unwrap_or_default(); Ok(Response { data: response_text, }) } StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => Err(RequestError::Unauthorized), StatusCode::NOT_FOUND => Err(RequestError::NotFoundOrNullBody), _ => Err(RequestError::NetworkError), }, Err(_) => Err(RequestError::NetworkError), } } async fn request_generic(&self, method: Method) -> RequestResult where T: DeserializeOwned + Debug, { let request = self.request(method, None).await; match request { Ok(response) => { let value: serde_json::Value = serde_json::from_str(response.data.as_str()) .map_err(|_| RequestError::NotJSON)?; let data: T = serde_json::from_value(value) .map_err(RequestError::DeserializeError)?; Ok(data) } Err(err) => Err(err), } } /// ```rust /// use firebase_rs::Firebase; /// use serde::{Serialize, Deserialize}; /// /// #[derive(Serialize, Deserialize, Debug)] /// struct User { /// name: String /// } /// /// # async fn run() { /// let user = User { name: String::default() }; /// let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().at("users"); /// let users = firebase.set(&user).await; /// # } /// ``` pub async fn set(&self, data: &T) -> RequestResult where T: Serialize + Debug, { let data = serde_json::to_value(&data).map_err(RequestError::SerializeError)?; self.request(Method::POST, Some(data)).await } /// ```rust /// use firebase_rs::Firebase; /// use serde::{Serialize, Deserialize}; /// /// #[derive(Serialize, Deserialize, Debug)] /// struct User { /// name: String /// } /// /// # async fn run() { /// let user = User { name: String::default() }; /// let mut firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().at("users"); /// let users = firebase.set_with_key("myKey", &user).await; /// # } /// ``` pub async fn set_with_key(&mut self, key: &str, data: &T) -> RequestResult where T: Serialize + Debug, { self.uri = self.build_uri(key); let data = serde_json::to_value(&data).map_err(RequestError::SerializeError)?; self.request(Method::PUT, Some(data)).await } /// ```rust /// use std::collections::HashMap; /// use firebase_rs::Firebase; /// use serde::{Serialize, Deserialize}; /// /// #[derive(Serialize, Deserialize, Debug)] /// struct User { /// name: String /// } /// /// # async fn run() { /// let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().at("users"); /// let users = firebase.get::>().await; /// # } /// ``` pub async fn get_as_string(&self) -> RequestResult { self.request(Method::GET, None).await } /// ```rust /// use std::collections::HashMap; /// use firebase_rs::Firebase; /// use serde::{Serialize, Deserialize}; /// /// #[derive(Serialize, Deserialize, Debug)] /// struct User { /// name: String /// } /// /// # async fn run() { /// let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().at("users").at("USER_ID"); /// let user = firebase.get::().await; /// /// // OR /// /// let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().at("users"); /// let user = firebase.get::>().await; /// # } /// ``` pub async fn get(&self) -> RequestResult where T: DeserializeOwned + Debug, { self.request_generic::(Method::GET).await } /// ```rust /// use firebase_rs::Firebase; /// /// # async fn run() { /// let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().at("users").at("USER_ID"); /// firebase.delete().await; /// # } /// ``` pub async fn delete(&self) -> RequestResult { self.request(Method::DELETE, None).await } /// ```rust /// use firebase_rs::Firebase; /// use serde::{Serialize, Deserialize}; /// /// #[derive(Serialize, Deserialize, Debug)] /// struct User { /// name: String /// } /// /// # async fn run() { /// let user = User { name: String::default() }; /// let firebase = Firebase::new("https://myfirebase.firebaseio.com").unwrap().at("users").at("USER_ID"); /// let users = firebase.update(&user).await; /// # } /// ``` pub async fn update(&self, data: &T) -> RequestResult where T: Serialize + Debug, { let value = serde_json::to_value(&data).map_err(RequestError::SerializeError)?; self.request(Method::PATCH, Some(value)).await } } #[cfg(test)] mod tests { use crate::{Firebase, UrlParseError}; const URI: &str = "https://firebase_id.firebaseio.com"; const URI_WITH_SLASH: &str = "https://firebase_id.firebaseio.com/"; const URI_NON_HTTPS: &str = "http://firebase_id.firebaseio.com/"; #[tokio::test] async fn simple() { let firebase = Firebase::new(URI).unwrap(); assert_eq!(URI_WITH_SLASH.to_string(), firebase.get_uri()); } #[tokio::test] async fn non_https() { let firebase = Firebase::new(URI_NON_HTTPS).map_err(|e| e.to_string()); assert_eq!( firebase.err(), Some(String::from(UrlParseError::NotHttps.to_string())) ); } #[tokio::test] async fn with_auth() { let firebase = Firebase::auth(URI, "auth_key").unwrap(); assert_eq!( format!("{}/?auth=auth_key", URI.to_string()), firebase.get_uri() ); } #[tokio::test] async fn with_sse_events() { // TODO: SSE Events Test } } ================================================ FILE: src/params.rs ================================================ use crate::constants::{ END_AT, EQUAL_TO, EXPORT, FORMAT, LIMIT_TO_FIRST, LIMIT_TO_LAST, ORDER_BY, SHALLOW, START_AT, }; use crate::Firebase; use itertools::Itertools; use std::collections::HashMap; use url::Url; #[derive(Debug)] pub struct Params { pub uri: Url, pub params: HashMap, } impl Params { pub fn new(uri: Url) -> Self { Self { uri, params: Default::default(), } } pub fn set_params(&mut self) -> () { for (k, v) in self.params.iter().sorted() { self.uri.query_pairs_mut().append_pair(k, v); } } pub fn add_param(&mut self, key: &str, value: T) -> &mut Self where T: ToString, { self.params.insert(key.to_string(), value.to_string()); self.set_params(); self } pub fn order_by(&mut self, key: &str) -> &mut Params { self.add_param(ORDER_BY, key) } pub fn limit_to_first(&mut self, count: u32) -> &mut Params { self.add_param(LIMIT_TO_FIRST, count) } pub fn limit_to_last(&mut self, count: u32) -> &mut Params { self.add_param(LIMIT_TO_LAST, count) } pub fn start_at(&mut self, index: u32) -> &mut Params { self.add_param(START_AT, index) } pub fn end_at(&mut self, index: u32) -> &mut Params { self.add_param(END_AT, index) } pub fn equal_to(&mut self, value: u32) -> &mut Params { self.add_param(EQUAL_TO, value) } pub fn shallow(&mut self, flag: bool) -> &mut Params { self.add_param(SHALLOW, flag) } pub fn format(&mut self) -> &mut Params { self.add_param(FORMAT, EXPORT) } pub fn finish(&self) -> Firebase { Firebase::new(self.uri.as_str()).unwrap() } } #[cfg(test)] mod tests { use crate::params::Params; use std::collections::HashMap; use url::Url; #[test] fn check_params() { let mut params: HashMap = HashMap::new(); params.insert("param_1".to_owned(), "value_1".to_owned()); params.insert("param_2".to_owned(), "value_2".to_owned()); let mut param = Params { uri: Url::parse("https://github.com/emreyalvac").unwrap(), params, }; param.set_params(); assert_eq!( param.uri.as_str(), "https://github.com/emreyalvac?param_1=value_1¶m_2=value_2" ) } } ================================================ FILE: src/sse.rs ================================================ use eventsource_client::*; use futures_util::StreamExt; pub struct ServerEvents { client: ClientBuilder, } impl ServerEvents { pub fn new(url: &str) -> Option { let client = ClientBuilder::for_url(url); match client { Ok(stream_connection) => Some(ServerEvents { client: stream_connection, }), Err(_) => None, } } pub async fn listen( self, stream_event: impl Fn(String, Option), stream_err: impl Fn(Error), keep_alive_friendly: bool, ) { self.stream(keep_alive_friendly) .for_each(|event| { match event { Ok((event_type, maybe_data)) => stream_event(event_type, maybe_data), Err(x) => stream_err(x), } futures_util::future::ready(()) }) .await } pub fn stream( self, keep_alive_friendly: bool, ) -> std::pin::Pin)>> + Send>> { Box::pin( self.client .build() .stream() .filter_map(move |event| async move { match event { Ok(SSE::Event(ev)) => { if ev.event_type == "keep-alive" && !keep_alive_friendly { return None; } if ev.data == "null" { return Some(Ok((ev.event_type, None))); } return Some(Ok((ev.event_type, Some(ev.data)))); } Ok(SSE::Comment(_)) | Ok(SSE::Connected(_)) => return None, Err(x) => Some(Err(x)), } }), ) } } ================================================ FILE: src/utils.rs ================================================ use crate::errors::UrlParseResult; use crate::UrlParseError; use url::Url; pub fn check_uri(uri: &str) -> UrlParseResult { let uri = Url::parse(uri); let uri = match uri { Ok(res) => res, Err(err) => return Err(UrlParseError::Parser(err)), }; if uri.scheme() != "https" { return Err(UrlParseError::NotHttps); } Ok(uri) }