moon's haunted

This commit is contained in:
Rowan 2024-10-06 22:37:52 -05:00
commit dc4b909c5a
12 changed files with 2054 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
target/

6
.gitmodules vendored Normal file
View file

@ -0,0 +1,6 @@
[submodule "crates/subresource_integrity"]
path = crates/subresource_integrity
url = https://fem.mint.lgbt/kitsunecafe/subresource-integrity.git
[submodule "crates/maybe_owned"]
path = crates/maybe_owned
url = https://fem.mint.lgbt/kitsunecafe/maybe-owned.git

1461
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

19
Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "unity_release_api"
version = "0.1.0"
edition = "2021"
[dependencies]
derive = "1.0.0"
features = "0.10.0"
futures = "0.3.30"
maybe_owned = { version = "0.1.0", path = "crates/maybe_owned" }
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"] }
time = { version = "0.3.36", features = ["serde"] }
url = "2.5.2"
[dev-dependencies]
tokio = { version = "1.40.0", features = ["rt", "macros"] }

1
crates/maybe_owned Submodule

@ -0,0 +1 @@
Subproject commit 7c9bd9c81af7f5badcde8a88f710c453db9e413b

@ -0,0 +1 @@
Subproject commit 05f62a89aa7058616b104a2b5fbabb63fff023c2

2
rust-toolchain.toml Normal file
View file

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

90
src/common.rs Normal file
View file

@ -0,0 +1,90 @@
use std::fmt::Display;
#[derive(Default)]
pub enum APIVersion {
#[default]
V1,
}
impl Display for APIVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::V1 => write!(f, "v1"),
}
}
}
#[derive(Default, Debug, serde::Deserialize)]
pub enum Order {
Ascending,
#[default]
Descending,
}
impl Display for Order {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Ascending => write!(f, "RELEASE_DATE_ASC"),
Self::Descending => write!(f, "RELEASE_DATE_DESC"),
}
}
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Stream {
#[serde(rename = "LTS")]
LTS,
Beta,
Alpha,
Tech,
}
impl Display for Stream {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::LTS => write!(f, "LTS"),
Self::Beta => write!(f, "BETA"),
Self::Alpha => write!(f, "ALPHA"),
Self::Tech => write!(f, "TECH"),
}
}
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Platform {
Windows,
Linux,
#[serde(rename = "MAC_OS")]
MacOS,
}
impl Display for Platform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Windows => write!(f, "WINDOWS"),
Self::Linux => write!(f, "LINUX"),
Self::MacOS => write!(f, "MAC_OS"),
}
}
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Architecture {
X86_64,
Arm64,
}
impl Display for Architecture {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Arm64 => write!(f, "ARM64"),
Self::X86_64 => write!(f, "X86_64"),
}
}
}
#[derive(Debug, serde::Deserialize)]
pub struct Version(pub String);

4
src/lib.rs Normal file
View file

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

246
src/request.rs Normal file
View file

@ -0,0 +1,246 @@
use std::borrow::Cow;
use maybe_owned::MaybeOwned;
use url::Url;
use crate::{
common::*,
response::{self, OffsetConnection},
};
type Parameter<'a> = (&'a str, &'a str);
pub struct RequestBuilder<'a> {
client: Option<&'a reqwest::Client>,
api_version: APIVersion,
limit: u8,
offset: usize,
order: Order,
stream: Option<Stream>,
platform: Option<Platform>,
architecture: Option<Architecture>,
version: Option<Version>,
}
impl Default for RequestBuilder<'_> {
fn default() -> Self {
Self {
limit: 10,
client: Default::default(),
api_version: Default::default(),
offset: Default::default(),
order: Default::default(),
stream: Default::default(),
platform: Default::default(),
architecture: Default::default(),
version: Default::default(),
}
}
}
impl<'a> RequestBuilder<'a> {
pub fn new(client: &'a reqwest::Client) -> Self {
Self {
client: Some(client),
..Default::default()
}
}
pub fn with_limit(self, limit: u8) -> Self {
Self { limit, ..self }
}
pub fn with_offset(self, offset: usize) -> Self {
Self { offset, ..self }
}
pub fn with_order(self, order: Order) -> Self {
Self { order, ..self }
}
pub fn with_stream(self, stream: Stream) -> Self {
Self {
stream: Some(stream),
..self
}
}
pub fn with_platform(self, platform: Platform) -> Self {
Self {
platform: Some(platform),
..self
}
}
pub fn with_architecture(self, architecture: Architecture) -> Self {
Self {
architecture: Some(architecture),
..self
}
}
pub fn with_version(self, version: String) -> Self {
Self {
version: Some(Version(version)),
..self
}
}
pub async fn send(self) -> Result<OffsetConnection, response::Error> {
Into::<Request>::into(self).send().await
}
}
impl<'a> From<RequestBuilder<'a>> for Request<'a> {
fn from(val: RequestBuilder<'a>) -> Self {
Request {
client: Cow::Borrowed(val.client.unwrap()),
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()),
architecture: val
.architecture
.map_or_else(String::default, |a| a.to_string()),
version: val
.version
.map_or_else(String::default, |v| v.0.to_string()),
}
}
}
pub struct Request<'a> {
client: Cow<'a, reqwest::Client>,
api_version: String,
limit: String,
offset: String,
order: String,
stream: String,
platform: String,
architecture: String,
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> {
[
("limit", self.limit.as_str()),
("offset", self.offset.as_str()),
("order", self.order.as_str()),
("stream", self.stream.as_str()),
("platform", self.platform.as_str()),
("architecture", self.architecture.as_str()),
("version", self.version.as_str()),
]
.into_iter()
.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 build_uri(&self) -> Result<Url, url::ParseError> {
self.api_root()
.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
}
}
#[derive(Default)]
pub struct UnityReleaseClient<'a> {
client: MaybeOwned<'a, reqwest::Client>,
}
impl<'a> UnityReleaseClient<'a> {
pub fn new(client: impl Into<MaybeOwned<'a, reqwest::Client>>) -> 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),
}
}
pub async fn send(
&self,
request: impl Into<Request<'a>>,
) -> Result<OffsetConnection, response::Error> {
request.into().send().await
}
}
#[cfg(test)]
mod tests {
use crate::request::*;
#[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();
assert_eq!(request.build_uri().unwrap().as_str(), expected);
}
#[test]
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
.request()
.with_order(Order::Ascending)
.with_limit(5)
.with_offset(20)
.with_stream(Stream::LTS)
.with_platform(Platform::Linux)
.with_architecture(Architecture::Arm64)
.with_version("2020.3.44f1".to_string())
.into();
assert_eq!(request.build_uri().unwrap().as_str(), expected);
}
#[test]
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();
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:?}");
// }
}

