Skip to content

Commit

Permalink
make it more statik
Browse files Browse the repository at this point in the history
  • Loading branch information
TOwInOK committed Oct 27, 2024
1 parent 148b12e commit 2024547
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 124 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "anime-grubber"
version = "0.0.5"
version = "0.0.6"
edition = "2021"
authors = ["TOwInOK <[email protected]>"]
description = "A convenient library for extracting images of cute characters from websites."
Expand All @@ -16,6 +16,7 @@ categories = ["api-bindings", "web-programming"]
async-trait = "0.1.83"
miniserde = "0.1.40"
reqwest = "0.12.8"
smallvec = "1.13.2"
thiserror = "1.0.64"
tracing = "0.1.40"

Expand Down
93 changes: 30 additions & 63 deletions src/agent.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::borrow::Cow;

use crate::result::Result;
use async_trait::async_trait;
#[async_trait]
Expand All @@ -8,80 +10,45 @@ use async_trait::async_trait;
/// non-blocking operations when retrieving images from a source.
///
/// # Methods
/// - `get`: Asynchronously retrieves a single image and returns its URL as a `String`.
/// - `get_many`: Asynchronously retrieves multiple images and returns their URLs as a `Vec<String>`.
/// - `get_random`: Asynchronously retrieves a random image and returns its URL as a `String`.
/// - `get`: Asynchronously retrieves a single image and returns its URL as a `Cow<'_, str>`.
/// - `get_many`: Asynchronously retrieves multiple images and returns their URLs as a `Vec<Cow<'_, str>>`.
/// - `get_random`: Asynchronously retrieves a random image and returns its URL as a `Cow<'_, str>`.
///
pub trait Agent {
/// Asynchronously fetches a single image based on the current category and aspect.
/// Retrieves a single image.
///
/// This method constructs a URL using the category and aspect generated from
/// the current state of the `Waifu` instance. It performs an HTTP GET request
/// to retrieve the image. If the image is not found, it returns an error.
/// Returns image URL as a string reference.
///
/// # Returns
/// Returns a `Result<String>`, containing the URL of the fetched image or an
/// error if the request fails.
/// # Errors
/// Returns error if image cannot be retrieved.
///
/// # Example
/// ```rust
/// use anime_grubber::agent::Agent;
/// use anime_grubber::agents::waifu_pics::{Waifu, Categories, SFW};
/// # Returns
/// - `Ok(Cow<'_, str>)` - URL of retrieved image
/// - `Err(Error)` - If retrieval fails
async fn get(&self) -> Result<ImageUrl<'_>>;
/// Retrieves multiple images.
///
/// #[tokio::main]
/// async fn main() {
/// let Waifu = Waifu::new(Categories::SFW(SFW::Dance));
/// let image_url = Waifu.get().await.expect("Failed to fetch image");
/// println!("Fetched image URL: {}", image_url);
/// }
/// ```
async fn get(&self) -> Result<String>;

/// Asynchronously fetches multiple images based on the current category and aspect.
/// Returns vector of image URLs.
///
/// This method constructs a URL using the category and aspect generated from
/// the current state of the `Waifu` instance. It performs an HTTP POST request
/// to retrieve an array of images. If the images are not found, it returns an error.
/// # Errors
/// Returns error if images cannot be retrieved.
///
/// # Returns
/// Returns a `Result<Vec<String>>`, containing a vector of URLs for the fetched
/// images or an error if the request fails.
/// - `Ok(Vec<Cow<'_, str>>)` - URLs of retrieved images
/// - `Err(Error)` - If retrieval fails
async fn get_many(&self) -> Result<ImageUrls<'_>>;
/// Retrieves a random image.
///
/// # Example
/// ```rust
/// use anime_grubber::agent::Agent;
/// use anime_grubber::agents::waifu_pics::{Waifu, Categories, SFW};
/// Returns URL of random image.
///
/// #[tokio::main]
/// async fn main() {
/// let Waifu = Waifu::new(Categories::SFW(SFW::Dance));
/// let many_images = Waifu.get_many().await.expect("Failed to fetch images");
/// for image_url in many_images {
/// println!("Fetched image URL: {}", image_url);
/// }
/// }
/// ```
async fn get_many(&self) -> Result<Vec<String>>;
/// Asynchronously fetches a random image.
///
/// This method calls the `get` method to retrieve a random image. It behaves
/// the same as `get`, returning a URL of a randomly selected image.
/// # Errors
/// Returns error if image cannot be retrieved.
///
/// # Returns
/// Returns a `Result<String>`, containing the URL of the fetched random image
/// or an error if the request fails.
///
/// # Example
/// ```rust
/// use anime_grubber::agent::Agent;
/// use anime_grubber::agents::waifu_pics::{Waifu, Categories, SFW};
///
/// #[tokio::main]
/// async fn main() {
/// let Waifu = Waifu::new(Categories::SFW(SFW::Dance));
/// let random_image_url = Waifu.get_random().await.expect("Failed to fetch random image");
/// println!("Fetched random image URL: {}", random_image_url);
/// }
/// ```
async fn get_random(&self) -> Result<String>;
/// - `Ok(Cow<'_, str>)` - URL of random image
/// - `Err(Error)` - If retrieval fails
async fn get_random(&self) -> Result<ImageUrl<'_>>;
}

