use derive_builder::Builder;
use reqwest::Method;
use std::borrow::Cow;
use crate::api::project_memberships::GroupProjectMembership;
use crate::api::users::UserEssentials;
use crate::api::{Endpoint, QueryParams, ReturnsJsonResponse};
use serde::Serialize;
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, Clone)]
pub struct GroupEssentials {
pub id: u64,
pub name: String,
}
impl From<Group> for GroupEssentials {
fn from(v: Group) -> Self {
GroupEssentials {
id: v.id,
name: v.name,
}
}
}
impl From<&Group> for GroupEssentials {
fn from(v: &Group) -> Self {
GroupEssentials {
id: v.id,
name: v.name.to_owned(),
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
pub struct Group {
pub id: u64,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub users: Option<Vec<UserEssentials>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub memberships: Option<Vec<GroupProjectMembership>>,
}
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
pub struct ListGroups {}
impl ReturnsJsonResponse for ListGroups {}
impl ListGroups {
#[must_use]
pub fn builder() -> ListGroupsBuilder {
ListGroupsBuilder::default()
}
}
impl Endpoint for ListGroups {
fn method(&self) -> Method {
Method::GET
}
fn endpoint(&self) -> Cow<'static, str> {
"groups.json".into()
}
}
#[derive(Debug, Clone)]
pub enum GroupInclude {
Users,
Memberships,
}
impl std::fmt::Display for GroupInclude {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Users => {
write!(f, "users")
}
Self::Memberships => {
write!(f, "memberships")
}
}
}
}
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
pub struct GetGroup {
id: u64,
#[builder(default)]
include: Option<Vec<GroupInclude>>,
}
impl ReturnsJsonResponse for GetGroup {}
impl GetGroup {
#[must_use]
pub fn builder() -> GetGroupBuilder {
GetGroupBuilder::default()
}
}
impl Endpoint for GetGroup {
fn method(&self) -> Method {
Method::GET
}
fn endpoint(&self) -> Cow<'static, str> {
format!("groups/{}.json", &self.id).into()
}
fn parameters(&self) -> QueryParams {
let mut params = QueryParams::default();
params.push_opt("include", self.include.as_ref());
params
}
}
#[derive(Debug, Clone, Builder, Serialize)]
#[builder(setter(strip_option))]
pub struct CreateGroup<'a> {
#[builder(setter(into))]
name: Cow<'a, str>,
#[builder(default)]
user_ids: Option<Vec<u64>>,
}
impl<'a> ReturnsJsonResponse for CreateGroup<'a> {}
impl<'a> CreateGroup<'a> {
#[must_use]
pub fn builder() -> CreateGroupBuilder<'a> {
CreateGroupBuilder::default()
}
}
impl<'a> Endpoint for CreateGroup<'a> {
fn method(&self) -> Method {
Method::POST
}
fn endpoint(&self) -> Cow<'static, str> {
"groups.json".into()
}
fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
Ok(Some((
"application/json",
serde_json::to_vec(&GroupWrapper::<CreateGroup> {
group: (*self).to_owned(),
})?,
)))
}
}
#[derive(Debug, Clone, Builder, Serialize)]
#[builder(setter(strip_option))]
pub struct UpdateGroup<'a> {
#[serde(skip_serializing)]
id: u64,
#[builder(setter(into))]
name: Cow<'a, str>,
#[builder(default)]
user_ids: Option<Vec<u64>>,
}
impl<'a> UpdateGroup<'a> {
#[must_use]
pub fn builder() -> UpdateGroupBuilder<'a> {
UpdateGroupBuilder::default()
}
}
impl<'a> Endpoint for UpdateGroup<'a> {
fn method(&self) -> Method {
Method::PUT
}
fn endpoint(&self) -> Cow<'static, str> {
format!("groups/{}.json", self.id).into()
}
fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
Ok(Some((
"application/json",
serde_json::to_vec(&GroupWrapper::<UpdateGroup> {
group: (*self).to_owned(),
})?,
)))
}
}
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
pub struct DeleteGroup {
id: u64,
}
impl DeleteGroup {
#[must_use]
pub fn builder() -> DeleteGroupBuilder {
DeleteGroupBuilder::default()
}
}
impl Endpoint for DeleteGroup {
fn method(&self) -> Method {
Method::DELETE
}
fn endpoint(&self) -> Cow<'static, str> {
format!("groups/{}.json", &self.id).into()
}
}
#[derive(Debug, Builder, Serialize)]
#[builder(setter(strip_option))]
pub struct AddUserToGroup {
#[serde(skip_serializing)]
group_id: u64,
user_id: u64,
}
impl AddUserToGroup {
#[must_use]
pub fn builder() -> AddUserToGroupBuilder {
AddUserToGroupBuilder::default()
}
}
impl Endpoint for AddUserToGroup {
fn method(&self) -> Method {
Method::POST
}
fn endpoint(&self) -> Cow<'static, str> {
format!("groups/{}/users.json", &self.group_id).into()
}
fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
Ok(Some(("application/json", serde_json::to_vec(self)?)))
}
}
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
pub struct RemoveUserFromGroup {
group_id: u64,
user_id: u64,
}
impl RemoveUserFromGroup {
#[must_use]
pub fn builder() -> RemoveUserFromGroupBuilder {
RemoveUserFromGroupBuilder::default()
}
}
impl Endpoint for RemoveUserFromGroup {
fn method(&self) -> Method {
Method::DELETE
}
fn endpoint(&self) -> Cow<'static, str> {
format!("groups/{}/users/{}.json", &self.group_id, &self.user_id).into()
}
}
#[derive(Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
pub struct GroupsWrapper<T> {
pub groups: Vec<T>,
}
#[derive(Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
pub struct GroupWrapper<T> {
pub group: T,
}
#[cfg(test)]
pub(crate) mod test {
use super::*;
use crate::api::test_helpers::with_group;
use parking_lot::{const_rwlock, RwLock};
use pretty_assertions::assert_eq;
use std::error::Error;
use tracing_test::traced_test;
pub static GROUP_LOCK: RwLock<()> = const_rwlock(());
#[traced_test]
#[test]
fn test_list_groups_no_pagination() -> Result<(), Box<dyn Error>> {
let _r_groups = GROUP_LOCK.read();
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env()?;
let endpoint = ListGroups::builder().build()?;
redmine.json_response_body::<_, GroupsWrapper<Group>>(&endpoint)?;
Ok(())
}
#[traced_test]
#[test]
fn test_get_group() -> Result<(), Box<dyn Error>> {
let _r_groups = GROUP_LOCK.read();
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env()?;
let endpoint = GetGroup::builder().id(338).build()?;
redmine.json_response_body::<_, GroupWrapper<Group>>(&endpoint)?;
Ok(())
}
#[function_name::named]
#[traced_test]
#[test]
fn test_create_group() -> Result<(), Box<dyn Error>> {
let name = format!("unittest_{}", function_name!());
with_group(&name, |_, _, _| Ok(()))?;
Ok(())
}
#[function_name::named]
#[traced_test]
#[test]
fn test_update_project() -> Result<(), Box<dyn Error>> {
let name = format!("unittest_{}", function_name!());
with_group(&name, |redmine, id, _name| {
let update_endpoint = super::UpdateGroup::builder()
.id(id)
.name("unittest_rename_test")
.build()?;
redmine.ignore_response_body::<_>(&update_endpoint)?;
Ok(())
})?;
Ok(())
}
#[traced_test]
#[test]
fn test_completeness_group_type() -> Result<(), Box<dyn Error>> {
let _r_groups = GROUP_LOCK.read();
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env()?;
let endpoint = ListGroups::builder().build()?;
let GroupsWrapper { groups: values } =
redmine.json_response_body::<_, GroupsWrapper<serde_json::Value>>(&endpoint)?;
for value in values {
let o: Group = serde_json::from_value(value.clone())?;
let reserialized = serde_json::to_value(o)?;
assert_eq!(value, reserialized);
}
Ok(())
}
#[traced_test]
#[test]
fn test_completeness_group_type_all_group_details() -> Result<(), Box<dyn Error>> {
let _r_groups = GROUP_LOCK.read();
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env()?;
let endpoint = ListGroups::builder().build()?;
let GroupsWrapper { groups } =
redmine.json_response_body::<_, GroupsWrapper<Group>>(&endpoint)?;
for group in groups {
let get_endpoint = GetGroup::builder()
.id(group.id)
.include(vec![GroupInclude::Users, GroupInclude::Memberships])
.build()?;
let GroupWrapper { group: value } =
redmine.json_response_body::<_, GroupWrapper<serde_json::Value>>(&get_endpoint)?;
let o: Group = serde_json::from_value(value.clone())?;
let reserialized = serde_json::to_value(o)?;
assert_eq!(value, reserialized);
}
Ok(())
}
}