remove coupling with reqwest

This commit is contained in:
Rowan 2025-07-06 21:34:51 -04:00
parent 94be793df7
commit dde4762a05
9 changed files with 209 additions and 132 deletions

4
.gitmodules vendored
View file

@ -1,6 +1,6 @@
[submodule "crates/subresource_integrity"] [submodule "crates/subresource_integrity"]
path = 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"] [submodule "crates/maybe_owned"]
path = 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

25
Cargo.lock generated
View file

@ -988,9 +988,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.79" version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1040,6 +1040,26 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "time" name = "time"
version = "0.3.36" version = "0.3.36"
@ -1216,6 +1236,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"subresource_integrity", "subresource_integrity",
"thiserror",
"time", "time",
"tokio", "tokio",
"url", "url",

View file

@ -12,6 +12,7 @@ reqwest = "0.12.8"
serde = { version = "1.0.210", features = ["derive"] } serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.128" serde_json = "1.0.128"
subresource_integrity = { version = "0.1.0", path = "crates/subresource_integrity", features = ["serde", "md5"] } subresource_integrity = { version = "0.1.0", path = "crates/subresource_integrity", features = ["serde", "md5"] }
thiserror = "2.0.12"
time = { version = "0.3.36", features = ["serde"] } time = { version = "0.3.36", features = ["serde"] }
url = "2.5.2" url = "2.5.2"

35
src/client.rs Normal file
View file

@ -0,0 +1,35 @@
use crate::{error::Error, response::PagedResponse};
pub trait Client {
type Request;
fn get(&self, url: url::Url) -> Result<Self::Request, Error>;
fn fetch(
&self,
req: Self::Request,
) -> impl std::future::Future<Output = Result<PagedResponse, Error>> + Send;
}
impl Client for reqwest::Client {
type Request = reqwest::Request;
fn get(&self, url: url::Url) -> Result<Self::Request, Error> {
reqwest::Client::get(self, url)
.build()
.map_err(|e| Error::Request(Box::new(e)))
}
async fn fetch(&self, req: Self::Request) -> Result<PagedResponse, Error> {
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)
}
}

View file

