diff --git a/szurubooru-client/src/client.rs b/szurubooru-client/src/client.rs index f3a8490..152dcaa 100644 --- a/szurubooru-client/src/client.rs +++ b/szurubooru-client/src/client.rs @@ -597,16 +597,15 @@ impl<'a> SzurubooruRequest<'a> { .await } - /// Update an existing post based on the `contentUrl` field, which the server will use to - /// download and update the post + /// Update an existing post /// See [SzurubooruRequest::create_post_from_url] for more details about the fields in /// (CreateUpdatePost)[models::CreateUpdatePost] - pub async fn update_post_from_url( + pub async fn update_post( &self, post_id: u32, update_post: &CreateUpdatePost, ) -> SzurubooruResult { - let path = format!("/posts/{post_id}"); + let path = format!("/post/{post_id}"); self.create_update_post_from_url(&path, Method::PUT, update_post) .await } @@ -619,28 +618,37 @@ impl<'a> SzurubooruRequest<'a> { Ok(Part::stream(bytes)) } - async fn create_update_post_from_file( + async fn create_update_post_from_file( &self, - file: &mut File, + file: Option<&mut File>, thumbnail: Option<&mut File>, + file_name: Option, path: &str, method: Method, cupost: &CreateUpdatePost, - ) -> SzurubooruResult { + ) -> SzurubooruResult + where + T: AsRef, + { let request = self.prep_request(method, path, None); let metadata_str = serde_json::to_string(cupost).map_err(SzurubooruClientError::JSONSerializationError)?; let metadata_part = Part::text(metadata_str); - let content_part = self.part_from_file(file)?; + let mut form = Form::new().part("metadata", metadata_part); - let mut form = Form::new() - .part("content", content_part) - .part("metadata", metadata_part); + if let Some(file) = file { + let content_part = self + .part_from_file(file)? + .file_name(file_name.as_ref().unwrap().as_ref().to_string()); + form = form.part("content", content_part); + } if let Some(thumbnail) = thumbnail { - let thumbnail_part = self.part_from_file(thumbnail)?; + let thumbnail_part = self + .part_from_file(thumbnail)? + .file_name(format!("thumbnail_{}", file_name.unwrap().as_ref())); form = form.part("thumbnail", thumbnail_part); } @@ -650,14 +658,25 @@ impl<'a> SzurubooruRequest<'a> { /// Create a new post from a file handle /// See [SzurubooruRequest::create_post_from_url] for more details about the fields in /// (CreateUpdatePost)[models::CreateUpdatePost] - pub async fn create_post_from_file( + pub async fn create_post_from_file( &self, file: &mut File, thumbnail: Option<&mut File>, + file_name: T, new_post: &CreateUpdatePost, - ) -> SzurubooruResult { - self.create_update_post_from_file(file, thumbnail, "/posts", Method::POST, new_post) - .await + ) -> SzurubooruResult + where + T: AsRef, + { + self.create_update_post_from_file( + Some(file), + thumbnail, + Some(file_name), + "/posts", + Method::POST, + new_post, + ) + .await } /// Create a new post from a file path @@ -669,30 +688,57 @@ impl<'a> SzurubooruRequest<'a> { thumbnail: Option>, new_post: &CreateUpdatePost, ) -> SzurubooruResult { - let mut file = File::open(file_path).map_err(SzurubooruClientError::IOError)?; - + let mut file = File::open(&file_path).map_err(SzurubooruClientError::IOError)?; + let filename = file_path.as_ref().file_name().unwrap().to_str().unwrap(); let mut thumbnail_file = if let Some(t) = thumbnail { Some(File::open(t).map_err(SzurubooruClientError::IOError)?) } else { None }; - self.create_post_from_file(&mut file, thumbnail_file.as_mut(), new_post) + self.create_post_from_file(&mut file, thumbnail_file.as_mut(), filename, new_post) .await } + /// Create a post from a token previously generated by + /// (upload_temporary_file_from_path)[SzurubooruRequest::upload_temporary_file_from_path] + pub async fn create_post_from_token( + &self, + new_post: &CreateUpdatePost, + ) -> SzurubooruResult { + assert!(new_post.content_token.is_some()); + + self.create_update_post_from_file( + None, + None, + None::, + "/posts", + Method::POST, + new_post, + ) + .await + } + /// Update an existing post from an open File handle /// See [SzurubooruRequest::create_post_from_url] for more details about the fields in /// (CreateUpdatePost)[models::CreateUpdatePost] pub async fn update_post_from_file( &self, post_id: u32, - file: &mut File, + file: Option<&mut File>, thumbnail: Option<&mut File>, + file_name: impl AsRef, update_post: &CreateUpdatePost, ) -> SzurubooruResult { let path = format!("/posts/{post_id}"); - self.create_update_post_from_file(file, thumbnail, &path, Method::PUT, update_post) - .await + self.create_update_post_from_file( + file, + thumbnail, + Some(file_name), + &path, + Method::PUT, + update_post, + ) + .await } /// Update an existing post from a file path @@ -701,25 +747,60 @@ impl<'a> SzurubooruRequest<'a> { pub async fn update_post_from_file_path( &self, post_id: u32, - file_path: impl AsRef, + file_path: Option>, thumbnail: Option>, update_post: &CreateUpdatePost, ) -> SzurubooruResult { - let mut file = File::open(file_path).map_err(SzurubooruClientError::IOError)?; + let mut filename = None; + let mut file = if let Some(f) = file_path { + filename = Some( + f.as_ref() + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_string(), + ); + Some(File::open(f).map_err(SzurubooruClientError::IOError)?) + } else { + None + }; let mut thumbnail_file = if let Some(t) = thumbnail { + if let None = filename { + filename = Some( + t.as_ref() + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_string(), + ); + } Some(File::open(t).map_err(SzurubooruClientError::IOError)?) } else { None }; - self.update_post_from_file(post_id, &mut file, thumbnail_file.as_mut(), update_post) - .await + self.update_post_from_file( + post_id, + file.as_mut(), + thumbnail_file.as_mut(), + filename.unwrap(), + update_post, + ) + .await } async fn get_post_content(&self, post_id: u32) -> SzurubooruResult { let post_resource = self.get_post(post_id).await?; - let req = self.prep_request(Method::GET, post_resource.content_url.unwrap(), None); + let data_url = format!( + "{}/{}", + self.client.base_url, + post_resource.content_url.unwrap() + ); + + let req = self.prep_request(Method::GET, data_url, None); let request = req .build() .map_err(SzurubooruClientError::RequestBuilderError)?; @@ -729,7 +810,7 @@ impl<'a> SzurubooruRequest<'a> { .client .execute(request) .await - .map_err(|e| SzurubooruClientError::RequestError(e))?; + .map_err(SzurubooruClientError::RequestError)?; self.handle_response(resp_res).await } @@ -831,8 +912,9 @@ impl<'a> SzurubooruRequest<'a> { pub async fn delete_post(&self, post_id: u32, version: u32) -> SzurubooruResult<()> { let path = format!("/post/{post_id}"); let version_obj = ResourceVersion { version }; - self.do_request(Method::DELETE, &path, None, Some(&version_obj)) + self.do_request::(Method::DELETE, &path, None, Some(&version_obj)) .await + .map(|_| ()) } /// @@ -935,13 +1017,19 @@ impl<'a> SzurubooruRequest<'a> { } /// Deletes existing pool category. The pool category to be deleted must have no usages. - pub async fn delete_pool_category(&self, category_name: T) -> SzurubooruResult<()> + pub async fn delete_pool_category( + &self, + category_name: T, + version: u32, + ) -> SzurubooruResult<()> where T: AsRef + Display, { let path = format!("/pool-category/{category_name}"); - self.do_request(Method::DELETE, &path, None, None::<&String>) + let resource_obj = ResourceVersion { version }; + self.do_request::(Method::DELETE, &path, None, Some(&resource_obj)) .await + .map(|_| ()) } /// Sets given pool category as default. All new pools created manually or automatically will @@ -1353,15 +1441,32 @@ impl<'a> SzurubooruRequest<'a> { pub async fn upload_temporary_file( &self, file: &mut File, + file_name: impl AsRef, ) -> SzurubooruResult { let request = self.prep_request(Method::POST, "/uploads", None); - let content_part = self.part_from_file(file)?; + let content_part = self + .part_from_file(file)? + .file_name(file_name.as_ref().to_string()); let form = Form::new().part("content", content_part); self.handle_request(request.multipart(form)).await } + + /// Puts a file from a given file path in temporary storage and assigns it a token that can be + /// used in other requests. + /// The files uploaded that way are deleted after a short while so clients shouldn't use it + /// as a free upload service. + pub async fn upload_temporary_file_from_path( + &self, + file_path: impl AsRef, + ) -> SzurubooruResult { + let mut file = File::open(&file_path).map_err(SzurubooruClientError::IOError)?; + let filename = file_path.as_ref().file_name().unwrap().to_str().unwrap(); + + self.upload_temporary_file(&mut file, filename).await + } } /// Which kind of authentication is used. Automatically hides any sensitive information when printed diff --git a/szurubooru-client/src/errors.rs b/szurubooru-client/src/errors.rs index 6670beb..2b01581 100644 --- a/szurubooru-client/src/errors.rs +++ b/szurubooru-client/src/errors.rs @@ -77,6 +77,8 @@ impl IntoClientResult for SzuruEither { #[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] /// An error type returned by the server pub enum SzurubooruServerErrorType { + /// Inavlid pool category color + InvalidPoolCategoryColorError, /// Missing required file MissingRequiredFileError, /// Missing required parameter diff --git a/szurubooru-client/src/models.rs b/szurubooru-client/src/models.rs index c1a0b3b..326f837 100644 --- a/szurubooru-client/src/models.rs +++ b/szurubooru-client/src/models.rs @@ -46,7 +46,7 @@ pub struct PagedSearchResult { pub results: Vec, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] /// A [tag resource](TagResource) stripped down to `names`, `category` and `usages` fields. pub struct MicroTagResource { /// The tag names and aliases @@ -219,7 +219,8 @@ pub struct TagSibling { pub occurrences: u32, } -#[derive(Debug, Clone, Serialize, Deserialize, AsRefStr)] +#[derive(Debug, Clone, Serialize, Deserialize, AsRefStr, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] /// The type of post pub enum PostType { /// Image post @@ -240,7 +241,8 @@ pub enum PostType { Webm, } -#[derive(Debug, Clone, Serialize, Deserialize, AsRefStr)] +#[derive(Debug, Clone, Serialize, Deserialize, AsRefStr, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] /// How SFW/NSFW the post is pub enum PostSafety { /// Post is SFW @@ -253,7 +255,7 @@ pub enum PostSafety { Unsafe, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] /// A post resource stripped down to `id` and `thumbnailUrl` fields. pub struct MicroPostResource { @@ -269,7 +271,7 @@ pub(crate) struct PostId { pub id: u32, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] /// A post resource pub struct PostResource { @@ -313,6 +315,8 @@ pub struct PostResource { pub user: Option, /// The collective score (+1/-1 rating) of the given post pub score: Option, + /// The user's score for this post + pub own_score: Option, /// Whether the authenticated user has given post in their favorites pub own_favorite: Option, /// How many tags the post is tagged with @@ -348,30 +352,38 @@ pub struct PostResource { /// A `struct` used to create or update a post. For updating purposes /// the [version](CreateUpdatePost::version) field is required pub struct CreateUpdatePost { + #[serde(skip_serializing_if = "Option::is_none")] + /// Resource version. See [versioning](ResourceVersion) + #[builder(default)] + pub version: Option, /// Tags to use for this post. If specified tags do not exist yet, they will be automatically /// created. Tags created automatically have no implications, no suggestions, one name and /// their category is set to the first tag category found + #[builder(default)] pub tags: Vec, /// Required field, represents the SFW/NSFW state of a post pub safety: PostSafety, /// The origin of the post's content + #[builder(default)] pub source: Option, /// The IDs of related posts - pub relations: Option>, + #[builder(default)] + pub relations: Vec, /// Notes to be displayed on the post - pub notes: Option>, + #[builder(default)] + pub notes: Vec, /// Flags relevant to the post. If omitted they will be auto-detected - pub flags: Option>, + #[builder(default)] + pub flags: Vec, #[serde(skip_serializing_if = "Option::is_none")] /// The URL to download the content from + #[builder(default)] pub content_url: Option, #[serde(skip_serializing_if = "Option::is_none")] /// The token returned from /// [upload_temporary_file](crate::SzurubooruRequest::upload_temporary_file) + #[builder(default)] pub content_token: Option, - #[serde(skip_serializing_if = "Option::is_none")] - /// Resource version. See [versioning](ResourceVersion) - pub version: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -379,7 +391,7 @@ pub struct CreateUpdatePost { /// A token representing a temporary file upload pub struct TemporaryFileUpload { /// Temporary upload token - token: String, + pub token: String, } #[derive(Debug, Clone, Serialize, Deserialize, Builder)] @@ -409,7 +421,7 @@ pub struct RateResource { pub score: i8, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] /// A text annotation rendered on top of the post pub struct NoteResource { @@ -516,7 +528,7 @@ pub struct CreateUpdateUser { pub avatar_style: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] /// A user resource stripped down to `name` and `avatarUrl` fields pub struct MicroUserResource { @@ -663,17 +675,20 @@ pub struct PoolCategoryResource { /// ``` pub struct CreateUpdatePoolCategory { #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default)] /// Category version (used for updating) pub version: Option, /// Category name #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default)] pub name: Option, /// Category color #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default)] pub color: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] /// Type that represents a Pool resource pub struct PoolResource { @@ -771,7 +786,7 @@ pub struct MicroPoolResource { pub description: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] /// A type representing a Comment on a post pub struct CommentResource { diff --git a/szurubooru-integration-test/folly1.jpg b/szurubooru-integration-test/folly1.jpg new file mode 100644 index 0000000..ff83e59 Binary files /dev/null and b/szurubooru-integration-test/folly1.jpg differ diff --git a/szurubooru-integration-test/folly2.jpg b/szurubooru-integration-test/folly2.jpg new file mode 100644 index 0000000..c01f08d Binary files /dev/null and b/szurubooru-integration-test/folly2.jpg differ diff --git a/szurubooru-integration-test/folly3.jpg b/szurubooru-integration-test/folly3.jpg new file mode 100644 index 0000000..a5d0609 Binary files /dev/null and b/szurubooru-integration-test/folly3.jpg differ diff --git a/szurubooru-integration-test/folly3_thumb.jpg b/szurubooru-integration-test/folly3_thumb.jpg new file mode 100644 index 0000000..96e52bf Binary files /dev/null and b/szurubooru-integration-test/folly3_thumb.jpg differ diff --git a/szurubooru-integration-test/folly4.jpg b/szurubooru-integration-test/folly4.jpg new file mode 100644 index 0000000..08ca156 Binary files /dev/null and b/szurubooru-integration-test/folly4.jpg differ diff --git a/szurubooru-integration-test/src/main.rs b/szurubooru-integration-test/src/main.rs index ce26051..f40c795 100644 --- a/szurubooru-integration-test/src/main.rs +++ b/szurubooru-integration-test/src/main.rs @@ -1,13 +1,15 @@ use std::error::Error; +use std::fs::File; +use std::path::Path; use std::process::Stdio; use std::time::Duration; use szurubooru_client::models::*; +use szurubooru_client::tokens::QueryToken; use szurubooru_client::*; use tokio::process::Command; use tracing::level_filters::LevelFilter; -use tracing::{error, info, instrument}; +use tracing::{info, instrument}; use tracing_subscriber::filter::EnvFilter; -use tracing_subscriber::fmt::format::FmtSpan; #[tokio::main] #[tracing::instrument] @@ -48,6 +50,8 @@ async fn main() -> Result<(), Box> { test_tag_categories(&auth_client).await; test_tags(&auth_client).await; + test_creating_posts(&auth_client).await; + test_pool_categories(&auth_client).await; Command::new("sh") .current_dir(env!("CARGO_MANIFEST_DIR")) @@ -283,4 +287,289 @@ async fn test_tags(client: &SzurubooruClient) { .expect("Could not delete tag"); } -//TODO: test Tag siblings once we've tested a post +#[instrument(skip(client))] +async fn test_creating_posts(client: &SzurubooruClient) { + info!("Testing creating posts"); + + info!("Listing posts (should be empty)"); + let post_list = client + .request() + .list_posts(None) + .await + .expect("Could not list posts"); + assert_eq!(post_list.total, 0); + + info!("Testing upload by URL"); + let wiki_post_obj = CreateUpdatePostBuilder::default() + .content_url("https://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/Maine_Coon_cat_by_Tomitheos.JPG/225px-Maine_Coon_cat_by_Tomitheos.JPG".to_string()) + .tags(vec!["maine_coon".to_string(), "cat".to_string()]) + .safety(PostSafety::Safe) + .build() + .expect("Could not build wiki post object"); + let wiki_post = client + .request() + .create_post_from_url(&wiki_post_obj) + .await + .expect("Unable to create wiki post object"); + + info!("Updating existing post"); + let wiki_post_update = CreateUpdatePostBuilder::default() + .version(wiki_post.version.unwrap()) + .safety(wiki_post.safety.unwrap()) + .source("Wikipedia".to_string()) + .build() + .expect("Could not build wiki post update object"); + let wiki_post = client + .request() + .update_post(wiki_post.id.unwrap(), &wiki_post_update) + .await + .expect("Unable to up wiki post object"); + let post_list = client + .request() + .list_posts(None) + .await + .expect("Could not list posts"); + assert_eq!(post_list.total, 1); + + info!("Deleting wikipedia image"); + client + .request() + .delete_post(wiki_post.id.unwrap(), wiki_post.version.unwrap()) + .await + .expect("Could not delete wiki post"); + + info!("Test upload by File type"); + let folly1_obj = CreateUpdatePostBuilder::default() + .tags(vec![ + "maine_coon".to_string(), + "cat".to_string(), + "folly1".to_string(), + ]) + .safety(PostSafety::Safe) + .build() + .expect("Could not build first upload object"); + let folly1_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("folly1.jpg"); + let mut folly1_file = + File::open(&folly1_path).expect(&format!("Could not open file {folly1_path:?}")); + let folly1_post = client + .request() + .create_post_from_file(&mut folly1_file, None, "folly1.jpg", &folly1_obj) + .await + .expect("Could not create post from folly1 file"); + + info!("Test upload by file path"); + let folly2_obj = CreateUpdatePostBuilder::default() + .tags(vec![ + "maine_coon".to_string(), + "cat".to_string(), + "folly2".to_string(), + ]) + .safety(PostSafety::Safe) + .build() + .expect("Could not build second upload object"); + let folly2_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("folly2.jpg"); + let folly2_post = client + .request() + .create_post_from_file_path(folly2_path, None::, &folly2_obj) + .await + .expect("Could not create post from folly2 path"); + + info!("Test upload by file path with thumbnail"); + let folly3_obj = CreateUpdatePostBuilder::default() + .tags(vec![ + "maine_coon".to_string(), + "cat".to_string(), + "folly3".to_string(), + ]) + .safety(PostSafety::Safe) + .build() + .expect("Could not build third upload object"); + let folly3_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("folly3.jpg"); + let folly3_thumbnail = Path::new(env!("CARGO_MANIFEST_DIR")).join("folly3_thumb.jpg"); + let folly3_post = client + .request() + .create_post_from_file_path(folly3_path, Some(folly3_thumbnail), &folly3_obj) + .await + .expect("Could not create post with thumbnail"); + + info!("Testing temporary upload"); + let folly4_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("folly4.jpg"); + let folly4_temp_upload = client + .request() + .upload_temporary_file_from_path(folly4_path) + .await + .expect("Could not create temporary upload for folly4"); + let folly4_obj = CreateUpdatePostBuilder::default() + .tags(vec![ + "maine_coon".to_string(), + "cat".to_string(), + "folly4".to_string(), + ]) + .content_token(folly4_temp_upload.token) + .safety(PostSafety::Safe) + .build() + .expect("Could not build fourth upload object"); + let folly4_post = client + .request() + .create_post_from_token(&folly4_obj) + .await + .expect("Could not create upload from temporary token"); + + info!("Querying by tag"); + let f4_results = client + .request() + .list_posts(Some(&vec![QueryToken::anonymous("cat")])) + .await + .expect("Could not list posts by tag cat"); + assert_eq!(f4_results.total, 4); + + info!("Testing pagination"); + let post_list = client + .request() + .with_limit(1) + .list_posts(None) + .await + .expect("Could not list posts page 1"); + assert_eq!(post_list.results.len(), 1); + let post_list2 = client + .request() + .with_limit(1) + .with_offset(1) + .list_posts(None) + .await + .expect("Could not list posts page 2"); + assert_ne!(post_list.results, post_list2.results); + + info!("Testing tag siblings"); + let tag_occurrences = client + .request() + .get_tag_siblings("maine_coon") + .await + .expect("Could not fetch tag siblings"); + let occurrences_filtered = tag_occurrences + .results + .iter() + .filter(|oc| { + oc.tag + .names + .as_ref() + .map(|names| names.contains(&"cat".to_string())) + .unwrap_or(false) + }) + .count(); + assert_eq!(occurrences_filtered, 1); + + info!("Rating post"); + let folly3_post = client + .request() + .rate_post(folly3_post.id.unwrap(), 1) + .await + .expect("Could not rate post"); + assert_eq!(folly3_post.own_score, Some(1)); + + info!("Favoriting post"); + let folly3_post = client + .request() + .favorite_post(folly3_post.id.unwrap()) + .await + .expect("Could not favorite post"); + assert_eq!(folly3_post.own_favorite, Some(true)); + + info!("Unfavorite post"); + let folly3_post = client + .request() + .unfavorite_post(folly3_post.id.unwrap()) + .await + .expect("Could not unfavorite post"); + assert_eq!(folly3_post.own_favorite, Some(false)); + + info!("Featuring post"); + let featured_post = client + .request() + .get_featured_post() + .await + .expect("Could not get featured post"); + assert!(featured_post.is_none()); + + client + .request() + .set_featured_post(folly4_post.id.unwrap()) + .await + .expect("Could not set featured post"); + + let featured_post = client + .request() + .get_featured_post() + .await + .expect("Could not get featured post"); + assert!(featured_post.is_some()); +} + +#[instrument(skip(client))] +async fn test_pool_categories(client: &SzurubooruClient) { + info!("Testing pool categories"); + + info!("Listing pool categories"); + let pool_cats = client + .request() + .list_pool_categories() + .await + .expect("Could not list pool categories"); + assert!(!pool_cats.results.is_empty()); + + info!("Creating pool category"); + let create_cat = CreateUpdatePoolCategoryBuilder::default() + .name("cat_pool_category".to_string()) + .color("purple".to_string()) + .build() + .expect("Could not build pool category object"); + let pool_cat = client + .request() + .create_pool_category(&create_cat) + .await + .expect("Could not create pool category"); + + let create_dog_cat = CreateUpdatePoolCategoryBuilder::default() + .name("dog_category".to_string()) + .color("orange".to_string()) + .build() + .expect("Could not build pool category object"); + let dog_pool_cat = client + .request() + .create_pool_category(&create_dog_cat) + .await + .expect("Could not create pool category"); + + info!("Updating pool category"); + let update_cat = CreateUpdatePoolCategoryBuilder::default() + .version(pool_cat.version.unwrap()) + .color("white".to_string()) + .build() + .expect("Could not build pool category update"); + let pool_cat = client + .request() + .update_pool_category(pool_cat.name.unwrap(), &update_cat) + .await + .expect("Could not update pool category"); + + info!("Getting pool category"); + let pool_cat = client + .request() + .get_pool_category(pool_cat.name.unwrap()) + .await + .expect("Could not get pool category"); + + info!("Deleting pool category"); + client + .request() + .delete_pool_category(dog_pool_cat.name.unwrap(), dog_pool_cat.version.unwrap()) + .await + .expect("Could not delete pool category"); + + info!("Setting default pool category"); + let pool_cat = client + .request() + .set_default_pool_category(pool_cat.name.unwrap()) + .await + .expect("Could not set default pool category"); +}