Skip to content

Commit

Permalink
feat(get): basic impl with vanilla apis
Browse files Browse the repository at this point in the history
  • Loading branch information
LIMPIX31 committed May 28, 2024
1 parent 0669fff commit 4f27920
Show file tree
Hide file tree
Showing 8 changed files with 398 additions and 7 deletions.
31 changes: 24 additions & 7 deletions crates/spuz_get/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,32 @@ edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
readme.workspace = true
description = "Pack of apis to get any versions of the game, even modded, such as fabric, quilt, forge, etc"
readme = "readme.md"
keywords = ["minecraft", "launcher", "downlaoder", "client"]
categories = []

[dependencies]
arc-swap = { version = "1" }
reqwest = { version = "0", features = ["json"] }
serde = { workspace = true }
serde_json = { workspace = true }
spuz_piston = { workspace = true }

[lints]
workspace = true
thiserror = { version = "1" }
reqwest = { version = "0.12", features = ["json", "stream"], optional = true }
url = { version = "2" }
async-trait = { version = "0.1" }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1" }
tokio = { version = "1", features = ["fs"] }
futures-lite = { version = "2" }
futures-util = { version = "0.3", features = ["io"], optional = true }
async-compat = { version = "0.2" }

[dev-dependencies]
pollster = { version = "0.3" }

[features]
default = ["reqwest", "vanilla"]
reqwest = ["dep:reqwest", "dep:futures-util"]
vanilla = []

[lints]
workspace = true
26 changes: 26 additions & 0 deletions crates/spuz_get/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# spuz_get <sub><sub>*by [coppebars](https://github.com/coppebars)*<sub/><sub/>
All you need to install minecraft

# Example
```rust
use reqwest::Client;
use spuz_get::{vanilla::package, Error};
use spuz_piston::Manifest;

#[tokio::main]
async fn main() -> Result<(), Error> {
let client = Client::default();

let package =
package::<Manifest, _>(&client, "d585c8e981e58326237746ca1253dea15c9e4aaa", "24w21b")
.await
.map_err(Error::from_fetch)?
.json()
.await?;

println!("{package:#?}");

Ok(())
}

```
96 changes: 96 additions & 0 deletions crates/spuz_get/src/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use async_trait::async_trait;
use futures_lite::AsyncRead;
use serde::de::DeserializeOwned;
use thiserror::Error;
use url::Url;

pub(crate) type BoxedAsyncRead = Box<dyn AsyncRead + Unpin + Send + Sync + 'static>;

#[async_trait]
pub trait Client {
type Error: std::error::Error + 'static;

/// Sends a GET request and deserializes the result as json
/// # Example
/// ```no_run
/// # use std::error::Error;
/// use spuz_get::Client;
/// use spuz_piston::Manifest;
/// # type Result<T> = std::result::Result<T, Box<dyn Error>>;
///
/// async fn get_manifest<C: Client>(client: &C) -> Result<Manifest>
/// where
/// <C as Client>::Error: Error + 'static
/// {
/// let json = client.get_json("https://piston-meta.mojang.com/v1/packages/d585c8e981e58326237746ca1253dea15c9e4aaa/24w21b.json".parse()?).await?;
/// Ok(json)
/// }
/// ```
async fn get_json<T>(&self, url: Url) -> Result<T, Self::Error>
where
T: DeserializeOwned;

/// Sends a GET request and returns [AsyncRead] stream
///
/// # Example
/// ```no_run
/// # use std::error::Error;
/// use async_compat::CompatExt;
/// use futures_lite::io;
/// use tokio::fs::File;
/// use spuz_get::Client;
/// use spuz_piston::Manifest;
/// # type Result<T> = std::result::Result<T, Box<dyn Error>>;
///
/// async fn download_jar<C: Client>(client: &C) -> Result<()>
/// where
/// <C as Client>::Error: Error + 'static
/// {
/// let mut stream = client.get_stream("https://piston-data.mojang.com/v1/objects/9d5b45173a0123720bae94afc8a35d742e559d5a/client.jar".parse()?).await?;
/// let mut file = File::open("./client.jar").await?;
/// io::copy(&mut stream, &mut file.compat_mut()).await?;
/// Ok(())
/// }
/// ```
async fn get_stream(&self, url: Url) -> Result<BoxedAsyncRead, Self::Error>;
}