@ -33,8 +33,9 @@ impl Display for Order {
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Stream { pub enum Stream {
Supported,
#[serde(rename = "LTS")] #[serde(rename = "LTS")]
LTS, Lts,
Beta, Beta,
Alpha, Alpha,
Tech, Tech,
@ -43,7 +44,8 @@ pub enum Stream {
impl Display for Stream { impl Display for Stream {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::LTS => write!(f, "LTS"), Self::Supported => write!(f, "SUPPORTED"),
Self::Lts => write!(f, "LTS"),
Self::Beta => write!(f, "BETA"), Self::Beta => write!(f, "BETA"),
Self::Alpha => write!(f, "ALPHA"), Self::Alpha => write!(f, "ALPHA"),
Self::Tech => write!(f, "TECH"), Self::Tech => write!(f, "TECH"),
@ -57,7 +59,7 @@ pub enum Platform {
Windows, Windows,
Linux, Linux,
#[serde(rename = "MAC_OS")] #[serde(rename = "MAC_OS")]
MacOS, MacOs,
} }
impl Display for Platform { impl Display for Platform {
@ -65,7 +67,7 @@ impl Display for Platform {
match self { match self {
Self::Windows => write!(f, "WINDOWS"), Self::Windows => write!(f, "WINDOWS"),
Self::Linux => write!(f, "LINUX"), Self::Linux => write!(f, "LINUX"),
Self::MacOS => write!(f, "MAC_OS"), Self::MacOs => write!(f, "MAC_OS"),
} }
} }
} }

21
src/error.rs Normal file
View file

@ -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<dyn core::error::Error>),
#[error("connection error: {0}")]
Response(Box<dyn core::error::Error>),
#[error("api responded with error: {0}")]
Api(APIError),
}
impl From<serde_json::Error> for Error {
fn from(value: serde_json::Error) -> Self {
Self::Parse(value)
}
}

View file

@ -1,4 +1,5 @@
#![feature(result_flattening)] pub mod client;
pub mod common; pub mod common;
pub mod error;
pub mod request; pub mod request;
pub mod response; pub mod response;

View file

@ -1,17 +1,17 @@
use std::borrow::Cow; use std::ops::DerefMut;
use maybe_owned::MaybeOwned; use maybe_owned::MaybeOwnedMut;
use url::Url; use url::Url;
use crate::{ use crate::{client::Client, common::*, error, response::PagedResponse};
common::*,
response::{self, OffsetConnection},
};
type Parameter<'a> = (&'a str, &'a str); type Parameter<'a> = (&'a str, &'a str);
pub struct RequestBuilder<'a> { const RELEASE_API_URL: &'static str = "https://services.api.unity.com/unity/editor/release/";
client: Option<&'a reqwest::Client>,
pub struct RequestBuilder<'a, T> {
client: Option<MaybeOwnedMut<'a, T>>,
url: Option<Url>,
api_version: APIVersion, api_version: APIVersion,
limit: u8, limit: u8,
offset: usize, offset: usize,
@ -22,11 +22,12 @@ pub struct RequestBuilder<'a> {
version: Option<Version>, version: Option<Version>,
} }
impl Default for RequestBuilder<'_> { impl<T: Default> Default for RequestBuilder<'_, T> {
fn default() -> Self { fn default() -> Self {
Self { Self {
limit: 10, limit: 10,
client: Default::default(), client: Default::default(),
url: Some(Self::default_api_url()),
api_version: Default::default(), api_version: Default::default(),
offset: Default::default(), offset: Default::default(),
order: Default::default(), order: Default::default(),
@ -38,11 +39,39 @@ impl Default for RequestBuilder<'_> {
} }
} }
impl<'a> RequestBuilder<'a> { impl<'a, T: 'a> RequestBuilder<'a, T> {
pub fn new(client: &'a reqwest::Client) -> Self { fn default_api_url() -> Url {
Url::parse(RELEASE_API_URL).unwrap()
}
}
impl<'a, T: 'a + Client> RequestBuilder<'a, T> {
pub fn new<I: Into<MaybeOwnedMut<'a, T>>>(client: I) -> RequestBuilder<'a, T> {
Self { Self {
client: Some(client), client: Some(client.into()),
..Default::default() 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<I: Into<MaybeOwnedMut<'a, T>>>(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<OffsetConnection, response::Error> { pub async fn send(self) -> Result<PagedResponse, error::Error> {
Into::<Request>::into(self).send().await Into::<Request<'a, T>>::into(self).send().await
} }
} }
impl<'a> From<RequestBuilder<'a>> for Request<'a> { impl<'a, T: 'a + Client> From<RequestBuilder<'a, T>> for Request<'a, T> {
fn from(val: RequestBuilder<'a>) -> Self { fn from(val: RequestBuilder<'a, T>) -> Self {
Request { Request {
client: Cow::Borrowed(val.client.unwrap()), client: val.client.unwrap(),
api_url: val.url.unwrap_or_else(RequestBuilder::<T>::default_api_url),
api_version: val.api_version.to_string(), api_version: val.api_version.to_string(),
limit: val.limit.to_string(), limit: val.limit.to_string(),
offset: val.offset.to_string(), offset: val.offset.to_string(),
order: val.order.to_string(), order: val.order.to_string(),
stream: val.stream.map_or_else(String::default, |s| s.to_string()), stream: val.stream.map_or_else(String::default, |s| s.to_string()),
platform: val platform: val.platform.map_or_else(String::default, |p| p.to_string()),
.platform
.map_or_else(String::default, |p| p.to_string()),
architecture: val architecture: val
.architecture .architecture
@ -116,8 +144,9 @@ impl<'a> From<RequestBuilder<'a>> for Request<'a> {
} }
} }
pub struct Request<'a> { pub struct Request<'a, T> {
client: Cow<'a, reqwest::Client>, api_url: Url,
client: MaybeOwnedMut<'a, T>,
api_version: String, api_version: String,
limit: String, limit: String,
offset: String, offset: String,
@ -128,10 +157,8 @@ pub struct Request<'a> {
version: String, version: String,
} }
impl Request<'_> { impl<T: Client> Request<'_, T> {
const API_URI: &'static str = "https://services.api.unity.com/unity/editor/release/"; fn as_parameters(&self) -> impl Iterator<Item = Parameter<'_>> {
fn as_parameters(&self) -> impl Iterator<Item = Parameter> {
[ [
("limit", self.limit.as_str()), ("limit", self.limit.as_str()),
("offset", self.offset.as_str()), ("offset", self.offset.as_str()),
@ -145,50 +172,45 @@ impl Request<'_> {
.filter(|p| !p.1.is_empty()) .filter(|p| !p.1.is_empty())
} }
fn as_dir(value: &str) -> String { fn get_url(&self) -> Result<Url, url::ParseError> {
format!("{value}/") let version = format!("{}/", self.api_version);
} self.api_url.join(&version)?.join("releases")
fn api_root(&self) -> Result<Url, url::ParseError> {
Url::parse(Self::API_URI)
.and_then(|url| url.join(&Self::as_dir(&self.api_version)))
.and_then(|url| url.join("releases"))
} }
fn build_uri(&self) -> Result<Url, url::ParseError> { fn build_uri(&self) -> Result<Url, url::ParseError> {
self.api_root() self.get_url()
.and_then(|url| Url::parse_with_params(url.as_str(), self.as_parameters())) .and_then(|url| Url::parse_with_params(url.as_str(), self.as_parameters()))
} }
pub async fn send(&self) -> Result<OffsetConnection, response::Error> { pub async fn send(&mut self) -> Result<PagedResponse, error::Error> {
let response = self.client.get(self.build_uri().unwrap()).send().await?; let url = self.build_uri().unwrap();
response::parse(response).await let req = self.client.get(url)?;
self.client.fetch(req).await
} }
} }
#[derive(Default)] #[derive(Default)]
pub struct UnityReleaseClient<'a> { pub struct UnityReleaseClient<'a, T> {
client: MaybeOwned<'a, reqwest::Client>, client: MaybeOwnedMut<'a, T>,
} }
impl<'a> UnityReleaseClient<'a> { impl<'a, T: 'a> UnityReleaseClient<'a, T> {
pub fn new(client: impl Into<MaybeOwned<'a, reqwest::Client>>) -> Self { pub fn new(client: impl Into<MaybeOwnedMut<'a, T>>) -> Self {
Self { Self {
client: client.into(), client: client.into(),
} }
} }
}
pub fn request(&self) -> RequestBuilder { impl<'a, T: 'a + Client> UnityReleaseClient<'a, T> {
match &self.client { pub fn request(&mut self) -> RequestBuilder<'_, T> {
MaybeOwned::Owned(x) => RequestBuilder::new(x), RequestBuilder::new(MaybeOwnedMut::Borrowed(self.client.deref_mut()))
MaybeOwned::Borrowed(x) => RequestBuilder::new(x),
}
} }
pub async fn send( pub async fn send(
&self, &self,
request: impl Into<Request<'a>>, request: impl Into<Request<'a, T>>,
) -> Result<OffsetConnection, response::Error> { ) -> Result<PagedResponse, error::Error> {
request.into().send().await request.into().send().await
} }
} }
@ -196,12 +218,13 @@ impl<'a> UnityReleaseClient<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::request::*; use crate::request::*;
use reqwest;
#[test] #[test]
fn defaut_request_uri() { 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 expected = "https://services.api.unity.com/unity/editor/release/v1/releases?limit=10&offset=0&order=RELEASE_DATE_DESC";
let client = UnityReleaseClient::default(); let mut client = UnityReleaseClient::new(reqwest::Client::default());
let request: Request = client.request().into(); let request: Request<reqwest::Client> = client.request().into();
assert_eq!(request.build_uri().unwrap().as_str(), expected); assert_eq!(request.build_uri().unwrap().as_str(), expected);
} }
@ -209,13 +232,13 @@ mod tests {
fn custom_request_uri() { 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 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 mut client = UnityReleaseClient::default();
let request: Request = client let request: Request<reqwest::Client> = client
.request() .request()
.with_order(Order::Ascending) .with_order(Order::Ascending)
.with_limit(5) .with_limit(5)
.with_offset(20) .with_offset(20)
.with_stream(Stream::LTS) .with_stream(Stream::Lts)
.with_platform(Platform::Linux) .with_platform(Platform::Linux)
.with_architecture(Architecture::Arm64) .with_architecture(Architecture::Arm64)
.with_version("2020.3.44f1".to_string()) .with_version("2020.3.44f1".to_string())
@ -228,19 +251,20 @@ mod tests {
fn borrowed_client() { fn borrowed_client() {
let expected = "https://services.api.unity.com/unity/editor/release/v1/releases?limit=10&offset=0&order=RELEASE_DATE_ASC"; 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 request_client = reqwest::Client::default();
let client = UnityReleaseClient::new(request_client); let mut client = UnityReleaseClient::new(request_client);
let request: Request = client.request().with_order(Order::Ascending).into(); let request: Request<reqwest::Client> =
client.request().with_order(Order::Ascending).into();
assert_eq!(request.build_uri().unwrap().as_str(), expected); assert_eq!(request.build_uri().unwrap().as_str(), expected);
} }
// #[tokio::test] //#[tokio::test]
// async fn test_connection() { //async fn test_connection() {
// let client = UnityReleaseClient::default(); // let mut client = UnityReleaseClient::default();
// let request = client.request().with_offset(25).with_limit(25); // let request = client.request().with_offset(25).with_limit(25);
// let req: Request = request.into(); // let mut req: Request<reqwest::Client> = request.into();
// // println!("{}", req.build_uri().unwrap()); // // println!("{}", req.build_uri().unwrap());
// let response = req.send().await; // let response = req.send().await;
// print!("{response:?}"); // print!("{response:?}");
// } //}
} }

View file

@ -1,11 +1,10 @@
use std::path::PathBuf; use std::{fmt::Display, path::PathBuf, str::FromStr};
use reqwest::Response;
use serde::Deserialize; use serde::Deserialize;
use subresource_integrity::Integrity; use subresource_integrity::Integrity;
use time::Date; use time::Date;
use crate::common::*; use crate::{common::*, error::Error};
fn handle_fractional_numbers<'de, D>(deserializer: D) -> Result<usize, D::Error> fn handle_fractional_numbers<'de, D>(deserializer: D) -> Result<usize, D::Error>
where where
@ -56,18 +55,18 @@ pub struct ThirdPartyNotice {
pub enum FileKind { pub enum FileKind {
Text, Text,
#[serde(rename = "TAR_GZ")] #[serde(rename = "TAR_GZ")]
TarGZ, TarGz,
#[serde(rename = "TAR_XZ")] #[serde(rename = "TAR_XZ")]
TarXZ, TarXz,
ZIP, Zip,
PKG, Pkg,
EXE, Exe,
PO, Po,
DMG, Dmg,
LZMA, Lzma,
LZ4, Lz4,
MD, Md,
PDF, Pdf,
} }
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]
@ -84,7 +83,7 @@ pub enum ModuleCategory {
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SKUFamily { pub enum SKUFamily {
DOTS, Dots,
Classic, Classic,
} }
@ -157,60 +156,25 @@ pub struct Release {
} }
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]
pub struct OffsetConnection { pub struct PagedResponse {
pub offset: usize, pub offset: usize,
pub limit: u8, pub limit: u8,
pub total: usize, pub total: usize,
pub results: Vec<Release>, pub results: Vec<Release>,
} }
pub async fn parse(response: Response) -> Result<OffsetConnection, Error> {
let body = response.text().await?;
println!("{body}");
serde_json::from_str::<ResponseKind>(body.as_str())
.map(Result::from)
.map_err(Error::from)
.flatten()
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum ResponseKind { pub enum APIResponse {
Ok(OffsetConnection), Ok(PagedResponse),
Err(APIError), Err(APIError),
} }
#[derive(Debug)] impl FromStr for PagedResponse {
pub enum Error { type Err = Error;
API(APIError),
Parse(serde_json::Error),
Request(reqwest::Error),
}
impl From<ResponseKind> for Result<OffsetConnection, Error> { fn from_str(s: &str) -> Result<Self, Self::Err> {
fn from(value: ResponseKind) -> Self { serde_json::from_str(s).map_err(Error::from)
match value {
ResponseKind::Ok(v) => Ok(v),
ResponseKind::Err(e) => Err(Error::from(e)),
}
}
}
impl From<APIError> for Error {
fn from(value: APIError) -> Self {
Self::API(value)
}
}
impl From<serde_json::Error> for Error {
fn from(value: serde_json::Error) -> Self {
Self::Parse(value)
}
}
impl From<reqwest::Error> for Error {
fn from(value: reqwest::Error) -> Self {
Self::Request(value)
} }
} }
@ -220,3 +184,11 @@ pub struct APIError {
pub status: u16, pub status: u16,
pub detail: String, 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 {}