api-client/packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/models.rs

239 lines
5.9 KiB
Rust

use std::collections::HashMap;
use blake3::Hash;
use chrono::{DateTime, Utc};
use ed25519_dalek::Signature;
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BundleMetadata {
pub version: String,
pub created_at: DateTime<Utc>,
#[serde(with = "signature_serde")]
pub signature: Signature,
pub manifest: Manifest,
#[serde(default)]
pub properties: HashMap<String, String>,
}
mod signature_serde {
use super::*;
use base64::{engine::general_purpose::STANDARD, Engine};
use serde::{de::Error, Deserializer, Serializer};
pub fn serialize<S>(sig: &Signature, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&STANDARD.encode(sig.to_bytes()))
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Signature, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let bytes = STANDARD.decode(&s).map_err(D::Error::custom)?;
let bytes: [u8; 64] = bytes
.try_into()
.map_err(|_| D::Error::custom("invalid signature length"))?;
Ok(Signature::from_bytes(&bytes))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileEntry {
pub path: String,
pub size: u64,
#[serde(with = "hash_serde")]
pub hash: Hash,
pub mime_type: Option<String>,
}
mod hash_serde {
use super::*;
use base64::{engine::general_purpose::STANDARD, Engine};
use blake3::Hash;
pub fn serialize<S>(sig: &Hash, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&STANDARD.encode(sig.as_bytes()))
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Hash, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let s = String::deserialize(deserializer)?;
let bytes = STANDARD.decode(&s).map_err(D::Error::custom)?;
let bytes: [u8; 32] = bytes
.try_into()
.map_err(|_| D::Error::custom("invalid signature length"))?;
Ok(Hash::from_bytes(bytes))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Manifest {
pub files: Vec<FileEntry>,
#[serde(default)]
pub version: Option<String>,
}
impl Manifest {
pub fn total_size(&self) -> u64 {
self.files.iter().map(|f| f.size).sum()
}
pub fn get_file(&self, path: &str) -> Option<&FileEntry> {
self.files.iter().find(|f| f.path == path)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ApiResponse<T> {
pub success: bool,
#[serde(default)]
pub error: Option<String>,
pub data: T,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BundleList {
pub bundles: Vec<BundleSummary>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BundleSummary {
pub name: String,
pub version: String,
pub created_at: DateTime<Utc>,
pub file_count: usize,
}
#[derive(Debug, Clone, Deserialize)]
pub struct VerificationResponse {
pub status: String,
pub created_at: DateTime<Utc>,
pub version: String,
pub file_count: usize,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PublicKeyInfo {
pub key: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DownloadOptions {
pub server_url: Url,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DownloadResponse {
pub success: bool,
pub bundle_name: String,
pub server_url: Url,
pub version: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LoadOptions {
pub bundle_name: String,
/// Optional host override for the webview URL.
///
/// When provided, the webview will be loaded with `app://{host}/` instead of
/// `app://{bundle_name}/`. This enables cloud-for-orgs support where the same
/// bundle serves multiple organization subdomains.
///
/// Example: `host: "acme.hoppscotch.io"` will:
/// - Sanitize to "acme_hoppscotch_io"
/// - Create webview at `app://acme_hoppscotch_io/`
/// - Register mapping so file requests resolve to the correct bundle
#[serde(default)]
pub host: Option<String>,
#[serde(default)]
pub inline: bool,
#[serde(default)]
pub window: WindowOptions,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LoadResponse {
pub success: bool,
pub window_label: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CloseOptions {
pub window_label: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CloseResponse {
pub success: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RemoveOptions {
pub bundle_name: String,
pub server_url: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RemoveResponse {
pub success: bool,
pub bundle_name: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WindowOptions {
#[serde(default = "default_window_title")]
pub title: String,
#[serde(default = "default_window_width")]
pub width: f64,
#[serde(default = "default_window_height")]
pub height: f64,
#[serde(default = "default_resizable")]
pub resizable: bool,
}
fn default_window_title() -> String {
"Appload".into()
}
fn default_window_width() -> f64 {
800.0
}
fn default_window_height() -> f64 {
600.0
}
fn default_resizable() -> bool {
true
}
impl Default for WindowOptions {
fn default() -> Self {
Self {
title: default_window_title(),
width: default_window_width(),
height: default_window_height(),
resizable: default_resizable(),
}
}
}