222
src/response.rs Normal file
View file

@ -0,0 +1,222 @@
use std::path::PathBuf;
use reqwest::Response;
use serde::Deserialize;
use subresource_integrity::Integrity;
use time::Date;
use crate::common::*;
fn handle_fractional_numbers<'de, D>(deserializer: D) -> Result<usize, D::Error>
where
D: serde::Deserializer<'de>,
{
let value: f64 = Deserialize::deserialize(deserializer)?;
let value = value.ceil();
Ok(value as usize)
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum UnitKind {
Byte,
Kilobyte,
Megabyte,
Gigabyte,
}
#[derive(Debug, serde::Deserialize)]
pub struct DigitalValue {
#[serde(deserialize_with = "handle_fractional_numbers")]
pub value: usize,
pub unit: UnitKind,
}
#[derive(Debug, serde::Deserialize)]
pub struct ReleaseNotes {
pub url: PathBuf,
pub integrity: Option<Integrity>,
#[serde(rename = "type")]
pub kind: FileKind,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThirdPartyNotice {
#[serde(rename = "originalFileName")]
pub original_filename: String,
pub url: PathBuf,
pub integrity: Option<Integrity>,
#[serde(rename = "type")]
pub kind: FileKind,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum FileKind {
Text,
#[serde(rename = "TAR_GZ")]
TarGZ,
#[serde(rename = "TAR_XZ")]
TarXZ,
ZIP,
PKG,
EXE,
PO,
DMG,
LZMA,
LZ4,
MD,
PDF,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ModuleCategory {
Documentation,
Platform,
LanguagePack,
DevTool,
Plugin,
Component,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SKUFamily {
DOTS,
Classic,
}
#[derive(Debug, serde::Deserialize)]
pub struct ExtractedPathRename {
pub from: PathBuf,
pub to: PathBuf,
}
#[derive(Debug, serde::Deserialize)]
pub struct Eula {
pub url: PathBuf,
pub integrity: Option<Integrity>,
#[serde(rename = "type")]
pub kind: FileKind,
pub label: String,
pub message: String,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Module {
pub id: String,
pub slug: String,
pub name: String,
pub description: String,
pub category: ModuleCategory,
pub url: PathBuf,
#[serde(rename = "type")]
pub kind: FileKind,
pub download_size: DigitalValue,
pub installed_size: DigitalValue,
pub sub_modules: Vec<Module>,
pub required: bool,
pub hidden: bool,
pub extracted_path_rename: Option<ExtractedPathRename>,
#[serde(rename = "preSelected")]
pub preselected: bool,
pub destination: Option<String>,
pub eula: Option<Vec<Eula>>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Download {
pub url: PathBuf,
pub integrity: Option<Integrity>,
#[serde(rename = "type")]
pub kind: FileKind,
pub platform: Platform,
pub architecture: Architecture,
pub download_size: DigitalValue,
pub installed_size: DigitalValue,
pub modules: Vec<Module>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Release {
pub version: Version,
pub date: Option<Date>,
pub notes: Option<ReleaseNotes>,
pub stream: Stream,
pub downloads: Vec<Download>,
pub sku_family: SKUFamily,
pub recommended: bool,
pub unity_hub_link: Option<String>,
pub short_revision: String,
pub third_party_notices: Vec<ThirdPartyNotice>,
}
#[derive(Debug, serde::Deserialize)]
pub struct OffsetConnection {
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),
Err(APIError),
}
#[derive(Debug)]
pub enum Error {
API(APIError),
Parse(serde_json::Error),
Request(reqwest::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)
}
}
#[derive(Debug, serde::Deserialize)]
pub struct APIError {
pub title: String,
pub status: u16,
pub detail: String,
}

0
src/utils.rs Normal file
View file