feat: La Suite clients — 7 DRF services (75 endpoints)
PeopleClient, DocsClient, MeetClient, DriveClient, MessagesClient, CalendarsClient, FindClient — all with DRFPage<T> pagination and Bearer token auth. Bump: sunbeam-sdk v0.11.0
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -3591,7 +3591,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sunbeam-sdk"
|
name = "sunbeam-sdk"
|
||||||
version = "0.9.0"
|
version = "0.10.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sunbeam-sdk"
|
name = "sunbeam-sdk"
|
||||||
version = "0.10.0"
|
version = "0.11.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "Sunbeam SDK — reusable library for cluster management"
|
description = "Sunbeam SDK — reusable library for cluster management"
|
||||||
repository = "https://src.sunbeam.pt/studio/cli"
|
repository = "https://src.sunbeam.pt/studio/cli"
|
||||||
|
|||||||
191
sunbeam-sdk/src/lasuite/calendars.rs
Normal file
191
sunbeam-sdk/src/lasuite/calendars.rs
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
//! Calendars service client — calendars, events, RSVP.
|
||||||
|
|
||||||
|
use crate::client::{AuthMethod, HttpTransport, ServiceClient};
|
||||||
|
use crate::error::Result;
|
||||||
|
use reqwest::Method;
|
||||||
|
use super::types::*;
|
||||||
|
|
||||||
|
/// Client for the La Suite Calendars API.
|
||||||
|
pub struct CalendarsClient {
|
||||||
|
pub(crate) transport: HttpTransport,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceClient for CalendarsClient {
|
||||||
|
fn service_name(&self) -> &'static str {
|
||||||
|
"calendars"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_url(&self) -> &str {
|
||||||
|
&self.transport.base_url
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_parts(base_url: String, auth: AuthMethod) -> Self {
|
||||||
|
Self {
|
||||||
|
transport: HttpTransport::new(&base_url, auth),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CalendarsClient {
|
||||||
|
/// Build a CalendarsClient from domain (e.g. `https://calendar.{domain}/api/v1.0`).
|
||||||
|
pub fn connect(domain: &str) -> Self {
|
||||||
|
let base_url = format!("https://calendar.{domain}/api/v1.0");
|
||||||
|
Self::from_parts(base_url, AuthMethod::Bearer(String::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the bearer token for authentication.
|
||||||
|
pub fn with_token(mut self, token: &str) -> Self {
|
||||||
|
self.transport.set_auth(AuthMethod::Bearer(token.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Calendars ----------------------------------------------------------
|
||||||
|
|
||||||
|
/// List calendars.
|
||||||
|
pub async fn list_calendars(&self) -> Result<DRFPage<Calendar>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
"calendars/",
|
||||||
|
Option::<&()>::None,
|
||||||
|
"calendars list calendars",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a single calendar by ID.
|
||||||
|
pub async fn get_calendar(&self, id: &str) -> Result<Calendar> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("calendars/{id}/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"calendars get calendar",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new calendar.
|
||||||
|
pub async fn create_calendar(&self, body: &serde_json::Value) -> Result<Calendar> {
|
||||||
|
self.transport
|
||||||
|
.json(Method::POST, "calendars/", Some(body), "calendars create calendar")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Events -------------------------------------------------------------
|
||||||
|
|
||||||
|
/// List events in a calendar.
|
||||||
|
pub async fn list_events(&self, calendar_id: &str) -> Result<DRFPage<CalEvent>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("calendars/{calendar_id}/events/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"calendars list events",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a single event.
|
||||||
|
pub async fn get_event(
|
||||||
|
&self,
|
||||||
|
calendar_id: &str,
|
||||||
|
event_id: &str,
|
||||||
|
) -> Result<CalEvent> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("calendars/{calendar_id}/events/{event_id}/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"calendars get event",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new event in a calendar.
|
||||||
|
pub async fn create_event(
|
||||||
|
&self,
|
||||||
|
calendar_id: &str,
|
||||||
|
body: &serde_json::Value,
|
||||||
|
) -> Result<CalEvent> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::POST,
|
||||||
|
&format!("calendars/{calendar_id}/events/"),
|
||||||
|
Some(body),
|
||||||
|
"calendars create event",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update an event (partial).
|
||||||
|
pub async fn update_event(
|
||||||
|
&self,
|
||||||
|
calendar_id: &str,
|
||||||
|
event_id: &str,
|
||||||
|
body: &serde_json::Value,
|
||||||
|
) -> Result<CalEvent> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PATCH,
|
||||||
|
&format!("calendars/{calendar_id}/events/{event_id}/"),
|
||||||
|
Some(body),
|
||||||
|
"calendars update event",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete an event.
|
||||||
|
pub async fn delete_event(
|
||||||
|
&self,
|
||||||
|
calendar_id: &str,
|
||||||
|
event_id: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("calendars/{calendar_id}/events/{event_id}/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"calendars delete event",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RSVP to an event.
|
||||||
|
pub async fn rsvp(
|
||||||
|
&self,
|
||||||
|
calendar_id: &str,
|
||||||
|
event_id: &str,
|
||||||
|
body: &serde_json::Value,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::POST,
|
||||||
|
&format!("calendars/{calendar_id}/events/{event_id}/rsvp/"),
|
||||||
|
Some(body),
|
||||||
|
"calendars rsvp",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connect_url() {
|
||||||
|
let c = CalendarsClient::connect("sunbeam.pt");
|
||||||
|
assert_eq!(c.base_url(), "https://calendar.sunbeam.pt/api/v1.0");
|
||||||
|
assert_eq!(c.service_name(), "calendars");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_parts() {
|
||||||
|
let c = CalendarsClient::from_parts(
|
||||||
|
"http://localhost:8000/api/v1.0".into(),
|
||||||
|
AuthMethod::Bearer("tok".into()),
|
||||||
|
);
|
||||||
|
assert_eq!(c.base_url(), "http://localhost:8000/api/v1.0");
|
||||||
|
}
|
||||||
|
}
|
||||||
170
sunbeam-sdk/src/lasuite/docs.rs
Normal file
170
sunbeam-sdk/src/lasuite/docs.rs
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
//! Docs service client — documents, templates, versions, invitations.
|
||||||
|
|
||||||
|
use crate::client::{AuthMethod, HttpTransport, ServiceClient};
|
||||||
|
use crate::error::Result;
|
||||||
|
use reqwest::Method;
|
||||||
|
use super::types::*;
|
||||||
|
|
||||||
|
/// Client for the La Suite Docs API.
|
||||||
|
pub struct DocsClient {
|
||||||
|
pub(crate) transport: HttpTransport,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceClient for DocsClient {
|
||||||
|
fn service_name(&self) -> &'static str {
|
||||||
|
"docs"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_url(&self) -> &str {
|
||||||
|
&self.transport.base_url
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_parts(base_url: String, auth: AuthMethod) -> Self {
|
||||||
|
Self {
|
||||||
|
transport: HttpTransport::new(&base_url, auth),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DocsClient {
|
||||||
|
/// Build a DocsClient from domain (e.g. `https://docs.{domain}/api/v1.0`).
|
||||||
|
pub fn connect(domain: &str) -> Self {
|
||||||
|
let base_url = format!("https://docs.{domain}/api/v1.0");
|
||||||
|
Self::from_parts(base_url, AuthMethod::Bearer(String::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the bearer token for authentication.
|
||||||
|
pub fn with_token(mut self, token: &str) -> Self {
|
||||||
|
self.transport.set_auth(AuthMethod::Bearer(token.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Documents ----------------------------------------------------------
|
||||||
|
|
||||||
|
/// List documents with optional pagination.
|
||||||
|
pub async fn list_documents(&self, page: Option<u32>) -> Result<DRFPage<Document>> {
|
||||||
|
let path = match page {
|
||||||
|
Some(p) => format!("documents/?page={p}"),
|
||||||
|
None => "documents/".to_string(),
|
||||||
|
};
|
||||||
|
self.transport
|
||||||
|
.json(Method::GET, &path, Option::<&()>::None, "docs list documents")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a single document by ID.
|
||||||
|
pub async fn get_document(&self, id: &str) -> Result<Document> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("documents/{id}/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"docs get document",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new document.
|
||||||
|
pub async fn create_document(&self, body: &serde_json::Value) -> Result<Document> {
|
||||||
|
self.transport
|
||||||
|
.json(Method::POST, "documents/", Some(body), "docs create document")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update a document (partial).
|
||||||
|
pub async fn update_document(&self, id: &str, body: &serde_json::Value) -> Result<Document> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PATCH,
|
||||||
|
&format!("documents/{id}/"),
|
||||||
|
Some(body),
|
||||||
|
"docs update document",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a document.
|
||||||
|
pub async fn delete_document(&self, id: &str) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("documents/{id}/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"docs delete document",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Templates ----------------------------------------------------------
|
||||||
|
|
||||||
|
/// List templates with optional pagination.
|
||||||
|
pub async fn list_templates(&self, page: Option<u32>) -> Result<DRFPage<Template>> {
|
||||||
|
let path = match page {
|
||||||
|
Some(p) => format!("templates/?page={p}"),
|
||||||
|
None => "templates/".to_string(),
|
||||||
|
};
|
||||||
|
self.transport
|
||||||
|
.json(Method::GET, &path, Option::<&()>::None, "docs list templates")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new template.
|
||||||
|
pub async fn create_template(&self, body: &serde_json::Value) -> Result<Template> {
|
||||||
|
self.transport
|
||||||
|
.json(Method::POST, "templates/", Some(body), "docs create template")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Versions -----------------------------------------------------------
|
||||||
|
|
||||||
|
/// List versions of a document.
|
||||||
|
pub async fn list_versions(&self, doc_id: &str) -> Result<DRFPage<DocVersion>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("documents/{doc_id}/versions/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"docs list versions",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Invitations --------------------------------------------------------
|
||||||
|
|
||||||
|
/// Invite a user to collaborate on a document.
|
||||||
|
pub async fn invite_user(
|
||||||
|
&self,
|
||||||
|
doc_id: &str,
|
||||||
|
body: &serde_json::Value,
|
||||||
|
) -> Result<Invitation> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::POST,
|
||||||
|
&format!("documents/{doc_id}/invitations/"),
|
||||||
|
Some(body),
|
||||||
|
"docs invite user",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connect_url() {
|
||||||
|
let c = DocsClient::connect("sunbeam.pt");
|
||||||
|
assert_eq!(c.base_url(), "https://docs.sunbeam.pt/api/v1.0");
|
||||||
|
assert_eq!(c.service_name(), "docs");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_parts() {
|
||||||
|
let c = DocsClient::from_parts(
|
||||||
|
"http://localhost:8000/api/v1.0".into(),
|
||||||
|
AuthMethod::Bearer("tok".into()),
|
||||||
|
);
|
||||||
|
assert_eq!(c.base_url(), "http://localhost:8000/api/v1.0");
|
||||||
|
}
|
||||||
|
}
|
||||||
154
sunbeam-sdk/src/lasuite/drive.rs
Normal file
154
sunbeam-sdk/src/lasuite/drive.rs
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
//! Drive service client — files, folders, shares, permissions.
|
||||||
|
|
||||||
|
use crate::client::{AuthMethod, HttpTransport, ServiceClient};
|
||||||
|
use crate::error::Result;
|
||||||
|
use reqwest::Method;
|
||||||
|
use super::types::*;
|
||||||
|
|
||||||
|
/// Client for the La Suite Drive API.
|
||||||
|
pub struct DriveClient {
|
||||||
|
pub(crate) transport: HttpTransport,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceClient for DriveClient {
|
||||||
|
fn service_name(&self) -> &'static str {
|
||||||
|
"drive"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_url(&self) -> &str {
|
||||||
|
&self.transport.base_url
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_parts(base_url: String, auth: AuthMethod) -> Self {
|
||||||
|
Self {
|
||||||
|
transport: HttpTransport::new(&base_url, auth),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DriveClient {
|
||||||
|
/// Build a DriveClient from domain (e.g. `https://drive.{domain}/api/v1.0`).
|
||||||
|
pub fn connect(domain: &str) -> Self {
|
||||||
|
let base_url = format!("https://drive.{domain}/api/v1.0");
|
||||||
|
Self::from_parts(base_url, AuthMethod::Bearer(String::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the bearer token for authentication.
|
||||||
|
pub fn with_token(mut self, token: &str) -> Self {
|
||||||
|
self.transport.set_auth(AuthMethod::Bearer(token.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Files --------------------------------------------------------------
|
||||||
|
|
||||||
|
/// List files with optional pagination.
|
||||||
|
pub async fn list_files(&self, page: Option<u32>) -> Result<DRFPage<DriveFile>> {
|
||||||
|
let path = match page {
|
||||||
|
Some(p) => format!("files/?page={p}"),
|
||||||
|
None => "files/".to_string(),
|
||||||
|
};
|
||||||
|
self.transport
|
||||||
|
.json(Method::GET, &path, Option::<&()>::None, "drive list files")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a single file by ID.
|
||||||
|
pub async fn get_file(&self, id: &str) -> Result<DriveFile> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("files/{id}/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"drive get file",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload a new file.
|
||||||
|
pub async fn upload_file(&self, body: &serde_json::Value) -> Result<DriveFile> {
|
||||||
|
self.transport
|
||||||
|
.json(Method::POST, "files/", Some(body), "drive upload file")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a file.
|
||||||
|
pub async fn delete_file(&self, id: &str) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("files/{id}/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"drive delete file",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Folders ------------------------------------------------------------
|
||||||
|
|
||||||
|
/// List folders with optional pagination.
|
||||||
|
pub async fn list_folders(&self, page: Option<u32>) -> Result<DRFPage<DriveFolder>> {
|
||||||
|
let path = match page {
|
||||||
|
Some(p) => format!("folders/?page={p}"),
|
||||||
|
None => "folders/".to_string(),
|
||||||
|
};
|
||||||
|
self.transport
|
||||||
|
.json(Method::GET, &path, Option::<&()>::None, "drive list folders")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new folder.
|
||||||
|
pub async fn create_folder(&self, body: &serde_json::Value) -> Result<DriveFolder> {
|
||||||
|
self.transport
|
||||||
|
.json(Method::POST, "folders/", Some(body), "drive create folder")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Shares -------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Share a file with a user.
|
||||||
|
pub async fn share_file(&self, id: &str, body: &serde_json::Value) -> Result<FileShare> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::POST,
|
||||||
|
&format!("files/{id}/shares/"),
|
||||||
|
Some(body),
|
||||||
|
"drive share file",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Permissions --------------------------------------------------------
|
||||||
|
|
||||||
|
/// Get permissions for a file.
|
||||||
|
pub async fn get_permissions(&self, id: &str) -> Result<DRFPage<FilePermission>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("files/{id}/permissions/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"drive get permissions",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connect_url() {
|
||||||
|
let c = DriveClient::connect("sunbeam.pt");
|
||||||
|
assert_eq!(c.base_url(), "https://drive.sunbeam.pt/api/v1.0");
|
||||||
|
assert_eq!(c.service_name(), "drive");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_parts() {
|
||||||
|
let c = DriveClient::from_parts(
|
||||||
|
"http://localhost:8000/api/v1.0".into(),
|
||||||
|
AuthMethod::Bearer("tok".into()),
|
||||||
|
);
|
||||||
|
assert_eq!(c.base_url(), "http://localhost:8000/api/v1.0");
|
||||||
|
}
|
||||||
|
}
|
||||||
77
sunbeam-sdk/src/lasuite/find.rs
Normal file
77
sunbeam-sdk/src/lasuite/find.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
//! Find (search) service client.
|
||||||
|
|
||||||
|
use crate::client::{AuthMethod, HttpTransport, ServiceClient};
|
||||||
|
use crate::error::Result;
|
||||||
|
use reqwest::Method;
|
||||||
|
use super::types::*;
|
||||||
|
|
||||||
|
/// Client for the La Suite Find (search) API.
|
||||||
|
pub struct FindClient {
|
||||||
|
pub(crate) transport: HttpTransport,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceClient for FindClient {
|
||||||
|
fn service_name(&self) -> &'static str {
|
||||||
|
"find"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_url(&self) -> &str {
|
||||||
|
&self.transport.base_url
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_parts(base_url: String, auth: AuthMethod) -> Self {
|
||||||
|
Self {
|
||||||
|
transport: HttpTransport::new(&base_url, auth),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FindClient {
|
||||||
|
/// Build a FindClient from domain (e.g. `https://find.{domain}/api/v1.0`).
|
||||||
|
pub fn connect(domain: &str) -> Self {
|
||||||
|
let base_url = format!("https://find.{domain}/api/v1.0");
|
||||||
|
Self::from_parts(base_url, AuthMethod::Bearer(String::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the bearer token for authentication.
|
||||||
|
pub fn with_token(mut self, token: &str) -> Self {
|
||||||
|
self.transport.set_auth(AuthMethod::Bearer(token.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search across La Suite services.
|
||||||
|
pub async fn search(
|
||||||
|
&self,
|
||||||
|
query: &str,
|
||||||
|
page: Option<u32>,
|
||||||
|
) -> Result<DRFPage<SearchResult>> {
|
||||||
|
let path = match page {
|
||||||
|
Some(p) => format!("search/?q={query}&page={p}"),
|
||||||
|
None => format!("search/?q={query}"),
|
||||||
|
};
|
||||||
|
self.transport
|
||||||
|
.json(Method::GET, &path, Option::<&()>::None, "find search")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connect_url() {
|
||||||
|
let c = FindClient::connect("sunbeam.pt");
|
||||||
|
assert_eq!(c.base_url(), "https://find.sunbeam.pt/api/v1.0");
|
||||||
|
assert_eq!(c.service_name(), "find");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_parts() {
|
||||||
|
let c = FindClient::from_parts(
|
||||||
|
"http://localhost:8000/api/v1.0".into(),
|
||||||
|
AuthMethod::Bearer("tok".into()),
|
||||||
|
);
|
||||||
|
assert_eq!(c.base_url(), "http://localhost:8000/api/v1.0");
|
||||||
|
}
|
||||||
|
}
|
||||||
132
sunbeam-sdk/src/lasuite/meet.rs
Normal file
132
sunbeam-sdk/src/lasuite/meet.rs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
//! Meet service client — rooms and recordings.
|
||||||
|
|
||||||
|
use crate::client::{AuthMethod, HttpTransport, ServiceClient};
|
||||||
|
use crate::error::Result;
|
||||||
|
use reqwest::Method;
|
||||||
|
use super::types::*;
|
||||||
|
|
||||||
|
/// Client for the La Suite Meet API.
|
||||||
|
pub struct MeetClient {
|
||||||
|
pub(crate) transport: HttpTransport,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceClient for MeetClient {
|
||||||
|
fn service_name(&self) -> &'static str {
|
||||||
|
"meet"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_url(&self) -> &str {
|
||||||
|
&self.transport.base_url
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_parts(base_url: String, auth: AuthMethod) -> Self {
|
||||||
|
Self {
|
||||||
|
transport: HttpTransport::new(&base_url, auth),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MeetClient {
|
||||||
|
/// Build a MeetClient from domain (e.g. `https://meet.{domain}/api/v1.0`).
|
||||||
|
pub fn connect(domain: &str) -> Self {
|
||||||
|
let base_url = format!("https://meet.{domain}/api/v1.0");
|
||||||
|
Self::from_parts(base_url, AuthMethod::Bearer(String::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the bearer token for authentication.
|
||||||
|
pub fn with_token(mut self, token: &str) -> Self {
|
||||||
|
self.transport.set_auth(AuthMethod::Bearer(token.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Rooms --------------------------------------------------------------
|
||||||
|
|
||||||
|
/// List rooms with optional pagination.
|
||||||
|
pub async fn list_rooms(&self, page: Option<u32>) -> Result<DRFPage<MeetRoom>> {
|
||||||
|
let path = match page {
|
||||||
|
Some(p) => format!("rooms/?page={p}"),
|
||||||
|
None => "rooms/".to_string(),
|
||||||
|
};
|
||||||
|
self.transport
|
||||||
|
.json(Method::GET, &path, Option::<&()>::None, "meet list rooms")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new room.
|
||||||
|
pub async fn create_room(&self, body: &serde_json::Value) -> Result<MeetRoom> {
|
||||||
|
self.transport
|
||||||
|
.json(Method::POST, "rooms/", Some(body), "meet create room")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a single room by ID.
|
||||||
|
pub async fn get_room(&self, id: &str) -> Result<MeetRoom> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("rooms/{id}/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"meet get room",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update a room (partial).
|
||||||
|
pub async fn update_room(&self, id: &str, body: &serde_json::Value) -> Result<MeetRoom> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PATCH,
|
||||||
|
&format!("rooms/{id}/"),
|
||||||
|
Some(body),
|
||||||
|
"meet update room",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a room.
|
||||||
|
pub async fn delete_room(&self, id: &str) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("rooms/{id}/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"meet delete room",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Recordings ---------------------------------------------------------
|
||||||
|
|
||||||
|
/// List recordings for a room.
|
||||||
|
pub async fn list_recordings(&self, room_id: &str) -> Result<DRFPage<Recording>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("rooms/{room_id}/recordings/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"meet list recordings",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connect_url() {
|
||||||
|
let c = MeetClient::connect("sunbeam.pt");
|
||||||
|
assert_eq!(c.base_url(), "https://meet.sunbeam.pt/api/v1.0");
|
||||||
|
assert_eq!(c.service_name(), "meet");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_parts() {
|
||||||
|
let c = MeetClient::from_parts(
|
||||||
|
"http://localhost:8000/api/v1.0".into(),
|
||||||
|
AuthMethod::Bearer("tok".into()),
|
||||||
|
);
|
||||||
|
assert_eq!(c.base_url(), "http://localhost:8000/api/v1.0");
|
||||||
|
}
|
||||||
|
}
|
||||||
166
sunbeam-sdk/src/lasuite/messages.rs
Normal file
166
sunbeam-sdk/src/lasuite/messages.rs
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
//! Messages (mail) service client — mailboxes, messages, folders, contacts.
|
||||||
|
|
||||||
|
use crate::client::{AuthMethod, HttpTransport, ServiceClient};
|
||||||
|
use crate::error::Result;
|
||||||
|
use reqwest::Method;
|
||||||
|
use super::types::*;
|
||||||
|
|
||||||
|
/// Client for the La Suite Messages (mail) API.
|
||||||
|
pub struct MessagesClient {
|
||||||
|
pub(crate) transport: HttpTransport,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceClient for MessagesClient {
|
||||||
|
fn service_name(&self) -> &'static str {
|
||||||
|
"messages"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_url(&self) -> &str {
|
||||||
|
&self.transport.base_url
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_parts(base_url: String, auth: AuthMethod) -> Self {
|
||||||
|
Self {
|
||||||
|
transport: HttpTransport::new(&base_url, auth),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessagesClient {
|
||||||
|
/// Build a MessagesClient from domain (e.g. `https://mail.{domain}/api/v1.0`).
|
||||||
|
pub fn connect(domain: &str) -> Self {
|
||||||
|
let base_url = format!("https://mail.{domain}/api/v1.0");
|
||||||
|
Self::from_parts(base_url, AuthMethod::Bearer(String::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the bearer token for authentication.
|
||||||
|
pub fn with_token(mut self, token: &str) -> Self {
|
||||||
|
self.transport.set_auth(AuthMethod::Bearer(token.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Mailboxes ----------------------------------------------------------
|
||||||
|
|
||||||
|
/// List mailboxes.
|
||||||
|
pub async fn list_mailboxes(&self) -> Result<DRFPage<Mailbox>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
"mailboxes/",
|
||||||
|
Option::<&()>::None,
|
||||||
|
"messages list mailboxes",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a single mailbox by ID.
|
||||||
|
pub async fn get_mailbox(&self, id: &str) -> Result<Mailbox> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("mailboxes/{id}/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"messages get mailbox",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Messages -----------------------------------------------------------
|
||||||
|
|
||||||
|
/// List messages in a mailbox folder.
|
||||||
|
pub async fn list_messages(
|
||||||
|
&self,
|
||||||
|
mailbox_id: &str,
|
||||||
|
folder: &str,
|
||||||
|
) -> Result<DRFPage<EmailMessage>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("mailboxes/{mailbox_id}/messages/?folder={folder}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"messages list messages",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a single message.
|
||||||
|
pub async fn get_message(
|
||||||
|
&self,
|
||||||
|
mailbox_id: &str,
|
||||||
|
message_id: &str,
|
||||||
|
) -> Result<EmailMessage> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("mailboxes/{mailbox_id}/messages/{message_id}/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"messages get message",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a message from a mailbox.
|
||||||
|
pub async fn send_message(
|
||||||
|
&self,
|
||||||
|
mailbox_id: &str,
|
||||||
|
body: &serde_json::Value,
|
||||||
|
) -> Result<EmailMessage> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::POST,
|
||||||
|
&format!("mailboxes/{mailbox_id}/messages/"),
|
||||||
|
Some(body),
|
||||||
|
"messages send message",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Folders ------------------------------------------------------------
|
||||||
|
|
||||||
|
/// List folders in a mailbox.
|
||||||
|
pub async fn list_folders(&self, mailbox_id: &str) -> Result<DRFPage<MailFolder>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("mailboxes/{mailbox_id}/folders/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"messages list folders",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Contacts -----------------------------------------------------------
|
||||||
|
|
||||||
|
/// List contacts in a mailbox.
|
||||||
|
pub async fn list_contacts(&self, mailbox_id: &str) -> Result<DRFPage<MailContact>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("mailboxes/{mailbox_id}/contacts/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"messages list contacts",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connect_url() {
|
||||||
|
let c = MessagesClient::connect("sunbeam.pt");
|
||||||
|
assert_eq!(c.base_url(), "https://mail.sunbeam.pt/api/v1.0");
|
||||||
|
assert_eq!(c.service_name(), "messages");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_parts() {
|
||||||
|
let c = MessagesClient::from_parts(
|
||||||
|
"http://localhost:8000/api/v1.0".into(),
|
||||||
|
AuthMethod::Bearer("tok".into()),
|
||||||
|
);
|
||||||
|
assert_eq!(c.base_url(), "http://localhost:8000/api/v1.0");
|
||||||
|
}
|
||||||
|
}
|
||||||
18
sunbeam-sdk/src/lasuite/mod.rs
Normal file
18
sunbeam-sdk/src/lasuite/mod.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
//! La Suite service clients (People, Docs, Meet, Drive, Messages, Calendars, Find).
|
||||||
|
|
||||||
|
pub mod people;
|
||||||
|
pub mod docs;
|
||||||
|
pub mod meet;
|
||||||
|
pub mod drive;
|
||||||
|
pub mod messages;
|
||||||
|
pub mod calendars;
|
||||||
|
pub mod find;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
pub use people::PeopleClient;
|
||||||
|
pub use docs::DocsClient;
|
||||||
|
pub use meet::MeetClient;
|
||||||
|
pub use drive::DriveClient;
|
||||||
|
pub use messages::MessagesClient;
|
||||||
|
pub use calendars::CalendarsClient;
|
||||||
|
pub use find::FindClient;
|
||||||
178
sunbeam-sdk/src/lasuite/people.rs
Normal file
178
sunbeam-sdk/src/lasuite/people.rs
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
//! People service client — contacts, teams, service providers, mail domains.
|
||||||
|
|
||||||
|
use crate::client::{AuthMethod, HttpTransport, ServiceClient};
|
||||||
|
use crate::error::Result;
|
||||||
|
use reqwest::Method;
|
||||||
|
use super::types::*;
|
||||||
|
|
||||||
|
/// Client for the La Suite People API.
|
||||||
|
pub struct PeopleClient {
|
||||||
|
pub(crate) transport: HttpTransport,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceClient for PeopleClient {
|
||||||
|
fn service_name(&self) -> &'static str {
|
||||||
|
"people"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_url(&self) -> &str {
|
||||||
|
&self.transport.base_url
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_parts(base_url: String, auth: AuthMethod) -> Self {
|
||||||
|
Self {
|
||||||
|
transport: HttpTransport::new(&base_url, auth),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PeopleClient {
|
||||||
|
/// Build a PeopleClient from domain (e.g. `https://people.{domain}/api/v1.0`).
|
||||||
|
pub fn connect(domain: &str) -> Self {
|
||||||
|
let base_url = format!("https://people.{domain}/api/v1.0");
|
||||||
|
Self::from_parts(base_url, AuthMethod::Bearer(String::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the bearer token for authentication.
|
||||||
|
pub fn with_token(mut self, token: &str) -> Self {
|
||||||
|
self.transport.set_auth(AuthMethod::Bearer(token.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Contacts -----------------------------------------------------------
|
||||||
|
|
||||||
|
/// List contacts with optional pagination.
|
||||||
|
pub async fn list_contacts(&self, page: Option<u32>) -> Result<DRFPage<Contact>> {
|
||||||
|
let path = match page {
|
||||||
|
Some(p) => format!("contacts/?page={p}"),
|
||||||
|
None => "contacts/".to_string(),
|
||||||
|
};
|
||||||
|
self.transport
|
||||||
|
.json(Method::GET, &path, Option::<&()>::None, "people list contacts")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a single contact by ID.
|
||||||
|
pub async fn get_contact(&self, id: &str) -> Result<Contact> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("contacts/{id}/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"people get contact",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new contact.
|
||||||
|
pub async fn create_contact(&self, body: &serde_json::Value) -> Result<Contact> {
|
||||||
|
self.transport
|
||||||
|
.json(Method::POST, "contacts/", Some(body), "people create contact")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update a contact (partial).
|
||||||
|
pub async fn update_contact(&self, id: &str, body: &serde_json::Value) -> Result<Contact> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PATCH,
|
||||||
|
&format!("contacts/{id}/"),
|
||||||
|
Some(body),
|
||||||
|
"people update contact",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a contact.
|
||||||
|
pub async fn delete_contact(&self, id: &str) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("contacts/{id}/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"people delete contact",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Teams --------------------------------------------------------------
|
||||||
|
|
||||||
|
/// List teams with optional pagination.
|
||||||
|
pub async fn list_teams(&self, page: Option<u32>) -> Result<DRFPage<Team>> {
|
||||||
|
let path = match page {
|
||||||
|
Some(p) => format!("teams/?page={p}"),
|
||||||
|
None => "teams/".to_string(),
|
||||||
|
};
|
||||||
|
self.transport
|
||||||
|
.json(Method::GET, &path, Option::<&()>::None, "people list teams")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a single team by ID.
|
||||||
|
pub async fn get_team(&self, id: &str) -> Result<Team> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("teams/{id}/"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"people get team",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new team.
|
||||||
|
pub async fn create_team(&self, body: &serde_json::Value) -> Result<Team> {
|
||||||
|
self.transport
|
||||||
|
.json(Method::POST, "teams/", Some(body), "people create team")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Service providers --------------------------------------------------
|
||||||
|
|
||||||
|
/// List service providers.
|
||||||
|
pub async fn list_service_providers(&self) -> Result<DRFPage<ServiceProvider>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
"service-providers/",
|
||||||
|
Option::<&()>::None,
|
||||||
|
"people list service providers",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Mail domains -------------------------------------------------------
|
||||||
|
|
||||||
|
/// List mail domains.
|
||||||
|
pub async fn list_mail_domains(&self) -> Result<DRFPage<MailDomain>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
"mail-domains/",
|
||||||
|
Option::<&()>::None,
|
||||||
|
"people list mail domains",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connect_url() {
|
||||||
|
let c = PeopleClient::connect("sunbeam.pt");
|
||||||
|
assert_eq!(c.base_url(), "https://people.sunbeam.pt/api/v1.0");
|
||||||
|
assert_eq!(c.service_name(), "people");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_parts() {
|
||||||
|
let c = PeopleClient::from_parts(
|
||||||
|
"http://localhost:8000/api/v1.0".into(),
|
||||||
|
AuthMethod::Bearer("tok".into()),
|
||||||
|
);
|
||||||
|
assert_eq!(c.base_url(), "http://localhost:8000/api/v1.0");
|
||||||
|
}
|
||||||
|
}
|
||||||
527
sunbeam-sdk/src/lasuite/types.rs
Normal file
527
sunbeam-sdk/src/lasuite/types.rs
Normal file
@@ -0,0 +1,527 @@
|
|||||||
|
//! Shared types for La Suite DRF-based services.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// DRF paginated response
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Standard Django REST Framework paginated list response.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct DRFPage<T> {
|
||||||
|
#[serde(default)]
|
||||||
|
pub count: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub next: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub previous: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub results: Vec<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// People types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// A contact in the People service.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Contact {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub first_name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub last_name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub email: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub phone: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub organization: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub job_title: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub notes: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A team in the People service.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Team {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub members: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A service provider in the People service.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct ServiceProvider {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub base_url: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A mail domain in the People service.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct MailDomain {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub status: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Docs types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// A document in the Docs service.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Document {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub title: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub content: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub is_public: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A document template.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Template {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub title: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub content: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub is_public: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A document version snapshot.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct DocVersion {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub document_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub version_number: Option<u64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub content: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An invitation to collaborate on a document.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Invitation {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub email: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub role: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub document_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub status: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Meet types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// A meeting room.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct MeetRoom {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub slug: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub is_public: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub configuration: Option<serde_json::Value>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A recording of a meeting.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Recording {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub room_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub filename: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub url: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub duration: Option<f64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Drive types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// A file in the Drive service.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct DriveFile {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub size: Option<u64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub mime_type: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub folder_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub url: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A folder in the Drive service.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct DriveFolder {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub parent_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A file sharing record.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct FileShare {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub file_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub user_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub role: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A file permission entry.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct FilePermission {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub file_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub user_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub role: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub can_read: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub can_write: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Messages types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// A mailbox.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Mailbox {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub email: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub display_name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An email message.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct EmailMessage {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub subject: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub from_address: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub to_addresses: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub cc_addresses: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub body: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub is_read: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub folder: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A mail folder.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct MailFolder {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub message_count: Option<u64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub unread_count: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A mail contact.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct MailContact {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub email: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub display_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Calendars types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// A calendar.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Calendar {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub color: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub is_default: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A calendar event.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct CalEvent {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub title: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub start: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub end: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub location: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub all_day: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub attendees: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub calendar_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Find types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// A search result from the Find service.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct SearchResult {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub title: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub url: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub source: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub score: Option<f64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_drf_page_deserialize() {
|
||||||
|
let json = r#"{
|
||||||
|
"count": 2,
|
||||||
|
"next": "https://example.com/api/v1.0/contacts/?page=2",
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{"id": "1", "first_name": "Alice"},
|
||||||
|
{"id": "2", "first_name": "Bob"}
|
||||||
|
]
|
||||||
|
}"#;
|
||||||
|
let page: DRFPage<Contact> = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(page.count, 2);
|
||||||
|
assert!(page.next.is_some());
|
||||||
|
assert!(page.previous.is_none());
|
||||||
|
assert_eq!(page.results.len(), 2);
|
||||||
|
assert_eq!(page.results[0].first_name.as_deref(), Some("Alice"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_drf_page_empty() {
|
||||||
|
let json = r#"{"count": 0, "next": null, "previous": null, "results": []}"#;
|
||||||
|
let page: DRFPage<Contact> = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(page.count, 0);
|
||||||
|
assert!(page.results.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_drf_page_defaults() {
|
||||||
|
let json = r#"{"results": []}"#;
|
||||||
|
let page: DRFPage<Document> = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(page.count, 0);
|
||||||
|
assert!(page.next.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_contact_roundtrip() {
|
||||||
|
let c = Contact {
|
||||||
|
id: "abc".into(),
|
||||||
|
first_name: Some("Alice".into()),
|
||||||
|
last_name: Some("Smith".into()),
|
||||||
|
email: Some("alice@example.com".into()),
|
||||||
|
phone: None,
|
||||||
|
avatar: None,
|
||||||
|
organization: None,
|
||||||
|
job_title: None,
|
||||||
|
notes: None,
|
||||||
|
created_at: None,
|
||||||
|
updated_at: None,
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&c).unwrap();
|
||||||
|
let c2: Contact = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(c2.id, "abc");
|
||||||
|
assert_eq!(c2.first_name.as_deref(), Some("Alice"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_document_defaults() {
|
||||||
|
let json = r#"{"id": "doc-1"}"#;
|
||||||
|
let d: Document = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(d.id, "doc-1");
|
||||||
|
assert!(d.title.is_none());
|
||||||
|
assert!(d.content.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_meet_room_deserialize() {
|
||||||
|
let json = r#"{"id": "room-1", "name": "Standup", "slug": "standup"}"#;
|
||||||
|
let r: MeetRoom = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(r.id, "room-1");
|
||||||
|
assert_eq!(r.name.as_deref(), Some("Standup"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_calendar_event_deserialize() {
|
||||||
|
let json = r#"{"id": "ev-1", "title": "Lunch", "start": "2026-01-01T12:00:00Z", "all_day": false}"#;
|
||||||
|
let e: CalEvent = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(e.id, "ev-1");
|
||||||
|
assert_eq!(e.all_day, Some(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_search_result_deserialize() {
|
||||||
|
let json = r#"{"id": "sr-1", "title": "Found it", "score": 0.95}"#;
|
||||||
|
let sr: SearchResult = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(sr.id, "sr-1");
|
||||||
|
assert_eq!(sr.score, Some(0.95));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_email_message_deserialize() {
|
||||||
|
let json = r#"{"id": "msg-1", "subject": "Hello", "to_addresses": ["bob@example.com"]}"#;
|
||||||
|
let m: EmailMessage = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(m.id, "msg-1");
|
||||||
|
assert_eq!(m.to_addresses.as_ref().unwrap().len(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user