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"]
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

25
Cargo.lock generated
View file

@ -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",

View file

@ -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"

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)]
#[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"),
}
}
}

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 error;
pub mod request;
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 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<MaybeOwnedMut<'a, T>>,
url: Option<Url>,
api_version: APIVersion,
limit: u8,
offset: usize,
@ -22,11 +22,12 @@ pub struct RequestBuilder<'a> {
version: Option<Version>,
}
impl Default for RequestBuilder<'_> {
impl<T: Default> 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<I: Into<MaybeOwnedMut<'a, T>>>(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<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> {
Into::<Request>::into(self).send().await
pub async fn send(self) -> Result<PagedResponse, error::Error> {
Into::<Request<'a, T>>::into(self).send().await
}
}
impl<'a> From<RequestBuilder<'a>> for Request<'a> {
fn from(val: RequestBuilder<'a>) -> Self {
impl<'a, T: 'a + Client> From<RequestBuilder<'a, T>> 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::<T>::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<RequestBuilder<'a>> 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<Item = Parameter> {
impl<T: Client> Request<'_, T> {
fn as_parameters(&self) -> impl Iterator<Item = Parameter<'_>> {
[
("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, 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 get_url(&self) -> Result<Url, url::ParseError> {
let version = format!("{}/", self.api_version);
self.api_url.join(&version)?.join("releases")
}
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()))
}
pub async fn send(&self) -> Result<OffsetConnection, response::Error> {
let response = self.client.get(self.build_uri().unwrap()).send().await?;
response::parse(response).await
pub async fn send(&mut self) -> Result<PagedResponse, error::Error> {
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<MaybeOwned<'a, reqwest::Client>>) -> Self {
impl<'a, T: 'a> UnityReleaseClient<'a, T> {
pub fn new(client: impl Into<MaybeOwnedMut<'a, T>>) -> 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<Request<'a>>,
) -> Result<OffsetConnection, response::Error> {
request: impl Into<Request<'a, T>>,
) -> Result<PagedResponse, error::Error> {
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<reqwest::Client> = 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<reqwest::Client> = 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<reqwest::Client> =
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<reqwest::Client> = request.into();
// // println!("{}", req.build_uri().unwrap());
// let response = req.send().await;
// 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 subresource_integrity::Integrity;
use time::Date;
use crate::common::*;
use crate::{common::*, error::Error};
fn handle_fractional_numbers<'de, D>(deserializer: D) -> Result<usize, D::Error>
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<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)]
#[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<ResponseKind> for Result<OffsetConnection, Error> {
fn from(value: ResponseKind) -> Self {
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)
fn from_str(s: &str) -> Result<Self, Self::Err> {
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 {}