Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ edition = "2024"

[features]
default = ["rustls-tls"]
native-tls = ["reqwest/native-tls"]
rustls-tls = ["reqwest/rustls-tls"]
reqwest=["dep:reqwest"]
native-tls = ["reqwest/native-tls","reqwest"]
rustls-tls = ["reqwest/rustls-tls","reqwest"]

[dependencies]
async-trait = "0.1.88"
chrono = { version = "0.4.40", features = ["serde"] }
derive_more = { version = "2.0.1", features = ["deref", "display", "from"] }
erased-serde = "0.4.6"
http = "1.3.1"
jsonwebtoken = "9.3.1"
querystring = "1.1.0"
reqwest = { version = "0.12.0", features = ["json"] }
reqwest = { version = "0.12.0", features = ["json"], optional = true }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
thiserror = "2.0.0"
Expand Down
2 changes: 1 addition & 1 deletion src/admin_portal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::WorkOs;
///
/// [WorkOS Docs: Admin Portal Guide](https://workos.com/docs/admin-portal/guide)
pub struct AdminPortal<'a> {
workos: &'a WorkOs,
workos: &'a WorkOs<'a>,
}

impl<'a> AdminPortal<'a> {
Expand Down
2 changes: 1 addition & 1 deletion src/admin_portal/operations/generate_portal_link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ impl GeneratePortalLink for AdminPortal<'_> {
.send()
.await?
.handle_unauthorized_or_generic_error()?
.json::<GeneratePortalLinkResponse>()
.json::<GeneratePortalLinkResponse, _>()
.await?;

Ok(generate_link_response)
Expand Down
2 changes: 2 additions & 0 deletions src/core.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
mod error;
mod response;
///Traits for requests and other core infrastructure
pub mod traits;
mod types;

pub use error::*;
Expand Down
6 changes: 5 additions & 1 deletion src/core/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ pub enum WorkOsError<E> {

/// An unhandled error occurred with the API request.
#[error("request error")]
RequestError(#[from] reqwest::Error),
RequestError(#[from] crate::traits::RequestError),

/// An error occurred when deserializing JSON
#[error("json error")]
Json(#[from] serde_json::Error),
}

/// A WorkOS SDK result.
Expand Down
33 changes: 23 additions & 10 deletions src/core/response.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,53 @@
use reqwest::{Response, StatusCode};
use http::StatusCode;
use serde::de::DeserializeOwned;

use crate::{WorkOsError, WorkOsResult};
use crate::{WorkOsError, WorkOsResult, traits::ClientResponse};

pub trait ResponseExt
pub trait ResponseExt<'a>
where
Self: Sized,
{
/// Handles an unauthorized error from the WorkOS API by converting it into a
/// [`WorkOsError::Unauthorized`] response.
fn handle_unauthorized_error<E>(self) -> WorkOsResult<Self, E>;
fn handle_unauthorized_error<E>(self) -> WorkOsResult<Box<dyn ClientResponse + 'a>, E>;

/// Handles a generic error from the WorkOS API by converting it into a
/// [`WorkOsError::RequestError`] response.
fn handle_generic_error<E>(self) -> WorkOsResult<Self, E>;
fn handle_generic_error<E>(self) -> WorkOsResult<Box<dyn ClientResponse + 'a>, E>;

/// Handles an unauthorized or generic error from the WorkOS API.
fn handle_unauthorized_or_generic_error<E>(self) -> WorkOsResult<Self, E>;
fn handle_unauthorized_or_generic_error<E>(
self,
) -> WorkOsResult<Box<dyn ClientResponse + 'a>, E>;

async fn json<T: DeserializeOwned, E>(self) -> WorkOsResult<T, E>;
}

impl ResponseExt for Response {
fn handle_unauthorized_error<E>(self) -> WorkOsResult<Self, E> {
impl<'a> ResponseExt<'a> for Box<dyn ClientResponse + 'a> {
fn handle_unauthorized_error<E>(self) -> WorkOsResult<Box<dyn ClientResponse + 'a>, E> {
if self.status() == StatusCode::UNAUTHORIZED {
Err(WorkOsError::Unauthorized)
} else {
Ok(self)
}
}

fn handle_generic_error<E>(self) -> WorkOsResult<Self, E> {
fn handle_generic_error<E>(self) -> WorkOsResult<Box<dyn ClientResponse + 'a>, E> {
match self.error_for_status() {
Ok(response) => Ok(response),
Err(err) => Err(WorkOsError::RequestError(err)),
}
}

fn handle_unauthorized_or_generic_error<E>(self) -> WorkOsResult<Self, E> {
fn handle_unauthorized_or_generic_error<E>(
self,
) -> WorkOsResult<Box<dyn ClientResponse + 'a>, E> {
self.handle_unauthorized_error()?.handle_generic_error()
}

async fn json<T: DeserializeOwned, E>(self) -> WorkOsResult<T, E> {
let t = self.text().await.map_err(WorkOsError::RequestError)?;

Ok(serde_json::from_str(&t)?)
}
}
205 changes: 205 additions & 0 deletions src/core/traits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
use std::fmt::Display;

use async_trait::async_trait;
use http::StatusCode;
use url::Url;
///An error with a ststus code
pub trait StatusError: std::error::Error {
///The status code, if available, of this error
fn status(&self) -> Option<StatusCode>;
}
///A HTTP error
#[derive(Debug)]
pub struct RequestError {
///The status code, if available, of this error
pub err: Box<dyn StatusError + Send + Sync>,
}

impl StatusError for RequestError {
fn status(&self) -> Option<StatusCode> {
self.err.status()
}
}

impl RequestError {
///The status code, if available, of this error
pub fn status(&self) -> Option<StatusCode> {
StatusError::status(self)
}
}

impl Display for RequestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.err)
}
}
impl std::error::Error for RequestError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&*self.err)
}
}

