#![cfg_attr(doc, doc = include_str!("../README.md"))]
#![expect(
clippy::multiple_inherent_impl,
reason = "Raw filter grammar and request execution have separate modules"
)]
extern crate alloc;
mod builder;
mod count;
mod error;
mod filter;
mod response;
pub use builder::Builder;
pub use count::{Count, CountError, Counted};
pub use error::{ConfigError, Error, ResponseMetadata};
pub use reqwest;
pub use rp_postgrest_error;
use alloc::sync::Arc;
use reqwest::{
Client, Url,
header::{HeaderMap, HeaderName, HeaderValue},
};
use serde::Serialize;
#[derive(Clone, Debug)]
struct Configuration {
base: Url,
schema: Option<String>,
headers: HeaderMap,
client: Client,
}
#[derive(Clone, Debug)]
pub struct Postgrest {
configuration: Arc<Configuration>,
}
impl Postgrest {
pub fn new<U: AsRef<str>>(base_url: U) -> Result<Self, Error> {
let client = Client::builder()
.redirect(same_origin_redirects())
.build()
.map_err(ConfigError::Client)?;
Self::new_with_client(base_url, client)
}
pub fn new_with_client<U: AsRef<str>>(base_url: U, client: Client) -> Result<Self, Error> {
let mut base = Url::parse(base_url.as_ref()).map_err(ConfigError::Url)?;
if !matches!(base.scheme(), "http" | "https")
|| base.host_str().is_none()
|| !base.username().is_empty()
|| base.password().is_some()
|| base.query().is_some()
|| base.fragment().is_some()
{
return Err(ConfigError::InvalidBaseUrl.into());
}
let path = format!("{}/", base.path().trim_end_matches('/'));
base.set_path(&path);
Ok(Self {
configuration: Arc::new(Configuration {
base,
schema: None,
headers: HeaderMap::new(),
client,
}),
})
}
pub fn auth<T: AsRef<str>>(self, token: T) -> Result<Self, Error> {
self.insert_header("authorization", format!("Bearer {}", token.as_ref()))
}
pub fn insert_header<N: AsRef<str>, V: AsRef<str>>(
mut self,
name: N,
value: V,
) -> Result<Self, Error> {
let name =
HeaderName::from_bytes(name.as_ref().as_bytes()).map_err(ConfigError::HeaderName)?;
let mut value = HeaderValue::from_str(value.as_ref()).map_err(ConfigError::HeaderValue)?;
if matches!(name.as_str(), "authorization" | "apikey") {
value.set_sensitive(true);
}
Arc::make_mut(&mut self.configuration)
.headers
.insert(name, value);
Ok(self)
}
#[must_use]
pub fn schema<S: AsRef<str>>(mut self, schema: S) -> Self {
Arc::make_mut(&mut self.configuration).schema = Some(schema.as_ref().to_owned());
self
}
pub fn from<T: AsRef<str>>(&self, literal_table: T) -> Builder {
Builder::new(
Arc::clone(&self.configuration),
literal_table.as_ref(),
false,
)
}
pub fn rpc<F: AsRef<str>, B: Into<String>>(&self, literal_function: F, body: B) -> Builder {
Builder::new(
Arc::clone(&self.configuration),
literal_function.as_ref(),
true,
)
.rpc(body)
}
pub fn rpc_json<T: Serialize + ?Sized, F: AsRef<str>>(
&self,
literal_function: F,
args: &T,
) -> Builder {
Builder::new(
Arc::clone(&self.configuration),
literal_function.as_ref(),
true,
)
.rpc_json(args)
}
}
fn same_origin_redirects() -> reqwest::redirect::Policy {
const MAX_REDIRECTS: usize = 10;
reqwest::redirect::Policy::custom(|attempt| {
let same_origin = attempt
.previous()
.last()
.is_some_and(|previous| previous.origin() == attempt.url().origin());
if !same_origin {
attempt.stop()
} else if attempt.previous().len() > MAX_REDIRECTS {
attempt.error("too many redirects")
} else {
attempt.follow()
}
})
}