pub type ImageUrl<'a> = Cow<'a, str>;
pub type ImageUrls<'a> = Box<[Cow<'a, str>]>;
123 changes: 64 additions & 59 deletions src/agents/waifu_pics.rs
Original file line number Diff line number Diff line change
@@ -1,39 +1,49 @@
use std::borrow::Cow;
use std::time::Duration;

use crate::agent::{ImageUrl, ImageUrls};
use crate::error::Error;
use crate::result::Result;
use crate::{agent::Agent, gen_enum, url};
use async_trait::async_trait;
use miniserde::{json, Deserialize, Serialize};
use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
use reqwest::StatusCode;
use tracing::{debug, info, instrument, trace};

const SOLO_URL: &str = "https://api.waifu.pics";
const MANY_URL: &str = "https://api.waifu.pics/many";

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
/// A struct representing an image-fetching agent from the [Waifu.pics API](https://waifu.pics/docs).
///
/// The `Waifu` struct implements the `Agent` trait and provides methods to retrieve
/// images based on specified categories. It supports fetching single images, multiple images,
/// and random images. The struct utilizes asynchronous operations for efficient network
/// requests.
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
const DEFAULT_POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(90);
const DEFAULT_POOL_MAX_IDLE: usize = 32;
#[derive(Debug, Clone)]
/// An image-fetching agent from the [Waifu.pics API](https://waifu.pics/docs).
///
/// # Fields
/// - `categorie`: The category of images to fetch, either SFW (Safe for Work) or NSFW (Not Safe for Work).
/// This crate provides methods to retrieve images based on specified categories.
/// It supports fetching single images, multiple images and random images.
/// The images are returned as `Cow<str>` containing URLs.
///
/// # Example
/// # Examples
/// ```rust
/// use anime_grubber::agent::Agent;
/// use anime_grubber::agents::waifu_pics::{Waifu, Categories, SFW};
/// use std::borrow::Cow;
/// async fn example() {
/// let mut waifu = Waifu::new(Categories::SFW(SFW::Dance));
///
/// // Get single image URL
/// let image_url: Cow<str> = waifu.get().await.unwrap();
///
/// // Get multiple image URLs
/// let many_urls: Box<[Cow<str>]> = waifu.get_many().await.unwrap();
///
/// async fn test() {
/// let mut Waifu = Waifu::new(Categories::SFW(SFW::Dance));
/// let image_url = Waifu.get().await.expect("Problem with intrnet");
/// let many_images = Waifu.get_many().await.expect("Problem with intrnet");
/// let random_image = Waifu.get_random().await.expect("Problem with intrnet");
/// // Get random image URL
/// let random_url: Cow<str> = waifu.get_random().await.unwrap();
/// }
/// ```
pub struct Waifu {
categorie: Categories,
pub categorie: Categories,
client: reqwest::Client,
}