#[async_trait]
///An HTTP client
pub trait Client {
///Create a GET request
fn get(&self, url: Url) -> Box<dyn ClientRequest + '_>;
///Create a POST request
fn post(&self, url: Url) -> Box<dyn ClientRequest + '_>;
///Create a PUT request
fn put(&self, url: Url) -> Box<dyn ClientRequest + '_>;
///Create a DELETE request
fn delete(&self, url: Url) -> Box<dyn ClientRequest + '_>;
}

#[async_trait]
///A response to a HTTP request
pub trait ClientResponse: Send + Sync {
///That response's status code
fn status(&self) -> StatusCode;
///Attept to catch error-signaling status codes and return them as errors
fn error_for_status<'a>(self: Box<Self>) -> Result<Box<dyn ClientResponse + 'a>, RequestError>
where
Self: 'a;
///Attept to catch error-signaling status codes and return them as errors
fn error_for_status_ref(&self) -> Result<&(dyn ClientResponse + '_), RequestError>;
///The text of the response
async fn text(self: Box<Self>) -> Result<String, RequestError>;
}

#[async_trait]
///A HTTP request
pub trait ClientRequest {
///Its authenticaton
fn bearer_auth<'a>(self: Box<Self>, x: &(dyn Display + '_)) -> Box<dyn ClientRequest + 'a>
where
Self: 'a;
///Adds ad JSON body
fn json<'a>(
self: Box<Self>,
x: &(dyn erased_serde::Serialize + '_),
) -> Box<dyn ClientRequest + 'a>
where
Self: 'a;
///Adds a query string
fn query<'a>(
self: Box<Self>,
x: &(dyn erased_serde::Serialize + '_),
) -> Box<dyn ClientRequest + 'a>
where
Self: 'a;
///Adds a form body
fn form<'a>(
self: Box<Self>,
x: &(dyn erased_serde::Serialize + '_),
) -> Box<dyn ClientRequest + 'a>
where
Self: 'a;
///Sends the request
async fn send<'a>(self: Box<Self>) -> Result<Box<dyn ClientResponse + 'a>, RequestError>
where
Self: 'a;
}
#[cfg(feature = "reqwest")]
const _: () = {
impl StatusError for reqwest::Error {
fn status(&self) -> Option<StatusCode> {
self.status()
}
}

impl From<reqwest::Error> for RequestError {
fn from(value: reqwest::Error) -> Self {
Self {
err: Box::new(value),
}
}
}

#[async_trait]
impl Client for reqwest::Client {
fn get(&self, url: Url) -> Box<dyn ClientRequest + '_> {
Box::new(self.get(url))
}
fn post(&self, url: Url) -> Box<dyn ClientRequest + '_> {
Box::new(self.post(url))
}
fn put(&self, url: Url) -> Box<dyn ClientRequest + '_> {
Box::new(self.put(url))
}
fn delete(&self, url: Url) -> Box<dyn ClientRequest + '_> {
Box::new(self.delete(url))
}
}
#[async_trait]
impl ClientResponse for reqwest::Response {
fn status(&self) -> StatusCode {
self.status()
}
fn error_for_status<'a>(
self: Box<Self>,
) -> Result<Box<dyn ClientResponse + 'a>, RequestError>
where
Self: 'a,
{
match (*self).error_for_status() {
Err(e) => Err(e.into()),
Ok(a) => Ok(Box::new(a)),
}
}
fn error_for_status_ref(&self) -> Result<&(dyn ClientResponse + '_), RequestError> {
match self.error_for_status_ref() {
Err(e) => Err(e.into()),
Ok(v) => Ok(v),
}
}
async fn text(self: Box<Self>) -> Result<String, RequestError> {
(*self).text().await.map_err(Into::into)
}
}
#[async_trait]
impl ClientRequest for reqwest::RequestBuilder {
async fn send<'a>(self: Box<Self>) -> Result<Box<dyn ClientResponse + 'a>, RequestError>
where
Self: 'a,
{
match reqwest::RequestBuilder::send(*self).await {
Err(e) => Err(e.into()),
Ok(a) => Ok(Box::new(a)),
}
}
fn bearer_auth<'a>(self: Box<Self>, x: &(dyn Display + '_)) -> Box<dyn ClientRequest + 'a>
where
Self: 'a,
{
Box::new((*self).bearer_auth(x))
}
fn json<'a>(
self: Box<Self>,
x: &(dyn erased_serde::Serialize + '_),
) -> Box<dyn ClientRequest + 'a>
where
Self: 'a,
{
Box::new((*self).json(x))
}
fn query<'a>(
self: Box<Self>,
x: &(dyn erased_serde::Serialize + '_),
) -> Box<dyn ClientRequest + 'a>
where
Self: 'a,
{
Box::new((*self).query(x))
}
fn form<'a>(
self: Box<Self>,
x: &(dyn erased_serde::Serialize + '_),
) -> Box<dyn ClientRequest + 'a>
where
Self: 'a,
{
Box::new((*self).form(x))
}
}
};
2 changes: 1 addition & 1 deletion src/directory_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::WorkOs;
///
/// [WorkOS Docs: Directory Sync Guide](https://workos.com/docs/directory-sync/guide)
pub struct DirectorySync<'a> {
workos: &'a WorkOs,
workos: &'a WorkOs<'a>,
}

impl<'a> DirectorySync<'a> {
Expand Down
2 changes: 1 addition & 1 deletion src/directory_sync/operations/get_directory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ impl GetDirectory for DirectorySync<'_> {
.send()
.await?
.handle_unauthorized_or_generic_error()?
.json::<Directory>()
.json::<Directory, _>()
.await?;

Ok(directory)
Expand Down
2 changes: 1 addition & 1 deletion src/directory_sync/operations/get_directory_group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ impl GetDirectoryGroup for DirectorySync<'_> {
.send()
.await?
.handle_unauthorized_or_generic_error()?
.json::<DirectoryGroup>()
.json::<DirectoryGroup, _>()
.await?;

Ok(directory_group)
Expand Down
Loading