blockpalettes_client/
lib.rs

1//! A client for the Block Palettes API.
2//!
3//! This crate provides a convenient asynchronous client for interacting with the
4//! [Block Palettes](https://www.blockpalettes.com) website's API. It allows you to
5//! search for palettes, retrieve popular blocks, get detailed information about
6//! specific palettes, and even scrape some information directly from the website's
7//! HTML pages.
8//!
9//! The client is built on top of `reqwest` for HTTP requests and `serde` for
10//! JSON serialization/deserialization. It also uses `chrono` for date parsing
11//! and `scraper` for HTML parsing when scraping.
12//!
13//! # Features
14//!
15//! - Search for palettes based on contained blocks.
16//! - Retrieve lists of popular blocks.
17//! - Fetch detailed information for individual palettes.
18//! - Get similar palettes based on a given palette ID.
19//! - Scrape palette page details (blocks and similar palette IDs) directly from HTML.
20//! - Robust error handling with custom error types.
21//!
22//! # Error Handling
23//!
24//! The crate defines a custom error type, [`BlockPalettesError`], which
25//! encapsulates various issues that can occur, such as HTTP errors, API-specific
26//! errors, HTML parsing failures, and invalid date formats.
27//!
28//! # Data Structures
29//!
30//! Key data structures like [`Palette`], [`PaletteDetails`], and [`PopularBlock`]
31//! are provided to represent the API responses.
32
33use chrono::NaiveDateTime;
34use reqwest::Client;
35use scraper::{Html, Selector};
36use serde::{Deserialize, Serialize};
37use std::collections::HashSet;
38use thiserror::Error;
39
40/// Represents the possible errors that can occur when interacting with the
41/// Block Palettes API.
42#[derive(Debug, Error)]
43pub enum BlockPalettesError {
44    /// An HTTP request failed, typically due to network issues, DNS resolution,
45    /// or invalid URLs.
46    ///
47    /// This error wraps the underlying `reqwest::Error`.
48    #[error("HTTP request failed: {0}")]
49    Http(#[from] reqwest::Error),
50    /// The Block Palettes API returned an error message or indicated a failure
51    /// in its response.
52    ///
53    /// The contained `String` provides more details about the API-specific error.
54    #[error("API error: {0}")]
55    Api(String),
56    /// An error occurred during the parsing of HTML content, typically when
57    /// scraping a palette page.
58    ///
59    /// This can happen if the HTML structure changes unexpectedly.
60    #[error("HTML parsing error")]
61    HtmlParse,
62    /// The date string received from the API could not be parsed into a
63    /// `NaiveDateTime` object.
64    ///
65    /// This usually indicates an unexpected date format from the API.
66    #[error("Invalid date format")]
67    InvalidDateFormat,
68}
69
70/// A specialized `Result` type for Block Palettes operations.
71///
72/// This type is a convenience alias for `std::result::Result<T, BlockPalettesError>`.
73pub type Result<T, E = BlockPalettesError> = std::result::Result<T, E>;
74
75/// An asynchronous client for the Block Palettes API.
76///
77/// This struct provides methods to interact with various endpoints of the
78/// Block Palettes API, allowing you to search for palettes, retrieve block
79/// information, and get palette details.
80///
81/// # Examples
82///
83/// ```rust
84/// use blockpalettes_client::{BlockPalettesClient, SortOrder};
85///
86/// #[tokio::main]
87/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
88///     let client = BlockPalettesClient::new(reqwest::Client::new());
89///     let popular_blocks = client.popular_blocks().await?;
90///     println!("First popular block: {}", popular_blocks[0].name);
91///     Ok(())
92/// }
93/// ```
94#[derive(Debug, Clone)]
95pub struct BlockPalettesClient<'a> {
96    client: Client,
97    base_url: &'a str,
98}
99
100impl<'a> BlockPalettesClient<'a> {
101    /// Creates a new [`BlockPalettesClient`] instance.
102    ///
103    /// # Arguments
104    ///
105    /// * `client` - An instance of `reqwest::Client` to use for making HTTP requests.
106    ///
107    /// # Returns
108    ///
109    /// A new `BlockPalettesClient` configured to use the provided `reqwest::Client`
110    /// and the default base URL (`https://www.blockpalettes.com`).
111    ///
112    /// # Examples
113    ///
114    /// ```rust
115    /// use blockpalettes_client::BlockPalettesClient;
116    ///
117    /// let reqwest_client = reqwest::Client::new();
118    /// let bp_client = BlockPalettesClient::new(reqwest_client);
119    /// ```
120    pub const fn new(client: Client) -> Self {
121        Self {
122            client,
123            base_url: "https://www.blockpalettes.com",
124        }
125    }
126
127    /// Searches for blocks that match a given query string.
128    ///
129    /// This method queries the `/api/palettes/search-block.php` endpoint.
130    ///
131    /// # Arguments
132    ///
133    /// * `query` - The search string for blocks (e.g., "stone", "wood").
134    ///
135    /// # Returns
136    ///
137    /// A `Result` containing a `Vec<String>` of block names if successful,
138    /// or a [`BlockPalettesError`] if the request fails or the API returns an error.
139    ///
140    /// [`BlockPalettesError`]: enum.BlockPalettesError.html
141    ///
142    /// # Examples
143    ///
144    /// ```rust
145    /// use blockpalettes_client::BlockPalettesClient;
146    ///
147    /// #[tokio::main]
148    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
149    ///     let client = BlockPalettesClient::new(reqwest::Client::new());
150    ///     let blocks = client.search_blocks("dirt").await?;
151    ///     println!("Found blocks: {:?}", blocks);
152    ///     Ok(())
153    /// }
154    /// ```
155    pub async fn search_blocks(&self, query: impl AsRef<str>) -> Result<Vec<String>> {
156        let url = format!("{}/api/palettes/search-block.php", self.base_url);
157        let response = self
158            .client
159            .get(&url)
160            .query(&[("query", query.as_ref())])
161            .send()
162            .await?
163            .json::<BlockSearchResponse>()
164            .await?;
165
166        if response.success {
167            Ok(response.blocks)
168        } else {
169            Err(BlockPalettesError::Api("Search failed".into()))
170        }
171    }
172
173    /// Retrieves a list of popular blocks.
174    ///
175    /// This method queries the `/api/palettes/popular-blocks.php` endpoint.
176    ///
177    /// # Returns
178    ///
179    /// A `Result` containing a `Vec<PopularBlock>` if successful,
180    /// or a [`BlockPalettesError`] if the request fails or the API returns an error.
181    ///
182    /// [`PopularBlock`]: struct.PopularBlock.html
183    /// [`BlockPalettesError`]: enum.BlockPalettesError.html
184    ///
185    /// # Examples
186    ///
187    /// ```rust
188    /// use blockpalettes_client::BlockPalettesClient;
189    ///
190    /// #[tokio::main]
191    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
192    ///     let client = BlockPalettesClient::new(reqwest::Client::new());
193    ///     let popular_blocks = client.popular_blocks().await?;
194    ///     for block in popular_blocks.iter().take(3) {
195    ///         println!("Block: {}, Count: {}", block.name, block.count);
196    ///     }
197    ///     Ok(())
198    /// }
199    /// ```
200    pub async fn popular_blocks(&self) -> Result<Vec<PopularBlock>> {
201        let url = format!("{}/api/palettes/popular-blocks.php", self.base_url);
202        let response = self
203            .client
204            .get(&url)
205            .send()
206            .await?
207            .json::<PopularBlocksResponse>()
208            .await?;
209
210        if response.success {
211            Ok(response.blocks)
212        } else {
213            Err(BlockPalettesError::Api(
214                "Popular blocks request failed".into(),
215            ))
216        }
217    }
218
219    /// Retrieves a list of palettes based on specified blocks, sorting order,
220    /// pagination, and limit.
221    ///
222    /// This method queries the `/api/palettes/all_palettes.php` endpoint.
223    /// It internally filters the results to ensure all specified blocks are present
224    /// in the returned palettes.
225    ///
226    /// # Arguments
227    ///
228    /// * `blocks` - A slice of string references representing the blocks that
229    ///   must be present in the palettes.
230    /// * `sort` - The desired sorting order for the palettes (e.g., `SortOrder::Recent`).
231    /// * `page` - The page number of the results to retrieve (1-indexed).
232    /// * `limit` - The maximum number of palettes to return per page.
233    ///
234    /// # Returns
235    ///
236    /// A `Result` containing a [`PaletteResponse`] if successful,
237    /// or a [`BlockPalettesError`] if the request fails or the API returns an error.
238    ///
239    /// # Examples
240    ///
241    /// ```rust
242    /// use blockpalettes_client::{BlockPalettesClient, SortOrder};
243    ///
244    /// #[tokio::main]
245    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
246    ///     let client = BlockPalettesClient::new(reqwest::Client::new());
247    ///     let blocks_to_search = &["oak_log", "dirt"];
248    ///     let response = client
249    ///         .get_palettes(blocks_to_search, SortOrder::Popular, 1, 5)
250    ///         .await?;
251    ///
252    ///     if let Some(palettes) = response.palettes {
253    ///         println!("Found {} popular palettes containing oak_log and dirt:", palettes.len());
254    ///         for palette in palettes {
255    ///             println!("- ID: {}, Name: {:?}", palette.id, palette.name());
256    ///         }
257    ///     }
258    ///     Ok(())
259    /// }
260    /// ```
261    pub async fn get_palettes(
262        &self,
263        blocks: &[&str],
264        sort: SortOrder,
265        page: u32,
266        limit: u32,
267    ) -> Result<PaletteResponse> {
268        let url = format!("{}/api/palettes/all_palettes.php", self.base_url);
269
270        let mut all_palettes = Vec::new();
271        let mut total_results = 0;
272        let mut total_pages = 0;
273
274        for &block in blocks {
275            let response = self
276                .client
277                .get(&url)
278                .query(&[
279                    ("sort", sort.to_string()),
280                    ("page", page.to_string()),
281                    ("limit", limit.to_string()),
282                    ("blocks", block.to_string()),
283                ])
284                .send()
285                .await?
286                .json::<PaletteResponse>()
287                .await?;
288
289            if total_results == 0 {
290                total_results = response.total_results;
291                total_pages = response.total_pages;
292            }
293
294            if let Some(mut ps) = response.palettes {
295                all_palettes.append(&mut ps);
296            }
297        }
298
299        // filter the collected palettes to ensure they contain ALL specified blocks
300        let filtered = all_palettes
301            .into_iter()
302            .filter(|p| p.contains_all_blocks(blocks))
303            .collect();
304
305        Ok(PaletteResponse {
306            success: true,
307            palettes: Some(filtered),
308            total_results,
309            total_pages,
310        })
311    }
312
313    /// Retrieves detailed information for a single palette by its ID.
314    ///
315    /// This method queries the `/api/palettes/single_palette.php` endpoint.
316    ///
317    /// # Arguments
318    ///
319    /// * `id` - The unique identifier of the palette.
320    ///
321    /// # Returns
322    ///
323    /// A `Result` containing a [`PaletteDetails`] if successful,
324    /// or a [`BlockPalettesError`] if the request fails, the API returns an error
325    /// (e.g., palette not found), or the response cannot be deserialized.
326    ///
327    /// # Examples
328    ///
329    /// ```rust
330    /// use blockpalettes_client::BlockPalettesClient;
331    ///
332    /// #[tokio::main]
333    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
334    ///     let client = BlockPalettesClient::new(reqwest::Client::new());
335    ///     let palette_id = 12345; // Replace with an actual palette ID
336    ///     match client.get_palette_details(palette_id).await {
337    ///         Ok(details) => println!("Palette ID {}: Username {}", details.id, details.username),
338    ///         Err(e) => eprintln!("Failed to get palette details: {}", e),
339    ///     }
340    ///     Ok(())
341    /// }
342    /// ```
343    pub async fn get_palette_details(&self, id: u64) -> Result<PaletteDetails> {
344        let url = format!("{}/api/palettes/single_palette.php", self.base_url);
345        let response = self
346            .client
347            .get(&url)
348            .query(&[("id", id.to_string())])
349            .send()
350            .await?
351            .json::<SinglePaletteResponse>()
352            .await?;
353
354        if response.success {
355            Ok(response.palette)
356        } else {
357            Err(BlockPalettesError::Api("Palette not found".into()))
358        }
359    }
360
361    /// Retrieves a list of palettes similar to a given palette ID.
362    ///
363    /// This method queries the `/api/palettes/similar_palettes.php` endpoint.
364    ///
365    /// # Arguments
366    ///
367    /// * `palette_id` - The ID of the reference palette to find similar ones.
368    ///
369    /// # Returns
370    ///
371    /// A `Result` containing a `Vec<Palette>` of similar palettes if successful,
372    /// or a [`BlockPalettesError`] if the request fails, the API returns an error,
373    /// or the response cannot be deserialized.
374    ///
375    /// # Examples
376    ///
377    /// ```rust
378    /// use blockpalettes_client::BlockPalettesClient;
379    ///
380    /// #[tokio::main]
381    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
382    ///     let client = BlockPalettesClient::new(reqwest::Client::new());
383    ///     let reference_palette_id = 56655; // Replace with an actual palette ID
384    ///     let similar_palettes = client.get_similar_palettes(reference_palette_id).await?;
385    ///     println!("Found {} similar palettes for ID {}:", similar_palettes.len(), reference_palette_id);
386    ///     for palette in similar_palettes.iter().take(3) {
387    ///         println!("- ID: {}, Name: {:?}", palette.id, palette.name());
388    ///     }
389    ///     Ok(())
390    /// }
391    /// ```
392    pub async fn get_similar_palettes(&self, palette_id: u64) -> Result<Vec<Palette>> {
393        let url = format!("{}/api/palettes/similar_palettes.php", self.base_url);
394        let response = self
395            .client
396            .get(&url)
397            .query(&[("palette_id", palette_id.to_string())])
398            .send()
399            .await?
400            .json::<SimilarPalettesResponse>()
401            .await?;
402
403        if response.success {
404            Ok(response.palettes)
405        } else {
406            Err(BlockPalettesError::Api("Similar palettes not found".into()))
407        }
408    }
409
410    /// Scrapes details directly from a Block Palettes HTML page for a given palette ID.
411    ///
412    /// This method is useful for extracting information that might not be available
413    /// directly through the public API endpoints, such as the full list of blocks
414    /// displayed on the page or IDs of similar palettes linked on the page.
415    ///
416    /// # Arguments
417    ///
418    /// * `palette_id` - The ID of the palette whose page details are to be scraped.
419    ///
420    /// # Returns
421    ///
422    /// A `Result` containing a [`PalettePageDetails`] if successful,
423    /// or a [`BlockPalettesError`] if the request fails, HTML parsing fails,
424    /// or expected elements are not found.
425    ///
426    /// # Caveats
427    ///
428    /// This method relies on the specific HTML structure of `blockpalettes.com`.
429    /// Any changes to the website's front-end might break this scraping functionality.
430    ///
431    /// # Examples
432    ///
433    /// ```rust
434    /// use blockpalettes_client::BlockPalettesClient;
435    ///
436    /// #[tokio::main]
437    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
438    ///     let client = BlockPalettesClient::new(reqwest::Client::new());
439    ///     let palette_id = 12345; // Replace with an actual palette ID
440    ///     match client.scrape_palette_page(palette_id).await {
441    ///         Ok(details) => {
442    ///             println!("Scraped blocks for palette {}: {:?}", palette_id, details.blocks);
443    ///             println!("Similar palette IDs: {:?}", details.similar_palette_ids);
444    ///         },
445    ///         Err(e) => eprintln!("Failed to scrape palette page: {}", e),
446    ///     }
447    ///     Ok(())
448    /// }
449    /// ```
450    pub async fn scrape_palette_page(&self, palette_id: u64) -> Result<PalettePageDetails> {
451        let url = format!("{}/palette/{}", self.base_url, palette_id);
452        let html = self.client.get(&url).send().await?.text().await?;
453
454        let document = Html::parse_document(&html);
455
456        // extract palette blocks
457        let block_selector =
458            Selector::parse(".single-block").map_err(|_| BlockPalettesError::HtmlParse)?;
459        let mut blocks = Vec::new();
460
461        for element in document.select(&block_selector) {
462            if let Some(block_name) = element.text().last() {
463                blocks.push(block_name.trim().to_string());
464            }
465        }
466
467        // extract similar palettes if available
468        let similar_selector =
469            Selector::parse(".palette-card").map_err(|_| BlockPalettesError::HtmlParse)?;
470        let mut similar = Vec::new();
471
472        for element in document.select(&similar_selector) {
473            if let Some(id) = element
474                .value()
475                .attr("href")
476                .and_then(|href| href.split('/').next_back())
477                .and_then(|id| id.parse::<u64>().ok())
478            {
479                similar.push(id);
480            }
481        }
482
483        Ok(PalettePageDetails {
484            blocks,
485            similar_palette_ids: similar,
486        })
487    }
488}
489
490/// Represents the different sorting orders available for retrieving palettes.
491///
492/// These variants correspond to the `sort` parameter in the Block Palettes API.
493#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
494#[serde(rename_all = "snake_case")]
495pub enum SortOrder {
496    /// Sort by the most recently added palettes.
497    Recent,
498    /// Sort by the most popular palettes.
499    Popular,
500    /// Sort by the oldest palettes.
501    Oldest,
502    /// Sort by trending palettes.
503    Trending,
504}
505
506impl std::fmt::Display for SortOrder {
507    /// Converts the `SortOrder` enum variant into its corresponding API string representation.
508    ///
509    /// # Returns
510    ///
511    /// A `String` slice representing the sort order (e.g., "recent", "popular").
512    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
513        match self {
514            SortOrder::Recent => write!(f, "recent"),
515            SortOrder::Popular => write!(f, "popular"),
516            SortOrder::Oldest => write!(f, "oldest"),
517            SortOrder::Trending => write!(f, "trending"),
518        }
519    }
520}
521
522/// Internal struct for deserializing the response from the `/api/palettes/search-block.php` endpoint.
523#[derive(Debug, Deserialize)]
524struct BlockSearchResponse {
525    success: bool,
526    blocks: Vec<String>,
527}
528
529/// Internal struct for deserializing the response from the `/api/palettes/popular-blocks.php` endpoint.
530#[derive(Debug, Deserialize)]
531struct PopularBlocksResponse {
532    success: bool,
533    blocks: Vec<PopularBlock>,
534}
535
536/// Internal struct for deserializing the response from the `/api/palettes/single_palette.php` endpoint.
537#[derive(Debug, Deserialize)]
538struct SinglePaletteResponse {
539    success: bool,
540    palette: PaletteDetails,
541}
542
543/// Internal struct for deserializing the response from the `/api/palettes/similar_palettes.php` endpoint.
544#[derive(Debug, Deserialize)]
545struct SimilarPalettesResponse {
546    success: bool,
547    palettes: Vec<Palette>,
548}
549
550/// Represents a popular block returned by the API.
551#[derive(Debug, Deserialize, Serialize)]
552pub struct PopularBlock {
553    /// The name of the block (e.g., "stone", "dirt").
554    #[serde(rename = "block")]
555    pub name: String,
556    /// The number of palettes this block appears in.
557    pub count: u32,
558}
559
560/// Represents the response structure when fetching a list of palettes.
561#[derive(Debug, Deserialize, Serialize)]
562pub struct PaletteResponse {
563    /// Indicates if the API request was successful.
564    pub success: bool,
565    /// The total number of results found for the query.
566    pub total_results: u32,
567    /// The total number of pages available for the query.
568    pub total_pages: u32,
569    /// An optional vector of [`Palette`] objects. It will be `None` if no palettes were found.
570    pub palettes: Option<Vec<Palette>>,
571}
572
573/// Represents a single palette returned by the Block Palettes API.
574///
575/// This struct contains core information about a palette, including its ID,
576/// associated blocks, likes, and creation date.
577#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
578pub struct Palette {
579    /// The unique identifier for the palette.
580    pub id: u64,
581    /// The ID of the user who created the palette.
582    pub user_id: u64,
583    /// The creation date of the palette as a string (e.g., "YYYY-MM-DD HH:MM:SS").
584    pub date: String,
585    /// The number of likes the palette has received.
586    pub likes: u32,
587    /// The first block in the palette.
588    #[serde(rename = "blockOne")]
589    pub block_one: String,
590    /// The second block in the palette.
591    #[serde(rename = "blockTwo")]
592    pub block_two: String,
593    /// The third block in the palette.
594    #[serde(rename = "blockThree")]
595    pub block_three: String,
596    /// The fourth block in the palette.
597    #[serde(rename = "blockFour")]
598    pub block_four: String,
599    /// The fifth block in the palette.
600    #[serde(rename = "blockFive")]
601    pub block_five: String,
602    /// The sixth block in the palette.
603    #[serde(rename = "blockSix")]
604    pub block_six: String,
605    /// A flag indicating if the palette is hidden (0 for not hidden, 1 for hidden).
606    #[serde(default)]
607    pub hidden: Option<u8>,
608    /// A flag indicating if the palette is featured (0 for not featured, 1 for featured).
609    #[serde(default)]
610    pub featured: Option<u8>,
611    /// An optional hash associated with the palette.
612    pub hash: Option<String>,
613    /// A human-readable string indicating how long ago the palette was created (e.g., "2 days ago").
614    pub time_ago: String,
615}
616
617impl Palette {
618    /// Returns a vector containing all six block names from the palette.
619    ///
620    /// This is a convenience method to access all blocks without individually
621    /// referencing `block_one`, `block_two`, etc.
622    ///
623    /// # Returns
624    ///
625    /// A `Vec<String>` containing the names of the six blocks in the palette.
626    ///
627    /// # Examples
628    ///
629    /// ```rust
630    /// # use blockpalettes_client::Palette;
631    /// # let palette = Palette {
632    /// #    id: 1, user_id: 1, date: "2023-01-01 12:00:00".to_string(), likes: 10,
633    /// #    block_one: "stone".to_string(), block_two: "dirt".to_string(),
634    /// #    block_three: "grass_block".to_string(), block_four: "oak_log".to_string(),
635    /// #    block_five: "cobblestone".to_string(), block_six: "sand".to_string(),
636    /// #    hidden: Some(0), featured: Some(0), hash: None, time_ago: "1 day ago".to_string()
637    /// # };
638    /// let blocks = palette.name();
639    /// assert_eq!(blocks.len(), 6);
640    /// assert!(blocks.contains(&"stone".to_string()));
641    /// ```
642    pub fn name(&self) -> Vec<String> {
643        vec![
644            self.block_one.clone(),
645            self.block_two.clone(),
646            self.block_three.clone(),
647            self.block_four.clone(),
648            self.block_five.clone(),
649            self.block_six.clone(),
650        ]
651    }
652
653    /// Checks if the palette contains all the specified blocks.
654    ///
655    /// This method is useful for client-side filtering of palettes.
656    ///
657    /// # Arguments
658    ///
659    /// * `blocks` - A slice of string references, where each string is a block name
660    ///   to check for.
661    ///
662    /// # Returns
663    ///
664    /// `true` if the palette contains all blocks specified in the `blocks` slice,
665    /// `false` otherwise. The comparison is case-sensitive.
666    ///
667    /// # Examples
668    ///
669    /// ```rust
670    /// # use blockpalettes_client::Palette;
671    /// # let palette = Palette {
672    /// #    id: 1, user_id: 1, date: "2023-01-01 12:00:00".to_string(), likes: 10,
673    /// #    block_one: "stone".to_string(), block_two: "dirt".to_string(),
674    /// #    block_three: "grass_block".to_string(), block_four: "oak_log".to_string(),
675    /// #    block_five: "cobblestone".to_string(), block_six: "sand".to_string(),
676    /// #    hidden: Some(0), featured: Some(0), hash: None, time_ago: "1 day ago".to_string()
677    /// # };
678    /// assert!(palette.contains_all_blocks(&["stone", "dirt"]));
679    /// assert!(!palette.contains_all_blocks(&["stone", "diamond_block"]));
680    /// ```
681    pub fn contains_all_blocks(&self, blocks: &[&str]) -> bool {
682        let palette_blocks: HashSet<&str> = HashSet::from([
683            self.block_one.as_str(),
684            self.block_two.as_str(),
685            self.block_three.as_str(),
686            self.block_four.as_str(),
687            self.block_five.as_str(),
688            self.block_six.as_str(),
689        ]);
690
691        blocks.iter().all(|&b| palette_blocks.contains(b))
692    }
693
694    /// Parses the `date` string of the palette into a `NaiveDateTime` object.
695    ///
696    /// This provides a more structured way to work with the palette's creation date.
697    ///
698    /// # Returns
699    ///
700    /// A `Result` containing a `NaiveDateTime` if the date string is successfully parsed,
701    /// or a [`BlockPalettesError::InvalidDateFormat`] if the string does not match
702    /// the expected format ("YYYY-MM-DD HH:MM:SS").
703    ///
704    /// # Examples
705    ///
706    /// ```rust
707    /// # use blockpalettes_client::Palette;
708    /// # use chrono::{NaiveDate, Timelike};
709    /// # let palette = Palette {
710    /// #    id: 1, user_id: 1, date: "2023-01-01 12:30:00".to_string(), likes: 10,
711    /// #    block_one: "stone".to_string(), block_two: "dirt".to_string(),
712    /// #    block_three: "grass_block".to_string(), block_four: "oak_log".to_string(),
713    /// #    block_five: "cobblestone".to_string(), block_six: "sand".to_string(),
714    /// #    hidden: Some(0), featured: Some(0), hash: None, time_ago: "1 day ago".to_string()
715    /// # };
716    /// let datetime = palette.parse_date().unwrap();
717    /// assert_eq!(datetime.date(), NaiveDate::from_ymd_opt(2023, 1, 1).unwrap());
718    /// assert_eq!(datetime.hour(), 12);
719    /// ```
720    pub fn parse_date(&self) -> Result<NaiveDateTime> {
721        NaiveDateTime::parse_from_str(&self.date, "%Y-%m-%d %H:%M:%S")
722            .map_err(|_| BlockPalettesError::InvalidDateFormat)
723    }
724}
725
726/// Represents detailed information for a single palette, including the username.
727///
728/// This struct is typically returned by the [`BlockPalettesClient::get_palette_details`] method.
729/// It extends the basic [`Palette`] information with the `username` of the creator.
730#[derive(Debug, Deserialize, Serialize)]
731pub struct PaletteDetails {
732    /// The unique identifier for the palette.
733    pub id: u64,
734    /// The ID of the user who created the palette.
735    #[serde(rename = "user_id")]
736    pub user_id: u64,
737    /// The creation date of the palette as a string (e.g., "YYYY-MM-DD HH:MM:SS").
738    pub date: String,
739    /// The number of likes the palette has received.
740    pub likes: u32,
741    /// The first block in the palette.
742    #[serde(rename = "blockOne")]
743    pub block_one: String,
744    /// The second block in the palette.
745    #[serde(rename = "blockTwo")]
746    pub block_two: String,
747    /// The third block in the palette.
748    #[serde(rename = "blockThree")]
749    pub block_three: String,
750    /// The fourth block in the palette.
751    #[serde(rename = "blockFour")]
752    pub block_four: String,
753    /// The fifth block in the palette.
754    #[serde(rename = "blockFive")]
755    pub block_five: String,
756    /// The sixth block in the palette.
757    #[serde(rename = "blockSix")]
758    pub block_six: String,
759    /// A flag indicating if the palette is hidden (0 for not hidden, 1 for hidden).
760    pub hidden: u8,
761    /// A flag indicating if the palette is featured (0 for not featured, 1 for featured).
762    pub featured: u8,
763    /// The hash associated with the palette.
764    pub hash: String,
765    /// The username of the palette creator.
766    pub username: String,
767    /// A human-readable string indicating how long ago the palette was created (e.g., "2 days ago").
768    #[serde(rename = "time_ago")]
769    pub time_ago: String,
770}
771
772/// Represents details scraped directly from a palette's HTML page.
773///
774/// This struct is typically returned by the [`BlockPalettesClient::scrape_palette_page`] method.
775/// It contains information extracted by parsing the HTML, which might include
776/// blocks displayed on the page and IDs of similar palettes linked.
777///
778/// [`BlockPalettesClient::scrape_palette_page`]: struct.BlockPalettesClient.html#method.scrape_palette_page
779#[derive(Debug, Serialize)]
780pub struct PalettePageDetails {
781    /// A list of block names found on the palette's page.
782    pub blocks: Vec<String>,
783    /// A list of IDs of similar palettes linked on the page.
784    pub similar_palette_ids: Vec<u64>,
785}