impl Waifu {
Expand All @@ -54,7 +64,19 @@ impl Waifu {
/// ```
#[instrument(skip(categorie))]
pub fn new(categorie: Categories) -> Self {
Self { categorie }
let client = reqwest::Client::builder()
.timeout(DEFAULT_TIMEOUT)
.pool_idle_timeout(DEFAULT_POOL_IDLE_TIMEOUT)
.pool_max_idle_per_host(DEFAULT_POOL_MAX_IDLE)
.default_headers({
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
headers
})
.build()
.expect("Failed to create HTTP client");

Self { categorie, client }
}

/// Updates the category of the `Waifu` instance.
Expand All @@ -73,40 +95,22 @@ impl Waifu {
pub fn set_categorie(&mut self, categorie: Categories) {
self.categorie = categorie;
}

/// Generates string representations of the current category and aspect.
///
/// This method returns a tuple containing the string representations of the
/// current category and aspect.
///
/// # Returns
/// Returns a tuple `(&str, &str)` representing the category and aspect.
#[instrument(skip(self))]
fn gen_aspects(&self) -> (&str, &str) {
debug!("Custing aspects");
let category: &str = (&self.categorie).into();
let aspect: &str = match &self.categorie {
Categories::SFW(value) => value.into(),
Categories::NSFW(value) => value.into(),
};
debug!("Category -> {} \\<::>/ Aspect -> {}", &category, &aspect);
(category, aspect)
}
}

#[async_trait]
impl Agent for Waifu {
#[instrument(skip(self))]
async fn get(&self) -> Result<String> {
async fn get(&self) -> Result<ImageUrl<'_>> {
info!("Fetch data");
let (category, aspect) = self.gen_aspects();
let category: &str = (&self.categorie).into();
let aspect = self.categorie.nested_str();
let url = url!(SOLO_URL, category, aspect);

let res = reqwest::get(url).await?;
let res = self.client.get(url).send().await?;
if res.status() == reqwest::StatusCode::NOT_FOUND {
return Err(Error::NotFound);
}

trace!("Response received: status={}", res.status());
trace!("res -> {:#?}", res);
let res_text = res.text().await?;

Expand All @@ -116,34 +120,35 @@ impl Agent for Waifu {
}

#[instrument(skip(self))]
async fn get_many(&self) -> Result<Vec<String>> {
async fn get_many(&self) -> Result<ImageUrls<'_>> {
info!("Fetch many data");
let (category, aspect) = self.gen_aspects();
let category: &str = (&self.categorie).into();
let aspect = self.categorie.nested_str();
let url = url!(MANY_URL, category, aspect);

let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
let req = reqwest::Client::builder()
.default_headers(headers)
.build()?;
let res = req
let res = self
.client
.post(url)
.body(json::to_string(&Body::default()))
.send()
.await?;
if res.status() == reqwest::StatusCode::NOT_FOUND {
return Err(Error::NotFound);
if !res.status().is_success() {
return match res.status() {
StatusCode::NOT_FOUND => Err(Error::NotFound),
StatusCode::TOO_MANY_REQUESTS => Err(Error::RateLimit),
status => Err(Error::RequestFailed(status)),
};
}
trace!("Response received: status={}", res.status());
trace!("res -> {:#?}", res);
let res_text = res.text().await?;

let conveted = json::from_str::<ManyImages>(&res_text)?;

Ok(conveted.into())
}

#[instrument(skip(self))]
async fn get_random(&self) -> Result<String> {
async fn get_random(&self) -> Result<ImageUrl<'_>> {
info!("Fetch \"random\" data");
self.get().await
}
Expand All @@ -153,23 +158,23 @@ impl Agent for Waifu {
struct SoloImage {
url: String,
}
impl From<SoloImage> for String {
impl<'a> From<SoloImage> for Cow<'a, str> {
fn from(value: SoloImage) -> Self {
value.url
Cow::Owned(value.url)
}
}
#[derive(Debug, Serialize, Default)]
struct Body {
exclude: Vec<String>,
struct Body<'a> {
exclude: Box<[&'a str]>,
}

#[derive(Debug, Deserialize)]
struct ManyImages {
files: Vec<String>,
}
impl From<ManyImages> for Vec<String> {
impl<'a> From<ManyImages> for Box<[Cow<'a, str>]> {
fn from(value: ManyImages) -> Self {
value.files
value.files.into_iter().map(Cow::Owned).collect()
}
}

Expand Down
39 changes: 39 additions & 0 deletions src/builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//! # Implementing New Agents
//!
//! To implement a new agent, you need to:
//! 1. Create a new agent type
//! 2. Implement required traits
//! 3. Create a public constructor function
//!
//! ## Example Implementation
//!
//! ```no_compile
//! // 1. Define your agent type
//! pub struct MyNewAgent;
//!
//! // 2. Implement required traits
//! impl SimpleAgent for MyNewAgent {
//! fn builder(self) -> AgentBuilder<Simple> {
//! AgentBuilder::new()
//! }
//! }
//!
//! // Optionally implement TaggedAgent if your source supports tags
//! impl TaggedAgent for MyNewAgent {
//! fn tagged_builder(self) -> AgentBuilder<Tagged> {
//! AgentBuilder::new()
//! }
//! }
//!
//! // 3. Create a public constructor
//! pub fn my_new_agent() -> MyNewAgent {
//! MyNewAgent
//! }
//! ```
use crate::{Agent, Result};
use std::marker::PhantomData;

// Маркерные типы для билдера
pub struct Simple;
pub struct Tagged;
5 changes: 5 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use reqwest::StatusCode;
use thiserror::Error;
/// Represents the errors that can occur in the application.
///
Expand All @@ -15,6 +16,10 @@ use thiserror::Error;
pub enum Error {
#[error("Not found")]
NotFound,
#[error("Too many requsts")]
RateLimit,
#[error("Request failed with status: {0}")]
RequestFailed(StatusCode),
#[error("Some reqwest trouble")]
Reqwest(#[from] reqwest::Error),
#[error("Desirialise error")]
Expand Down
Loading

0 comments on commit 2024547

Please sign in to comment.