Skip to content
Merged
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
17 changes: 7 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,15 @@ jobs:
include:
- build: macos
os: macos-latest
rust: 1.81.0
rust: 1.86.0
- build: ubuntu
os: ubuntu-latest
rust: 1.81.0
rust: 1.86.0
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ matrix.rust }}
default: true
- run: cargo test -- --test-threads=1
env:
FRONTEGG_CLIENT_ID: 50864121-dfcc-4847-aab5-d56a993cd696
Expand All @@ -36,10 +35,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.81.0
default: true
toolchain: 1.86.0
components: rustfmt
- run: cargo fmt -- --check

Expand All @@ -48,10 +46,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.81.0
default: true
toolchain: 1.86.0
components: clippy
- uses: actions-rs/clippy-check@v1
with:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ Versioning].

## [Unreleased] <!-- #release:date -->

## [0.8.0] - 2025-05-28

* Update rust 1.86.0
* Add feature to request users in parts

## [0.7.0] - 2024-03-03

* Update reqwest to 0.12.12
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ An async Rust API client for the [Frontegg] user management service.
```
# Cargo.toml
[dependencies]
frontegg = "0.7.0"
frontegg = "0.8.0"
```

**[View documentation.](https://docs.rs/frontegg/0.7.0)**
**[View documentation.](https://docs.rs/frontegg/0.8.0)**

[Frontegg]: https://frontegg.com

83 changes: 83 additions & 0 deletions src/client/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,54 @@ impl UserListConfig {
}
}

/// Configuration for the [`Client::list_users_part`] operation.
#[derive(Debug, Clone)]
pub struct UserListPartConfig {
tenant_id: Option<Uuid>,
page_size: u64,
max_pages: u64,
starting_page: Option<u64>,
}

impl Default for UserListPartConfig {
fn default() -> UserListPartConfig {
UserListPartConfig {
tenant_id: None,
page_size: 50,
max_pages: 100,
starting_page: None,
}
}
}

impl UserListPartConfig {
/// Sets the tenant ID to filter users to.
///
/// If this method is not called, users for all tenants are returned.
pub fn tenant_id(mut self, tenant_id: Uuid) -> Self {
self.tenant_id = Some(tenant_id);
self
}

/// Sets the page size.
pub fn page_size(mut self, page_size: u64) -> Self {
self.page_size = page_size;
self
}

/// Sets the starting page
pub fn starting_page(mut self, starting_page: u64) -> Self {
self.starting_page = Some(starting_page);
self
}

/// Sets the max pages returned
pub fn max_pages(mut self, max_pages: u64) -> Self {
self.max_pages = max_pages;
self
}
}

/// The subset of [`User`] used in create requests.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -228,6 +276,41 @@ impl Client {
}
}

/// List a portion of users, either for all tenants or for a single tenant.
///
/// The underlying API call is paginated. The returned stream will fetch
/// additional pages as it is consumed starting at the starting_page and ending
/// once max_pages has been reached.
pub fn list_users_part(
&self,
config: UserListPartConfig,
) -> impl Stream<Item = Result<User, Error>> + '_ {
try_stream! {
let mut page = config.starting_page.unwrap_or(0);
let hault_page = config.max_pages + page;
loop {
let mut req = self.build_request(Method::GET, USER_PATH);
if let Some(tenant_id) = config.tenant_id {
req = req.tenant(tenant_id);
}
let req = req.query(&[
("_limit", &*config.page_size.to_string()),
("_offset", &*page.to_string())
]);
let res: Paginated<User> = self.send_request(req).await?;
for user in res.items {
yield user
}
page += 1;
if page >= res.metadata.total_pages {
break;
} else if page >= hault_page {
Err(Error::PaginationHault(page))?
}
}
}
}

