use crate::model::file::{ExtractedPathRename, FileType};
use crate::model::platform::{
UnityReleaseDownloadArchitecture, UnityReleaseDownloadPlatform,
UnityReleaseSkuFamily, UnityReleaseStream,
};
use crate::Size;
use derive_getters::Getters;
use serde::{Deserialize, Deserializer, Serialize};
use ssri::Integrity;
use std::str::FromStr;
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Release {
pub version: String,
pub product_name: String,
pub release_date: String,
pub release_notes: ReleaseNotes,
pub stream: UnityReleaseStream,
pub downloads: Vec<Download>,
pub sku_family: UnityReleaseSkuFamily,
pub recommended: bool,
pub unity_hub_deep_link: String,
pub short_revision: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub third_party_notices: Vec<UnityThirdPartyNotice>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Download {
#[serde(flatten)]
pub release_file: UnityReleaseFile,
pub platform: UnityReleaseDownloadPlatform,
pub architecture: UnityReleaseDownloadArchitecture,
pub modules: Vec<Module>,
pub download_size: Size,
pub installed_size: Size,
}
impl Download {
pub fn iter_modules(&self) -> impl Iterator<Item = &Module> {
ModuleIterator::new(&self.modules)
}
}
#[derive(Debug, Deserialize, Serialize, Clone, Getters)]
#[serde(rename_all = "camelCase")]
pub struct Module {
#[serde(rename = "__typename")]
#[getter(skip)]
type_name: Option<String>,
#[serde(flatten)]
release_file: UnityReleaseFile,
download_url: Option<String>,
id: String,
#[getter(skip)]
slug: Option<String>,
#[getter(skip)]
name: String,
description: String,
category: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
sub_modules: Vec<Module>,
#[getter(skip)]
#[serde(default)]
required: bool,
#[serde(default)]
hidden: bool,
#[serde(default)]
pre_selected: bool,
#[getter(skip)]
destination: Option<String>,
extracted_path_rename: Option<ExtractedPathRename>,
#[getter(skip)]
pub download_size: Size,
#[getter(skip)]
pub installed_size: Size,
#[serde(default, deserialize_with = "null_to_empty_vec")]
eula: Vec<Eula>,
}
impl Module {
pub fn destination(&self) -> Option<String> {
if &self.id == "ios" {
self.destination
.as_ref()
.map(|d| format!("{}/iOSSupport", d.to_string()).to_string())
} else {
self.destination.clone()
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Eula {
#[serde(flatten)]
pub release_file: UnityReleaseFile,
pub label: String,
pub message: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UnityThirdPartyNotice {
#[serde(flatten)]
release_file: UnityReleaseFile,
original_file_name: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UnityReleaseFile {
#[serde(default)]
pub url: String,
#[serde(default, deserialize_with = "deserialize_sri")]
pub integrity: Option<Integrity>,
#[serde(rename = "type")]
pub file_type: Option<FileType>,
}
pub type ReleaseNotes = UnityReleaseFile;
fn deserialize_sri<'de, D>(deserializer: D) -> Result<Option<Integrity>, D::Error>
where
D: Deserializer<'de>,
{
let sri_str: Option<String> = Option::deserialize(deserializer)?;
match sri_str {
Some(s) if s.is_empty() => Ok(None),
Some(s) => match Integrity::from_str(&s) {
Ok(integrity) => Ok(Some(integrity)),
Err(_) => Ok(None), },
None => Ok(None),
}
}
fn null_to_empty_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}
pub struct ModuleIterator<'a> {
stack: Vec<&'a Module>,
}
impl<'a> ModuleIterator<'a> {
pub fn new(modules: &'a [Module]) -> Self {
Self {
stack: modules.iter().rev().collect(),
}
}
}
impl<'a> Iterator for ModuleIterator<'a> {
type Item = &'a Module;
fn next(&mut self) -> Option<Self::Item> {
let next = self.stack.pop()?;
self.stack.extend(next.sub_modules().iter().rev());
Some(next)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn deserialize_integrity(json: &str) -> Option<Integrity> {
let file: UnityReleaseFile = serde_json::from_str(json).unwrap();
file.integrity
}
#[test]
fn empty_string_integrity_deserializes_to_none() {
let result = deserialize_integrity(r#"{"integrity": ""}"#);
assert!(result.is_none());
}
#[test]
fn null_integrity_deserializes_to_none() {
let result = deserialize_integrity(r#"{"integrity": null}"#);
assert!(result.is_none());
}
#[test]
fn missing_integrity_field_deserializes_to_none() {
let result = deserialize_integrity(r#"{}"#);
assert!(result.is_none());
}
#[test]
fn valid_integrity_deserializes_to_some() {
let result = deserialize_integrity(
r#"{"integrity": "sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="}"#,
);
assert!(result.is_some());
}
}