use http::Method;
use serde::{Deserialize, Serialize};
use crate::core::operation::{Operation, encode_path_segment, json_body, push_opt};
use crate::core::pagination::{DEFAULT_PAGE_SIZE, Listing, Page, Pagination, Paginator};
use crate::error::Result;
use crate::types::{DeleteResponse, Group, GroupIconResponse, GroupMember, Json};
#[allow(missing_docs)]
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct ListGroupsResponse {
pub groups: Vec<Group>,
pub pagination: Option<Pagination>,
}
impl Listing for ListGroupsResponse {
type Item = Group;
fn into_page(self) -> Page<Self::Item> {
Page {
items: self.groups,
pagination: self.pagination,
}
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct ListGroupMembersResponse {
pub group_id: Option<String>,
pub group_name: Option<String>,
pub icon_url: Option<String>,
pub members: Vec<GroupMember>,
pub pagination: Option<Pagination>,
}
impl Listing for ListGroupMembersResponse {
type Item = GroupMember;
fn into_page(self) -> Page<Self::Item> {
Page {
items: self.members,
pagination: self.pagination,
}
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct AddGroupMemberResponse {
pub message: Option<String>,
pub member: Option<GroupMember>,
pub contact_created: Option<bool>,
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct RemoveGroupMemberResponse {
pub success: Option<bool>,
pub removed_at: Option<i64>,
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Default)]
pub struct ListGroups {
pub limit: Option<u32>,
pub offset: Option<u32>,
pub q: Option<String>,
pub sort: Option<String>,
}
impl Operation for ListGroups {
type Output = ListGroupsResponse;
const METHOD: Method = Method::GET;
fn path(&self) -> String {
"/groups".into()
}
fn query(&self) -> Vec<(&'static str, String)> {
let mut q = Vec::new();
push_opt(&mut q, "limit", self.limit);
push_opt(&mut q, "offset", self.offset);
push_opt(&mut q, "q", self.q.as_ref());
push_opt(&mut q, "sort", self.sort.as_ref());
q
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Serialize)]
pub struct CreateGroup {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub chat_guid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub members: Option<Vec<String>>,
}
impl CreateGroup {
pub fn new(name: impl Into<String>) -> Self {
CreateGroup {
name: name.into(),
chat_guid: None,
members: None,
}
}
#[must_use]
pub fn chat_guid(mut self, v: impl Into<String>) -> Self {
self.chat_guid = Some(v.into());
self
}
#[must_use]
pub fn members(mut self, v: Vec<String>) -> Self {
self.members = Some(v);
self
}
}
impl Operation for CreateGroup {
type Output = Json;
const METHOD: Method = Method::POST;
fn path(&self) -> String {
"/groups".into()
}
fn body(&self) -> Result<Option<Vec<u8>>> {
json_body(self)
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct GetGroup {
pub group_id: String,
}
impl Operation for GetGroup {
type Output = Group;
const METHOD: Method = Method::GET;
fn path(&self) -> String {
format!("/groups/{}", encode_path_segment(&self.group_id))
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Serialize)]
pub struct UpdateGroup {
#[serde(skip)]
pub group_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl Operation for UpdateGroup {
type Output = Json;
const METHOD: Method = Method::PATCH;
fn path(&self) -> String {
format!("/groups/{}", encode_path_segment(&self.group_id))
}
fn body(&self) -> Result<Option<Vec<u8>>> {
json_body(self)
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct DeleteGroup {
pub group_id: String,
}
impl Operation for DeleteGroup {
type Output = DeleteResponse;
const METHOD: Method = Method::DELETE;
fn path(&self) -> String {
format!("/groups/{}", encode_path_segment(&self.group_id))
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Serialize)]
pub struct SetGroupIcon {
#[serde(skip)]
pub group_id: String,
pub icon: String,
}
impl Operation for SetGroupIcon {
type Output = GroupIconResponse;
const METHOD: Method = Method::POST;
fn path(&self) -> String {
format!("/groups/{}/icon", encode_path_segment(&self.group_id))
}
fn body(&self) -> Result<Option<Vec<u8>>> {
json_body(self)
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct RemoveGroupIcon {
pub group_id: String,
}
impl Operation for RemoveGroupIcon {
type Output = GroupIconResponse;
const METHOD: Method = Method::DELETE;
fn path(&self) -> String {
format!("/groups/{}/icon", encode_path_segment(&self.group_id))
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct ListGroupMembers {
pub group_id: String,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
impl Operation for ListGroupMembers {
type Output = ListGroupMembersResponse;
const METHOD: Method = Method::GET;
fn path(&self) -> String {
format!("/groups/{}/members", encode_path_segment(&self.group_id))
}
fn query(&self) -> Vec<(&'static str, String)> {
let mut q = Vec::new();
push_opt(&mut q, "limit", self.limit);
push_opt(&mut q, "offset", self.offset);
q
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Serialize)]
pub struct AddGroupMember {
#[serde(skip)]
pub group_id: String,
pub contact_id: String,
}
impl Operation for AddGroupMember {
type Output = AddGroupMemberResponse;
const METHOD: Method = Method::POST;
fn path(&self) -> String {
format!("/groups/{}/members", encode_path_segment(&self.group_id))
}
fn body(&self) -> Result<Option<Vec<u8>>> {
json_body(self)
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct RemoveGroupMember {
pub group_id: String,
pub contact_id: String,
}
impl Operation for RemoveGroupMember {
type Output = RemoveGroupMemberResponse;
const METHOD: Method = Method::DELETE;
fn path(&self) -> String {
format!(
"/groups/{}/members/{}",
encode_path_segment(&self.group_id),
encode_path_segment(&self.contact_id)
)
}
}
#[derive(Debug)]
pub struct Groups<'c, C> {
pub(crate) client: &'c C,
}
#[derive(Debug)]
pub struct GroupMembers<'c, C> {
pub(crate) client: &'c C,
pub(crate) group_id: String,
}
#[cfg(feature = "async")]
impl crate::Client {
pub fn groups(&self) -> Groups<'_, crate::Client> {
Groups { client: self }
}
}
#[cfg(feature = "sync")]
impl crate::BlockingClient {
pub fn groups(&self) -> Groups<'_, crate::BlockingClient> {
Groups { client: self }
}
}
#[cfg(feature = "async")]
impl<'c> Groups<'c, crate::Client> {
pub async fn list(&self) -> Result<ListGroupsResponse> {
self.client.send(ListGroups::default()).await
}
pub async fn list_with(&self, query: ListGroups) -> Result<ListGroupsResponse> {
self.client.send(query).await
}
pub fn list_all(
&self,
) -> Paginator<'c, crate::Client, impl Fn(u32, u32) -> ListGroups + use<'c>, ListGroups> {
Paginator::new(self.client, DEFAULT_PAGE_SIZE, |offset, limit| ListGroups {
offset: Some(offset),
limit: Some(limit),
..Default::default()
})
}
pub async fn create(&self, op: CreateGroup) -> Result<Json> {
self.client.send(op).await
}
pub async fn get(&self, group_id: impl Into<String>) -> Result<Group> {
self.client
.send(GetGroup {
group_id: group_id.into(),
})
.await
}
pub async fn update(&self, group_id: impl Into<String>, name: Option<String>) -> Result<Json> {
self.client
.send(UpdateGroup {
group_id: group_id.into(),
name,
})
.await
}
pub async fn delete(&self, group_id: impl Into<String>) -> Result<DeleteResponse> {
self.client
.send(DeleteGroup {
group_id: group_id.into(),
})
.await
}
pub async fn set_icon(
&self,
group_id: impl Into<String>,
icon: impl Into<String>,
) -> Result<GroupIconResponse> {
self.client
.send(SetGroupIcon {
group_id: group_id.into(),
icon: icon.into(),
})
.await
}
pub async fn remove_icon(&self, group_id: impl Into<String>) -> Result<GroupIconResponse> {
self.client
.send(RemoveGroupIcon {
group_id: group_id.into(),
})
.await
}
pub fn members(&self, group_id: impl Into<String>) -> GroupMembers<'c, crate::Client> {
GroupMembers {
client: self.client,
group_id: group_id.into(),
}
}
}
#[cfg(feature = "sync")]
impl<'c> Groups<'c, crate::BlockingClient> {
pub fn list(&self) -> Result<ListGroupsResponse> {
self.client.send(ListGroups::default())
}
pub fn list_with(&self, query: ListGroups) -> Result<ListGroupsResponse> {
self.client.send(query)
}
pub fn list_all(
&self,
) -> Paginator<'c, crate::BlockingClient, impl Fn(u32, u32) -> ListGroups + use<'c>, ListGroups>
{
Paginator::new(self.client, DEFAULT_PAGE_SIZE, |offset, limit| ListGroups {
offset: Some(offset),
limit: Some(limit),
..Default::default()
})
}
pub fn create(&self, op: CreateGroup) -> Result<Json> {
self.client.send(op)
}
pub fn get(&self, group_id: impl Into<String>) -> Result<Group> {
self.client.send(GetGroup {
group_id: group_id.into(),
})
}
pub fn update(&self, group_id: impl Into<String>, name: Option<String>) -> Result<Json> {
self.client.send(UpdateGroup {
group_id: group_id.into(),
name,
})
}
pub fn delete(&self, group_id: impl Into<String>) -> Result<DeleteResponse> {
self.client.send(DeleteGroup {
group_id: group_id.into(),
})
}
pub fn set_icon(
&self,
group_id: impl Into<String>,
icon: impl Into<String>,
) -> Result<GroupIconResponse> {
self.client.send(SetGroupIcon {
group_id: group_id.into(),
icon: icon.into(),
})
}
pub fn remove_icon(&self, group_id: impl Into<String>) -> Result<GroupIconResponse> {
self.client.send(RemoveGroupIcon {
group_id: group_id.into(),
})
}
pub fn members(&self, group_id: impl Into<String>) -> GroupMembers<'c, crate::BlockingClient> {
GroupMembers {
client: self.client,
group_id: group_id.into(),
}
}
}
#[cfg(feature = "async")]
impl<'c> GroupMembers<'c, crate::Client> {
pub async fn list(&self) -> Result<ListGroupMembersResponse> {
self.client
.send(ListGroupMembers {
group_id: self.group_id.clone(),
limit: None,
offset: None,
})
.await
}
pub fn list_all(
&self,
) -> Paginator<
'c,
crate::Client,
impl Fn(u32, u32) -> ListGroupMembers + use<'c>,
ListGroupMembers,
> {
let group_id = self.group_id.clone();
Paginator::new(self.client, DEFAULT_PAGE_SIZE, move |offset, limit| {
ListGroupMembers {
group_id: group_id.clone(),
offset: Some(offset),
limit: Some(limit),
}
})
}
pub async fn add(&self, contact_id: impl Into<String>) -> Result<AddGroupMemberResponse> {
self.client
.send(AddGroupMember {
group_id: self.group_id.clone(),
contact_id: contact_id.into(),
})
.await
}
pub async fn remove(&self, contact_id: impl Into<String>) -> Result<RemoveGroupMemberResponse> {
self.client
.send(RemoveGroupMember {
group_id: self.group_id.clone(),
contact_id: contact_id.into(),
})
.await
}
}
#[cfg(feature = "sync")]
impl<'c> GroupMembers<'c, crate::BlockingClient> {
pub fn list(&self) -> Result<ListGroupMembersResponse> {
self.client.send(ListGroupMembers {
group_id: self.group_id.clone(),
limit: None,
offset: None,
})
}
pub fn list_all(
&self,
) -> Paginator<
'c,
crate::BlockingClient,
impl Fn(u32, u32) -> ListGroupMembers + use<'c>,
ListGroupMembers,
> {
let group_id = self.group_id.clone();
Paginator::new(self.client, DEFAULT_PAGE_SIZE, move |offset, limit| {
ListGroupMembers {
group_id: group_id.clone(),
offset: Some(offset),
limit: Some(limit),
}
})
}
pub fn add(&self, contact_id: impl Into<String>) -> Result<AddGroupMemberResponse> {
self.client.send(AddGroupMember {
group_id: self.group_id.clone(),
contact_id: contact_id.into(),
})
}
pub fn remove(&self, contact_id: impl Into<String>) -> Result<RemoveGroupMemberResponse> {
self.client.send(RemoveGroupMember {
group_id: self.group_id.clone(),
contact_id: contact_id.into(),
})
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::print_stdout,
clippy::unreadable_literal
)]
mod tests {
use super::*;
use crate::core::operation::Operation;
#[test]
fn list_groups_method_and_path() {
assert_eq!(ListGroups::METHOD, http::Method::GET);
let op = ListGroups::default();
assert_eq!(op.path(), "/groups");
}
#[test]
fn list_groups_query_empty_when_no_options() {
let op = ListGroups::default();
assert!(op.query().is_empty());
}
#[test]
fn list_groups_query_with_all_options() {
let op = ListGroups {
limit: Some(10),
offset: Some(20),
q: Some("test".into()),
sort: Some("asc".into()),
};
let q = op.query();
assert!(q.contains(&("limit", "10".into())));
assert!(q.contains(&("offset", "20".into())));
assert!(q.contains(&("q", "test".into())));
assert!(q.contains(&("sort", "asc".into())));
assert_eq!(q.len(), 4);
}
#[test]
fn list_groups_query_omits_unset_optionals() {
let op = ListGroups {
limit: Some(5),
offset: None,
q: None,
sort: None,
};
let q = op.query();
assert_eq!(q.len(), 1);
assert!(q.contains(&("limit", "5".into())));
}
#[test]
fn create_group_method_and_path() {
assert_eq!(CreateGroup::METHOD, http::Method::POST);
let op = CreateGroup {
name: "MyGroup".into(),
chat_guid: None,
members: None,
};
assert_eq!(op.path(), "/groups");
}
#[test]
fn create_group_body_minimal() {
let op = CreateGroup {
name: "MyGroup".into(),
chat_guid: None,
members: None,
};
let body = op.body().unwrap().unwrap();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(v, serde_json::json!({ "name": "MyGroup" }));
}
#[test]
fn create_group_body_populated() {
let op = CreateGroup {
name: "MyGroup".into(),
chat_guid: Some("chat-abc".into()),
members: Some(vec!["m1".into(), "m2".into()]),
};
let body = op.body().unwrap().unwrap();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(
v,
serde_json::json!({
"name": "MyGroup",
"chat_guid": "chat-abc",
"members": ["m1", "m2"]
})
);
}
#[test]
fn get_group_method_and_path() {
assert_eq!(GetGroup::METHOD, http::Method::GET);
let op = GetGroup {
group_id: "g1".into(),
};
assert_eq!(op.path(), "/groups/g1");
}
#[test]
fn update_group_method_and_path() {
assert_eq!(UpdateGroup::METHOD, http::Method::PATCH);
let op = UpdateGroup {
group_id: "g1".into(),
name: None,
};
assert_eq!(op.path(), "/groups/g1");
}
#[test]
fn update_group_body_minimal_omits_name() {
let op = UpdateGroup {
group_id: "g1".into(),
name: None,
};
let body = op.body().unwrap().unwrap();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(v, serde_json::json!({}));
}
#[test]
fn update_group_body_with_name() {
let op = UpdateGroup {
group_id: "g1".into(),
name: Some("Renamed".into()),
};
let body = op.body().unwrap().unwrap();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(v, serde_json::json!({ "name": "Renamed" }));
}
#[test]
fn delete_group_method_and_path() {
assert_eq!(DeleteGroup::METHOD, http::Method::DELETE);
let op = DeleteGroup {
group_id: "g1".into(),
};
assert_eq!(op.path(), "/groups/g1");
}
#[test]
fn set_group_icon_method_and_path() {
assert_eq!(SetGroupIcon::METHOD, http::Method::POST);
let op = SetGroupIcon {
group_id: "g1".into(),
icon: "https://example.com/icon.png".into(),
};
assert_eq!(op.path(), "/groups/g1/icon");
}
#[test]
fn set_group_icon_body() {
let op = SetGroupIcon {
group_id: "g1".into(),
icon: "https://example.com/icon.png".into(),
};
let body = op.body().unwrap().unwrap();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(
v,
serde_json::json!({ "icon": "https://example.com/icon.png" })
);
}
#[test]
fn remove_group_icon_method_and_path() {
assert_eq!(RemoveGroupIcon::METHOD, http::Method::DELETE);
let op = RemoveGroupIcon {
group_id: "g1".into(),
};
assert_eq!(op.path(), "/groups/g1/icon");
}
#[test]
fn list_group_members_method_and_path() {
assert_eq!(ListGroupMembers::METHOD, http::Method::GET);
let op = ListGroupMembers {
group_id: "g1".into(),
limit: None,
offset: None,
};
assert_eq!(op.path(), "/groups/g1/members");
}
#[test]
fn list_group_members_query_empty_when_no_options() {
let op = ListGroupMembers {
group_id: "g1".into(),
limit: None,
offset: None,
};
assert!(op.query().is_empty());
}
#[test]
fn list_group_members_query_with_options() {
let op = ListGroupMembers {
group_id: "g1".into(),
limit: Some(25),
offset: Some(50),
};
let q = op.query();
assert_eq!(q.len(), 2);
assert!(q.contains(&("limit", "25".into())));
assert!(q.contains(&("offset", "50".into())));
}
#[test]
fn add_group_member_method_and_path() {
assert_eq!(AddGroupMember::METHOD, http::Method::POST);
let op = AddGroupMember {
group_id: "g1".into(),
contact_id: "m1".into(),
};
assert_eq!(op.path(), "/groups/g1/members");
}
#[test]
fn add_group_member_body() {
let op = AddGroupMember {
group_id: "g1".into(),
contact_id: "m1".into(),
};
let body = op.body().unwrap().unwrap();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(v, serde_json::json!({ "contact_id": "m1" }));
}
#[test]
fn remove_group_member_method_and_path() {
assert_eq!(RemoveGroupMember::METHOD, http::Method::DELETE);
let op = RemoveGroupMember {
group_id: "g1".into(),
contact_id: "m1".into(),
};
assert_eq!(op.path(), "/groups/g1/members/m1");
}
}