use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Firmware {
pub id: u64,
pub file_name: String,
pub file_name_no_tags: String,
pub file_name_no_ext: String,
pub file_extension: String,
pub file_path: String,
pub file_size_bytes: u64,
pub full_path: String,
pub is_verified: bool,
pub crc_hash: String,
pub md5_hash: String,
pub sha1_hash: String,
pub missing_from_fs: bool,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Platform {
pub id: u64,
pub slug: String,
pub fs_slug: String,
pub rom_count: u64,
pub name: String,
pub igdb_slug: Option<String>,
pub moby_slug: Option<String>,
pub hltb_slug: Option<String>,
pub custom_name: Option<String>,
pub igdb_id: Option<i64>,
pub sgdb_id: Option<i64>,
pub moby_id: Option<i64>,
pub launchbox_id: Option<i64>,
pub ss_id: Option<i64>,
pub ra_id: Option<i64>,
pub hasheous_id: Option<i64>,
pub tgdb_id: Option<i64>,
pub flashpoint_id: Option<i64>,
pub category: Option<String>,
pub generation: Option<i64>,
pub family_name: Option<String>,
pub family_slug: Option<String>,
pub url: Option<String>,
pub url_logo: Option<String>,
pub firmware: Vec<Firmware>,
pub aspect_ratio: Option<String>,
pub created_at: String,
pub updated_at: String,
pub fs_size_bytes: u64,
pub is_unidentified: bool,
pub is_identified: bool,
pub missing_from_fs: bool,
pub display_name: Option<String>,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum RomFileCategory {
Game,
Dlc,
Hack,
Manual,
Patch,
Update,
Mod,
Demo,
Translation,
Prototype,
Cheat,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RomFile {
pub id: u64,
pub rom_id: u64,
pub file_name: String,
pub file_path: String,
pub file_size_bytes: u64,
#[serde(default)]
pub category: Option<RomFileCategory>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Rom {
pub id: u64,
pub platform_id: u64,
pub platform_slug: Option<String>,
pub platform_fs_slug: Option<String>,
pub platform_custom_name: Option<String>,
pub platform_display_name: Option<String>,
pub fs_name: String,
pub fs_name_no_tags: String,
pub fs_name_no_ext: String,
pub fs_extension: String,
pub fs_path: String,
pub fs_size_bytes: u64,
pub name: String,
pub slug: Option<String>,
pub summary: Option<String>,
pub path_cover_small: Option<String>,
pub path_cover_large: Option<String>,
pub url_cover: Option<String>,
#[serde(default)]
pub has_manual: bool,
#[serde(default)]
pub path_manual: Option<String>,
#[serde(default)]
pub url_manual: Option<String>,
pub is_unidentified: bool,
pub is_identified: bool,
#[serde(default)]
pub files: Vec<RomFile>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RomList {
pub items: Vec<Rom>,
pub total: u64,
pub limit: u64,
pub offset: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct SaveMetadata {
pub id: u64,
#[serde(default, alias = "filename", alias = "name")]
pub file_name: String,
#[serde(default)]
pub emulator: Option<String>,
#[serde(default)]
pub slot: Option<String>,
#[serde(default, alias = "updated")]
pub updated_at: Option<String>,
#[serde(default, alias = "sha256", alias = "content_hash")]
pub hash: Option<String>,
#[serde(default, alias = "file_size_bytes", alias = "size")]
pub size_bytes: Option<u64>,
#[serde(default)]
pub device_id: Option<String>,
#[serde(default)]
pub device_name: Option<String>,
}
impl SaveMetadata {
pub fn from_api_value(value: Value) -> anyhow::Result<Vec<Self>> {
let rows = value
.get("items")
.or_else(|| value.get("saves"))
.cloned()
.unwrap_or(value);
Ok(serde_json::from_value(rows)?)
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct VirtualCollectionRow {
pub id: String,
pub name: String,
#[serde(rename = "type")]
pub collection_type: String,
#[serde(default)]
pub rom_count: u64,
#[serde(default)]
pub is_virtual: bool,
}
impl From<VirtualCollectionRow> for Collection {
fn from(v: VirtualCollectionRow) -> Self {
Self {
id: 0,
name: v.name,
collection_type: Some(v.collection_type),
rom_count: Some(v.rom_count),
is_smart: false,
is_virtual: true,
virtual_id: Some(v.id),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Collection {
pub id: u64,
pub name: String,
#[serde(rename = "type")]
pub collection_type: Option<String>,
pub rom_count: Option<u64>,
#[serde(default)]
pub is_smart: bool,
#[serde(default)]
pub is_virtual: bool,
#[serde(default)]
pub virtual_id: Option<String>,
}
#[cfg(test)]
mod rom_files_serde_tests {
use super::{Rom, RomFile, RomFileCategory};
use serde_json::json;
fn minimal_rom_json() -> serde_json::Value {
json!({
"id": 1,
"platform_id": 2,
"platform_slug": null,
"platform_fs_slug": null,
"platform_custom_name": null,
"platform_display_name": null,
"fs_name": "game.nsp",
"fs_name_no_tags": "game",
"fs_name_no_ext": "game",
"fs_extension": "nsp",
"fs_path": "/game.nsp",
"fs_size_bytes": 100,
"name": "Game",
"slug": null,
"summary": null,
"path_cover_small": null,
"path_cover_large": null,
"url_cover": null,
"has_manual": false,
"path_manual": null,
"url_manual": null,
"is_unidentified": false,
"is_identified": true
})
}
#[test]
fn rom_deserializes_empty_files_when_field_missing() {
let rom: Rom = serde_json::from_value(minimal_rom_json()).expect("rom");
assert!(rom.files.is_empty());
}
#[test]
fn rom_deserializes_when_manual_fields_missing() {
let mut v = minimal_rom_json();
let obj = v.as_object_mut().expect("object");
obj.remove("has_manual");
obj.remove("path_manual");
obj.remove("url_manual");
let rom: Rom = serde_json::from_value(v).expect("rom");
assert!(!rom.has_manual);
assert_eq!(rom.path_manual, None);
assert_eq!(rom.url_manual, None);
}
#[test]
fn rom_deserializes_files_array() {
let mut v = minimal_rom_json();
v["files"] = json!([
{
"id": 10,
"rom_id": 1,
"file_name": "base.nsp",
"file_path": "/base.nsp",
"file_size_bytes": 60,
"category": "game"
},
{
"id": 11,
"rom_id": 1,
"file_name": "upd.nsp",
"file_path": "/upd.nsp",
"file_size_bytes": 40,
"category": "update"
}
]);
let rom: Rom = serde_json::from_value(v).expect("rom");
assert_eq!(rom.files.len(), 2);
assert_eq!(rom.files[0].category, Some(RomFileCategory::Game));
assert_eq!(rom.files[1].category, Some(RomFileCategory::Update));
}
#[test]
fn rom_file_category_none_deserializes() {
let f: RomFile = serde_json::from_value(json!({
"id": 1,
"rom_id": 2,
"file_name": "x.bin",
"file_path": "/x.bin",
"file_size_bytes": 1
}))
.expect("romfile");
assert_eq!(f.category, None);
}
}