#[cfg(feature = "reqwest")]
#[async_trait]
impl Client for reqwest::Client {
type Error = reqwest::Error;

#[inline]
async fn get_json<T>(&self, url: Url) -> Result<T, Self::Error>
where
T: DeserializeOwned,
{
self.get(url).send().await?.json().await
}

#[inline]
async fn get_stream(&self, url: Url) -> Result<BoxedAsyncRead, Self::Error> {
use futures_util::TryStreamExt;

#[inline]
fn map_err(err: reqwest::Error) -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::Other, err)
}

let bytes_stream = self.get(url).send().await?.bytes_stream().map_err(map_err);

Ok(Box::new(bytes_stream.into_async_read()))
}
}

#[derive(Debug, Error)]
pub enum FetchError<C: Client> {
#[error("Fetch failed: {0}")]
Client(#[source] C::Error),
#[error("Invalid url: {0}")]
ParseUrl(
#[from]
#[source]
url::ParseError,
),
}
29 changes: 29 additions & 0 deletions crates/spuz_get/src/err.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use std::fmt::Debug;

use thiserror::Error;

use crate::{
ext::{FsExtLoadError, FsExtSaveError},
json_resource::{JsonResourceParseError, JsonResourceSaveError},
Client, FetchError,
};

#[derive(Debug, Error)]
pub enum Error {
#[error("FetchError: {0}")]
Fetch(#[source] Box<dyn std::error::Error>),
#[error(transparent)]
JsonResourceSave(#[from] JsonResourceSaveError),
#[error(transparent)]
JsonResourceParse(#[from] JsonResourceParseError),
#[error(transparent)]
FsExtSave(#[from] FsExtSaveError),
#[error(transparent)]
FsExtLoda(#[from] FsExtLoadError),
}

impl Error {
pub fn from_fetch<C: Client + Debug + 'static>(err: FetchError<C>) -> Self {
Self::Fetch(Box::new(err))
}
}
86 changes: 86 additions & 0 deletions crates/spuz_get/src/ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use std::{io, path::Path};

use async_trait::async_trait;
use spuz_piston::{AssetIndex, Manifest, RuntimeManifest};
use thiserror::Error;
use tokio::{fs, io::AsyncWriteExt};

#[derive(Debug, Error)]
#[error("{0}")]
pub enum FsExtLoadError {
ReadFile(
#[from]
#[source]
io::Error,
),
Deserialize(
#[from]
#[source]
serde_json::Error,
),
}

#[derive(Debug, Error)]
#[error("{0}")]
pub enum FsExtSaveError {
CreateFile(#[source] io::Error),
Copy(#[source] io::Error),
}

/// Allows you to [save](Self::save) or [load](Self::load) documents from the
/// file system
#[async_trait]
pub trait FsExt: Sized {
/// Saves the document locally
/// # Example
/// ```no_run
/// # use std::error::Error;
/// # use pollster::FutureExt;
/// use spuz_piston::Manifest;
/// use spuz_get::FsExt;
///
/// # async move {
/// let manifest = Manifest::load("./1.20.6.json").await?;
/// manifest.save("./1.20.6-new.json").await?;
/// # Result::<(), Box<dyn Error>>::Ok(())
/// # }.block_on();
/// ```
async fn save(&self, path: impl AsRef<Path> + Send) -> Result<(), FsExtSaveError>;
/// Loads the document from the fs
/// # Example
/// ```no_run
/// # use std::error::Error;
/// # use pollster::FutureExt;
/// use spuz_piston::Manifest;
/// use spuz_get::FsExt;
///
/// # async move {
/// let manifest = Manifest::load("./1.20.6.json").await?;
/// manifest.save("./1.20.6-new.json").await?;
/// # Result::<(), Box<dyn Error>>::Ok(())
/// # }.block_on();
/// ```
async fn load(path: impl AsRef<Path> + Send) -> Result<Self, FsExtLoadError>;
}

macro_rules! save_ext {
($what:ident) => {
#[async_trait]
impl FsExt for $what {
async fn save(&self, path: impl AsRef<Path> + Send) -> Result<(), FsExtSaveError> {
let mut file = fs::File::create(path).await.map_err(FsExtSaveError::CreateFile)?;
file.write_all(self.to_string().as_bytes()).await.map_err(FsExtSaveError::Copy)?;
Ok(())
}

async fn load(path: impl AsRef<Path> + Send) -> Result<Self, FsExtLoadError> {
let content = fs::read_to_string(path).await?;
content.parse().map_err(Into::into)
}
}
};
}

save_ext!(Manifest);
save_ext!(AssetIndex);
save_ext!(RuntimeManifest);
85 changes: 85 additions & 0 deletions crates/spuz_get/src/json_resource.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use std::{
fmt::{Debug, Formatter},
marker::PhantomData,
path::Path,
};

use async_compat::CompatExt;
use futures_lite::{io, AsyncRead};
use futures_util::AsyncReadExt;
use serde::de::DeserializeOwned;
use thiserror::Error;
use tokio::fs::File;

/// You can get `JsonResource` from some api calls. If you just need to get the
/// structure from the json response, use the [json](JsonResource::json) method.
/// Sometimes you may need to save the result to a file, then use the
/// [save](JsonResource::save) method to avoid unnecessary parsing.
///
/// It is recommended that you perform one of the actions immediately, as
/// prolonged inactivity may result in a timeout.
///
/// # Example
/// ```no_run
/// # use pollster::FutureExt;///
/// # async move {
/// let json_resource = todo!();
///
/// let result = json_resource.json().await?;
/// // Or
/// json_resource.save("./local.json").await?;
/// # Result::<(), spuz_get::Error>::Ok(())
/// # }.block_on()
/// ```
pub struct JsonResource<R, D> {
pub(crate) stream: R,
pub(crate) json: PhantomData<D>,
}

impl<R, D> Debug for JsonResource<R, D> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
"JsonResource { ... }".fmt(f)
}
}

impl<R, D> JsonResource<R, D>
where
R: AsyncRead + Unpin,
D: DeserializeOwned,
{
/// Reads the underlying stream to end and parses as [D]
pub async fn json(&mut self) -> Result<D, JsonResourceParseError> {
let mut content = String::new();
self.stream.read_to_string(&mut content).await?;
Ok(serde_json::from_str(&content)?)
}

/// Copies the underlying stream to the file
pub async fn save(&mut self, path: impl AsRef<Path>) -> Result<(), JsonResourceSaveError> {
let mut file = File::create(path).await.map_err(JsonResourceSaveError::CreateFile)?;
io::copy(&mut self.stream, file.compat_mut()).await.map_err(JsonResourceSaveError::Copy)?;
Ok(())
}
}

#[derive(Debug, Error)]
#[error("JsonResource parse error: {0}")]
pub enum JsonResourceParseError {
Read(
#[source]
#[from]
io::Error,
),
Parse(
#[source]
#[from]
serde_json::Error,
),
}

#[derive(Debug, Error)]
#[error("JsonResource save error: {0}")]
pub enum JsonResourceSaveError {
CreateFile(#[source] io::Error),
Copy(#[source] io::Error),
}
12 changes: 12 additions & 0 deletions crates/spuz_get/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
pub mod client;
mod err;
pub mod ext;
pub mod json_resource;
#[cfg(feature = "vanilla")]
pub mod vanilla;

pub use crate::{
client::{Client, FetchError},
err::Error,
ext::FsExt,
json_resource::JsonResource,
};
Loading

0 comments on commit 4f27920

Please sign in to comment.