Implements a first vertical slice of the Outline RPC API (auth, documents, collections, users) covering the structural patterns used across the whole API: single object, paginated list, tree, create/update, delete, search. Chosen as the basis for future CLI and GUI clients built on top of this crate. - Handwritten client (not codegen) against the vendored OpenAPI spec, since Outline's API is uniformly POST /api/<resource>.<action> with JSON bodies and inline/anonymous schemas that generators handle poorly - Async (reqwest + tokio) Client, cheaply cloneable, no &mut self methods - thiserror-based Error with ErrorKind classification and boxed API error context; forward-compatible models (unknown fields ignored, unknown string-enum values preserved via a catch-all variant) since the API is unversioned and self-hosted instances vary in age - Pagination via Page<T>/Paginator with next_page/collect_all/into_stream - wiremock-based test suite plus a spec-coverage test guarding against typos in RPC method names
131 lines
3.7 KiB
Rust
131 lines
3.7 KiB
Rust
use serde::Serialize;
|
|
|
|
use crate::client::{Client, MethodDef};
|
|
use crate::error::Result;
|
|
use crate::models::{Id, SortDirection, User, UserRole};
|
|
use crate::page::{DEFAULT_PAGE_SIZE, Page, PageParams, Paginated, Paginator};
|
|
|
|
const LIST: MethodDef = MethodDef {
|
|
name: "users.list",
|
|
idempotent: true,
|
|
};
|
|
const INFO: MethodDef = MethodDef {
|
|
name: "users.info",
|
|
idempotent: true,
|
|
};
|
|
|
|
/// Access to the `users.*` endpoints.
|
|
#[derive(Debug)]
|
|
pub struct UsersApi<'a> {
|
|
client: &'a Client,
|
|
}
|
|
|
|
impl<'a> UsersApi<'a> {
|
|
pub(crate) fn new(client: &'a Client) -> Self {
|
|
UsersApi { client }
|
|
}
|
|
|
|
/// Retrieves a single user by id.
|
|
pub async fn info(&self, id: impl Into<Id>) -> Result<User> {
|
|
#[derive(Serialize)]
|
|
struct Params {
|
|
id: Id,
|
|
}
|
|
self.client.rpc(INFO, &Params { id: id.into() }).await
|
|
}
|
|
|
|
/// Starts building a `users.list` request.
|
|
pub fn list(&self) -> ListUsers<'a> {
|
|
ListUsers::new(self.client)
|
|
}
|
|
}
|
|
|
|
/// Request parameters for `users.list`.
|
|
#[derive(Debug, Clone, Default, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ListUsersParams {
|
|
/// Pagination parameters.
|
|
#[serde(flatten)]
|
|
pub page: PageParams,
|
|
/// Filters results by name or email.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub query: Option<String>,
|
|
/// Restricts results to the given email addresses.
|
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
pub emails: Vec<String>,
|
|
/// Restricts results to users with the given role.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub role: Option<UserRole>,
|
|
}
|
|
|
|
impl Paginated for ListUsersParams {
|
|
fn page_params_mut(&mut self) -> &mut PageParams {
|
|
&mut self.page
|
|
}
|
|
}
|
|
|
|
/// A builder for `users.list`, obtained from [`UsersApi::list`].
|
|
#[derive(Debug)]
|
|
pub struct ListUsers<'a> {
|
|
client: &'a Client,
|
|
params: ListUsersParams,
|
|
}
|
|
|
|
impl<'a> ListUsers<'a> {
|
|
fn new(client: &'a Client) -> Self {
|
|
ListUsers {
|
|
client,
|
|
params: ListUsersParams::default(),
|
|
}
|
|
}
|
|
|
|
/// Filters results by name or email.
|
|
pub fn query(mut self, query: impl Into<String>) -> Self {
|
|
self.params.query = Some(query.into());
|
|
self
|
|
}
|
|
|
|
/// Restricts results to the given email addresses.
|
|
pub fn emails(mut self, emails: impl IntoIterator<Item = String>) -> Self {
|
|
self.params.emails = emails.into_iter().collect();
|
|
self
|
|
}
|
|
|
|
/// Restricts results to users with the given role.
|
|
pub fn role(mut self, role: UserRole) -> Self {
|
|
self.params.role = Some(role);
|
|
self
|
|
}
|
|
|
|
/// Sorts results by the given field (e.g. `"name"`) and direction.
|
|
pub fn sort(mut self, field: impl Into<String>, direction: SortDirection) -> Self {
|
|
self.params.page.sort = Some(field.into());
|
|
self.params.page.direction = Some(direction);
|
|
self
|
|
}
|
|
|
|
/// Sets the maximum number of results per page (server default: 25).
|
|
pub fn limit(mut self, limit: u32) -> Self {
|
|
self.params.page.limit = Some(limit);
|
|
self
|
|
}
|
|
|
|
/// Sets the number of results to skip.
|
|
pub fn offset(mut self, offset: u32) -> Self {
|
|
self.params.page.offset = Some(offset);
|
|
self
|
|
}
|
|
|
|
/// Fetches a single page of results.
|
|
pub async fn send(self) -> Result<Page<User>> {
|
|
self.client.rpc_list(LIST, &self.params).await
|
|
}
|
|
|
|
/// Returns a [`Paginator`] that fetches successive pages, using [`Self::limit`]
|
|
/// as the page size (server default: 25) if it was set.
|
|
pub fn paginate(self) -> Paginator<'a, ListUsersParams, User> {
|
|
let page_size = self.params.page.limit.unwrap_or(DEFAULT_PAGE_SIZE);
|
|
Paginator::new(self.client, LIST, self.params, page_size)
|
|
}
|
|
}
|