/// Creates a new user.
///
/// Only partial information about the created user is returned. To fetch
Expand Down
4 changes: 4 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,17 @@ pub enum Error {
Transport(reqwest_middleware::Error),
/// An error returned by the API.
Api(ApiError),
/// A "error" a paginated response stream.
/// Holds the next page the stream can be resumed with.
PaginationHault(u64),
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::Transport(e) => write!(f, "frontegg error: transport: {e}"),
Error::Api(e) => write!(f, "frontegg error: api: {e}"),
Error::PaginationHault(e) => write!(f, "frontegg pagination haulted: next_page: {e}"),
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ mod util;
pub use client::roles::{Permission, Role};
pub use client::tenants::{Tenant, TenantRequest};
pub use client::users::{
CreatedUser, User, UserListConfig, UserRequest, WebhookTenantBinding, WebhookUser,
CreatedUser, User, UserListConfig, UserListPartConfig, UserRequest, WebhookTenantBinding,
WebhookUser,
};
pub use client::Client;
pub use config::{ClientBuilder, ClientConfig};
Expand Down
2 changes: 1 addition & 1 deletion src/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pub mod nested_json {
#[derive(Default)]
struct NestedJson;

impl<'de> Visitor<'de> for NestedJson {
impl Visitor<'_> for NestedJson {
type Value = serde_json::Value;

fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
Expand Down
65 changes: 60 additions & 5 deletions tests/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ use tracing::info;
use uuid::Uuid;
use wiremock::{matchers, Mock, MockServer, ResponseTemplate};

use frontegg::{ApiError, Client, ClientConfig, Error, TenantRequest, UserListConfig, UserRequest};
use frontegg::{
ApiError, Client, ClientConfig, Error, TenantRequest, UserListConfig, UserListPartConfig,
UserRequest,
};

pub static CLIENT_ID: Lazy<String> =
Lazy::new(|| env::var("FRONTEGG_CLIENT_ID").expect("missing FRONTEGG_CLIENT_ID"));
Expand All @@ -48,10 +51,16 @@ pub static SECRET_KEY: Lazy<String> =
const TENANT_NAME_PREFIX: &str = "test tenant";

fn new_client() -> Client {
Client::new(ClientConfig {
client_id: CLIENT_ID.clone(),
secret_key: SECRET_KEY.clone(),
})
Client::builder()
.with_retry_policy(
ExponentialBackoff::builder()
.retry_bounds(Duration::from_millis(500), Duration::from_secs(20))
.build_with_max_retries(20),
)
.build(ClientConfig {
client_id: CLIENT_ID.clone(),
secret_key: SECRET_KEY.clone(),
})
}

async fn delete_existing_tenants(client: &Client) {
Expand Down Expand Up @@ -281,6 +290,52 @@ async fn test_tenants_and_users() {
assert!(expected.difference(&actual).collect::<Vec<_>>().is_empty());
}

// Ensure that listing users parts works for a variety of sizes.
let pages: Result<Vec<_>, Error> = client
.list_users_part(
UserListPartConfig::default()
.page_size(1)
.max_pages(1)
.starting_page(0),
)
.map_ok(|u| u.id)
.try_collect()
.await;
if let frontegg::Error::PaginationHault(v) = pages.as_ref().unwrap_err() {
assert_eq!(v, &1);
} else {
panic!("{:?} should be PaginationHault Error", pages);
};
// Page should go up
let pages: Result<Vec<_>, Error> = client
.list_users_part(
UserListPartConfig::default()
.page_size(1)
.max_pages(1)
.starting_page(1),
)
.map_ok(|u| u.id)
.try_collect()
.await;
if let frontegg::Error::PaginationHault(v) = pages.as_ref().unwrap_err() {
assert_eq!(v, &2);
} else {
panic!("{:?} should be PaginationHault Error", pages);
};

// Should act like normal stream if max not hit
let pages: Result<Vec<_>, Error> = client
.list_users_part(
UserListPartConfig::default()
.page_size(100)
.max_pages(100)
.starting_page(0),
)
.map_ok(|u| u.id)
.try_collect()
.await;
assert!(pages.is_ok());

// Ensure that the user list can be filtered to a single tenant.
{
let expected: HashSet<_> = users.iter().take(3).map(|u| u.id).collect();
Expand Down