moon's haunted
This commit is contained in:
commit
dc4b909c5a
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
target/
|
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal 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
1461
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
19
Cargo.toml
Normal file
19
Cargo.toml
Normal 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
1
crates/maybe_owned
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 7c9bd9c81af7f5badcde8a88f710c453db9e413b
|
1
crates/subresource_integrity
Submodule
1
crates/subresource_integrity
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 05f62a89aa7058616b104a2b5fbabb63fff023c2
|
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "nightly"
|
90
src/common.rs
Normal file
90
src/common.rs
Normal 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
4
src/lib.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
#![feature(result_flattening)]
|
||||
pub mod common;
|
||||
pub mod request;
|
||||
pub mod response;
|
246
src/request.rs
Normal file
246
src/request.rs
Normal 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
222
src/response.rs
Normal 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
0
src/utils.rs
Normal file
Loading…
Reference in a new issue