Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V1.4 #6

Merged
merged 5 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ readme = "README.md"
documentation = "https://docs.rs/aghpb"
repository = "https://github.com/THEGOLDENPRO/aghpb.rs"
edition = "2021"
version = "1.3.2"
version = "1.4.0"

[dependencies]
reqwest = "0.11.18"
image = "0.24.6"
serde_json = "1"
chrono = "0.4.31"
bytes = "1.5.0"
urlencoding = "2.1.3"

[dev-dependencies]
tokio = { version = "1.29.1", features = ["full"] }
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ More install instructions at [crates.io](https://crates.io/crates/aghpb).
## Examples
This is how you may retrieve a random anime girls holding programming books:
```rust
use tokio::fs;
use std::error::Error;

#[tokio::main]
Expand All @@ -47,7 +48,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
```
You can also retrieve specific categories of anime girls holding programming books like so:
```rust
let book = aghpb::random(Some("rust")).await?;
let book = aghpb::random(Some("rust".into())).await?;
```

<br>
Expand All @@ -68,4 +69,35 @@ async fn main() -> Result<(), Box<dyn Error>> {
}
```

<br>

How to search for an anime girl holding a programming book.
> [!NOTE]
> NEW in v1.4!
```rust
use std::error::Error;

use tokio::fs;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let books = aghpb::search("tohru".into(), None, None).await?;

let book_data = &books[0]; // I'm selecting the first book just for this example.

println!("Name: {}", book_data.name);
println!("Category: {}", book_data.category);
println!("Commit Author: {}", book_data.commit_author);
println!("Commit URL: {}", book_data.commit_url);
println!("Date Added: {}", book_data.date_added);
println!("Search ID: {}", book_data.search_id);

let book = book_data.get_book().await?;

fs::write("./anime_girl.png", book.raw_bytes).await?;

Ok(())
}
```

Made using my API at 👉 https://api.devgoldy.xyz/aghpb/v1/
102 changes: 100 additions & 2 deletions src/book.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,114 @@
use std::{collections::HashMap, error::Error};

use bytes::Bytes;
use chrono::{DateTime, FixedOffset};
use image::DynamicImage;
use reqwest::header::HeaderMap;

pub struct Book {
use crate::get_id;

pub struct BookData {
pub name: String,
pub category: String,
pub date_added: DateTime<FixedOffset>,
pub search_id: String,
pub commit_url: String,
pub commit_author: String,
}

impl BookData {
/// Creates a book from a json dictionary.
pub fn from_json(book_dict: HashMap<String, String>) -> BookData {
let name = book_dict.get("name").expect(
"Failed acquiring book name from json!"
).to_owned();

let category = book_dict.get("category").expect(
"Failed acquiring book category from json!"
).to_owned();

let date_added = DateTime::parse_from_str(book_dict.get("date_added").expect(
"Failed acquiring book date added header!"
), "%Y-%m-%d %H:%M:%S%z").expect(
"Failed to convert book's date added header to date time object."
);

let search_id = book_dict.get("search_id").expect(
"Failed acquiring book search id header!"
).to_owned();

let commit_url = book_dict.get("commit_url").expect(
"Failed acquiring book commit url header!"
).to_owned();

let commit_author = book_dict.get("commit_author").expect(
"Failed acquiring book commit author header!"
).to_owned();

Self {
name,
category,
date_added,
search_id,
commit_url,
commit_author
}
}

// Get's the book from the api.
pub async fn get_book(&self) -> Result<Book, Box<dyn Error>> {
get_id(String::from(&self.search_id)).await
}
}

pub struct Book {
pub details: BookData,
pub raw_bytes: Bytes,
}

impl Book {
/// Creates a book from a response's headers and bytes.
pub fn from_response(headers: HeaderMap, bytes: Bytes) -> Book {
let name = headers.get("book-name").expect(
"Failed acquiring book name header!"
).to_str().expect("Failed converting book name to string.").to_owned();

let category = headers.get("book-category").expect(
"Failed acquiring book category header!"
).to_str().expect("Failed converting book category to string.").to_owned();

let date_added = DateTime::parse_from_str(headers.get("book-date-added").expect(
"Failed acquiring book date added header!"
).to_str().expect("Failed converting book date time to string."), "%Y-%m-%d %H:%M:%S%z").expect(
"Failed to convert book's date added header to date time object."
);

let search_id = headers.get("book-search-id").expect(
"Failed acquiring book search id header!"
).to_str().expect("Failed converting book search id to string.").to_owned();

let commit_url = headers.get("book-commit-url").expect(
"Failed acquiring book commit url header!"
).to_str().expect("Failed converting book commit url to string.").to_owned();

let commit_author = headers.get("book-commit-author").expect(
"Failed acquiring book commit author header!"
).to_str().expect("Failed converting book commit author to string.").to_owned();

Self {
details: BookData {
name,
category,
date_added,
search_id,
commit_url,
commit_author
},
raw_bytes: bytes
}
}

pub fn to_image(&self) -> DynamicImage {
image::load_from_memory(&self.raw_bytes).expect("Failed to convert bytes into image.")
image::load_from_memory(&self.raw_bytes).expect("Failed to convert bytes into dynamic image object.")
}
}
111 changes: 64 additions & 47 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use std::{collections::HashMap, error::Error, fmt};

use chrono::DateTime;
use reqwest::header::HeaderMap;
use bytes::Bytes;
use reqwest::Response;
use urlencoding::encode;

use crate::book::Book;
use crate::book::{Book, BookData};

#[derive(Clone, Debug)]
pub struct Client {
Expand Down Expand Up @@ -34,71 +33,89 @@ impl Client {
}
}

/// Asynchronously grabs a random anime girl holding a programming book.
///
/// WARNING: Will panic on incorrect category.
/// Grabs a random anime girl holding a programming book.
///
/// Uses the ``/v1/random`` endpoint.
pub async fn random(&self, category: Option<&str>) -> Result<Book, Box<dyn Error>> {
let mut queries: Vec<(&str, &str)> = Vec::new();
pub async fn random(&self, category: Option<String>) -> Result<Book, Box<dyn Error>> {
let mut queries: Vec<(String, String)> = Vec::new();

if let Some(category) = category {
queries.push(("category", category));
queries.push(("category".into(), category));
}

let response = self.client.get(self.api_url.clone() + "/v1/random").query(&queries).send().await?;

if response.status().is_success() {
let headers = response.headers().to_owned();
let bytes = response.bytes().await?;

Ok(get_book(headers, bytes))
} else {
let error_json: HashMap<String, String> = serde_json::from_str(&response.text().await?).unwrap();
Err(
AGHPBError {
error: error_json.get("error").unwrap().to_string(),
message: error_json.get("message").unwrap().to_string()
}.into()
)
}
get_book_or_error(response).await

}

/// Asynchronously grabs list of available categories.
/// Grabs list of available categories.
///
/// Uses the ``/v1/categories`` endpoint.
pub async fn categories(&self) -> Result<Vec<String>, reqwest::Error> {
let mut base_url = self.api_url.clone();
let res = self.client.get(self.api_url.clone() + "/v1/categories").send().await?;
let json: Vec<String> = serde_json::from_str(&res.text().await?).expect("Failed to deserialize json response!");

Ok(json)
}

base_url.push_str("/v1/categories");
/// Allows you to search for anime girls holding programming books.
///
/// Uses the ``/v1/search`` endpoint.
pub async fn search(&self, query: String, category: Option<String>, limit: Option<u8>) -> Result<Vec<BookData>, reqwest::Error> {
let mut queries: Vec<(String, String)> = Vec::new();
queries.push(("query".into(), query));

let res = self.client.get(base_url).send().await?;
let json: Vec<String> = serde_json::from_str(&res.text().await?).expect("Failed to deserialize json response");
if let Some(category) = category {
queries.push(("category".into(), category));
}

Ok(json)
if let Some(limit) = limit {
queries.push(("limit".into(), limit.to_string()));
}

let res = self.client.get(self.api_url.clone() + "/v1/search").query(&queries).send().await?;
let json: Vec<HashMap<String, String>> = serde_json::from_str(&res.text().await?).expect("Failed to deserialize json response!");

let mut books: Vec<BookData> = Vec::new();

for book_dict in json {
books.push(
BookData::from_json(book_dict)
);
}

Ok(books)
}
}

/// Allows you to get a specific anime girls holding programming book by search ID.
///
/// Uses the ``/v1/get/id`` endpoint.
pub async fn get_id(&self, search_id: String) -> Result<Book, Box<dyn Error>> {

fn get_book(headers: HeaderMap, bytes: Bytes) -> Book {
let name = headers.get("book-name").expect("Failed acquiring book name header!").to_str().expect(
"Failed converting book name to string."
).to_owned();
let response = self.client.get(
self.api_url.clone() + "/v1/get/id/" + encode(search_id.as_str()).to_string().as_str()
).send().await?;

let category = headers.get("book-category").expect("Failed acquiring book category header!").to_str().expect(
"Failed converting book category to string.").to_owned();
get_book_or_error(response).await
}
}

let date_added = DateTime::parse_from_str(headers.get("book-date-added").expect(
"Failed acquiring book date added header!"
).to_str().expect("Failed converting book date time to string."), "%Y-%m-%d %H:%M:%S%z").expect(
"Failed to convert book's date added header to date time object."
);

Book {
name,
category,
date_added,
raw_bytes: bytes
/// Get's a book from a response or throws an API error.
async fn get_book_or_error(response: Response) -> Result<Book, Box<dyn Error>> {
if response.status().is_success() {
let headers = response.headers().to_owned();
let bytes = response.bytes().await?;

Ok(Book::from_response(headers, bytes))
} else {
let error_json: HashMap<String, String> = serde_json::from_str(&response.text().await?).unwrap();
Err(
AGHPBError {
error: error_json.get("error").unwrap().to_string(),
message: error_json.get("message").unwrap().to_string()
}.into()
)
}
}
Loading
Loading