diff --git a/.gitmodules b/.gitmodules index ab9c5ba..5124523 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "crates/subresource_integrity"] path = crates/subresource_integrity - url = https://fem.mint.lgbt/kitsunecafe/subresource-integrity.git + url = https://forgejo.archandle.net/rowan/subresource-integrity.git [submodule "crates/maybe_owned"] path = crates/maybe_owned - url = https://fem.mint.lgbt/kitsunecafe/maybe-owned.git + url = https://forgejo.archandle.net/rowan/maybe-owned.git diff --git a/Cargo.lock b/Cargo.lock index f5ef937..bed3d0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -988,9 +988,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.79" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -1040,6 +1040,26 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.3.36" @@ -1216,6 +1236,7 @@ dependencies = [ "serde", "serde_json", "subresource_integrity", + "thiserror", "time", "tokio", "url", diff --git a/Cargo.toml b/Cargo.toml index c93fae0..66d9093 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ reqwest = "0.12.8" serde = { version = "1.0.210", features = ["derive"] } serde_json = "1.0.128" subresource_integrity = { version = "0.1.0", path = "crates/subresource_integrity", features = ["serde", "md5"] } +thiserror = "2.0.12" time = { version = "0.3.36", features = ["serde"] } url = "2.5.2" diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..f50bb44 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,35 @@ +use crate::{error::Error, response::PagedResponse}; + +pub trait Client { + type Request; + + fn get(&self, url: url::Url) -> Result; + fn fetch( + &self, + req: Self::Request, + ) -> impl std::future::Future> + Send; +} + +impl Client for reqwest::Client { + type Request = reqwest::Request; + + fn get(&self, url: url::Url) -> Result { + reqwest::Client::get(self, url) + .build() + .map_err(|e| Error::Request(Box::new(e))) + } + + async fn fetch(&self, req: Self::Request) -> Result { + let response = self + .execute(req) + .await + .map_err(|e| Error::Response(Box::new(e)))?; + + let response = response + .text() + .await + .map_err(|e| Error::Response(Box::new(e)))?; + + str::parse(&response) + } +} diff --git a/src/common.rs b/src/common.rs index 231d6d3..1eaf74e 100644 --- a/src/common.rs +++ b/src/common.rs @@ -33,8 +33,9 @@ impl Display for Order { #[derive(Debug, serde::Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum Stream { + Supported, #[serde(rename = "LTS")] - LTS, + Lts, Beta, Alpha, Tech, @@ -43,7 +44,8 @@ pub enum Stream { impl Display for Stream { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::LTS => write!(f, "LTS"), + Self::Supported => write!(f, "SUPPORTED"), + Self::Lts => write!(f, "LTS"), Self::Beta => write!(f, "BETA"), Self::Alpha => write!(f, "ALPHA"), Self::Tech => write!(f, "TECH"), @@ -57,7 +59,7 @@ pub enum Platform { Windows, Linux, #[serde(rename = "MAC_OS")] - MacOS, + MacOs, } impl Display for Platform { @@ -65,7 +67,7 @@ impl Display for Platform { match self { Self::Windows => write!(f, "WINDOWS"), Self::Linux => write!(f, "LINUX"), - Self::MacOS => write!(f, "MAC_OS"), + Self::MacOs => write!(f, "MAC_OS"), } } } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..28b656e --- /dev/null +++ b/src/error.rs @@ -0,0 +1,21 @@ +use thiserror::Error; + +use crate::response::APIError; + +#[derive(Error, Debug)] +pub enum Error { + #[error("error parsing result: {0}")] + Parse(serde_json::Error), + #[error("invalid request: {0}")] + Request(Box), + #[error("connection error: {0}")] + Response(Box), + #[error("api responded with error: {0}")] + Api(APIError), +} + +impl From for Error { + fn from(value: serde_json::Error) -> Self { + Self::Parse(value) + } +} diff --git a/src/lib.rs b/src/lib.rs index af9e13b..50a74bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ -#![feature(result_flattening)] +pub mod client; pub mod common; +pub mod error; pub mod request; pub mod response; diff --git a/src/request.rs b/src/request.rs index ecb531a..0354f13 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,17 +1,17 @@ -use std::borrow::Cow; +use std::ops::DerefMut; -use maybe_owned::MaybeOwned; +use maybe_owned::MaybeOwnedMut; use url::Url; -use crate::{ - common::*, - response::{self, OffsetConnection}, -}; +use crate::{client::Client, common::*, error, response::PagedResponse}; type Parameter<'a> = (&'a str, &'a str); -pub struct RequestBuilder<'a> { - client: Option<&'a reqwest::Client>, +const RELEASE_API_URL: &'static str = "https://services.api.unity.com/unity/editor/release/"; + +pub struct RequestBuilder<'a, T> { + client: Option>, + url: Option, api_version: APIVersion, limit: u8, offset: usize, @@ -22,11 +22,12 @@ pub struct RequestBuilder<'a> { version: Option, } -impl Default for RequestBuilder<'_> { +impl Default for RequestBuilder<'_, T> { fn default() -> Self { Self { limit: 10, client: Default::default(), + url: Some(Self::default_api_url()), api_version: Default::default(), offset: Default::default(), order: Default::default(), @@ -38,11 +39,39 @@ impl Default for RequestBuilder<'_> { } } -impl<'a> RequestBuilder<'a> { - pub fn new(client: &'a reqwest::Client) -> Self { +impl<'a, T: 'a> RequestBuilder<'a, T> { + fn default_api_url() -> Url { + Url::parse(RELEASE_API_URL).unwrap() + } +} + +impl<'a, T: 'a + Client> RequestBuilder<'a, T> { + pub fn new>>(client: I) -> RequestBuilder<'a, T> { Self { - client: Some(client), - ..Default::default() + client: Some(client.into()), + url: Some(Self::default_api_url()), + limit: 10, + api_version: Default::default(), + offset: Default::default(), + order: Default::default(), + stream: Default::default(), + platform: Default::default(), + architecture: Default::default(), + version: Default::default(), + } + } + + pub fn with_client>>(self, client: I) -> RequestBuilder<'a, T> { + Self { + client: Some(client.into()), + ..self + } + } + + pub fn with_url(self, url: Url) -> Self { + Self { + url: Some(url), + ..self } } @@ -86,24 +115,23 @@ impl<'a> RequestBuilder<'a> { } } - pub async fn send(self) -> Result { - Into::::into(self).send().await + pub async fn send(self) -> Result { + Into::>::into(self).send().await } } -impl<'a> From> for Request<'a> { - fn from(val: RequestBuilder<'a>) -> Self { +impl<'a, T: 'a + Client> From> for Request<'a, T> { + fn from(val: RequestBuilder<'a, T>) -> Self { Request { - client: Cow::Borrowed(val.client.unwrap()), + client: val.client.unwrap(), + api_url: val.url.unwrap_or_else(RequestBuilder::::default_api_url), api_version: val.api_version.to_string(), limit: val.limit.to_string(), offset: val.offset.to_string(), order: val.order.to_string(), stream: val.stream.map_or_else(String::default, |s| s.to_string()), - platform: val - .platform - .map_or_else(String::default, |p| p.to_string()), + platform: val.platform.map_or_else(String::default, |p| p.to_string()), architecture: val .architecture @@ -116,8 +144,9 @@ impl<'a> From> for Request<'a> { } } -pub struct Request<'a> { - client: Cow<'a, reqwest::Client>, +pub struct Request<'a, T> { + api_url: Url, + client: MaybeOwnedMut<'a, T>, api_version: String, limit: String, offset: String, @@ -128,10 +157,8 @@ pub struct Request<'a> { version: String, } -impl Request<'_> { - const API_URI: &'static str = "https://services.api.unity.com/unity/editor/release/"; - - fn as_parameters(&self) -> impl Iterator { +impl Request<'_, T> { + fn as_parameters(&self) -> impl Iterator> { [ ("limit", self.limit.as_str()), ("offset", self.offset.as_str()), @@ -145,50 +172,45 @@ impl Request<'_> { .filter(|p| !p.1.is_empty()) } - fn as_dir(value: &str) -> String { - format!("{value}/") - } - - fn api_root(&self) -> Result { - Url::parse(Self::API_URI) - .and_then(|url| url.join(&Self::as_dir(&self.api_version))) - .and_then(|url| url.join("releases")) + fn get_url(&self) -> Result { + let version = format!("{}/", self.api_version); + self.api_url.join(&version)?.join("releases") } fn build_uri(&self) -> Result { - self.api_root() + self.get_url() .and_then(|url| Url::parse_with_params(url.as_str(), self.as_parameters())) } - pub async fn send(&self) -> Result { - let response = self.client.get(self.build_uri().unwrap()).send().await?; - response::parse(response).await + pub async fn send(&mut self) -> Result { + let url = self.build_uri().unwrap(); + let req = self.client.get(url)?; + self.client.fetch(req).await } } #[derive(Default)] -pub struct UnityReleaseClient<'a> { - client: MaybeOwned<'a, reqwest::Client>, +pub struct UnityReleaseClient<'a, T> { + client: MaybeOwnedMut<'a, T>, } -impl<'a> UnityReleaseClient<'a> { - pub fn new(client: impl Into>) -> Self { +impl<'a, T: 'a> UnityReleaseClient<'a, T> { + pub fn new(client: impl Into>) -> Self { Self { client: client.into(), } } +} - pub fn request(&self) -> RequestBuilder { - match &self.client { - MaybeOwned::Owned(x) => RequestBuilder::new(x), - MaybeOwned::Borrowed(x) => RequestBuilder::new(x), - } +impl<'a, T: 'a + Client> UnityReleaseClient<'a, T> { + pub fn request(&mut self) -> RequestBuilder<'_, T> { + RequestBuilder::new(MaybeOwnedMut::Borrowed(self.client.deref_mut())) } pub async fn send( &self, - request: impl Into>, - ) -> Result { + request: impl Into>, + ) -> Result { request.into().send().await } } @@ -196,12 +218,13 @@ impl<'a> UnityReleaseClient<'a> { #[cfg(test)] mod tests { use crate::request::*; + use reqwest; #[test] fn defaut_request_uri() { let expected = "https://services.api.unity.com/unity/editor/release/v1/releases?limit=10&offset=0&order=RELEASE_DATE_DESC"; - let client = UnityReleaseClient::default(); - let request: Request = client.request().into(); + let mut client = UnityReleaseClient::new(reqwest::Client::default()); + let request: Request = client.request().into(); assert_eq!(request.build_uri().unwrap().as_str(), expected); } @@ -209,13 +232,13 @@ mod tests { fn custom_request_uri() { let expected = "https://services.api.unity.com/unity/editor/release/v1/releases?limit=5&offset=20&order=RELEASE_DATE_ASC&stream=LTS&platform=LINUX&architecture=ARM64&version=2020.3.44f1"; - let client = UnityReleaseClient::default(); - let request: Request = client + let mut client = UnityReleaseClient::default(); + let request: Request = client .request() .with_order(Order::Ascending) .with_limit(5) .with_offset(20) - .with_stream(Stream::LTS) + .with_stream(Stream::Lts) .with_platform(Platform::Linux) .with_architecture(Architecture::Arm64) .with_version("2020.3.44f1".to_string()) @@ -228,19 +251,20 @@ mod tests { fn borrowed_client() { let expected = "https://services.api.unity.com/unity/editor/release/v1/releases?limit=10&offset=0&order=RELEASE_DATE_ASC"; let request_client = reqwest::Client::default(); - let client = UnityReleaseClient::new(request_client); - let request: Request = client.request().with_order(Order::Ascending).into(); + let mut client = UnityReleaseClient::new(request_client); + let request: Request = + client.request().with_order(Order::Ascending).into(); assert_eq!(request.build_uri().unwrap().as_str(), expected); } - // #[tokio::test] - // async fn test_connection() { - // let client = UnityReleaseClient::default(); - // let request = client.request().with_offset(25).with_limit(25); - // let req: Request = request.into(); - // // println!("{}", req.build_uri().unwrap()); - // let response = req.send().await; - // print!("{response:?}"); - // } + //#[tokio::test] + //async fn test_connection() { + // let mut client = UnityReleaseClient::default(); + // let request = client.request().with_offset(25).with_limit(25); + // let mut req: Request = request.into(); + // // println!("{}", req.build_uri().unwrap()); + // let response = req.send().await; + // print!("{response:?}"); + //} } diff --git a/src/response.rs b/src/response.rs index 675956e..9894326 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,11 +1,10 @@ -use std::path::PathBuf; +use std::{fmt::Display, path::PathBuf, str::FromStr}; -use reqwest::Response; use serde::Deserialize; use subresource_integrity::Integrity; use time::Date; -use crate::common::*; +use crate::{common::*, error::Error}; fn handle_fractional_numbers<'de, D>(deserializer: D) -> Result where @@ -56,18 +55,18 @@ pub struct ThirdPartyNotice { pub enum FileKind { Text, #[serde(rename = "TAR_GZ")] - TarGZ, + TarGz, #[serde(rename = "TAR_XZ")] - TarXZ, - ZIP, - PKG, - EXE, - PO, - DMG, - LZMA, - LZ4, - MD, - PDF, + TarXz, + Zip, + Pkg, + Exe, + Po, + Dmg, + Lzma, + Lz4, + Md, + Pdf, } #[derive(Debug, serde::Deserialize)] @@ -84,7 +83,7 @@ pub enum ModuleCategory { #[derive(Debug, serde::Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum SKUFamily { - DOTS, + Dots, Classic, } @@ -157,60 +156,25 @@ pub struct Release { } #[derive(Debug, serde::Deserialize)] -pub struct OffsetConnection { +pub struct PagedResponse { pub offset: usize, pub limit: u8, pub total: usize, pub results: Vec, } -pub async fn parse(response: Response) -> Result { - let body = response.text().await?; - println!("{body}"); - serde_json::from_str::(body.as_str()) - .map(Result::from) - .map_err(Error::from) - .flatten() -} - #[derive(Debug, Deserialize)] #[serde(untagged)] -pub enum ResponseKind { - Ok(OffsetConnection), +pub enum APIResponse { + Ok(PagedResponse), Err(APIError), } -#[derive(Debug)] -pub enum Error { - API(APIError), - Parse(serde_json::Error), - Request(reqwest::Error), -} +impl FromStr for PagedResponse { + type Err = Error; -impl From for Result { - fn from(value: ResponseKind) -> Self { - match value { - ResponseKind::Ok(v) => Ok(v), - ResponseKind::Err(e) => Err(Error::from(e)), - } - } -} - -impl From for Error { - fn from(value: APIError) -> Self { - Self::API(value) - } -} - -impl From for Error { - fn from(value: serde_json::Error) -> Self { - Self::Parse(value) - } -} - -impl From for Error { - fn from(value: reqwest::Error) -> Self { - Self::Request(value) + fn from_str(s: &str) -> Result { + serde_json::from_str(s).map_err(Error::from) } } @@ -220,3 +184,11 @@ pub struct APIError { pub status: u16, pub detail: String, } + +impl Display for APIError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} ({}): {}", self.title, self.status, self.detail) + } +} + +impl core::error::Error for APIError {}